#!/usr/bin/env bash

set -Eeuo pipefail
umask 027

DRY_RUN=${DRY_RUN:-0}
BUNDLE_ROOT=${BUNDLE_ROOT:-/opt/zhct-deploy}
BACKUP_ROOT=${BACKUP_ROOT:-/var/backups/zhct-deploy}
RUN_ID=${RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)}
LOG_PREFIX=${LOG_PREFIX:-zhct-deploy}

log() {
    printf '%s [%s] %s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')" "$LOG_PREFIX" "$*" >&2
}

die() {
    log "ERROR: $*"
    exit 1
}

quote_command() {
    printf '%q ' "$@"
    printf '\n'
}

run() {
    log "RUN: $(quote_command "$@")"
    if [[ "$DRY_RUN" == "1" ]]; then
        return 0
    fi
    "$@"
}

run_shell() {
    local command=$1
    log "RUN-SHELL: $command"
    if [[ "$DRY_RUN" == "1" ]]; then
        return 0
    fi
    /usr/bin/env bash -Eeuo pipefail -c "$command"
}

require_root() {
    if [[ "$DRY_RUN" != "1" && $(id -u) -ne 0 ]]; then
        die "non-dry-run actions require root"
    fi
}

require_command() {
    command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

require_file() {
    [[ -f "$1" ]] || die "required file not found: $1"
}

require_dir() {
    [[ -d "$1" ]] || die "required directory not found: $1"
}

require_value() {
    local name=$1
    local value=${2:-}
    [[ -n "$value" ]] || die "$name is empty"
    [[ "$value" != *'@@'* ]] || die "$name still contains an unresolved placeholder"
}

ensure_dir() {
    local path=$1 mode=${2:-0750} owner=${3:-root} group=${4:-root}
    run install -d -m "$mode" -o "$owner" -g "$group" "$path"
}

backup_file() {
    local path=$1
    [[ -e "$path" || -L "$path" ]] || return 0
    local relative=${path#/}
    local target="$BACKUP_ROOT/$RUN_ID/$relative"
    ensure_dir "$(dirname "$target")" 0750 root root
    run cp -a "$path" "$target"
}

install_file() {
    local source=$1 destination=$2 mode=${3:-0640} owner=${4:-root} group=${5:-root}
    require_file "$source"
    if [[ -f "$destination" ]] && cmp -s "$source" "$destination"; then
        log "UNCHANGED: $destination"
        return 0
    fi
    backup_file "$destination"
    ensure_dir "$(dirname "$destination")" 0750 root root
    run install -m "$mode" -o "$owner" -g "$group" "$source" "$destination"
}

render_template() {
    local source=$1 destination=$2 mode=${3:-0640} owner=${4:-root} group=${5:-root}
    require_file "$source"
    require_value NODE_KEY "${NODE_KEY:-}"
    require_value NODE_IP "${NODE_IP:-}"
    require_value NODE_NAME "${NODE_NAME:-}"
    require_value NODE_INTERFACE "${NODE_INTERFACE:-}"
    local temp
    temp=$(mktemp)
    sed \
        -e "s|@@NODE_KEY@@|$NODE_KEY|g" \
        -e "s|@@NODE_IP@@|$NODE_IP|g" \
        -e "s|@@NODE_NAME@@|$NODE_NAME|g" \
        -e "s|@@NODE_INTERFACE@@|$NODE_INTERFACE|g" \
        "$source" >"$temp"
    if grep -Eq '@@[A-Z0-9_]+@@' "$temp"; then
        rm -f "$temp"
        die "unresolved non-secret placeholder in $source"
    fi
    install_file "$temp" "$destination" "$mode" "$owner" "$group"
    rm -f "$temp"
}

assert_expected_ip() {
    local expected=$1
    require_value expected_ip "$expected"
    if [[ "${ZHCT_STATIC_VALIDATE:-0}" == "1" ]]; then
        log "STATIC: skip guest IP assertion for $expected"
        return 0
    fi
    if ! ip -o -4 addr show scope global | awk '{print $4}' | cut -d/ -f1 | grep -Fxq "$expected"; then
        die "expected IP $expected is not configured on this guest"
    fi
}

assert_role() {
    local role=$1
    case "$role" in
        nginx|app|mysql|redis|emqx) ;;
        *) die "unknown role: $role" ;;
    esac
}

