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

script_path="$(readlink -f "${BASH_SOURCE[0]}")"
app_root="$(cd "$(dirname "$script_path")/.." && pwd)"
if [[ -f "$app_root/Xu4.Support.sln" ]]; then
  install_mode="source"
  overlay_content_root="$app_root/samples/overlay"
  default_config="$app_root/config/xu4-support.env"
  default_runtime="$app_root/var/operator"
  voice_script="$app_root/scripts/prototype-voice-acting.py"
  redactor="$app_root/scripts/redact-log.py"
else
  install_mode="installed"
  overlay_content_root="$app_root/share/overlay"
  config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
  state_home="${XDG_STATE_HOME:-$HOME/.local/state}"
  default_config="$config_home/xu4-support/config.env"
  default_runtime="$state_home/xu4-support"
  if [[ -f "$app_root/install-layout.env" ]]; then
    while IFS= read -r layout_line || [[ -n "$layout_line" ]]; do
      case "$layout_line" in
        XU4_SUPPORT_CONFIG=*) default_config="${layout_line#*=}" ;;
        XU4_OPERATOR_RUNTIME_DIR=*) default_runtime="${layout_line#*=}" ;;
      esac
    done <"$app_root/install-layout.env"
  fi
  voice_script="$app_root/libexec/prototype-voice-acting.py"
  redactor="$app_root/libexec/redact-log.py"
fi

config_path="${XU4_SUPPORT_CONFIG:-$default_config}"
runtime_dir="${XU4_OPERATOR_RUNTIME_DIR:-$default_runtime}"
log_dir="$runtime_dir/logs"
pid_dir="$runtime_dir/run"

usage() {
  cat <<'EOF'
Usage: xu4-support [--config PATH] COMMAND [OPTIONS]

Commands:
  init                          Create user configuration and state safely.
  start                         Start hub, overlay, bridge, and optional voice.
  stop                          Stop every xu4-support process.
  status                        Show process and hub health.
  smoke                         Verify hub, telemetry input, and overlay mode.
  logs [COMPONENT] [--follow]   Show redacted logs (all, hub, overlay, bridge, voice).
  diagnostics [OUTPUT]          Create a sanitized diagnostics .tar.gz.
  version                       Show installed/source version information.
EOF
}

fail() {
  printf 'xu4-support: %s\n' "$*" >&2
  exit 2
}

if [[ "${1:-}" == "--config" ]]; then
  [[ -n "${2:-}" ]] || fail "--config requires a path"
  config_path="$2"
  shift 2
fi

command="${1:-}"
[[ -n "$command" ]] || {
  usage >&2
  exit 2
}
shift || true

case "$command" in
  init|start|stop|status|smoke|logs|diagnostics|version) ;;
  *)
    usage >&2
    exit 2
    ;;
esac

