/**
 * Headless UI Handling — auto-response, progress formatting, and supervised stdin
 *
 * Handles extension UI requests (auto-responding in headless mode),
 * formats progress events for stderr output, and reads orchestrator
 * commands from stdin in supervised mode.
 */

import type { Readable } from "node:stream";

import {
	attachJsonlLineReader,
	type RpcClient,
} from "@singularity-forge/rpc-client";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface ExtensionUIRequest {
	type: "extension_ui_request";
	id: string;
	method: string;
	title?: string;
	options?: string[];
	message?: string;
	prefill?: string;
	timeout?: number;
	[key: string]: unknown;
}

export type { ExtensionUIRequest };

/** Context passed alongside an event for richer formatting. */
export interface ProgressContext {
	verbose: boolean;
	toolDuration?: number; // ms, for tool_execution_end
	lastCost?: { costUsd: number; inputTokens: number; outputTokens: number };
	thinkingPreview?: string; // accumulated LLM text to show before tool calls
	isError?: boolean; // tool execution ended with an error
}

/** Context for periodic headless heartbeat lines during long quiet waits. */
export interface HeadlessHeartbeatContext {
	elapsedMs: number;
	quietMs: number;
	totalEvents: number;
	toolCallCount: number;
	eventDelta?: number;
	toolCallDelta?: number;
	openToolCount?: number;
	openToolDetails?: string[];
	activeUnit?: string;
	activeModel?: string;
	lastEventType?: string;
	lastEventDetail?: string;
}

export interface AssistantPreviewDelta {
	kind: "text" | "thinking";
	text: string;
}

/**
 * Extract a concise preview delta from an assistant message update.
 *
 * Purpose: keep headless non-verbose output honest by separating assistant text
 * from model thinking before both are flushed at tool starts and message end.
 *
 * Consumer: headless.ts streaming event loop and headless progress tests.
 */
export function extractAssistantPreviewDelta(
	assistantMessageEvent: unknown,
): AssistantPreviewDelta | null {
	if (!assistantMessageEvent || typeof assistantMessageEvent !== "object") {
		return null;
	}
	const event = assistantMessageEvent as Record<string, unknown>;
	const type = String(event.type ?? "");
	if (type !== "text_delta" && type !== "thinking_delta") return null;
	const text = String(event.delta ?? event.text ?? "");
	if (!text) return null;
	return {
		kind: type === "thinking_delta" ? "thinking" : "text",
		text,
	};
}

// ---------------------------------------------------------------------------
// ANSI Color Helpers
// ---------------------------------------------------------------------------

const _c = {
	reset: "\x1b[0m",
	bold: "\x1b[1m",
	dim: "\x1b[2m",
	italic: "\x1b[3m",
	red: "\x1b[31m",
	green: "\x1b[32m",
	yellow: "\x1b[33m",
	cyan: "\x1b[36m",
	gray: "\x1b[90m",
};

/** Build a no-op color map (all codes empty). */
function noColor(): typeof _c {
	const nc: Record<string, string> = {};
	for (const k of Object.keys(_c)) nc[k] = "";
	return nc as typeof _c;
}

const colorsDisabled = !!process.env["NO_COLOR"] || !process.stderr.isTTY;
const c: typeof _c = colorsDisabled ? noColor() : _c;

// ---------------------------------------------------------------------------
// Tag prefix helper (uniform column width across all formatters)
// ---------------------------------------------------------------------------

// Width chosen so every tag lines up on the same column. 11 covers the
// widest existing labels ("[headless] ", "[thinking] ").
const TAG_WIDTH = 11;

function tag(label: string): string {
	const t = `[${label}]`;
	return t + " ".repeat(Math.max(1, TAG_WIDTH - t.length));
}

// TUI footer widget keys registered by extensions (emoji indicator,
// color band, permissions tier, ollama status, service-tier fast/auto).
// These are sticky indicators for the interactive footer — not workflow
// phases — so suppress them in headless output.
const TUI_FOOTER_STATUS_KEYS = new Set([
	"0-emoji",
	"0-color-band",
	"authority",
	"ollama",
	"sf-fast",
	"sf-auto",
	"zz-notifications",
]);

