/**
 * SET 58 — the shell's single data loop. 100% NEW software.
 *
 * Reads the EXISTING extension channels verbatim (tier1:meta / live / status /
 * runs / record) and sends the same verbs the old UI sent (tier1:run / cancel /
 * export). Writes nothing anywhere else — no storage, no schema, no SW change.
 *
 * First law: no silent failure. Every channel error is recorded with a
 * timestamp into a diagnostics ring that the page RENDERS.
 */

export type Diag = { ts: string; channel: string; ok: boolean; note: string };

export type ShellRunEntry = {
  id: string; ts?: string; prompt?: string; model?: string | null;
  status?: string; projectId?: string | null;
};

export type ShellEvent = { ts: string; phase: string; featureId: string | null; message: string };

/** SET 68 — judgment-gate evidence card surfaced on tier1:status while paused. */
export type JudgmentCard = {
  featureId: string; caseName: string; classKey: string;
  assertion: string; expected?: string; received?: string;
  exhibit?: string; trace?: string; licenseSearch: string;
};

export type ShellStatus = {
  id?: string | null; running?: boolean; hasResult?: boolean;
  paused?: boolean; pausedGate?: string | null;
  judgmentCard?: JudgmentCard | null;
  prompt?: string | null; model?: string | null; mode?: string;
  events?: ShellEvent[]; metrics?: Record<string, unknown> | null;
  cost?: { usdTotal?: number | null } | null; error?: string | null;
  projectId?: string | null; vaultVersion?: string | null;
  contractVersion?: string | null;
};

export type ShellRecord = ShellStatus & {
  ts?: string; finalized?: boolean; status?: string | null; checkpointAt?: string | null;
  persistError?: string | null;
};

export type ShellState = {
  ext: 'unknown' | 'present' | 'absent';
  meta: { unlocked?: boolean; running?: boolean; vaultVersion?: string;
    active?: { name?: string; model?: string } | null } | null;
  liveId: string | null;
  liveRunning: boolean;
  status: ShellStatus | null;
  runs: ShellRunEntry[];
  records: Record<string, ShellRecord | 'missing' | 'loading'>;
  viewingId: string | null;
  diags: Diag[];
  lastError: string | null;
};

type Listener = () => void;

const state: ShellState = {
  ext: 'unknown', meta: null, liveId: null, liveRunning: false,
  status: null, runs: [], records: {}, viewingId: null, diags: [], lastError: null,
};

const listeners = new Set<Listener>();
let snapshot: ShellState = { ...state };

function publish(): void {
  snapshot = { ...state, records: { ...state.records }, diags: [...state.diags], runs: [...state.runs] };
  for (const l of listeners) l();
}

export function subscribe(l: Listener): () => void {
  listeners.add(l);
  return () => listeners.delete(l);
}

export function getSnapshot(): ShellState {
  return snapshot;
}

export function noteDiag(channel: string, ok: boolean, note: string): void {
  state.diags.push({ ts: new Date().toISOString(), channel, ok, note: note.slice(0, 300) });
  if (state.diags.length > 60) state.diags.splice(0, state.diags.length - 60);
  if (!ok) state.lastError = `${channel}: ${note.slice(0, 200)}`;
}

/** The extension messaging seam — same handshake globals the site has always
 * used; injectable nowhere, observable everywhere (every call lands in diags). */
declare global {
  interface Window {
    __RHA_EXT__?: { version?: string; hash?: string; extensionId?: string };
  }
}

function ext<T>(msg: Record<string, unknown>): Promise<T> {
  return new Promise((resolve, reject) => {
    const id = window.__RHA_EXT__?.extensionId;
    const rt = (window as unknown as {
      chrome?: { runtime?: {
        sendMessage?: (extId: string, m: unknown, cb: (r: unknown) => void) => void;
        lastError?: { message?: string };
      } };
    }).chrome?.runtime;
    if (!id || !rt?.sendMessage) { reject(new Error('extension not available')); return; }
    try {
      rt.sendMessage(id, msg, (r: unknown) => {
        const err = rt.lastError;
        if (err) { reject(new Error(err.message || 'extension unreachable')); return; }
        const resp = r as { error?: string } | undefined;
        if (resp?.error) { reject(new Error(resp.error)); return; }
        resolve(r as T);
      });
    } catch (e) { reject(e as Error); }
  });
}

