/**
 * write_outlier_gap.ts — Taleb's primary writer.
 *
 * INSERTs a row into public.gaps and writes case/gaps/G-NNNN.md.
 * Reuses the gap_id_seq slot (G = gap, per CLAUDE.md). The `scope` JSONB
 * column carries {kind: "outlier", chunk_ref, dominant_model, why_surprising,
 * what_it_implies} so the case-writer can render the outlier as a residual
 * uncertainty in the final case-report.md.
 */
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { audit } from "../lib/audit";
import { env } from "../lib/env";
import { allocate } from "../lib/ids";
import { query, queryOne } from "../lib/pg";

export interface WriteOutlierGapArgs {
  title: string;
  title_pt_br?: string;
  doc_id: string;
  chunk_id: string;
  dominant_model: string;
  dominant_model_pt_br?: string;
  why_surprising: string;
  why_surprising_pt_br?: string;
  what_it_implies: string;
  what_it_implies_pt_br?: string;
  suggested_next_move: string;
  suggested_next_move_pt_br?: string;
}

export interface WriteOutlierGapContext {
  job_id: string;
  detective: string;
}

function normalizeChunkId(raw: string): string {
  const m = raw.match(/c\d{4,}$/);
  return m ? m[0] : raw;
}

function renderMd(
  id: string,
  body: WriteOutlierGapArgs,
  chunk_pk: number,
  page: number,
  ctx: WriteOutlierGapContext,
): string {
  const pageStr = String(page).padStart(3, "0");
  const fm = [
    "---",
    `schema_version: "0.1.0"`,
    `type: gap`,
    `gap_id: ${id}`,
    `kind: outlier`,
    `title: ${JSON.stringify(body.title)}`,
    `chunk_pk: ${chunk_pk}`,
    `chunk_ref: ${JSON.stringify(`${body.doc_id}/p${pageStr}#${body.chunk_id}`)}`,
    `created_by: ${ctx.detective}`,
    `job_id: ${ctx.job_id}`,
    `created_at: ${new Date().toISOString()}`,
    "---",
  ].join("\n");

  return [
    fm,
    "",
    `# Outlier ${id} — ${body.title}`,
    `**Título (PT-BR).** ${body.title_pt_br ?? body.title}`,
    "",
    `**Source.** [[${body.doc_id}/p${pageStr}#${body.chunk_id}]]`,
    "",
    "## Dominant model (EN)",
    "",
    body.dominant_model,
    "",
    "## Modelo dominante (PT-BR)",
    "",
    body.dominant_model_pt_br ?? body.dominant_model,
    "",
    "## Why surprising (EN)",
    "",
    body.why_surprising,
    "",
    "## Por que é surpreendente (PT-BR)",
    "",
    body.why_surprising_pt_br ?? body.why_surprising,
    "",
    "## What it implies (EN)",
    "",
    body.what_it_implies,
    "",
    "## O que implica (PT-BR)",
    "",
    body.what_it_implies_pt_br ?? body.what_it_implies,
    "",
    "## Suggested next move (EN)",
    "",
    body.suggested_next_move,
    "",
    "## Próximo passo sugerido (PT-BR)",
    "",
    body.suggested_next_move_pt_br ?? body.suggested_next_move,
    "",
  ].join("\n");
}

export async function writeOutlierGap(
  body: WriteOutlierGapArgs,
  ctx: WriteOutlierGapContext,
): Promise<{ gap_id: string; case_file: string }> {
  if (!body.title?.trim()) throw new Error("title required");
  if (!body.title_pt_br?.trim()) throw new Error("title_pt_br required (bilingual contract)");
  if (!body.doc_id?.trim() || !body.chunk_id?.trim()) throw new Error("doc_id + chunk_id required");
  if (!body.dominant_model?.trim()) throw new Error("dominant_model required");
  if (!body.dominant_model_pt_br?.trim()) throw new Error("dominant_model_pt_br required (bilingual contract)");
  if (!body.why_surprising?.trim()) throw new Error("why_surprising required");
  if (!body.why_surprising_pt_br?.trim()) throw new Error("why_surprising_pt_br required (bilingual contract)");
  if (!body.what_it_implies?.trim()) throw new Error("what_it_implies required");
  if (!body.what_it_implies_pt_br?.trim()) throw new Error("what_it_implies_pt_br required (bilingual contract)");
  if (!body.suggested_next_move?.trim()) throw new Error("suggested_next_move required");
  if (!body.suggested_next_move_pt_br?.trim()) throw new Error("suggested_next_move_pt_br required (bilingual contract)");
  if (body.why_surprising.length > 600) throw new Error(`why_surprising too long`);
  if (body.why_surprising_pt_br.length > 600) throw new Error(`why_surprising_pt_br too long`);

  const cid = normalizeChunkId(body.chunk_id);
  const chunk = await queryOne<{ chunk_pk: number; page: number }>(
    `SELECT chunk_pk, page FROM public.chunks WHERE doc_id = $1 AND chunk_id = $2`,
    [body.doc_id, cid],
  );
  if (!chunk) throw new Error(`chunk ${body.doc_id}/${cid} not found`);

  const gap_id = await allocate.gapId();
  const scope = {
    kind: "outlier",
    chunk_pk: chunk.chunk_pk,
    doc_id: body.doc_id,
    chunk_id: cid,
    page: chunk.page,
    title:                  body.title,
    title_pt_br:            body.title_pt_br,
    dominant_model:         body.dominant_model,
    dominant_model_pt_br:   body.dominant_model_pt_br,
    why_surprising:         body.why_surprising,
    why_surprising_pt_br:   body.why_surprising_pt_br,
    what_it_implies:        body.what_it_implies,
    what_it_implies_pt_br:  body.what_it_implies_pt_br,
  };

  await query(
    `INSERT INTO public.gaps
       (gap_id, description, description_pt_br, scope,
        suggested_next_move, suggested_next_move_pt_br, status, created_by)
     VALUES ($1, $2, $3, $4::jsonb, $5, $6, 'open', $7)`,
    [
      gap_id,
      body.title,
      body.title_pt_br,
      JSON.stringify(scope),
      body.suggested_next_move,
      body.suggested_next_move_pt_br,
      ctx.detective,
    ],
  );

  const dir = path.join(env.CASE_ROOT, "gaps");
  await mkdir(dir, { recursive: true });
  const file = path.join(dir, `${gap_id}.md`);
  await writeFile(file, renderMd(gap_id, { ...body, chunk_id: cid }, chunk.chunk_pk, chunk.page, ctx), "utf-8");

  await audit({
    event: "write_outlier_gap",
    job_id: ctx.job_id,
    detective: ctx.detective,
    gap_id,
    chunk_pk: chunk.chunk_pk,
    title: body.title.slice(0, 120),
    file,
  });

  return { gap_id, case_file: file };
}
