/**
 * Headless commands for marking SF state out-of-band:
 *
 *   sf headless complete-slice    <M>/<S> [--reason "..."]
 *   sf headless skip-slice        <M>/<S> [--reason "..."]
 *   sf headless complete-milestone <M>     [--reason "..."]
 *
 * Why these exist: there are several legitimate cases where an
 * operator needs to flip slice or milestone status without going
 * through the autonomous flow:
 *
 *   - Migration: pulling a planning doc into the DB-first format
 *     leaves stale placeholder slices that wedge the active-slice
 *     resolver. Without a CLI you'd drop to sqlite3 / python.
 *   - Recovery: an autonomous run dies between writing the
 *     filesystem evidence and flipping the DB row.
 *   - Recording out-of-band work: code shipped through a parallel
 *     channel (manual dev, paired session) and the slice needs to
 *     reflect that without re-running execute-slice.
 *
 * All three commands bypass the RPC child for the same reason
 * `headless query` does — they're deterministic DB operations that
 * shouldn't pay the cost of spinning up an agent. They are
 * idempotent: a slice already in the target status returns ok
 * with a note rather than failing.
 *
 * Refuses cross-milestone-id ambiguity. Refuses unknown ids
 * outright instead of silently no-op'ing (which would be a
 * dangerous-looking 0-row UPDATE).
 */

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

export interface MarkStateResult {
	exitCode: number;
}

interface MarkStateOptions {
	command: "complete-slice" | "skip-slice" | "complete-milestone";
	args: string[];
	json: boolean;
}

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

function parseRef(ref: string): {
	milestoneId: string;
	sliceId: string | null;
} {
	const trimmed = ref.trim();
	const slash = trimmed.indexOf("/");
	if (slash < 0) return { milestoneId: trimmed, sliceId: null };
	return {
		milestoneId: trimmed.slice(0, slash),
		sliceId: trimmed.slice(slash + 1),
	};
}

function extractReason(args: string[]): string {
	const idx = args.indexOf("--reason");
	if (idx >= 0 && idx + 1 < args.length) {
		return args[idx + 1] ?? "";
	}
	return "";
}

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

export async function handleMarkState(
	basePath: string,
	options: MarkStateOptions,
): Promise<MarkStateResult> {
	// First positional arg is the milestone/slice ref. The dispatcher in
	// headless.ts has already filtered the flags out of args, but we still
	// have to skip --reason and its value here.
	const positional = options.args.filter((arg, i, all) => {
		if (arg === "--reason") return false;
		if (i > 0 && all[i - 1] === "--reason") return false;
		return !arg.startsWith("--");
	});
	if (positional.length === 0) {
		process.stderr.write(
			`[headless] Error: ${options.command} requires a reference (e.g. M010/S03)\n`,
		);
		return { exitCode: 2 };
	}
	const reason = extractReason(options.args);
	const ref = parseRef(positional[0]);

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

	const slicesModule = (await jiti.import(
		sfExtensionPath("sf-db/sf-db-slices"),
		{},
	)) as {
		getSlice: (mid: string, sid: string) => { status: string } | null;
		updateSliceStatus: (
			mid: string,
			sid: string,
			status: string,
			completedAt: string | null,
		) => void;
		setSliceSummaryMd?: (
			mid: string,
			sid: string,
			summaryMd: string,
			uatMd: string,
		) => void;
	};
	const milestonesModule = (await jiti.import(
		sfExtensionPath("sf-db/sf-db-milestones"),
		{},
	)) as {
		getMilestone: (id: string) => { status: string } | null;
		updateMilestoneStatus: (
			id: string,
			status: string,
			completedAt: string | null,
		) => void;
	};

	const now = new Date().toISOString();

	if (options.command === "complete-milestone") {
		if (ref.sliceId !== null) {
			process.stderr.write(
				`[headless] Error: complete-milestone takes a milestone id, not <M>/<S>\n`,
			);
			return { exitCode: 2 };
		}
		const m = milestonesModule.getMilestone(ref.milestoneId);
		if (!m) {
			process.stderr.write(
				`[headless] Error: milestone ${ref.milestoneId} not found\n`,
			);
			return { exitCode: 1 };
		}
		if (m.status === "complete") {
			emit(
				process.stdout,
				options.json,
				{
					ok: true,
					idempotent: true,
					milestone_id: ref.milestoneId,
					status: m.status,
				},
				`${ref.milestoneId} already complete`,
			);
			return { exitCode: 0 };
		}
		milestonesModule.updateMilestoneStatus(ref.milestoneId, "complete", now);
		emit(
			process.stdout,
			options.json,
			{
				ok: true,
				milestone_id: ref.milestoneId,
				previous_status: m.status,
				status: "complete",
				completed_at: now,
				reason,
			},
			`${ref.milestoneId}: ${m.status} → complete`,
		);
		return { exitCode: 0 };
	}

	// complete-slice / skip-slice
	if (ref.sliceId === null) {
		process.stderr.write(
			`[headless] Error: ${options.command} requires <milestone>/<slice> (e.g. M010/S03)\n`,
		);
		return { exitCode: 2 };
	}
	const slice = slicesModule.getSlice(ref.milestoneId, ref.sliceId);
	if (!slice) {
		process.stderr.write(
			`[headless] Error: slice ${ref.milestoneId}/${ref.sliceId} not found\n`,
		);
		return { exitCode: 1 };
	}
	const targetStatus =
		options.command === "complete-slice" ? "complete" : "skipped";
	if (slice.status === targetStatus) {
		emit(
			process.stdout,
			options.json,
			{
				ok: true,
				idempotent: true,
				milestone_id: ref.milestoneId,
				slice_id: ref.sliceId,
				status: slice.status,
			},
			`${ref.milestoneId}/${ref.sliceId} already ${targetStatus}`,
		);
		return { exitCode: 0 };
	}
	slicesModule.updateSliceStatus(
		ref.milestoneId,
		ref.sliceId,
		targetStatus,
		now,
	);
	emit(
		process.stdout,
		options.json,
		{
			ok: true,
			milestone_id: ref.milestoneId,
			slice_id: ref.sliceId,
			previous_status: slice.status,
			status: targetStatus,
			completed_at: now,
			reason,
		},
		`${ref.milestoneId}/${ref.sliceId}: ${slice.status} → ${targetStatus}`,
	);
	return { exitCode: 0 };
}
