/**
 * Server-side state for an in-flight Scan-now run. Mirrors dotpm_v2's
 * principle: server owns run state, clients observe. Client polls
 * /api/now/scan-status; on page reload it can rehydrate the "Scanning…"
 * indicator instead of pretending nothing's happening.
 *
 * In-memory copy is authoritative. The JSON file is only consulted at
 * server boot — if `running: true` is found there, we infer the prior
 * process crashed mid-scan and reset to idle. (We can't resume the
 * actual SDK query — that died with the process.)
 */
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { SCAN_STATE_FILE } from "./paths";

export type ScanCurrent = { source: string; kind: string; index: number; total: number };

/** Coarse scan phase. Discovery = agent listing items from active sources;
 *  checking = per-item verdict runs. Frontend renders both, separately. */
export type ScanPhase = "discovering" | "checking" | null;

/** Tool-call name + timestamp emitted by the active phase. The dashboard
 *  shows the last few so the user can see what the agent is doing right
 *  now without flipping into the full transcript. */
export type ScanToolEvent = { name: string; at: string };

export type ScanState = {
  running: boolean;
  startedAt: string | null;
  phase: ScanPhase;
  /** Free-text label for what the current phase is doing. Discovery sets
   *  things like "Listing Linear issues" or "Reading claude-output".
   *  Per-item check leaves this null and uses `current` instead. */
  phaseLabel: string | null;
  current: ScanCurrent | null;
  /** Last 8 tool-call names from the active phase, newest-first. */
  recentTools: ScanToolEvent[];
  /** Discovery summary, set when discovery completes inside a tick. */
  discovered: { added: number; refreshed: number; total: number } | null;
  lastCompletedAt: string | null;
  /** Last result reason — `ran` | `all-fresh` | `no-items` | `hourly-cap` | `crashed` | null. */
  lastReason: string | null;
  /** When the background auto-tick last ran a check (not user-triggered). */
  lastBackgroundAt: string | null;
};

/** Maximum plausible scan duration. If startedAt is older than this on
 *  boot, we assume a crash and reset state. */
const MAX_SCAN_DURATION_MS = 30 * 60 * 1000; // 30 min

const TOOL_RING_SIZE = 8;

const IDLE_STATE: ScanState = {
  running: false,
  startedAt: null,
  phase: null,
  phaseLabel: null,
  current: null,
  recentTools: [],
  discovered: null,
  lastCompletedAt: null,
  lastReason: null,
  lastBackgroundAt: null,
};

let state: ScanState = { ...IDLE_STATE };

function persist() {
  try {
    mkdirSync(dirname(SCAN_STATE_FILE), { recursive: true });
    writeFileSync(SCAN_STATE_FILE, JSON.stringify(state, null, 2), "utf8");
  } catch (e) {
    console.error("[scan-state] write failed:", e);
  }
}

/** Called once on server boot. Detects orphan "running:true" from a
 *  previous crashed process and resets to idle. */
export function initScanState(): void {
  if (!existsSync(SCAN_STATE_FILE)) return;
  try {
    const data = JSON.parse(readFileSync(SCAN_STATE_FILE, "utf8"));
    if (data && typeof data === "object") {
      const restored: ScanState = {
        ...IDLE_STATE,
        running: !!data.running,
        startedAt: data.startedAt ?? null,
        phase: (data.phase as ScanPhase) ?? null,
        phaseLabel: data.phaseLabel ?? null,
        current: data.current ?? null,
        recentTools: Array.isArray(data.recentTools) ? data.recentTools.slice(0, TOOL_RING_SIZE) : [],
        discovered: data.discovered ?? null,
        lastCompletedAt: data.lastCompletedAt ?? null,
        lastReason: data.lastReason ?? null,
        lastBackgroundAt: data.lastBackgroundAt ?? null,
      };
      // Orphan detection: running but startedAt too old → crash.
      if (restored.running && restored.startedAt) {
        const age = Date.now() - Date.parse(restored.startedAt);
        if (!isFinite(age) || age > MAX_SCAN_DURATION_MS) {
          console.log("[scan-state] orphan running:true detected, resetting");
          state = { ...IDLE_STATE, lastCompletedAt: restored.startedAt, lastReason: "crashed" };
          persist();
          return;
        }
      }
      state = restored;
    }
  } catch (e) {
    console.error("[scan-state] read failed, ignoring:", e);
  }
}

export function getScanState(): ScanState {
  return { ...state, recentTools: [...state.recentTools] };
}

export function setScanRunning(total: number): void {
  state = {
    ...IDLE_STATE,
    running: true,
    startedAt: new Date().toISOString(),
    phase: "checking",
    current: { source: "", kind: "", index: 0, total },
  };
  persist();
}

/** Begin (or re-enter) a phase. Tool log accumulates across phases — the
 *  user wants to see what the agent did during discovery even after we
 *  transition to checking. The ring buffer is bounded by TOOL_RING_SIZE. */
export function setScanPhase(phase: ScanPhase, label: string | null = null): void {
  state = { ...state, running: true, phase, phaseLabel: label };
  if (!state.startedAt) state.startedAt = new Date().toISOString();
  persist();
}

export function setScanCurrent(source: string, kind: string, index: number, total: number): void {
  if (!state.running) return;
  state = { ...state, current: { source, kind, index, total } };
  persist();
}

/** Append a tool-call event observed during the active phase. */
export function pushScanTool(name: string): void {
  if (!state.running) return;
  const event: ScanToolEvent = { name, at: new Date().toISOString() };
  const next = [event, ...state.recentTools].slice(0, TOOL_RING_SIZE);
  state = { ...state, recentTools: next };
  persist();
}

export function setScanDiscovered(summary: { added: number; refreshed: number; total: number }): void {
  state = { ...state, discovered: summary };
  persist();
}

export function setScanIdle(reason: string): void {
  state = {
    ...IDLE_STATE,
    discovered: state.discovered,
    lastCompletedAt: new Date().toISOString(),
    lastReason: reason,
    lastBackgroundAt: state.lastBackgroundAt,
  };
  persist();
}

/** Called when the background auto-tick finishes a check run (not user-triggered). */
export function setBackgroundComplete(): void {
  state = { ...state, lastBackgroundAt: new Date().toISOString() };
  persist();
}
