// Global maintenance signal + self-clearing poll. `active` is true while the edge is in a
// planned deploy window (a caddy 503 carried `X-Scrabble-Maintenance`; see maintenance.ts).
// A non-dismissable overlay (MaintenanceOverlay.svelte) covers the app while active, and a
// cheap read is retried on a capped-backoff cadence (mirroring connection.svelte.ts) — the
// first success lifts the overlay, so it can never get stuck even with no other traffic.
// Mock mode never reports maintenance, so it simply stays inactive.

import { backoffMs } from './retry';

let active = $state(false);
let retryHintMs = $state(0);
let pollTimer: ReturnType<typeof setTimeout> | null = null;
let attempt = 0;
let probing = false;
let probe: (() => Promise<void>) | null = null;

export const maintenance = {
  /** active is true while the edge is in a planned maintenance window. */
  get active(): boolean {
    return active;
  },
  /** retryHintMs is the last Retry-After hint in milliseconds (0 when unknown). */
  get retryHintMs(): number {
    return retryHintMs;
  },
};

/** registerMaintenanceProbe installs the reachability read the poll fires while active
 *  (the transport wires a cheap authenticated read). */
export function registerMaintenanceProbe(fn: () => Promise<void>): void {
  probe = fn;
}

/** reportMaintenance raises the overlay and starts the self-clearing poll. Idempotent: a
 *  repeat hit only refreshes the hint, it never restarts a running poll. */
export function reportMaintenance(retryAfterMs: number): void {
  retryHintMs = retryAfterMs;
  if (active) return;
  active = true;
  attempt = 0;
  if (probe) schedulePoll();
}

/** clearMaintenance lowers the overlay and stops the poll without reloading (a reset, or the
 *  e2e hook). Prod recovery goes through maintenanceRecovered instead. */
export function clearMaintenance(): void {
  active = false;
  if (pollTimer) {
    clearTimeout(pollTimer);
    pollTimer = null;
  }
}

/** maintenanceRecovered runs when the edge answers again after a window we were showing.
 *  The deploy that ended the window may have shipped a client-incompatible change (a wire /
 *  schema bump), and the running bundle is the OLD one — so reload to pick up the fresh
 *  client instead of merely hiding the overlay. A no-op unless the overlay was up; the cover
 *  stays over the brief reload flash. In prod recovery is the only way the overlay clears. */
export function maintenanceRecovered(): void {
  if (!active) return;
  active = false;
  if (pollTimer) {
    clearTimeout(pollTimer);
    pollTimer = null;
  }
  if (typeof location !== 'undefined') location.reload();
}

/** retryNow forces an immediate re-check (the overlay's button). It never closes the
 *  overlay itself — only a successful probe does. A no-op while a probe is in flight. */
export function retryNow(): void {
  if (!active || !probe || probing) return;
  if (pollTimer) {
    clearTimeout(pollTimer);
    pollTimer = null;
  }
  attempt = 0;
  runProbe();
}

function schedulePoll(): void {
  pollTimer = setTimeout(runProbe, backoffMs(++attempt));
}

function runProbe(): void {
  pollTimer = null;
  if (!active || !probe || probing) return;
  probing = true;
  probe().then(
    () => {
      probing = false;
      maintenanceRecovered();
    },
    () => {
      probing = false;
      if (active) schedulePoll();
    },
  );
}
