/**
 * self-feedback-drain.ts - dispatch high-priority sf self-feedback as repair work.
 *
 * Purpose: high/critical self-feedback should not remain a passive startup
 * warning; it should become an executable repair turn when sf is dogfooding
 * itself.
 *
 * Consumer: session_start hook in bootstrap/register-hooks.ts.
 */
import { execFileSync } from "node:child_process";
import {
	existsSync,
	mkdirSync,
	readFileSync,
	unlinkSync,
	writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { getErrorMessage } from "./error-utils.js";
import { sfRuntimeRoot } from "./paths.js";
import {
	readAllSelfFeedback,
	readUpstreamSelfFeedback,
} from "./self-feedback.js";

const CLAIM_TTL_MS = 30 * 60 * 1000;
function claimPath(basePath) {
	return join(
		sfRuntimeRoot(basePath),
		"runtime",
		"self-feedback-inline-fix.json",
	);
}
function readClaim(basePath) {
	try {
		const path = claimPath(basePath);
		if (!existsSync(path)) return null;
		return JSON.parse(readFileSync(path, "utf-8"));
	} catch {
		return null;
	}
}
function writeClaim(basePath, ids) {
	const path = claimPath(basePath);
	mkdirSync(dirname(path), { recursive: true });
	writeFileSync(
		path,
		JSON.stringify({ ids, dispatchedAt: new Date().toISOString() }, null, 2),
		"utf-8",
	);
}
function writeFailedClaim(basePath, ids, error) {
	const path = claimPath(basePath);
	mkdirSync(dirname(path), { recursive: true });
	writeFileSync(
		path,
		JSON.stringify(
			{
				ids,
				dispatchedAt: new Date(Date.now() - CLAIM_TTL_MS - 1).toISOString(),
				lastDispatchError: error,
			},
			null,
			2,
		),
		"utf-8",
	);
}
function clearClaim(basePath) {
	try {
		unlinkSync(claimPath(basePath));
	} catch {
		/* non-fatal */
	}
}
function sameIds(a, b) {
	return a.length === b.length && a.every((id, idx) => id === b[idx]);
}
function claimStillFresh(claim, ids) {
	if (!sameIds(claim.ids, ids)) return false;
	const age = Date.now() - new Date(claim.dispatchedAt).getTime();
	return Number.isFinite(age) && age >= 0 && age < CLAIM_TTL_MS;
}
function isForgeRepo(basePath) {
	try {
		const pkg = JSON.parse(
			readFileSync(join(basePath, "package.json"), "utf-8"),
		);
		return pkg?.name === "singularity-forge";
	} catch {
		return false;
	}
}
function hasUncommittedChanges(basePath) {
	try {
		const out = execFileSync("git", ["status", "--porcelain"], {
			cwd: basePath,
			encoding: "utf-8",
			stdio: ["ignore", "pipe", "ignore"],
		});
		return out.trim().length > 0;
	} catch {
		return true;
	}
}
/**
 * Return unresolved high/critical forge-local self-feedback entries.
 *
 * Purpose: isolate the direct-drain candidate policy from the startup hook so
 * tests and future dispatch paths can verify the same selection rule.
 *
 * Consumer: dispatchSelfFeedbackInlineFixIfNeeded during session_start.
 */
export function selectInlineFixCandidates(basePath) {
	if (!isForgeRepo(basePath)) return [];
	return [...readAllSelfFeedback(basePath), ...readUpstreamSelfFeedback()]
		.filter(
			(entry) =>
				!entry.resolvedAt &&
				entry.blocking &&
				(entry.severity === "high" || entry.severity === "critical"),
		)
		.sort((a, b) => a.ts.localeCompare(b.ts));
}
function buildInlineFixPrompt(entries) {
	const rendered = entries
		.map((entry) =>
			[
				`## ${entry.id} — ${entry.kind}`,
				`- Severity: ${entry.severity}`,
				`- Summary: ${entry.summary}`,
				entry.acceptanceCriteria
					? `- Acceptance criteria: ${entry.acceptanceCriteria}`
					: "- Acceptance criteria: verify the reported failure is gone and add/adjust a regression test where practical.",
				entry.evidence
					? `\nEvidence:\n\n\`\`\`\n${entry.evidence}\n\`\`\``
					: "",
				entry.suggestedFix ? `\nSuggested fix: ${entry.suggestedFix}` : "",
			]
				.filter(Boolean)
				.join("\n"),
		)
		.join("\n\n");
	return [
		"You are executing SF self-feedback inline-fix mode.",
		"",
		"These high/critical self-feedback entries are unresolved sf defects. Do not only triage them; repair the current codebase directly.",
		"",
		rendered,
		"",
		"Instructions:",
		"1. Verify each entry still applies before editing.",
		"2. Fix the smallest coherent set of code/docs/tests needed to satisfy the acceptance criteria.",
		"3. Run focused verification and typecheck for touched areas.",
		"4. Commit the fix with a conventional commit message.",
		"5. Call `sf_self_feedback_resolve` for each repaired entry with agent-fix evidence and the commit SHA.",
		"6. If an entry is already fixed, verify it and call `sf_self_feedback_resolve` with the verification evidence.",
		"7. Do not hand-edit `.sf/self-feedback.jsonl` or `.sf/SELF-FEEDBACK.md`; use the resolver tool so the durable self-feedback store, markdown projection, and reload detection stay consistent.",
		"",
		"When done, say: Self-feedback inline fix complete.",
	].join("\n");
}
/**
 * Dispatch a focused inline-fix turn for unresolved high/critical self-feedback.
 *
 * Purpose: convert startup self-feedback warnings into executable work while
 * preventing repeated dispatch of the same candidate set on every session.
 *
 * Consumer: bootstrap/register-hooks.ts session_start drain.
 */
export function dispatchSelfFeedbackInlineFixIfNeeded(basePath, ctx, pi) {
	const candidates = selectInlineFixCandidates(basePath);
	if (candidates.length === 0) return 0;
	const ids = candidates.map((entry) => entry.id);
	const claim = readClaim(basePath);
	if (claim && claimStillFresh(claim, ids)) {
		ctx.ui.notify(
			`Self-feedback inline fix already dispatched for ${ids.length} high/critical entr${ids.length === 1 ? "y" : "ies"}; waiting for completion and clean commit before reload.`,
			"info",
		);
		return 0;
	}
	// Auto-transition to repair work mode when high/critical self-feedback detected
	try {
		const { getAutoSession } = require("../auto/session.js");
		const session = getAutoSession();
		if (session && session.workMode !== "repair") {
			const transition = session.setMode({
				workMode: "repair",
				reason: "self-feedback-drain",
			});
			ctx.ui.notify(
				`Auto-transition: ${transition.from.workMode} → ${transition.to.workMode} (high/critical self-feedback detected)`,
				"warning",
			);
		}
	} catch {
		// Mode transition is best-effort
	}
	writeClaim(basePath, ids);
	const prompt = buildInlineFixPrompt(candidates);
	ctx.ui.notify(
		`Queueing self-feedback inline fix for ${ids.length} high/critical entr${ids.length === 1 ? "y" : "ies"}.`,
		"warning",
	);
	const dispatch = pi.sendMessage(
		{
			customType: "sf-self-feedback-inline-fix",
			content: prompt,
			display: false,
		},
		{ triggerTurn: true, deliverAs: "followUp" },
	);
	void Promise.resolve(dispatch).catch((error) => {
		const message = getErrorMessage(error);
		writeFailedClaim(basePath, ids, message);
		ctx.ui.notify(
			`Self-feedback inline fix dispatch failed; will retry at the next idle point: ${message}`,
			"warning",
		);
	});
	return candidates.length;
}
/**
 * Consume a completed inline-fix claim when all claimed entries are resolved.
 *
 * Purpose: self-patches must become active without the operator manually
 * noticing that files changed; this detects the end of the repair turn and lets
 * the lifecycle hook request a deferred reload.
 *
 * Consumer: register-hooks.ts turn_end handler.
 */
export function consumeCompletedInlineFixClaim(basePath) {
	const claim = readClaim(basePath);
	if (!claim || claim.ids.length === 0) return [];
	const byId = new Map(
		[...readAllSelfFeedback(basePath), ...readUpstreamSelfFeedback()].map(
			(entry) => [entry.id, entry],
		),
	);
	const allResolved = claim.ids.every((id) => {
		const entry = byId.get(id);
		return Boolean(entry?.resolvedAt);
	});
	if (!allResolved) return [];
	if (hasUncommittedChanges(basePath)) return [];
	clearClaim(basePath);
	return claim.ids;
}
