#!/usr/bin/env bash
# Shared pog422_uartd launch/stop for hat_fixture_bridge.sh and hat_fixture_listen.sh.
# Source this file; do not execute directly.
#
# EG 2.2: one daemon per ttyXRUSB. CHANNEL=both starts machine + ois sockets.

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  echo "FATAL: source uartd_common.sh from hat_fixture_bridge.sh or hat_fixture_listen.sh" >&2
  exit 1
fi

# Pi CPU lane for uartd (CPU3 EG) — best-effort; no-op if <4 cores.
_UARTD_COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "${_UARTD_COMMON_DIR}/cpu_layout.sh" ]]; then
  # shellcheck source=cpu_layout.sh
  source "${_UARTD_COMMON_DIR}/cpu_layout.sh"
fi
unset _UARTD_COMMON_DIR

UARTD_PIDS=()
UARTD_LOG_WATCH_PIDS=()

# Match EventLog debug capture default (pog422_core.DEBUG_LOG_DEFAULT_MAX_BYTES).
UARTD_LOG_MAX_BYTES="${UARTD_LOG_MAX_BYTES:-$((25 * 1024 * 1024))}"
UARTD_LOG_ROTATE_POLL_S="${UARTD_LOG_ROTATE_POLL_S:-30}"
UARTD_LOG_PRUNE_AGE_S="${UARTD_LOG_PRUNE_AGE_S:-$((7 * 86400))}"

# Empty capture dirs in place (keeps data/ and logs/ themselves).
# Removes rotated events.jsonl.* / packets.raw.* and any other leftovers —
# CLEAR_CAPTURE used to only unlink a short allowlist.
#
# Preserve out-of-band companion instruments that are open in another terminal
# (hs_gpio_logger). A full wipe while that fd is open orphans the inode: the logger
# keeps counting edges, but ls/rclone never see the file again.
#
# Only a *live* holder earns the exemption. A leftover hs_gpio capture with nobody
# writing it is stale from a previous run, and keeping it would mix old edges into
# this session's archive — worse than deleting it, because the analyzer would
# silently correlate HS timing against the wrong events.jsonl.
pog422_wipe_capture_always_keep() {
  local base
  base="$(basename "$1")"
  case "${base}" in
    pogcc-com.log|pogcc-com.log.*) return 0 ;;
  esac
  return 1
}

pog422_wipe_capture_candidate() {
  local base
  base="$(basename "$1")"
  case "${base}" in
    hs_gpio_machine.jsonl|hs_gpio_*.jsonl|hs_gpio_*.jsonl.*) return 0 ;;
  esac
  return 1
}

