#!/usr/bin/env bash
# WiFi AP provisioning — boot-time captive portal for first-boot connectivity.
#
# Called by systemd wifi-provision.service (system-level, runs as root).
# Checks for existing connectivity (saved WiFi or ethernet). If neither
# exists, activates a temporary access point with a captive portal so the
# user can configure WiFi from their phone.
#
# State machine:
#
#   checking ──┬── (has connectivity) ──► skipped
#              │
#              ├── (no connectivity) ──► scanning ──► ap-starting ──► ap-ready
#              │                                                        │
#              │                                         POST /connect  │
#              │                                                        ▼
#              │                                                    connecting
#              │                                                     │     │
#              │                                               (success) (fail)
#              │                                                     │     │
#              │                                                     ▼     ▼
#              │                                               connected  connect-failed
#              │                                                     │        │
#              │                                                     ▼        └──► ap-ready
#              │                                                  teardown
#              │
#              └── (saved WiFi, fallback) ──► wifi-waiting ──┬── (connected) ──► skipped
#                                                            └── (timeout 60s) ──► scanning
#
# Usage: wifi-provision.sh
#        wifi-provision.sh cleanup   (called by ExecStopPost for crash recovery)

set -uo pipefail

# ── Brand-aware config directory resolution ──────────────────────────
# Same pattern as vnc.sh — derive from brand.json so logs go to the
# correct brand-specific directory.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLATFORM_ROOT="${MAXY_PLATFORM_ROOT:-$(dirname "$SCRIPT_DIR")}"
BRAND_JSON="${PLATFORM_ROOT}/config/brand.json"
CONFIG_DIR=".maxy"
PRODUCT_NAME="Maxy"
if [ -f "$BRAND_JSON" ] && command -v jq >/dev/null 2>&1; then
  _dir=$(jq -r '.configDir // empty' "$BRAND_JSON" 2>/dev/null) || true
  [ -n "$_dir" ] && CONFIG_DIR="$_dir"
  _name=$(jq -r '.productName // empty' "$BRAND_JSON" 2>/dev/null) || true
  [ -n "$_name" ] && PRODUCT_NAME="$_name"
fi
# HOSTNAME_NAME must be the **actual system hostname** (the name Avahi
# advertises and the post-connect URL must use), not the brand default
# from brand.json. The operator's --hostname flag rewrites the system
# hostname but never touches brand.json's hostname field, so reading
# from the brand mis-routed the captive portal's success URL to a name
# that doesn't resolve. `hostname -s` is the canonical answer Avahi
# itself follows when avahi-daemon.conf has `host-name` left empty,
# so this is the same source of truth.
HOSTNAME_NAME="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo maxy)"

# Determine the home directory of the installing user. The service runs as
# root, but logs and config belong to the user who installed the platform.
# The installer writes INSTALL_USER into the service unit's Environment.
INSTALL_HOME="${INSTALL_HOME:-/home/${INSTALL_USER:-admin}}"
MAXY_DIR="${INSTALL_HOME}/${CONFIG_DIR}"
LOG_DIR="${MAXY_DIR}/logs"
LOG_FILE="${LOG_DIR}/wifi-provision.log"

mkdir -p "$LOG_DIR"

# Resolve the brand service's public port so the captive portal can render
# the post-connect URL with the correct port (the captive portal itself
# runs on :80, but the device's admin URL is on the brand port). Mirrors
# the same precedence as the installer: ~/{configDir}/.env override, then
# the systemd unit's Environment=PORT, then the documented default.
BRAND_PORT="19200"
if [ -f "${MAXY_DIR}/.env" ]; then
  _p=$(grep -E '^PORT=' "${MAXY_DIR}/.env" 2>/dev/null | tail -1 | cut -d= -f2 | tr -d '"' | tr -d "'")
  [ -n "$_p" ] && BRAND_PORT="$_p"
fi
if [ "$BRAND_PORT" = "19200" ]; then
  _svc="${INSTALL_HOME}/.config/systemd/user/${HOSTNAME_NAME}.service"
  if [ -f "$_svc" ]; then
    _p=$(grep -E '^Environment=PORT=' "$_svc" 2>/dev/null | tail -1 | cut -d= -f3)
    [ -n "$_p" ] && BRAND_PORT="$_p"
  fi
