#!/usr/bin/env bash
# =============================================================================
# onx-snapshot-create — Create a portable account snapshot (cpmove / native)
#
# Purpose:
#   Same idea as onx-backup-run but in a layout that's interoperable with the
#   cPanel `cpmove` format (or in our native `onoxsoft` layout). The cpmove
#   format is what cPanel/WHM's "Transfer Tool" ingests, so snapshots can be
#   restored into a cPanel server without conversion.
#
# Input (stdin JSON):
#   {
#     "account_id":  1,
#     "username":    "onx_acme01",
#     "format":      "cpmove",                              -- "cpmove" | "onoxsoft"
#     "output_path": "/var/backups/onoxsoft/cpmove-onx_acme01-{ts}.tar.gz",
#     "domain":      "acme.com"                             -- optional
#   }
#
# Output (stdout JSON):
#   {
#     "snapshot_path": "/var/backups/...",
#     "format":        "cpmove",
#     "size_bytes":    N,
#     "sha256":        "...",
#     "username":      "...",
#     "duration_seconds": N
#   }
#
# Deployed to: /usr/local/onoxsoft/bin/onx-snapshot-create
# =============================================================================

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"

require_root

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

onx_json_input

ACCOUNT_ID=$(onx_json_field "account_id" "0")
USERNAME=$(onx_json_field   "username")
FORMAT=$(onx_json_field     "format" "cpmove")
OUTPUT_PATH=$(onx_json_field "output_path")
DOMAIN=$(onx_json_field     "domain" "")

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

case "${FORMAT}" in
    cpmove|onoxsoft) : ;;
    *) onx_die 1 "format must be 'cpmove' or 'onoxsoft' (got '${FORMAT}')" ;;
esac

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 must be under ${BACKUP_ROOT_ALLOWED}/" ;;
esac

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"

HOME_DIR="/home/${USERNAME}"

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
[[ -e "${OUTPUT_PATH}" ]] && onx_die 2 "output file already exists: ${OUTPUT_PATH}"

# ── Working directory ────────────────────────────────────────────────────────
WORK_DIR="/tmp/onx-snapshot-${USERNAME}-$$"
SNAP_ROOT="${WORK_DIR}/cpmove-${USERNAME}"
mkdir -p "${SNAP_ROOT}"
chmod 0700 "${WORK_DIR}"

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 "snapshot-create start: account=${ACCOUNT_ID} user=${USERNAME} format=${FORMAT}"

# ── cpmove tree skeleton ─────────────────────────────────────────────────────
# Reference: cPanel's pkgacct output:
#   cpmove-<user>/
#     ├─ etc/<user>/                  -- system files (cron, passwd line, etc.)
#     ├─ homedir/                     -- /home/<user> content
#     ├─ mysql/                       -- one .sql per DB
#     ├─ mysql.sql                    -- grants
#     ├─ dnszones/                    -- one .db per zone
#     ├─ httpfiles/                   -- vhost overrides
#     ├─ va/                          -- virtual aliases
#     ├─ vad/                         -- virtual autoresponders
#     ├─ vf/                          -- virtual forwarders
#     ├─ vfilters/                    -- virtual filters (sieve)
#     ├─ cp/cpmove.<user>.txt         -- account metadata
#     └─ main                         -- top-level metadata

mkdir -p \
    "${SNAP_ROOT}/etc/${USERNAME}" \
    "${SNAP_ROOT}/homedir" \
    "${SNAP_ROOT}/mysql" \
    "${SNAP_ROOT}/dnszones" \
    "${SNAP_ROOT}/httpfiles" \
    "${SNAP_ROOT}/va" \
    "${SNAP_ROOT}/vad" \
    "${SNAP_ROOT}/vf" \
    "${SNAP_ROOT}/vfilters" \
    "${SNAP_ROOT}/cp"

# ── Home directory contents ──────────────────────────────────────────────────
if [[ "${MOCK_MODE}" == "1" ]]; then
    printf 'MOCK homedir for %s\n' "${USERNAME}" > "${SNAP_ROOT}/homedir/.placeholder"
else
    # cpmove expects homedir/* to be the contents of /home/<user> directly,
    # not /home/<user>/ itself. Trailing slash on rsync source achieves this.
    rsync -a "${HOME_DIR}/" "${SNAP_ROOT}/homedir/" 2>/dev/null \
        || onx_die 3 "rsync home failed"