/**
 * Categorize notification messages so headless logs are scannable —
 * `[mcp]` for MCP-client ready lines, `[search]` for search status,
 * `[parallel]` for slice-parallel/subagent dispatch. Returns null to
 * fall through to the default formatting.
 */
function formatCategorizedNotification(message: string): string | null {
	if (message.startsWith("MCP client ready")) return `[mcp]     ${message}`;
	if (message.startsWith("Web search:")) return `[search]  ${message}`;
	if (message.startsWith("Native Anthropic web search"))
		return `[search]  ${message}`;
	if (message.includes("dispatching") && message.includes("subagents"))
		return `[parallel] ${message}`;
	if (message.startsWith("Slice-parallel:")) return `[parallel] ${message}`;
	if (message.startsWith("sf-reactive:")) return `[parallel] ${message}`;
	return null;
}

/**
 * True when statusKey indicates a real phase/milestone/slice/task transition
 * (rather than a generic status update). Drives [phase] vs [status] tagging.
 */
function isPhaseStatusKey(statusKey: string): boolean {
	return (
		statusKey.startsWith("phase:") ||
		statusKey.startsWith("milestone:") ||
		statusKey.startsWith("slice:") ||
		statusKey.startsWith("task:")
	);
}

// ---------------------------------------------------------------------------
// Tool-Arg Summarizer
// ---------------------------------------------------------------------------

/**
 * Produce a short human-readable summary of tool arguments.
 * Returns a string like "path/to/file.ts" or "grep pattern *.ts" — never the
 * full JSON blob.
 */
export function summarizeToolArgs(
	toolName: unknown,
	toolInput: unknown,
): string {
	const name = String(toolName ?? "");
	const input =
		toolInput && typeof toolInput === "object"
			? (toolInput as Record<string, unknown>)
			: {};

	// Helper: extract file path from either 'path' or 'file_path' (tools use both)
	const filePath = (): string => shortPath(input.path ?? input.file_path) || "";

	switch (name) {
		case "Read":
		case "read":
			return filePath();
		case "Write":
		case "write":
			return filePath();
		case "Edit":
		case "edit":
			return filePath();
		case "Bash":
		case "bash": {
			const cmd = String(input.command ?? "");
			return cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
		}
		case "async_bash": {
			const cmd = String(input.command ?? "");
			return cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd;
		}
		case "await_job": {
			const jobs = input.jobs;
			if (Array.isArray(jobs) && jobs.length > 0) return jobs.join(", ");
			return "";
		}
		case "cancel_job":
			return String(input.job_id ?? "");
		case "Glob":
		case "glob":
			return String(input.pattern ?? "");
		case "find": {
			const pat = String(input.pattern ?? "");
			const p = shortPath(input.path);
			return p ? `${pat} in ${p}` : pat;
		}
		case "Grep":
		case "grep":
		case "Search":
		case "search": {
			const pat = String(input.pattern ?? "");
			const g = input.glob ? ` ${input.glob}` : "";
			return `${pat}${g}`;
		}
		case "ls":
			return shortPath(input.path) || "";
		case "lsp": {
			const action = String(input.action ?? "");
			const file = shortPath(input.file);
			const sym = input.symbol ? ` ${input.symbol}` : "";
			return file ? `${action} ${file}${sym}` : action;
		}
		case "Task":
		case "task": {
			const desc = String(input.description ?? input.prompt ?? "");
			return desc.length > 60 ? desc.slice(0, 57) + "..." : desc;
		}
		case "subagent": {
			const agent = String(input.agent ?? "");
			const t = String(input.task ?? "");
			const summary = t.length > 50 ? t.slice(0, 47) + "..." : t;
			return agent ? `${agent}: ${summary}` : summary;
		}
		case "browser_navigate":
			return String(input.url ?? "");
		default: {
			// SF tools: show milestone/slice/task IDs when present
			if (name.startsWith("sf_")) {
				return summarizeSfTool(name, input);
			}
			// Fallback: show first string-valued key up to 60 chars
			for (const v of Object.values(input)) {
				if (typeof v === "string" && v.length > 0) {
					return v.length > 60 ? v.slice(0, 57) + "..." : v;
				}
			}
			return "";
		}
	}
}

