/**
 * Print mode (single-shot): Send prompts, output result, exit.
 *
 * Used for:
 * - `pi -p "prompt"` - text output
 * - `pi --mode json "prompt"` - JSON event stream
 */

import { type ChildProcess, spawn } from "node:child_process";
import type { AssistantMessage, ImageContent } from "@singularity-forge/ai";
import type { AgentSession } from "../core/agent-session.js";
import type { ExtensionUIContext } from "../core/extensions/types.js";
import { theme } from "./interactive/theme/theme.js";
import { createDefaultCommandContextActions } from "./shared/command-context-actions.js";

/**
 * Options for print mode.
 */
export interface PrintModeOptions {
	/** Output mode: "text" for final response only, "json" for all events */
	mode: "text" | "json";
	/** Array of additional prompts to send after initialMessage */
	messages?: string[];
	/** First message to send (may contain @file content) */
	initialMessage?: string;
	/** Images to attach to the initial message */
	initialImages?: ImageContent[];
	/** Per-prompt timeout in milliseconds. 0 disables the watchdog. */
	timeoutMs?: number;
}

const DEFAULT_PRINT_TIMEOUT_MS = 55_000;

export class PrintModeTimeoutError extends Error {
	constructor(timeoutMs: number) {
		super(
			`sf --print timed out after ${timeoutMs}ms waiting for the model response. Set SF_PRINT_TIMEOUT_MS=0 to disable or a larger millisecond value to extend.`,
		);
		this.name = "PrintModeTimeoutError";
	}
}

export function resolvePrintModeTimeoutMs(
	env: NodeJS.ProcessEnv = process.env,
): number {
	const raw = env.SF_PRINT_TIMEOUT_MS;
	if (raw === undefined || raw.trim() === "") return DEFAULT_PRINT_TIMEOUT_MS;
	const parsed = Number.parseInt(raw, 10);
	if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_PRINT_TIMEOUT_MS;
	return parsed;
}

export async function promptWithPrintTimeout(
	session: AgentSession,
	message: string,
	options: { images?: ImageContent[]; runExtensionHooks?: boolean } | undefined,
	timeoutMs: number,
): Promise<void> {
	if (timeoutMs <= 0) {
		await session.prompt(message, options);
		return;
	}

	const processWatchdog =
		timeoutMs >= 1_000 ? startPrintModeProcessWatchdog(timeoutMs) : null;
	let timer: ReturnType<typeof setTimeout> | undefined;
	const timeout = new PrintModeTimeoutError(timeoutMs);
	const timeoutPromise = new Promise<never>((_, reject) => {
		timer = setTimeout(() => {
			void session.abort().catch(() => {});
			reject(timeout);
		}, timeoutMs);
	});

	try {
		await Promise.race([session.prompt(message, options), timeoutPromise]);
	} finally {
		if (timer) clearTimeout(timer);
		stopPrintModeProcessWatchdog(processWatchdog);
	}
}

function startPrintModeProcessWatchdog(timeoutMs: number): ChildProcess | null {
	const script = [
		`const timeoutMs = ${JSON.stringify(timeoutMs)};`,
		`const pid = ${JSON.stringify(process.pid)};`,
		"setTimeout(() => {",
		"  console.error('sf --print timed out after ' + timeoutMs + 'ms waiting for the model response. Set SF_PRINT_TIMEOUT_MS=0 to disable or a larger millisecond value to extend.');",
		"  try { process.kill(pid, 'SIGTERM'); } catch {}",
		"}, timeoutMs);",
	].join("\n");
	try {
		const child = spawn(process.execPath, ["-e", script], {
			stdio: ["ignore", "ignore", "inherit"],
			detached: false,
		});
		child.unref();
		return child;
	} catch {
		return null;
	}
}

function stopPrintModeProcessWatchdog(child: ChildProcess | null): void {
	if (!child || child.killed) return;
	try {
		child.kill("SIGTERM");
	} catch {
		/* already exited */
	}
}

/**
 * Run in print (single-shot) mode.
 * Sends prompts to the agent and outputs the result.
 */
