/**
 * headless-triage.ts — `sf headless triage`
 *
 * Purpose: operator-driven entry point for self-feedback triage. Lists open
 * forge-local self-feedback entries (sorted by impact ↓ effort ↑ ts ↑) and
 * either:
 *   - emits the canonical triage prompt (default): the same one the
 *     bootstrap session_start drain queues as a followUp, but rendered
 *     synchronously to stdout so operators can pipe it into any model
 *     (`sf headless triage | sf headless -p -`, or any external assistant)
 *     without depending on the autonomous-loop turn semantics that
 *     swallow the followUp when no other unit is dispatchable.
 *   - --list: human-readable candidate digest, no prompt — for scanning
 *     the queue at a glance.
 *   - --json: structured candidate list for tooling.
 *
 * Why this command exists (sf-mp4rxkwb-l4baga): the inline-fix worker
 * currently delivers via `triggerTurn:true, deliverAs:"followUp"`. When
 * autonomous mode bails at milestone validation before any turn runs,
 * the followUp never lands and the queue stays unprocessed. This command
 * gives operators a deterministic path to drain the queue today, ahead of
 * the larger refactor that promotes triage to a real SF unit type.
 *
 * Consumer: headless.ts when command === "triage".
 */

import { existsSync } from "node:fs";
import { join } from "node:path";
import { createJiti } from "@mariozechner/jiti";
import { resolveBundledSourceResource } from "./bundled-resource-path.js";
import { getSfEnv } from "./env.js";

const jiti = createJiti(import.meta.filename, {
	interopDefault: true,
	debug: false,
});

const agentExtensionsDir = join(getSfEnv().agentDir, "extensions", "sf");
const useAgentDir = existsSync(join(agentExtensionsDir, "state.js"));

function sfExtensionPath(moduleName: string): string {
	if (useAgentDir) return join(agentExtensionsDir, `${moduleName}.js`);
	const tsPath = resolveBundledSourceResource(
		import.meta.url,
		"extensions",
		"sf",
		`${moduleName}.ts`,
	);
	if (existsSync(tsPath)) return tsPath;
	return resolveBundledSourceResource(
		import.meta.url,
		"extensions",
		"sf",
		`${moduleName}.js`,
	);
}

export interface HandleTriageOptions {
	json?: boolean;
	list?: boolean;
	max?: number;
	run?: boolean;
	model?: string;
}

export interface HandleTriageResult {
	exitCode: number;
}

interface TriageCandidate {
	id: string;
	kind: string;
	severity: string;
	summary: string;
	ts: string;
	impactScore?: number;
	effortEstimate?: number;
}

/**
 * Render the triage queue or canonical triage prompt to stdout.
 *
 * Never throws — best-effort, returns non-zero exit on assembly failure.
 */
