#!/usr/bin/env bash
# =============================================================================
# onx-backup-run — Create a full account backup (home + dbs + mail + dns)
#
# Purpose:
#   Produces a single tar.gz that contains everything needed to restore an
#   account: home directory, all MySQL databases owned by the account, the
#   Maildir tree (optional), DNS zone JSON dump, and an account-level
#   manifest.json with versioning + SHA-256 checksums.
#
# Input (stdin JSON):
#   {
#     "account_id":   1,                              -- required
#     "username":     "onx_acme01",                   -- required
#     "home":         "/home/onx_acme01",             -- required
#     "output_path":  "/var/backups/onoxsoft/onx_acme01-{ts}.tar.gz", -- required
#     "include_mail": true,                           -- default true
#     "include_dbs":  true,                           -- default true
#     "domain":       "acme.com"                      -- optional (for DNS dump)
#   }
#
# Output (stdout JSON):
#   {
#     "account_id":       1,
#     "path":             "/var/backups/...",
#     "size_bytes":       N,
#     "sha256":           "...",
#     "duration_seconds": N,
#     "included": {
#       "home":      true,
#       "databases": ["a", "b"],
#       "mail":      true,
#       "dns":       true
#     }
#   }
#
# Exit codes: 0=ok 1=invalid 2=preflight 3=exec 4=rolled-back 5=rollback-fail
#
# Production requirements:
#   - `tar`, `mysqldump`, `sha256sum`, `du`, `realpath` available
#   - /var/backups/onoxsoft directory exists, owned root:root mode 0700
#   - secrets in /etc/onoxsoft/secrets.env (mysql credentials)
#   - sudoers entry already covers /usr/local/onoxsoft/bin/onx-*
#
# Deployed to: /usr/local/onoxsoft/bin/onx-backup-run
# =============================================================================

set -euo pipefail

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

readonly MANIFEST_VERSION="2.0"
readonly BACKUP_ROOT_ALLOWED="/var/backups"

require_root

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

# ── Read & parse ─────────────────────────────────────────────────────────────
onx_json_input

ACCOUNT_ID=$(onx_json_field "account_id" "0")
USERNAME=$(onx_json_field   "username")

# Runtime detect home directory (handles /home/users/<user> vs /home/<user>)
USER_HOME=$(onx_resolve_home "${USERNAME}" 2>/dev/null || echo "/home/${USERNAME}")

HOME_DIR=$(onx_json_field   "home")
OUTPUT_PATH=$(onx_json_field "output_path")
INCLUDE_MAIL=$(onx_json_get_bool "${INPUT}" "include_mail" "true")
INCLUDE_DBS=$(onx_json_get_bool  "${INPUT}" "include_dbs"  "true")
# v2 manifest — FTP hesapları, SSL sertifikaları, cron job'ları da yedeklenir.
# Varsayılan true → "her şeyi yedekle"; BackupService config switch'lerinden türetir.
INCLUDE_FTP=$(onx_json_get_bool  "${INPUT}" "include_ftp"  "true")
INCLUDE_SSL=$(onx_json_get_bool  "${INPUT}" "include_ssl"  "true")
INCLUDE_CRON=$(onx_json_get_bool "${INPUT}" "include_cron" "true")
DOMAIN=$(onx_json_field "domain" "")

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

onx_validate_username "${USERNAME}"

[[ -n "${HOME_DIR}" ]]    || onx_die 1 "home is required"
[[ -n "${OUTPUT_PATH}" ]] || onx_die 1 "output_path is required"

# Home directory must live under /home/ veya /home/users/ ve match username
# ONOXSOFT config: home_base = /home/users (cPanel'den farklı). Legacy /home/<user>
# de kabul edilir (eski hesaplar için compat). Symlink resolve ediyoruz.
EXPECTED_HOMES=("${USER_HOME}" "/home/users/${USERNAME}")
HOME_OK=0
for expected in "${EXPECTED_HOMES[@]}"; do
    if [[ "${HOME_DIR}" == "${expected}" ]]; then
        HOME_OK=1
        break
    fi
    HOME_REAL="$(realpath -m "${HOME_DIR}" 2>/dev/null || printf '%s' "${HOME_DIR}")"
    EXPECTED_REAL="$(realpath -m "${expected}" 2>/dev/null || printf '%s' "${expected}")"
    if [[ "${HOME_REAL}" == "${EXPECTED_REAL}" ]]; then
        HOME_OK=1
        break
    fi
