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

VERSION="0.1.0"
INSTALL_MISSING=0
PURGE_INSTALLED_DEPS=0
KEEP_OUTPUT=0
SKIP_STRICT_VALIDATE=0
PLAN_ONLY=0
STRICT_TLS_MS=50
SCRIPT_PATH=""
WORK_DIR=""
OUT_DIR=""
PKG_MANAGER=""
FILTER_ARGS=()
INSTALLED_PACKAGES=()
PACKAGE_SNAPSHOT_BEFORE=""
PACKAGE_SNAPSHOT_AFTER=""

usage() {
  cat <<'USAGE'
Ephemeral one-shot runner for reality-domain-filter.sh.

Usage:
  ./reality-domain-filter-once.sh [runner options] -- [filter options]

Runner options:
  --install-missing          Install missing helper commands before running.
  --purge-installed-deps     Remove packages installed by this runner during cleanup.
  --keep-output              Keep the temporary work/output directory.
  --strict-tls-ms <ms>       Strict post-check TLS handshake threshold. Default: 50.
  --script-path <file>       Path to reality-domain-filter.sh. Default: beside this runner.
  --skip-strict-validate     Skip non-redirect/TLS-latency/CDN post-check.
  --plan-only                Print dependency installation plan and exit.
  --help                     Show this help.
  --version                  Show version.

Examples:
  ./reality-domain-filter-once.sh --install-missing -- --max-candidates 100
  ./reality-domain-filter-once.sh --install-missing --purge-installed-deps -- --ip 1.2.3.4

Notes:
  - Temporary script/output files are removed by default.
  - Package cleanup is opt-in and only targets packages this runner installed.
  - Use --keep-output if you want to inspect result.csv/pass.txt after the run.
USAGE
}

log() {
  printf '[once] %s\n' "$*" >&2
}

stage() {
  local current="$1"
  local total="$2"
  local label="$3"
  local width=20
  local filled=$((current * width / total))
  local empty=$((width - filled))
  local bar
  bar="$(printf '%*s' "$filled" '' | tr ' ' '#')"
  bar="${bar}$(printf '%*s' "$empty" '' | tr ' ' '.')"
  printf '[%s/%s] [%s] %s\n' "$current" "$total" "$bar" "$label"
}

warn() {
  printf '[once][WARN] %s\n' "$*" >&2
}

die() {
  printf '[once][ERROR] %s\n' "$*" >&2
  exit 1
}

unique_words() {
  awk '
    !seen[$0]++ && $0 != "" { print }
  '
}

cleanup_packages() {
  ((PURGE_INSTALLED_DEPS == 1)) || return 0
  ((${#INSTALLED_PACKAGES[@]} > 0)) || return 0
  [[ -n "$PKG_MANAGER" ]] || return 0

  local packages=()
  while IFS= read -r package_name; do
    packages+=("$package_name")
  done < <(printf '%s\n' "${INSTALLED_PACKAGES[@]}" | unique_words)
  ((${#packages[@]} > 0)) || return 0

  warn "removing packages installed by this runner: ${packages[*]}"
  case "$PKG_MANAGER" in
    apt)
      run_as_root apt-get purge -y "${packages[@]}" >/dev/null || warn "apt purge failed"
      ;;
    dnf)
      run_as_root dnf remove -y "${packages[@]}" >/dev/null || warn "dnf remove failed"
      ;;
    yum)
      run_as_root yum remove -y "${packages[@]}" >/dev/null || warn "yum remove failed"
      ;;
    apk)
      run_as_root apk del "${packages[@]}" >/dev/null || warn "apk del failed"
      ;;
    pacman)
      run_as_root pacman -Rns --noconfirm "${packages[@]}" >/dev/null || warn "pacman remove failed"
      ;;
  esac
}

cleanup() {
  local rc=$?
  if [[ -n "${WORK_DIR:-}" && -d "$WORK_DIR" && "$KEEP_OUTPUT" -eq 0 ]]; then
    rm -rf "$WORK_DIR"
  fi
  cleanup_packages
  [[ -n "${PACKAGE_SNAPSHOT_BEFORE:-}" && -f "$PACKAGE_SNAPSHOT_BEFORE" ]] && rm -f "$PACKAGE_SNAPSHOT_BEFORE"
  [[ -n "${PACKAGE_SNAPSHOT_AFTER:-}" && -f "$PACKAGE_SNAPSHOT_AFTER" ]] && rm -f "$PACKAGE_SNAPSHOT_AFTER"
  exit "$rc"
}

trap cleanup EXIT

parse_args() {
  while (($# > 0)); do
    case "$1" in
      --install-missing)
        INSTALL_MISSING=1
        shift
        ;;
      --purge-installed-deps)
        PURGE_INSTALLED_DEPS=1
        INSTALL_MISSING=1
        shift
        ;;
      --keep-output)
        KEEP_OUTPUT=1
        shift
        ;;
      --strict-tls-ms)
        [[ $# -ge 2 ]] || die "--strict-tls-ms requires a value"
        STRICT_TLS_MS="$2"
        shift 2
        ;;
      --script-path)
        [[ $# -ge 2 ]] || die "--script-path requires a value"
        SCRIPT_PATH="$2"
        shift 2
        ;;
      --skip-strict-validate)
        SKIP_STRICT_VALIDATE=1
        shift
        ;;
      --plan-only)
        PLAN_ONLY=1
        INSTALL_MISSING=1
        shift
        ;;
      --help|-h)
        usage
        exit 0
        ;;
      --version)
        printf '%s\n' "$VERSION"
        exit 0
        ;;
      --)
        shift
        FILTER_ARGS=("$@")
        break
        ;;
      *)
        FILTER_ARGS+=("$1")
        shift
        ;;
    esac
  done
}

