import { readdirSync, readFileSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

import {
	collectRecentLogEvents,
	formatMergedLogEvent,
	getProjectSessionKey,
} from "./cli-logs.js";
import type { QuerySnapshot } from "./headless-query.js";

interface StatusArgs {
	watch: boolean;
	recoveryMode?: boolean;
	recoveryUnitId?: string;
}

interface StatusDeps {
	basePath: string;
	sfHome?: string;
	stdout?: Pick<typeof process.stdout, "write" | "isTTY">;
	stderr?: Pick<typeof process.stderr, "write">;
}

interface CurrentModel {
	provider: string;
	model: string;
}

function parseStatusArgs(argv: string[]): StatusArgs {
	const args = argv.slice(1);
	if (args[0] === "recovery") {
		return {
			watch: false,
			recoveryMode: true,
			recoveryUnitId: args[1],
		};
	}
	return {
		watch: args.includes("--watch"),
	};
}

function formatRef(
	ref: { id: string; title?: string } | string | null | undefined,
): string {
	if (!ref) return "n/a";
	if (typeof ref === "string") return ref;
	return ref.title ? `${ref.id} ${ref.title}` : ref.id;
}

function formatDispatch(snapshot: QuerySnapshot): string {
	const next = snapshot.next;
	if (next.action === "dispatch") {
		return `${next.unitType ?? "unit"} ${next.unitId ?? "n/a"}`;
	}
	if (next.action === "stop")
		return `stop: ${next.reason ?? snapshot.state.nextAction}`;
	return `skip: ${next.reason ?? snapshot.state.nextAction}`;
}

function formatCost(snapshot: QuerySnapshot): string {
	const total = snapshot.cost.total;
	const workers = snapshot.cost.workers.length;
	return `$${total.toFixed(4)}${workers > 0 ? ` (${workers} worker${workers === 1 ? "" : "s"})` : ""}`;
}

function readSolverStatus(basePath: string): string | null {
	let state: Record<string, any>;
	try {
		state = JSON.parse(
			readFileSync(
				join(basePath, ".sf", "runtime", "autonomous-solver", "active.json"),
				"utf-8",
			),
		);
	} catch {
		return null;
	}
	const checkpoint = state.latestCheckpoint ?? {};
	const parts = [
		`${state.unitType ?? "unit"} ${state.unitId ?? "n/a"}`,
		`iter ${state.iteration ?? "?"}/${state.maxIterations ?? "?"}`,
		`outcome ${checkpoint.outcome ?? "none"}`,
	];
	const remaining = Array.isArray(checkpoint.remainingItems)
		? checkpoint.remainingItems.length
		: null;
	if (remaining !== null) parts.push(`${remaining} remaining`);
	if (checkpoint.blockerReason)
		parts.push(`blocker: ${checkpoint.blockerReason}`);
	if (checkpoint.decisionQuestion)
		parts.push(`decision: ${checkpoint.decisionQuestion}`);
	if (checkpoint.summary) parts.push(String(checkpoint.summary));
	return parts.join(" · ");
}

function latestJsonlFile(dir: string): string | null {
	try {
		const entries = readdirSync(dir)
			.filter((file) => file.endsWith(".jsonl"))
			.map((file) => {
				const path = join(dir, file);
				try {
					return { path, mtimeMs: statSync(path).mtimeMs };
				} catch {
					return null;
				}
			})
			.filter(
				(entry): entry is { path: string; mtimeMs: number } => entry !== null,
			);
		entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
		return entries[0]?.path ?? null;
	} catch {
		return null;
	}
}

function extractCurrentModelFromSession(filePath: string): CurrentModel | null {
	let current: CurrentModel | null = null;
	let raw: string;
	try {
		raw = readFileSync(filePath, "utf-8");
	} catch {
		return null;
	}

	for (const line of raw.split(/\n/)) {
		if (!line.trim()) continue;
		let entry: Record<string, unknown>;
		try {
			entry = JSON.parse(line) as Record<string, unknown>;
		} catch {
			continue;
		}

		if (entry.type === "model_change") {
			const provider = String(entry.provider ?? "");
			const model = String(entry.modelId ?? "");
			if (provider || model) current = { provider, model };
			continue;
		}

		if (entry.type === "message") {
			const message = entry.message as Record<string, unknown> | undefined;
			if (message?.role === "assistant") {
				const provider = String(message.provider ?? "");
				const model = String(message.model ?? "");
				if (provider || model) current = { provider, model };
			}
		}
	}

	return current;
}

function getCurrentModel(
	basePath: string,
	sfHome: string,
): CurrentModel | null {
	const key = getProjectSessionKey(basePath);
	const sessionFile =
		latestJsonlFile(join(sfHome, "agent", "sessions", key)) ??
		latestJsonlFile(join(sfHome, "sessions", key));
	return sessionFile ? extractCurrentModelFromSession(sessionFile) : null;
}

function formatModel(model: CurrentModel | null): string {
	if (!model) return "n/a";
	if (model.provider && model.model) return `${model.provider}/${model.model}`;
	return model.model || model.provider || "n/a";
}

export function renderLiveStatus(
	snapshot: QuerySnapshot,
	opts: {
		basePath?: string;
		model: CurrentModel | null;
		recentEvents: string[];
	},
): string {
	const lines: string[] = [];
	lines.push("SF Status");
	lines.push("---------");
	lines.push(
		`Milestone: ${formatRef(snapshot.state.activeMilestone ?? snapshot.state.lastCompletedMilestone)}`,
	);
	lines.push(`Slice:     ${formatRef(snapshot.state.activeSlice)}`);
	lines.push(`Task:      ${formatRef(snapshot.state.activeTask)}`);
	lines.push(`Phase:     ${snapshot.state.phase}`);
	lines.push(`Dispatch:  ${formatDispatch(snapshot)}`);
	lines.push(`Cost:      ${formatCost(snapshot)}`);
	lines.push(`Model:     ${formatModel(opts.model)}`);
	const solverStatus = opts.basePath ? readSolverStatus(opts.basePath) : null;
	if (solverStatus) lines.push(`Solver:    ${solverStatus}`);
	lines.push("");
	lines.push("Last Events:");
	if (opts.recentEvents.length === 0) {
		lines.push("  n/a");
	} else {
		for (const event of opts.recentEvents) {
			lines.push(`  ${event.trimEnd()}`);
		}
	}
	return lines.join("\n") + "\n";
}

async function buildStatusText(
	basePath: string,
	sfHome: string,
): Promise<string> {
	const { buildQuerySnapshot } = await import("./headless-query.js");
	const snapshot = await buildQuerySnapshot(basePath);
	const recentEvents = collectRecentLogEvents({
		basePath,
		sfHome,
		limit: 50,
	})
		.filter((event) => event.source === "notif" || event.source === "activity")
		.slice(-10)
		.map(formatMergedLogEvent);

	return renderLiveStatus(snapshot, {
		basePath,
		model: getCurrentModel(basePath, sfHome),
		recentEvents,
	});
}

type RuntimeSummaryRow = {
	unitType?: unknown;
	unitId?: unknown;
	updatedAt?: unknown;
};

/**
 * Resolve which on-disk runtime record recovery output should describe.
 *
 * Purpose: `.sf/runtime/units/` names files `${unitType}-${unitId}.json`; using a
 * hard-coded unit type misses non-execute-task rows when auto-picking the latest
 * record or when querying by unit id alone.
 *
 * Consumer: sf status recovery.
 */
export function resolveRecoveryPick(
	basePath: string,
	records: RuntimeSummaryRow[],
	explicitUnitId: string | undefined,
	readRecord: (
		root: string,
		unitType: string,
		unitId: string,
	) => unknown | null,
): { unitType: string; unitId: string } | null {
	const valid = records.filter(
		(r): r is { unitType: string; unitId: string; updatedAt?: number } =>
			typeof r.unitType === "string" &&
			r.unitType.length > 0 &&
			typeof r.unitId === "string" &&
			r.unitId.length > 0,
	);
	if (explicitUnitId) {
		const matches = valid
			.filter((r) => r.unitId === explicitUnitId)
			.sort((a, b) => (Number(b.updatedAt) || 0) - (Number(a.updatedAt) || 0));
		if (matches[0]) {
			return {
				unitType: matches[0].unitType,
				unitId: matches[0].unitId,
			};
		}
		if (readRecord(basePath, "execute-task", explicitUnitId)) {
			return { unitType: "execute-task", unitId: explicitUnitId };
		}
		return null;
	}
	if (valid.length === 0) return null;
	const sorted = [...valid].sort(
		(a, b) => (Number(b.updatedAt) || 0) - (Number(a.updatedAt) || 0),
	);
	return { unitType: sorted[0].unitType, unitId: sorted[0].unitId };
}

async function renderRecoveryDiagnostics(
	basePath: string,
	unitId: string | undefined,
	stdout: Pick<typeof process.stdout, "write">,
	stderr: Pick<typeof process.stderr, "write">,
): Promise<number> {
	try {
		const {
			getRecoveryDiagnostics,
			listUnitRuntimeRecords,
			readUnitRuntimeRecord,
		} = await import("./resources/extensions/sf/uok/unit-runtime.js");
		const rows = listUnitRuntimeRecords(basePath);
		const picked = resolveRecoveryPick(
			basePath,
			rows,
			unitId,
			readUnitRuntimeRecord,
		);
		if (!picked) {
			if (rows.length === 0) {
				stderr.write("sf status recovery: no runtime records found\n");
			} else {
				stderr.write(
					unitId
						? `sf status recovery: no runtime record for ${unitId}\n`
						: "sf status recovery: no usable runtime records found\n",
				);
			}
			return 1;
		}
		const diagnostics = getRecoveryDiagnostics(
			basePath,
			picked.unitType,
			picked.unitId,
		);
		if (!diagnostics) {
			stderr.write(
				`sf status recovery: no runtime record for ${picked.unitType} ${picked.unitId}\n`,
			);
			return 1;
		}
		const lines: string[] = [];
		lines.push("Recovery Diagnostics");
		lines.push("--------------------");
		lines.push(`Unit:      ${diagnostics.unitType} ${diagnostics.unitId}`);
		lines.push(`Status:    ${diagnostics.status}`);
		lines.push(
			`Retries:   ${diagnostics.retryCount}/${diagnostics.maxRetries}`,
		);
		lines.push(
			`Progress:  ${diagnostics.progressCount} (${diagnostics.lastProgressKind})`,
		);
		lines.push(`Recovery attempts: ${diagnostics.recoveryAttempts}`);
		if (diagnostics.lastRecoveryReason) {
			lines.push(`Last recovery reason: ${diagnostics.lastRecoveryReason}`);
		}
		if (diagnostics.lineageSummary) {
			lines.push(
				`Lineage:   ${diagnostics.lineageSummary.status} · ${diagnostics.lineageSummary.workerCount} worker(s) · ${diagnostics.lineageSummary.eventCount} event(s)`,
			);
		}
		lines.push(
			`Started:   ${diagnostics.startedAt ? new Date(diagnostics.startedAt).toISOString() : "n/a"}`,
		);
		lines.push(
			`Updated:   ${diagnostics.updatedAt ? new Date(diagnostics.updatedAt).toISOString() : "n/a"}`,
		);
		stdout.write(lines.join("\n") + "\n");
		return 0;
	} catch (err) {
		stderr.write(
			`sf status recovery: ${err instanceof Error ? err.message : String(err)}\n`,
		);
		return 1;
	}
}

export async function runStatusCli(
	argv: string[],
	deps: StatusDeps,
): Promise<number> {
	const stdout = deps.stdout ?? process.stdout;
	const stderr = deps.stderr ?? process.stderr;
	const sfHome = deps.sfHome ?? process.env.SF_HOME ?? join(homedir(), ".sf");
	const args = parseStatusArgs(argv);

	if (args.recoveryMode) {
		return renderRecoveryDiagnostics(
			deps.basePath,
			args.recoveryUnitId,
			stdout,
			stderr,
		);
	}

	const renderOnce = async () => {
		try {
			const text = await buildStatusText(deps.basePath, sfHome);
			if (args.watch && stdout.isTTY) stdout.write("\x1b[2J\x1b[H");
			stdout.write(text);
		} catch (err) {
			stderr.write(
				`sf status: ${err instanceof Error ? err.message : String(err)}\n`,
			);
			throw err;
		}
	};

	if (!args.watch) {
		await renderOnce();
		return 0;
	}

	await renderOnce();
	await new Promise<void>((resolve) => {
		const timer = setInterval(() => {
			renderOnce().catch(() => {
				clearInterval(timer);
				resolve();
			});
		}, 2000);
		const stop = () => {
			clearInterval(timer);
			process.off("SIGINT", stop);
			process.off("SIGTERM", stop);
			resolve();
		};
		process.on("SIGINT", stop);
		process.on("SIGTERM", stop);
	});
	return 0;
}