fi

# ── MySQL databases ──────────────────────────────────────────────────────────
DB_LIST=()
if [[ "${MOCK_MODE}" == "1" ]]; then
    for i in 1 2; do
        DB_NAME="${USERNAME}_db${i}"
        printf -- '-- MOCK dump for %s\n' "${DB_NAME}" > "${SNAP_ROOT}/mysql/${DB_NAME}.sql"
        DB_LIST+=("${DB_NAME}")
    done
    # mock grants file
    printf -- '-- MOCK grants\n' > "${SNAP_ROOT}/mysql.sql"
else
    DB_NAMES=$(mysql_exec "" "SHOW DATABASES LIKE '${USERNAME}_%';" 2>/dev/null | tail -n +1 || true)
    if [[ -n "${DB_NAMES}" ]]; then
        while IFS= read -r DB_NAME; do
            [[ -z "${DB_NAME}" ]] && continue
            [[ "${DB_NAME}" =~ ^${USERNAME}_[a-z0-9_]+$ ]] || continue
            [[ -n "${_MYCNF_TMP:-}" ]] || _mycnf_tmp
            mysqldump --defaults-extra-file="${_MYCNF_TMP}" \
                --single-transaction --routines --triggers --no-tablespaces \
                "${DB_NAME}" > "${SNAP_ROOT}/mysql/${DB_NAME}.sql" 2>/dev/null \
                || onx_die 3 "mysqldump failed for ${DB_NAME}"
            DB_LIST+=("${DB_NAME}")
        done <<< "${DB_NAMES}"
    fi
    # Dump grants relevant to the account's MySQL users
    mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" --batch --silent -e \
        "SELECT CONCAT('GRANT ALL ON \`',db,'\`.* TO \`',user,'\`@\`',host,'\`;') \
         FROM mysql.db WHERE user LIKE '${USERNAME}_%';" 2>/dev/null \
        > "${SNAP_ROOT}/mysql.sql" || true
fi

# ── DNS zones (BIND-flavoured for cpmove compat) ─────────────────────────────
if [[ -n "${DOMAIN}" ]]; then
    onx_validate_domain "${DOMAIN}"
    ZONE_FILE="${SNAP_ROOT}/dnszones/${DOMAIN}.db"
    if [[ "${MOCK_MODE}" == "1" ]]; then
        printf '; MOCK zone for %s\n%s. IN SOA ns1.%s. admin.%s. 1 7200 3600 1209600 3600\n' \
            "${DOMAIN}" "${DOMAIN}" "${DOMAIN}" "${DOMAIN}" > "${ZONE_FILE}"
    else
        # Dump records from PowerDNS as a BIND-style zone (best-effort)
        {
            printf '; Zone for %s exported by onx-snapshot-create\n' "${DOMAIN}"
            mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" --batch --silent --raw \
                "${ONX_PDNS_DB}" -e \
                "SELECT r.name, r.ttl, 'IN', r.type, r.content \
                 FROM records r JOIN domains d ON r.domain_id=d.id \
                 WHERE (d.name='${DOMAIN}' OR d.name LIKE '%.${DOMAIN}') AND r.disabled=0;" \
                2>/dev/null | awk -F'\t' '{ printf "%s %s %s %s %s\n", $1, $2, $3, $4, $5 }' || true
        } > "${ZONE_FILE}"
    fi
fi

# ── Mail metadata (virtual forwarders/aliases/autoresponders) ────────────────
if [[ -n "${DOMAIN}" ]]; then
    if [[ "${MOCK_MODE}" == "1" ]]; then
        printf 'mockuser: mockforward@example.com\n' > "${SNAP_ROOT}/vf/${DOMAIN}"
        printf 'mockuser: mockalias@example.com\n'   > "${SNAP_ROOT}/va/${DOMAIN}"
    else
        # Forwarders: one file per domain, "<localpart>: <destination>"
        mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" --batch --silent --raw \
            "${ONX_MAIL_DB}" -e \
            "SELECT CONCAT(SUBSTRING_INDEX(source,'@',1), ': ', destination) \
             FROM forwarders WHERE source LIKE '%@${DOMAIN}';" 2>/dev/null \
            > "${SNAP_ROOT}/vf/${DOMAIN}" 2>/dev/null || true

        mysql --defaults-extra-file="${_MYCNF_TMP:-/dev/null}" --batch --silent --raw \
            "${ONX_MAIL_DB}" -e \
            "SELECT CONCAT(SUBSTRING_INDEX(source,'@',1), ': ', destination) \
             FROM virtual_aliases WHERE source LIKE '%@${DOMAIN}';" 2>/dev/null \
            > "${SNAP_ROOT}/va/${DOMAIN}" 2>/dev/null || true
    fi
