#!/usr/bin/env bash
# =============================================================================
# onx-backup-restore — Restore an account backup produced by onx-backup-run
#
# Purpose:
#   Verifies the archive checksum, extracts to a sandbox, reads the manifest,
#   then rsyncs home, imports SQL dumps, restores Maildir, and inserts missing
#   DNS records — all gated by the caller-supplied restore_options flags.
#
# Input (stdin JSON):
#   {
#     "account_id":  1,
#     "username":    "onx_acme01",
#     "backup_path": "/var/backups/onoxsoft/onx_acme01-20260514.tar.gz",
#     "restore_options": {
#       "home": true,
#       "dbs":  true,
#       "mail": true,
#       "dns":  true
#     }
#   }
#
# Output (stdout JSON):
#   {
#     "restored": true,
#     "items": {
#       "home":  N_files,
#       "dbs":   N_rows_estimated,
#       "mail":  N_messages,
#       "dns":   N_records
#     },
#     "duration_seconds": N
#   }
#
# Exit codes: 0=ok 1=invalid 2=preflight 3=exec 4=rolled-back 5=rollback-fail
#
# Deployed to: /usr/local/onoxsoft/bin/onx-backup-restore
# =============================================================================

set -euo pipefail

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

readonly BACKUP_ROOT_ALLOWED="/var/backups"
readonly VMAIL_ROOT="/var/vmail"

require_root

# ── Dependencies ─────────────────────────────────────────────────────────────
if [[ "${MOCK_MODE}" != "1" ]]; then
    command -v tar       >/dev/null 2>&1 || onx_die 2 "tar not found"
    command -v rsync     >/dev/null 2>&1 || onx_die 2 "rsync not found"
    command -v sha256sum >/dev/null 2>&1 || onx_die 2 "sha256sum not found"
    command -v mysql     >/dev/null 2>&1 || onx_die 2 "mysql not found"
fi

onx_json_input

ACCOUNT_ID=$(onx_json_field "account_id" "0")
USERNAME=$(onx_json_field   "username")
BACKUP_PATH=$(onx_json_field "backup_path")
OPTS_JSON=$(printf '%s' "${INPUT}" | jq -c '.restore_options // {}')

RESTORE_HOME=$(onx_json_get_bool "${OPTS_JSON}" "home" "true")
RESTORE_DBS=$(onx_json_get_bool  "${OPTS_JSON}" "dbs"  "true")
RESTORE_MAIL=$(onx_json_get_bool "${OPTS_JSON}" "mail" "true")
RESTORE_DNS=$(onx_json_get_bool  "${OPTS_JSON}" "dns"  "true")

# ── Validation ───────────────────────────────────────────────────────────────
[[ "${ACCOUNT_ID}" =~ ^[0-9]+$ ]] && [[ "${ACCOUNT_ID}" -gt 0 ]] \
    || onx_die 1 "account_id must be positive integer"
onx_validate_username "${USERNAME}"
[[ -n "${BACKUP_PATH}" ]] || onx_die 1 "backup_path is required"

