// eq.ts
// Scope: local AI call transport, activity history, and local AI report storage.
//
// Decision logic, prompt construction, lane classification, and routing trees
// have been removed to the Nodarama server. This module receives a RoutePackage
// from the Relay Manifold and uses it to call the customer's local LM Studio.
//
// Public surface is unchanged — AI1correction, AI2chat, AI3diagnostic, recordChatIn.
// Internal mechanics are now: ask manifold → get prompts → call local AI → store result.

import fs from "node:fs/promises";
import path from "node:path";
import { logAI, logSystem } from "./capture";
import { queueProposalToTape } from "./playback";
import {
  loadSettings,
  ensureEQState,
  readJson,
  writeJson,
  syncModelFromLMStudio,
  type AITransportProfile
} from "./power";
import { controller } from "./controller";
import { projectLookup } from "./mgrSNP";
import {
  requestRoutePackage,
  buildHypToken,
  scrubContext,
  type ManifoldVia,
  type RoutePackage
} from "./manifold";
import {
  ROOT,
  EQ_STATE_DIR,
  EQ_REPORTS_PATH
} from "./pathList";

const EQ_ACTIVITY_PATH = path.join(EQ_STATE_DIR, "activity.json");

// ── Types (public surface unchanged) ──────────────────────────────────────────

type Role = "user" | "assistant" | "system";

type ActivityMessage = {
  id:   string;
  role: Role;
  text: string;
  ts:   number;
};

export type ManifestLike = {
  Task?:    string | null;
  Action?:  string;
  FilePath?: string | null;
  Payload?: any;
  Comment?: string | null;
  batchID?: string;
  [key: string]: any;
};

export type CorrectionStage = "preflight" | "apply" | "verification" | "unknown";
export type FailureLane     = "A" | "B" | "C" | "OTHER";
export type AI1Mode         = "AI1A" | "AI1B" | "AI1C" | "OTHER";

export type ManifestFailureInput = {
  manifest:   ManifestLike;
  error:      string;
  batchID?:   string;
  stage?:     CorrectionStage;
  fileText?:  string | null;
  failureLane?:  FailureLane;
  failureMode?:  AI1Mode;
  rawError?:     string;
  repairAttempt?: number;
};

export type DiagnosticIssue = {
  severity: "info" | "warning" | "error";
  kind:     string;
  filePath?: string;
  message:  string;
};

export type SuggestedRepair = {
  manifest:  ManifestLike;
  rationale: string;
};

export type EQCategory   = "run" | "diagnostic" | "error" | "enhancement";
export type EQSource     = "AI1correction" | "AI2chat" | "AI3diagnostic";
export type EQCompletion = "resolved" | "pending" | "failed" | "none";

export type EQReport = {
  kind:       "assistMatch" | "errorReport" | "diagnosticRepair" | "chatAssist" | "enhancement";
  category:   EQCategory;
  source:     EQSource;
  completion: EQCompletion;
  status:
    | "resolved"
    | "repair_prepared"
    | "report_only"
    | "failed_attempt"
    | "enhancement_prepared";
  summary:           string;
  confidence:        number;
  originalManifest?: ManifestLike;
  adjustedManifest?: ManifestLike;
  suggestedRepair?:  SuggestedRepair;
  issues:            DiagnosticIssue[];
  ts:                number;
  batchID?:          string;
  meta?:             Record<string, any>;
};

type AIResponseEnvelope = {
  Response?:        string;
  Manifest?:        any;
  manifestKind?:    "enhancement" | "diagnostic" | "build" | string;
  summary?:         string;
  confidence?:      number;
  issues?:          DiagnosticIssue[];
  adjustedManifest?: ManifestLike;
  suggestedRepair?:  SuggestedRepair;
  correction?:      Partial<ManifestLike>;
  rationale?:       string;
};

type ProjectIntentSection = {
  title: string;
  type:  string;
  items: any[];
};

// ── Paths ─────────────────────────────────────────────────────────────────────

const LAST_AI_PACKAGE = path.join(EQ_STATE_DIR, "last-ai-package.json");
// ── Utilities ─────────────────────────────────────────────────────────────────

function now(): number { return Date.now(); }

function id(prefix: string): string {
  return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}

function isObject(v: unknown): v is Record<string, any> {
  return Boolean(v) && typeof v === "object" && !Array.isArray(v);
}

function truncateText(text: string, max: number): string {
  const c = String(text ?? "");
  return c.length <= max ? c : `${c.slice(0, max - 1)}…`;
}

function stripFence(text: string): string {
  const c = String(text ?? "").trim();
  const f = c.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
  return f ? f[1].trim() : c;
}

function normalizeConfidence(v: unknown, fallback = 0): number {
  const n = Number(v);
  return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : fallback;
}

function normalizeAction(a: string): string {
  return String(a || "").trim().toLowerCase();
}

function laneFromMode(mode: AI1Mode): FailureLane {
  if (mode === "AI1A") return "A";
  if (mode === "AI1B") return "B";
  if (mode === "AI1C") return "C";
  return "OTHER";
}

function viaFromFailureMode(mode: AI1Mode): ManifoldVia {
  if (mode === "AI1A") return "AI1A";
  if (mode === "AI1B") return "AI1B";
  if (mode === "AI1C") return "AI1C";
  return "AI1OTHER";
}

// ── Normalizers ───────────────────────────────────────────────────────────────