resolve_path() {
  local variable="$1"
  local value="${!variable}"
  if [[ "$value" != /* ]]; then
    printf -v "$variable" '%s/%s' "$app_root" "$value"
  fi
}

load_config() {
  [[ -f "$config_path" ]] || fail "configuration does not exist: $config_path"
  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line%$'\r'}"
    [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
    [[ "$line" == *=* ]] || fail "invalid configuration line"
    key="${line%%=*}"
    value="${line#*=}"
    [[ "$key" =~ ^[A-Z][A-Z0-9_]*$ ]] || fail "invalid configuration key: $key"
    export "$key=$value"
  done < "$config_path"

  : "${XU4_TELEMETRY_PATH:?XU4_TELEMETRY_PATH is required}"
  : "${XU4_HUB_BASE_URL:?XU4_HUB_BASE_URL is required}"
  : "${XU4_BRIDGE_WEB_KEY_FILE:?XU4_BRIDGE_WEB_KEY_FILE is required}"
  : "${XU4_OVERLAY_URL:?XU4_OVERLAY_URL is required}"
  XU4_KNOWLEDGE_MODE="${XU4_KNOWLEDGE_MODE:-DiscoveredOnly}"
  XU4_VOICE_ENABLED="${XU4_VOICE_ENABLED:-false}"
  XU4_VOICE_STATE_PATH="${XU4_VOICE_STATE_PATH:-$runtime_dir/voice-state.json}"
  XU4_BUILD_ON_START="${XU4_BUILD_ON_START:-true}"

  [[ "$XU4_KNOWLEDGE_MODE" =~ ^(DiscoveredOnly|Nudge|Oracle)$ ]] ||
    fail "XU4_KNOWLEDGE_MODE must be DiscoveredOnly, Nudge, or Oracle"
  [[ "$XU4_VOICE_ENABLED" =~ ^(true|false)$ ]] ||
    fail "XU4_VOICE_ENABLED must be true or false"
  [[ "$XU4_BUILD_ON_START" =~ ^(true|false)$ ]] ||
    fail "XU4_BUILD_ON_START must be true or false"

  resolve_path XU4_TELEMETRY_PATH
  resolve_path XU4_BRIDGE_WEB_KEY_FILE
  resolve_path XU4_VOICE_STATE_PATH
}

pid_running() {
  local pid_file="$1"
  [[ -f "$pid_file" ]] || return 1
  local pid
  pid="$(<"$pid_file")"
  [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null
}

component_pid_files() {
  local name="$1"
  printf '%s\n' "$pid_dir/$name.pid" "$runtime_dir/$name.pid"
}

component_running() {
  local name="$1"
  local pid_file
  while IFS= read -r pid_file; do
    pid_running "$pid_file" && return 0
  done < <(component_pid_files "$name")
  return 1
}

start_process() {
  local name="$1"
  shift
  local pid_file="$pid_dir/$name.pid"
  component_running "$name" && fail "$name is already running"
  nohup "$@" >"$log_dir/$name.log" 2>&1 &
  printf '%s\n' "$!" >"$pid_file"
}

stop_process() {
  local name="$1"
  local pid_file pid
  while IFS= read -r pid_file; do
    if pid_running "$pid_file"; then
      pid="$(<"$pid_file")"
      kill "$pid"
      for _ in $(seq 1 30); do
        kill -0 "$pid" 2>/dev/null || break
        sleep 0.1
      done
      kill -0 "$pid" 2>/dev/null &&
        fail "$name process $pid did not stop; preserving $pid_file"
    fi
    rm -f "$pid_file"
  done < <(component_pid_files "$name")
}

# Every hub request is bounded. Without --max-time, a socket that accepts a
# connection but never answers -- another process already holding the port, or
# a wedged hub -- makes curl block forever, so the retry bound below never
# advances and `start` hangs indefinitely instead of reporting an unhealthy hub.
XU4_CURL_MAX_TIME="${XU4_CURL_MAX_TIME:-5}"

hub_curl() {
  curl -fsS --max-time "$XU4_CURL_MAX_TIME" "$@"
}

# Bound the readiness wait by wall-clock time rather than attempt count, since
# each attempt can now cost up to XU4_CURL_MAX_TIME. Give up immediately if the
# hub process has already exited -- the usual cause is the port being taken, and
# there is nothing to wait for once it is gone.
XU4_HUB_READY_TIMEOUT="${XU4_HUB_READY_TIMEOUT:-30}"

wait_for_hub() {
  local deadline=$((SECONDS + XU4_HUB_READY_TIMEOUT))
  while (( SECONDS < deadline )); do
    hub_curl "$XU4_HUB_BASE_URL/healthz" >/dev/null 2>&1 && return 0
    component_running hub || return 1
    sleep 0.5
  done
  return 1
}

require_installed_runtime() {
  command -v dotnet >/dev/null || fail "dotnet is required"
  # Capture once rather than piping into grep: under `pipefail`, `grep -q`
  # exits at the first match and can kill the writer with SIGPIPE before it
  # finishes, failing the pipeline and reporting a runtime that is installed
  # as missing.
  local installed_runtimes
  installed_runtimes="$(dotnet --list-runtimes)"
  grep -Eq '^Microsoft\.NETCore\.App 10\.' <<<"$installed_runtimes" ||
    fail ".NET 10 runtime is required"
  grep -Eq '^Microsoft\.AspNetCore\.App 10\.' <<<"$installed_runtimes" ||
    fail "ASP.NET Core 10 runtime is required"
}

hub_command() {
  if [[ "$install_mode" == "source" ]]; then
    printf '%s\0' dotnet run --no-build --project \
      "$app_root/src/Xu4.SignalR.SampleHub" --urls "$XU4_HUB_BASE_URL"
  else
    printf '%s\0' dotnet "$app_root/lib/hub/Xu4.SignalR.SampleHub.dll" \
      --urls "$XU4_HUB_BASE_URL"
  fi
}

overlay_command() {
  if [[ "$install_mode" == "source" ]]; then
    printf '%s\0' dotnet run --no-build --project \
      "$app_root/src/Xu4.Overlay.Sample" --urls "$XU4_OVERLAY_URL"
  else
    printf '%s\0' dotnet "$app_root/lib/overlay/Xu4.Overlay.Sample.dll" \
      --urls "$XU4_OVERLAY_URL"
  fi
}

bridge_command() {
  if [[ "$install_mode" == "source" ]]; then
    printf '%s\0' dotnet run --no-build --project \
      "$app_root/src/Xu4.SignalR.BridgeRunner"
  else
    printf '%s\0' dotnet "$app_root/lib/bridge/Xu4.SignalR.BridgeRunner.dll"
  fi
}

show_version() {
  if [[ -f "$app_root/VERSION" ]]; then
    printf 'xu4-support %s (%s)\n' "$(<"$app_root/VERSION")" "$install_mode"
  else
    printf 'xu4-support source (%s)\n' "$install_mode"
  fi
}

initialize_user() {
  local config_dir web_key_file telemetry_file
  config_dir="$(dirname -- "$config_path")"
  web_key_file="$config_dir/web-key"
  telemetry_file="$runtime_dir/avatar-events.jsonl"

  for directory in "$config_dir" "$runtime_dir"; do
    [[ "$directory" == /* ]] || fail "init paths must be absolute: $directory"
    [[ "$directory" != "/" ]] || fail "unsafe init path: $directory"
    [[ ! -L "$directory" ]] || fail "init directory must not be a symlink: $directory"
    [[ ! -e "$directory" || -d "$directory" ]] ||
      fail "init path is not a directory: $directory"
  done

  for file in "$config_path" "$web_key_file" "$telemetry_file"; do
    [[ ! -L "$file" ]] || fail "init file must not be a symlink: $file"
    [[ ! -e "$file" || -f "$file" ]] ||
      fail "init path is not a regular file: $file"
  done

  umask 077
  mkdir -p -- "$config_dir" "$runtime_dir/logs" "$runtime_dir/run"

  if [[ ! -e "$web_key_file" ]]; then
    python3 - "$web_key_file" <<'PY'
import os
import secrets
import sys

path = sys.argv[1]
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as stream:
    stream.write(secrets.token_urlsafe(32) + "\n")
PY
  fi
  chmod 0600 -- "$web_key_file"

  if [[ ! -e "$telemetry_file" ]]; then
    python3 - "$telemetry_file" <<'PY'
import os
import sys

fd = os.open(sys.argv[1], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
os.close(fd)
PY
  fi

  if [[ ! -e "$config_path" ]]; then
    python3 - "$config_path" "$telemetry_file" "$web_key_file" "$runtime_dir" <<'PY'
import os
import sys

path, telemetry, web_key, state = sys.argv[1:]
content = f"""# Generated by xu4-support init. Edit locally; do not commit credentials.
XU4_TELEMETRY_PATH={telemetry}
XU4_HUB_BASE_URL=http://127.0.0.1:5084
XU4_BRIDGE_WEB_KEY_FILE={web_key}
XU4_KNOWLEDGE_MODE=DiscoveredOnly
XU4_OVERLAY_URL=http://127.0.0.1:5085
XU4_VOICE_ENABLED=false
XU4_VOICE_STATE_PATH={state}/voice-state.json
XU4_BUILD_ON_START=false
"""
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as stream:
    stream.write(content)
PY
  fi

  printf 'xu4-support initialized\n'
  printf 'Configuration: %s\n' "$config_path"
  printf 'State: %s\n' "$runtime_dir"
  printf 'Existing configuration, WebKey, and telemetry were preserved.\n'
}

if [[ "$command" == "version" ]]; then
  show_version
  exit 0
fi

if [[ "$command" == "init" ]]; then
  [[ $# -eq 0 ]] || fail "init does not accept arguments"
  initialize_user
  exit 0
fi

if [[ "$command" == "stop" ]]; then
  stop_process voice
  stop_process bridge
  stop_process overlay
  stop_process hub
  printf 'xu4-support stopped; gameplay and engine telemetry remain independent\n'
  exit 0
fi

load_config
mkdir -p "$runtime_dir" "$log_dir" "$pid_dir"

case "$command" in
  start)
    require_installed_runtime
    [[ -r "$XU4_TELEMETRY_PATH" ]] ||
      fail "telemetry input is not readable: $XU4_TELEMETRY_PATH"
    [[ -r "$XU4_BRIDGE_WEB_KEY_FILE" ]] ||
      fail "WebKey file is not readable: $XU4_BRIDGE_WEB_KEY_FILE"
    IFS= read -r web_key < "$XU4_BRIDGE_WEB_KEY_FILE"
    [[ -n "$web_key" ]] || fail "WebKey file is empty"

    if [[ "$install_mode" == "source" && "$XU4_BUILD_ON_START" == "true" ]]; then
      dotnet build "$app_root/Xu4.Support.sln"
    fi

    mapfile -d '' -t hub < <(hub_command)
    start_process hub env "Xu4__Bridge__WebKey=$web_key" "${hub[@]}"
    if ! wait_for_hub; then
      stop_process hub
      fail "hub did not become healthy; inspect $log_dir/hub.log"
    fi

    mapfile -d '' -t overlay < <(overlay_command)
    start_process overlay env \
      "XU4_OVERLAY_CONTENT_ROOT=$overlay_content_root" \
      "${overlay[@]}"
    mapfile -d '' -t bridge < <(bridge_command)
    start_process bridge env \
      "XU4_BRIDGE_HUB_URL=$XU4_HUB_BASE_URL/hub" \
      "XU4_BRIDGE_WEB_KEY=$web_key" \
      "XU4_TELEMETRY_PATH=$XU4_TELEMETRY_PATH" \
      "XU4_BRIDGE_BUFFER_PATH=$runtime_dir/bridge-buffer.jsonl" \
      "${bridge[@]}"

    if [[ "$XU4_VOICE_ENABLED" == "true" ]]; then
      start_process voice "$voice_script" \
        --input "$XU4_TELEMETRY_PATH" \
        --follow \
        --state-file "$XU4_VOICE_STATE_PATH"
    fi

    printf 'xu4-support started\n'
    printf 'Hub: %s\n' "$XU4_HUB_BASE_URL"
    printf 'Overlay bind: %s\n' "$XU4_OVERLAY_URL"
    printf 'Open locally: http://127.0.0.1:%s/?stateUrl=%s/overlay-state?mode=%s\n' \
      "${XU4_OVERLAY_URL##*:}" "$XU4_HUB_BASE_URL" "$XU4_KNOWLEDGE_MODE"
    printf 'Runtime logs: %s\n' "$log_dir"
    ;;
  status)
    failed=0
    for name in hub overlay bridge; do
      if component_running "$name"; then
        printf '%s: running\n' "$name"
      else
        printf '%s: stopped\n' "$name"
        failed=1
      fi
    done
    if [[ "$XU4_VOICE_ENABLED" == "true" ]]; then
      if component_running voice; then
        printf 'voice: running\n'
      else
        printf 'voice: stopped\n'
        failed=1
      fi
    else
      printf 'voice: disabled\n'
    fi
    hub_curl "$XU4_HUB_BASE_URL/healthz" >/dev/null 2>&1 &&
      printf 'hub health: ok\n' ||
      { printf 'hub health: unavailable\n'; failed=1; }
    exit "$failed"
    ;;
  smoke)
    hub_curl "$XU4_HUB_BASE_URL/healthz" >/dev/null
    snapshot="$(hub_curl "$XU4_HUB_BASE_URL/overlay-state?mode=$XU4_KNOWLEDGE_MODE")"
    [[ "$snapshot" == *"\"spoilerMode\":\"$XU4_KNOWLEDGE_MODE\""* ]] ||
      fail "overlay snapshot did not preserve $XU4_KNOWLEDGE_MODE"
    [[ -r "$XU4_TELEMETRY_PATH" ]] || fail "telemetry input became unreadable"
    event_count="$(hub_curl "$XU4_HUB_BASE_URL/events" |
      python3 -c 'import json,sys; print(len(json.load(sys.stdin)))')"
    printf 'smoke: hub healthy, telemetry readable, overlay mode %s, events %s\n' \
      "$XU4_KNOWLEDGE_MODE" "$event_count"
    ;;
  logs)
    component="all"
    follow="false"
    lines="100"
    while [[ $# -gt 0 ]]; do
      case "$1" in
        --follow) follow="true" ;;
        --lines)
          [[ -n "${2:-}" ]] || fail "--lines requires a number"
          lines="$2"
          shift
          ;;
        all|hub|overlay|bridge|voice) component="$1" ;;
        *) fail "unknown logs option: $1" ;;
      esac
      shift
    done
    [[ "$lines" =~ ^[1-9][0-9]*$ ]] || fail "--lines must be positive"
    files=()
    for name in hub overlay bridge voice; do
      [[ "$component" == "all" || "$component" == "$name" ]] || continue
      [[ -f "$log_dir/$name.log" ]] && files+=("$log_dir/$name.log")
    done
    [[ ${#files[@]} -gt 0 ]] || fail "no matching logs exist in $log_dir"
    if [[ "$follow" == "true" ]]; then
      tail -n "$lines" -F "${files[@]}" |
        python3 -u "$redactor" --home "$HOME"
    else
      tail -n "$lines" "${files[@]}" |
        python3 "$redactor" --home "$HOME"
    fi
    ;;
  diagnostics)
    umask 077
    output="${1:-$runtime_dir/diagnostics/xu4-support-diagnostics-$(date -u +%Y%m%dT%H%M%SZ).tar.gz}"
    mkdir -p "$(dirname "$output")"
    diagnostics_dir="$(mktemp -d "$runtime_dir/diagnostics-build.XXXXXX")"
    cleanup_diagnostics() {
      [[ "$diagnostics_dir" == "$runtime_dir"/diagnostics-build.* ]] &&
        rm -r -- "$diagnostics_dir"
    }
    trap cleanup_diagnostics EXIT
    {
      show_version
      uname -srmo
      dotnet --version 2>/dev/null || true
      printf 'generated_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    } | python3 "$redactor" --home "$HOME" >"$diagnostics_dir/system.txt"
    "$0" --config "$config_path" status 2>&1 |
      python3 "$redactor" --home "$HOME" >"$diagnostics_dir/status.txt" || true
    "$0" --config "$config_path" smoke 2>&1 |
      python3 "$redactor" --home "$HOME" >"$diagnostics_dir/smoke.txt" || true
    python3 "$redactor" --home "$HOME" <"$config_path" >"$diagnostics_dir/config.txt"
    for log_file in "$log_dir"/*.log; do
      [[ -f "$log_file" ]] || continue
      [[ "$(basename "$log_file")" != "voice.log" ]] || continue
      tail -n 500 "$log_file" |
        python3 "$redactor" --home "$HOME" \
        >"$diagnostics_dir/$(basename "$log_file")"
    done
    printf '%s\n' 'voice.log omitted because it may contain captured dialogue.' \
      >"$diagnostics_dir/voice-log-omitted.txt"
    tar -C "$diagnostics_dir" -czf "$output" .
    chmod 0600 "$output"
    cleanup_diagnostics
    trap - EXIT
    printf 'Sanitized diagnostics: %s\n' "$output"
    printf 'No diagnostics were uploaded; share this file explicitly if desired.\n'
    ;;
esac