done
if [[ "${HOME_OK}" -eq 0 ]]; then
    onx_die 1 "home path '${HOME_DIR}' does not match expected (/home/${USERNAME} veya /home/users/${USERNAME})"
fi

# Output path validation: must land under /var/backups/
OUTPUT_DIR="$(dirname "${OUTPUT_PATH}")"
OUTPUT_REAL_PARENT="$(realpath -m "${OUTPUT_DIR}" 2>/dev/null || printf '%s' "${OUTPUT_DIR}")"
case "${OUTPUT_REAL_PARENT}" in
    ${BACKUP_ROOT_ALLOWED}|${BACKUP_ROOT_ALLOWED}/*) : ;;
    *) onx_die 1 "output_path '${OUTPUT_PATH}' must be under ${BACKUP_ROOT_ALLOWED}/" ;;
esac

# Reject obvious traversal in basename
OUTPUT_BASENAME="$(basename "${OUTPUT_PATH}")"
[[ "${OUTPUT_BASENAME}" == *..* ]] && onx_die 1 "output filename contains '..'"
[[ "${OUTPUT_BASENAME}" =~ \.tar\.gz$ ]] || onx_die 1 "output_path must end in .tar.gz"

if [[ "${INCLUDE_MAIL}" == "true" || "${INCLUDE_DBS}" == "true" ]]; then
    : # ok, at least one body type
fi

# ── Preflight ────────────────────────────────────────────────────────────────
if [[ "${MOCK_MODE}" != "1" ]]; then
    [[ -d "${HOME_DIR}" ]] || onx_die 2 "home directory does not exist: ${HOME_DIR}"
fi

mkdir -p "${OUTPUT_DIR}" || onx_die 3 "cannot create output dir: ${OUTPUT_DIR}"
chmod 0700 "${OUTPUT_DIR}" 2>/dev/null || true

# Prevent clobbering an existing archive
[[ -e "${OUTPUT_PATH}" ]] && onx_die 2 "output file already exists: ${OUTPUT_PATH}"

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

# Rollback: wipe partial files + working dir on any error
trap 'onx_rollback_run' ERR
onx_rollback_register "rm -rf '${WORK_DIR}' 2>/dev/null || true"
onx_rollback_register "rm -f '${OUTPUT_PATH}' '${OUTPUT_PATH}.sha256' 2>/dev/null || true"

START_TS=$(date +%s)
onx_log "backup-run start: account=${ACCOUNT_ID} user=${USERNAME} out=${OUTPUT_PATH}"

# ── 1. Home directory ────────────────────────────────────────────────────────
HOME_INCLUDED="false"
HOME_TAR="${WORK_DIR}/home.tar.gz"

if [[ "${MOCK_MODE}" == "1" ]]; then
    # Mock: small placeholder file
    printf 'MOCK home tar for %s\n' "${USERNAME}" > "${HOME_TAR}"
    HOME_INCLUDED="true"
else
    # Skip Maildir if INCLUDE_MAIL=false (Maildir handled separately for proper layout)
    TAR_EXCLUDES=()
    if [[ "${INCLUDE_MAIL}" != "true" ]]; then
        TAR_EXCLUDES+=(--exclude='Maildir' --exclude='Maildir/*')
    fi

    # Volatile/dev/buyuk dosyalari disla: error_log (canli + dev boyut), logs, trash,
    # lscache, tmp, kirik Apache symlink (access-logs). Boyut kuculur + "file changed" azalir.
    TAR_EXCLUDES+=(--exclude='*/error_log' --exclude='*/logs/*' --exclude='*/lscache/*' --exclude='*/.trash/*' --exclude='*/.trash.cpanel/*' --exclude='*/tmp/*' --exclude='access-logs')
    # tar exit 1 = "file changed as we read it" / kirik symlink "cannot stat" = UYARI;
    # arsiv YINE gecerli (canli error_log/Maildir degisirken normal). Yalniz exit>=2 (fatal)
    # basarisiz say. "|| TAR_EC=$?" set -e guvenli (BARE komut exit!=0'da abort ederdi).
    TAR_EC=0
    tar --ignore-failed-read --warning=no-file-changed --warning=no-file-removed \
        -czf "${HOME_TAR}" \
        "${TAR_EXCLUDES[@]}" \
        -C "$(dirname "${HOME_DIR}")" "$(basename "${HOME_DIR}")" 2>/dev/null \
        || TAR_EC=$?
    [[ "${TAR_EC}" -gt 1 ]] && onx_die 3 "tar home failed for ${HOME_DIR} (exit ${TAR_EC})"
    HOME_INCLUDED="true"
fi

HOME_TAR_SHA=$(sha256sum "${HOME_TAR}" 2>/dev/null | awk '{print $1}')
HOME_TAR_BYTES=$(stat -c '%s' "${HOME_TAR}" 2>/dev/null || echo 0)

# ── 2. MySQL databases ───────────────────────────────────────────────────────
DB_DIR="${WORK_DIR}/databases"
mkdir -p "${DB_DIR}"

DB_LIST_JSON="[]"
DBS_INCLUDED=()

if [[ "${INCLUDE_DBS}" == "true" ]]; then
    if [[ "${MOCK_MODE}" == "1" ]]; then
        # Mock: 1-3 fake dbs
        for i in 1 2; do
            DB_NAME="${USERNAME}_db${i}"
            printf '-- MOCK dump for %s\n' "${DB_NAME}" > "${DB_DIR}/${DB_NAME}.sql"
            DBS_INCLUDED+=("${DB_NAME}")
        done
    else
        # Enumerate databases prefixed with username_
        DB_PATTERN="${USERNAME}_%"
        # SHOW DATABASES LIKE doesn't accept binding; pattern is regex-safe (only [a-z0-9_]).
        DB_NAMES=$(mysql_exec "" "SHOW DATABASES LIKE '${DB_PATTERN}';" 2>/dev/null | tail -n +1 || true)

        if [[ -n "${DB_NAMES}" ]]; then
            while IFS= read -r DB_NAME; do
                [[ -z "${DB_NAME}" ]] && continue
                # Defence-in-depth: only allow our prefix pattern
                [[ "${DB_NAME}" =~ ^${USERNAME}_[a-z0-9_]+$ ]] || continue

                DUMP_FILE="${DB_DIR}/${DB_NAME}.sql"
                # mysqldump uses _MYCNF_TMP via env; mysql_exec already initialised it
                [[ -n "${_MYCNF_TMP:-}" ]] || _mycnf_tmp
                if ! mysqldump --defaults-extra-file="${_MYCNF_TMP}" \
                        --single-transaction \
                        --routines \
                        --triggers \
                        --no-tablespaces \
                        "${DB_NAME}" > "${DUMP_FILE}" 2>/dev/null; then
                    onx_die 3 "mysqldump failed for ${DB_NAME}"
                fi
                DBS_INCLUDED+=("${DB_NAME}")
            done <<< "${DB_NAMES}"
        fi
    fi

    # Build JSON array of DB names
    if [[ ${#DBS_INCLUDED[@]} -gt 0 ]]; then
        DB_LIST_JSON=$(printf '%s\n' "${DBS_INCLUDED[@]}" | jq -R . | jq -sc .)
    fi
fi

# ── 3. DNS zones (PowerDNS) ──────────────────────────────────────────────────
DNS_DUMP="${WORK_DIR}/dns.json"
DNS_INCLUDED="false"

if [[ "${MOCK_MODE}" == "1" ]]; then
    printf '{"domains":[],"records":[]}\n' > "${DNS_DUMP}"
    DNS_INCLUDED="true"
elif [[ -n "${DOMAIN}" ]]; then
    onx_validate_domain "${DOMAIN}"
    # Query PowerDNS domains + records — owner_id mapping is panel-side; here
    # we pull every zone whose name matches the account's domain or sub-domains.
    # mysql_exec doğru credential ailesini (pdns) seçer; eskiden _MYCNF_TMP (önceki
    # mail çağrısından kalan) ile pdns'e bağlanıp "Access denied" alıyordu → fail →
    # pipefail + (içerideki) ||printf çift '[]' basıp --argjson'u patlatıyordu.
    # Fallback artık ATAMA düzeyinde → tek geçerli JSON garantisi.
    DOMAINS_JSON=$(mysql_exec "${ONX_PDNS_DB}" \
        "SELECT id, name, master, type FROM domains WHERE name='${DOMAIN}' OR name LIKE '%.${DOMAIN}';" \
        2>/dev/null | jq -R 'split("\t") | {id:.[0],name:.[1],master:.[2],type:.[3]}' | jq -sc .) || DOMAINS_JSON='[]'
    [[ -n "${DOMAINS_JSON}" ]] || DOMAINS_JSON='[]'

    RECORDS_JSON=$(mysql_exec "${ONX_PDNS_DB}" \
        "SELECT r.id, r.domain_id, r.name, r.type, r.content, r.ttl, r.prio, r.disabled FROM records r JOIN domains d ON r.domain_id=d.id WHERE d.name='${DOMAIN}' OR d.name LIKE '%.${DOMAIN}';" \
        2>/dev/null | jq -R 'split("\t") | {id:.[0],domain_id:.[1],name:.[2],type:.[3],content:.[4],ttl:.[5],prio:.[6],disabled:.[7]}' | jq -sc .) || RECORDS_JSON='[]'
    [[ -n "${RECORDS_JSON}" ]] || RECORDS_JSON='[]'

    jq -n --argjson d "${DOMAINS_JSON:-[]}" --argjson r "${RECORDS_JSON:-[]}" \
        '{domains:$d,records:$r}' > "${DNS_DUMP}"
    DNS_INCLUDED="true"
else
    printf '{"domains":[],"records":[]}\n' > "${DNS_DUMP}"
fi

# ── 4. Email metadata (forwarders, aliases, autoresponders) ──────────────────
MAIL_META_DUMP="${WORK_DIR}/mail-meta.json"
MAIL_INCLUDED="false"

if [[ "${INCLUDE_MAIL}" == "true" ]]; then
    if [[ "${MOCK_MODE}" == "1" ]]; then
        printf '{"forwarders":[],"aliases":[],"autoresponders":[]}\n' > "${MAIL_META_DUMP}"
    else
        # Forwarders / aliases / autoresponders are panel-side tables; query
        # them in the mail database. Empty arrays when tables don't exist.
        # mysql_exec mail credential'ı seçer; fallback atama düzeyinde (çift-'[]' önler).
        FORWARDERS=$(mysql_exec "${ONX_MAIL_DB}" \
            "SELECT source, destination FROM forwarders WHERE source LIKE '%@${DOMAIN}';" \
            2>/dev/null | jq -R 'split("\t") | {source:.[0],destination:.[1]}' | jq -sc .) || FORWARDERS='[]'
        [[ -n "${FORWARDERS}" ]] || FORWARDERS='[]'

        ALIASES=$(mysql_exec "${ONX_MAIL_DB}" \
            "SELECT source, destination FROM virtual_aliases WHERE source LIKE '%@${DOMAIN}';" \
            2>/dev/null | jq -R 'split("\t") | {source:.[0],destination:.[1]}' | jq -sc .) || ALIASES='[]'
        [[ -n "${ALIASES}" ]] || ALIASES='[]'

        AUTORESP=$(mysql_exec "${ONX_MAIL_DB}" \
            "SELECT email, subject, body, enabled FROM autoresponders WHERE email LIKE '%@${DOMAIN}';" \
            2>/dev/null | jq -R 'split("\t") | {email:.[0],subject:.[1],body:.[2],enabled:.[3]}' | jq -sc .) || AUTORESP='[]'
        [[ -n "${AUTORESP}" ]] || AUTORESP='[]'

        jq -n \
            --argjson f "${FORWARDERS:-[]}" \
            --argjson a "${ALIASES:-[]}" \
            --argjson r "${AUTORESP:-[]}" \
            '{forwarders:$f,aliases:$a,autoresponders:$r}' > "${MAIL_META_DUMP}"
    fi
    MAIL_INCLUDED="true"
else
    printf '{"forwarders":[],"aliases":[],"autoresponders":[]}\n' > "${MAIL_META_DUMP}"
fi

# ── 4b. FTP accounts (panel ftp_users) ───────────────────────────────────────
# ftp_users panel DB'sinde (Laravel default connection). account_id ile keylenir.
# password_hash dahil tüm tanım korunur → restore'da yeniden insert edilir.
FTP_DUMP="${WORK_DIR}/ftp.json"
FTP_INCLUDED="false"

if [[ "${INCLUDE_FTP}" == "true" ]]; then
    if [[ "${MOCK_MODE}" == "1" ]]; then
        printf '{"accounts":[]}\n' > "${FTP_DUMP}"
    else
        # ACCOUNT_ID digits-only (yukarıda valide edildi) → güvenli interpolasyon.
        # NOT: yalnız temel migration (2026_05_12) kolonları — is_default/backend
        # v88 migration'ından gelir ve sunucuda henüz yok olabilir ("NO EXECUTE in
        # this sprint"). Onlara dokunursak SELECT patlar → FTP yedeği boş kalır.
        FTP_ROWS=$(mysql_exec "${ONX_PANEL_DB}" \
            "SELECT user, password_hash, uid, gid, dir, quota_size, quota_files, ul_bandwidth, dl_bandwidth, ip_access, logical_name, is_main_account FROM ftp_users WHERE account_id=${ACCOUNT_ID} AND deleted_at IS NULL;" \
            2>/dev/null | jq -R 'split("\t") | {user:.[0], password_hash:.[1], uid:.[2], gid:.[3], dir:.[4], quota_size:.[5], quota_files:.[6], ul_bandwidth:.[7], dl_bandwidth:.[8], ip_access:.[9], logical_name:.[10], is_main_account:.[11]}' \
            | jq -sc 'map(select(.user != null and .user != ""))') || FTP_ROWS='[]'
        [[ -n "${FTP_ROWS}" ]] || FTP_ROWS='[]'
        jq -n --argjson a "${FTP_ROWS:-[]}" '{accounts:$a}' > "${FTP_DUMP}"
    fi
    FTP_INCLUDED="true"
else
    printf '{"accounts":[]}\n' > "${FTP_DUMP}"
fi

# ── 4c. SSL certificates (on-disk cert/key + metadata) ───────────────────────
# Fonksiyonel cert/key dosyaları diskte: LE → /etc/letsencrypt/live/<domain>/,
# manuel → /etc/onoxsoft/ssl/manual/<domain>/. Bunları kopyalarız (restore'da
# webserver bunları okur). ssl.json yalnız tek-satırlık metadata (PEM blob'ları
# DB'de çok-satırlı olduğu için parse'ı bozmamak adına buraya dahil edilmez).
SSL_DIR="${WORK_DIR}/ssl"
SSL_META="${WORK_DIR}/ssl.json"
SSL_INCLUDED="false"

if [[ "${INCLUDE_SSL}" == "true" ]]; then
    mkdir -p "${SSL_DIR}"
    if [[ "${MOCK_MODE}" == "1" ]]; then
        printf '{"certificates":[]}\n' > "${SSL_META}"
    else
        ACC_DOMAINS=$(mysql_exec "${ONX_PANEL_DB}" \
            "SELECT name FROM domains WHERE account_id=${ACCOUNT_ID};" 2>/dev/null || true)
        while IFS= read -r DNAME; do
            [[ -z "${DNAME}" ]] && continue
            onx_validate_domain "${DNAME}" >/dev/null 2>&1 || continue
            for src in "/etc/letsencrypt/live/${DNAME}" "/etc/onoxsoft/ssl/manual/${DNAME}"; do
                if [[ -d "${src}" ]]; then
                    mkdir -p "${SSL_DIR}/${DNAME}"
                    # -L: LE live/ symlink'lerini deref et (archive/'e işaret eder);
                    # -p: 0600 privkey izinlerini koru.
                    cp -rLp "${src}/." "${SSL_DIR}/${DNAME}/" 2>/dev/null || true
                fi
            done
        done <<< "${ACC_DOMAINS}"

        SSL_ROWS=$(mysql_exec "${ONX_PANEL_DB}" \
            "SELECT d.name, s.issuer, s.common_name, s.expires_at, s.status, s.is_san_secondary FROM ssl_certificates s JOIN domains d ON s.domain_id=d.id WHERE d.account_id=${ACCOUNT_ID};" \
            2>/dev/null | jq -R 'split("\t") | {domain:.[0], issuer:.[1], common_name:.[2], expires_at:.[3], status:.[4], is_san_secondary:.[5]}' \
            | jq -sc 'map(select(.domain != null and .domain != ""))') || SSL_ROWS='[]'
        [[ -n "${SSL_ROWS}" ]] || SSL_ROWS='[]'
        jq -n --argjson c "${SSL_ROWS:-[]}" '{certificates:$c}' > "${SSL_META}"
    fi
    SSL_INCLUDED="true"
else
    printf '{"certificates":[]}\n' > "${SSL_META}"
fi

# ── 4d. Cron jobs (rendered /etc/cron.d file + panel cron_jobs) ──────────────
# /etc/cron.d/onoxsoft-<user> = crond'un okuduğu fonksiyonel dosya (verbatim
# kopyalanır). cron_jobs panel DB'de source-of-truth; command serbest metin
# olduğu için TO_BASE64 ile satır-güvenli aktarılır.
CRON_DUMP="${WORK_DIR}/cron.json"
CRON_DIR="${WORK_DIR}/cron"
CRON_INCLUDED="false"

if [[ "${INCLUDE_CRON}" == "true" ]]; then
    mkdir -p "${CRON_DIR}"
    if [[ "${MOCK_MODE}" == "1" ]]; then
        printf '{"jobs":[]}\n' > "${CRON_DUMP}"
    else
        CRON_FILE="/etc/cron.d/onoxsoft-${USERNAME}"
        if [[ -f "${CRON_FILE}" ]]; then
            cp -p "${CRON_FILE}" "${CRON_DIR}/cron.d" 2>/dev/null || true
        fi
        # REPLACE(TO_BASE64(command),'\n','') → tek satır base64 (TO_BASE64 76
        # karakterde bir \n ekler; onları sileriz). jq @base64d ile restore'da çözülür.
        CRON_ROWS=$(mysql_exec "${ONX_PANEL_DB}" \
            "SELECT label, expression, REPLACE(TO_BASE64(command),'\n',''), email_output, is_active FROM cron_jobs WHERE account_id=${ACCOUNT_ID} AND deleted_at IS NULL;" \
            2>/dev/null | jq -R 'split("\t") | {label:.[0], expression:.[1], command_b64:.[2], email_output:.[3], is_active:.[4]}' \
            | jq -sc 'map(select(.expression != null and .expression != ""))') || CRON_ROWS='[]'
        [[ -n "${CRON_ROWS}" ]] || CRON_ROWS='[]'
        jq -n --argjson j "${CRON_ROWS:-[]}" '{jobs:$j}' > "${CRON_DUMP}"
    fi
    CRON_INCLUDED="true"
else
    printf '{"jobs":[]}\n' > "${CRON_DUMP}"
fi

# ── 5. Manifest ──────────────────────────────────────────────────────────────
CREATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

MANIFEST_FILE="${WORK_DIR}/manifest.json"
jq -n \
    --arg ver "${MANIFEST_VERSION}" \
    --argjson aid "${ACCOUNT_ID}" \
    --arg user "${USERNAME}" \
    --arg home "${HOME_DIR}" \
    --arg domain "${DOMAIN}" \
    --arg created "${CREATED_AT}" \
    --arg home_sha "${HOME_TAR_SHA}" \
    --argjson home_bytes "${HOME_TAR_BYTES}" \
    --argjson home_inc "${HOME_INCLUDED}" \
    --argjson mail_inc "${MAIL_INCLUDED}" \
    --argjson dns_inc "${DNS_INCLUDED}" \
    --argjson ftp_inc "${FTP_INCLUDED}" \
    --argjson ssl_inc "${SSL_INCLUDED}" \
    --argjson cron_inc "${CRON_INCLUDED}" \
    --argjson dbs "${DB_LIST_JSON}" \
    '{
        manifest_version: $ver,
        account_id: $aid,
        username: $user,
        home: $home,
        domain: $domain,
        created_at: $created,
        included: {
            home: $home_inc,
            mail: $mail_inc,
            dns:  $dns_inc,
            ftp:  $ftp_inc,
            ssl:  $ssl_inc,
            cron: $cron_inc,
            databases: $dbs
        },
        artifacts: {
            "home.tar.gz": { sha256: $home_sha, bytes: $home_bytes }
        }
    }' > "${MANIFEST_FILE}"

# ── 6. Final tar.gz ──────────────────────────────────────────────────────────
if [[ "${MOCK_MODE}" == "1" ]]; then
    # In mock mode just touch the file with a sentinel so size > 0
    printf 'MOCK onx-backup-run output for %s\n' "${USERNAME}" > "${OUTPUT_PATH}"
else
    tar -czf "${OUTPUT_PATH}" -C "${WORK_DIR}" . 2>/dev/null \
        || onx_die 3 "final tar failed: ${OUTPUT_PATH}"
fi

chmod 0600 "${OUTPUT_PATH}"

# ── 7. Checksum file ─────────────────────────────────────────────────────────
SHA256=$(sha256sum "${OUTPUT_PATH}" | awk '{print $1}')
printf '%s  %s\n' "${SHA256}" "${OUTPUT_BASENAME}" > "${OUTPUT_PATH}.sha256"
chmod 0600 "${OUTPUT_PATH}.sha256"

SIZE_BYTES=$(stat -c '%s' "${OUTPUT_PATH}" 2>/dev/null || echo 0)

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

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

# Disarm rollback (we succeeded)
_ONX_ROLLBACK_STACK=()
trap - ERR

onx_audit "onx-backup" "run account=${ACCOUNT_ID} user=${USERNAME} path=${OUTPUT_PATH} bytes=${SIZE_BYTES} duration=${DURATION}s"

# ── Output ───────────────────────────────────────────────────────────────────
jq -nc \
    --argjson aid "${ACCOUNT_ID}" \
    --arg path "${OUTPUT_PATH}" \
    --argjson size "${SIZE_BYTES}" \
    --arg sha "${SHA256}" \
    --argjson dur "${DURATION}" \
    --argjson home_inc "${HOME_INCLUDED}" \
    --argjson mail_inc "${MAIL_INCLUDED}" \
    --argjson dns_inc "${DNS_INCLUDED}" \
    --argjson ftp_inc "${FTP_INCLUDED}" \
    --argjson ssl_inc "${SSL_INCLUDED}" \
    --argjson cron_inc "${CRON_INCLUDED}" \
    --argjson dbs "${DB_LIST_JSON}" \
    '{
        account_id: $aid,
        path: $path,
        size_bytes: $size,
        sha256: $sha,
        duration_seconds: $dur,
        included: {
            home: $home_inc,
            databases: $dbs,
            mail: $mail_inc,
            dns: $dns_inc,
            ftp: $ftp_inc,
            ssl: $ssl_inc,
            cron: $cron_inc
        }
    }'
