/**
 * write_hypothesis.ts — Holmes's primary writer.
 *
 * Inserts a row into public.hypotheses and renders case/hypotheses/H-NNNN.md.
 * Validates:
 *  - question + position + arguments are non-empty strings
 *  - prior ∈ [0,1], posterior ∈ [0,1] when present
 *  - confidence_band matches Tetlock thresholds (high ≥ 0.90, medium 0.60-0.89,
 *    low 0.30-0.59, speculation < 0.30); auto-corrects to the band that
 *    matches the posterior when caller disagrees
 *  - evidence_refs[].evidence_id must already exist in public.evidence
 */
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 EvidenceRef {
  evidence_id: string;
  supports?: boolean;
  weight?: number;
}

export interface WriteHypothesisArgs {
  question: string;
  question_pt_br?: string;
  position: string;
  position_pt_br?: string;
  argument_for?: string;
  argument_for_pt_br?: string;
  argument_against?: string;
  argument_against_pt_br?: string;
  prior?: number;
  posterior?: number;
  confidence_band?: "high" | "medium" | "low" | "speculation";
  evidence_refs?: EvidenceRef[];
  status?: "open" | "closed" | "dormant" | "superseded";
}

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

function bandFromPosterior(p: number): "high" | "medium" | "low" | "speculation" {
  if (p >= 0.90) return "high";
  if (p >= 0.60) return "medium";
  if (p >= 0.30) return "low";
  return "speculation";
}

function clamp01(n: unknown): number | null {
  if (typeof n !== "number" || !Number.isFinite(n)) return null;
  return Math.max(0, Math.min(1, n));
}

function renderMd(id: string, body: WriteHypothesisArgs, ctx: WriteHypothesisContext): string {
  const evRefs = (body.evidence_refs ?? []).map((r) =>
    `  - [[evidence/${r.evidence_id}]] (${r.supports === false ? "refutes" : "supports"}${typeof r.weight === "number" ? `, w=${r.weight}` : ""})`,
  ).join("\n");
  const fm = [
    "---",
    `schema_version: "0.1.0"`,
    `type: hypothesis`,
    `hypothesis_id: ${id}`,
    body.prior !== undefined ? `prior: ${body.prior}` : null,
    body.posterior !== undefined ? `posterior: ${body.posterior}` : null,
    body.confidence_band ? `confidence_band: ${body.confidence_band}` : null,
    `status: ${body.status ?? "open"}`,
    `created_by: ${ctx.detective}`,
    `job_id: ${ctx.job_id}`,
    `created_at: ${new Date().toISOString()}`,
    "---",
  ].filter(Boolean).join("\n");

  const ptQuestion = body.question_pt_br ?? body.question;
  const ptPosition = body.position_pt_br ?? body.position;
  const ptFor      = body.argument_for_pt_br ?? body.argument_for;
  const ptAgainst  = body.argument_against_pt_br ?? body.argument_against;

  return [
    fm,
    "",
    `# Hypothesis ${id}`,
    "",
    `**Question (EN).** ${body.question}`,
    `**Pergunta (PT-BR).** ${ptQuestion}`,
    "",
    `**Position (EN).** ${body.position}`,
    `**Posição (PT-BR).** ${ptPosition}`,
    "",
    "## Argument for (EN)",
    "",
    body.argument_for || "_(none recorded — speculation)_",
    "",
    "## Argumento a favor (PT-BR)",
    "",
    ptFor || "_(nenhum registrado — especulação)_",
    "",
    "## Argument against (EN)",
    "",
    body.argument_against || "_(none recorded — no counter-argument framed yet)_",
    "",
    "## Argumento contra (PT-BR)",
    "",
    ptAgainst || "_(nenhum registrado — sem contra-argumento ainda)_",
    "",
    "## Evidence",
    "",
    evRefs || "_(none linked yet — Locard chain pending)_",
    "",
  ].join("\n");
}

export async function writeHypothesis(
  body: WriteHypothesisArgs,
  ctx: WriteHypothesisContext,
): Promise<{ hypothesis_id: string; case_file: string }> {
  if (!body.question?.trim()) throw new Error("question required");
  if (!body.position?.trim()) throw new Error("position required");
  if (!body.position_pt_br?.trim()) throw new Error("position_pt_br required (bilingual contract)");
  if (body.argument_for && !body.argument_for_pt_br?.trim()) {
    throw new Error("argument_for_pt_br required when argument_for is set (bilingual contract)");
  }
  if (body.argument_against && !body.argument_against_pt_br?.trim()) {
    throw new Error("argument_against_pt_br required when argument_against is set (bilingual contract)");
  }

  const prior = clamp01(body.prior);
  const posterior = clamp01(body.posterior);

  // Recompute band from posterior when set. Holmes can mis-label its own
  // confidence; we keep the model's intent in audit but persist the
  // band that matches the actual posterior.
  let band = body.confidence_band ?? null;
  if (posterior !== null) {
    const expected = bandFromPosterior(posterior);
    if (!band || band !== expected) band = expected;
  }

  // Validate evidence refs exist.
  const refs: EvidenceRef[] = [];
  for (const r of body.evidence_refs ?? []) {
    if (!r?.evidence_id?.trim()) continue;
    const e = await queryOne<{ evidence_pk: number }>(
      `SELECT evidence_pk FROM public.evidence WHERE evidence_id = $1`,
      [r.evidence_id],
    );
    if (!e) throw new Error(`evidence not found: ${r.evidence_id}`);
    refs.push({ evidence_id: r.evidence_id, supports: r.supports !== false, weight: r.weight });
  }

  const hypothesis_id = await allocate.hypothesisId();
  await query(
    `INSERT INTO public.hypotheses
       (hypothesis_id, question, question_pt_br, position, position_pt_br,
        argument_for, argument_for_pt_br, argument_against, argument_against_pt_br,
        evidence_refs, prior, posterior, confidence_band, status, created_by)
     VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb,$11,$12,$13,$14,$15)`,
    [
      hypothesis_id,
      body.question, body.question_pt_br ?? body.question,
      body.position, body.position_pt_br,
      body.argument_for ?? null, body.argument_for_pt_br ?? null,
      body.argument_against ?? null, body.argument_against_pt_br ?? null,
      JSON.stringify(refs),
      prior, posterior, band, body.status ?? "open",
      ctx.detective,
    ],
  );

  const dir = path.join(env.CASE_ROOT, "hypotheses");
  await mkdir(dir, { recursive: true });
  const file = path.join(dir, `${hypothesis_id}.md`);
  await writeFile(file, renderMd(hypothesis_id, { ...body, evidence_refs: refs, confidence_band: band ?? undefined }, ctx), "utf-8");

  await audit({
    event: "write_hypothesis",
    job_id: ctx.job_id,
    detective: ctx.detective,
    hypothesis_id,
    posterior,
    band,
    n_evidence: refs.length,
    file,
  });
  return { hypothesis_id, case_file: file };
}
