#!/usr/bin/env bash
set -euo pipefail

# CyberStrikeAI GitHub one-click upgrade script (Release/Tag)
#
# Default preserves:
# - config.yaml
# - data/
# - venv/ (disabled with --no-venv)
# - tools/ (user extensions; never overwritten by upgrade)
# - roles/
# - skills/

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"

BINARY_NAME="cyberstrike-ai"
CONFIG_FILE="$ROOT_DIR/config.yaml"
DATA_DIR="$ROOT_DIR/data"
VENV_DIR="$ROOT_DIR/venv"
KNOWLEDGE_BASE_DIR="$ROOT_DIR/knowledge_base"

BACKUP_BASE_DIR="$ROOT_DIR/.upgrade-backup"
UPGRADE_TMP_DIR=""

GITHUB_REPO="Ed1s0nZ/CyberStrikeAI"

TAG=""
PRESERVE_VENV=1
STOP_SERVICE=1
FORCE_STOP=0
YES=0

usage() {
  cat <<EOF
Usage:
  ./upgrade.sh [--tag vX.Y.Z] [--no-venv] [--no-stop]
                [--force-stop] [--yes]

Options:
  --tag <tag>          Specify GitHub Release tag (e.g. v1.3.28).
                        If omitted, the script uses the latest release.
  --no-venv             Do not preserve venv/ (Python deps will be re-installed).
  --no-stop             Do not try to stop the running service.
  --force-stop         If no process matching current directory is found, also stop
                        any cyberstrike-ai processes (use with caution).
  --yes                 Do not ask for confirmation.

Description:
  The script backs up config.yaml/data/tools/roles/skills/ (and optionally venv/) to
  .upgrade-backup/
EOF
}

log() { printf "%s\n" "$*"; }
info() { log "[INFO]  $*"; }
warn() { log "[WARN]  $*"; }
err() { log "[ERROR] $*"; }

have_cmd() { command -v "$1" >/dev/null 2>&1; }