# True when some process holds this exact inode open. Compares dev/inode rather
# than readlink text, since an already-unlinked fd reports "path (deleted)".
pog422_capture_entry_in_use() {
  local target="$1" want fd got
  want="$(stat -c '%d %i' "${target}" 2>/dev/null)" || return 1
  for fd in /proc/[0-9]*/fd/*; do
    [[ -L "${fd}" ]] || continue
    got="$(stat -L -c '%d %i' "${fd}" 2>/dev/null)" || continue
    [[ "${got}" == "${want}" ]] && return 0
  done
  return 1
}

pog422_wipe_capture_preserve() {
  pog422_wipe_capture_candidate "$1" || return 1
  pog422_capture_entry_in_use "$1" || return 1
  return 0
}

pog422_wipe_capture_dirs() {
  local d entry
  for d in "$@"; do
    [[ -n "${d}" ]] || continue
    mkdir -p "${d}"
    shopt -s nullglob
    for entry in "${d}"/* "${d}"/.[!.]* "${d}"/..?*; do
      [[ -e "${entry}" || -L "${entry}" ]] || continue
      if pog422_wipe_capture_always_keep "${entry}"; then
        echo "CLEAR_CAPTURE preserve (pogcc-com log): ${entry}" >&2
        continue
      fi
      if pog422_wipe_capture_preserve "${entry}"; then
        echo "CLEAR_CAPTURE preserve (open by a live logger): ${entry}" >&2
        continue
      fi
      if pog422_wipe_capture_candidate "${entry}"; then
        echo "CLEAR_CAPTURE removing stale companion capture: ${entry}" >&2
      fi
      rm -rf "${entry}" 2>/dev/null || true
    done
    shopt -u nullglob
  done
}

# Prune uartd.log.* / uartd_*.log.* older than UARTD_LOG_PRUNE_AGE_S (0 = keep all).
pog422_prune_uartd_log_rollovers() {
  local log_dir="$1"
  local age_s="${UARTD_LOG_PRUNE_AGE_S}"
  local f now
  [[ -d "${log_dir}" ]] || return 0
  if [[ "${age_s}" -le 0 ]]; then
    return 0
  fi
  now="$(date +%s)"
  shopt -s nullglob
  for f in "${log_dir}"/uartd.log.* "${log_dir}"/uartd_*.log.*; do
    [[ -f "${f}" ]] || continue
    # Skip if newer than cutoff (mtime).
    if [[ "$(( now - $(stat -c '%Y' "${f}" 2>/dev/null || echo "${now}") ))" -gt "${age_s}" ]]; then
      rm -f "${f}" 2>/dev/null || true
    fi
  done
  shopt -u nullglob
}

# Background: when active uartd log hits UARTD_LOG_MAX_BYTES, copytruncate to
# <name>.YYYYMMDDTHHMMSSZ so the open >> redirect keeps the same inode.
pog422_watch_uartd_log_rotate() {
  local path="$1"
  local daemon_pid="$2"
  local max_b="${UARTD_LOG_MAX_BYTES}"
  local poll_s="${UARTD_LOG_ROTATE_POLL_S}"
  local sz stamp dest n

  while kill -0 "${daemon_pid}" 2>/dev/null; do
    sleep "${poll_s}"
    [[ -f "${path}" ]] || continue
    sz="$(stat -c '%s' "${path}" 2>/dev/null || echo 0)"
    if [[ "${sz}" -lt "${max_b}" ]]; then
      continue
    fi
    stamp="$(date -u +%Y%m%dT%H%M%SZ)"
    dest="${path}.${stamp}"
    n=1
    while [[ -e "${dest}" ]]; do
      dest="${path}.${stamp}.${n}"
      n=$((n + 1))
    done
    # copytruncate — uartd still holds the open fd from shell redirect.
    if cp -a "${path}" "${dest}" 2>/dev/null; then
      : >"${path}"
      echo "uartd log rotated: $(basename "${dest}") (${sz} bytes)" >&2
      pog422_prune_uartd_log_rollovers "$(dirname "${path}")"
    fi
  done
}

pog422_stop_uartd() {
  local pid i
  for pid in "${UARTD_LOG_WATCH_PIDS[@]:-}"; do
    if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
      kill -TERM "${pid}" 2>/dev/null || true
    fi
  done
  UARTD_LOG_WATCH_PIDS=()
  for pid in "${UARTD_PIDS[@]:-}"; do
    if [[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null; then
      kill -TERM "${pid}" 2>/dev/null || true
      # USB skb wait can ignore SIGTERM — don't hang the shell on Ctrl-C.
      for i in 1 2 3 4 5; do
        kill -0 "${pid}" 2>/dev/null || break
        sleep 0.2
      done
      if kill -0 "${pid}" 2>/dev/null; then
        # SIGKILL on pog422_uartd wedges the XR USB driver until a hard power
        # cycle. Default: leave the daemon; listen will attach to the live socket.
        if [[ "${ETHERGOLD_UARTD_ALLOW_SIGKILL:-0}" == "1" ]]; then
          echo "WARN: SIGKILL pog422_uartd pid ${pid} (ETHERGOLD_UARTD_ALLOW_SIGKILL=1)" >&2
          kill -KILL "${pid}" 2>/dev/null || true
          wait "${pid}" 2>/dev/null || true
        else
          echo "WARN: pog422_uartd pid ${pid} ignored SIGTERM — leaving it up (SIGKILL bricks XR USB)" >&2
        fi
      else
        wait "${pid}" 2>/dev/null || true
      fi
    fi
  done
  UARTD_PIDS=()
  UARTD_PID=""
}

# Print who owns a ttyXRUSB so "port in use" is actionable (not a mystery crash).
pog422_diagnose_uart_busy() {
  local uart_port="$1"
  echo "--- UART busy diagnostics for ${uart_port} ---" >&2
  if command -v fuser >/dev/null 2>&1; then
    fuser -v "${uart_port}" 2>&1 || true
  else
    echo "(fuser not installed — cannot list openers)" >&2
  fi
  echo "Related processes (supervisor / bridge / listen / uartd / collector):" >&2
  ps -ef 2>/dev/null | grep -E 'ethergold_listen_supervisor|fixture_supervisor|pog422_fixture_bridge|pog422_bridge|pog422_listen|pog422_fixture_listen|hat_fixture_|pog422_uartd|run_listen|run_bridge' | grep -v grep || echo "  (none matched)" >&2
  echo "systemd units that often hold the hat UART:" >&2
  systemctl is-active ethergold-collector.service ethergold.service fixture-collector.service fixture-prod.service 2>/dev/null || true
  systemctl list-units --type=service --all 2>/dev/null | grep -iE 'fixture|pog422|ethergold' || true
  echo "Typical fix (witness/tip must not share UART with prod collector):" >&2
  echo "  sudo systemctl stop ethergold-collector.service ethergold.service fixture-prod.service" >&2
  echo "  # permanent witness lock: touch /var/lib/ethergold/WITNESS_NO_PROD" >&2
  echo "  pkill -f '/EtherGold/prod/ethergold_listen_supervisor' || true" >&2
  echo "  pkill -f 'pog422_uartd' || true" >&2
  echo "Then re-run tip listen (ALLOW_UART_SHARE=1 only if intentional)." >&2
  echo "-------------------------------------------" >&2
}

# Start one daemon: ROOT PORT_CHANNEL SOCK_CHANNEL FIXTURE_POS LOG_DIR
# PORT_CHANNEL = machine|ois (which ttyXRUSB)
# SOCK_CHANNEL = machine|ois|""  ("" → legacy /tmp/pog422_uart_N.sock)
pog422_start_uartd_one() {
  local root="$1" port_channel="$2" sock_channel="$3" fixture_pos="$4" log_dir="$5"
  local uart_sock uart_port log_name ready i pid

  if [[ -n "${sock_channel}" ]]; then
    uart_sock="/tmp/pog422_uart_${fixture_pos}_${sock_channel}.sock"
    log_name="uartd_${sock_channel}.log"
    case "${sock_channel}" in
      machine) export POG422_UARTD_SOCK_MACHINE="${uart_sock}" ;;
      ois) export POG422_UARTD_SOCK_OIS="${uart_sock}" ;;
    esac
  else
    uart_sock="${POG422_UARTD_SOCK:-/tmp/pog422_uart_${fixture_pos}.sock}"
    log_name="uartd.log"
    export POG422_UARTD_SOCK="${uart_sock}"
  fi

  uart_port="$(
    PYTHONPATH="${root}${PYTHONPATH:+:${PYTHONPATH}}" python3 -c \
      "from pog422_fixture_core import resolve_channel, resolve_serial_port; c=resolve_channel('${port_channel}'); print(resolve_serial_port(c, None))"
  )"
  if command -v fuser >/dev/null 2>&1 && fuser -s "${uart_port}" 2>/dev/null; then
    if [[ -S "${uart_sock}" ]]; then
      echo "WARN: ${uart_port} already owned — attaching to live uartd socket ${uart_sock}" >&2
      pog422_diagnose_uart_busy "${uart_port}" || true
      export POG422_UARTD_SOCK="${uart_sock}"
      export POG422_UARTD_PORT="${uart_port}"
      export POG422_UARTD_CHANNEL="${port_channel}"
      export POG422_UARTD_LOG="${log_dir}/${log_name}"
      export POG422_UARTD_BIN="${root}/inject-shim/pog422_uartd"
      return 0
    fi
    echo "FATAL: ${uart_port} in use — another process already owns the HAT UART." >&2
    echo "This is not a FIXTURE_POS bug; listen cannot share the port with prod bridge/uartd." >&2
    pog422_diagnose_uart_busy "${uart_port}"
    return 1
  fi
  if [[ -e "${uart_sock}" ]] && ! rm -f "${uart_sock}" 2>/dev/null; then
    echo "FATAL: stale socket ${uart_sock} — remove with: sudo rm -f ${uart_sock}" >&2
    return 1
  fi
  echo "Starting pog422_uartd port=${uart_port} channel=${port_channel} socket=${uart_sock}" >&2
  : > "${log_dir}/${log_name}"
  if type ohd_eg_run >/dev/null 2>&1; then
    ohd_eg_run "${root}/inject-shim/pog422_uartd" --port "${uart_port}" --channel "${port_channel}" --socket "${uart_sock}" >> "${log_dir}/${log_name}" 2>&1 &
  else
    "${root}/inject-shim/pog422_uartd" --port "${uart_port}" --channel "${port_channel}" --socket "${uart_sock}" >> "${log_dir}/${log_name}" 2>&1 &
  fi
  pid=$!
  UARTD_PIDS+=("${pid}")
  UARTD_PID="${pid}"
  # EG 3.0.1.3: bridge adopts these for auto-reinit after USB/uartd death (pos5 CGS).
  export POG422_UARTD_PID="${pid}"
  export POG422_UARTD_PORT="${uart_port}"
  export POG422_UARTD_CHANNEL="${port_channel}"
  export POG422_UARTD_LOG="${log_dir}/${log_name}"
  export POG422_UARTD_BIN="${root}/inject-shim/pog422_uartd"
  # Mid-soak size rotate (default 25 MiB) — copytruncate so >> keeps writing.
  pog422_watch_uartd_log_rotate "${log_dir}/${log_name}" "${pid}" &
  UARTD_LOG_WATCH_PIDS+=("$!")
  ready=0
  for i in $(seq 1 50); do
    if [[ -S "${uart_sock}" ]]; then
      ready=1
      break
    fi
    if ! kill -0 "${pid}" 2>/dev/null; then
      break
    fi
    sleep 0.1
  done
  if [[ "${ready}" != "1" ]]; then
    echo "FATAL: pog422_uartd socket not ready: ${uart_sock}" >&2
    if kill -0 "${pid}" 2>/dev/null; then
      echo "  daemon pid ${pid} still running but no socket — check /tmp permissions" >&2
      kill "${pid}" 2>/dev/null || true
    else
      echo "  daemon exited — see ${log_dir}/${log_name}:" >&2
      sed -n '1,40p' "${log_dir}/${log_name}" >&2 || true
      if grep -qiE 'busy|in use|Permission denied|Resource temporarily' "${log_dir}/${log_name}" 2>/dev/null; then
        pog422_diagnose_uart_busy "${uart_port}"
      fi
    fi
    return 1
  fi
  return 0
}

# pog422_start_uartd ROOT FIXTURE_POS CHANNEL LOG_DIR
# CHANNEL=both -> two daemons (machine + ois). Else one daemon on CHANNEL port.
pog422_start_uartd() {
  local root="$1" fixture_pos="$2" channel="$3" log_dir="$4"
  if command -v make >/dev/null 2>&1; then
    # Honor HW_DEBUG from the operator shell (bench dense timeline). Default 0.
    local hw_debug="${HW_DEBUG:-0}"
    echo "Building pog422_uartd (HW_DEBUG=${hw_debug})..." >&2
    make -C "${root}/inject-shim" HW_DEBUG="${hw_debug}" pog422_uartd
  elif [[ ! -x "${root}/inject-shim/pog422_uartd" ]]; then
    echo "FATAL: pog422_uartd missing and make not found" >&2
    return 1
  fi
  if [[ -x "${root}/inject-shim/pog422_uartd" ]]; then
    local built_ver
    built_ver="$("${root}/inject-shim/pog422_uartd" --help 2>&1 | head -1 || true)"
    echo "pog422_uartd binary: ${built_ver:-built}" >&2
  fi

  export POG422_UARTD=1
  export POG422_UARTD_INJECT="${POG422_UARTD_INJECT:-1}"
  if [[ "${channel}" == "both" ]]; then
    pog422_start_uartd_one "${root}" "machine" "machine" "${fixture_pos}" "${log_dir}" || return 1
    pog422_start_uartd_one "${root}" "ois" "ois" "${fixture_pos}" "${log_dir}" || return 1
    unset POG422_UARTD_SOCK || true
  else
    # Single channel: legacy /tmp/pog422_uart_N.sock; port follows CHANNEL.
    # Phase 3: if port busy, brief backoff (hardening) before FATAL.
    local tries=0
    while true; do
      tries=$((tries + 1))
      if pog422_start_uartd_one "${root}" "${channel}" "" "${fixture_pos}" "${log_dir}"; then
        break
      fi
      if [[ "${tries}" -ge 3 ]]; then
        return 1
      fi
      echo "WARN: uartd start failed (UART busy/contention?) try ${tries}/3 — backoff" >&2
      sleep 2
    done
  fi
  return 0
}
