/**
 * commands-todo.ts - triage the repo-root TODO.md dump inbox.
 *
 * Purpose: turn low-friction human dumps into reviewable eval, harness, memory,
 * docs, test, and implementation artifacts without treating raw notes as
 * approved runtime behavior.
 *
 * Consumer: `/todo triage` command.
 */

import { createHash } from "node:crypto";
import {
	existsSync,
	mkdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { projectRoot } from "./commands/context.js";
import { getErrorMessage } from "./error-utils.js";
import { sfRoot } from "./paths.js";
import {
	addBacklogItem,
	insertTriageEval,
	insertTriageItem,
	insertTriageRun,
	insertTriageSkill,
	openDatabase,
} from "./sf-db.js";

const _EMPTY_TODO = "# TODO\n\nDump anything here.\n";
const MAX_DUMP_CHARS = 48_000;
const TODO_TRIAGE_SCHEMA_VERSION = 1;
const PREFERRED_TRIAGE_MODEL_PATTERNS = [
	/minimax.*m2\.7.*highspeed/i,
	/minimax.*m2\.5.*highspeed/i,
	/minimax.*m2\.7/i,
	/minimax.*m2\.5/i,
	/haiku/i,
];
function timestampId(date = new Date()) {
	const pad = (n) => String(n).padStart(2, "0");
	return [
		date.getFullYear(),
		pad(date.getMonth() + 1),
		pad(date.getDate()),
		"-",
		pad(date.getHours()),
		pad(date.getMinutes()),
		pad(date.getSeconds()),
	].join("");
}
function extractJsonObject(text) {
	const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
	if (fenced?.[1]?.trim()) return fenced[1].trim();
	const first = text.indexOf("{");
	const last = text.lastIndexOf("}");
	if (first !== -1 && last > first) return text.slice(first, last + 1);
	return text;
}
function stringArray(value) {
	if (!Array.isArray(value)) return [];
	return value
		.filter((item) => typeof item === "string")
		.map((item) => item.trim())
		.filter(Boolean);
}
function evalCandidates(value) {
	if (!Array.isArray(value)) return [];
	return value
		.filter((item) => {
			return (
				typeof item === "object" &&
				item !== null &&
				typeof item.task_input === "string" &&
				typeof item.expected_behavior === "string"
			);
		})
		.map((item, idx) => ({
			id:
				typeof item.id === "string" && item.id.trim()
					? item.id.trim()
					: `todo.eval.${String(idx + 1).padStart(3, "0")}`,
			task_input:
				typeof item.task_input === "string" ? item.task_input.trim() : "",
			expected_behavior:
				typeof item.expected_behavior === "string"
					? item.expected_behavior.trim()
					: "",
			failure_mode:
				typeof item.failure_mode === "string"
					? item.failure_mode.trim()
					: undefined,
			evidence:
				typeof item.evidence === "string" ? item.evidence.trim() : undefined,
			source: typeof item.source === "string" ? item.source.trim() : "TODO.md",
			suggested_location:
				typeof item.suggested_location === "string"
					? item.suggested_location.trim()
					: undefined,
		}))
		.filter((item) => item.task_input && item.expected_behavior);
}
export function parseTodoTriageResponse(response) {
	const parsed = JSON.parse(extractJsonObject(response));
	return {
		summary:
			typeof parsed.summary === "string" && parsed.summary.trim()
				? parsed.summary.trim()
				: "TODO dump triaged.",
		eval_candidates: evalCandidates(parsed.eval_candidates),
		implementation_tasks: stringArray(parsed.implementation_tasks),
		memory_requirements: stringArray(parsed.memory_requirements),
		harness_suggestions: stringArray(parsed.harness_suggestions),
		docs_or_tests: stringArray(parsed.docs_or_tests),
		unclear_notes: stringArray(parsed.unclear_notes),
	};
}
export function extractTodoDump(rawTodo) {
	const lines = rawTodo.replace(/\r\n/g, "\n").split("\n");
	const body = lines
		.filter((line, idx) => {
			if (idx === 0 && line.trim().toLowerCase() === "# todo") return false;
			if (line.trim() === "Dump anything here.") return false;
			return true;
		})
		.join("\n")
		.trim();
	return body;
}
function section(title, items) {
	if (items.length === 0) return `## ${title}\n\nNone.\n`;
	return `## ${title}\n\n${items.map((item) => `- ${item}`).join("\n")}\n`;
}
export function renderTriageMarkdown(result, sourcePath) {
	const evals =
		result.eval_candidates.length === 0
			? "None.\n"
			: result.eval_candidates
					.map((item) => {
						const lines = [
							`- ${item.id ?? "todo.eval"}`,
							`  - Trigger/input: ${item.task_input}`,
							`  - Expected behavior: ${item.expected_behavior}`,
						];
						if (item.failure_mode)
							lines.push(`  - Failure mode observed: ${item.failure_mode}`);
						if (item.evidence)
							lines.push(`  - Evidence/source: ${item.evidence}`);
						if (item.suggested_location)
							lines.push(`  - Suggested location: ${item.suggested_location}`);
						return lines.join("\n");
					})
					.join("\n\n") + "\n";
	return [
		"# TODO Triage",
		"",
		`Source: ${sourcePath}`,
		`Generated: ${new Date().toISOString()}`,
		"",
		"## Summary",
		"",
		result.summary,
		"",
		"## Eval Candidates",
		"",
		evals,
		section("Implementation Tasks", result.implementation_tasks),
		section("Memory Requirements", result.memory_requirements),
		section("Harness Suggestions", result.harness_suggestions),
		section("Docs Or Tests", result.docs_or_tests),
		section("Unclear Notes", result.unclear_notes),
	].join("\n");
}
function _renderEvalJsonl(result) {
	return (
		result.eval_candidates
			.map((item) =>
				JSON.stringify({
					schemaVersion: TODO_TRIAGE_SCHEMA_VERSION,
					...item,
					source: item.source ?? "TODO.md",
				}),
			)
			.join("\n") + (result.eval_candidates.length > 0 ? "\n" : "")
	);
}
function detectRecurringPatterns(result) {
	const proposals = [];
	// Pattern 1: repeated eval candidates with similar task_input suggest a skill
	const evalGroups = new Map();
	for (const item of result.eval_candidates) {
		const key = item.task_input
			.toLowerCase()
			.replace(/[^a-z0-9]+/g, " ")
			.trim();
		const words = key.split(/\s+/).slice(0, 6).join(" ");
		const existing = evalGroups.get(words) ?? [];
		existing.push(item);
		evalGroups.set(words, existing);
	}
	for (const [pattern, items] of evalGroups) {
		if (items.length >= 2) {
			proposals.push({
				id: `skill.${timestampId()}`,
				title: `Skill: handle "${pattern.slice(0, 40)}${pattern.length > 40 ? "..." : ""}"`,
				trigger_pattern: pattern.slice(0, 60),
				description: `Recurring eval candidate (${items.length} occurrences) suggesting a reusable skill for this pattern.`,
				example_input: items[0].task_input,
				example_output: items[0].expected_behavior,
				confidence: items.length >= 3 ? "high" : "medium",
				source_evidence: items
					.map((i) => i.evidence ?? i.task_input)
					.filter(Boolean),
			});
		}
	}
	// Pattern 2: harness suggestions that appear multiple times
	const harnessGroups = new Map();
	for (const item of result.harness_suggestions) {
		const key = item
			.toLowerCase()
			.replace(/[^a-z0-9]+/g, " ")
			.trim();
		const words = key.split(/\s+/).slice(0, 6).join(" ");
		const existing = harnessGroups.get(words) ?? [];
		existing.push(item);
		harnessGroups.set(words, existing);
	}
	for (const [pattern, items] of harnessGroups) {
		if (items.length >= 2) {
			proposals.push({
				id: `skill.${timestampId()}`,
				title: `Skill: gate/harness for "${pattern.slice(0, 40)}${pattern.length > 40 ? "..." : ""}"`,
				trigger_pattern: pattern.slice(0, 60),
				description: `Recurring harness suggestion (${items.length} occurrences) suggesting a reusable quality gate or harness.`,
				example_input: items[0],
				example_output:
					"Deterministic gate passes / fails with structured output.",
				confidence: items.length >= 3 ? "high" : "medium",
				source_evidence: items,
			});
		}
	}
	// Pattern 3: memory requirements that appear multiple times
	const memoryGroups = new Map();
	for (const item of result.memory_requirements) {
		const key = item
			.toLowerCase()
			.replace(/[^a-z0-9]+/g, " ")
			.trim();
		const words = key.split(/\s+/).slice(0, 6).join(" ");
		const existing = memoryGroups.get(words) ?? [];
		existing.push(item);
		memoryGroups.set(words, existing);
	}
	for (const [pattern, items] of memoryGroups) {
		if (items.length >= 2) {
			proposals.push({
				id: `skill.${timestampId()}`,
				title: `Skill: remember "${pattern.slice(0, 40)}${pattern.length > 40 ? "..." : ""}"`,
				trigger_pattern: pattern.slice(0, 60),
				description: `Recurring memory requirement (${items.length} occurrences) suggesting a durable memory extraction skill.`,
				example_input: items[0],
				example_output: "Memory captured with confidence score and category.",
				confidence: items.length >= 3 ? "high" : "medium",
				source_evidence: items,
			});
		}
	}
	return proposals;
}
function _renderSkillProposals(result) {
	const proposals = detectRecurringPatterns(result);
	if (proposals.length === 0) return "\n";
	return (
		proposals
			.map((p) =>
				JSON.stringify({
					schemaVersion: TODO_TRIAGE_SCHEMA_VERSION,
					...p,
				}),
			)
			.join("\n") + "\n"
	);
}
function renderBacklogJsonl(items, triagedAt) {
	return (
		items
			.map((item) =>
				JSON.stringify({
					schemaVersion: TODO_TRIAGE_SCHEMA_VERSION,
					id: item.id,
					title: item.title,
					source: "todo-triage",
					kind: "implementation_task",
					...(item.evidence ? { evidence: item.evidence } : {}),
					triaged_at: triagedAt,
					status: "pending",
				}),
			)
			.join("\n") + (items.length > 0 ? "\n" : "")
	);
}
function appendBacklogItems(basePath, titles, triageRunId) {
	const cleanTitles = titles.map((title) => title.trim()).filter(Boolean);
	if (cleanTitles.length === 0) return 0;
	const date = new Date().toISOString().slice(0, 10);
	const triagedAt = new Date().toISOString();
	const backlogItems = [];
	const root = sfRoot(basePath);
	mkdirSync(root, { recursive: true });
	openDatabase(join(root, "sf.db"));
	for (const title of cleanTitles) {
		const cleanTitle = title.replace(/^['"]|['"]$/g, "");
		const id = addBacklogItem({
			title: cleanTitle,
			status: "pending",
			note: `triaged ${date}`,
			source: "todo-triage",
			triageRunId,
		});
		backlogItems.push({ id, title: cleanTitle });
	}
	// Also write versioned JSONL triage evidence; the executable backlog lives in DB.
	const backlogDir = join(basePath, ".sf", "triage", "backlog");
	mkdirSync(backlogDir, { recursive: true });
	const jsonlPath = join(backlogDir, `${triageRunId}.jsonl`);
	const existing = existsSync(jsonlPath)
		? readFileSync(jsonlPath, "utf-8")
		: "";
	const jsonlContent = renderBacklogJsonl(
		backlogItems.map((item) => ({ ...item, evidence: undefined })),
		triagedAt,
	);
	writeFileSync(jsonlPath, existing + jsonlContent, "utf-8");
	return cleanTitles.length;
}
function normalizedItems(result, createdAt) {
	const items = [];
	let seq = 1;
	const push = (kind, content, evidence) => {
		items.push({
			id: `triage.${String(seq++).padStart(3, "0")}`,
			source: "todo.md",
			kind,
			content,
			evidence,
			status: "pending",
			created_at: createdAt,
		});
	};
	for (const item of result.eval_candidates) {
		push(
			"eval_candidate",
			`${item.task_input}\nExpected: ${item.expected_behavior}`,
			item.evidence ?? item.failure_mode,
		);
	}
	for (const item of result.implementation_tasks)
		push("implementation_task", item);
	for (const item of result.memory_requirements)
		push("memory_requirement", item);
	for (const item of result.harness_suggestions)
		push("harness_suggestion", item);
	for (const item of result.docs_or_tests) push("docs_or_tests", item);
	for (const item of result.unclear_notes) push("unclear_note", item);
	return items;
}
function _renderNormalizedJsonl(result, createdAt) {
	const items = normalizedItems(result, createdAt);
	return (
		items
			.map((item) =>
				JSON.stringify({
					schemaVersion: TODO_TRIAGE_SCHEMA_VERSION,
					...item,
				}),
			)
			.join("\n") + (items.length ? "\n" : "")
	);
}
function buildTriagePrompt(dump) {
	return {
		system: `You are a triage agent for a software engineering repository.
Convert a messy TODO.md dump into structured, reviewable project work.

Return ONLY valid JSON with this shape:
{
  "summary": "short summary",
  "eval_candidates": [
    {
      "id": "short stable id if obvious",
      "task_input": "user/task input that should be evaluated",
      "expected_behavior": "specific expected behavior",
      "failure_mode": "observed failure or risk",
      "evidence": "quote or short source note",
      "source": "TODO.md",
      "suggested_location": "suggested eval/test/harness path"
    }
  ],
  "implementation_tasks": ["concrete implementation task"],
  "memory_requirements": ["memory extraction or retention requirement"],
  "harness_suggestions": ["gate/eval/harness suggestion"],
  "docs_or_tests": ["doc or test artifact to add/update"],
  "unclear_notes": ["notes that need clarification"]
}

Rules:
- Preserve concrete details from the dump.
- Do not invent completed work.
- Raw dump notes are evidence, not approved runtime behavior.
- Repeated failures should become eval candidates before behavior changes.
- Prefer deterministic tests/gates when possible; use model judges only as advisory unless calibrated.`,
		user: `Triage this repo-root TODO.md dump:\n\n<TODO_DUMP>\n${dump}\n</TODO_DUMP>`,
	};
}
async function triageWithModel(dump, llmCall) {
	const prompt = buildTriagePrompt(dump.slice(0, MAX_DUMP_CHARS));
	const response = await llmCall(prompt.system, prompt.user);
	return parseTodoTriageResponse(response);
}
function chooseTodoTriageModel(ctx) {
	try {
		const available = ctx.modelRegistry?.getAvailable?.() ?? [];
		for (const pattern of PREFERRED_TRIAGE_MODEL_PATTERNS) {
			const match = available.find((model) => {
				return (
					pattern.test(`${model.provider}/${model.id}`) ||
					pattern.test(model.name ?? "")
				);
			});
			if (match) return match;
		}
		return ctx.model ?? available[0] ?? null;
	} catch {
		return ctx.model ?? null;
	}
}
export function buildTodoTriageLLMCall(ctx) {
	const model = chooseTodoTriageModel(ctx);
	if (!model) return null;
	const resolvedKeyPromise = ctx.modelRegistry
		?.getApiKey?.(model)
		.catch(() => undefined);
	return async (system, user) => {
		const { completeSimple } = await import("@singularity-forge/ai");
		const resolvedApiKey = await resolvedKeyPromise;
		const result = await completeSimple(
			model,
			{
				systemPrompt: system,
				messages: [
					{
						role: "user",
						content: [{ type: "text", text: user }],
						timestamp: Date.now(),
					},
				],
			},
			{
				maxTokens: 4096,
				temperature: 0,
				...(resolvedApiKey ? { apiKey: resolvedApiKey } : {}),
			},
		);
		return result.content
			.filter((part) => part.type === "text")
			.map((part) => part.text)
			.join("");
	};
}
function computeHash(content) {
	return createHash("sha256").update(content).digest("hex").slice(0, 16);
}
function lastHashPath(basePath) {
	return join(sfRoot(basePath), "triage", ".last-hash");
}
function readLastHash(basePath) {
	const path = lastHashPath(basePath);
	if (!existsSync(path)) return null;
	try {
		return readFileSync(path, "utf-8").trim();
	} catch {
		return null;
	}
}
function writeLastHash(basePath, hash) {
	const path = lastHashPath(basePath);
	mkdirSync(dirname(path), { recursive: true });
	writeFileSync(path, hash, "utf-8");
}
function deterministicSuffix() {
	const envSuffix = process.env.GITHUB_SHA || process.env.SF_TRIAGE_SUFFIX;
	if (envSuffix) return envSuffix.slice(0, 16);
	return timestampId();
}
export function validateJsonlFile(path, schemaName) {
	if (!existsSync(path)) return { ok: true };
	const content = readFileSync(path, "utf-8");
	const lines = content.split("\n").filter((line) => line.trim());
	for (let i = 0; i < lines.length; i++) {
		try {
			JSON.parse(lines[i]);
		} catch (err) {
			return {
				ok: false,
				error: `${schemaName} line ${i + 1}: ${getErrorMessage(err)}`,
			};
		}
	}
	return { ok: true };
}
export async function triageTodoDump(basePath, llmCall, options = {}) {
	const todoPath = join(basePath, "TODO.md");
	if (!existsSync(todoPath)) {
		throw new Error("No root TODO.md found.");
	}
	const raw = readFileSync(todoPath, "utf-8");
	const dump = extractTodoDump(raw);
	if (!dump) {
		throw new Error("TODO.md has no dump content to triage.");
	}
	// CI mode: force no-clear + backlog
	const clear = options.ci ? false : options.clear;
	const backlog = options.ci ? true : options.backlog;
	// Hash-based idempotency check in CI mode
	if (options.ci) {
		const currentHash = computeHash(raw);
		const lastHash = readLastHash(basePath);
		if (lastHash === currentHash) {
			return {
				markdownPath: "",
				evalJsonlPath: "",
				normalizedJsonlPath: "",
				skillJsonlPath: "",
				backlogItemsAdded: 0,
				result: {
					summary: "TODO.md unchanged since last triage — skipping.",
					eval_candidates: [],
					implementation_tasks: [],
					memory_requirements: [],
					harness_suggestions: [],
					docs_or_tests: [],
					unclear_notes: [],
				},
				skipped: true,
			};
		}
	}
	const result = await triageWithModel(dump, llmCall);
	const id = options.ci ? deterministicSuffix() : timestampId(options.date);
	const createdAt = (options.date ?? new Date()).toISOString();
	const triageRoot = join(basePath, ".sf", "triage");
	const reportsDir = join(triageRoot, "reports");
	mkdirSync(reportsDir, { recursive: true });
	const markdownPath = join(reportsDir, `${id}.md`);
	writeFileSync(markdownPath, renderTriageMarkdown(result, "TODO.md"));
	// Write triage results to DB (replaces JSONL files)
	const dbRoot = sfRoot(basePath);
	mkdirSync(dbRoot, { recursive: true });
	openDatabase(join(dbRoot, "sf.db"));
	insertTriageRun(id, join(basePath, "TODO.md"), createdAt);
	for (const item of result.eval_candidates) {
		insertTriageEval(crypto.randomUUID(), id, item, createdAt);
	}
	for (const item of normalizedItems(result, createdAt)) {
		insertTriageItem(
			crypto.randomUUID(),
			id,
			item.kind,
			item.content,
			item.evidence,
			createdAt,
		);
	}
	const skillProposals = detectRecurringPatterns(result);
	for (const skill of skillProposals) {
		insertTriageSkill(crypto.randomUUID(), id, skill, createdAt);
	}
	const backlogItemsAdded =
		backlog === true
			? appendBacklogItems(basePath, result.implementation_tasks, id)
			: 0;
	if (clear !== false) {
		rmSync(todoPath, { force: true });
	}
	// Update hash after successful triage in CI mode
	if (options.ci) {
		writeLastHash(basePath, computeHash(raw));
	}
	return {
		markdownPath,
		evalJsonlPath: null,
		normalizedJsonlPath: null,
		skillJsonlPath: null,
		backlogItemsAdded,
		result,
		skipped: false,
	};
}
export async function handleTodo(args, ctx, _pi) {
	const parts = args.trim().split(/\s+/).filter(Boolean);
	const subcommand = parts[0] || "triage";
	const clear = !parts.includes("--no-clear");
	const backlog = parts.includes("--backlog");
	const ci = parts.includes("--ci");
	if (subcommand !== "triage") {
		ctx.ui.notify(
			"Usage: /todo triage [--no-clear] [--backlog] [--ci]\nReads root TODO.md, writes .sf/triage artifacts, and clears processed dump notes by default.",
			"warning",
		);
		return;
	}
	// Check for empty/inbox-template-only TODO.md before wasting an LLM call
	const todoPath = join(projectRoot(), "TODO.md");
	if (existsSync(todoPath)) {
		const raw = readFileSync(todoPath, "utf-8");
		const dump = extractTodoDump(raw);
		if (!dump) {
			if (!ci) {
				rmSync(todoPath, { force: true });
				ctx.ui.notify("TODO.md was empty — removed.", "info");
			} else {
				ctx.ui.notify(
					"TODO.md was empty — nothing to triage in CI mode.",
					"info",
				);
			}
			return;
		}
	}
	const llmCall = buildTodoTriageLLMCall(ctx);
	if (!llmCall) {
		ctx.ui.notify("No model available for TODO triage.", "warning");
		return;
	}
	try {
		const output = await triageTodoDump(projectRoot(), llmCall, {
			clear,
			backlog,
			ci,
		});
		if (output.skipped) {
			ctx.ui.notify(
				"TODO.md unchanged since last triage — skipping LLM call.",
				"info",
			);
			return;
		}
		ctx.ui.notify(
			[
				"TODO triage complete.",
				`Report: ${output.markdownPath}`,
				`Normalized inbox: ${output.normalizedJsonlPath}`,
				`Eval candidates: ${output.evalJsonlPath}`,
				`Skill proposals: ${output.skillJsonlPath}`,
				`Eval candidate count: ${output.result.eval_candidates.length}`,
				`Backlog items added: ${output.backlogItemsAdded}`,
				clear
					? "TODO.md was reset to the empty dump inbox."
					: "TODO.md was left unchanged.",
			].join("\n"),
			"info",
		);
	} catch (err) {
		ctx.ui.notify(`TODO triage failed: ${getErrorMessage(err)}`, "warning");
	}
}
