#!/usr/bin/env bash

set -Eeuo pipefail
umask 027

DRY_RUN=${DRY_RUN:-0}
RELEASE_TEST_MODE=${RELEASE_TEST_MODE:-0}
RELEASE_SUCCESS=0
RELEASE_SWITCHED=0
RELEASE_HAD_TARGET=0
RELEASE_STAGE=
RELEASE_BACKUP=

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

release_die() {
    release_log "ERROR: $*"
    return 1
}

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

release_cleanup() {
    local status=$?
    trap - EXIT
    if [[ "$status" -ne 0 && "$RELEASE_SWITCHED" == 1 ]]; then
        local failed="${RELEASE_STATE_DIR}/failed-${RELEASE_COMPONENT}-${RELEASE_VERSION}-${RELEASE_TIMESTAMP}-$$"
        release_log "deployment failed; restoring previous release"
        [[ ! -e "$failed" && -e "$RELEASE_TARGET" ]] && mv "$RELEASE_TARGET" "$failed" || true
        if [[ "$RELEASE_HAD_TARGET" == 1 && -d "$RELEASE_BACKUP" ]]; then
            mv "$RELEASE_BACKUP" "$RELEASE_TARGET" || true
        fi
    fi
    if [[ -n "$RELEASE_STAGE" && -d "$RELEASE_STAGE" ]]; then
        rm -rf "$RELEASE_STAGE"
    fi
    if [[ "$status" -ne 0 ]]; then
        release_log "FAILED: component=$RELEASE_COMPONENT version=$RELEASE_VERSION status=$status"
    fi
    exit "$status"
}

release_init() {
    RELEASE_COMPONENT=$1
    RELEASE_VERSION=$2
    RELEASE_PACKAGE_PREFIX=$3
    RELEASE_TARGET=$4
    RELEASE_STATE_DIR=$5

    [[ "$RELEASE_VERSION" =~ ^[0-9A-Za-z._-]+$ ]] || release_die 'version contains unsupported characters'
    [[ -n "${RELEASE_SCRIPT_DIR:-}" ]] || release_die 'RELEASE_SCRIPT_DIR is not set by wrapper'

    PACKAGE_DIR=${PACKAGE_DIR:-$(cd "$RELEASE_SCRIPT_DIR/.." && pwd)}
    RELEASE_CHECKSUM_FILE=${RELEASE_CHECKSUM_FILE:-$PACKAGE_DIR/release-packages.sha256}
    RELEASE_PACKAGE="${RELEASE_PACKAGE_PREFIX}_${RELEASE_VERSION}.tgz"
    RELEASE_PACKAGE_PATH="$PACKAGE_DIR/$RELEASE_PACKAGE"
    RELEASE_TIMESTAMP=$(date '+%Y%m%d%H%M%S')

    [[ "$DRY_RUN" == 1 || "$RELEASE_TEST_MODE" == 1 || $(id -u) -eq 0 ]] || release_die 'live deployment requires root'
    release_require_command awk
    release_require_command grep
    release_require_command sha256sum
    release_require_command tar
    [[ -f "$RELEASE_PACKAGE_PATH" ]] || release_die "local package not found: $RELEASE_PACKAGE_PATH"
    [[ -f "$RELEASE_CHECKSUM_FILE" ]] || release_die "checksum manifest not found: $RELEASE_CHECKSUM_FILE"

    local expected actual matches
    matches=$(awk -v file="$RELEASE_PACKAGE" '$2 == file {count++} END {print count+0}' "$RELEASE_CHECKSUM_FILE")
    [[ "$matches" -eq 1 ]] || release_die "checksum manifest must contain exactly one entry for $RELEASE_PACKAGE"
    expected=$(awk -v file="$RELEASE_PACKAGE" '$2 == file {print $1}' "$RELEASE_CHECKSUM_FILE")
    actual=$(sha256sum "$RELEASE_PACKAGE_PATH" | awk '{print $1}')
    [[ "$actual" == "$expected" ]] || release_die "SHA256 mismatch for $RELEASE_PACKAGE"
    tar -tzf "$RELEASE_PACKAGE_PATH" >/dev/null
    if tar -tzf "$RELEASE_PACKAGE_PATH" | awk '/^\// || /(^|\/)\.\.($|\/)/ {bad=1} END {exit !bad}'; then
        release_die "unsafe path found in $RELEASE_PACKAGE"
    fi
    if tar -tvzf "$RELEASE_PACKAGE_PATH" | awk '$1 !~ /^[-d]/ {bad=1} END {exit !bad}'; then
        release_die "links or special files are not allowed in $RELEASE_PACKAGE"
    fi

    if [[ "$DRY_RUN" == 1 ]]; then
        RELEASE_STAGE=$(mktemp -d "${TMPDIR:-/tmp}/sanquan-${RELEASE_COMPONENT}-${RELEASE_VERSION}.XXXXXX")
    else
        mkdir -p "$RELEASE_STATE_DIR" "$(dirname "$RELEASE_TARGET")"
        if [[ "$RELEASE_TEST_MODE" != 1 ]]; then
            release_require_command flock
            local lock_dir=${RELEASE_LOCK_DIR:-/run/lock/zhct}
            mkdir -p "$lock_dir"
            exec 9>"$lock_dir/deploy-${RELEASE_COMPONENT}.lock"
            flock -n 9 || release_die "another $RELEASE_COMPONENT deployment is running"
        fi
        RELEASE_STAGE="$RELEASE_STATE_DIR/.staging-${RELEASE_COMPONENT}-${RELEASE_VERSION}-${RELEASE_TIMESTAMP}-$$"
        [[ ! -e "$RELEASE_STAGE" ]] || release_die "staging path already exists: $RELEASE_STAGE"
        mkdir -p "$RELEASE_STAGE"
    fi
    RELEASE_BACKUP="$RELEASE_STATE_DIR/${RELEASE_COMPONENT}-before-${RELEASE_VERSION}-${RELEASE_TIMESTAMP}-$$"
    trap release_cleanup EXIT
    release_log "validated local package $RELEASE_PACKAGE sha256=$actual"
}