# Validate backup_path is under /var/backups/
BACKUP_REAL="$(realpath -m "${BACKUP_PATH}" 2>/dev/null || printf '%s' "${BACKUP_PATH}")"
case "${BACKUP_REAL}" in
    ${BACKUP_ROOT_ALLOWED}|${BACKUP_ROOT_ALLOWED}/*) : ;;
    *) onx_die 1 "backup_path '${BACKUP_PATH}' must be under ${BACKUP_ROOT_ALLOWED}/" ;;
esac

[[ -f "${BACKUP_REAL}" ]] || onx_die 2 "backup file not found: ${BACKUP_REAL}"

# ── Verify checksum ──────────────────────────────────────────────────────────
SHA_FILE="${BACKUP_REAL}.sha256"
if [[ -f "${SHA_FILE}" ]]; then
    # Validate the recorded checksum matches the file
    EXPECTED_SHA=$(awk '{print $1}' "${SHA_FILE}")
    ACTUAL_SHA=$(sha256sum "${BACKUP_REAL}" | awk '{print $1}')
    if [[ "${EXPECTED_SHA}" != "${ACTUAL_SHA}" ]]; then
        onx_die 2 "checksum mismatch: expected=${EXPECTED_SHA} actual=${ACTUAL_SHA}"
    fi
    onx_log "checksum verified: ${ACTUAL_SHA}"
else
    onx_log "WARNING: no .sha256 sidecar for ${BACKUP_REAL} — restoring without verification"
fi

# ── Working directory ────────────────────────────────────────────────────────
WORK_DIR="/tmp/onx-restore-${USERNAME}-$$"
mkdir -p "${WORK_DIR}" || onx_die 3 "cannot create work dir: ${WORK_DIR}"
chmod 0700 "${WORK_DIR}"

trap 'onx_rollback_run' ERR
onx_rollback_register "rm -rf '${WORK_DIR}' 2>/dev/null || true"

START_TS=$(date +%s)
onx_log "backup-restore start: account=${ACCOUNT_ID} user=${USERNAME} src=${BACKUP_REAL}"

# ── Extract archive ──────────────────────────────────────────────────────────
if [[ "${MOCK_MODE}" == "1" ]]; then
    # Mock: synthesise a manifest + home.tar.gz so subsequent steps don't crash
    printf 'MOCK home.tar.gz\n' > "${WORK_DIR}/home.tar.gz"
    jq -n \
        --arg user "${USERNAME}" \
        --argjson aid "${ACCOUNT_ID}" \
        '{manifest_version:"1.0",username:$user,account_id:$aid,included:{home:true,mail:true,dns:true,databases:["mockdb1"]}}' \
        > "${WORK_DIR}/manifest.json"
    printf '{"domains":[],"records":[]}\n' > "${WORK_DIR}/dns.json"
    printf '{"forwarders":[],"aliases":[],"autoresponders":[]}\n' > "${WORK_DIR}/mail-meta.json"
    mkdir -p "${WORK_DIR}/databases"
    printf -- '-- mock dump\n' > "${WORK_DIR}/databases/mockdb1.sql"
else
    tar -xzf "${BACKUP_REAL}" -C "${WORK_DIR}" 2>/dev/null \
        || onx_die 3 "extract failed: ${BACKUP_REAL}"
fi

MANIFEST_FILE="${WORK_DIR}/manifest.json"
[[ -f "${MANIFEST_FILE}" ]] || onx_die 3 "manifest.json missing in archive"

MANIFEST_VER=$(jq -r '.manifest_version // ""' "${MANIFEST_FILE}")
MANIFEST_USER=$(jq -r '.username // ""' "${MANIFEST_FILE}")
MANIFEST_HOME=$(jq -r '.home // ""' "${MANIFEST_FILE}")

[[ -z "${MANIFEST_VER}" ]] && onx_die 3 "manifest is missing manifest_version"

# Sanity: username in manifest must match target (or be empty for snapshot use)
if [[ -n "${MANIFEST_USER}" && "${MANIFEST_USER}" != "${USERNAME}" ]]; then
    onx_log "WARNING: manifest username='${MANIFEST_USER}' != target='${USERNAME}' — proceeding"
fi

# ── Stats accumulators ───────────────────────────────────────────────────────
RESTORED_HOME=0
RESTORED_DBS=0
RESTORED_MAIL=0
RESTORED_DNS=0

# ── 1. Home directory restore ────────────────────────────────────────────────
TARGET_HOME="/home/${USERNAME}"

if [[ "${RESTORE_HOME}" == "true" && -f "${WORK_DIR}/home.tar.gz" ]]; then
    HOME_EXTRACT="${WORK_DIR}/home-extract"
    mkdir -p "${HOME_EXTRACT}"

    if [[ "${MOCK_MODE}" == "1" ]]; then
        RESTORED_HOME=$((RANDOM % 500 + 100))
    else
        tar -xzf "${WORK_DIR}/home.tar.gz" -C "${HOME_EXTRACT}" 2>/dev/null \
            || onx_die 3 "home.tar.gz extract failed"

        # Locate the extracted home dir (it carries the original basename)
        EXTRACTED_HOME="${HOME_EXTRACT}/$(basename "${MANIFEST_HOME:-${USERNAME}}")"
        if [[ ! -d "${EXTRACTED_HOME}" ]]; then
            EXTRACTED_HOME=$(find "${HOME_EXTRACT}" -mindepth 1 -maxdepth 1 -type d | head -n 1)
        fi
        [[ -d "${EXTRACTED_HOME}" ]] || onx_die 3 "could not locate extracted home dir"

        mkdir -p "${TARGET_HOME}"
        # rsync with -a preserves perms, ownership, timestamps; trailing slash
        # so contents (not the dir itself) land in TARGET_HOME.
        RSYNC_OUT=$(rsync -a --stats "${EXTRACTED_HOME}/" "${TARGET_HOME}/" 2>&1) \
            || onx_die 3 "rsync home failed"
        RESTORED_HOME=$(echo "${RSYNC_OUT}" | awk -F': ' '/Number of regular files transferred/{print $2}' | tr -d ',')
        RESTORED_HOME="${RESTORED_HOME:-0}"

        # Re-apply ownership (rsync -a preserved it, but the user might not exist
        # locally with the original uid — best-effort chown if id known)
        if id "${USERNAME}" >/dev/null 2>&1; then
            chown -R "${USERNAME}:${USERNAME}" "${TARGET_HOME}" 2>/dev/null || true
        fi
    fi
    onx_log "home restored: ${RESTORED_HOME} files into ${TARGET_HOME}"
fi

# ── 2. MySQL databases restore ───────────────────────────────────────────────
if [[ "${RESTORE_DBS}" == "true" && -d "${WORK_DIR}/databases" ]]; then
    DB_FILES=("${WORK_DIR}"/databases/*.sql)
    if [[ -e "${DB_FILES[0]}" ]]; then
        for DUMP in "${DB_FILES[@]}"; do
            [[ -f "${DUMP}" ]] || continue
            DB_NAME="$(basename "${DUMP}" .sql)"
            # Defence: only restore DBs that match our user prefix
            [[ "${DB_NAME}" =~ ^${USERNAME}_[a-z0-9_]+$ ]] || {
                onx_log "skipping db dump with mismatched prefix: ${DB_NAME}"
                continue
            }

            if [[ "${MOCK_MODE}" == "1" ]]; then
                RESTORED_DBS=$(( RESTORED_DBS + (RANDOM % 1000 + 50) ))
                continue
            fi

            # Drop & recreate before import so we have a clean slate
            mysql_exec "" "CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\`;" \
                || onx_die 3 "CREATE DATABASE ${DB_NAME} failed"

            # Pipe dump into mysql — rough row count is the number of INSERT lines
            INSERT_LINES=$(grep -c '^INSERT ' "${DUMP}" 2>/dev/null || echo 0)
            mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" --batch "${DB_NAME}" < "${DUMP}" \
                || onx_die 3 "mysql import failed for ${DB_NAME}"
            RESTORED_DBS=$(( RESTORED_DBS + INSERT_LINES ))
            onx_log "db restored: ${DB_NAME} (~${INSERT_LINES} inserts)"
        done
    fi
fi

# ── 3. Mail restore (Maildir) ────────────────────────────────────────────────
if [[ "${RESTORE_MAIL}" == "true" ]]; then
    # The home.tar.gz already contains Maildir under home/Maildir if include_mail
    # was true at backup time. We additionally support a sibling vmail/ directory
    # for accounts whose Maildir was virtual (Postfix+Dovecot SQL backend).
    if [[ "${MOCK_MODE}" == "1" ]]; then
        RESTORED_MAIL=$((RANDOM % 1000 + 100))
    else
        # Count messages restored under TARGET_HOME/Maildir (cur+new+tmp)
        if [[ -d "${TARGET_HOME}/Maildir" ]]; then
            RESTORED_MAIL=$(find "${TARGET_HOME}/Maildir" \( -path '*/cur/*' -o -path '*/new/*' \) -type f 2>/dev/null | wc -l)
        fi

        # Optional vmail tree restore from extracted backup (older format)
        if [[ -d "${WORK_DIR}/vmail" ]]; then
            for DOMAIN_DIR in "${WORK_DIR}"/vmail/*/; do
                [[ -d "${DOMAIN_DIR}" ]] || continue
                DOMAIN_NAME="$(basename "${DOMAIN_DIR}")"
                onx_validate_domain "${DOMAIN_NAME}" >/dev/null 2>&1 || continue
                rsync -a "${DOMAIN_DIR}" "${VMAIL_ROOT}/${DOMAIN_NAME}/" 2>/dev/null || true
                MSG_COUNT=$(find "${VMAIL_ROOT}/${DOMAIN_NAME}" \( -path '*/cur/*' -o -path '*/new/*' \) -type f 2>/dev/null | wc -l)
                RESTORED_MAIL=$(( RESTORED_MAIL + MSG_COUNT ))
            done
        fi
    fi
    onx_log "mail restored: ${RESTORED_MAIL} messages"