arg_present() {
  local needle="$1"
  local arg
  ((${#FILTER_ARGS[@]} > 0)) || return 1
  for arg in "${FILTER_ARGS[@]}"; do
    [[ "$arg" == "$needle" ]] && return 0
  done
  return 1
}

validate_args() {
  [[ "$STRICT_TLS_MS" =~ ^[1-9][0-9]*$ ]] || die "--strict-tls-ms must be a positive integer"
  if [[ -z "$SCRIPT_PATH" ]]; then
    local script_dir
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
    SCRIPT_PATH="$script_dir/reality-domain-filter.sh"
  fi
  [[ -r "$SCRIPT_PATH" ]] || die "main script not readable: $SCRIPT_PATH"
  if arg_present "--out-dir"; then
    die "filter --out-dir is controlled by the one-shot runner; use --keep-output instead"
  fi
}

run_as_root() {
  if [[ "$(id -u)" -eq 0 ]]; then
    "$@"
  elif command -v sudo >/dev/null 2>&1; then
    sudo "$@"
  else
    die "root or sudo is required for dependency installation"
  fi
}

detect_pkg_manager() {
  if command -v apt-get >/dev/null 2>&1; then
    PKG_MANAGER="apt"
  elif command -v dnf >/dev/null 2>&1; then
    PKG_MANAGER="dnf"
  elif command -v yum >/dev/null 2>&1; then
    PKG_MANAGER="yum"
  elif command -v apk >/dev/null 2>&1; then
    PKG_MANAGER="apk"
  elif command -v pacman >/dev/null 2>&1; then
    PKG_MANAGER="pacman"
  else
    PKG_MANAGER=""
  fi
}

package_for_command() {
  local cmd="$1"
  case "$PKG_MANAGER:$cmd" in
    apt:bash) printf 'bash\n' ;;
    apt:curl) printf 'curl\n' ;;
    apt:openssl) printf 'openssl\n' ;;
    apt:timeout|apt:sort) printf 'coreutils\n' ;;
    apt:awk) printf 'gawk\n' ;;
    apt:sed) printf 'sed\n' ;;
    apt:grep) printf 'grep\n' ;;
    apt:dig|apt:host|apt:nslookup) printf 'bind9-dnsutils\n' ;;
    apt:jq) printf 'jq\n' ;;
    apt:python3) printf 'python3\n' ;;

    dnf:bash|yum:bash) printf 'bash\n' ;;
    dnf:curl|yum:curl) printf 'curl\n' ;;
    dnf:openssl|yum:openssl) printf 'openssl\n' ;;
    dnf:timeout|dnf:sort|yum:timeout|yum:sort) printf 'coreutils\n' ;;
    dnf:awk|yum:awk) printf 'gawk\n' ;;
    dnf:sed|yum:sed) printf 'sed\n' ;;
    dnf:grep|yum:grep) printf 'grep\n' ;;
    dnf:dig|dnf:host|dnf:nslookup|yum:dig|yum:host|yum:nslookup) printf 'bind-utils\n' ;;
    dnf:jq|yum:jq) printf 'jq\n' ;;
    dnf:python3|yum:python3) printf 'python3\n' ;;

    apk:bash) printf 'bash\n' ;;
    apk:curl) printf 'curl\n' ;;
    apk:openssl) printf 'openssl\n' ;;
    apk:timeout|apk:sort) printf 'coreutils\n' ;;
    apk:awk) printf 'gawk\n' ;;
    apk:sed) printf 'sed\n' ;;
    apk:grep) printf 'grep\n' ;;
    apk:dig|apk:host|apk:nslookup) printf 'bind-tools\n' ;;
    apk:jq) printf 'jq\n' ;;
    apk:python3) printf 'python3\n' ;;

    pacman:bash) printf 'bash\n' ;;
    pacman:curl) printf 'curl\n' ;;
    pacman:openssl) printf 'openssl\n' ;;
    pacman:timeout|pacman:sort) printf 'coreutils\n' ;;
    pacman:awk) printf 'gawk\n' ;;
    pacman:sed) printf 'sed\n' ;;
    pacman:grep) printf 'grep\n' ;;
    pacman:dig|pacman:host|pacman:nslookup) printf 'bind\n' ;;
    pacman:jq) printf 'jq\n' ;;
    pacman:python3) printf 'python\n' ;;
  esac
}