release_extract() {
    tar --no-same-owner --no-same-permissions -xzf "$RELEASE_PACKAGE_PATH" -C "$RELEASE_STAGE"
    [[ -n "$(find "$RELEASE_STAGE" -mindepth 1 -maxdepth 1 -print -quit)" ]] || release_die 'package extracted no files'
}

release_require_stage_file() {
    [[ -f "$RELEASE_STAGE/$1" ]] || release_die "required package file missing: $1"
}

release_preserve_path() {
    local relative=$1
    [[ "$relative" != /* && "$relative" != *..* ]] || release_die "unsafe persistent path: $relative"
    [[ "$DRY_RUN" != 1 && -e "$RELEASE_TARGET/$relative" ]] || return 0
    rm -rf "$RELEASE_STAGE/$relative"
    mkdir -p "$RELEASE_STAGE/$relative"
    cp -a "$RELEASE_TARGET/$relative/." "$RELEASE_STAGE/$relative/"
    release_log "preserved $relative"
}

release_ensure_stage_dir() {
    mkdir -p "$RELEASE_STAGE/$1"
}

release_write_metadata() {
    local sha
    sha=$(sha256sum "$RELEASE_PACKAGE_PATH" | awk '{print $1}')
    {
        printf 'component=%s\n' "$RELEASE_COMPONENT"
        printf 'version=%s\n' "$RELEASE_VERSION"
        printf 'package=%s\n' "$RELEASE_PACKAGE"
        printf 'sha256=%s\n' "$sha"
        printf 'deployed_at=%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z')"
    } >"$RELEASE_STAGE/.release-meta"
}

release_set_permissions() {
    find "$RELEASE_STAGE" -type d -exec chmod 0750 {} +
    find "$RELEASE_STAGE" -type f -exec chmod 0640 {} +
    local member
    while IFS= read -r member; do
        member=${member#./}
        [[ -n "$member" && "$member" != /* && "$member" != *..* ]] || release_die "unsafe executable path: $member"
        [[ -f "$RELEASE_STAGE/$member" ]] && chmod 0750 "$RELEASE_STAGE/$member"
    done < <(tar -tvzf "$RELEASE_PACKAGE_PATH" | awk '$1 ~ /^-/ && $1 ~ /x/ {print $NF}')
    if [[ "$DRY_RUN" != 1 && "$RELEASE_TEST_MODE" != 1 ]]; then
        chown -R "${RELEASE_OWNER:-www}:${RELEASE_GROUP:-www}" "$RELEASE_STAGE"
    fi
}

release_switch() {
    if [[ "$DRY_RUN" == 1 ]]; then
        release_log "DRY-RUN: package validated and staged; would replace $RELEASE_TARGET"
        return 0
    fi
    if [[ -e "$RELEASE_TARGET" ]]; then
        [[ ! -e "$RELEASE_BACKUP" ]] || release_die "backup path already exists: $RELEASE_BACKUP"
        mv "$RELEASE_TARGET" "$RELEASE_BACKUP"
        RELEASE_HAD_TARGET=1
    fi
    if ! mv "$RELEASE_STAGE" "$RELEASE_TARGET"; then
        [[ "$RELEASE_HAD_TARGET" == 1 ]] && mv "$RELEASE_BACKUP" "$RELEASE_TARGET" || true
        release_die 'atomic release switch failed'
    fi
    RELEASE_STAGE=
    RELEASE_SWITCHED=1
    if [[ "$RELEASE_TEST_MODE" == 1 && "${RELEASE_TEST_FAIL_AFTER_SWITCH:-0}" == 1 ]]; then
        release_die 'injected post-switch failure for rollback verification'
    fi
}

release_reload_fpm() {
    local unit=$1
    [[ "$DRY_RUN" != 1 && "$RELEASE_TEST_MODE" != 1 ]] || return 0
    if systemctl is-active --quiet "$unit"; then
        systemctl reload "$unit"
        systemctl is-active --quiet "$unit"
        release_log "reloaded $unit"
    else
        release_log "$unit is inactive; reload deferred"
    fi
}

release_restart_supervisor_groups() {
    [[ "$DRY_RUN" != 1 && "$RELEASE_TEST_MODE" != 1 ]] || return 0
    systemctl is-active --quiet supervisor.service || { release_log 'Supervisor is inactive; worker restart deferred to Q-263'; return 0; }
    local group output
    for group in "$@"; do
        /usr/local/bin/supervisorctl -c /etc/supervisord.conf restart "$group:*"
        output=$(/usr/local/bin/supervisorctl -c /etc/supervisord.conf status "$group:*")
        awk 'NF && $2 != "RUNNING" {bad=1} END {exit bad}' <<<"$output" || release_die "Supervisor group not RUNNING after restart: $group"
    done
}

release_apply_file_policy() {
    [[ "$DRY_RUN" != 1 && "$RELEASE_TEST_MODE" != 1 ]] || return 0
    [[ -x /usr/local/sbin/apply-file-role-policy.sh && -r /etc/zhct/node.env ]] || return 0
    local node
    node=$(awk -F= '$1 == "NODE_KEY" {print $2}' /etc/zhct/node.env)
    [[ "$node" == app01 || "$node" == app02 ]] || release_die 'invalid app node identity for file policy'
    /usr/local/sbin/apply-file-role-policy.sh --node "$node"
}

release_finish() {
    RELEASE_SUCCESS=1
    if [[ "$DRY_RUN" == 1 ]]; then
        release_log "PASS dry-run: component=$RELEASE_COMPONENT version=$RELEASE_VERSION"
    else
        release_log "PASS deployed: component=$RELEASE_COMPONENT version=$RELEASE_VERSION target=$RELEASE_TARGET backup=${RELEASE_BACKUP:-none}"
    fi
}
