/**
 * Native-tunnel health probe — TASK B4/H4 (PLAN_TUNNEL_AUTOMATION §3.2 / §5, INV9).
 *
 * Reports four independent sub-checks that together say whether this host's native frp tunnel is
 * actually serving:
 *   (a) frpcAlive        — the supervised frpc child process is running;
 *   (b) baseAppReachable — the loopback base app answers `GET http://127.0.0.1:PORT` (loopback ONLY);
 *   (c) proxyStarted     — frpc logged a "start proxy success" line (control channel + proxy up);
 *   (d) certFresh        — the frp-client leaf's `notAfter` is beyond the renewal window (not expiring).
 *
 * Every side effect is an INJECTABLE seam (process-liveness checker, loopback HTTP probe, log
 * scanner, clock) so the logic is pure and offline-testable. The status renderer emits ONLY
 * non-secret identifiers (subdomain, host id, cert expiry date, boolean flags) — NEVER keys, certs,
 * tokens, or CSRs (INV9). No `console.log`.
 */

/** Default renewal window: 8h — one third of the 24h host frp-client TTL (renew at ~2/3 TTL). */
export const DEFAULT_CERT_RENEW_WINDOW_MS = 8 * 60 * 60 * 1000

/** frpc emits this line on the control channel once a proxy is registered and forwarding. */
export const FRPC_START_SUCCESS_RE = /start proxy success/i

/** Loopback host the base-app probe targets — hardcoded so the probe can never reach off-host. */
export const LOOPBACK_PROBE_HOST = '127.0.0.1'

const MIN_PORT = 1
const MAX_PORT = 65535

/** The four independent sub-checks plus the derived overall verdict. */
export interface HealthReport {
  /** The supervised frpc child is running. */
  readonly frpcAlive: boolean
  /** `GET http://127.0.0.1:PORT` returned a response (base app is up on loopback). */
  readonly baseAppReachable: boolean
  /** frpc logged "start proxy success" (the tunnel proxy is registered). */
  readonly proxyStarted: boolean
  /** The frp-client cert's `notAfter` is beyond the renewal window (not near expiry). */
  readonly certFresh: boolean
  /** True iff all four sub-checks pass. */
  readonly healthy: boolean
}

/** Injectable side effects the probe consumes; each is independently faked in tests. */
export interface HealthProbeSeams {
  /** Process-liveness checker (the supervisor exposes whether its frpc child is alive). */
  readonly isFrpcAlive: () => boolean
  /** Loopback HTTP probe of the base app; resolves true iff it answered ok. */
  readonly probeBaseApp: () => Promise<boolean>
  /** Returns the accumulated frpc stdout/log text to scan for the success line. */
  readonly readFrpcLog: () => string
  /** The stored frp-client leaf's `notAfter`, or null if no cert is available. */
  readonly certNotAfter: () => Date | null
  /** Current time (injected for deterministic expiry tests). */
  readonly now: () => Date
}

export interface HealthProbeConfig {
  /** Certs within this many ms of `notAfter` are "near expiry" (default 8h). */
  readonly renewWindowMs?: number
}

/** Pure: true iff the frpc log contains a "start proxy success" line. */
export function frpcProxyStarted(logText: string): boolean {
  return FRPC_START_SUCCESS_RE.test(logText)
}

/** Pure: true iff `notAfter` is strictly beyond the renewal window from `now` (not near expiry). */
export function certIsFresh(notAfter: Date | null, now: Date, renewWindowMs: number): boolean {
  if (notAfter === null) return false
  return notAfter.getTime() - now.getTime() > renewWindowMs
}

/** Minimal shape of a fetch response the loopback probe cares about. */
export interface LoopbackResponse {
  readonly ok: boolean
}

/** Injectable loopback HTTP fetch (real wiring passes global `fetch`). */
export type LoopbackFetch = (url: string) => Promise<LoopbackResponse>

/**
 * Probe the loopback base app with `GET http://127.0.0.1:PORT/`. The host is hardcoded loopback, so
 * the probe can never reach an off-host target (anti-SSRF). Any thrown/rejected fetch — or a
 * non-integer/out-of-range port — resolves to `false` rather than propagating (a probe never throws).
 */