fi

# ── 4. DNS restore ───────────────────────────────────────────────────────────
if [[ "${RESTORE_DNS}" == "true" && -f "${WORK_DIR}/dns.json" ]]; then
    if [[ "${MOCK_MODE}" == "1" ]]; then
        RESTORED_DNS=$((RANDOM % 50 + 5))
    else
        # Insert only records that don't already exist (by domain+name+type+content).
        # We use the PowerDNS schema; some columns are nullable so we coalesce.
        RECORDS=$(jq -c '.records[]?' "${WORK_DIR}/dns.json" 2>/dev/null || true)
        while IFS= read -r REC; do
            [[ -z "${REC}" ]] && continue
            DOMAIN_ID=$(echo "${REC}" | jq -r '.domain_id // ""')
            REC_NAME=$(echo "${REC}" | jq -r '.name // ""')
            REC_TYPE=$(echo "${REC}" | jq -r '.type // ""')
            REC_CONTENT=$(echo "${REC}" | jq -r '.content // ""')
            REC_TTL=$(echo "${REC}" | jq -r '.ttl // 3600')
            REC_PRIO=$(echo "${REC}" | jq -r '.prio // 0')

            [[ -z "${DOMAIN_ID}" || -z "${REC_NAME}" ]] && continue

            # Escape single quotes (defensive — content can contain them)
            REC_NAME_ESC="${REC_NAME//\'/\\\'}"
            REC_TYPE_ESC="${REC_TYPE//\'/\\\'}"
            REC_CONTENT_ESC="${REC_CONTENT//\'/\\\'}"

            mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" --batch "${ONX_PDNS_DB}" -e \
                "INSERT IGNORE INTO records (domain_id, name, type, content, ttl, prio, disabled) \
                 VALUES (${DOMAIN_ID}, '${REC_NAME_ESC}', '${REC_TYPE_ESC}', '${REC_CONTENT_ESC}', ${REC_TTL}, ${REC_PRIO}, 0);" \
                2>/dev/null && RESTORED_DNS=$(( RESTORED_DNS + 1 )) || true
        done <<< "${RECORDS}"
    fi
    onx_log "dns restored: ${RESTORED_DNS} records"
fi

# ── Cleanup ──────────────────────────────────────────────────────────────────
rm -rf "${WORK_DIR}"

END_TS=$(date +%s)
DURATION=$(( END_TS - START_TS ))

_ONX_ROLLBACK_STACK=()
trap - ERR

onx_audit "onx-backup" "restore account=${ACCOUNT_ID} user=${USERNAME} src=${BACKUP_REAL} duration=${DURATION}s"

jq -nc \
    --arg restored "true" \
    --argjson home "${RESTORED_HOME}" \
    --argjson dbs "${RESTORED_DBS}" \
    --argjson mail "${RESTORED_MAIL}" \
    --argjson dns "${RESTORED_DNS}" \
    --argjson dur "${DURATION}" \
    '{
        restored: true,
        items: {
            home: $home,
            dbs:  $dbs,
            mail: $mail,
            dns:  $dns
        },
        duration_seconds: $dur
    }'