asset_path() {
    printf '%s/packages/%s\n' "$BUNDLE_ROOT" "$1"
}

verify_rpm_closure_manifest() {
    local manifest=$1
    require_file "$manifest"
    local package_root rpm_dir manifest_paths actual_paths rpm_count
    package_root=$(dirname "$manifest")
    rpm_dir="$package_root/rpms"
    require_dir "$rpm_dir"
    manifest_paths=$(mktemp)
    actual_paths=$(mktemp)
    if ! awk '
        NF != 2 || $1 !~ /^[0-9a-f]{64}$/ || $2 !~ /^rpms\/.*\.rpm$/ ||
            length(substr($2, 6)) <= 4 || index(substr($2, 6), "/") != 0 || seen[$2]++ {exit 1}
        {print $2}
    ' "$manifest" | sort >"$manifest_paths"; then
        rm -f "$manifest_paths" "$actual_paths"
        die "invalid or duplicate RPM closure manifest entry: $manifest"
    fi
    (cd "$package_root" && sha256sum -c "$(basename "$manifest")") || {
        rm -f "$manifest_paths" "$actual_paths"
        die "RPM closure SHA256 verification failed: $manifest"
    }
    (cd "$package_root" && find rpms -maxdepth 1 -type f -name '*.rpm' -print | sort) >"$actual_paths"
    rpm_count=$(wc -l <"$actual_paths")
    if [[ "$rpm_count" -eq 0 ]] || ! cmp -s "$manifest_paths" "$actual_paths"; then
        rm -f "$manifest_paths" "$actual_paths"
        die "RPM closure files do not exactly match $manifest"
    fi
    rm -f "$manifest_paths" "$actual_paths"
}

verify_locked_assets() {
    local digest_manifest=$1
    require_file "$digest_manifest"
    local artifact sha status source actual
    while IFS=$'\t' read -r artifact sha status source; do
        [[ "$artifact" == "artifact" || -z "$artifact" ]] && continue
        case "$status" in
            LOCKED)
                require_value "sha256 for $artifact" "$sha"
                if [[ "${ZHCT_STATIC_VALIDATE:-0}" == "1" && ! -f "$(asset_path "$artifact")" ]]; then
                    log "STATIC: locked asset not staged locally: $artifact"
                    continue
                fi
                require_file "$(asset_path "$artifact")"
                actual=$(sha256sum "$(asset_path "$artifact")" | awk '{print $1}')
                [[ "$actual" == "$sha" ]] || die "SHA256 mismatch for $artifact"
                if [[ "$artifact" == "rpms.sha256" ]]; then
                    verify_rpm_closure_manifest "$(asset_path "$artifact")"
                fi
                ;;
            PENDING|REPO_SNAPSHOT)
                if [[ "$DRY_RUN" != "1" ]]; then
                    die "$artifact digest is $status; complete the role download task before installation"
                fi
                log "PENDING: $artifact ($status, source: $source)"
                ;;
            *) die "unknown digest status for $artifact: $status" ;;
        esac
    done <"$digest_manifest"
}

