#!/usr/bin/env bash
# =============================================================================
# onx-zip-extract — Extract a zip / tar(.gz|.bz2|.xz) / 7z archive under home
#
# Purpose:
#   File Manager "Extract" backend. Zip-slip guard on every entry: we refuse
#   to extract any member whose canonicalised destination escapes the chosen
#   `dest` directory. Optional password support for zip.
#
# Input (stdin JSON):
#   {
#     "username":   "onx_xxxx",
#     "archive":    "backups/snapshot.zip",
#     "dest":       "restore",
#     "password":   null,                  -- optional; zip-only
#     "overwrite":  false                  -- optional; default false
#   }
#
# Output (stdout JSON):
#   {
#     "archive":          "/home/onx_xxxx/backups/snapshot.zip",
#     "dest":             "/home/onx_xxxx/restore",
#     "extracted_count":  42,
#     "format":           "zip",
#     "skipped_unsafe":   0
#   }
#
# Exit codes: 0=ok 1=invalid-input 2=preflight-fail 3=execution-fail
# Deployed to: /usr/local/onoxsoft/bin/onx-zip-extract
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"

require_cmd jq

onx_json_input

USERNAME="$(onx_json_field username)"
ARCHIVE_REL="$(onx_json_field archive)"
DEST_REL="$(onx_json_field dest)"
PASSWORD="$(onx_json_field password "")"
OVERWRITE="$(onx_json_get_bool "$INPUT" overwrite false)"

onx_validate_username "$USERNAME"
[[ -z "${ARCHIVE_REL}" ]] && onx_die 1 "archive is required"
[[ -z "${DEST_REL}" ]]    && onx_die 1 "dest is required"

HOME_DIR="/home/${USERNAME}"
[[ -d "$HOME_DIR" ]] || onx_die 2 "home directory missing: ${HOME_DIR}"