install_packages() {
  local packages=("$@")
  ((${#packages[@]} > 0)) || return 0
  case "$PKG_MANAGER" in
    apt)
      run_as_root env DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
      run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
      ;;
    dnf)
      run_as_root dnf install -y "${packages[@]}"
      ;;
    yum)
      run_as_root yum install -y "${packages[@]}"
      ;;
    apk)
      run_as_root apk add --no-cache "${packages[@]}"
      ;;
    pacman)
      run_as_root pacman -Sy --noconfirm "${packages[@]}"
      ;;
    *)
      die "no supported package manager found for dependency installation"
      ;;
  esac
}

print_install_plan() {
  local packages=("$@")
  ((${#packages[@]} > 0)) || {
    printf 'Packages to install before test: none\n'
    printf 'Estimated download: 0 B\n'
    printf 'Estimated installed size: 0 B\n'
    return 0
  }

  local estimate download size simulation
  download="unknown"
  size="unknown"
  case "$PKG_MANAGER" in
    apt)
      simulation="$(apt-get -s install --no-install-recommends "${packages[@]}" 2>/dev/null || true)"
      download="$(printf '%s\n' "$simulation" | awk -F'Need to get | of archives\\.' '/Need to get/ {print $2; exit}')"
      size="$(printf '%s\n' "$simulation" | awk -F'After this operation, | of additional disk space will be used\\.' '/After this operation/ {print $2; exit}')"
      if [[ -z "$download" || -z "$size" ]]; then
        local apt_packages apt_estimates
        apt_packages="$(printf '%s\n' "$simulation" | awk '/^Inst / {print $2}' | sort -u)"
        [[ -n "$apt_packages" ]] || apt_packages="$(printf '%s\n' "${packages[@]}")"
        apt_estimates="$(apt-cache show $apt_packages 2>/dev/null | awk '
          /^Package:/ {pkg=$2}
          /^Size:/ && pkg != "" && !size_seen[pkg]++ {download += $2}
          /^Installed-Size:/ && pkg != "" && !installed_seen[pkg]++ {installed += $2}
          END {
            if (download > 0) printf "%.1f MiB", download / 1024 / 1024; else printf "unknown";
            printf "\t";
            if (installed > 0) printf "%.1f MiB", installed / 1024; else printf "unknown";
          }
        ' || true)"
        [[ -n "$download" ]] && download="$download" || download="${apt_estimates%%$'\t'*}"
        [[ -n "$size" ]] && size="$size" || size="${apt_estimates#*$'\t'}"
      fi
      ;;
    dnf)
      simulation="$(dnf install --assumeno "${packages[@]}" 2>/dev/null || true)"
      download="$(printf '%s\n' "$simulation" | awk -F': *' '/Total download size/ {print $2; exit}')"
      size="$(printf '%s\n' "$simulation" | awk -F': *' '/Installed size/ {print $2; exit}')"
      ;;
    yum)
      simulation="$(yum install --assumeno "${packages[@]}" 2>/dev/null || true)"
      download="$(printf '%s\n' "$simulation" | awk -F': *' '/Total download size/ {print $2; exit}')"
      size="$(printf '%s\n' "$simulation" | awk -F': *' '/Installed size/ {print $2; exit}')"
      ;;
    apk)
      simulation="$(apk add --simulate "${packages[@]}" 2>/dev/null || true)"
      estimate="$(printf '%s\n' "$simulation" | awk -F'[()]' '/[0-9]+ MiB in [0-9]+ packages/ {print $2; exit}')"
      [[ -n "$estimate" ]] && size="$estimate"
      ;;
    pacman)
      simulation="$(pacman -Sp --print-format '%n %s' "${packages[@]}" 2>/dev/null || true)"
      download="$(printf '%s\n' "$simulation" | awk '{sum += $2} END {if (sum > 0) printf "%.1f MiB", sum / 1024 / 1024}')"
      ;;
  esac

  [[ -n "$download" ]] || download="unknown"
  [[ -n "$size" ]] || size="unknown"
  printf 'Packages to install before test: %s\n' "${packages[*]}"
  printf 'Estimated download: %s\n' "$download"
  printf 'Estimated installed size: %s\n' "$size"
}