fi

# ── Derived constants ────────────────────────────────────────────────
# SSID: lowercase the product name and replace spaces with hyphens — gives
# `maxy`, `real-agent`, etc. (kebab-case, no "-Setup" suffix). The user's
# phone-side WiFi list is the brand surface; an extra "-Setup" suffix is
# operator jargon, not customer language.
SSID="$(echo "$PRODUCT_NAME" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')"
# Captive-portal hostname served by dnsmasq's wildcard — gives the phone
# a friendly URL bar (`http://maxy.setup/`) instead of `http://192.168.4.1/`.
PORTAL_HOST="${SSID}.setup"
AP_IP="192.168.4.1"
AP_SUBNET="192.168.4.0/24"
DHCP_RANGE_START="192.168.4.2"
DHCP_RANGE_END="192.168.4.20"
DHCP_LEASE="1h"
SCAN_CACHE="/tmp/wifi-provision-scan.json"
HOSTAPD_CONF="/tmp/wifi-provision-hostapd.conf"
DNSMASQ_CONF="/tmp/wifi-provision-dnsmasq.conf"
CONNECT_FIFO="/tmp/wifi-provision-connect"
RESULT_FILE="/tmp/wifi-provision-result"
FALLBACK_TIMEOUT=60
HTTP_SERVER_PID=""
HOSTAPD_PID=""
DNSMASQ_PID=""

# ── Logging ──────────────────────────────────────────────────────────
log() {
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] wifi-provision: $*" >> "$LOG_FILE"
}

log_state() {
  log "provision state=$1${2:+ $2}"
}

# ── Cleanup (trap + ExecStopPost) ────────────────────────────────────
# Defense in depth: trap catches script-level crashes, ExecStopPost
# catches SIGKILL/OOM. Both call the same cleanup function.
cleanup() {
  log "cleanup: restoring wlan0 to managed mode"

  # Kill child processes
  if [ -n "$HTTP_SERVER_PID" ] && kill -0 "$HTTP_SERVER_PID" 2>/dev/null; then
    kill "$HTTP_SERVER_PID" 2>/dev/null || true
    wait "$HTTP_SERVER_PID" 2>/dev/null || true
  fi
  if [ -n "$HOSTAPD_PID" ] && kill -0 "$HOSTAPD_PID" 2>/dev/null; then
    kill "$HOSTAPD_PID" 2>/dev/null || true
    wait "$HOSTAPD_PID" 2>/dev/null || true
  fi
  if [ -n "$DNSMASQ_PID" ] && kill -0 "$DNSMASQ_PID" 2>/dev/null; then
    kill "$DNSMASQ_PID" 2>/dev/null || true
    wait "$DNSMASQ_PID" 2>/dev/null || true
  fi

  # Also kill by name in case PIDs are stale (ExecStopPost path)
  pkill -f "hostapd.*${HOSTAPD_CONF}" 2>/dev/null || true
  pkill -f "dnsmasq.*${DNSMASQ_CONF}" 2>/dev/null || true
  pkill -f "node.*wifi-provision-server" 2>/dev/null || true

  # Restore wlan0 to NetworkManager
  ip addr del "${AP_IP}/24" dev wlan0 2>/dev/null || true
  nmcli device set wlan0 managed yes 2>/dev/null || true

  # Clean up temp files
  rm -f "$HOSTAPD_CONF" "$DNSMASQ_CONF" "$SCAN_CACHE" "$CONNECT_FIFO" "$RESULT_FILE" 2>/dev/null || true

  log "cleanup: complete"
}

# If called with "cleanup" arg, run cleanup and exit (ExecStopPost path)
if [ "${1:-}" = "cleanup" ]; then
  cleanup
  exit 0
fi

trap cleanup EXIT

# ── Connectivity checks ────────────────────────────────��─────────────

has_ethernet() {
  # Check for any active ethernet connection
  nmcli -t -f DEVICE,TYPE,STATE device 2>/dev/null | grep -q "ethernet:connected"
}

has_saved_wifi() {
  # Check for any saved WiFi connection profile
  nmcli -t -f TYPE connection show 2>/dev/null | grep -q "802-11-wireless"
}