/** Invalidates in-flight channel responses across stop/reset — a late resolve
 * from a torn-down loop must never mutate the current state. */
let generation = 0;

async function poll(
  channel: string,
  msg: Record<string, unknown>,
  apply: (r: unknown) => string | void,
): Promise<boolean> {
  const g = generation;
  try {
    const r = await ext<unknown>(msg);
    if (g !== generation) return false; // stale response — discard
    const note = apply(r);
    noteDiag(channel, true, note || 'ok');
    return true;
  } catch (e) {
    if (g !== generation) return false;
    noteDiag(channel, false, e instanceof Error ? e.message : String(e));
    return false;
  }
}

let timer: ReturnType<typeof setInterval> | null = null;
let slowTimer: ReturnType<typeof setInterval> | null = null;

/**
 * Extension handshake — BOTH signals the content script emits:
 *  1. `document.documentElement.dataset.rhaExt` (set at document_start, so it
 *     survives any mount-order race), and
 *  2. the `rha-ext-meta` postMessage (fires before React mounts — listener
 *     installed in startStore for late/re-announcements).
 * Either one hydrates `window.__RHA_EXT__` so messaging learns the id.
 */
function detectExt(): void {
  if (window.__RHA_EXT__?.extensionId) { state.ext = 'present'; return; }
  try {
    const raw = document.documentElement.dataset.rhaExt;
    if (raw) {
      const j = JSON.parse(raw) as { version?: string; extensionId?: string };
      if (j?.extensionId) {
        window.__RHA_EXT__ = { ...window.__RHA_EXT__, version: j.version, extensionId: j.extensionId };
        if (state.ext !== 'present') noteDiag('handshake', true, `dataset: v${j.version ?? '?'}`);
        state.ext = 'present';
        return;
      }
    }
  } catch (e) {
    noteDiag('handshake', false, `dataset parse: ${e instanceof Error ? e.message : String(e)}`);
  }
  state.ext = state.ext === 'present' ? 'present' : 'absent';
}

function onExtAnnounce(ev: MessageEvent): void {
  const d = ev.data as { type?: string; source?: string; version?: string; extensionId?: string } | null;
  if (!d || d.type !== 'rha-ext-meta' || d.source !== 'rha-provider-vault' || !d.extensionId) return;
  window.__RHA_EXT__ = { ...window.__RHA_EXT__, version: d.version, extensionId: d.extensionId };
  if (state.ext !== 'present') {
    state.ext = 'present';
    noteDiag('handshake', true, `postMessage: v${d.version ?? '?'}`);
    publish();
    void tick();
    void slowTick();
  }
}

let statusFailStreak = 0;

async function tick(): Promise<void> {
  detectExt();
  if (state.ext !== 'present') { publish(); return; }
  await poll('tier1:meta', { action: 'tier1:meta' }, r => { state.meta = r as ShellState['meta']; });
  await poll('tier1:live', { action: 'tier1:live' }, r => {
    const v = r as { liveId?: string | null; running?: boolean };
    state.liveId = v?.liveId ?? null;
    state.liveRunning = !!v?.running;
    return `liveId=${v?.liveId ?? '-'} running=${!!v?.running}`;
  });
  const okStatus = await poll('tier1:status', { action: 'tier1:status' }, r => {
    const st = r as ShellStatus;
    state.status = st;
    return `id=${st?.id ?? '-'} running=${!!st?.running} events=${st?.events?.length ?? 0}`;
  });
  // SET 58c — resilience ladder: tier1:status is the battle-tested channel and
  // OVERRIDES tier1:live whenever they disagree (56a was never
  // browser-verified; a broken live channel must not blind the shell).
  if (okStatus) {
    statusFailStreak = 0;
    const st = state.status;
    if (st?.running && st.id) {
      state.liveId = String(st.id);
      state.liveRunning = true;
    } else if (st && !st.running && state.liveRunning && String(st.id ?? '') === (state.liveId ?? '')) {
      state.liveRunning = false;
    }
  } else {
    statusFailStreak++;
    if (statusFailStreak >= 3 && state.liveRunning) state.liveRunning = false;
  }
  // A live job takes the screen unless the user pinned another run.
  if (state.liveRunning && state.liveId && state.viewingId == null) state.viewingId = state.liveId;
  publish();
}

