#!/usr/bin/env bash
# Configure and verify the HTTPS reverse proxy for VPD Backoffice.
set -Eeuo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# shellcheck disable=SC1091
. "$ROOT/scripts/lib/common.sh"

HOST="hermes"
PUBLIC_HOST=""
TLS_EMAIL=""
EXPECTED_ADDRESS=""
REMOTE_DIR='$HOME/apps/vpd-backoffice'
APP_PORT="8082"
IDENTITY_FILE=""
KNOWN_HOSTS_FILE=""
ACCEPT_HOST_KEY="N"
DRY_RUN="N"
VERIFY_ONLY="N"
INSPECT_ONLY="N"

usage() {
  cat <<'USAGE'
Usage: scripts/configure-backoffice-https-vm.sh \
  --public-host FQDN_OR_IPV4 --tls-email EMAIL --expected-address IPV4 [options]

Options:
  --host SSH_ALIAS             SSH target (default: hermes)
  --public-host FQDN_OR_IPV4   Owned public DNS name or controlled public IPv4
  --tls-email EMAIL            ACME expiry/security notification address
  --expected-address IPV4      Address that the public DNS name must resolve to
  --remote-dir PATH            Application directory (default: $HOME/apps/vpd-backoffice)
  --app-port PORT              Loopback Spring Boot port (default: 8082)
  --identity-file KEY          SSH private key
  --known-hosts-file FILE      Dedicated SSH known_hosts file
  --accept-host-key            Accept a new host key (not recommended for routine use)
  --dry-run                    Validate and render locally without network access
  --inspect                    Read-only VM listener/Caddy/firewalld preflight
  --verify-only                Skip configuration and run external checks only

This script does not change DNS, OCI NSG, or firewalld rules.
USAGE
}