list_installed_packages() {
  case "$PKG_MANAGER" in
    apt)
      dpkg-query -W -f='${binary:Package}\n' 2>/dev/null | sort -u
      ;;
    dnf|yum)
      rpm -qa --qf '%{NAME}\n' 2>/dev/null | sort -u
      ;;
    apk)
      apk info 2>/dev/null | sort -u
      ;;
    pacman)
      pacman -Qq 2>/dev/null | sort -u
      ;;
  esac
}

ensure_dependencies() {
  local required=(bash curl openssl timeout awk sed sort grep)
  local preferred=(dig jq python3)
  local missing_required=()
  local missing_for_install=()
  local cmd pkg

  for cmd in "${required[@]}"; do
    if ! command -v "$cmd" >/dev/null 2>&1; then
      missing_required+=("$cmd")
      missing_for_install+=("$cmd")
    fi
  done

  for cmd in "${preferred[@]}"; do
    if ! command -v "$cmd" >/dev/null 2>&1; then
      missing_for_install+=("$cmd")
    fi
  done

  if ((${#missing_required[@]} > 0 && INSTALL_MISSING == 0)); then
    die "missing required command(s): ${missing_required[*]}; rerun with --install-missing"
  fi

  if ((INSTALL_MISSING == 0)); then
    return 0
  fi

  detect_pkg_manager
  [[ -n "$PKG_MANAGER" ]] || die "no supported package manager found"

  local packages=()
  for cmd in "${missing_for_install[@]}"; do
    pkg="$(package_for_command "$cmd" || true)"
    [[ -n "$pkg" ]] && packages+=("$pkg")
  done
  local deduped_packages=()
  while IFS= read -r package_name; do
    deduped_packages+=("$package_name")
  done < <(printf '%s\n' "${packages[@]}" | unique_words)
  packages=("${deduped_packages[@]}")
  print_install_plan "${packages[@]}"

  if ((PLAN_ONLY == 1)); then
    exit 0
  fi

  if ((${#packages[@]} > 0)); then
    log "installing missing dependency package(s): ${packages[*]}"
    PACKAGE_SNAPSHOT_BEFORE="$(mktemp)"
    PACKAGE_SNAPSHOT_AFTER="$(mktemp)"
    list_installed_packages > "$PACKAGE_SNAPSHOT_BEFORE" || true
    install_packages "${packages[@]}"
    list_installed_packages > "$PACKAGE_SNAPSHOT_AFTER" || true
    while IFS= read -r package_name; do
      INSTALLED_PACKAGES+=("$package_name")
    done < <(comm -13 "$PACKAGE_SNAPSHOT_BEFORE" "$PACKAGE_SNAPSHOT_AFTER" || true)
  fi

  for cmd in "${missing_for_install[@]}"; do
    if command -v "$cmd" >/dev/null 2>&1; then
      :
    elif [[ " ${required[*]} " == *" $cmd "* ]]; then
      die "dependency still missing after install: $cmd"
    else
      warn "optional command still missing after install: $cmd"
    fi
  done
}

prepare_workspace() {
  local parent="${RDF_ONCE_TMPDIR_PARENT:-/tmp}"
  mkdir -p "$parent"
  WORK_DIR="$(mktemp -d "${parent%/}/reality-domain-filter-once.XXXXXX")"
  OUT_DIR="$WORK_DIR/out"
  mkdir -p "$OUT_DIR"
  cp "$SCRIPT_PATH" "$WORK_DIR/reality-domain-filter.sh"
  chmod +x "$WORK_DIR/reality-domain-filter.sh"
}

print_outputs() {
  printf '\n=== summary.txt ===\n'
  sed -n '1,220p' "$OUT_DIR/summary.txt" 2>/dev/null || true
  printf '\n=== pass.txt ===\n'
  cat "$OUT_DIR/pass.txt" 2>/dev/null || true
}

strict_validate() {
  ((SKIP_STRICT_VALIDATE == 0)) || return 0
  if ! command -v python3 >/dev/null 2>&1; then
    warn "skip strict validation: python3 missing"
    return 0
  fi
  if ! command -v curl >/dev/null 2>&1; then
    warn "skip strict validation: curl missing"
    return 0
  fi

  STRICT_TLS_MS="$STRICT_TLS_MS" OUT_DIR="$OUT_DIR" python3 <<'PY'
import csv
import os
import statistics
import subprocess
from pathlib import Path

out_dir = Path(os.environ["OUT_DIR"])
threshold = float(os.environ["STRICT_TLS_MS"])
rows = {}
result = out_dir / "result.csv"
passes = out_dir / "pass.txt"

if not result.exists() or not passes.exists():
    raise SystemExit

with result.open(newline="") as f:
    for row in csv.DictReader(f):
        rows[row["domain"]] = row

domains = [line.strip() for line in passes.read_text().splitlines() if line.strip()]
print("\n=== strict non-redirect / low-SNI-latency / non-CDN check ===")
print("domain,is_cdn,asn,http_codes,redirects,tls_ms_median,strict_ok")
for domain in domains:
    row = rows.get(domain, {})
    codes = []
    redirects = set()
    tls_times = []
    for _ in range(2):
        cmd = [
            "curl", "-k", "-sS", "-o", "/dev/null",
            "--max-time", "8",
            "--connect-timeout", "5",
            "-w", "%{http_code}\t%{redirect_url}\t%{time_connect}\t%{time_appconnect}",
            f"https://{domain}/",
        ]
        proc = subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
        parts = proc.stdout.strip().split("\t")
        if len(parts) != 4:
            continue
        code, redirect, connect_s, appconnect_s = parts
        codes.append(code)
        redirects.add(redirect or "none")
        try:
            tls_times.append((float(appconnect_s) - float(connect_s)) * 1000)
        except ValueError:
            pass

    tls_med = statistics.median(tls_times) if tls_times else None
    non_redirect = bool(codes) and all(not code.startswith("3") for code in codes) and redirects <= {"none"}
    non_cdn = row.get("is_cdn") == "0"
    low_tls = tls_med is not None and tls_med < threshold
    strict_ok = non_redirect and non_cdn and low_tls
    print(",".join([
        domain,
        row.get("is_cdn", ""),
        row.get("asn", ""),
        "/".join(sorted(set(codes))) or "n/a",
        "/".join(sorted(redirects)) or "none",
        f"{tls_med:.1f}" if tls_med is not None else "n/a",
        "1" if strict_ok else "0",
    ]))
PY
}

run_filter() {
  local final_args=("${FILTER_ARGS[@]}")
  if ! arg_present "--max-candidates"; then
    final_args=(--max-candidates 100 "${final_args[@]}")
  fi
  final_args+=(--out-dir "$OUT_DIR")
  log "running filter in temporary workspace: $WORK_DIR"
  "$WORK_DIR/reality-domain-filter.sh" "${final_args[@]}"
  print_outputs
  strict_validate
  stage 5 5 "cleanup temporary files"
  if ((KEEP_OUTPUT == 1)); then
    printf '\n[once] output kept at: %s\n' "$WORK_DIR"
  else
    printf '\n[once] temporary files will be removed on exit\n'
  fi
}

main() {
  parse_args "$@"
  stage 1 5 "validate runner arguments"
  validate_args
  stage 2 5 "check/install dependencies"
  ensure_dependencies
  stage 3 5 "prepare temporary workspace"
  prepare_workspace
  stage 4 5 "run domain filter"
  run_filter
}

main "$@"
