// capture.ts
// Scope: recording what happened.
// Central landing module for logs, code diffs, tape execution records, manual restore points,
// auto-backup notes, hash updates, and future scrapes.
// Current version wraps existing playback/recovery/hashState owners while they are being reduced.

import path from "node:path";
import { promises as fs } from "node:fs";
import { execFile } from "node:child_process";
import {
  readLog,
  clearLog,
  queueProposalToTape,
  appendExecutionToTape,
  approveManifestOnTape,
  loadTape,
  loadDubsTape,
  setDubsClearAfter,
  clearTapeItems,
  markTapeItemsRetrieved,
  replaceCurrentIssuesOnTape,
  nextDubID,
  type LogStream,
  type AIConversationStream
} from "./playback";
import { controller, BACKSTAGE_SERIAL } from "./controller";
import {
  noteSuccessfulRecorderRun,
  saveWriteSnapshot,
  saveRewriteOriginal,
  createRestorePoint,
  listRestorePoints,
  restoreFromPoint,
  captureFileTree,
  captureScrape
} from "./restore";
import {
  detectExternalDrift,
  summarizeFileState,
  updateKnownFileHash
} from "./hashState";
import { ROOT, RuntimePaths, DataPaths } from "./pathList";

export type CaptureEvent = {
  tag?: string;
  event: string;
  [key: string]: any;
};

function pad(n: number): string { return String(n).padStart(2, "0"); }
function currentSerial(): string { return String(controller.activeProjectInquiry().projectSerial || BACKSTAGE_SERIAL).trim() || BACKSTAGE_SERIAL; }
function normalizePathValue(value: unknown): string { return String(value || "").trim().replace(/\\/g, "/"); }
function extractFilePath(value: any): string {
  if (!value) return "";
  if (typeof value === "string") {
    const text = value.trim();
    for (const p of [/"FilePath"\s*:\s*"([^"]+)"/i, /"filePath"\s*:\s*"([^"]+)"/i]) {
      const m = text.match(p);
      if (m?.[1]) return normalizePathValue(m[1]);
    }
    return "";
  }
  if (typeof value !== "object") return "";
  for (const c of [value.FilePath, value.filePath, value.path]) {
    const clean = normalizePathValue(c);
    if (clean) return clean;
  }
  return extractFilePath(value.manifest) || extractFilePath(value.result) || extractFilePath(value.Payload) || extractFilePath(value.payload) || "";
}
function terminalIcon(tag: string, event: string): string {
  const t = String(tag || "").toLowerCase(), e = String(event || "").toLowerCase();
  if (t.includes("eq") || t.includes("ai")) return "🧠";
  if (t.includes("recorder") || e.includes("record") || e.includes("execute")) return "⏺️";
  if (t.includes("deck") || t.includes("power") || t.includes("playback") || t.includes("restore")) return "⚙️";
  return "•";
}
function terminalStatusIcon(event: string, summary: string): string {
  const text = `${event} ${summary}`.toLowerCase();
  if (/fail|error|missing|blocked|unavailable|invalid|rejected/.test(text)) return "🔴";
  if (/ready|started|complete|done|created|success|ok|pass|verified|allowed|refreshed/.test(text)) return "🟢";
  return "⚪";
}
function buildSummary(payload: Record<string, any>): string {
  const skip = new Set(["event", "before", "after", "Payload", "project", "filePath", "FilePath", "path", "projectSerial"]);
  return Object.entries(payload || {})
    .filter(([k, v]) => !skip.has(k) && v != null)
    .map(([k, v]) => {
      const str = typeof v === "string" ? v : JSON.stringify(v);
      return str.length > 120 ? `${k}:${str.slice(0, 120)}…` : `${k}:${str}`;
    })
    .join(" ");
}
async function append(file: string, line: string): Promise<void> {
  await fs.mkdir(path.dirname(file), { recursive: true });
  await fs.appendFile(file, line + "\n", "utf8");
}
function ledgerCurrentFile(): string { return path.join(DataPaths.logsDir, "ledger-current.jsonl"); }
function ledgerArchiveDir(): string { return path.join(DataPaths.logsDir, "archive"); }
function ledgerArchiveFile(): string {
  const d = new Date();
  return path.join(ledgerArchiveDir(), `ledger-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}-${Date.now()}.jsonl`);
}
const LEDGER_MAX_BYTES = 25 * 1024 * 1024;
const LEDGER_MAX_LINES = 50000;
async function ensureLedgerCapacity(): Promise<void> {
  const current = ledgerCurrentFile();
  let stat = null as Awaited<ReturnType<typeof fs.stat>> | null;
  try { stat = await fs.stat(current); } catch { return; }
  if (!stat || stat.size <= 0) return;

  let lineCount = 0;
  try { lineCount = (await fs.readFile(current, "utf8")).split("\n").filter(Boolean).length; } catch {}
  if (stat.size < LEDGER_MAX_BYTES && lineCount < LEDGER_MAX_LINES) return;

  await fs.mkdir(ledgerArchiveDir(), { recursive: true });
  await fs.rename(current, ledgerArchiveFile());
}
function ledgerRecordID(value?: Record<string, any>): string {
  return String(value?.recordID || value?.dubID || value?.id || Date.now().toString() + Math.random().toString(36).slice(2, 6)).trim();
}
function ledgerStatus(value?: Record<string, any>): string {
  return String(value?.status || value?.outcome || value?.event || "recorded").trim();
}
async function appendLedgerRow(row: Record<string, any>): Promise<void> {
  const payload = row.payload && typeof row.payload === "object" ? row.payload : {};
  const filePath = normalizePathValue(row.filePath || extractFilePath(payload));
  const absPath = filePath ? path.resolve(ROOT, filePath) : "";
  const pathProject = absPath ? controller.projectForPath(absPath) : null;
  const projectSerial = String(pathProject?.serialNo || row.projectSerial || payload.projectSerial || currentSerial()).trim() || BACKSTAGE_SERIAL;
  const entry = {
    timestamp: Number(row.timestamp || payload.timestamp || Date.now()) || Date.now(),
    recordID: ledgerRecordID(payload),
    projectSerial,
    source: String(row.source || payload.source || "SYSTEM").trim(),
    logType: String(row.logType || "SYSTEM").trim(),
    status: ledgerStatus(payload),
    filePath,
    payload
  };
  await ensureLedgerCapacity();
  await append(ledgerCurrentFile(), JSON.stringify(entry));
}