install_offline_rpms() {
    local rpm_dir="$BUNDLE_ROOT/packages/rpms"
    local rpm_manifest="$BUNDLE_ROOT/packages/rpms.sha256"
    [[ -d "$rpm_dir" ]] || {
        [[ "$DRY_RUN" == "1" ]] && { log "PENDING: offline RPM directory $rpm_dir"; return 0; }
        die "offline RPM directory not found: $rpm_dir"
    }
    verify_rpm_closure_manifest "$rpm_manifest"
    local -a rpms=()
    while IFS= read -r -d '' rpm; do rpms+=("$rpm"); done < <(find "$rpm_dir" -maxdepth 1 -type f -name '*.rpm' -print0 | sort -z)
    ((${#rpms[@]} > 0)) || {
        [[ "$DRY_RUN" == "1" ]] && { log "PENDING: no RPMs in $rpm_dir"; return 0; }
        die "no offline RPMs found in $rpm_dir"
    }
    run dnf --disablerepo='*' --setopt=install_weak_deps=False install -y "${rpms[@]}"
}

extract_archive() {
    local archive=$1 destination=$2
    if [[ "${ZHCT_STATIC_VALIDATE:-0}" == "1" && ! -f "$archive" ]]; then
        log "STATIC: skip archive extraction for $(basename "$archive")"
        return 0
    fi
    require_file "$archive"
    ensure_dir "$destination" 0750 root root
    case "$archive" in
        *.tar.gz|*.tgz) run tar -xzf "$archive" -C "$destination" ;;
        *.tar.bz2) run tar -xjf "$archive" -C "$destination" ;;
        *.tar.xz) run tar -xJf "$archive" -C "$destination" ;;
        *) die "unsupported archive: $archive" ;;
    esac
}

assert_node_key() {
    local nodes_manifest=$1 node_key=$2
    require_file "$nodes_manifest"
    awk -F'\t' -v node="$node_key" 'NR > 1 && $1 == node {found=1} END {exit !found}' "$nodes_manifest" || \
        die "node $node_key is not allowed by $nodes_manifest"
}

apply_node_unit_policy() {
    local units_manifest=$1 node_key=$2
    require_file "$units_manifest"
    local unit template_state node_states purpose state pair
    while IFS=$'\t' read -r unit template_state node_states purpose; do
        [[ "$unit" == "unit" || -z "$unit" ]] && continue
        state=
        IFS=';' read -ra pairs <<<"$node_states"
        for pair in "${pairs[@]}"; do
            if [[ "${pair%%=*}" == "$node_key" ]]; then
                state=${pair#*=}
                break
            fi
        done
        [[ -n "$state" ]] || die "no unit policy for $unit on $node_key"
        case "$state" in
            enabled,active) run systemctl enable --now "$unit" ;;
            disabled,inactive) run systemctl disable --now "$unit" ;;
            *) die "unsupported unit policy for $unit on $node_key: $state" ;;
        esac
    done <"$units_manifest"
}

write_node_identity() {
    local destination=/etc/zhct/node.env
    local temp
    temp=$(mktemp)
    {
        printf 'ROLE=%q\n' "$ROLE"
        printf 'NODE_KEY=%q\n' "$NODE_KEY"
        printf 'NODE_IP=%q\n' "$NODE_IP"
        printf 'NODE_NAME=%q\n' "$NODE_NAME"
        printf 'APPLIED_AT=%q\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    } >"$temp"
    install_file "$temp" "$destination" 0640 root root
    rm -f "$temp"
}

stop_disable_units() {
    local units_manifest=$1
    require_file "$units_manifest"
    local unit template_state node_states purpose
    while IFS=$'\t' read -r unit template_state node_states purpose; do
        [[ "$unit" == "unit" || -z "$unit" ]] && continue
        run systemctl disable --now "$unit"
    done <"$units_manifest"
}

verify_template_units_inactive() {
    local units_manifest=$1
    require_file "$units_manifest"
    local unit template_state node_states purpose enabled_state
    while IFS=$'\t' read -r unit template_state node_states purpose; do
        [[ "$unit" == "unit" || -z "$unit" ]] && continue
        enabled_state=$(systemctl is-enabled "$unit" 2>/dev/null || true)
        case "$enabled_state" in
            disabled|masked) ;;
            *) die "template contains enabled or missing unit: $unit ($enabled_state)" ;;
        esac
        if systemctl is-active --quiet "$unit"; then
            die "template contains active unit: $unit"
        fi
    done <"$units_manifest"
}