validate_public_host() {
  local value="${1:-}"
  if [[ "$value" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
    validate_ipv4 "$value"
    return
  fi
  [[ "$value" =~ ^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$ ]] \
    || die "공개 host는 소유한 FQDN 또는 public IPv4여야 합니다: $value"
  [[ "$value" != *".."* && "$value" != *"*"* ]] \
    || die "wildcard/빈 label host는 허용되지 않습니다: $value"
}

validate_tls_email() {
  local value="${1:-}"
  [[ "$value" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]] \
    || die "유효한 TLS 운영 이메일이 필요합니다: $value"
}

validate_ssh_host() {
  local value="${1:-}"
  [[ "$value" =~ ^[A-Za-z0-9._-]+$ && "$value" != -* ]] \
    || die "SSH host alias에 허용되지 않은 문자가 있습니다: $value"
}

validate_ipv4() {
  local value="${1:-}" part octet
  local -a parts
  [[ "$value" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] \
    || die "expected address는 IPv4 형식이어야 합니다: $value"
  IFS=. read -r -a parts <<< "$value"
  for part in "${parts[@]}"; do
    octet=$((10#$part))
    (( octet >= 0 && octet <= 255 )) || die "IPv4 octet 범위가 잘못됐습니다: $value"
  done
}

validate_port() {
  local value="${1:-}" port
  [[ "$value" =~ ^[0-9]+$ ]] || die "port는 1..65535 숫자여야 합니다: $value"
  port=$((10#$value))
  (( port >= 1 && port <= 65535 )) \
    || die "port는 1..65535 숫자여야 합니다: $value"
}

validate_remote_dir() {
  local value="${1:-}"
  [[ "$value" =~ ^(\$HOME|~|/)[A-Za-z0-9._/-]+$ ]] \
    || die "remote-dir에 허용되지 않은 문자가 있습니다: $value"
  [[ "$value" != *".."* && "$value" != *"//"* ]] \
    || die "remote-dir의 상위/빈 경로 segment는 허용되지 않습니다: $value"
}

render_caddyfile() {
  local output="${1:?output path is required}" tls_options=""
  if [[ "$PUBLIC_HOST" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
    tls_options=$'tls {\n\t\tissuer acme {\n\t\t\tprofile shortlived\n\t\t}\n\t}'
  fi
  awk \
    -v public_host="$PUBLIC_HOST" \
    -v tls_email="$TLS_EMAIL" \
    -v app_port="$APP_PORT" \
    -v tls_options="$tls_options" '
      {
        gsub(/{{PUBLIC_HOST}}/, public_host)
        gsub(/{{TLS_EMAIL}}/, tls_email)
        gsub(/{{APP_PORT}}/, app_port)
        gsub(/{{TLS_OPTIONS}}/, tls_options)
        print
      }
    ' "$ROOT/deploy/caddy/Caddyfile.template" > "$output"
}

build_ssh_args() {
  SSH_ARGS=(
    -o BatchMode=yes
    -o ConnectTimeout=8
    -o ConnectionAttempts=1
    -o ServerAliveInterval=5
    -o ServerAliveCountMax=2
  )
  if [[ -n "$IDENTITY_FILE" ]]; then
    [[ -f "$IDENTITY_FILE" ]] || die "identity file을 찾을 수 없습니다: $IDENTITY_FILE"
    SSH_ARGS+=(-i "$IDENTITY_FILE")
  fi
  if [[ -n "$KNOWN_HOSTS_FILE" ]]; then
    SSH_ARGS+=(-o "UserKnownHostsFile=$KNOWN_HOSTS_FILE")
  fi
  if [[ "$ACCEPT_HOST_KEY" == "Y" ]]; then
    SSH_ARGS+=(-o StrictHostKeyChecking=accept-new)
  else
    SSH_ARGS+=(-o StrictHostKeyChecking=yes)
  fi
}

verify_dns() {
  local addresses
  if [[ "$PUBLIC_HOST" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
    [[ "$PUBLIC_HOST" == "$EXPECTED_ADDRESS" ]] \
      || die "public IPv4와 expected address가 다릅니다: $PUBLIC_HOST != $EXPECTED_ADDRESS"
    return
  fi
  addresses="$(getent ahostsv4 "$PUBLIC_HOST" | awk '{print $1}' | sort -u)"
  [[ -n "$addresses" ]] || die "DNS A record를 조회할 수 없습니다: $PUBLIC_HOST"
  grep -Fxq "$EXPECTED_ADDRESS" <<< "$addresses" \
    || die "$PUBLIC_HOST DNS가 expected address $EXPECTED_ADDRESS 를 가리키지 않습니다 (actual: ${addresses//$'\n'/,})"
}

verify_external() {
  local tmp_dir http_headers https_headers cookie_line location
  tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/vpd-backoffice-https-verify.XXXXXX")"
  http_headers="$tmp_dir/http.headers"
  https_headers="$tmp_dir/https.headers"

  curl -sS --connect-timeout 8 --max-time 20 -D "$http_headers" -o /dev/null \
    "http://$PUBLIC_HOST/login"
  location="$(awk 'BEGIN{IGNORECASE=1} /^Location:/{sub(/\r$/, "", $2); print $2; exit}' "$http_headers")"
  [[ "$location" == "https://$PUBLIC_HOST/login" ]] \
    || die "HTTP→HTTPS redirect가 올바르지 않습니다: ${location:-missing}"

  curl -sS --connect-timeout 8 --max-time 20 -D "$https_headers" -o /dev/null \
    "https://$PUBLIC_HOST/login"
  grep -Eiq '^Strict-Transport-Security:.*max-age=31536000' "$https_headers" \
    || die "HTTPS 응답에 1년 HSTS가 없습니다."
  cookie_line="$(grep -Ei '^Set-Cookie:[[:space:]]*JSESSIONID=' "$https_headers" | head -n 1 || true)"
  [[ "$cookie_line" == *"Secure"* && "$cookie_line" == *"HttpOnly"* ]] \
    || die "JSESSIONID Secure/HttpOnly 속성을 확인할 수 없습니다."
  grep -Eiq '^Set-Cookie:.*JSESSIONID=.*SameSite=Lax' <<< "$cookie_line" \
    || die "JSESSIONID SameSite=Lax 속성을 확인할 수 없습니다."

  if curl -sS --connect-timeout 3 --max-time 5 -o /dev/null \
      "http://$PUBLIC_HOST:$APP_PORT/login" 2>/dev/null; then
    die "외부에서 애플리케이션 포트 $APP_PORT 에 직접 접근할 수 있습니다."
  fi

  rm -rf "$tmp_dir"
  ok "HTTPS 검증 통과: redirect, TLS, HSTS, session cookie, direct-port deny"
}

main() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --host) HOST="${2:?--host requires a value}"; shift 2 ;;
      --public-host) PUBLIC_HOST="${2:?--public-host requires a value}"; shift 2 ;;
      --tls-email) TLS_EMAIL="${2:?--tls-email requires a value}"; shift 2 ;;
      --expected-address) EXPECTED_ADDRESS="${2:?--expected-address requires a value}"; shift 2 ;;
      --remote-dir) REMOTE_DIR="${2:?--remote-dir requires a value}"; shift 2 ;;
      --app-port) APP_PORT="${2:?--app-port requires a value}"; shift 2 ;;
      --identity-file) IDENTITY_FILE="${2:?--identity-file requires a value}"; shift 2 ;;
      --known-hosts-file) KNOWN_HOSTS_FILE="${2:?--known-hosts-file requires a value}"; shift 2 ;;
      --accept-host-key) ACCEPT_HOST_KEY="Y"; shift ;;
      --dry-run) DRY_RUN="Y"; shift ;;
      --inspect) INSPECT_ONLY="Y"; shift ;;
      --verify-only) VERIFY_ONLY="Y"; shift ;;
      -h|--help) usage; exit 0 ;;
      *) die "알 수 없는 옵션: $1" ;;
    esac
  done

  if [[ "$INSPECT_ONLY" == "Y" ]]; then
    validate_ssh_host "$HOST"
    need_cmd ssh "OpenSSH client"
    need_cmd timeout "bounded SSH preflight"
    build_ssh_args
    timeout 30s ssh "${SSH_ARGS[@]}" "$HOST" 'bash -s' <<'INSPECT'