export function logSystem(tag: string, payload: Record<string, any>): void {
  const event = String(payload?.event ?? "event");
  const filePath = extractFilePath(payload);
  const serial = String(payload?.projectSerial || currentSerial());
  const summary = buildSummary(payload);
  const terminal = [new Date().toLocaleTimeString(), terminalIcon(tag, event), terminalStatusIcon(event, summary), `[${tag}]`, event, `serial:${serial}`, filePath ? `file:${filePath}` : "", summary].filter(Boolean).join(" ");
  console.log(terminal);
  void appendLedgerRow({
    projectSerial: serial,
    source: payload?.source || tag,
    logType: "SYSTEM",
    filePath,
    payload: { ...payload, tag, event }
  }).catch(() => {});
}

export function logManifestFull(tag: string, event: string, payload: unknown): void {
  const filePath = extractFilePath(payload);
  const serial = currentSerial();
  void appendLedgerRow({
    projectSerial: serial,
    source: tag,
    logType: "MANIFEST",
    filePath,
    payload: { tag, event, manifest: payload ?? null }
  }).catch(() => {});
}

export function logAI(direction: "in" | "out", role: string, text: string): void;
export function logAI(stream: AIConversationStream, direction: "in" | "out", role: string, text: string): void;
export function logAI(a: AIConversationStream | "in" | "out", b: "in" | "out" | string, c: string, d?: string): void {
  const stream = d === undefined ? "ai-chat" : a as AIConversationStream;
  const direction = d === undefined ? a as "in" | "out" : b as "in" | "out";
  const role = d === undefined ? b as string : c;
  const text = d === undefined ? c : d;
  const serial = currentSerial();
  void appendLedgerRow({
    projectSerial: serial,
    source: "AI",
    logType: "AI_CALL",
    payload: { stream, direction, role, text: String(text ?? "").trim() }
  }).catch(() => {});
}