http_get() {
  # $1: url
  if have_cmd curl; then
    # If GITHUB_TOKEN is provided, use it for api.github.com to avoid low rate limits.
    if [[ -n "${GITHUB_TOKEN:-}" && "$1" == https://api.github.com/* ]]; then
      # Do not use `-f` so we can parse GitHub error JSON bodies and show `message`.
      curl -sSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$1"
    else
      # Do not use `-f` so we can parse GitHub error JSON bodies and show `message`.
      curl -sSL "$1"
    fi
  elif have_cmd wget; then
    wget -qO- "$1"
  else
    err "curl or wget is required to download GitHub releases. Please install one of them."
    exit 1
  fi
}

stop_service() {
  # Try to stop the service that is running from the current project directory.
  # If nothing is found and --force-stop is enabled, stop all cyberstrike-ai processes.
  if [[ "$STOP_SERVICE" -ne 1 ]]; then
    return 0
  fi

  local pids=""
  if have_cmd pgrep; then
    # Prefer matches where the command line contains the current project path.
    pids="$(pgrep -f "${ROOT_DIR}.*${BINARY_NAME}" || true)"
    if [[ -z "$pids" && "$FORCE_STOP" -eq 1 ]]; then
      warn "No ${BINARY_NAME} process found under the current directory. Will try to force-stop all matching ${BINARY_NAME} processes."
      pids="$(pgrep -f "${BINARY_NAME}" || true)"
    fi
  fi

  if [[ -z "$pids" ]]; then
    info "No ${BINARY_NAME} process detected (or no matching process). Skipping stop step."
    return 0
  fi

  warn "Detected running PID(s): ${pids}"
  for pid in $pids; do
    if kill -0 "$pid" 2>/dev/null; then
      info "Sending SIGTERM to PID=${pid}..."
      kill -TERM "$pid" 2>/dev/null || true
    fi
  done

  # Wait for exit
  local deadline=$((SECONDS + 20))
  while [[ $SECONDS -lt $deadline ]]; do
    local alive=0
    for pid in $pids; do
      if kill -0 "$pid" 2>/dev/null; then
        alive=1
        break
      fi
    done
    if [[ "$alive" -eq 0 ]]; then
      info "Service stopped."
      return 0
    fi
    sleep 1
  done

  warn "Timed out waiting for processes to exit. Still running PID(s): ${pids} (may still hold file handles)."
  return 0
}

backup_dir_tgz() {
  # $1: label, $2: path
  local label="$1"
  local path="$2"
  if [[ -e "$path" ]]; then
    info "Backing up ${label} -> ${BACKUP_BASE_DIR}/$(basename "$path").tgz"
    tar -czf "${BACKUP_BASE_DIR}/$(basename "$path").tgz" -C "$ROOT_DIR" "$(basename "$path")"
  fi
}

backup_config() {
  if [[ -f "$CONFIG_FILE" ]]; then
    cp -a "$CONFIG_FILE" "${BACKUP_BASE_DIR}/config.yaml"
  fi
}

ensure_git_style_env() {
  # No hard requirement; just a sanity check.
  if [[ ! -f "$CONFIG_FILE" ]]; then
    err "Could not find ${CONFIG_FILE}. Please verify you are in the correct project directory."
    exit 1
  fi
}

confirm_or_exit() {
  if [[ "$YES" -eq 1 ]]; then
    return 0
  fi

  if [[ ! -t 0 ]]; then
    err "Non-interactive terminal detected. Please add --yes to continue."
    exit 1
  fi

  warn "About to perform upgrade:"
  info " - Preserve config.yaml: yes"
  info " - Preserve data/: yes"
  if [[ "$PRESERVE_VENV" -eq 1 ]]; then
    info " - Preserve venv/: yes"
  else
    info " - Preserve venv/: no (will remove old venv and re-install deps)"
  fi
  info " - Preserve tools/: yes (always)"
  info " - Preserve roles/skills: yes (always)"
  info " - Stop service: ${STOP_SERVICE}"
  echo ""
  read -r -p "Continue? (y/N) " ans
  if [[ "${ans:-N}" != "y" && "${ans:-N}" != "Y" ]]; then
    err "Cancelled."
    exit 1
  fi
}

resolve_tag() {
  if [[ -n "$TAG" ]]; then
    info "Using specified tag: $TAG"
    return 0
  fi

  local api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest"
  info "Fetching latest Release..."
  local json
  json="$(http_get "$api_url")"
  TAG="$(printf '%s' "$json" | python3 - <<'PY'
import json, sys
data=json.loads(sys.stdin.read() or "{}")
print(data.get("tag_name",""))
PY
)"

  if [[ -z "$TAG" ]]; then
    local msg
    msg="$(printf '%s' "$json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read() or '{}'); print(d.get('message',''))" 2>/dev/null || true)"

    # Fallback: try query releases list (sometimes latest endpoint returns error JSON without tag_name).
    local fallback_url="https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=1"
    info "Fallback to: ${fallback_url}"
    local fallback_json
    fallback_json="$(http_get "$fallback_url" 2>/dev/null || true)"
    local fallback_tag
    fallback_tag="$(printf '%s' "$fallback_json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read() or '[]'); print(d[0].get('tag_name','') if isinstance(d,list) and d else '')" 2>/dev/null || true)"

    if [[ -n "$fallback_tag" ]]; then
      TAG="$fallback_tag"
      info "Latest Release tag (fallback): $TAG"
      return 0
    fi

    local snippet
    snippet="$(printf '%s' "$json" | python3 -c "import sys; s=sys.stdin.read(); print(s[:300].replace('\\n',' '))" 2>/dev/null || true)"

    if [[ -n "$msg" ]]; then
      err "Failed to fetch latest tag: ${msg}"
    else
      err "Failed to fetch latest tag."
    fi
    if [[ -n "$snippet" ]]; then
      err "API response snippet: ${snippet}"
    fi
    err "Please try using --tag to specify the version, or set export GITHUB_TOKEN=\"...\"."
    exit 1
  fi
  info "Latest Release tag: $TAG"
}

update_config_version() {
  # Replace config.yaml's version: ... with the specified tag.
  local new_tag="$1"
  python3 - "$CONFIG_FILE" "$new_tag" <<PY
import re, sys
path=sys.argv[1]
tag=sys.argv[2]
with open(path, "r", encoding="utf-8") as f:
    lines=f.readlines()

out=[]
replaced=False
for line in lines:
    if re.match(r'^\s*version\s*:', line):
        out.append(f'version: "{tag}"\\n')
        replaced=True
    else:
        out.append(line)

if not replaced:
    # If no version field is found, insert at the beginning (near the top).
    out.insert(0, f'version: "{tag}"\\n')

with open(path, "w", encoding="utf-8") as f:
    f.writelines(out)
PY
}