function normalizeIssue(v: any): DiagnosticIssue {
  return {
    severity: v?.severity === "warning" ? "warning" : v?.severity === "error" ? "error" : "info",
    kind:     String(v?.kind   || "note").trim()    || "note",
    ...(typeof v?.filePath === "string" && v.filePath.trim() ? { filePath: v.filePath.trim() } : {}),
    message:  String(v?.message || "").trim() || "No details provided."
  };
}

function normalizeNewFormatManifest(parsed: any): { kind?: "enhancement" | "diagnostic" | "build"; manifest?: ManifestLike; rationale?: string } {
  if (!isObject(parsed)) return {};

  const rawManifest = isObject(parsed.Manifest) ? parsed.Manifest : parsed;
  const rawKind = String(
    (isObject(parsed.Manifest) ? parsed.Manifest.kind || parsed.Manifest.Kind || parsed.Manifest.type || parsed.Manifest.label : parsed.Manifest) ||
    parsed.manifestKind ||
    parsed.kind ||
    ""
  ).trim().toLowerCase();

  const kind = rawKind === "build" || rawKind === "diagnostic" || rawKind === "enhancement"
    ? rawKind as "enhancement" | "diagnostic" | "build"
    : undefined;

  const candidate = isObject(rawManifest) ? {
    Task: rawManifest.Task ?? rawManifest.task,
    Action: rawManifest.Action ?? rawManifest.action,
    FilePath: rawManifest.FilePath ?? rawManifest.filePath ?? rawManifest.path,
    Payload: rawManifest.Payload ?? rawManifest.payload,
    Comment: rawManifest.Comment ?? rawManifest.comment
  } : {
    Task: parsed.Task ?? parsed.task,
    Action: parsed.Action ?? parsed.action,
    FilePath: parsed.FilePath ?? parsed.filePath ?? parsed.path,
    Payload: parsed.Payload ?? parsed.payload,
    Comment: parsed.Comment ?? parsed.comment
  };

  const manifest = candidate.Action || candidate.FilePath || candidate.Payload !== undefined
    ? normalizeManifestLike(candidate)
    : undefined;

  return {
    kind,
    manifest,
    rationale: String((isObject(rawManifest) ? rawManifest.Comment ?? rawManifest.comment : parsed.Comment ?? parsed.comment) || parsed.rationale || "").trim()
  };
}

function normalizeManifestLike(v: unknown): ManifestLike | undefined {
  if (!isObject(v)) return undefined;
  const next: ManifestLike = {
    ...(v.Task    !== undefined ? { Task:    v.Task    == null ? null : String(v.Task) }                        : {}),
    ...(v.Action  !== undefined ? { Action:  String(v.Action  || "").trim() }                                   : {}),
    ...(v.FilePath !== undefined ? { FilePath: v.FilePath == null ? null : String(v.FilePath || "").trim() || null } : {}),
    ...(v.Comment !== undefined ? { Comment: v.Comment == null ? null : String(v.Comment) }                    : {}),
    ...(v.Payload !== undefined ? { Payload: v.Payload }                                                        : {})
  };
  if (v.batchID !== undefined) next.batchID = String(v.batchID || "").trim();
  for (const [k, val] of Object.entries(v)) {
    if (!(k in next)) next[k] = val;
  }
  return next;
}

function mergeCorrection(
  original: ManifestLike,
  correction?: Partial<ManifestLike>
): ManifestLike | undefined {
  if (!correction || !isObject(correction)) return undefined;
  return {
    ...original,
    ...(correction.Task    !== undefined ? { Task:    correction.Task }    : {}),
    ...(correction.Action  !== undefined ? { Action:  correction.Action }  : {}),
    ...(correction.FilePath !== undefined ? { FilePath: correction.FilePath } : {}),
    ...(correction.Comment !== undefined ? { Comment: correction.Comment } : {}),
    ...(correction.Payload !== undefined
      ? { Payload: isObject(original.Payload) && isObject(correction.Payload)
            ? { ...original.Payload, ...correction.Payload }
            : correction.Payload }
      : {})
  };
}

function classifyAIOutput(report: EQReport): "reply_only" | "manifest_reply" {
  return report.suggestedRepair?.manifest || report.adjustedManifest
    ? "manifest_reply"
    : "reply_only";
}

function summarizeManifestChange(original: ManifestLike, adjusted?: ManifestLike): string {
  if (!adjusted) return "No safe manifest change was prepared.";

  const changes: string[] = [];
  const originalAction = String(original.Action || "").trim();
  const adjustedAction = String(adjusted.Action || "").trim();
  const originalFile = String(original.FilePath || "").trim();
  const adjustedFile = String(adjusted.FilePath || "").trim();

  if (originalAction !== adjustedAction) changes.push(`Action: ${originalAction || "(empty)"} → ${adjustedAction || "(empty)"}`);
  if (originalFile !== adjustedFile) changes.push(`FilePath: ${originalFile || "(empty)"} → ${adjustedFile || "(empty)"}`);

  const originalPayload = JSON.stringify(original.Payload ?? null);
  const adjustedPayload = JSON.stringify(adjusted.Payload ?? null);
  if (originalPayload !== adjustedPayload) {
    if (isObject(original.Payload) && isObject(adjusted.Payload)) {
      for (const key of Array.from(new Set([...Object.keys(original.Payload), ...Object.keys(adjusted.Payload)]))) {
        const before = JSON.stringify(original.Payload[key] ?? null);
        const after = JSON.stringify(adjusted.Payload[key] ?? null);
        if (before !== after) changes.push(`Payload.${key} changed`);
      }
    } else {
      changes.push("Payload changed");
    }
  }

  return changes.length ? changes.join("; ") : "AI prepared a manifest but did not change Action, FilePath, or Payload.";
}