function buildDiff(before: string, after: string, ctx = 2): { lines: string[]; range: string } {
  const bL = before.split("\n"), aL = after.split("\n");
  let start = 0;
  while (start < bL.length && start < aL.length && bL[start] === aL[start]) start++;
  let bEnd = bL.length - 1, aEnd = aL.length - 1;
  while (bEnd > start && aEnd > start && bL[bEnd] === aL[aEnd]) { bEnd--; aEnd--; }
  const ctxStart = Math.max(0, start - ctx), ctxBEnd = Math.min(bL.length - 1, bEnd + ctx);
  const lines: string[] = [];
  for (let i = ctxStart; i < start; i++) lines.push(`  ${bL[i]}`);
  for (let i = start; i <= bEnd; i++) lines.push(`- ${bL[i]}`);
  for (let i = start; i <= aEnd; i++) lines.push(`+ ${aL[i]}`);
  for (let i = bEnd + 1; i <= ctxBEnd; i++) lines.push(`  ${bL[i]}`);
  return { lines, range: `${ctxStart + 1}-${ctxBEnd + 1}` };
}

export function logCode(opts: { action: string; filePath: string; before?: string; after?: string; changed: boolean; outcome?: string }): void {
  if (String(opts.action || "").trim().toLowerCase() === "rewritefile") return;

  const serial = currentSerial();
  const hasTextPair = opts.before != null && opts.after != null;
  const diff = opts.changed && hasTextPair ? buildDiff(String(opts.before), String(opts.after)) : null;

  void appendLedgerRow({
    projectSerial: serial,
    source: "RECORDER",
    logType: "DIFF",
    filePath: opts.filePath,
    payload: diff ? { ...opts, diff } : { ...opts }
  }).catch(() => {});
}
type TapeBackfillItem = {
  dubID?: string;
  status?: string;
  recorderStatus?: string;
  kind?: string;
  source?: string;
  projectSerial?: string;
  manifest?: any;
  result?: any;
  timestamp?: number;
};

async function readJsonValue<T>(filePath: string, fallback: T): Promise<T> {
  try {
    const raw = await fs.readFile(filePath, "utf8");
    return raw.trim() ? JSON.parse(raw) as T : fallback;
  } catch { return fallback; }
}

function tapeFileForSerial(serial: string): string { return path.join(DataPaths.logsDir, serial, "tape.json"); }
async function discoverTapeSerials(): Promise<string[]> {
  const serials = new Set<string>();
  try {
    const entries = await fs.readdir(DataPaths.logsDir, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      const serial = String(entry.name || "").trim();
      if (!serial || serial === "archive") continue;
      try {
        await fs.access(tapeFileForSerial(serial));
        serials.add(serial);
      } catch {}
    }
  } catch {}
  return [...serials].sort((a, b) => a.localeCompare(b));
}

async function ledgerFilesNewestFirst(): Promise<string[]> {
  const files = [ledgerCurrentFile()];
  try {
    const archives = (await fs.readdir(ledgerArchiveDir(), { withFileTypes: true }))
      .filter(entry => entry.isFile() && entry.name.endsWith(".jsonl"))
      .map(entry => path.join(ledgerArchiveDir(), entry.name))
      .sort((a, b) => b.localeCompare(a));
    files.push(...archives);
  } catch {}
  return files;
}

async function existingLedgerTapeIDs(): Promise<Set<string>> {
  const ids = new Set<string>();
  for (const file of await ledgerFilesNewestFirst()) {
    let raw = "";
    try { raw = await fs.readFile(file, "utf8"); }
    catch { continue; }
    for (const line of raw.split("\n")) {
      if (!line.trim()) continue;
      try {
        const row = JSON.parse(line);
        if (String(row?.logType || "").toUpperCase() !== "TAPE") continue;
        const id = String(row?.payload?.dubID || row?.recordID || "").trim();
        if (id) ids.add(id);
      } catch {}
    }
  }
  return ids;
}