/** Summarize SF extension tool args into a compact identifier string. */
function summarizeSfTool(name: string, input: Record<string, unknown>): string {
	const parts: string[] = [];
	if (input.milestoneId) parts.push(String(input.milestoneId));
	if (input.sliceId) parts.push(String(input.sliceId));
	if (input.taskId) parts.push(String(input.taskId));
	if (parts.length > 0) {
		const id = parts.join("/");
		// For completion tools, add the one-liner if present
		if (name.includes("complete") && typeof input.oneLiner === "string") {
			const ol =
				input.oneLiner.length > 50
					? input.oneLiner.slice(0, 47) + "..."
					: input.oneLiner;
			return `${id} ${ol}`;
		}
		return id;
	}
	// Fallback for SF tools without IDs (e.g. sf_decision_save)
	if (input.decision) {
		const d = String(input.decision);
		return d.length > 60 ? d.slice(0, 57) + "..." : d;
	}
	return "";
}

function shortPath(p: unknown): string {
	if (typeof p !== "string") return "";
	// Strip common CWD prefix to save space
	const cwd = process.cwd();
	if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
	// Strip /Users/*/Developer/ prefix
	return p.replace(/^\/Users\/[^/]+\/Developer\//, "");
}

// ---------------------------------------------------------------------------
// Format Duration
// ---------------------------------------------------------------------------

function formatDuration(ms: number): string {
	if (ms < 1000) return `${ms}ms`;
	if (ms >= 60_000) {
		const minutes = Math.floor(ms / 60_000);
		const seconds = Math.floor((ms % 60_000) / 1000);
		return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
	}
	const s = (ms / 1000).toFixed(1);
	return `${s}s`;
}

// ---------------------------------------------------------------------------
// Extension UI Auto-Responder
// ---------------------------------------------------------------------------

export function handleExtensionUIRequest(
	event: ExtensionUIRequest,
	client: RpcClient,
): void {
	const { id, method } = event;

	switch (method) {
		case "select": {
			// Lock-guard prompts list "View status" first, but headless needs "Force start"
			// to proceed. Detect by title and pick the force option.
			const title = String(event.title ?? "");
			let selected = event.options?.[0] ?? "";
			if (title.includes("Autonomous mode is running") && event.options) {
				const forceOption = event.options.find((o) =>
					o.toLowerCase().includes("force start"),
				);
				if (forceOption) selected = forceOption;
			}
			client.sendUIResponse(id, { value: selected });
			break;
		}
		case "confirm":
			client.sendUIResponse(id, { confirmed: true });
			break;
		case "input":
			client.sendUIResponse(id, { value: "" });
			break;
		case "editor":
			client.sendUIResponse(id, { value: event.prefill ?? "" });
			break;
		case "notify":
		case "setStatus":
		case "setWidget":
		case "setTitle":
		case "set_editor_text":
			client.sendUIResponse(id, { value: "" });
			break;
		default:
			process.stderr.write(
				`[headless] Warning: unknown extension_ui_request method "${method}", cancelling\n`,
			);
			client.sendUIResponse(id, { cancelled: true });
			break;
	}
}

// ---------------------------------------------------------------------------
// Progress Formatter
// ---------------------------------------------------------------------------

export function formatProgress(
	event: Record<string, unknown>,
	ctx: ProgressContext,
): string | null {
	const type = String(event.type ?? "");

	// Emit accumulated thinking preview before tool calls
	if (ctx.thinkingPreview) {
		// thinkingPreview is handled by the caller in headless.ts — it prepends
		// the thinking line before the current event's line. We return the thinking
		// line as a prefix joined with newline.
	}

	switch (type) {
		case "tool_execution_start": {
			if (!ctx.verbose) return null;
			const name = String(event.toolName ?? "unknown");
			const args = summarizeToolArgs(event.toolName, event.args);
			const argStr = args ? ` ${c.dim}${args}${c.reset}` : "";
			return `${c.dim}${tag("tool")}${c.reset}${name}${argStr}`;
		}

		case "tool_execution_end": {
			if (!ctx.verbose) return null;
			const name = String(event.toolName ?? "unknown");
			const durationStr =
				ctx.toolDuration != null
					? ` ${c.dim}${formatDuration(ctx.toolDuration)}${c.reset}`
					: "";
			if (ctx.isError) {
				return `${c.red}${tag("tool")}${name} error${c.reset}${durationStr}`;
			}
			return `${c.dim}${tag("tool")}${name} done${c.reset}${durationStr}`;
		}

		case "agent_start":
			return `${c.dim}${tag("agent")}Session started${c.reset}`;

		case "agent_end": {
			let line = `${c.dim}${tag("agent")}Session ended${c.reset}`;
			if (ctx.lastCost) {
				const cost = `$${ctx.lastCost.costUsd.toFixed(4)}`;
				const tokens = `${ctx.lastCost.inputTokens + ctx.lastCost.outputTokens} tokens`;
				line += ` ${c.dim}(${cost}, ${tokens})${c.reset}`;
			}
			return line;
		}

		case "extension_error":
			return `${c.red}${tag("error")}${formatExtensionError(event)}${c.reset}`;

		case "extension_ui_request": {
			const method = String(event.method ?? "");

			if (method === "notify") {
				const msg = String(event.message ?? "");
				if (!msg) return null;
				// Categorise known prefixes so headless logs are scannable.
				const categorised = formatCategorizedNotification(msg);
				if (categorised) return categorised;
				// Bold important notifications
				const isImportant =
					/^(committed:|verification gate:|milestone|blocked:)/i.test(msg);
				return isImportant
					? `${c.bold}${tag("forge")}${msg}${c.reset}`
					: `${tag("forge")}${msg}`;
			}

			if (method === "setStatus") {
				const statusKey = String(event.statusKey ?? "");
				const msg = String(event.message ?? "");
				if (!statusKey && !msg) return null; // suppress empty status lines
				// Drop sticky TUI footer widgets — they have no meaning in a headless run.
				if (TUI_FOOTER_STATUS_KEYS.has(statusKey)) return null;
				// Show meaningful phase transitions.
				if (statusKey) {
					const label = parsePhaseLabel(statusKey, msg);
					if (label) {
						const labelTag = isPhaseStatusKey(statusKey)
							? tag("phase")
							: tag("status");
						return `${c.cyan}${labelTag}${label}${c.reset}`;
					}
				}
				// Fallback: show message if non-empty.
				if (msg) return `${c.cyan}${tag("status")}${msg}${c.reset}`;
				return null;
			}

			return null;
		}

		default:
			return null;
	}
}

function formatExtensionError(event: Record<string, unknown>): string {
	const extensionPath = String(event.extensionPath ?? "unknown extension");
	const eventName = String(event.event ?? "unknown event");
	const error = event.error as Record<string, unknown> | string | undefined;
	const errorText =
		typeof error === "string"
			? error
			: error && typeof error === "object"
				? String(error.message ?? error.name ?? JSON.stringify(error))
				: "unknown error";
	return `Extension error in ${extensionPath} during ${eventName}: ${errorText}`;
}

/**
 * Format a thinking preview line from accumulated LLM text deltas.
 * Used as a fallback when streaming is not enabled — shows a truncated one-liner.
 */
export function formatThinkingLine(text: string): string {
	const trimmed = text.replace(/\s+/g, " ").trim();
	const truncated =
		trimmed.length > 120 ? trimmed.slice(0, 117) + "..." : trimmed;
	return `${c.dim}${c.italic}${tag("thinking")}${truncated}${c.reset}`;
}

/**
 * Format a text preview line from accumulated assistant text deltas.
 * Used as a fallback when streaming is not enabled — shows a truncated one-liner.
 * Unlike thinking, text is NOT italicized.
 */
export function formatTextLine(text: string): string {
	const trimmed = text.replace(/\s+/g, " ").trim();
	const truncated =
		trimmed.length > 120 ? trimmed.slice(0, 117) + "..." : trimmed;
	return `${c.dim}${tag("text")}${truncated}${c.reset}`;
}

// ---------------------------------------------------------------------------
// Streaming Text / Thinking Formatters
// ---------------------------------------------------------------------------

/**
 * Format a text_start marker — printed once when the assistant begins a text block.
 */
export function formatTextStart(): string {
	return `${c.dim}${tag("text")}${c.reset}`.trimEnd();
}

/**
 * Format a text_end marker — printed after the last text_delta.
 */
export function formatTextEnd(): string {
	return ""; // empty — newline handled by caller
}

/**
 * Format a thinking_start marker.
 */
export function formatThinkingStart(): string {
	return `${c.dim}${c.italic}${tag("thinking")}${c.reset}`.trimEnd();
}

/**
 * Format a thinking_end marker.
 */
export function formatThinkingEnd(): string {
	return ""; // empty — newline handled by caller
}

/**
 * Format a cost line (used for periodic cost updates in verbose mode).
 */
export function formatCostLine(
	costUsd: number,
	inputTokens: number,
	outputTokens: number,
): string {
	return `${c.dim}${tag("cost")}$${costUsd.toFixed(4)} (${inputTokens + outputTokens} tokens)${c.reset}`;
}

/**
 * Format a periodic liveness line for headless runs.
 *
 * Purpose: make long model calls and quiet autonomous mode phases observable without
 * changing machine-readable JSON output.
 */
export function formatHeadlessHeartbeat(ctx: HeadlessHeartbeatContext): string {
	const lastEvent = ctx.lastEventType
		? ctx.lastEventDetail
			? `${ctx.lastEventType}:${ctx.lastEventDetail}`
			: ctx.lastEventType
		: "none";
	const activityParts: string[] = [];
	if (ctx.eventDelta !== undefined) {
		if (ctx.eventDelta > 0) {
			activityParts.push(`+${ctx.eventDelta} events`);
		} else {
			activityParts.push("no new events");
		}
	}
	if (ctx.toolCallDelta !== undefined && ctx.toolCallDelta > 0) {
		activityParts.push(`+${ctx.toolCallDelta} tools`);
	}
	const activity =
		activityParts.length > 0 ? `; activity=${activityParts.join(", ")}` : "";
	const openTools =
		ctx.openToolCount && ctx.openToolCount > 0
			? ctx.openToolDetails?.length
				? `; openTools=${ctx.openToolCount}[${ctx.openToolDetails.join(", ")}]`
				: `; openTools=${ctx.openToolCount}`
			: "";
	const unit = ctx.activeUnit ? `; unit=${ctx.activeUnit}` : "";
	const model = ctx.activeModel ? `; model=${ctx.activeModel}` : "";
	return `${c.dim}${tag("headless")}still running ${formatDuration(ctx.elapsedMs)}; quiet ${formatDuration(ctx.quietMs)}; last=${lastEvent}; events=${ctx.totalEvents}; tools=${ctx.toolCallCount}${activity}${openTools}${unit}${model}${c.reset}`;
}

/**
 * Format a capped prompt preview for verbose headless dogfooding.
 *
 * Purpose: make the actual instruction payload visible when diagnosing
 * autonomous quality without flooding stderr with full context files.
 */
export function formatPromptTraceLines(
	customType: string,
	content: string,
	sessionFile: string,
	options: { maxChars?: number; maxLines?: number } = {},
): string[] {
	const maxChars = Math.max(0, options.maxChars ?? 2400);
	const maxLines = Math.max(1, options.maxLines ?? 80);
	const clipped = content.slice(0, maxChars);
	const rawLines = clipped.split(/\r?\n/).slice(0, maxLines);
	const lines = [
		`${tag("prompt")}${customType || "custom"} instructions (${content.length} chars) session=${sessionFile}`,
		`${tag("prompt")}--- preview ---`,
		...rawLines.map((line) => `${tag("prompt")}${line}`),
	];
	if (
		content.length > clipped.length ||
		clipped.split(/\r?\n/).length > maxLines
	) {
		lines.push(`${tag("prompt")}... truncated`);
	}
	return lines;
}

// ---------------------------------------------------------------------------
// Phase Label Parser
// ---------------------------------------------------------------------------

/**
 * Parse a statusKey into a human-readable phase label.
 * statusKey format varies but common patterns:
 *   "milestone:M1", "slice:S1.1", "task:T1.1.1", "phase:discuss", etc.
 */
function parsePhaseLabel(statusKey: string, message: string): string | null {
	// Direct phase/milestone/slice/task keys
	const parts = statusKey.split(":");
	if (parts.length >= 2) {
		const [kind, value] = parts;
		switch (kind.toLowerCase()) {
			case "milestone":
				return `Milestone ${value}${message ? " -- " + message : ""}`;
			case "slice":
				return `Slice ${value}${message ? " -- " + message : ""}`;
			case "task":
				return `Task ${value}${message ? " -- " + message : ""}`;
			case "phase":
				return `Phase: ${value}${message ? " -- " + message : ""}`;
			default:
				return `${kind}: ${value}${message ? " -- " + message : ""}`;
		}
	}

	// Unknown single-word keys: only surface when there's an accompanying
	// message worth showing. Bare widget keys (e.g. a new footer indicator
	// not yet added to TUI_FOOTER_STATUS_KEYS) are suppressed rather than
	// leaking through.
	return message ? `${statusKey}: ${message}` : null;
}

// ---------------------------------------------------------------------------
// Supervised Stdin Reader
// ---------------------------------------------------------------------------

export function startSupervisedStdinReader(
	client: RpcClient,
	onResponse: (id: string) => void,
): () => void {
	return attachJsonlLineReader(process.stdin as Readable, (line) => {
		handleSupervisedStdinLine(line, client, onResponse);
	});
}

function shouldWarnInvalidOrchestratorLine(line: string): boolean {
	// Only warn for lines that look like JSON-RPC frames — not incidental JSON-like
	// log output (e.g. `{ "level": "warn", ... }` or `[INFO] Starting task`).
	// Real JSON-RPC 2.0 frames must have `"jsonrpc":"2.0"` somewhere in the string.
	const trimmed = line.trimStart();
	if (!trimmed.startsWith("{")) return false;
	return line.includes('"jsonrpc"') || line.includes("'jsonrpc'");
}

/**
 * Handle one supervised JSONL stdin frame.
 *
 * Purpose: accept real orchestrator control records while ignoring accidental
 * pasted prose in headless terminals without flooding stderr.
 *
 * Consumer: startSupervisedStdinReader, which receives LF-framed stdin lines.
 */
export function handleSupervisedStdinLine(
	line: string,
	client: RpcClient,
	onResponse: (id: string) => void,
	stderr: Pick<NodeJS.WriteStream, "write"> = process.stderr,
): void {
	let msg: Record<string, unknown>;
	try {
		msg = JSON.parse(line);
	} catch {
		if (shouldWarnInvalidOrchestratorLine(line)) {
			stderr.write(
				`[headless] Warning: invalid JSON from orchestrator stdin, skipping\n`,
			);
		}
		return;
	}

	const type = String(msg.type ?? "");

	switch (type) {
		case "extension_ui_response": {
			const id = String(msg.id ?? "");
			const value = msg.value !== undefined ? String(msg.value) : undefined;
			const confirmed =
				typeof msg.confirmed === "boolean" ? msg.confirmed : undefined;
			const cancelled =
				typeof msg.cancelled === "boolean" ? msg.cancelled : undefined;
			client.sendUIResponse(id, { value, confirmed, cancelled });
			if (id) {
				onResponse(id);
			}
			break;
		}
		case "prompt":
			client.prompt(String(msg.message ?? ""));
			break;
		case "steer":
			client.steer(String(msg.message ?? ""));
			break;
		case "follow_up":
			client.followUp(String(msg.message ?? ""));
			break;
		default:
			stderr.write(
				`[headless] Warning: unknown message type "${type}" from orchestrator stdin\n`,
			);
			break;
	}
}