wifi_is_connected() {
  # Check if wlan0 is currently connected to a network
  local state
  state=$(nmcli -t -f DEVICE,STATE device 2>/dev/null | grep "^wlan0:" | cut -d: -f2)
  [ "$state" = "connected" ]
}

get_wifi_ip() {
  nmcli -t -f IP4.ADDRESS device show wlan0 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1
}

# ── WiFi scanning ────────────────────────────────────────────────────

scan_networks() {
  log "scanning WiFi networks"

  # `nmcli device wifi list --rescan yes` triggers a rescan but returns the
  # currently cached list immediately — on cold boot the cache is empty
  # because the radio hasn't completed its first scan yet. Trigger an
  # explicit rescan, then poll the cached list until it has at least one
  # visible (non-hidden) SSID, up to a hard timeout.
  nmcli device wifi rescan 2>/dev/null || true
  local raw=""
  local elapsed=0
  local max_wait=15
  while [ "$elapsed" -lt "$max_wait" ]; do
    raw=$(nmcli -t -f SSID,SIGNAL,SECURITY device wifi list 2>/dev/null) || true
    # Lines have form SSID:SIGNAL:SECURITY; a leading ':' means hidden SSID.
    # Break as soon as at least one line starts with a non-':' character.
    if [ -n "$raw" ] && echo "$raw" | grep -qE '^[^:]'; then
      break
    fi
    sleep 1
    elapsed=$((elapsed + 1))
  done
  if [ "$elapsed" -ge "$max_wait" ]; then
    log "scan timeout: no visible networks after ${max_wait}s — proceeding with empty list"
  else
    log "scan settled in ${elapsed}s"
  fi

  # Parse nmcli terse output into JSON using jq for safe escaping.
  # Terse mode uses colon separators and escapes literal colons as \:
  # jq handles all JSON special characters (newlines, tabs, quotes, etc.)
  # that hand-rolled escaping would miss in SSIDs.
  local json="[]"
  if [ -n "$raw" ]; then
    json=$(echo "$raw" | while IFS= read -r line; do
      [ -z "$line" ] && continue
      # Unescape \: to a placeholder, split on :, restore colons
      local cleaned="${line//\\:/COLONPLACEHOLDER}"
      local ssid signal security
      ssid=$(echo "$cleaned" | cut -d: -f1)
      ssid="${ssid//COLONPLACEHOLDER/:}"
      signal=$(echo "$cleaned" | cut -d: -f2)
      security=$(echo "$cleaned" | cut -d: -f3-)
      security="${security//COLONPLACEHOLDER/:}"
      # Skip hidden networks (empty SSID)
      [ -z "$ssid" ] && continue
      # Output tab-separated for jq to consume safely
      printf '%s\t%s\t%s\n' "$ssid" "${signal:-0}" "${security:-open}"
    done | jq -Rn '[inputs | split("\t") | select(length >= 3) |
      {ssid: .[0], signal: (.[1] | tonumber), security: .[2]}]')
  fi

  echo "$json" > "$SCAN_CACHE"
  log "scan complete: $(echo "$json" | grep -o '"ssid"' | wc -l | tr -d ' ') networks found"
}

# ── AP mode management ───────────────────────────────────────────────

