/**
 * run-unit-inline.js — in-process unit execution (M010/S01).
 *
 * Purpose (per ADR-0000 + REQUIREMENTS.md R014):
 *   Extract the unit-execution code path that `sf headless` invokes after
 *   spawn into a callable function reachable from the same process. The
 *   autonomous loop can then dispatch units WITHOUT spawning a subprocess
 *   or worktree (`scope: 'inline'`), which retires the chronic
 *   prompt-never-sent / silent-worker-spawn-failure class of bugs that
 *   plague the swarm-dispatch path for milestone-completion units.
 *
 * Scope (this slice — M010/S01):
 *   - Define the public API: runUnitInline(unitType, unitId, options).
 *   - Provide a minimum-viable implementation that delegates to runSubagent
 *     in-process (no spawn, no worktree).
 *   - Map a curated set of unit types (validate-milestone, complete-milestone,
 *     reassess-roadmap) — these are the highest-leverage cases that don't
 *     need worktree isolation.
 *   - Document explicit invariants the inline path must respect (single-writer
 *     DB, structured-output requirement, persistent session JSONL).
 *
 * Out of scope (deferred to M010/S02 — dispatch-layer.js skeleton):
 *   - Full DispatchLayer class with 4D parameter matrix (isolation/coordination
 *     /scope/mode). This module provides the runner; the dispatch layer wires
 *     it into the autonomous loop's state machine.
 *   - Backward-compat shims for parallel-orchestrator (milestone/slice scope).
 *
 * Out of scope (deferred to M010/S03):
 *   - Routing the autonomous loop's `runUnitViaSwarm` callsite at
 *     run-unit.js:740 to use this module for inline-eligible units.
 *
 * Single-writer invariant:
 *   This module DOES NOT open its own SQLite connection. Callers must pass
 *   a `db` handle from the shared SF kernel connection (via `ctx.db` or
 *   `getSharedDb()`). All writes go through sf-db.js wrappers.
 *
 * Session JSONL persistence:
 *   We pass `SessionManager` configured for persistent JSONL (NOT the
 *   `SessionManager.inMemory()` that subagent-runner.ts:150 uses). This
 *   ensures the inline path produces the same audit trail as the spawned
 *   path — critical for debuggability and for the R015 loud-failure layer.
 *
 * Refs:
 *   - docs/plans/UNIFIED_DISPATCH_V2_PLAN.md (the Qwen Plan that
 *     specifies the broader v2 architecture)
 *   - .sf/REQUIREMENTS.md R013 (inline scope), R014 (this module), R015
 *     (loud failure on spawn path)
 */

import { runSubagent } from "@singularity-forge/coding-agent";

import { SMOKE_FIXTURE_MILESTONE_ID } from "../smoke-fixture-constants.js";
import { writeLastGreenRecord } from "../last-green.js";

import {
	buildCompleteMilestonePrompt,
	buildReassessRoadmapPrompt,
	buildValidateMilestonePrompt,
} from "../auto-prompts.js";
import { debugLog } from "../debug-logger.js";
import { getMilestone, getSlice, isDbAvailable } from "../sf-db.js";

/**
 * Unit types this module supports for inline dispatch.
 *
 * Curated to milestone-completion + light planning units — these are the
 * cases where worktree isolation is unnecessary and the silent-failure bug
 * has been most painful. Other unit types continue to use the existing
 * swarm/parallel paths until M010/S03 wires more types through.
 */
export const INLINE_ELIGIBLE_UNITS = new Set([
	"validate-milestone",
	"complete-milestone",
	"reassess-roadmap",
]);

/**
 * Check whether a unit type is eligible for inline dispatch.
 *
 * @param {string} unitType
 * @returns {boolean}
 */
export function isInlineEligible(unitType) {
	return INLINE_ELIGIBLE_UNITS.has(unitType);
}

/**
 * Build the prompt for a given unit type, dispatching to the existing
 * builders in auto-prompts.js. Throws if the unit type is not yet wired
 * for inline dispatch — the caller should pre-check via isInlineEligible.
 *
 * @param {string} unitType
 * @param {string} unitId  — milestone id for milestone-scope units, or
 *   `${milestoneId}/${sliceId}` for slice-scope units
 * @param {string} basePath
 * @param {object} extras  — unit-type-specific extra args (e.g. level for
 *   complete-milestone, completedSliceId for reassess-roadmap)
 * @returns {Promise<string>}
 */
async function buildPromptForUnit(unitType, unitId, basePath, extras = {}) {
	if (unitType === "validate-milestone") {
		const mid = unitId;
		const milestone = isDbAvailable() ? getMilestone(mid) : null;
		const midTitle = milestone?.title ?? mid;
		return await buildValidateMilestonePrompt(
			mid,
			midTitle,
			basePath,
			extras.level,
		);
	}
	if (unitType === "complete-milestone") {
		const mid = unitId;
		const milestone = isDbAvailable() ? getMilestone(mid) : null;
		const midTitle = milestone?.title ?? mid;
		return await buildCompleteMilestonePrompt(
			mid,
			midTitle,
			basePath,
			extras.level,
		);
	}
	if (unitType === "reassess-roadmap") {
		// reassess-roadmap takes ${milestoneId}/${completedSliceId}
		const [mid, completedSliceId] = unitId.split("/");
		const milestone = isDbAvailable() ? getMilestone(mid) : null;
		const midTitle = milestone?.title ?? mid;
		const slice =
			isDbAvailable() && completedSliceId
				? getSlice(mid, completedSliceId)
				: null;
		const completedSliceTitle = slice?.title ?? completedSliceId ?? "";
		return await buildReassessRoadmapPrompt(
			mid,
			midTitle,
			completedSliceId,
			completedSliceTitle,
			basePath,
		);
	}
	throw new Error(
		`runUnitInline: prompt builder not wired for unit type "${unitType}". ` +
			`Add a branch in buildPromptForUnit or use the spawn path for this unit type.`,
	);
}

