#!/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="1.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")
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")
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/ and match username
EXPECTED_HOME="/home/${USERNAME}"
if [[ "${HOME_DIR}" != "${EXPECTED_HOME}" ]]; then
    HOME_REAL="$(realpath -m "${HOME_DIR}" 2>/dev/null || printf '%s' "${HOME_DIR}")"
    [[ "${HOME_REAL}" == "${EXPECTED_HOME}" ]] \
        || onx_die 1 "home path '${HOME_DIR}' does not match expected '${EXPECTED_HOME}'"
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

    tar -czf "${HOME_TAR}" \
        "${TAR_EXCLUDES[@]}" \
        -C "$(dirname "${HOME_DIR}")" "$(basename "${HOME_DIR}")" 2>/dev/null \
        || onx_die 3 "tar home failed for ${HOME_DIR}"
    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.
    DOMAINS_JSON=$(mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" \
        --batch --silent --raw "${ONX_PDNS_DB}" -e \
        "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 . \
        || printf '[]')

    RECORDS_JSON=$(mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" \
        --batch --silent --raw "${ONX_PDNS_DB}" -e \
        "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 . \
        || printf '[]')

    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.
        FORWARDERS=$(mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" \
            --batch --silent --raw "${ONX_MAIL_DB}" -e \
            "SELECT source, destination FROM forwarders WHERE source LIKE '%@${DOMAIN}';" \
            2>/dev/null | jq -R 'split("\t") | {source:.[0],destination:.[1]}' | jq -sc . \
            || printf '[]')

        ALIASES=$(mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" \
            --batch --silent --raw "${ONX_MAIL_DB}" -e \
            "SELECT source, destination FROM virtual_aliases WHERE source LIKE '%@${DOMAIN}';" \
            2>/dev/null | jq -R 'split("\t") | {source:.[0],destination:.[1]}' | jq -sc . \
            || printf '[]')

        AUTORESP=$(mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" \
            --batch --silent --raw "${ONX_MAIL_DB}" -e \
            "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 . \
            || printf '[]')

        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

# ── 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 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,
            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 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
        }
    }'