start_ap() {
  log_state "ap-starting" "ssid=\"${SSID}\""

  # Tell NetworkManager to release wlan0
  nmcli device set wlan0 managed no 2>/dev/null
  sleep 1

  # Configure wlan0 with static IP
  ip addr flush dev wlan0 2>/dev/null || true
  ip addr add "${AP_IP}/24" dev wlan0
  ip link set wlan0 up

  # Write hostapd config (open AP, no WPA)
  cat > "$HOSTAPD_CONF" <<HOSTAPD_EOF
interface=wlan0
driver=nl80211
ssid=${SSID}
hw_mode=g
channel=7
wmm_enabled=0
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0
HOSTAPD_EOF

  # Write dnsmasq config (DHCP + DNS redirect for captive portal)
  cat > "$DNSMASQ_CONF" <<DNSMASQ_EOF
interface=wlan0
listen-address=${AP_IP}
bind-interfaces
dhcp-range=${DHCP_RANGE_START},${DHCP_RANGE_END},${DHCP_LEASE}
address=/#/${AP_IP}
DNSMASQ_EOF

  # Start hostapd
  hostapd "$HOSTAPD_CONF" >> "$LOG_FILE" 2>&1 &
  HOSTAPD_PID=$!
  sleep 2

  if ! kill -0 "$HOSTAPD_PID" 2>/dev/null; then
    log "ERROR: hostapd failed to start (PID $HOSTAPD_PID exited)"
    log_state "ap-failed" "reason=\"hostapd-exit\""
    return 1
  fi

  # Start dnsmasq
  dnsmasq -C "$DNSMASQ_CONF" --no-daemon --log-facility="$LOG_FILE" &
  DNSMASQ_PID=$!
  sleep 1

  if ! kill -0 "$DNSMASQ_PID" 2>/dev/null; then
    log "ERROR: dnsmasq failed to start (PID $DNSMASQ_PID exited)"
    log_state "ap-failed" "reason=\"dnsmasq-exit\""
    return 1
  fi

  # Create the FIFO for connect requests from the HTTP server
  rm -f "$CONNECT_FIFO" "$RESULT_FILE"
  mkfifo "$CONNECT_FIFO"

  # Start HTTP server (captive portal)
  local server_dir="${SCRIPT_DIR}/wifi-provision-server"
  node "$server_dir/server.js" \
    --port 80 \
    --ap-ip "$AP_IP" \
    --scan-cache "$SCAN_CACHE" \
    --connect-fifo "$CONNECT_FIFO" \
    --result-file "$RESULT_FILE" \
    --brand-json "$BRAND_JSON" \
    --hostname "$HOSTNAME_NAME" \
    --portal-host "$PORTAL_HOST" \
    --device-port "$BRAND_PORT" \
    >> "$LOG_FILE" 2>&1 &
  HTTP_SERVER_PID=$!
  sleep 1

  if ! kill -0 "$HTTP_SERVER_PID" 2>/dev/null; then
    log "ERROR: HTTP server failed to start (PID $HTTP_SERVER_PID exited)"
    log_state "ap-failed" "reason=\"http-server-exit\""
    return 1
  fi

  log_state "ap-ready" "ip=\"${AP_IP}\" ssid=\"${SSID}\""
}

# ── Connection handling ──────────────────────────────────��───────────
# The HTTP server writes {ssid}\n{password} to the FIFO when the user
# submits credentials. This function reads the FIFO, attempts connection,
# and writes the result to RESULT_FILE for the HTTP server to read.