function logAI1CorrectionOutcome(input: {
  via: ManifoldVia;
  strategy?: string;
  manifest: ManifestLike;
  adjustedManifest?: ManifestLike;
  summary: string;
  confidence: number;
  status: string;
  error?: string;
}) {
  const filePath = String(input.adjustedManifest?.FilePath || input.manifest.FilePath || "").trim();
  const action = String(input.adjustedManifest?.Action || input.manifest.Action || "").trim();
  const changed = Boolean(input.adjustedManifest);
  logAI("ai-diagnostic", "out", input.via, [
    changed ? "Correction prepared." : "No safe correction prepared.",
    `Status: ${input.status}`,
    `Action: ${action || "(empty)"}`,
    `FilePath: ${filePath || "(empty)"}`,
    `Confidence: ${input.confidence.toFixed(2)}`,
    input.error ? `Error: ${input.error}` : "",
    `Change: ${summarizeManifestChange(input.manifest, input.adjustedManifest)}`,
    input.summary ? `Summary: ${input.summary}` : ""
  ].filter(Boolean).join("\n"));
}

// ── Local state ───────────────────────────────────────────────────────────────

async function pushReport(report: EQReport): Promise<void> {
  await ensureEQState();
  const reports = await readJson<EQReport[]>(EQ_REPORTS_PATH, []);
  reports.push(report);
  await writeJson(EQ_REPORTS_PATH, reports.slice(-200));
}

async function pushActivityMessage(role: Role, text: string): Promise<void> {
  await ensureEQState();
  const settings = await loadSettings();
  const activity  = await readJson<ActivityMessage[]>(EQ_ACTIVITY_PATH, []);
  activity.push({ id: id("msg"), role, text: String(text ?? ""), ts: now() });
  await writeJson(EQ_ACTIVITY_PATH, activity.slice(-Math.max(1, settings.activityHistoryLimit || 40)));
}

async function tapeAIManifest(report: EQReport): Promise<any | null> {
  const manifest = report.suggestedRepair?.manifest ?? report.adjustedManifest ?? null;
  if (!manifest) return null;

  const settings         = await loadSettings();
  const autoPlayHandling = (settings as any).autoPlayHandling || {};

  const recorderStatus =
    report.kind === "chatAssist"      && autoPlayHandling.chatManifests      ? "YES" :
    report.kind === "diagnosticRepair" && autoPlayHandling.diagnosticRepair   ? "YES" :
    report.kind === "enhancement"     && autoPlayHandling.enhancementSuggestions ? "YES" :
    "NO";

  const kind =
    report.kind === "enhancement"  ? "enhancement" :
    report.kind === "errorReport"  ? "error"        :
    "diagnostic";

  if (isObject(manifest) && manifest.Payload !== undefined) {
    logAI("ai-diagnostic", "out", report.source, JSON.stringify({
      source: report.source,
      kind: report.kind,
      status: report.status,
      manifest
    }, null, 2));
  }

  return await queueProposalToTape({ manifest, report, kind, source: report.source, recorderStatus });
}

// ── Project context builders ──────────────────────────────────────────────────

async function readFileIfExists(filePath: string): Promise<string | null> {
  const rel = String(filePath || "").trim();
  if (!rel) return null;
  const abs  = path.resolve(ROOT, rel);
  const root = path.resolve(ROOT);
  if (!abs.startsWith(root + path.sep) && abs !== root) return null;
  try { return await fs.readFile(abs, "utf8"); } catch { return null; }
}

async function buildProjectOverview(): Promise<{
  name: string; root: string; summary: string; notes: string;
  intentRepo: ProjectIntentSection[];
}> {
  const state = controller.state();
  const activeName = String(state.activeProject || (state.isBackstage ? "Backstage" : "Universal")).trim();
  const record = projectLookup({ serialNo: state.projectSerial }).project;
  const isUniversal = Boolean(state.isBackstage) || activeName.toLowerCase() === "universal";
  const projectRoot = isUniversal ? "./projects" : (String(record?.location || record?.path || "./projects").trim() || "./projects");
  const result      = { name: isUniversal ? "Universal" : activeName, root: projectRoot, summary: "", notes: "", intentRepo: [] as ProjectIntentSection[] };

  if (isUniversal) return result;

  const outlinePath = path.resolve(ROOT, projectRoot, "project.json");
  try {
    const raw = JSON.parse(await fs.readFile(outlinePath, "utf8"));
    if (isObject(raw.details)) {
      result.summary = String(raw.details.summary || "").trim();
      result.notes   = String(raw.details.notes   || "").trim();
    }
    if (Array.isArray(raw.sections)) {
      result.intentRepo = raw.sections
        .filter((s: any) => isObject(s) && Boolean(s.intentRepo))
        .map((s: any) => ({ title: String(s.title || "").trim(), type: String(s.type || "numbered").trim(), items: Array.isArray(s.items) ? s.items : [] }))
        .filter((s: any) => s.title);
    }
  } catch {}

  return result;
}

// ── AI call — transport only ───────────────────────────────────────────────────

const JSON_ONLY_INSTRUCTION = "Return valid JSON only. No markdown. No prose outside the JSON object.";