async function slowTick(): Promise<void> {
  if (state.ext !== 'present') return;
  await poll('tier1:runs', { action: 'tier1:runs' }, r => {
    const v = r as { runs?: ShellRunEntry[] };
    state.runs = Array.isArray(v?.runs) ? v.runs : [];
    return `${state.runs.length} runs`;
  });
  // SET 58c — checkpoint-follow: when the viewed run is still unfinalized and
  // the live/status channels are not serving it, re-read its record so the
  // log advances at checkpoint cadence even with both live channels dead.
  const vid = state.viewingId;
  if (vid && (state.status?.id !== vid || statusFailStreak > 0)) {
    const rec = state.records[vid];
    const unfinalized = rec && rec !== 'missing' && rec !== 'loading'
      && (rec.running || rec.status === 'running' || rec.finalized !== true);
    if (unfinalized || rec === 'missing') {
      delete state.records[vid];
      fetchRecord(vid);
    }
  }
  publish();
}

export function startStore(): void {
  if (timer) return;
  window.addEventListener('message', onExtAnnounce);
  void tick();
  void slowTick();
  timer = setInterval(() => void tick(), 1000);
  slowTimer = setInterval(() => void slowTick(), 2500);
}

export function stopStore(): void {
  generation++;
  window.removeEventListener('message', onExtAnnounce);
  if (timer) { clearInterval(timer); timer = null; }
  if (slowTimer) { clearInterval(slowTimer); slowTimer = null; }
}

/** Test-only: restore the module singleton to its boot state. */
export function __resetStoreForTests(): void {
  stopStore();
  state.ext = 'unknown'; state.meta = null; state.liveId = null; state.liveRunning = false;
  state.status = null; state.runs = []; state.records = {}; state.viewingId = null;
  state.diags = []; state.lastError = null;
  publish();
}

function fetchRecord(id: string): void {
  if (state.records[id]) return;
  state.records[id] = 'loading';
  const g = generation;
  void ext<ShellRecord>({ action: 'tier1:record', runId: id })
    .then(r => {
      if (g !== generation) return; // stale — discard
      state.records[id] = r && (r.events?.length || r.metrics || r.prompt) ? r : 'missing';
      noteDiag('tier1:record', true, `${id}: ${(r?.events?.length ?? 0)} events`);
      publish();
    })
    .catch(e => {
      if (g !== generation) return;
      state.records[id] = 'missing';
      noteDiag('tier1:record', false, `${id}: ${e instanceof Error ? e.message : String(e)}`);
      publish();
    });
}

export function selectRun(id: string | null): void {
  state.viewingId = id;
  if (id && id !== state.liveId) fetchRecord(id);
  publish();
}

export function refreshRecord(id: string): void {
  delete state.records[id];
  selectRun(id);
}

export async function compile(prompt: string, mode: 'rha' | 'vanilla', supervised: boolean): Promise<void> {
  await ext({ action: 'tier1:run', payload: { prompt, mode, supervised } });
  state.viewingId = null; // follow live via status until liveId arrives
  publish();
}

export function resumeRun(
  decision?: { caseName: string; action: 'amend' | 'direct' | 'drop'; intentText?: string },
): Promise<unknown> {
  return ext(decision ? { action: 'tier1:resume', decision } : { action: 'tier1:resume' });
}

export async function continueProject(prompt: string, fromId: string, mode: 'rha' | 'vanilla'): Promise<void> {
  await ext({ action: 'tier1:run', payload: { prompt, continueFrom: fromId, mode } });
  state.viewingId = null;
  publish();
}

export function cancelRun(): Promise<unknown> {
  return ext({ action: 'tier1:cancel' });
}

/**
 * SET 79 — download the built artifact as a real git repository (working tree
 * + .git + .supergit ledger). The SW already had the path; nothing surfaced it.
 */
export function downloadArtifact(): Promise<unknown> {
  return ext({ action: 'tier1:export' });
}

export function exportRun(): Promise<unknown> {
  return ext({ action: 'tier1:export' });
}