clean_common_clone_identity() {
    run rm -f /etc/machine-id /var/lib/dbus/machine-id
    run truncate -s 0 /etc/machine-id
    run rm -f /etc/ssh/ssh_host_ecdsa_key /etc/ssh/ssh_host_ecdsa_key.pub
    run rm -f /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key.pub
    run rm -f /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_rsa_key.pub
}

regenerate_common_clone_identity() {
    local identity=/etc/zhct/node.env stored_role= stored_node= stored_ip=
    if [[ -r "$identity" ]]; then
        stored_role=$(awk -F= '$1 == "ROLE" {print $2}' "$identity")
        stored_node=$(awk -F= '$1 == "NODE_KEY" {print $2}' "$identity")
        stored_ip=$(awk -F= '$1 == "NODE_IP" {print $2}' "$identity")
    fi
    if [[ "$stored_role" == "$ROLE" && "$stored_node" == "$NODE_KEY" && "$stored_ip" == "$NODE_IP" && -s /etc/machine-id && -s /etc/ssh/ssh_host_ed25519_key ]]; then
        log "UNCHANGED: clone identity already belongs to $ROLE/$NODE_KEY/$NODE_IP"
        return 0
    fi
    run rm -f /etc/machine-id /var/lib/dbus/machine-id
    run rm -f /etc/ssh/ssh_host_ecdsa_key /etc/ssh/ssh_host_ecdsa_key.pub
    run rm -f /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key.pub
    run rm -f /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_rsa_key.pub
    run systemd-machine-id-setup
    run ssh-keygen -A
}

restore_backup() {
    local backup_id=$1
    require_value backup_id "$backup_id"
    local source="$BACKUP_ROOT/$backup_id"
    require_dir "$source"
    run rsync -aHAX --numeric-ids "$source/" /
}

materialize_secret() {
    local destination=$1 placeholder=$2 secret_file=$3
    if [[ ! -f "$destination" ]]; then
        [[ "$DRY_RUN" == "1" ]] && { log "PENDING: secret target not found: $destination"; return 0; }
        die "secret target not found: $destination"
    fi
    grep -Fq "$placeholder" "$destination" || return 0
    if [[ ! -r "$secret_file" ]]; then
        [[ "$DRY_RUN" == "1" ]] && { log "PENDING: required secret file not found: $secret_file"; return 0; }
        die "required secret file not found: $secret_file"
    fi
    local mode
    mode=$(stat -c '%a' "$secret_file")
    [[ "$mode" == 600 || "$mode" == 400 ]] || die "secret file must be mode 0600 or 0400: $secret_file"
    [[ -s "$secret_file" ]] || die "secret file is empty: $secret_file"
    local temp target_mode target_owner target_group
    target_mode=$(stat -c '%a' "$destination")
    target_owner=$(stat -c '%U' "$destination")
    target_group=$(stat -c '%G' "$destination")
    temp=$(mktemp)
    python3 - "$destination" "$placeholder" "$secret_file" "$temp" <<'PY'
import pathlib
import sys

source = pathlib.Path(sys.argv[1])
placeholder = sys.argv[2]
secret_path = pathlib.Path(sys.argv[3])
output = pathlib.Path(sys.argv[4])
secret = secret_path.read_text(encoding="utf-8").rstrip("\r\n")
if not secret or "\n" in secret or "\r" in secret:
    raise SystemExit("secret must be one non-empty line")
if any(char.isspace() or char in "\\\"'" for char in secret):
    raise SystemExit("secret contains whitespace, quote, or backslash unsafe for generated configs")
text = source.read_text(encoding="utf-8")
output.write_text(text.replace(placeholder, secret), encoding="utf-8")
PY
    backup_file "$destination"
    run install -m "$target_mode" -o "$target_owner" -g "$target_group" "$temp" "$destination"
    rm -f "$temp"
}