async function callLocalAI(
  routePkg:    RoutePackage,
  userPayload: Record<string, any>
): Promise<AIResponseEnvelope | null> {
  await ensureEQState();

  let settings = await loadSettings();
  let model    = String(settings.model || "").trim();

  if (!model) {
    model = await syncModelFromLMStudio().catch(() => "");
    if (!model) {
      logSystem("EQ", { event: "ai_unavailable", via: routePkg.via, reason: "no_model" });
      return null;
    }
    settings = await loadSettings();
  }

  const profile  = settings.aiTransport;
  const baseUrl  = String(settings.lmStudioUrl || "http://127.0.0.1:1234").replace(/\/$/, "");
  const plainDiscussionMode = routePkg.via === "AI2chat" && String(userPayload?.requestMode || "") === "discussion";

  const systemContent = plainDiscussionMode
    ? "You are NV's local AI chat assistant. Reply briefly in plain English. Do not create a manifest unless the user explicitly asks for a code change."
    : profile.jsonInstructionMode === "system" || profile.jsonInstructionMode === "both"
      ? `${routePkg.systemPrompt}\n\n${JSON_ONLY_INSTRUCTION}`
      : routePkg.systemPrompt;

  const userContent = plainDiscussionMode
    ? String(userPayload?.userMessage || "").trim()
    : profile.jsonInstructionMode === "user" || profile.jsonInstructionMode === "both"
      ? `${JSON.stringify(userPayload, null, 2)}\n\n${JSON_ONLY_INSTRUCTION}`
      : JSON.stringify(userPayload, null, 2);

  const messages = [
    { role: "system" as const, content: systemContent },
    { role: "user"   as const, content: userContent   }
  ];

  logAI("ai-chat", "out", routePkg.via, JSON.stringify({
    via: routePkg.via,
    strategy: routePkg.strategy,
    model,
    baseUrl,
    messages
  }, null, 2));

  try {
    await ensureEQState();
    await fs.writeFile(
      LAST_AI_PACKAGE,
      JSON.stringify({ ts: now(), via: routePkg.via, strategy: routePkg.strategy, model, baseUrl, profile, userPayload, messages }, null, 2),
      "utf8"
    );
  } catch {}

  const attempts = Math.max(1, Number(profile.retries || 1));

  for (let attempt = 0; attempt < attempts; attempt++) {
    const ctrl  = new AbortController();
    const timeoutMs = plainDiscussionMode
      ? Math.max(60000, Number(profile.timeoutMs || 0))
      : Math.max(15000, Number(profile.timeoutMs || 0));
    const timer = setTimeout(() => ctrl.abort(), timeoutMs);

    try {
      const body: Record<string, any> = { model, temperature: Number(profile.temperature ?? 0.1), messages };
      if (profile.useResponseFormat) body.response_format = { type: "json_object" };

      const res = await fetch(`${baseUrl}/v1/chat/completions`, {
        method:  "POST",
        headers: { "Content-Type": "application/json" },
        body:    JSON.stringify(body),
        signal:  ctrl.signal
      });

      if (!res.ok) throw new Error(`lmstudio_http_${res.status}`);

      const data = await res.json() as any;
      const raw  = String(data?.choices?.[0]?.message?.content ?? "").trim();
      logAI("ai-chat", "in", routePkg.via,
        String(raw ?? "").trim() || "(empty reply)"
      );

      const parsed = extractJsonFromAI(raw);
      if (!parsed && plainDiscussionMode && raw) {
        return {
          Response: raw,
          summary: raw,
          confidence: 0.5,
          issues: []
        };
      }
      if (!parsed) throw new Error("ai_json_parse_failed");

      const newFormat = normalizeNewFormatManifest(parsed);
      const suggestedManifest = isObject(parsed.suggestedRepair)
        ? normalizeManifestLike(parsed.suggestedRepair.manifest)
        : newFormat.manifest;

      return {
        Response:         String(parsed.Response || "").trim(),
        Manifest:         parsed.Manifest,
        manifestKind:     newFormat.kind,
        summary:          String(parsed.Response || parsed.summary || "").trim(),
        confidence:       normalizeConfidence(parsed.confidence, 0),
        issues:           Array.isArray(parsed.issues) ? parsed.issues.map(normalizeIssue) : [],
        adjustedManifest: normalizeManifestLike(parsed.adjustedManifest),
        suggestedRepair:  suggestedManifest
          ? { manifest: suggestedManifest, rationale: String(parsed.suggestedRepair?.rationale || newFormat.rationale || parsed.rationale || "").trim() }
          : undefined,
        correction: normalizeManifestLike(parsed.correction)
      };
    } catch (err: any) {
      if (attempt === attempts - 1) {
        logSystem("EQ", { event: "ai_call_failed", via: routePkg.via, error: err?.message ?? String(err) });
      }
    } finally {
      clearTimeout(timer);
    }
  }

  return null;
}

function extractJsonFromAI(text: string): AIResponseEnvelope | null {
  const clean = stripFence(text);
  if (!clean) return null;
  try { const p = JSON.parse(clean); return isObject(p) ? p as AIResponseEnvelope : null; } catch {}

  const start = clean.indexOf("{");
  if (start < 0) return null;

  let depth = 0, quote: '"' | "'" | "`" | null = null, escaped = false;
  for (let i = start; i < clean.length; i++) {
    const ch = clean[i];
    if (quote) { if (escaped) { escaped = false; continue; } if (ch === "\\") { escaped = true; continue; } if (ch === quote) quote = null; continue; }
    if (ch === '"' || ch === "'" || ch === "`") { quote = ch as any; continue; }
    if (ch === "{") depth++;
    if (ch === "}") { depth--; if (depth === 0) { try { const p = JSON.parse(clean.slice(start, i + 1)); return isObject(p) ? p as AIResponseEnvelope : null; } catch { return null; } } }
  }
  return null;
}