sync_code() {
  local tmp_dir="$1"
  local new_src_dir="$2"

  # rsync sync: overwrite files from the new version and delete removed files.
  # Preserve user data/config (and optional directories).

  if ! have_cmd rsync; then
    err "rsync not found. This script depends on rsync for safe synchronization. Please install it and retry."
    exit 1
  fi

  local -a rsync_excludes
  rsync_excludes+=( "--exclude=.upgrade-backup/" )
  rsync_excludes+=( "--exclude=config.yaml" )
  rsync_excludes+=( "--exclude=data/" )
  rsync_excludes+=( "--exclude=tmp/" )

  if [[ "$PRESERVE_VENV" -eq 1 ]]; then
    rsync_excludes+=( "--exclude=venv/" )
  fi

  # knowledge_base may not be referenced in config, but many users treat it as the knowledge files directory.
  if [[ -d "$KNOWLEDGE_BASE_DIR" ]]; then
    rsync_excludes+=( "--exclude=knowledge_base/" )
  fi

  # User tool extensions: never replace or delete during upgrade.
  rsync_excludes+=( "--exclude=tools/" )
  rsync_excludes+=( "--exclude=roles/" )
  rsync_excludes+=( "--exclude=skills/" )

  # Ensure this upgrade script itself is not deleted.
  rsync_excludes+=( "--exclude=upgrade.sh" )

  # shellcheck disable=SC2068
  info "Syncing code into current directory (preserving data/config; using rsync --delete)..."
  rsync -a --delete \
    ${rsync_excludes[@]} \
    "${new_src_dir}/" "${ROOT_DIR}/"
}

main() {
  ensure_git_style_env

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --tag)
        TAG="${2:-}"
        shift 2
        ;;
      --no-venv)
        PRESERVE_VENV=0
        shift 1
        ;;
      --no-stop)
        STOP_SERVICE=0
        shift 1
        ;;
      --force-stop)
        FORCE_STOP=1
        shift 1
        ;;
      --yes)
        YES=1
        shift 1
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        err "Unknown parameter: $1"
        usage
        exit 1
        ;;
    esac
  done

  confirm_or_exit

  stop_service

  resolve_tag

  local ts
  ts="$(date +"%Y%m%d_%H%M%S")"
  BACKUP_BASE_DIR="${BACKUP_BASE_DIR}/${ts}"
  mkdir -p "$BACKUP_BASE_DIR"

  info "Starting backup into: $BACKUP_BASE_DIR"
  backup_config
  backup_dir_tgz "data" "$DATA_DIR"
  if [[ "$PRESERVE_VENV" -eq 1 ]]; then
    backup_dir_tgz "venv" "$VENV_DIR"
  else
    if [[ -d "$VENV_DIR" ]]; then
      warn "With --no-venv: removing old venv/ (run.sh will re-install Python deps after upgrade)."
      rm -rf "$VENV_DIR"
    fi
  fi
  if [[ -d "$KNOWLEDGE_BASE_DIR" ]]; then
    backup_dir_tgz "knowledge_base" "$KNOWLEDGE_BASE_DIR"
  fi
  if [[ -d "$ROOT_DIR/tools" ]]; then
    backup_dir_tgz "tools" "$ROOT_DIR/tools"
  fi
  if [[ -d "$ROOT_DIR/roles" ]]; then
    backup_dir_tgz "roles" "$ROOT_DIR/roles"
  fi
  if [[ -d "$ROOT_DIR/skills" ]]; then
    backup_dir_tgz "skills" "$ROOT_DIR/skills"
  fi

  UPGRADE_TMP_DIR="$(mktemp -d)"
  trap 'rm -rf "${UPGRADE_TMP_DIR:-}" >/dev/null 2>&1 || true' EXIT

  local tarball="${UPGRADE_TMP_DIR}/source.tar.gz"
  local url="https://github.com/${GITHUB_REPO}/archive/refs/tags/${TAG}.tar.gz"
  info "Downloading source package: ${url}"
  http_get "$url" >"$tarball"

  info "Extracting source package..."
  tar -xzf "$tarball" -C "$UPGRADE_TMP_DIR"

  # GitHub tarball usually creates a top-level directory.
  local extracted_dir
  extracted_dir="$(ls -d "${UPGRADE_TMP_DIR}"/*/ 2>/dev/null | head -n 1 || true)"
  if [[ -z "$extracted_dir" || ! -f "${extracted_dir}/run.sh" ]]; then
    err "run.sh not found in the extracted directory. Please check network/download contents."
    exit 1
  fi

  sync_code "$UPGRADE_TMP_DIR" "$extracted_dir"

  # Update config.yaml version display
  if [[ -f "$CONFIG_FILE" ]]; then
    info "Updating config.yaml version field to: $TAG"
    update_config_version "$TAG"
  fi

  info "Upgrade complete. Starting service..."
  chmod +x ./run.sh
  ./run.sh
}

main "$@"
