/**
 * schneier.ts — red-team review detective.
 *
 * Reads an existing hypothesis (DB row + evidence chain) and produces a
 * structured attack: hidden assumptions, failure modes, alternative
 * explanations, recommended discriminating tests, and a severity verdict.
 *
 * Workflow:
 *  1. Fetch the hypothesis row by hypothesis_id (404 → skipped).
 *  2. Fetch evidence rows referenced via evidence_refs FK chain — these
 *     give Schneier the actual quotes to attack. If empty, the prompt
 *     gets only the argument prose.
 *  3. Call Claude with schneier.md system prompt; parse strict JSON.
 *  4. Call writeRedTeamReview() which UPDATEs hypotheses.reviewed_by +
 *     appends a section to the H-NNNN.md case file.
 */
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, queryOne } from "../lib/pg";
import { writeRedTeamReview, type RedTeamReviewArgs } from "../tools/write_red_team_review";

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

export interface SchneierTask {
  job_id: string;
  hypothesis_id: string;
  budget_cap_usd?: number;
}

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;
  evidence_refs: unknown;
}

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

function buildPrompt(h: HypothesisRow, evidence: EvidenceRow[]): string {
  const evBlock = evidence.length === 0
    ? "_(no evidence cataloged yet — work from the argument prose alone)_"
    : evidence.map((e) =>
        [
          `--- ${e.evidence_id} (Grade ${e.grade})${e.confidence_band ? `, ${e.confidence_band}` : ""} ---`,
          `source: ${e.source_page_id}`,
          "",
          `> ${e.verbatim_excerpt.slice(0, 600)}`,
        ].join("\n"),
      ).join("\n\n");

  return [
    `# Hypothesis under red-team review`,
    "",
    `**ID.** ${h.hypothesis_id}`,
    `**Question.** ${h.question}`,
    `**Position.** ${h.position}`,
    `**Prior.** ${h.prior ?? "—"} · **Posterior.** ${h.posterior ?? "—"} · **Band.** ${h.confidence_band ?? "—"}`,
    "",
    "## Argument for (per author)",
    h.argument_for || "_(none recorded)_",
    "",
    "## Argument against (per author)",
    h.argument_against || "_(none recorded)_",
    "",
    "## Evidence chain",
    evBlock,
    "",
    "## Your task",
    "",
    "Red-team the hypothesis. Find what the author didn't address. Emit the",
    "JSON object exactly as specified by the system prompt — no prose, no",
    "code fence, no preamble. **Bilingual is mandatory:** every narrative",
    "field appears in both EN and PT-BR with matching array lengths. If",
    "the hypothesis is too thin to attack, emit the literal word",
    "`INSUFFICIENT_HYPOTHESIS`.",
  ].join("\n");
}

function extractJsonObject(text: string): Record<string, unknown> | null {
  const t = text.trim();
  if (/^`?INSUFFICIENT_HYPOTHESIS`?\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(`schneier returned no JSON object: ${t.slice(0, 200)}`);
  }
  const parsed = JSON.parse(stripped.slice(first, last + 1));
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error("schneier JSON is not an object");
  }
  return parsed as Record<string, unknown>;
}

function coerceStringArray(raw: unknown): string[] {
  if (!Array.isArray(raw)) return [];
  return raw.map((x) => typeof x === "string" ? x.trim() : "").filter((s) => s.length > 0);
}

export async function runSchneier(task: SchneierTask): Promise<
  | { hypothesis_id: string; case_file: string; severity: string }
  | { skipped: true; reason: string }
> {
  const h = await queryOne<HypothesisRow>(
    `SELECT hypothesis_id, question, position, argument_for, argument_against,
            prior, posterior, confidence_band, evidence_refs
       FROM public.hypotheses WHERE hypothesis_id = $1`,
    [task.hypothesis_id],
  );
  if (!h) return { skipped: true, reason: "hypothesis_not_found" };

  // Pull evidence chain. evidence_refs is a JSONB array of { evidence_id, ... }
  const refIds: string[] = [];
  if (Array.isArray(h.evidence_refs)) {
    for (const r of h.evidence_refs as Array<Record<string, unknown>>) {
      if (typeof r?.evidence_id === "string") refIds.push(r.evidence_id);
    }
  }
  const evidence = refIds.length > 0
    ? await query<EvidenceRow>(
        `SELECT evidence_id, grade, verbatim_excerpt, source_page_id, confidence_band
           FROM public.evidence WHERE evidence_id = ANY($1::text[]) ORDER BY evidence_id`,
        [refIds],
      )
    : [];

  await audit({
    event: "schneier_grounded",
    job_id: task.job_id,
    detective: "schneier@detective",
    hypothesis_id: h.hypothesis_id,
    n_evidence: evidence.length,
  });

  const systemPrompt = await readFile(PROMPT_PATH, "utf-8");
  const prompt = buildPrompt(h, evidence);
  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: "schneier@detective",
    cost_usd: llm.costUsd,
    tokens_in: llm.tokensIn,
    tokens_out: llm.tokensOut,
    duration_ms: llm.durationMs,
  });
  console.error(`[schneier] response (${llm.text.length} chars): ${llm.text.slice(0, 800)}`);

  const obj = extractJsonObject(llm.text);
  if (obj === null) return { skipped: true, reason: "INSUFFICIENT_HYPOTHESIS" };

  const severityRaw = typeof obj.severity === "string" ? obj.severity.toLowerCase() : "";
  const severity: "low" | "medium" | "high" =
    severityRaw === "high" ? "high" : severityRaw === "medium" ? "medium" : "low";
  const args: RedTeamReviewArgs = {
    hypothesis_id: h.hypothesis_id,
    severity,
    hidden_assumptions:             coerceStringArray(obj.hidden_assumptions),
    hidden_assumptions_pt_br:       coerceStringArray(obj.hidden_assumptions_pt_br),
    failure_modes:                  coerceStringArray(obj.failure_modes),
    failure_modes_pt_br:            coerceStringArray(obj.failure_modes_pt_br),
    alternative_explanations:       coerceStringArray(obj.alternative_explanations),
    alternative_explanations_pt_br: coerceStringArray(obj.alternative_explanations_pt_br),
    recommended_tests:              coerceStringArray(obj.recommended_tests),
    recommended_tests_pt_br:        coerceStringArray(obj.recommended_tests_pt_br),
    verdict_one_sentence: typeof obj.verdict_one_sentence === "string"
      ? obj.verdict_one_sentence.trim() : "",
    verdict_one_sentence_pt_br: typeof obj.verdict_one_sentence_pt_br === "string"
      ? obj.verdict_one_sentence_pt_br.trim() : "",
  };
  if (!args.verdict_one_sentence || !args.verdict_one_sentence_pt_br) {
    return { skipped: true, reason: "no_verdict_bilingual" };
  }

  return await writeRedTeamReview(args, {
    job_id: task.job_id,
    detective: "schneier@detective",
  });
}