// ── User payload builders ─────────────────────────────────────────────────────

function extractLookupString(payload: unknown): string {
  if (!isObject(payload)) return "";
  for (const k of ["target", "search", "find"]) {
    if (typeof payload[k] === "string" && payload[k].trim()) return payload[k].trim();
  }
  return "";
}

function buildExcerpt(source: string, lookup: string, radius = 20): string | null {
  const clean = String(source || "").replace(/\r\n/g, "\n");
  if (!clean.trim()) return null;
  const lines = clean.split("\n");
  let lineIdx = 0;
  if (lookup) {
    const abs = clean.toLowerCase().indexOf(lookup.toLowerCase());
    if (abs >= 0) {
      let count = 0;
      for (let i = 0; i < lines.length; i++) {
        const w = lines[i].length + 1;
        if (count + w > abs) { lineIdx = i; break; }
        count += w;
      }
    }
  }
  return lines.slice(Math.max(0, lineIdx - radius), Math.min(lines.length, lineIdx + radius + 1)).join("\n");
}

async function buildAI1UserPayload(input: ManifestFailureInput): Promise<Record<string, any>> {
  const manifest  = normalizeManifestLike(input.manifest) || {};
  const filePath  = String(manifest.FilePath ?? "").trim();
  const lookup    = extractLookupString(manifest.Payload);
  const fileText  = typeof input.fileText === "string" && input.fileText.trim()
    ? input.fileText
    : await readFileIfExists(filePath);

  return {
    manifest: {
      Task:     manifest.Task    ?? null,
      Action:   manifest.Action  ?? "",
      FilePath: manifest.FilePath ?? null,
      Payload:  manifest.Payload,
      Comment:  manifest.Comment ?? null
    },
    evidence: {
      rawError:     String(input.rawError  || input.error || "").trim(),
      stage:        input.stage || "unknown",
      lookupString: lookup || null,
      localExcerpt: fileText ? buildExcerpt(fileText, lookup) : null
    }
  };
}

const AI2_CHANGE_HINTS = ["change","fix","error","update","modify","adjust","make","replace","move","rename","style","font","color","button","css","html","js","ts","build","create","add","implement","proposal","propose","plan","planning"];
const PROJECT_SCAN_IGNORE = new Set(["node_modules",".git",".eq","logs","dist","build","coverage","scrapes","restore"]);
const PROJECT_SCAN_EXTENSIONS = new Set([".ts",".tsx",".js",".jsx",".css",".html",".json",".md",".txt"]);

function tokenizeForSearch(text: string): string[] {
  return [...new Set(String(text || "").toLowerCase().match(/[a-z0-9._-]{3,}/g) || [])]
    .filter(t => !["that","with","from","this","have","there","should","would","could"].includes(t))
    .slice(0, 24);
}

function isLikelyChangeRequest(text: string): boolean {
  const c = String(text || "").toLowerCase();
  return AI2_CHANGE_HINTS.some(t => c.includes(t));
}

async function collectProjectFiles(rootRel: string): Promise<string[]> {
  const start   = path.resolve(ROOT, rootRel || ".");
  const rootAbs = path.resolve(ROOT);
  const out: string[] = [];

  async function walk(dir: string): Promise<void> {
    if (!dir.startsWith(rootAbs) && dir !== rootAbs) return;
    let entries: any[] = [];
    try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
    for (const e of entries) {
      if (e.name.startsWith(".")) continue;
      if (e.isDirectory()) { if (PROJECT_SCAN_IGNORE.has(e.name)) continue; await walk(path.join(dir, e.name)); continue; }
      if (!e.isFile()) continue;
      const ext = path.extname(e.name).toLowerCase();
      if (!PROJECT_SCAN_EXTENSIONS.has(ext)) continue;
      out.push(path.relative(ROOT, path.join(dir, e.name)).replace(/\\/g, "/"));
      if (out.length >= 250) return;
    }
  }

  await walk(start);
  return out;
}

async function findRelevantProjectFiles(
  userMessage: string,
  projectRoot:  string
): Promise<Array<{ filePath: string; excerpt: string }>> {
  const tokens = tokenizeForSearch(userMessage);
  const files  = await collectProjectFiles(projectRoot || ".");
  const scored: Array<{ filePath: string; score: number; excerpt: string }> = [];

  for (const filePath of files) {
    let score = 0;
    const lowerPath = filePath.toLowerCase();
    for (const t of tokens) { if (lowerPath.includes(t)) score += 6; }
    let content = "";
    try { content = await fs.readFile(path.resolve(ROOT, filePath), "utf8"); } catch { continue; }
    const lowerContent = content.toLowerCase();
    for (const t of tokens) { if (lowerContent.includes(t)) score += 2; }
    if (/font|color|style|css/.test(String(userMessage).toLowerCase()) && /\.css$/i.test(filePath)) score += 5;
    if (/html|label|button|text|checkbox|section/.test(String(userMessage).toLowerCase()) && /\.(html|tsx?|jsx?)$/i.test(filePath)) score += 4;
    if (score <= 0) continue;
    const lookup  = tokens.find(t => lowerContent.includes(t)) || tokens[0] || "";
    const excerpt = truncateText(buildExcerpt(content, lookup, 24) || content.slice(0, 1600), 1600);
    scored.push({ filePath, score, excerpt });
  }

  return scored.sort((a, b) => b.score - a.score).slice(0, 6).map(i => ({ filePath: i.filePath, excerpt: i.excerpt }));
}

const MANIFEST_REFERENCE = `Core manifest shape\n{\n  "Task": "required",\n  "Action": "required",\n  "FilePath": "required",\n  "Payload": "required"\n}\n\nSupported actions\n- newfile\n- rewritefile\n- replacestring\n- replaceblock\n- replacefunction\n- insertbefore\n- insertafter\n\nRules\n- Prefer the smallest valid edit.\n- Do not invent files or locator strings.\n- newfile and rewritefile require string Payload.\n- replacestring requires { target, replacement }.\n- replaceblock requires either { search, replace } or { target, replacement }.\n- replacefunction requires { target, replacement }.\n- insertbefore and insertafter require { find, content }.\n- Return one manifest object only, not an array.`;

const AI2_MAX_HISTORY_ITEMS = 4;
const AI2_MAX_HISTORY_TEXT = 220;
const AI2_MAX_INTENT_SECTIONS = 3;
const AI2_MAX_INTENT_ITEMS = 5;
const AI2_MAX_RELEVANT_FILES = 3;
const AI2_MAX_RELEVANT_EXCERPT = 700;

function boundAI2IntentRepo(sections: ProjectIntentSection[]): ProjectIntentSection[] {
  return (Array.isArray(sections) ? sections : [])
    .slice(0, AI2_MAX_INTENT_SECTIONS)
    .map(section => ({
      title: truncateText(String(section.title || ""), 80),
      type: String(section.type || "numbered"),
      items: Array.isArray(section.items)
        ? section.items.slice(0, AI2_MAX_INTENT_ITEMS).map(item => {
            if (isObject(item)) {
              return {
                label: truncateText(String(item.label || item.name || ""), 60),
                description: truncateText(String(item.description || item.text || ""), 180)
              };
            }
            return truncateText(String(item || ""), 180);
          })
        : []
    }))
    .filter(section => section.title);
}

function boundAI2RelevantFiles(files: Array<{ filePath: string; excerpt: string }>): Array<{ filePath: string; excerpt: string }> {
  return (Array.isArray(files) ? files : [])
    .slice(0, AI2_MAX_RELEVANT_FILES)
    .map(file => ({
      filePath: String(file.filePath || ""),
      excerpt: truncateText(String(file.excerpt || ""), AI2_MAX_RELEVANT_EXCERPT)
    }))
    .filter(file => file.filePath);
}

async function buildAI2UserPayload(
  userMessage:   string,
  recentHistory: Array<{ role: Role; text: string }>
): Promise<Record<string, any>> {
  const project        = await buildProjectOverview();
  const relevantFiles  = isLikelyChangeRequest(userMessage)
    ? await findRelevantProjectFiles(userMessage, project.root)
    : [];

  return {
    userMessage: truncateText(userMessage, 900),
    requestMode: relevantFiles.length ? "change" : "discussion",
    recentHistory: recentHistory
      .slice(-AI2_MAX_HISTORY_ITEMS)
      .map(item => ({ role: item.role, text: truncateText(item.text, AI2_MAX_HISTORY_TEXT) })),
    manifestReference: MANIFEST_REFERENCE,
    projectOverview: {
      name: truncateText(project.name, 80),
      root: truncateText(project.root, 120),
      summary: truncateText(project.summary, 300),
      notes: truncateText(project.notes, 300)
    },
    intentRepo: boundAI2IntentRepo(project.intentRepo),
    relevantFiles: boundAI2RelevantFiles(relevantFiles)
  };
}

async function buildAI3UserPayload(input: {
  manifests?: ManifestLike[];
  results?:   any[];
  batchMode?: boolean;
}): Promise<Record<string, any>> {
  const project = await buildProjectOverview();
  return {
    batchMode: input.batchMode ?? false,
    projectOverview: {
      name:    project.name,
      root:    project.root,
      summary: project.summary,
      notes:   project.notes
    },
    intentRepo:     project.intentRepo,
    completedTasks: (input.manifests ?? []).slice(-12).map(m => ({
      Task:     m?.Task     ?? null,
      Action:   m?.Action   ?? "",
      FilePath: m?.FilePath ?? null
    })),
    resultSummary: (input.results ?? []).slice(-12).map(r => ({
      status:  r?.status   ?? null,
      ok:      r?.ok       ?? null,
      message: typeof r?.message === "string" ? truncateText(r.message, 160) : null,
      error:   typeof r?.error   === "string" ? truncateText(r.error,   160) : null
    }))
  };
}

// ── Public API ─────────────────────────────────────────────────────────────────

export async function AI1correction(input: ManifestFailureInput): Promise<EQReport | null> {
  await ensureEQState();

  const manifest   = normalizeManifestLike(input.manifest) || {};
  const error      = String(input.error ?? "").trim();
  const batchID    = String(input.batchID ?? manifest.batchID ?? "").trim() || undefined;
  const filePath   = String(manifest.FilePath ?? "").trim();
  const failMode   = input.failureMode ?? "OTHER";
  const failLane   = input.failureLane ?? laneFromMode(failMode);
  const via        = viaFromFailureMode(failMode);

  if (via === "AI1OTHER" || failLane === "OTHER") {
    const report: EQReport = {
      kind: "errorReport", category: "error", source: "AI1correction",
      completion: "none", status: "report_only",
      summary: `Execution failed: ${error || "unknown error"}`,
      confidence: 0.4,
      originalManifest: manifest,
      issues: [{ severity: "error", kind: "manifestFailure", ...(filePath ? { filePath } : {}), message: error || "unknown error" }],
      ts: now(), batchID,
      meta: { stage: input.stage ?? "unknown", failureLane: failLane, failureMode: failMode }
    };
    await pushReport(report);
    await tapeAIManifest(report);
    return report;
  }

  const hypToken   = buildHypToken(via, String(manifest.Action || "unknown"), 1);
  const routePkg   = await requestRoutePackage({
    via,
    pathway: "AI1correction",
    context: scrubContext({ lane: failLane, mode: failMode, action: manifest.Action, stage: input.stage, filePath }),
    hypToken
  });

  const userPayload = await buildAI1UserPayload(input);
  const ai          = await callLocalAI(routePkg, userPayload);

  const adjustedManifest =
    mergeCorrection(manifest, ai?.correction) || normalizeManifestLike(ai?.adjustedManifest);

  if (adjustedManifest) {
    const report: EQReport = {
      kind: "assistMatch", category: "diagnostic", source: "AI1correction",
      completion: "pending", status: "repair_prepared",
      summary:    ai?.summary || `Prepared a bounded ${failMode} correction.`,
      confidence: normalizeConfidence(ai?.confidence, 0.7),
      originalManifest: manifest, adjustedManifest,
      issues: ai?.issues?.length
        ? ai.issues
        : [{ severity: "warning", kind: "correctionPrepared", ...(filePath ? { filePath } : {}), message: "A manifest correction was prepared." }],
      ts: now(), batchID,
      meta: { stage: input.stage ?? "unknown", failureLane: failLane, failureMode: failMode, offline: routePkg.offline }
    };
    logAI1CorrectionOutcome({
      via,
      strategy: routePkg.strategy,
      manifest,
      adjustedManifest,
      summary: report.summary,
      confidence: report.confidence,
      status: report.status,
      error
    });
    await pushReport(report);
    await tapeAIManifest(report);
    return report;
  }

  const report: EQReport = {
    kind: "errorReport", category: "error", source: "AI1correction",
    completion: "none", status: "report_only",
    summary:    ai?.summary || `Execution failed for ${filePath || "the current manifest"} and no safe correction was found.`,
    confidence: normalizeConfidence(ai?.confidence, 0),
    originalManifest: manifest,
    issues: ai?.issues?.length
      ? ai.issues
      : [{ severity: "error", kind: "searchFailure", ...(filePath ? { filePath } : {}), message: error }],
    ts: now(), batchID,
    meta: { stage: input.stage ?? "unknown", failureLane: failLane, failureMode: failMode, offline: routePkg.offline }
  };
  logAI1CorrectionOutcome({
    via,
    strategy: routePkg.strategy,
    manifest,
    summary: report.summary,
    confidence: report.confidence,
    status: report.status,
    error
  });
  await pushReport(report);
  await tapeAIManifest(report);
  return report;
}

export async function AI2chat(text: string): Promise<EQReport | null> {
  await ensureEQState();

  const clean = String(text ?? "").trim();
  if (!clean) return null;

  const settings = await loadSettings();
  const activity  = await readJson<ActivityMessage[]>(EQ_ACTIVITY_PATH, []);
  const history  = activity
    .slice(-Math.max(1, Math.min(12, settings.upstreamImportLimit || 12)))
    .map(m => ({ role: m.role, text: truncateText(m.text, 500) }));

  const hypToken   = buildHypToken("AI2chat", "unknown", 1);
  const routePkg   = await requestRoutePackage({
    via:      "AI2chat",
    pathway:  "AI2chat",
    context:  scrubContext({ requestMode: "chat", historyCount: history.length }),
    hypToken
  });

  const userPayload = await buildAI2UserPayload(clean, history);
  const ai          = await callLocalAI(routePkg, userPayload);
  if (!ai) return null;

  const project    = userPayload.projectOverview;
  const intentRepo = Array.isArray(userPayload.intentRepo) ? userPayload.intentRepo : [];
  const manifestLabel = ai.manifestKind === "diagnostic" ? "diagnostic" : ai.manifestKind === "build" ? "build" : ai.suggestedRepair ? "enhancement" : undefined;
  const reportCategory: EQCategory = manifestLabel === "diagnostic" ? "diagnostic" : manifestLabel ? "enhancement" : "diagnostic";
  const reportKind: EQReport["kind"] = reportCategory === "enhancement" ? "enhancement" : "chatAssist";
  const preparedStatus: EQReport["status"] = reportCategory === "enhancement" ? "enhancement_prepared" : "repair_prepared";


  const report: EQReport = ai.suggestedRepair
    ? {
        kind: reportKind, category: reportCategory, source: "AI2chat",
        completion: "pending", status: preparedStatus,
        summary:   ai.Response || ai.summary || "Chat assistance prepared a manifest reply.",
        confidence: normalizeConfidence(ai.confidence, 0),
        issues: ai.issues || [], suggestedRepair: ai.suggestedRepair,
        ts: now(),
        meta: { requestMode: userPayload.requestMode, manifestLabel, historyCount: history.length, project: project?.name, projectRoot: project?.root, intentRepoCount: intentRepo.length, relevantFiles: Array.isArray(userPayload.relevantFiles) ? userPayload.relevantFiles.map((i: any) => i?.filePath).filter(Boolean) : [], offline: routePkg.offline }
      }
    : {
        kind: "chatAssist", category: "diagnostic", source: "AI2chat",
        completion: "none", status: "report_only",
        summary:   ai.Response || ai.summary || "Chat assistance reviewed the incoming request.",
        confidence: normalizeConfidence(ai.confidence, 0),
        issues: ai.issues || [],
        ts: now(),
        meta: { requestMode: userPayload.requestMode, historyCount: history.length, project: project?.name, projectRoot: project?.root, intentRepoCount: intentRepo.length, relevantFiles: [], offline: routePkg.offline }
      };

  await pushReport(report);

  if (classifyAIOutput(report) !== "reply_only") {
    const tapeItem = await tapeAIManifest(report);
    if (tapeItem) {
      report.meta = { ...(report.meta || {}), tapeDubID: tapeItem.dubID, tapeRecorderStatus: tapeItem.recorderStatus };
    }
  }

  return report;
}