function isCompletedTapeRun(item: TapeBackfillItem): boolean {
  return item?.status === "done" || item?.status === "error";
}

async function backfillTapeRunsToLedger(minRunsPerProject = 10): Promise<{ ok: true; scannedProjects: number; added: number; projects: Record<string, number> }> {
  const minimum = Math.max(1, Math.floor(Number(minRunsPerProject || 10)));
  const existing = await existingLedgerTapeIDs();
  const projects: Record<string, number> = {};
  let added = 0;
  const serials = await discoverTapeSerials();

  for (const serial of serials) {
    const tape = await readJsonValue<TapeBackfillItem[]>(tapeFileForSerial(serial), []);
    const completed = tape
      .filter(isCompletedTapeRun)
      .sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0))
      .slice(0, minimum)
      .reverse();

    for (const item of completed) {
      const dubID = String(item.dubID || "").trim();
      if (!dubID || existing.has(dubID)) continue;
      await appendLedgerRow({
        timestamp: item.timestamp,
        projectSerial: item.projectSerial || serial,
        source: item.source || "tape-backfill",
        logType: "TAPE",
        filePath: extractFilePath(item.manifest),
        payload: {
          event: "tape_execution_backfilled",
          dubID,
          status: item.status,
          source: item.source || "tape-backfill",
          kind: item.kind,
          recorderStatus: item.recorderStatus,
          manifest: item.manifest,
          result: item.result,
          timestamp: item.timestamp
        }
      });
      existing.add(dubID);
      projects[serial] = (projects[serial] || 0) + 1;
      added++;
    }
  }

  return { ok: true, scannedProjects: serials.length, added, projects };
}

type CurrentIssue = { message: string; filePath?: string; line?: number; column?: number; code?: string };