fi

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

# main: cPanel-style top-level metadata (newline-separated KEY=value)
cat > "${SNAP_ROOT}/main" <<EOF
USER=${USERNAME}
DOMAIN=${DOMAIN}
EXPORTED_AT=${CREATED_AT}
EXPORTER=onoxsoft
EXPORTER_VERSION=1.0
EOF

# cp/cpmove.<user>.txt: legacy single-line metadata
cat > "${SNAP_ROOT}/cp/cpmove.${USERNAME}.txt" <<EOF
$(printf '%s' "${USERNAME}")
$(printf '%s' "${DOMAIN}")
EOF

# etc/<user>/quota — placeholder unless we read it
printf '%s' "0" > "${SNAP_ROOT}/etc/${USERNAME}/quota"

# Native ONOXSOFT manifest (in addition to cpmove files)
DB_LIST_JSON="[]"
if [[ ${#DB_LIST[@]} -gt 0 ]]; then
    DB_LIST_JSON=$(printf '%s\n' "${DB_LIST[@]}" | jq -R . | jq -sc .)
fi

jq -n \
    --arg ver "1.0" \
    --argjson aid "${ACCOUNT_ID}" \
    --arg user "${USERNAME}" \
    --arg domain "${DOMAIN}" \
    --arg fmt "${FORMAT}" \
    --arg created "${CREATED_AT}" \
    --argjson dbs "${DB_LIST_JSON}" \
    '{
        manifest_version: $ver,
        snapshot_type: "snapshot",
        account_id: $aid,
        username: $user,
        domain: $domain,
        format: $fmt,
        created_at: $created,
        databases: $dbs
    }' > "${SNAP_ROOT}/manifest.json"

# ── Native (onoxsoft) format: skip the cpmove wrapper, tar the work dir flat ─
if [[ "${FORMAT}" == "onoxsoft" ]]; then
    # Move the native manifest to the work root and tar from there
    mv "${SNAP_ROOT}/manifest.json" "${WORK_DIR}/manifest.json"
    if [[ "${MOCK_MODE}" == "1" ]]; then
        printf 'MOCK onoxsoft snapshot for %s\n' "${USERNAME}" > "${OUTPUT_PATH}"
    else
        tar -czf "${OUTPUT_PATH}" -C "${SNAP_ROOT}" . 2>/dev/null \
            || onx_die 3 "final tar failed"
    fi
else
    # cpmove layout: tar the cpmove-<user>/ directory
    if [[ "${MOCK_MODE}" == "1" ]]; then
        printf 'MOCK cpmove snapshot for %s\n' "${USERNAME}" > "${OUTPUT_PATH}"
    else
        tar -czf "${OUTPUT_PATH}" -C "${WORK_DIR}" "cpmove-${USERNAME}" 2>/dev/null \
            || onx_die 3 "final tar failed"
    fi
fi

chmod 0600 "${OUTPUT_PATH}"

# ── Checksum ─────────────────────────────────────────────────────────────────
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)
rm -rf "${WORK_DIR}"

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

_ONX_ROLLBACK_STACK=()
trap - ERR

onx_audit "onx-snapshot" "create account=${ACCOUNT_ID} user=${USERNAME} format=${FORMAT} bytes=${SIZE_BYTES} duration=${DURATION}s"

jq -nc \
    --arg path "${OUTPUT_PATH}" \
    --arg fmt "${FORMAT}" \
    --argjson size "${SIZE_BYTES}" \
    --arg sha "${SHA256}" \
    --arg user "${USERNAME}" \
    --argjson dur "${DURATION}" \
    '{
        snapshot_path: $path,
        format: $fmt,
        size_bytes: $size,
        sha256: $sha,
        username: $user,
        duration_seconds: $dur
    }'