export async function handleTriage(
	cwd: string,
	options: HandleTriageOptions = {},
): Promise<HandleTriageResult> {
	// Open the project DB before reading. The one-shot bypass path doesn't
	// run the full SF agent bootstrap, so DB-open isn't done for us.
	try {
		const autoStartModule = (await jiti.import(
			sfExtensionPath("auto-start"),
			{},
		)) as { openProjectDbIfPresent?: (cwd: string) => Promise<unknown> };
		if (typeof autoStartModule.openProjectDbIfPresent === "function") {
			await autoStartModule.openProjectDbIfPresent(cwd);
		}
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		process.stderr.write(`[triage] DB pre-open warning: ${msg}\n`);
	}

	let drainModule: {
		selectInlineFixCandidates: (basePath: string) => TriageCandidate[];
		buildInlineFixPrompt: (entries: TriageCandidate[]) => string;
		runTriage: (
			prompt: string,
			options?: { model?: string; timeoutMs?: number },
		) => Promise<{
			ok: boolean;
			content?: string;
			error?: string;
			cleanFinish?: boolean;
			provider?: string;
			modelId?: string;
		}>;
		writeTriageDecisionReport: (
			basePath: string,
			content: string,
		) => string | null;
	};
	try {
		drainModule = (await jiti.import(
			sfExtensionPath("self-feedback-drain"),
		)) as typeof drainModule;
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		process.stderr.write(
			`[triage] failed to load self-feedback-drain module: ${msg}\n`,
		);
		return { exitCode: 1 };
	}

	let candidates: TriageCandidate[];
	try {
		candidates = drainModule.selectInlineFixCandidates(cwd);
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		process.stderr.write(`[triage] candidate selection failed: ${msg}\n`);
		return { exitCode: 1 };
	}

	if (typeof options.max === "number" && options.max > 0) {
		candidates = candidates.slice(0, options.max);
	}

	if (candidates.length === 0) {
		if (options.json) {
			process.stdout.write(
				`${JSON.stringify({ ok: true, candidates: [] })}\n`,
			);
		} else {
			process.stdout.write("No open self-feedback candidates to triage.\n");
		}
		return { exitCode: 0 };
	}

	// --run takes precedence over --json/--list because they describe the
	// OUTPUT FORMAT, not the action. With --run, --json controls whether
	// the run-result is JSON vs. human text. Without --run, --json emits
	// the candidate digest as JSON (the inspect path).
	if (!options.run) {
		if (options.json) {
			process.stdout.write(
				`${JSON.stringify({
					ok: true,
					count: candidates.length,
					candidates: candidates.map((c) => ({
						id: c.id,
						kind: c.kind,
						severity: c.severity,
						summary: c.summary,
						ts: c.ts,
						impact: c.impactScore ?? null,
						effort: c.effortEstimate ?? null,
					})),
				})}\n`,
			);
			return { exitCode: 0 };
		}

		if (options.list) {
			process.stdout.write(
				`${candidates.length} candidate${candidates.length === 1 ? "" : "s"} (priority: impact↓ effort↑ ts↑)\n\n`,
			);
			for (const c of candidates) {
				const impact = c.impactScore != null ? `i${c.impactScore}` : "i?";
				const effort =
					c.effortEstimate != null ? `e${c.effortEstimate}` : "e?";
				process.stdout.write(
					`  [${c.severity}] ${impact} ${effort}  ${c.id}  ${c.kind}\n`,
				);
				process.stdout.write(`    ${c.summary}\n`);
			}
			return { exitCode: 0 };
		}
	}

	// Render the canonical triage prompt (used by both the default
	// pipe-to-model output and the --run dispatch path below).
	let prompt: string;
	try {
		prompt = drainModule.buildInlineFixPrompt(candidates);
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		process.stderr.write(`[triage] prompt render failed: ${msg}\n`);
		return { exitCode: 1 };
	}

	if (!options.run) {
		process.stdout.write(`${prompt}\n`);
		return { exitCode: 0 };
	}

	// --run: dispatch the prompt via @singularity-forge/ai completeSimple,
	// capture the decision text, persist to .sf/triage/decisions/<ts>.md.
	// Same shape as `sf headless reflect --run`. The model's output is a
	// decision matrix — applying the decisions (resolve_issue calls, code
	// edits) is operator-driven; a tool-enabled variant is follow-up work.
	process.stderr.write(
		"[triage] dispatching to model (this can take a few minutes)…\n",
	);
	const result = await drainModule.runTriage(prompt, { model: options.model });
	if (!result.ok) {
		const payload = {
			ok: false,
			error: result.error ?? "unknown triage error",
			provider: result.provider,
			modelId: result.modelId,
		};
		process.stdout.write(
			options.json
				? `${JSON.stringify(payload)}\n`
				: `[triage] failed: ${payload.error}\n`,
		);
		return { exitCode: 1 };
	}
	const reportPath = drainModule.writeTriageDecisionReport(
		cwd,
		result.content ?? "",
	);
	const payload = {
		ok: true,
		reportPath,
		cleanFinish: result.cleanFinish === true,
		provider: result.provider,
		modelId: result.modelId,
	};
	if (options.json) {
		process.stdout.write(`${JSON.stringify(payload)}\n`);
	} else {
		process.stdout.write(`Triage decisions written to: ${reportPath}\n`);
		if (!result.cleanFinish) {
			process.stderr.write(
				'[triage] WARNING: report did not include "Self-feedback triage complete" terminator — output may be truncated\n',
			);
		}
	}
	return { exitCode: 0 };
}
