/**
 * Headless commands for the self-feedback subsystem:
 *
 *   sf headless feedback add     [flags]
 *   sf headless feedback list    [--unresolved] [--severity <s>] [--json]
 *   sf headless feedback resolve <id> --reason "..." [--evidence-kind <k>]
 *
 * Why these exist: today the only writers to the self_feedback table
 * are SF's autonomous runtime and a handful of internal detectors.
 * Operators (and Claude Code instances running outside the autonomous
 * loop) have no path to file feedback without dropping to SQL.
 *
 * Same pattern as headless-mark-state.ts: bypass the RPC child, use
 * the established sf-db primitives, idempotent on conflict.
 *
 * resolve uses the same evidence shape the autonomous triage flow
 * already accepts (`{kind: "human-clear" | "agent-fix" | ...}`), so
 * resolution semantics are consistent across operator and agent paths.
 */

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";

export interface FeedbackResult {
	exitCode: number;
}

interface FeedbackOptions {
	subcommand: "add" | "list" | "resolve";
	args: string[];
	json: boolean;
}

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`,
	);
}

// Match self-feedback.js's newId() so ids look uniform across writers.
function newId(): string {
	const ts = Date.now().toString(36);
	const rnd = Math.random().toString(36).slice(2, 8);
	return `sf-${ts}-${rnd}`;
}

function readFlag(args: string[], name: string): string | undefined {
	const i = args.indexOf(name);
	if (i < 0 || i + 1 >= args.length) return undefined;
	return args[i + 1];
}

function readBoolFlag(args: string[], name: string): boolean {
	return args.includes(name);
}

function parseIntOrUndefined(s: string | undefined): number | undefined {
	if (s === undefined) return undefined;
	const n = Number.parseInt(s, 10);
	return Number.isFinite(n) ? n : undefined;
}

const VALID_SEVERITIES = new Set(["low", "medium", "high", "critical"]);
const DEFAULT_KIND_DOMAIN = "improvement-idea";

function emit(
	json: boolean,
	payload: Record<string, unknown>,
	human: string,
): void {
	if (json) {
		process.stdout.write(`${JSON.stringify(payload)}\n`);
	} else {
		process.stdout.write(`${human}\n`);
	}
}

async function loadDb(basePath: string): Promise<void> {
	const autoStart = (await jiti.import(sfExtensionPath("auto-start"), {})) as {
		openProjectDbIfPresent: (basePath: string) => Promise<void>;
	};
	await autoStart.openProjectDbIfPresent(basePath);
}

async function handleAdd(
	basePath: string,
	options: FeedbackOptions,
): Promise<FeedbackResult> {
	const summary = readFlag(options.args, "--summary");
	if (!summary || summary.trim() === "") {
		process.stderr.write(
			"[headless] Error: feedback add requires --summary <text>\n",
		);
		return { exitCode: 2 };
	}
	const severity = readFlag(options.args, "--severity") ?? "medium";
	if (!VALID_SEVERITIES.has(severity)) {
		process.stderr.write(
			`[headless] Error: --severity must be one of: low, medium, high, critical (got '${severity}')\n`,
		);
		return { exitCode: 2 };
	}
	const kind = readFlag(options.args, "--kind") ?? DEFAULT_KIND_DOMAIN;
	const evidence = readFlag(options.args, "--evidence") ?? "";
	const suggestedFix = readFlag(options.args, "--suggested-fix") ?? "";
	const milestone = readFlag(options.args, "--milestone");
	const slice = readFlag(options.args, "--slice");
	const task = readFlag(options.args, "--task");
	const unitType = readFlag(options.args, "--unit-type");
	const impactScore = parseIntOrUndefined(
		readFlag(options.args, "--impact-score"),
	);
	const effortEstimate = parseIntOrUndefined(
		readFlag(options.args, "--effort-estimate"),
	);
	// purpose_anchor (sf-db v71, ADR-0000): free-text fragment of the milestone
	// vision or slice goal sentence this feedback is filed against. Optional —
	// the CLI accepts omission so legacy callers keep working, but triage can
	// only prioritize against purpose for rows that supply it.
	const purposeAnchorRaw = readFlag(options.args, "--purpose");
	const purposeAnchor =
		typeof purposeAnchorRaw === "string" && purposeAnchorRaw.trim() !== ""
			? purposeAnchorRaw.trim()
			: undefined;
	const blocking =
		readBoolFlag(options.args, "--blocking") ||
		severity === "high" ||
		severity === "critical";

	const id = newId();
	const ts = new Date().toISOString();
	// Detect forge-vs-external repo via the canonical helper used by
	// recordSelfFeedback; this affects which channel the entry is read from.
	// Previously hardcoded "external", which incorrectly tagged forge-repo
	// entries — combined with sfVersion="" below, this fed the
	// auto-version-bump resolver false positives that swallowed legitimate
	// operator-direction within seconds of filing (R124 incident 2026-05-17).
	const sfHelpers = (await jiti.import(sfExtensionPath("self-feedback"), {})) as {
		isForgeRepo?: (basePath: string) => boolean;
		getCurrentSfVersion?: () => string;
	};
	const repoIdentity: "forge" | "external" = sfHelpers.isForgeRepo?.(basePath)
		? "forge"
		: "external";
	// sfVersion must reflect the SF version that filed the entry. An empty
	// string sorts BELOW any real semver — so triageBlockedEntries would
	// retry-close every empty-version entry as "version-bumped" (R124
	// incident 2026-05-17). Use the canonical helper + env fallback.
	const sfVersion =
		sfHelpers.getCurrentSfVersion?.() ??
		process.env.SF_VERSION ??
		"unknown";
	const entry = {
		id,
		ts,
		kind,
		severity,
		blocking,
		repoIdentity,
		sfVersion,
		basePath,
		occurredIn: {
			unitType: unitType ?? null,
			milestone: milestone ?? null,
			slice: slice ?? null,
			task: task ?? null,
		},
		summary: summary.trim(),
		evidence,
		suggestedFix,
		impactScore,
		effortEstimate,
		purposeAnchor,
		source: "headless-cli",
	};

	await loadDb(basePath);
	const sfDb = (await jiti.import(
		sfExtensionPath("sf-db/sf-db-self-feedback"),
		{},
	)) as {
		insertSelfFeedbackEntry: (e: typeof entry) => void;
	};
	sfDb.insertSelfFeedbackEntry(entry);

	emit(
		options.json,
		{
			ok: true,
			id,
			ts,
			kind,
			severity,
			blocking,
			impact_score: impactScore,
			purpose_anchor: purposeAnchor ?? null,
			summary: entry.summary,
		},
		`${id} ${severity.padEnd(8)} ${kind}  ${entry.summary}`,
	);
	return { exitCode: 0 };
}

async function handleList(
	basePath: string,
	options: FeedbackOptions,
): Promise<FeedbackResult> {
	const wantUnresolved = readBoolFlag(options.args, "--unresolved");
	const severityFilter = readFlag(options.args, "--severity");
	if (severityFilter && !VALID_SEVERITIES.has(severityFilter)) {
		process.stderr.write(
			`[headless] Error: --severity must be one of: low, medium, high, critical (got '${severityFilter}')\n`,
		);
		return { exitCode: 2 };
	}
	// --purpose <fragment>: filter to rows whose purpose_anchor contains the
	// fragment. Pushed into the DB query (LIKE %fragment%) so triage doesn't
	// pull the full table just to scope by purpose (ADR-0000, v71).
	const purposeFragmentRaw = readFlag(options.args, "--purpose");
	const purposeFragment =
		typeof purposeFragmentRaw === "string" && purposeFragmentRaw.trim() !== ""
			? purposeFragmentRaw.trim()
			: undefined;

	await loadDb(basePath);
	const sfDb = (await jiti.import(
		sfExtensionPath("sf-db/sf-db-self-feedback"),
		{},
	)) as {
		listSelfFeedbackEntries: (options?: { purpose?: string }) => Array<{
			id: string;
			ts: string;
			kind: string;
			severity: string;
			blocking: boolean;
			summary: string;
			resolvedAt?: string | null;
			impactScore?: number | null;
			purposeAnchor?: string | null;
		}>;
	};
	let entries = sfDb.listSelfFeedbackEntries(
		purposeFragment ? { purpose: purposeFragment } : undefined,
	);
	if (wantUnresolved) {
		entries = entries.filter((e) => !e.resolvedAt);
	}
	if (severityFilter) {
		entries = entries.filter((e) => e.severity === severityFilter);
	}

	if (options.json) {
		process.stdout.write(
			`${JSON.stringify({ ok: true, count: entries.length, entries })}\n`,
		);
		return { exitCode: 0 };
	}
	if (entries.length === 0) {
		process.stdout.write("(no self-feedback entries match)\n");
		return { exitCode: 0 };
	}
	for (const e of entries) {
		const state = e.resolvedAt ? "RESOLVED" : "        ";
		process.stdout.write(
			`${e.id}  ${state}  ${e.severity.padEnd(8)} ${e.kind.padEnd(24)} ${e.summary}\n`,
		);
	}
	return { exitCode: 0 };
}

async function handleResolve(
	basePath: string,
	options: FeedbackOptions,
): Promise<FeedbackResult> {
	const positional = options.args.filter(
		(a, i, all) =>
			!a.startsWith("--") && (i === 0 || !all[i - 1].startsWith("--")),
	);
	const id = positional[0];
	if (!id) {
		process.stderr.write(
			"[headless] Error: feedback resolve requires an id positional (e.g. sf-mp4xxx-yyy)\n",
		);
		return { exitCode: 2 };
	}
	const reason = readFlag(options.args, "--reason") ?? "";
	const evidenceKind =
		readFlag(options.args, "--evidence-kind") ?? "human-clear";

	await loadDb(basePath);
	const sfDb = (await jiti.import(
		sfExtensionPath("sf-db/sf-db-self-feedback"),
		{},
	)) as {
		resolveSelfFeedbackEntry: (
			id: string,
			resolution: {
				reason: string;
				evidence: { kind: string };
				resolvedBySfVersion?: string;
				resolvedAt?: string;
			},
		) => boolean;
	};
	const ok = sfDb.resolveSelfFeedbackEntry(id, {
		reason,
		evidence: { kind: evidenceKind },
		resolvedBySfVersion: "",
	});
	if (!ok) {
		// Either id not found OR already resolved. The DB primitive returns
		// false for both — surface a 1-line note rather than failing hard,
		// since "already resolved" is the idempotent path.
		emit(
			options.json,
			{
				ok: false,
				idempotent: true,
				id,
				note: "no row updated (already resolved, or id not found)",
			},
			`${id}: nothing to resolve (already resolved, or id not found)`,
		);
		return { exitCode: 0 };
	}
	emit(
		options.json,
		{
			ok: true,
			id,
			resolved_at: new Date().toISOString(),
			evidence_kind: evidenceKind,
			reason,
		},
		`${id}: resolved (${evidenceKind})`,
	);
	return { exitCode: 0 };
}

export async function handleFeedback(
	basePath: string,
	options: FeedbackOptions,
): Promise<FeedbackResult> {
	switch (options.subcommand) {
		case "add":
			return handleAdd(basePath, options);
		case "list":
			return handleList(basePath, options);
		case "resolve":
			return handleResolve(basePath, options);
		default:
			process.stderr.write(
				`[headless] Error: feedback subcommand must be add|list|resolve (got '${options.subcommand}')\n`,
			);
			return { exitCode: 2 };
	}
}
