/**
 * case_writer.ts — final-narrative detective (Dr. Watson).
 *
 * Reads ALL artefacts and produces a five-act Watson-style narrative
 * tying them together. Takes a topic; if topic == "*" the report scopes
 * to the whole bureau.
 *
 * v0 strategy: select artefacts whose .topic / .question / position
 * relate to the topic via simple ILIKE. For finer matching we can swap
 * to embedding similarity later.
 */
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 { query } from "../lib/pg";
import { writeCaseReport } from "../tools/write_case_report";

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

export interface CaseWriterTask {
  job_id: string;
  topic: string;
  topic_pt_br?: string;
  /** When set, restrict to artefacts touching this doc_id (via chunk FK). */
  doc_id?: string;
  lang?: "pt" | "en";
  /** Slug for the output file. Defaults to a kebab-case of topic. */
  slug?: string;
  budget_cap_usd?: number;
}

interface EvidenceRow {
  evidence_id: string;
  grade: string;
  source_page_id: string;
  verbatim_excerpt: string;
  confidence_band: string | null;
  related_hypotheses: unknown;
}

interface HypothesisRow {
  hypothesis_id: string;
  question: string;
  position: string;
  argument_for: string | null;
  argument_against: string | null;
  prior: number | string | null;
  posterior: number | string | null;
  confidence_band: string | null;
  status: string;
  reviewed_by: string | null;
}

interface ContradictionRow {
  contradiction_id: string;
  topic: string;
  chunks: unknown;
  resolution_status: string;
  notes: string | null;
}

interface WitnessRow {
  witness_id: string;
  canonical_name: string | null;
  credibility: string | null;
  verdict: string | null;
  access_to_event: string | null;
  bias_notes: string | null;
}

interface GapRow {
  gap_id: string;
  description: string;
  scope: unknown;
  suggested_next_move: string | null;
  status: string;
}

