// The reactive net-state store (O2): the single source of truth for the detected
// connectivity/version state, running the pure reducer (netstate.ts, O1) over a `$state` snapshot and
// owning the side-effects the reducer asks for — the probe/recovery watcher, the OS-signal listeners
// and the offline/online toast. connection.svelte.ts and offline.svelte.ts are now thin derived shims
// over this module, so their ~40 consumers keep reading `connection.online` / `offlineMode.active`
// unchanged while a single machine drives them underneath.
//
// The store is a leaf module: it never imports app.svelte.ts. The pieces that need app context — the
// smart recovery (reconcile-or-reachability) and the toast text (i18n) — are injected via
// register*() hooks the bootstrap wires, which also breaks what would otherwise be an import cycle.

import { INITIAL, reduce, type Effect, type NetConfig, type NetEvent, type NetSnapshot } from './netstate';
import { backoffMs } from './retry';

const isMock = import.meta.env.MODE === 'mock';

// Production hysteresis tuning (owner-confirmed). k = 3 consecutive probe failures ⇒ offline (a single
// blip is one failure, so it rides out in `connecting`); debounceMs is the wall-clock backstop for a
// slow, spaced-out failure sequence. Both sit on top of the transport's own retry/backoff.
const cfg: NetConfig = { k: 3, debounceMs: 15000 };
// While offline, re-check reachability on this cadence (mirrors the old app.svelte.ts recovery poll).
const OFFLINE_POLL_MS = 4000;

let snap = $state<NetSnapshot>({ ...INITIAL });

/** netState exposes the reactive detected state; read the getters in markup / `$derived`. */
export const netState = {
  /** state is the raw machine state. */
  get state() {
    return snap.state;
  },
  /** online is true only when the gateway is reachable and the version accepted (full features). */
  get online() {
    return snap.state === 'online';
  },
  /** connecting is true while a call/probe is failing but the anti-flap window has not elapsed. */
  get connecting() {
    return snap.state === 'connecting';
  },
  /** offline is true in either offline state (the kill switch / blue chrome / local-only lobby). */
  get offline() {
    return snap.state === 'offlineNoNetwork' || snap.state === 'offlineVersionLocked';
  },
  /** versionLocked is true when the client is below the hard minimum version (the Update notice). */
  get versionLocked() {
    return snap.state === 'offlineVersionLocked';
  },
};

// Injected hooks (dependency inversion — see the module comment).
let probeFn: (() => Promise<void>) | null = null;
let recoverFn: (() => Promise<void>) | null = null;
let toastFn: ((toast: 'offline' | 'online') => void) | null = null;
let nudgeFn: (() => void) | null = null;

/** registerProbe installs the low-level reachability probe (a cheap authenticated read); the transport
 *  wires it, and checkReachable / the smart recovery use it. It should reject when there is no session. */
export function registerProbe(fn: () => Promise<void>): void {
  probeFn = fn;
}

/** registerRecovery installs the smart recovery the watcher runs while connecting/offline: reconcile a
 *  server guest when there is no session (native), otherwise a reachability check. It resolves when the
 *  gateway is reachable and rejects to stay offline. The bootstrap wires it. */
export function registerRecovery(fn: () => Promise<void>): void {
  recoverFn = fn;
}

/** registerNetToast installs the offline/online toast renderer (i18n text lives in app scope). */
export function registerNetToast(fn: (toast: 'offline' | 'online') => void): void {
  toastFn = fn;
}

/** registerNetNudge installs the soft "update available" latch the setNudge effect fires. It runs only
 *  when the machine accepts a versionRecommended from online (O1), so the nudge never shows offline. */
export function registerNetNudge(fn: () => void): void {
  nudgeFn = fn;
}

// The probe/recovery watcher: one self-rescheduling timer whose cadence follows the current state —
// exponential backoff while `connecting`, a fixed poll while `offlineNoNetwork`, and idle otherwise. It
// is inert under the mock (no real transport); the e2e drives transitions through the window hooks.
let timer: ReturnType<typeof setTimeout> | null = null;
let attempt = 0;
let probing = false;

function stopTimer(): void {
  if (timer) {
    clearTimeout(timer);
    timer = null;
  }
}