set -Eeuo pipefail
printf 'host=%s user=%s\n' "$(hostname)" "$(whoami)"
printf '%s\n' 'listeners:'
ss -ltn | awk 'NR == 1 || $4 ~ /:(80|443|8082)$/'
if command -v caddy >/dev/null 2>&1; then
  printf 'caddy=%s\n' "$(timeout 5s caddy version || true)"
  printf 'caddy_active=%s\n' "$(timeout 5s systemctl is-active caddy 2>/dev/null || true)"
  printf 'caddy_enabled=%s\n' "$(timeout 5s systemctl is-enabled caddy 2>/dev/null || true)"
else
  printf '%s\n' 'caddy=not-installed'
fi
if sudo -n true >/dev/null 2>&1 && command -v firewall-cmd >/dev/null 2>&1; then
  printf 'firewalld_ports=%s\n' "$(timeout 5s sudo firewall-cmd --list-ports 2>/dev/null || true)"
fi
printf 'app_loopback_http_status='
curl -sS --connect-timeout 3 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8082/login || true
INSPECT
    exit 0
  fi

  validate_public_host "$PUBLIC_HOST"
  validate_tls_email "$TLS_EMAIL"
  validate_ssh_host "$HOST"
  validate_ipv4 "$EXPECTED_ADDRESS"
  validate_port "$APP_PORT"
  validate_remote_dir "$REMOTE_DIR"

  local tmp_dir candidate remote_stage
  tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/vpd-backoffice-caddy.XXXXXX")"
  candidate="$tmp_dir/Caddyfile"
  render_caddyfile "$candidate"

  if [[ "$DRY_RUN" == "Y" ]]; then
    log "dry-run: host=$HOST"
    log "dry-run: public_host=$PUBLIC_HOST"
    log "dry-run: expected_address=$EXPECTED_ADDRESS"
    log "dry-run: app_upstream=127.0.0.1:$APP_PORT"
    log "dry-run: remote_dir=$REMOTE_DIR"
    cat "$candidate"
    rm -rf "$tmp_dir"
    exit 0
  fi

  need_cmd ssh "OpenSSH client"
  need_cmd curl "HTTPS 검증"
  need_cmd getent "DNS 검증"
  verify_dns
  build_ssh_args

  if [[ "$VERIFY_ONLY" != "Y" ]]; then
    remote_stage="${REMOTE_DIR%/}/.https-stage"
    log "Caddy 및 sudo 사전 점검: $HOST"
    ssh "${SSH_ARGS[@]}" "$HOST" \
      'command -v caddy >/dev/null && command -v systemctl >/dev/null && sudo -n true'

    log "Caddy candidate 업로드"
    ssh "${SSH_ARGS[@]}" "$HOST" "mkdir -p \"$remote_stage\""
    cp "$ROOT/deploy/caddy/install-caddy-config.sh" "$tmp_dir/install-caddy-config.sh"
    tar -C "$tmp_dir" -czf - Caddyfile install-caddy-config.sh \
      | ssh "${SSH_ARGS[@]}" "$HOST" "tar -C \"$remote_stage\" -xzf - && chmod 700 \"$remote_stage/install-caddy-config.sh\""

    log "Caddy 설정 검증·백업·적용"
    ssh "${SSH_ARGS[@]}" "$HOST" \
      "\"$remote_stage/install-caddy-config.sh\" \"$remote_stage/Caddyfile\" \"$REMOTE_DIR\""
  fi

  rm -rf "$tmp_dir"
  verify_external
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi
