/**
 * taleb.ts — outlier-hunter detective.
 *
 * Given a topic, locates the most surprising chunks given the dominant
 * explanations. Writes each as a public.gaps row + case/gaps/G-NNNN.md.
 *
 * Workflow:
 *  1. hybridSearch shortlist (k=18, same as Dupin — outliers need
 *     dispersion).
 *  2. Claude reads + emits a JSON array of 0-3 outliers.
 *  3. writeOutlierGap() validates chunk_pk FK + persists.
 */
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { audit } from "../lib/audit";
import { callClaude } from "../lib/claude";
import { env } from "../lib/env";
import { hybridSearch, type SearchHit } from "../lib/search";
import { writeOutlierGap, type WriteOutlierGapArgs } from "../tools/write_outlier_gap";

const HERE = path.dirname(fileURLToPath(import.meta.url));
const PROMPT_PATH = path.resolve(HERE, "..", "..", "prompts", "taleb.md");

export interface TalebTask {
  job_id: string;
  topic: string;
  doc_id?: string;
  lang?: "pt" | "en";
  context_chunks?: number;
  budget_cap_usd?: number;
}

function renderChunkBlock(hits: SearchHit[], lang: "pt" | "en"): string {
  return hits.map((h, i) => {
    const text = (lang === "en" ? h.content_en : h.content_pt) || h.content_en || h.content_pt || "";
    return [
      `--- chunk ${i + 1} ---`,
      `doc_id: ${h.doc_id}`,
      `chunk_id: ${h.chunk_id}`,
      `page: ${h.page}`,
      `type: ${h.type}`,
      h.classification ? `classification: ${h.classification}` : null,
      "",
      text.slice(0, 1100),
    ].filter(Boolean).join("\n");
  }).join("\n\n");
}

function buildPrompt(task: TalebTask, hits: SearchHit[], lang: "pt" | "en"): string {
  return [
    `# Topic to scan for outliers`,
    "",
    task.topic,
    "",
    `## Corpus shortlist (${hits.length} chunks${task.doc_id ? `, scoped to ${task.doc_id}` : ""})`,
    "",
    renderChunkBlock(hits, lang),
    "",
    "## Your task",
    "",
    "Identify AT MOST 3 outliers per the system prompt rules. State the",
    "dominant_model first, then the chunk that violates it. Emit the",
    "JSON array exactly as specified — no prose, no code fence. If",
    "nothing genuinely stands out, emit `NO_OUTLIERS`.",
  ].join("\n");
}

function extractJsonArray(text: string): unknown[] | null {
  const t = text.trim();
  if (/^`?NO_OUTLIERS`?\b/i.test(t)) return null;
  const stripped = t.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "");
  const first = stripped.indexOf("[");
  const last = stripped.lastIndexOf("]");
  if (first === -1 || last === -1) {
    throw new Error(`taleb returned no JSON array: ${t.slice(0, 200)}`);
  }
  const parsed = JSON.parse(stripped.slice(first, last + 1));
  if (!Array.isArray(parsed)) throw new Error("taleb JSON is not an array");
  return parsed;
}

export async function runTaleb(task: TalebTask): Promise<
  | { outliers: Array<{ gap_id: string; case_file: string }> }
  | { skipped: true; reason: string }
> {
  const lang: "pt" | "en" = task.lang ?? "pt";
  const k = task.context_chunks ?? 18;

  let hits = await hybridSearch({
    query: task.topic, lang,
    doc_id: task.doc_id ?? null,
    top_k: k, recall_k: 80,
  });
  let scope_widened = false;
  if (task.doc_id && hits.length < 3) {
    const widened = await hybridSearch({
      query: task.topic, lang, doc_id: null, top_k: k, recall_k: 80,
    });
    if (widened.length > hits.length) {
      hits = widened;
      scope_widened = true;
    }
  }

  await audit({
    event: "taleb_grounded",
    job_id: task.job_id,
    detective: "taleb@detective",
    topic: task.topic,
    n_chunks: hits.length,
    doc_id: task.doc_id ?? null,
    scope_widened,
  });

  if (hits.length < 3) {
    return { skipped: true, reason: "insufficient_corpus" };
  }

  const systemPrompt = await readFile(PROMPT_PATH, "utf-8");
  const prompt = buildPrompt(task, hits, lang);
  const llm = await callClaude({
    prompt, systemPrompt,
    model: env.CLAUDE_MODEL,
    allowedTools: [],
    timeoutMs: env.JOB_TIMEOUT_SECONDS * 1000,
    budgetCapUsd: task.budget_cap_usd ?? env.BUDGET_CAP_USD_PER_JOB,
  });
  await audit({
    event: "detective_completed",
    job_id: task.job_id,
    detective: "taleb@detective",
    cost_usd: llm.costUsd,
    tokens_in: llm.tokensIn,
    tokens_out: llm.tokensOut,
    duration_ms: llm.durationMs,
  });
  console.error(`[taleb] response (${llm.text.length} chars): ${llm.text.slice(0, 600)}`);

  const arr = extractJsonArray(llm.text);
  if (arr === null) return { skipped: true, reason: "NO_OUTLIERS" };

  const out: Array<{ gap_id: string; case_file: string }> = [];
  for (const raw of arr.slice(0, 3)) {
    if (!raw || typeof raw !== "object") continue;
    const o = raw as Record<string, unknown>;
    const args: WriteOutlierGapArgs = {
      title: typeof o.title === "string" ? o.title.trim() : "",
      doc_id: typeof o.doc_id === "string" ? o.doc_id.trim() : "",
      chunk_id: typeof o.chunk_id === "string" ? o.chunk_id.trim() : "",
      dominant_model: typeof o.dominant_model === "string" ? o.dominant_model.trim() : "",
      why_surprising: typeof o.why_surprising === "string" ? o.why_surprising.trim() : "",
      what_it_implies: typeof o.what_it_implies === "string" ? o.what_it_implies.trim() : "",
      suggested_next_move: typeof o.suggested_next_move === "string" ? o.suggested_next_move.trim() : "",
    };
    if (!args.title || !args.doc_id || !args.chunk_id || !args.dominant_model
        || !args.why_surprising || !args.what_it_implies || !args.suggested_next_move) {
      await audit({
        event: "write_outlier_gap_failed",
        job_id: task.job_id, detective: "taleb@detective",
        reason: "incomplete_outlier", title: args.title.slice(0, 120),
      });
      continue;
    }
    try {
      const r = await writeOutlierGap(args, { job_id: task.job_id, detective: "taleb@detective" });
      out.push(r);
    } catch (e) {
      await audit({
        event: "write_outlier_gap_failed",
        job_id: task.job_id, detective: "taleb@detective",
        error: (e as Error).message, title: args.title.slice(0, 120),
      });
    }
  }
  return { outliers: out };
}