function arm(delayMs: number): void {
  if (isMock) return;
  stopTimer();
  timer = setTimeout(runProbe, delayMs);
}

async function runProbe(): Promise<void> {
  timer = null;
  const st = snap.state;
  // Nothing to check when online, and the version lock only exits on an app update (a fresh boot).
  if (st === 'online' || st === 'offlineVersionLocked') return;
  if (probing || !recoverFn) return;
  probing = true;
  try {
    await recoverFn();
    emit('probeOk');
  } catch {
    emit('probeFailed');
    // Reschedule by the (possibly just-changed) state: keep backing off while connecting, poll while
    // offline; stop once online / version-locked (the guard at the top of the next run).
    if (snap.state === 'connecting') arm(backoffMs(Math.min(++attempt, 6)));
    else if (snap.state === 'offlineNoNetwork') arm(OFFLINE_POLL_MS);
  } finally {
    probing = false;
  }
}

/**
 * checkReachable runs the registered reachability probe once, bounded by timeoutMs, and reports whether
 * the gateway answered — a single attempt for the cold-start / recovery decision. It reports false when
 * no probe is registered or the timeout wins.
 */
export async function checkReachable(timeoutMs: number): Promise<boolean> {
  if (!probeFn) return false;
  try {
    await Promise.race([
      probeFn(),
      new Promise<never>((_, reject) => setTimeout(() => reject(new Error('reachability timeout')), timeoutMs)),
    ]);
    return true;
  } catch {
    return false;
  }
}

/** emit applies one event to the machine (stamping it with the current time — the reducer chose
 *  time-on-the-event) and runs the resulting effects. It is the single entry point every signal source
 *  funnels through. */
export function emit(type: NetEvent): void {
  const { next, effects } = reduce(snap, { type, at: Date.now() }, cfg);
  snap = next;
  for (const effect of effects) applyEffect(effect);
}

function applyEffect(effect: Effect): void {
  switch (effect.kind) {
    case 'toast':
      toastFn?.(effect.toast);
      break;
    case 'startProbe':
      // Kick the watcher from the top of the backoff (entering connecting, or an osOnline hint).
      attempt = 0;
      arm(backoffMs(1));
      break;
    case 'showVersionNotice':
      // The Update/Play-offline notice (UpdateOverlay.svelte) reads netState.versionLocked directly, so
      // this effect needs no side-effect here — the state transition is the signal.
      break;
    case 'setNudge':
      // Latch the soft "update available" nudge (update.svelte.ts, wired via registerNetNudge). Fires
      // only from online (O1), so the nudge is never raised while offline / version-locked.
      nudgeFn?.();
      break;
  }
}

/**
 * bootOffline drives the machine straight into offlineNoNetwork for a known-offline launch — a native
 * device-local guest with no server session, or a web cached-session launch the reachability check
 * could not confirm — and arms the recovery poll. It bypasses the connecting hysteresis on purpose:
 * boot is not a mid-session flap, so there is nothing to ride out. Pass kickNow to start the recovery
 * poll immediately (the native reconcile) rather than after the first interval.
 */
export function bootOffline(kickNow = false): void {
  // provisional: this offline is an assumption, not an observation — the first probe result confirms
  // it (silent heal if reachable, so a first launch that had connectivity does not flash a spurious
  // "back online" toast) or turns it into a real offline (see the reducer).
  snap = { state: 'offlineNoNetwork', fails: 0, connectingSince: 0, provisional: true };
  arm(kickNow ? 0 : OFFLINE_POLL_MS);
}

/** initNetSignals wires the web connectivity hints (navigator online/offline) to the machine. The
 *  native @capacitor/network hint is wired separately in native.ts. A no-op under SSR and the mock (the
 *  e2e drives connectivity through the window hooks). */
export function initNetSignals(): void {
  if (typeof window === 'undefined' || isMock) return;
  window.addEventListener('offline', () => emit('osOffline'));
  window.addEventListener('online', () => emit('osOnline'));
}

/** resetNetState returns the machine to online and stops the watcher (e.g. on logout). */
export function resetNetState(): void {
  stopTimer();
  attempt = 0;
  snap = { state: 'online', fails: 0, connectingSince: 0 };
}