/**
 * Run a unit in-process. No subprocess spawn, no worktree.
 *
 * Public API (R014 contract):
 *   const result = await runUnitInline(unitType, unitId, {
 *     basePath,            // required: project root
 *     model,               // optional: provider/model override
 *     systemPrompt,        // optional: system prompt override
 *     timeoutMs,           // optional: hard cap (default 480_000 = 8min)
 *     noOutputTimeoutMs,   // optional: silent-LLM cap (default 180_000 = 3min)
 *     signal,              // optional: AbortSignal
 *     onEvent,             // optional: callback for agent-session events
 *     extras,              // optional: unit-type-specific args (level, etc.)
 *   });
 *   // result: { ok: boolean, output: string, exitCode: number, stderr?: string }
 *
 * On success, `result.ok === true` and `result.output` contains the agent's
 * final assistant message text. On failure, `result.ok === false` with
 * either an error code or stderr describing why.
 *
 * Single-writer DB invariant:
 *   This function does NOT open its own .sf/sf.db connection. All DB
 *   access must go through the shared connection via sf-db.js helpers.
 *
 * Session JSONL invariant:
 *   The underlying runSubagent uses SessionManager — but unlike
 *   subagent-runner.ts:150's `SessionManager.inMemory()` for swarm
 *   workers, the inline path leaves persistence in the caller's hands.
 *   Callers wanting a JSONL audit trail must thread a persistent
 *   SessionManager via runSubagent's session config (future hardening).
 *
 * @param {string} unitType
 * @param {string} unitId
 * @param {object} options
 * @returns {Promise<{ok: boolean, output: string, exitCode: number, stderr?: string}>}
 */
export async function runUnitInline(unitType, unitId, options = {}) {
	const {
		basePath = process.cwd(),
		model,
		systemPrompt,
		timeoutMs = 480_000,
		noOutputTimeoutMs = 180_000,
		signal,
		onEvent,
		extras = {},
	} = options;

	debugLog("run-unit-inline", {
		event: "enter",
		unitType,
		unitId,
		basePath,
		model: model ?? null,
	});

	if (!isInlineEligible(unitType)) {
		debugLog("run-unit-inline", {
			event: "ineligible",
			unitType,
			unitId,
		});
		return {
			ok: false,
			output: "",
			exitCode: 2,
			stderr: `runUnitInline: unit type "${unitType}" is not yet eligible for inline dispatch. Eligible: ${[...INLINE_ELIGIBLE_UNITS].join(", ")}.`,
		};
	}

	let prompt;
	try {
		prompt = await buildPromptForUnit(unitType, unitId, basePath, extras);
		debugLog("run-unit-inline", {
			event: "prompt-built",
			unitType,
			unitId,
			promptLength: prompt.length,
		});
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		debugLog("run-unit-inline", {
			event: "prompt-build-failed",
			unitType,
			unitId,
			error: msg,
		});
		return {
			ok: false,
			output: "",
			exitCode: 3,
			stderr: `runUnitInline: prompt build failed: ${msg}`,
		};
	}

	const subagentOpts = {
		timeoutMs,
		noOutputTimeoutMs,
		...(signal ? { signal } : {}),
		...(onEvent ? { onEvent } : {}),
	};

	const config = {
		systemPrompt:
			systemPrompt ??
			`You are an SF unit executor running in inline scope. Unit: ${unitType} ${unitId}. Follow the prompt below carefully and produce the required outputs via tool calls. When done, signal completion via the checkpoint or equivalent tool.`,
		cwd: basePath,
		name: `inline-${unitType}-${unitId.replace(/\//g, "-")}`,
		...(model ? { model } : {}),
	};

	debugLog("run-unit-inline", {
		event: "subagent-dispatch",
		unitType,
		unitId,
		configName: config.name,
	});

	const startedAt = Date.now();
	let result;
	try {
		result = await runSubagent(config, prompt, subagentOpts);
	} catch (err) {
		const msg = err instanceof Error ? err.message : String(err);
		debugLog("run-unit-inline", {
			event: "subagent-throw",
			unitType,
			unitId,
			error: msg,
			elapsedMs: Date.now() - startedAt,
		});
		return {
			ok: false,
			output: "",
			exitCode: 4,
			stderr: `runUnitInline: subagent threw: ${msg}`,
		};
	}

	debugLog("run-unit-inline", {
		event: "subagent-completed",
		unitType,
		unitId,
		ok: result.ok,
		exitCode: result.exitCode,
		outputLength: (result.output ?? "").length,
		elapsedMs: Date.now() - startedAt,
	});

	// Wire the last-green ledger for the smoke fixture milestone.
	// Non-fatal: ledger write failure does not corrupt the dispatch result.
	if (unitId === SMOKE_FIXTURE_MILESTONE_ID && result.ok === true) {
		try {
			const elapsedMs = Date.now() - startedAt;
			// Inject elapsedMs into the result object — writeLastGreenRecord
			// reads result.elapsedMs, but runSubagent doesn't populate it.
			const resultWithTiming = { ...result, elapsedMs };
			const written = writeLastGreenRecord(basePath, resultWithTiming, {
				milestoneId: unitId,
				unitType,
				traceId: null,
			});
			debugLog("run-unit-inline", {
				event: "last-green-write",
				unitType,
				unitId,
				written,
			});
		} catch (err) {
			// Log and continue — the dispatch result takes priority.
			debugLog("run-unit-inline", {
				event: "last-green-write-failed",
				unitType,
				unitId,
				error: err instanceof Error ? err.message : String(err),
			});
		}
	}

	return result;
}