function topicSlug(topic: string): string {
  return topic
    .toLowerCase()
    .replace(/[̀-ͯ]/g, "")
    .replace(/[^a-z0-9-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "")
    .slice(0, 80);
}

function renderEvidence(rows: EvidenceRow[]): string {
  if (rows.length === 0) return "_(no evidence catalogued for this topic)_";
  return rows.map((e) => [
    `### ${e.evidence_id} (Grade ${e.grade}${e.confidence_band ? `, ${e.confidence_band}` : ""})`,
    `Source page: ${e.source_page_id}`,
    "",
    `> ${e.verbatim_excerpt.slice(0, 700)}`,
  ].join("\n")).join("\n\n");
}

function renderHypotheses(rows: HypothesisRow[]): string {
  if (rows.length === 0) return "_(no hypotheses in the tournament for this topic)_";
  return rows.map((h) => [
    `### ${h.hypothesis_id} — ${h.confidence_band ?? "—"} (prior ${h.prior ?? "—"} → posterior ${h.posterior ?? "—"}, status ${h.status})`,
    `**Position.** ${h.position}`,
    h.reviewed_by ? `Reviewed by ${h.reviewed_by}` : "",
    "",
    "**Argument for.**",
    h.argument_for || "_(none recorded)_",
    "",
    "**Argument against.**",
    h.argument_against || "_(none recorded)_",
  ].filter(Boolean).join("\n")).join("\n\n");
}

function renderContradictions(rows: ContradictionRow[]): string {
  if (rows.length === 0) return "_(no contradictions on file for this topic)_";
  return rows.map((c) => {
    const positions = Array.isArray(c.chunks) ? c.chunks as Array<Record<string, unknown>> : [];
    const posLines = positions.map((p, i) => {
      const stance = p.stance ? ` (${p.stance})` : "";
      return `  ${i + 1}. ${String(p.statement ?? "—")}${stance}  →  [[${p.doc_id}/p${String(p.page).padStart(3, "0")}#${p.chunk_id}]]`;
    }).join("\n");
    return [
      `### ${c.contradiction_id} — ${c.topic} (${c.resolution_status})`,
      posLines || "_(no positions recorded)_",
      c.notes ? `\n_Notes: ${c.notes}_` : "",
    ].filter(Boolean).join("\n");
  }).join("\n\n");
}

function renderWitnesses(rows: WitnessRow[]): string {
  if (rows.length === 0) return "_(no witness analyses on file)_";
  return rows.map((w) => [
    `### ${w.witness_id} — ${w.canonical_name ?? "—"} (${w.credibility ?? "—"})`,
    w.verdict ? `**Verdict.** ${w.verdict}` : "",
    w.access_to_event ? `Access: ${w.access_to_event}` : "",
    w.bias_notes ? `Bias: ${w.bias_notes}` : "",
  ].filter(Boolean).join("\n")).join("\n\n");
}

function renderGaps(rows: GapRow[]): string {
  if (rows.length === 0) return "_(no outliers / gaps on file)_";
  return rows.map((g) => {
    const s = g.scope as Record<string, unknown> | null;
    const kind = s?.kind === "outlier" ? " (outlier)" : "";
    const why = s?.why_surprising ? `\n_Why surprising:_ ${String(s.why_surprising)}` : "";
    const model = s?.dominant_model ? `\n_Dominant model:_ ${String(s.dominant_model)}` : "";
    return [
      `### ${g.gap_id} — ${g.description}${kind} (${g.status})`,
      model,
      why,
      g.suggested_next_move ? `\n_Next move:_ ${g.suggested_next_move}` : "",
    ].filter(Boolean).join("\n");
  }).join("\n\n");
}

function buildPrompt(
  task: CaseWriterTask,
  evidence: EvidenceRow[],
  hypotheses: HypothesisRow[],
  contradictions: ContradictionRow[],
  witnesses: WitnessRow[],
  gaps: GapRow[],
): string {
  return [
    `# Case folder`,
    "",
    `**Topic (EN).** ${task.topic}`,
    `**Tópico (PT-BR).** ${task.topic_pt_br ?? task.topic}`,
    "",
    task.doc_id ? `Scoped to document: ${task.doc_id}` : "Scope: all documents",
    "",
    "**Bilingual output mandatory.** Write each act in BOTH English and",
    "Brazilian Portuguese (PT-BR), interleaved per the system-prompt",
    "structure. UTF-8 accents preserved. Verbatim chunk quotes stay in",
    "their source language; only the surrounding narration is translated.",
    "",
    "## Artefacts available",
    "",
    `### Evidence (E-NNNN) · ${evidence.length}`,
    renderEvidence(evidence),
    "",
    `### Hypotheses (H-NNNN) · ${hypotheses.length}`,
    renderHypotheses(hypotheses),
    "",
    `### Contradictions (R-NNNN) · ${contradictions.length}`,
    renderContradictions(contradictions),
    "",
    `### Witness analyses (W-NNNN) · ${witnesses.length}`,
    renderWitnesses(witnesses),
    "",
    `### Outliers / gaps (G-NNNN) · ${gaps.length}`,
    renderGaps(gaps),
    "",
    "## Your task",
    "",
    "Assemble the five-act Watson narrative per the system prompt. Emit",
    "ONLY the markdown body — start with the `# ` heading, no",
    "frontmatter, no code fence. If the artefacts are too thin, emit",
    "`INSUFFICIENT_ARTEFACTS` and stop.",
  ].join("\n");
}

function extractBody(text: string): string | null {
  const t = text.trim();
  if (/^`?INSUFFICIENT_ARTEFACTS`?\b/i.test(t)) return null;
  const stripped = t.replace(/^```(?:markdown|md)?\s*\n?/i, "").replace(/\n?```\s*$/i, "");
  // Find first H1 — anything before is preamble we drop.
  const h1 = stripped.indexOf("\n# ");
  if (h1 === -1) {
    if (stripped.startsWith("# ")) return stripped;
    throw new Error(`case-writer returned no H1: ${t.slice(0, 200)}`);
  }
  return stripped.slice(h1 + 1);
}

export async function runCaseWriter(task: CaseWriterTask): Promise<
  | { case_file: string; slug: string }
  | { skipped: true; reason: string }
> {
  const topic = task.topic.trim();
  const slug = task.slug ?? topicSlug(topic);

  const filter = `%${topic.toLowerCase()}%`;
  const docIdFilter = task.doc_id ?? null;

  // Pull artefacts SEQUENTIALLY. The investigator role has rolconnlimit=4 and
  // pool.max=4; Promise.all of 5 queries × max_parallel=2 jobs would demand
  // 10 simultaneous connections and the role rejects "too many connections".
  // Sequential gather still finishes well under a second — case-writer's hot
  // path is the LLM call, not these queries.
  const evidence = await query<EvidenceRow>(
    docIdFilter
      ? `SELECT e.evidence_id, e.grade, e.source_page_id, e.verbatim_excerpt,
                e.confidence_band, e.related_hypotheses
           FROM public.evidence e
          WHERE e.source_page_id LIKE $1 || '/%'
          ORDER BY e.evidence_id LIMIT 20`
      : `SELECT e.evidence_id, e.grade, e.source_page_id, e.verbatim_excerpt,
                e.confidence_band, e.related_hypotheses
           FROM public.evidence e
          WHERE LOWER(e.verbatim_excerpt) LIKE $1
          ORDER BY e.evidence_id LIMIT 20`,
    [docIdFilter ?? filter],
  );
  const hypotheses = await query<HypothesisRow>(
    `SELECT hypothesis_id, question, position, argument_for, argument_against,
            prior, posterior, confidence_band, status, reviewed_by
       FROM public.hypotheses
      WHERE LOWER(question) LIKE $1 OR LOWER(position) LIKE $1
      ORDER BY hypothesis_id LIMIT 12`,
    [filter],
  );
  const contradictions = await query<ContradictionRow>(
    `SELECT contradiction_id, topic, chunks, resolution_status, notes
       FROM public.contradictions
      WHERE LOWER(topic) LIKE $1
      ORDER BY contradiction_id LIMIT 8`,
    [filter],
  );
  const witnesses = await query<WitnessRow>(
    `SELECT w.witness_id, e.canonical_name, w.credibility, w.verdict,
            w.access_to_event, w.bias_notes
       FROM public.witnesses w
  LEFT JOIN public.entities e ON e.entity_pk = w.person_entity_pk
      WHERE LOWER(COALESCE(w.verdict,'')) LIKE $1
         OR LOWER(COALESCE(e.canonical_name,'')) LIKE $1
      ORDER BY w.witness_id LIMIT 8`,
    [filter],
  );
  const gaps = await query<GapRow>(
    `SELECT gap_id, description, scope, suggested_next_move, status
       FROM public.gaps
      WHERE LOWER(description) LIKE $1
         OR LOWER(COALESCE(scope->>'title','')) LIKE $1
         OR LOWER(COALESCE(scope->>'why_surprising','')) LIKE $1
      ORDER BY gap_id LIMIT 8`,
    [filter],
  );

  await audit({
    event: "case_writer_grounded",
    job_id: task.job_id,
    detective: "case-writer@detective",
    topic, slug, doc_id: docIdFilter,
    n_evidence: evidence.length,
    n_hypotheses: hypotheses.length,
    n_contradictions: contradictions.length,
    n_witnesses: witnesses.length,
    n_gaps: gaps.length,
  });

  const total = evidence.length + hypotheses.length + contradictions.length
              + witnesses.length + gaps.length;
  if (total < 2 || (evidence.length === 0 && hypotheses.length === 0)) {
    return { skipped: true, reason: "insufficient_artefacts" };
  }

  const systemPrompt = await readFile(PROMPT_PATH, "utf-8");
  const prompt = buildPrompt(task, evidence, hypotheses, contradictions, witnesses, gaps);

  // Case-writer wants more output budget than the other detectives.
  const llm = await callClaude({
    prompt, systemPrompt,
    model: env.CLAUDE_MODEL,
    allowedTools: [],
    timeoutMs: Math.max(env.JOB_TIMEOUT_SECONDS, 240) * 1000,
    budgetCapUsd: task.budget_cap_usd ?? Math.max(env.BUDGET_CAP_USD_PER_JOB, 0.50),
  });
  await audit({
    event: "detective_completed",
    job_id: task.job_id,
    detective: "case-writer@detective",
    cost_usd: llm.costUsd,
    tokens_in: llm.tokensIn,
    tokens_out: llm.tokensOut,
    duration_ms: llm.durationMs,
  });
  console.error(`[case-writer] response (${llm.text.length} chars): ${llm.text.slice(0, 600)}`);

  const body_md = extractBody(llm.text);
  if (body_md === null) return { skipped: true, reason: "INSUFFICIENT_ARTEFACTS" };

  return await writeCaseReport({
    topic, topic_pt_br: task.topic_pt_br, slug, body_md,
    meta: {
      n_evidence: evidence.length,
      n_hypotheses: hypotheses.length,
      n_contradictions: contradictions.length,
      n_witnesses: witnesses.length,
      n_outliers: gaps.filter((g) => {
        const s = g.scope as Record<string, unknown> | null;
        return s?.kind === "outlier";
      }).length,
      n_calibrations: 0, // Calibrations live inside hypothesis case files, not a table yet.
    },
  }, { job_id: task.job_id, detective: "case-writer@detective" });
}