ARCHIVE_ABS="$(realpath -e "${HOME_DIR}/${ARCHIVE_REL#/}" 2>/dev/null)" \
    || onx_die 1 "archive not found: ${ARCHIVE_REL}"
case "${ARCHIVE_ABS}" in
    "${HOME_DIR}"|"${HOME_DIR}"/*) ;;
    *) onx_die 1 "archive escapes /home/${USERNAME}: ${ARCHIVE_REL}" ;;
esac

DEST_INPUT="${HOME_DIR}/${DEST_REL#/}"
DEST_ABS="$(realpath -m "${DEST_INPUT}" 2>/dev/null || printf '%s' "${DEST_INPUT}")"
case "${DEST_ABS}" in
    "${HOME_DIR}"|"${HOME_DIR}"/*) ;;
    *) onx_die 1 "dest escapes /home/${USERNAME}: ${DEST_REL}" ;;
esac

mkdir -p "${DEST_ABS}" || onx_die 3 "could not create dest: ${DEST_REL}"

# ── Detect format ───────────────────────────────────────────────────────────
FN="$(basename "${ARCHIVE_ABS}")"
FORMAT="unknown"
case "${FN,,}" in
    *.zip)       FORMAT="zip" ;;
    *.tar.gz|*.tgz)  FORMAT="tar.gz" ;;
    *.tar.bz2|*.tbz2) FORMAT="tar.bz2" ;;
    *.tar.xz|*.txz)  FORMAT="tar.xz" ;;
    *.tar)       FORMAT="tar" ;;
    *.7z)        FORMAT="7z" ;;
    *)           onx_die 1 "unrecognised archive format: ${FN}" ;;
esac

# ── Zip-slip safety: pre-flight scan of all entries ────────────────────────
SKIPPED_UNSAFE=0

_is_unsafe_entry() {
    local entry="$1"
    # Reject absolute paths and anything containing ../ after resolution.
    [[ "${entry}" == /* ]] && return 0
    case "${entry}" in
        *../*|../*|*/..) return 0 ;;
    esac
    # Canonicalise against DEST_ABS and ensure it stays inside.
    local canon
    canon="$(realpath -m "${DEST_ABS}/${entry}" 2>/dev/null || true)"
    [[ -z "${canon}" ]] && return 0
    case "${canon}" in
        "${DEST_ABS}"|"${DEST_ABS}"/*) return 1 ;;
        *) return 0 ;;
    esac
}

# Build entry list per-format.
ENTRIES_TMP="$(mktemp -t onx-zip-entries.XXXXXX)"
trap '[[ -f "${ENTRIES_TMP}" ]] && rm -f "${ENTRIES_TMP}"' EXIT

case "${FORMAT}" in
    zip)
        require_cmd unzip
        unzip -Z1 "${ARCHIVE_ABS}" 2>/dev/null > "${ENTRIES_TMP}" || true
        ;;
    tar.gz)
        require_cmd tar
        tar -tzf "${ARCHIVE_ABS}" 2>/dev/null > "${ENTRIES_TMP}" || true
        ;;
    tar.bz2)
        require_cmd tar
        tar -tjf "${ARCHIVE_ABS}" 2>/dev/null > "${ENTRIES_TMP}" || true
        ;;
    tar.xz)
        require_cmd tar
        tar -tJf "${ARCHIVE_ABS}" 2>/dev/null > "${ENTRIES_TMP}" || true
        ;;
    tar)
        require_cmd tar
        tar -tf "${ARCHIVE_ABS}" 2>/dev/null > "${ENTRIES_TMP}" || true
        ;;
    7z)
        require_cmd 7z
        # 7z l output is messy; parse the "Path = " lines.
        7z l -slt "${ARCHIVE_ABS}" 2>/dev/null | awk -F' = ' '/^Path = /{print $2}' | tail -n +2 > "${ENTRIES_TMP}" || true
        ;;
esac

while IFS= read -r entry; do
    [[ -z "${entry}" ]] && continue
    if _is_unsafe_entry "${entry}"; then
        SKIPPED_UNSAFE=$((SKIPPED_UNSAFE + 1))
    fi
done < "${ENTRIES_TMP}"

if (( SKIPPED_UNSAFE > 0 )); then
    onx_die 1 "archive contains ${SKIPPED_UNSAFE} unsafe entries (zip-slip) — refusing"
fi

# ── Extract ─────────────────────────────────────────────────────────────────
EXTRACTED_COUNT=0

case "${FORMAT}" in
    zip)
        ZIP_FLAGS=( -q -d "${DEST_ABS}" )
        [[ "${OVERWRITE}" == "true" ]] && ZIP_FLAGS+=( -o ) || ZIP_FLAGS+=( -n )
        [[ -n "${PASSWORD}" ]] && ZIP_FLAGS+=( -P "${PASSWORD}" )
        unzip "${ZIP_FLAGS[@]}" "${ARCHIVE_ABS}" 2>/dev/null \
            || onx_die 3 "unzip failed"
        EXTRACTED_COUNT="$(wc -l < "${ENTRIES_TMP}" | awk '{print $1}')"
        ;;
    tar.gz)
        TAR_FLAGS=( -xzf "${ARCHIVE_ABS}" -C "${DEST_ABS}" )
        [[ "${OVERWRITE}" == "true" ]] || TAR_FLAGS+=( --keep-old-files )
        tar "${TAR_FLAGS[@]}" 2>/dev/null || onx_die 3 "tar.gz extract failed"
        EXTRACTED_COUNT="$(wc -l < "${ENTRIES_TMP}" | awk '{print $1}')"
        ;;
    tar.bz2)
        TAR_FLAGS=( -xjf "${ARCHIVE_ABS}" -C "${DEST_ABS}" )
        [[ "${OVERWRITE}" == "true" ]] || TAR_FLAGS+=( --keep-old-files )
        tar "${TAR_FLAGS[@]}" 2>/dev/null || onx_die 3 "tar.bz2 extract failed"
        EXTRACTED_COUNT="$(wc -l < "${ENTRIES_TMP}" | awk '{print $1}')"
        ;;
    tar.xz)
        TAR_FLAGS=( -xJf "${ARCHIVE_ABS}" -C "${DEST_ABS}" )
        [[ "${OVERWRITE}" == "true" ]] || TAR_FLAGS+=( --keep-old-files )
        tar "${TAR_FLAGS[@]}" 2>/dev/null || onx_die 3 "tar.xz extract failed"
        EXTRACTED_COUNT="$(wc -l < "${ENTRIES_TMP}" | awk '{print $1}')"
        ;;
    tar)
        TAR_FLAGS=( -xf "${ARCHIVE_ABS}" -C "${DEST_ABS}" )
        [[ "${OVERWRITE}" == "true" ]] || TAR_FLAGS+=( --keep-old-files )
        tar "${TAR_FLAGS[@]}" 2>/dev/null || onx_die 3 "tar extract failed"
        EXTRACTED_COUNT="$(wc -l < "${ENTRIES_TMP}" | awk '{print $1}')"
        ;;
    7z)
        SEVEN_FLAGS=( x -y -o"${DEST_ABS}" )
        [[ -n "${PASSWORD}" ]] && SEVEN_FLAGS+=( "-p${PASSWORD}" )
        7z "${SEVEN_FLAGS[@]}" "${ARCHIVE_ABS}" >/dev/null 2>&1 \
            || onx_die 3 "7z extract failed"
        EXTRACTED_COUNT="$(wc -l < "${ENTRIES_TMP}" | awk '{print $1}')"
        ;;
esac

# chown extracted tree to home owner.
HOME_UID="$(stat -c '%u' "${HOME_DIR}" 2>/dev/null || echo "")"
HOME_GID="$(stat -c '%g' "${HOME_DIR}" 2>/dev/null || echo "")"
if [[ -n "${HOME_UID}" && -n "${HOME_GID}" ]]; then
    chown -R "${HOME_UID}:${HOME_GID}" "${DEST_ABS}" 2>/dev/null || true
fi

onx_log "zip-extract: user=${USERNAME} archive='${ARCHIVE_REL}' dest='${DEST_REL}' format=${FORMAT} count=${EXTRACTED_COUNT}"

jq -nc \
    --arg archive "${ARCHIVE_ABS}" \
    --arg dest "${DEST_ABS}" \
    --argjson extracted_count "${EXTRACTED_COUNT:-0}" \
    --arg format "${FORMAT}" \
    --argjson skipped_unsafe "${SKIPPED_UNSAFE}" \
    '{
        archive: $archive,
        dest: $dest,
        extracted_count: $extracted_count,
        format: $format,
        skipped_unsafe: $skipped_unsafe
     }'