export async function recordChatIn(text: string): Promise<EQReport | null> {
  const clean = String(text ?? "").trim();
  if (!clean) return null;
  await pushActivityMessage("user", clean);
  const report    = await AI2chat(clean);
  const replyText = String(report?.summary ?? "").trim();
  if (replyText) await pushActivityMessage("assistant", replyText);
  return report;
}

export async function AI3diagnostic(input: {
  batchID?:   string;
  manifests?: ManifestLike[];
  results?:   any[];
}): Promise<EQReport | null> {
  await ensureEQState();

  const batchID      = String(input.batchID || "").trim() || undefined;
  const manifestList = Array.isArray(input.manifests) ? input.manifests : [];
  const resultList   = Array.isArray(input.results)   ? input.results   : [];
  const batchMode    = manifestList.length > 1 || resultList.length > 1;

  const hypToken = buildHypToken("AI3diagnostic", batchMode ? "composite" : "unknown", manifestList.length || 1);
  const routePkg = await requestRoutePackage({
    via:      "AI3diagnostic",
    pathway:  "AI3diagnostic",
    context:  scrubContext({ batchMode, taskCount: manifestList.length, resultCount: resultList.length }),
    hypToken
  });

  const userPayload = await buildAI3UserPayload({ manifests: manifestList, results: resultList, batchMode });
  const ai          = await callLocalAI(routePkg, userPayload);
  if (!ai) return null;

  const finalIssues  = ai.issues || [];
  const hasRepair    = Boolean(ai.suggestedRepair);
  const topPriority  = routePkg.meta?.diagnosticPriority ?? "C";

  const report: EQReport = hasRepair
    ? {
        kind:       topPriority === "C" ? "enhancement" : "diagnosticRepair",
        category:   topPriority === "C" ? "enhancement" : "diagnostic",
        source:     "AI3diagnostic",
        completion: "pending",
        status:     topPriority === "C" ? "enhancement_prepared" : "repair_prepared",
        summary:    ai.summary || "Diagnostic audit prepared a recommendation.",
        confidence: normalizeConfidence(ai.confidence, 0),
        suggestedRepair: ai.suggestedRepair,
        issues:     finalIssues,
        ts:         now(), batchID,
        meta: { batchMode, diagnosticPriority: topPriority, offline: routePkg.offline }
      }
    : {
        kind:       topPriority === "A" ? "errorReport" : topPriority === "C" ? "enhancement" : "diagnosticRepair",
        category:   topPriority === "A" ? "error"       : topPriority === "C" ? "enhancement" : "diagnostic",
        source:     "AI3diagnostic",
        completion: "none",
        status:     "report_only",
        summary:    ai.summary || "Diagnostic audit completed.",
        confidence: normalizeConfidence(ai.confidence, 0),
        issues:     finalIssues,
        ts:         now(), batchID,
        meta: { batchMode, diagnosticPriority: topPriority, offline: routePkg.offline }
      };

  await pushReport(report);
  if (classifyAIOutput(report) !== "reply_only") await tapeAIManifest(report);
  return report;
}

export async function initEQ(): Promise<void> {
  await ensureEQState();
  try { await syncModelFromLMStudio(); } catch (err: any) {
    logSystem("EQ", { event: "eq_model_sync_warn", error: err?.message ?? String(err) });
  }
  logSystem("EQ", { event: "eq_ready" });
}

export async function getReports(limit = 50): Promise<EQReport[]> {
  await ensureEQState();
  const reports = await readJson<EQReport[]>(EQ_REPORTS_PATH, []);
  return reports.slice(-limit);
}

export async function getRecentActivity(limit = 40): Promise<ActivityMessage[]> {
  await ensureEQState();
  const activity = await readJson<ActivityMessage[]>(EQ_ACTIVITY_PATH, []);
  return activity.slice(-limit);
}

export async function diagnoseCompletedManifest(
  manifest: ManifestLike,
  result:   any
): Promise<EQReport | null> {
  const status = result?.status || (result?.ok === false ? "failed" : "completed");
  return await AI3diagnostic({
    manifests: [{ Task: manifest?.Task, Action: manifest?.Action, FilePath: manifest?.FilePath, Comment: typeof result?.message === "string" ? truncateText(result.message, 160) : `${status}` }],
    results:   [result]
  });
}

export async function investigateManifestFailure(
  input: ManifestFailureInput
): Promise<EQReport | null> {
  return await AI1correction(input);
}