#!/bin/bash
# Fast code-only update for BMC Hub (DEV/PROD)
#
# Purpose:
# - Deploy small app code/template/static changes without creating a new tag/package.
# - Keep full release workflow in updateto.sh for dependency/runtime/migration changes.
#
# Safety model:
# - Allowed paths: app/* and main.py
# - Disallowed: migrations, Docker/compose, requirements, env files, deploy scripts.

set -euo pipefail

PODMAN_COMPOSE_FILE="docker-compose.prod.yml"
STATE_DIR=".fast-update"
STATE_FILE="${STATE_DIR}/current_ref"
LAST_BACKUP_FILE="${STATE_DIR}/last_backup"

TARGET_REF=""
DRY_RUN=false
ALLOW_PROD=false
ROLLBACK_ID=""

usage() {
    cat <<'EOF'
Usage:
  ./update_fast.sh --ref <git-ref> [--dry-run] [--allow-prod]
  ./update_fast.sh --rollback <backup-id> [--allow-prod]

Examples:
  ./update_fast.sh --ref main --dry-run
  ./update_fast.sh --ref 08f4097
  ./update_fast.sh --rollback 20260517-142155

Notes:
- Fast mode is ONLY for code/template/static updates in app/* and main.py.
- For migrations, dependencies, Docker/compose, or env changes: use ./updateto.sh.
- On production hosts, --allow-prod is required.
EOF
}

if [ "${EUID:-$(id -u)}" -eq 0 ]; then
    echo "Error: do not run as root. Use the normal rootless podman user."
    exit 1
fi

while [ "$#" -gt 0 ]; do
    case "$1" in
        --ref)
            TARGET_REF="${2:-}"
            shift 2
            ;;
        --dry-run)
            DRY_RUN=true
            shift
            ;;
        --allow-prod)
            ALLOW_PROD=true
            shift
            ;;
        --rollback)
            ROLLBACK_ID="${2:-}"
            shift 2
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        *)
            echo "Error: unknown option: $1"
            usage
            exit 1
            ;;
    esac
done

if [ -n "$ROLLBACK_ID" ] && [ -n "$TARGET_REF" ]; then
    echo "Error: use either --ref or --rollback, not both."
    exit 1
fi

if [ -z "$ROLLBACK_ID" ] && [ -z "$TARGET_REF" ]; then
    echo "Error: missing required option --ref <git-ref> or --rollback <backup-id>."
    usage
    exit 1
fi

if [ ! -f ".env" ]; then
    echo "Error: .env not found in $(pwd)"
    exit 1
fi

if [ ! -f "$PODMAN_COMPOSE_FILE" ]; then
    echo "Error: $PODMAN_COMPOSE_FILE not found in $(pwd)"
    exit 1
fi

load_env_file() {
    local env_file="$1"
    local line=""
    local trimmed=""
    local key=""
    local value=""
    local first_char=""
    local last_char=""

    while IFS= read -r line || [ -n "$line" ]; do
        line="${line%$'\r'}"
        trimmed="${line#"${line%%[![:space:]]*}"}"

        if [ -z "$trimmed" ] || [[ "$trimmed" == \#* ]]; then
            continue
        fi

        if [[ "$line" != *=* ]]; then
            echo "Error: invalid line in .env: $line"
            exit 1
        fi

        key="${line%%=*}"
        value="${line#*=}"

        key="${key#"${key%%[![:space:]]*}"}"
        key="${key%"${key##*[![:space:]]}"}"

        if [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
            echo "Error: invalid .env key: $key"
            exit 1
        fi

        if [ "${#value}" -ge 2 ]; then
            first_char="${value:0:1}"
            last_char="${value: -1}"
            if { [ "$first_char" = '"' ] && [ "$last_char" = '"' ]; } || { [ "$first_char" = "'" ] && [ "$last_char" = "'" ]; }; then
                value="${value:1:${#value}-2}"
            fi
        fi

        export "$key=$value"
    done < "$env_file"
}

load_env_file .env

get_current_ip() {
    local ip=""

    if ip=$(hostname -I 2>/dev/null | awk '{print $1}'); then
        if [ -n "$ip" ]; then
            echo "$ip"
            return 0
        fi
    fi

    if command -v ipconfig >/dev/null 2>&1; then
        ip=$(ipconfig getifaddr en0 2>/dev/null || true)
        if [ -z "$ip" ]; then
            ip=$(ipconfig getifaddr en1 2>/dev/null || true)
        fi
        if [ -n "$ip" ]; then
            echo "$ip"
            return 0
        fi
    fi

    echo "unknown"
}

CURRENT_IP="$(get_current_ip)"
CURRENT_DIR=$(pwd)
IS_PROD=false
if [[ "$CURRENT_IP" == "172.16.31.183" ]] || [[ "$CURRENT_DIR" == "/srv/podman/bmc_hub_v2" ]] || [[ "$CURRENT_DIR" == "/srv/podman/bmc_hub_v1.0" ]]; then
    IS_PROD=true
fi

if [ "$IS_PROD" = true ] && [ "$ALLOW_PROD" != true ]; then
    echo "Error: production host detected. Re-run with --allow-prod to continue."
    exit 1
fi

if [ "$IS_PROD" = true ] && [ "$DRY_RUN" != true ]; then
    echo "Production safety check:"
    echo "  host ip:  $CURRENT_IP"
    echo "  cwd:      $CURRENT_DIR"
    read -r -p "Type FAST-UPDATE to continue: " CONFIRM
    if [ "$CONFIRM" != "FAST-UPDATE" ]; then
        echo "Aborted."
        exit 1
    fi
fi

mkdir -p "$STATE_DIR/backups"

GITEA_BASE="${GITEA_URL:-https://g.bmcnetworks.dk}"
REPO="${GITHUB_REPO:-ct/bmc_hub}"
API_PORT="${API_PORT:-8000}"

url_encode() {
    python3 - "$1" <<'PY'
import sys
from urllib.parse import quote
print(quote(sys.argv[1], safe=""))
PY
}

fetch_url_to_file() {
    local url="$1"
    local out_file="$2"

    if [ -n "${GITHUB_TOKEN:-}" ]; then
        curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" "$url" -o "$out_file"
    else
        curl -fsSL "$url" -o "$out_file"
    fi
}

build_changed_from_compare_json() {
    local compare_file="$1"
    local out_file="$2"

    python3 - "$compare_file" > "$out_file" <<'PY'
import json
import sys

with open(sys.argv[1], "r", encoding="utf-8") as f:
    data = json.load(f)

files = data.get("files") or []
for item in files:
    status = str(item.get("status") or "")
    filename = str(item.get("filename") or "")
    prev = str(item.get("previous_filename") or "")
    print(f"{status}\t{filename}\t{prev}")
PY
}

build_changed_from_local_git() {
    local base_ref="$1"
    local target_ref="$2"
    local out_file="$3"

    if ! git rev-parse --verify "$base_ref" >/dev/null 2>&1; then
        return 1
    fi
    if ! git rev-parse --verify "$target_ref" >/dev/null 2>&1; then
        return 1
    fi

    git diff --name-status "$base_ref".."$target_ref" \
        | awk 'BEGIN{OFS="\t"}
               /^A\t/ {print "added", $2, ""}
               /^M\t/ {print "modified", $2, ""}
               /^D\t/ {print "deleted", $2, ""}
               /^R[0-9]*\t/ {print "renamed", $3, $2}
               /^C[0-9]*\t/ {print "copied", $3, $2}
               /^T\t/ {print "type_changed", $2, ""}
               /^U\t/ {print "unmerged", $2, ""}
               /^X\t/ {print "unknown", $2, ""}
               /^B\t/ {print "broken", $2, ""}' > "$out_file"
}

is_disallowed_path() {
    local path="$1"
    case "$path" in
        migrations/*|Dockerfile|requirements.txt|docker-compose.yml|docker-compose.prod.yml|.env|.env.*|updateto.sh|update_fast.sh|scripts/*)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

is_allowed_path() {
    local path="$1"
    case "$path" in
        app/*|main.py)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

health_check() {
    local attempts=30
    local i=1
    while [ "$i" -le "$attempts" ]; do
        if curl -fsS "http://localhost:${API_PORT}/health" >/dev/null 2>&1; then
            return 0
        fi
        echo "Waiting for API health (${i}/${attempts})"
        sleep 2
        i=$((i + 1))
    done
    return 1
}

rebuild_api_latest() {
    RELEASE_VERSION=latest podman-compose -f "$PODMAN_COMPOSE_FILE" build api
    RELEASE_VERSION=latest podman-compose -f "$PODMAN_COMPOSE_FILE" up -d --no-deps api
}

restore_backup() {
    local backup_id="$1"
    local backup_dir="${STATE_DIR}/backups/${backup_id}"
    local manifest="${backup_dir}/manifest.tsv"

    if [ ! -d "$backup_dir" ]; then
        echo "Error: backup id not found: $backup_id"
        exit 1
    fi
    if [ ! -f "$manifest" ]; then
        echo "Error: manifest missing: $manifest"
        exit 1
    fi

    echo "Restoring backup: ${backup_id}"

    while IFS=$'\t' read -r status path existed; do
        [ -z "$path" ] && continue
        mkdir -p "$(dirname "$path")"
        if [ "$existed" = "1" ]; then
            cp "${backup_dir}/original/${path}" "$path"
        else
            rm -f "$path"
        fi
    done < "$manifest"

    rebuild_api_latest
    if ! health_check; then
        echo "Error: health check failed after rollback."
        exit 1
    fi

    if [ -f "${backup_dir}/meta.env" ]; then
        # shellcheck disable=SC1090
        source "${backup_dir}/meta.env"
        if [ -n "${BASE_REF:-}" ]; then
            echo "$BASE_REF" > "$STATE_FILE"
        fi
    fi

    echo "Rollback completed: ${backup_id}"
}

if [ -n "$ROLLBACK_ID" ]; then
    restore_backup "$ROLLBACK_ID"
    exit 0
fi

BASE_REF=""
if [ -f "$STATE_FILE" ]; then
    BASE_REF="$(cat "$STATE_FILE")"
elif [ -n "${RELEASE_VERSION:-}" ] && [ "${RELEASE_VERSION}" != "latest" ]; then
    BASE_REF="$RELEASE_VERSION"
elif [ -n "${FAST_BASE_REF:-}" ]; then
    BASE_REF="$FAST_BASE_REF"
else
    echo "Error: cannot determine base ref."
    echo "Set RELEASE_VERSION in .env, or create ${STATE_FILE}, or provide FAST_BASE_REF env var."
    exit 1
fi

echo "Fast update"
echo "  base ref:   $BASE_REF"
echo "  target ref: $TARGET_REF"

BASE_ENC="$(url_encode "$BASE_REF")"
TARGET_ENC="$(url_encode "$TARGET_REF")"
COMPARE_URL="${GITEA_BASE}/api/v1/repos/${REPO}/compare/${BASE_ENC}...${TARGET_ENC}"

TMP_COMPARE="$(mktemp)"
TMP_CHANGED="$(mktemp)"
trap 'rm -f "$TMP_COMPARE" "$TMP_CHANGED"' EXIT

COMPARE_SOURCE=""
if fetch_url_to_file "$COMPARE_URL" "$TMP_COMPARE"; then
    build_changed_from_compare_json "$TMP_COMPARE" "$TMP_CHANGED"
    COMPARE_SOURCE="gitea-api"
else
    if [ -d ".git" ] && build_changed_from_local_git "$BASE_REF" "$TARGET_REF" "$TMP_CHANGED"; then
        COMPARE_SOURCE="local-git"
        echo "Warning: compare API unavailable, using local git diff fallback."
    else
        echo "Error: could not fetch compare data from: $COMPARE_URL"
        echo "Hint: ensure GITHUB_TOKEN is set, refs exist, or run in a git clone where both refs are available."
        exit 1
    fi
fi

echo "Compare source: ${COMPARE_SOURCE}"

if [ ! -s "$TMP_CHANGED" ]; then
    echo "No changed files between ${BASE_REF} and ${TARGET_REF}."
    exit 0
fi

declare -a DISALLOWED_FILES=()
declare -a NOT_ALLOWED_FILES=()
declare -a UNSUPPORTED_STATUS=()

while IFS=$'\t' read -r status file_path prev_path; do
    [ -z "$file_path" ] && continue

    case "$status" in
        added|modified)
            ;;
        *)
            UNSUPPORTED_STATUS+=("${status}: ${file_path}")
            continue
            ;;
    esac

    if is_disallowed_path "$file_path"; then
        DISALLOWED_FILES+=("$file_path")
        continue
    fi

    if ! is_allowed_path "$file_path"; then
        NOT_ALLOWED_FILES+=("$file_path")
        continue
    fi

done < "$TMP_CHANGED"

if [ "${#UNSUPPORTED_STATUS[@]}" -gt 0 ]; then
    echo "Error: unsupported change types in fast mode:"
    for item in "${UNSUPPORTED_STATUS[@]}"; do
        echo "  - $item"
    done
    echo "Use ./updateto.sh for these changes."
    exit 1
fi

if [ "${#DISALLOWED_FILES[@]}" -gt 0 ]; then
    echo "Error: disallowed files changed for fast mode:"
    for file_path in "${DISALLOWED_FILES[@]}"; do
        echo "  - $file_path"
    done
    echo "Use ./updateto.sh for this update."
    exit 1
fi

if [ "${#NOT_ALLOWED_FILES[@]}" -gt 0 ]; then
    echo "Error: files outside fast-mode scope changed:"
    for file_path in "${NOT_ALLOWED_FILES[@]}"; do
        echo "  - $file_path"
    done
    echo "Fast mode only allows app/* and main.py. Use ./updateto.sh instead."
    exit 1
fi

echo "Changed files in fast scope:"
while IFS=$'\t' read -r status file_path prev_path; do
    [ -z "$file_path" ] && continue
    echo "  - ${status}: ${file_path}"
done < "$TMP_CHANGED"

if [ "$DRY_RUN" = true ]; then
    echo "Dry-run complete. No files updated."
    exit 0
fi

BACKUP_ID="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="${STATE_DIR}/backups/${BACKUP_ID}"
MANIFEST="${BACKUP_DIR}/manifest.tsv"
mkdir -p "${BACKUP_DIR}/original"

while IFS=$'\t' read -r status file_path prev_path; do
    [ -z "$file_path" ] && continue

    mkdir -p "$(dirname "$file_path")"

    existed=0
    if [ -f "$file_path" ]; then
        mkdir -p "${BACKUP_DIR}/original/$(dirname "$file_path")"
        cp "$file_path" "${BACKUP_DIR}/original/${file_path}"
        existed=1
    fi

    printf "%s\t%s\t%s\n" "$status" "$file_path" "$existed" >> "$MANIFEST"

    file_enc="$(url_encode "$file_path")"
    raw_url="${GITEA_BASE}/api/v1/repos/${REPO}/raw/${file_enc}?ref=${TARGET_ENC}"
    tmp_file="${file_path}.tmp.fast"

    fetch_url_to_file "$raw_url" "$tmp_file"
    mv "$tmp_file" "$file_path"
done < "$TMP_CHANGED"

cat > "${BACKUP_DIR}/meta.env" <<EOF
BASE_REF=${BASE_REF}
TARGET_REF=${TARGET_REF}
BACKUP_ID=${BACKUP_ID}
EOF

echo "Building and restarting API container only"
rebuild_api_latest

if ! health_check; then
    echo "Error: health check failed after fast update. Starting automatic rollback."
    restore_backup "$BACKUP_ID"
    echo "Rollback completed. Fast update aborted."
    exit 1
fi

echo "$TARGET_REF" > "$STATE_FILE"
echo "$BACKUP_ID" > "$LAST_BACKUP_FILE"

echo "Fast update complete"
echo "  backup id:  $BACKUP_ID"
echo "  deployed:   $TARGET_REF"
echo "  state file: $STATE_FILE"