handle_connect_requests() {
  while true; do
    if [ ! -p "$CONNECT_FIFO" ]; then
      sleep 1
      continue
    fi

    # Block until the HTTP server writes a connect request
    local request
    request=$(cat "$CONNECT_FIFO" 2>/dev/null) || continue
    [ -z "$request" ] && continue

    local target_ssid target_password
    target_ssid=$(echo "$request" | head -1)
    target_password=$(echo "$request" | tail -1)

    log_state "connecting" "ssid=\"${target_ssid}\""

    # Tear down AP components (keep HTTP server running for status page)
    kill "$HOSTAPD_PID" 2>/dev/null || true
    wait "$HOSTAPD_PID" 2>/dev/null || true
    HOSTAPD_PID=""
    kill "$DNSMASQ_PID" 2>/dev/null || true
    wait "$DNSMASQ_PID" 2>/dev/null || true
    DNSMASQ_PID=""

    # Restore wlan0 to NetworkManager
    ip addr del "${AP_IP}/24" dev wlan0 2>/dev/null || true
    nmcli device set wlan0 managed yes 2>/dev/null
    sleep 2

    # NM's wifi-list cache is empty immediately after wlan0 is handed back
    # from hostapd, so `nmcli device wifi connect` reports "No network with
    # SSID 'X' found" even when hostapd's pre-AP scan saw it. Trigger a
    # rescan and poll until the target SSID appears in the cache (or 15s
    # timeout) before attempting the connect.
    nmcli device wifi rescan 2>/dev/null || true
    local rescan_elapsed=0
    while [ "$rescan_elapsed" -lt 15 ]; do
      if nmcli -t -f SSID device wifi list 2>/dev/null | grep -Fxq "$target_ssid"; then
        break
      fi
      sleep 1
      rescan_elapsed=$((rescan_elapsed + 1))
    done
    log "rescan-before-connect: target=\"${target_ssid}\" elapsed=${rescan_elapsed}s"

    # Attempt WiFi connection. Capture exit code before || true so we
    # get nmcli's actual exit status, not the unconditional 0 from || true.
    local connect_output connect_exit
    # `--wait` / `-w` is a top-level nmcli option (must precede the
    # subcommand), not an argument to `device wifi connect`.
    connect_output=$(nmcli --wait 30 device wifi connect "$target_ssid" password "$target_password" 2>&1)
    connect_exit=$?

    if [ $connect_exit -eq 0 ] && wifi_is_connected; then
      local assigned_ip
      assigned_ip=$(get_wifi_ip)
      log_state "connected" "ssid=\"${target_ssid}\" ip=\"${assigned_ip}\""

      # Restart avahi-daemon so it withdraws any stale "<host>-2.local"
      # auto-renamed records from a previous boot (where hostapd's wlan0
      # transition triggered a self-conflict during withdraw/announce) and
      # re-claims the canonical hostname on the freshly-connected network.
      systemctl restart avahi-daemon 2>/dev/null || true
      log "avahi-daemon restarted to refresh ${HOSTNAME_NAME}.local advertisement"

      # Write success result for the HTTP server
      echo "{\"success\":true,\"ip\":\"${assigned_ip}\",\"hostname\":\"${HOSTNAME_NAME}\"}" > "$RESULT_FILE"

      # Give the HTTP server time to serve the success page
      sleep 10

      log_state "teardown"
      return 0
    else
      log_state "connect-failed" "ssid=\"${target_ssid}\" error=\"${connect_output}\""

      # Write failure result for the HTTP server
      local escaped_error
      escaped_error=$(printf '%s' "$connect_output" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
      echo "{\"success\":false,\"error\":\"${escaped_error}\"}" > "$RESULT_FILE"

      # Re-start AP for retry
      nmcli device set wlan0 managed no 2>/dev/null
      sleep 1
      ip addr flush dev wlan0 2>/dev/null || true
      ip addr add "${AP_IP}/24" dev wlan0
      ip link set wlan0 up

      hostapd "$HOSTAPD_CONF" >> "$LOG_FILE" 2>&1 &
      HOSTAPD_PID=$!
      sleep 2

      dnsmasq -C "$DNSMASQ_CONF" --no-daemon --log-facility="$LOG_FILE" &
      DNSMASQ_PID=$!
      sleep 1

      # Recreate FIFO for next attempt
      rm -f "$CONNECT_FIFO"
      mkfifo "$CONNECT_FIFO"

      log_state "ap-ready" "ip=\"${AP_IP}\" ssid=\"${SSID}\" (retry)"
    fi
  done
}

# ── Main ─────────────────────────────────────────────────────────────

log_state "checking"

# Check for ethernet connectivity — AP not needed
if has_ethernet; then
  log_state "skipped" "reason=\"ethernet\""
  trap - EXIT  # No cleanup needed — nothing was started
  exit 0
fi

# Check for saved WiFi connections
if has_saved_wifi; then
  if wifi_is_connected; then
    log_state "skipped" "reason=\"wifi-connected\""
    trap - EXIT
    exit 0
  fi

  # Fallback path: saved WiFi exists but not connected yet.
  # Wait for NetworkManager to auto-connect (it tries on boot).
  log_state "wifi-waiting" "timeout=${FALLBACK_TIMEOUT}s"
  elapsed=0
  while [ $elapsed -lt $FALLBACK_TIMEOUT ]; do
    if wifi_is_connected; then
      log_state "skipped" "reason=\"wifi-connected-after-wait\" elapsed=${elapsed}s"
      trap - EXIT
      exit 0
    fi
    sleep 5
    elapsed=$((elapsed + 5))
  done

  # Timeout — saved WiFi unreachable. Activate AP for reconfiguration.
  log "saved WiFi unreachable after ${FALLBACK_TIMEOUT}s — activating AP"
fi

# No connectivity — scan and activate AP
log_state "scanning"
scan_networks

if ! start_ap; then
  log "FATAL: could not start AP — exiting"
  exit 1
fi

# Block on connect requests until successful connection
handle_connect_requests

exit 0