export async function runPrintMode(
	session: AgentSession,
	options: PrintModeOptions,
): Promise<void> {
	const { mode, messages = [], initialMessage, initialImages } = options;
	const timeoutMs = options.timeoutMs ?? resolvePrintModeTimeoutMs();
	if (mode === "json") {
		const header = session.sessionManager.getHeader();
		if (header) {
			console.log(JSON.stringify(header));
		}
	}
	// Bind command context so `/todo ...` and other extension slash commands work
	// in print mode, but skip session_start/resources_discover lifecycle work.
	// Those startup hooks are interactive/RPC concerns and have previously blocked
	// `sf -p` before the prompt ran.
	await session.bindExtensions({
		commandContextActions: createDefaultCommandContextActions(session),
		uiContext: createPrintModeUIContext(),
		onError: (err) => {
			console.error(`Extension error (${err.extensionPath}): ${err.error}`);
		},
		runLifecycle: false,
	});
	const liveness = createPrintModeLivenessReporter(mode);

	// Always subscribe to enable session persistence via _handleAgentEvent
	const unsubscribe = session.subscribe((event) => {
		// In JSON mode, output all events
		if (mode === "json") {
			console.log(JSON.stringify(event));
		}
		liveness(event);
	});

	let exitCode = 0;
	let disposed = false;
	const signalCleanupHandlers: Array<() => void> = [];

	const disposeSession = (): void => {
		if (disposed) return;
		disposed = true;
		unsubscribe();
	};

	const registerSignalHandlers = (): void => {
		const signals: NodeJS.Signals[] = ["SIGTERM"];
		if (process.platform !== "win32") signals.push("SIGHUP");
		for (const signal of signals) {
			const handler = () => {
				disposeSession();
				process.exit(signal === "SIGHUP" ? 129 : 143);
			};
			process.on(signal, handler);
			signalCleanupHandlers.push(() => process.off(signal, handler));
		}
	};

	registerSignalHandlers();

	try {
		// Send initial message with attachments
		if (initialMessage) {
			await promptWithPrintTimeout(
				session,
				initialMessage,
				{ images: initialImages, runExtensionHooks: false },
				timeoutMs,
			);
		}

		// Send remaining messages
		for (const message of messages) {
			await promptWithPrintTimeout(
				session,
				message,
				{ runExtensionHooks: false },
				timeoutMs,
			);
		}

		// In text mode, output final response
		if (mode === "text") {
			const state = session.state;
			const lastMessage = state.messages[state.messages.length - 1];

			if (lastMessage?.role === "assistant") {
				const assistantMsg = lastMessage as AssistantMessage;

				// Check for error/aborted
				if (
					assistantMsg.stopReason === "error" ||
					assistantMsg.stopReason === "aborted"
				) {
					console.error(
						assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`,
					);
					exitCode = 1;
				} else {
					// Output text content
					for (const content of assistantMsg.content) {
						if (content.type === "text") {
							console.log(content.text);
						}
					}
				}
			}
		}
	} catch (err) {
		if (err instanceof PrintModeTimeoutError) {
			console.error(err.message);
			exitCode = 124;
		} else {
			throw err;
		}
	} finally {
		// Ensure stdout is fully flushed before returning
		// This prevents race conditions where the process exits before all output is written
		await new Promise<void>((resolve, reject) => {
			process.stdout.write("", (err) => {
				if (err) reject(err);
				else resolve();
			});
		});
		for (const cleanup of signalCleanupHandlers) cleanup();
		disposeSession();
	}

	if (exitCode !== 0) {
		process.exit(exitCode);
	}
}

function createPrintModeUIContext(): ExtensionUIContext {
	return {
		select: async () => undefined,
		confirm: async () => false,
		input: async () => undefined,
		notify: (message: string) => {
			process.stdout.write(message.endsWith("\n") ? message : `${message}\n`);
		},
		onTerminalInput: () => () => {},
		setStatus: () => {},
		setWorkingMessage: () => {},
		setWorkingVisible: () => {},
		setWidget: () => {},
		setFooter: () => {},
		setHeader: () => {},
		setTitle: () => {},
		custom: async () => undefined as never,
		pasteToEditor: () => {},
		setEditorText: () => {},
		getEditorText: () => "",
		editor: async () => undefined,
		setEditorComponent: () => {},
		get theme() {
			return theme;
		},
		getAllThemes: () => [],
		getTheme: () => undefined,
		setTheme: () => ({ success: false, error: "UI not available" }),
		getToolsExpanded: () => false,
		setToolsExpanded: () => {},
	};
}

export function createPrintModeLivenessReporter(
	mode: "text" | "json",
): (event: { type: string; assistantMessageEvent?: { type: string } }) => void {
	if (mode !== "text") return () => {};
	let sawAssistant = false;
	let sawThinking = false;
	let sawText = false;
	return (event) => {
		if (event.type === "message_start" && !sawAssistant) {
			sawAssistant = true;
			process.stderr.write("[forge] model responding...\n");
			return;
		}
		if (event.type !== "message_update") return;
		const streamType = event.assistantMessageEvent?.type;
		if (
			(streamType === "thinking_start" || streamType === "thinking_delta") &&
			!sawThinking
		) {
			sawThinking = true;
			process.stderr.write("[forge] thinking...\n");
			return;
		}
		if (
			(streamType === "text_start" || streamType === "text_delta") &&
			!sawText
		) {
			sawText = true;
			process.stderr.write("[forge] writing response...\n");
		}
	};
}