export async function probeLoopbackBaseApp(port: number, fetchImpl: LoopbackFetch): Promise<boolean> {
  if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) return false
  const url = `http://${LOOPBACK_PROBE_HOST}:${port}/`
  try {
    const res = await fetchImpl(url)
    return res.ok
  } catch {
    return false
  }
}

/** Run all four sub-checks and derive the overall verdict. Never throws (each seam is guarded). */
export async function runHealthProbe(
  seams: HealthProbeSeams,
  config: HealthProbeConfig = {},
): Promise<HealthReport> {
  const renewWindowMs = config.renewWindowMs ?? DEFAULT_CERT_RENEW_WINDOW_MS
  const frpcAlive = seams.isFrpcAlive()
  const baseAppReachable = await seams.probeBaseApp()
  const proxyStarted = frpcProxyStarted(seams.readFrpcLog())
  const certFresh = certIsFresh(seams.certNotAfter(), seams.now(), renewWindowMs)
  const healthy = frpcAlive && baseAppReachable && proxyStarted && certFresh
  return { frpcAlive, baseAppReachable, proxyStarted, certFresh, healthy }
}

/** Non-secret identifiers safe to print in `status` (INV9): NO key/cert/token/CSR material. */
export interface StatusIdentifiers {
  readonly subdomain: string | null
  readonly hostId: string | null
  readonly certNotAfter: Date | null
}

/**
 * Render `status` lines from non-secret identifiers + a health report (INV9). Emits the subdomain,
 * host id, cert EXPIRY DATE (never the cert bytes), and the boolean sub-check flags — never any key,
 * cert, token, or CSR material.
 */
export function renderHealthStatus(
  ids: StatusIdentifiers,
  report: HealthReport,
): readonly string[] {
  return [
    `subdomain: ${ids.subdomain ?? '(none)'}`,
    `host_id: ${ids.hostId ?? '(none)'}`,
    `cert_expiry: ${ids.certNotAfter ? ids.certNotAfter.toISOString() : '(unknown)'}`,
    `frpc_alive: ${report.frpcAlive}`,
    `base_app_reachable: ${report.baseAppReachable}`,
    `proxy_started: ${report.proxyStarted}`,
    `cert_fresh: ${report.certFresh}`,
    `healthy: ${report.healthy}`,
  ]
}

/** Default periodic health-monitor interval (30s). */
export const DEFAULT_HEALTH_INTERVAL_MS = 30_000

/** Minimal injectable interval timer (fake-timer-testable). */
export interface IntervalTimer {
  setInterval(cb: () => void, ms: number): unknown
  clearInterval(handle: unknown): void
}

const realIntervalTimer: IntervalTimer = {
  setInterval: (cb, ms) => setInterval(cb, ms),
  clearInterval: (h) => clearInterval(h as ReturnType<typeof setInterval>),
}

/** Handle to a running health monitor. */
export interface HealthMonitor {
  stop(): void
}

/**
 * Start a periodic health monitor: every `intervalMs`, run `probe()` and hand the report to
 * `onReport` (the run-loop wires this to a redacting logger — non-secret lines only). A rejected
 * probe is swallowed (a monitor must never crash the supervisor). `stop()` clears the interval.
 */
export function startHealthMonitor(
  probe: () => Promise<HealthReport>,
  onReport: (report: HealthReport) => void,
  opts: { intervalMs?: number; timer?: IntervalTimer } = {},
): HealthMonitor {
  const intervalMs = opts.intervalMs ?? DEFAULT_HEALTH_INTERVAL_MS
  const timer = opts.timer ?? realIntervalTimer
  const handle = timer.setInterval(() => {
    void probe()
      .then(onReport)
      .catch(() => {
        /* a probe failure is itself an unhealthy signal; never let it crash the monitor */
      })
  }, intervalMs)
  return {
    stop(): void {
      timer.clearInterval(handle)
    },
  }
}