function parseTypecheckIssues(output: string): CurrentIssue[] {
  const issues: CurrentIssue[] = [];
  const lines = String(output || "").split(/\r?\n/);
  for (const line of lines) {
    const clean = line.trim();
    if (!clean) continue;
    const match = clean.match(/^(.+?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/);
    if (match) {
      issues.push({ filePath: match[1].replace(/\\/g, "/"), line: Number(match[2]), column: Number(match[3]), code: match[4], message: match[5] });
      continue;
    }
    if (/\berror\s+TS\d+:/i.test(clean)) issues.push({ message: clean });
  }
  return issues.slice(0, 50);
}

async function refreshCurrentIssues(sourceDubID = ""): Promise<void> {
  await new Promise<void>((resolve) => {
    const localTsc = path.join(ROOT, "node_modules", "typescript", "bin", "tsc");
    execFile(process.execPath, [localTsc, "--noEmit"], { cwd: ROOT, windowsHide: true, timeout: 120000 }, async (err, stdout, stderr) => {
      const output = [stdout, stderr].filter(Boolean).join("\n");
      const issues = err ? parseTypecheckIssues(output) : [];
      const outputExcerpt = err && issues.length === 0
        ? String(output || err?.message || "").trim().replace(/\s+/g, " ").slice(0, 700)
        : "";
      try {
        await replaceCurrentIssuesOnTape({ source: "typecheck", issues });
        logSystem("Capture", {
          event: err ? (issues.length ? "typecheck_issues_detected" : "typecheck_unparsed_failure") : "typecheck_clear",
          sourceDubID,
          issueCount: issues.length,
          ...(outputExcerpt ? { outputExcerpt } : {})
        });
      } catch (captureErr: any) {
        logSystem("Capture", { event: "typecheck_issue_capture_failed", sourceDubID, error: captureErr?.message ?? String(captureErr) });
      }
      resolve();
    });
  });
}

async function recordTapeExecution(input: Parameters<typeof appendExecutionToTape>[0]): Promise<void> {
  await appendExecutionToTape(input);
  await appendLedgerRow({
    projectSerial: currentSerial(),
    source: input.source || "recorder",
    logType: "TAPE",
    filePath: extractFilePath(input.manifest),
    payload: {
      event: "tape_execution_recorded",
      dubID: input.dubID,
      status: input.status,
      source: input.source || "recorder",
      manifest: input.manifest,
      result: input.result
    }
  });

  if (input.status !== "done") return;
  await refreshCurrentIssues(input.dubID);
  await noteSuccessfulRecorderRun({ manifests: [input.manifest], dubID: input.dubID, source: input.source })
    .catch((e: any) => logSystem("Recovery", { event: "auto_backup_note_failed", error: e?.message ?? String(e), dubID: input.dubID }));
}

export const capture = {
  system(input: CaptureEvent): void {
    const { tag = "System", ...payload } = input;
    logSystem(tag, payload);
  },

  manifest(tag: string, event: string, payload: unknown): void {
    logManifestFull(tag, event, payload);
  },

  ai(...args: any[]): void {
    (logAI as any)(...args);
  },

  code(input: Parameters<typeof logCode>[0]): void {
    logCode(input);
  },

  async readLog(stream: Parameters<typeof readLog>[0], limit?: number) {
    return await readLog(stream, limit);
  },

  async clearLog(stream: Parameters<typeof clearLog>[0]) {
    return await clearLog(stream);
  },

  async beforeWrite(input: { serial: string; filePath: string; dubID: string }) {
    const abs = path.resolve(input.filePath);
    const rel = path.relative(ROOT, abs).replace(/\\/g, "/");
    const text = await fs.readFile(abs, "utf8").catch(() => null);
    if (text === null) return { ok: true, exists: false, text: null };

    const drift = await detectExternalDrift(input.serial, rel).catch(() => null);
    if (drift?.drifted) {
      logSystem("Capture", {
        event: "external_drift_detected",
        dubID: input.dubID,
        filePath: input.filePath,
        knownHash: drift.knownHash,
        currentHash: drift.currentHash
      });
    }

    return { ok: true, exists: true, text, drift };
  },

  async afterWrite(input: { serial: string; filePath: string }) {
    const abs = path.resolve(input.filePath);
    const rel = path.relative(ROOT, abs).replace(/\\/g, "/");
    const summary = await summarizeFileState(abs);
    await updateKnownFileHash(input.serial, rel, summary);
    return { ok: true, summary };
  },

  async writeSnapshot(input: Parameters<typeof saveWriteSnapshot>[0]) {
    return await saveWriteSnapshot(input);
  },

  async rewriteOriginal(input: Parameters<typeof saveRewriteOriginal>[0]) {
    return await saveRewriteOriginal(input);
  },

  async successfulRecorderRun(input: Parameters<typeof noteSuccessfulRecorderRun>[0]) {
    return await noteSuccessfulRecorderRun(input);
  },

  tree: captureFileTree,
  scrape: captureScrape,

  tape: {
    nextDubID,
    queueProposal: queueProposalToTape,
    appendExecution: recordTapeExecution,
    approveManifest: approveManifestOnTape,
    load: loadTape,
    loadDubs: loadDubsTape,
    clearDubs: setDubsClearAfter,
    clearItems: clearTapeItems,
    markRetrieved: markTapeItemsRetrieved,
    replaceCurrentIssues: replaceCurrentIssuesOnTape,
    backfillRecordedHistory: backfillTapeRunsToLedger
  },

  restorePoints: {
    create: createRestorePoint,
    list: listRestorePoints,
    apply: restoreFromPoint
  },

  paths: {
    root: ROOT,
    temp: RuntimePaths.tempDir,
    logs: DataPaths.logsDir,
    scrapes: DataPaths.scrapesDir
  }
};

export const logCaptureSystem = capture.system;
export const logCaptureCode = capture.code;
export const logCaptureManifest = capture.manifest;
export const readCaptureLog = capture.readLog;
export const clearCaptureLog = capture.clearLog;
export const captureBeforeWrite = capture.beforeWrite;
export const captureAfterWrite = capture.afterWrite;
export const saveCaptureWriteSnapshot = capture.writeSnapshot;
export const saveCaptureRewriteOriginal = capture.rewriteOriginal;
export const noteCaptureSuccessfulRecorderRun = capture.successfulRecorderRun;
export const captureTree = capture.tree;
export const captureScrapeSnapshot = capture.scrape;

