import path from "node:path";
import { pathToFileURL } from "node:url";
import type { AssistantMessage } from "@earendil-works/pi-ai";
import {
	CustomEditor,
	keyText,
	type ExtensionAPI,
	type ExtensionContext,
	type ContextUsage,
	type KeybindingsManager,
} from "@earendil-works/pi-coding-agent";
import {
	getCapabilities,
	hyperlink,
	truncateToWidth,
	visibleWidth,
	type Component,
	type EditorTheme,
	type TUI,
} from "@earendil-works/pi-tui";
import { renderFixedEditorCluster } from "./fixed-editor/cluster.ts";
import {
	emergencyTerminalModeReset,
	TerminalSplitCompositor,
} from "./fixed-editor/terminal-split.ts";

const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
const PI_LOGO = "⡯⢣"; // 4x4 braille rasterisation of the official Pi SVG mark.

const DEEP_BLUE: Rgb = [59, 130, 246];
const BLUE: Rgb = [34, 211, 238];
const SKY: Rgb = [168, 85, 247];
const ICE: Rgb = [236, 72, 153];
const PALETTE: Rgb[] = [DEEP_BLUE, BLUE, SKY, ICE, SKY, BLUE];

const THEME_PALETTES: Record<string, Rgb[]> = {
	"github-dark-default": [
		[31, 111, 235],
		[56, 139, 253],
		[121, 192, 255],
		[165, 214, 255],
		[121, 192, 255],
		[56, 139, 253],
	],
	"pi-electric-aurora": PALETTE,
	"pi-ocean-glass": [
		[2, 132, 199],
		[56, 189, 248],
		[165, 243, 252],
		[186, 230, 253],
		[125, 211, 252],
		[45, 212, 191],
	],
	"pi-synthwave": [
		[124, 58, 237],
		[217, 70, 239],
		[244, 114, 182],
		[251, 146, 60],
		[250, 204, 21],
		[34, 211, 238],
	],
	"pi-terminal-emerald": [
		[5, 150, 105],
		[16, 185, 129],
		[52, 211, 153],
		[94, 234, 212],
		[187, 247, 208],
		[45, 212, 191],
	],
	"pi-royal": [
		[67, 56, 202],
		[124, 58, 237],
		[139, 92, 246],
		[196, 181, 253],
		[147, 197, 253],
		[240, 171, 252],
	],
};

type Rgb = [number, number, number];
type ThinkingLevel =
	| "off"
	| "minimal"
	| "low"
	| "medium"
	| "high"
	| "xhigh"
	| "max"
	| string;

const TITLE_LINES = [
	"▀████████████▀",
	" ╘███    ███  ",
	"  ███    ███  ",
	"  ███    ███  ",
	" ▄███▄  ▄███▄ ",
];

function mix(a: number, b: number, t: number) {
	return Math.round(a + (b - a) * t);
}

function paletteForTheme(themeName: string | undefined) {
	return THEME_PALETTES[themeName ?? ""] ?? PALETTE;
}

function sampleGradient(position: number, palette: Rgb[]) {
	const wrapped = ((position % 1) + 1) % 1;
	const scaled = wrapped * palette.length;
	const index = Math.floor(scaled);
	const nextIndex = (index + 1) % palette.length;
	const t = scaled - index;
	const a = palette[index]!;
	const b = palette[nextIndex]!;
	return [mix(a[0], b[0], t), mix(a[1], b[1], t), mix(a[2], b[2], t)] as Rgb;
}

function fg([r, g, b]: Rgb, text: string) {
	return `\x1b[38;2;${r};${g};${b}m${text}${RESET}`;
}

function stripAnsi(text: string) {
	return text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
}

function gradientText(text: string, phase: number, palette = PALETTE) {
	const chars = [...stripAnsi(text)];
	const span = Math.max(chars.length - 1, 1);
	return chars
		.map((char, index) =>
			char === " "
				? char
				: fg(sampleGradient(index / span + phase, palette), char),
		)
		.join("");
}

function projectName(cwd: string) {
	return path.basename(cwd) || "session";
}

function shortCwd(cwd: string) {
	const normalized = cwd.replace(/\\/g, "/").replace(/\/$/, "").toLowerCase();
	if (normalized === "c:/windows/system32") return "~";

	const home = process.env.USERPROFILE || process.env.HOME;
	return home && cwd.toLowerCase().startsWith(home.toLowerCase())
		? `~${cwd.slice(home.length)}`
		: cwd;
}

function formatTokens(count: number) {
	if (count < 1000) return count.toString();
	if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
	if (count < 1000000) return `${Math.round(count / 1000)}k`;
	if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
	return `${Math.round(count / 1000000)}M`;
}

function formatModel(modelId: string) {
	return modelId.replace(/^models\//, "");
}

function providerLogo(providerId: string | undefined) {
	const id = providerId?.toLowerCase() ?? "";
	if (id.includes("openai") || id.includes("codex")) return "󰚩";
	if (id.includes("copilot") || id.includes("github")) return "";
	if (id.includes("opencode")) return "󰅪";
	return "◆";
}

function renderPiLogo(palette = PALETTE) {
	return `${fg(palette[0] ?? DEEP_BLUE, PI_LOGO[0] ?? "")}${fg(palette[2] ?? SKY, PI_LOGO[1] ?? "")}`;
}

function contextMeterIcon(percent: number | null | undefined) {
	if (percent === null || percent === undefined) return "○";
	const slices = ["󰪞", "󰪟", "󰪠", "󰪡", "󰪢", "󰪣", "󰪤", "󰪥"];
	const index = Math.max(
		0,
		Math.min(slices.length - 1, Math.ceil(percent / 12.5) - 1),
	);
	return slices[index];
}

function formatContext(usage: ContextUsage | undefined) {
	if (!usage || usage.percent === null) return "○ ?%/?";
	const max = usage.contextWindow ?? 0;
	const maxText = max > 0 ? formatTokens(max) : "?";
	return `${contextMeterIcon(usage.percent)} ${Math.round(usage.percent)}%/${maxText}`;
}

function formatPathSegment(cwd: string, theme: any) {
	const short = shortCwd(cwd).replace(/\\/g, "/");
	let rendered: string;
	if (short === "~") {
		rendered = theme.fg("text", theme.bold(" ~"));
	} else {
		const slash = short.lastIndexOf("/");
		if (slash === -1)
			rendered = `${theme.fg("dim", " ")}${theme.fg("text", theme.bold(short))}`;
		else {
			const parent = short.slice(0, slash + 1);
			const base = short.slice(slash + 1);
			rendered = `${theme.fg("dim", ` ${parent}`)}${theme.fg("text", theme.bold(base))}`;
		}
	}

	try {
		if (!getCapabilities().hyperlinks) return rendered;
		return hyperlink(rendered, pathToFileURL(path.resolve(cwd)).href);
	} catch {
		return rendered;
	}
}

function closeDanglingHyperlink(line: string) {
	const markerCount = (line.match(/\x1b\]8;;/g) ?? []).length;
	return markerCount % 2 === 1 ? `${line}\x1b]8;;\x1b\\` : line;
}

function contextColor(percent: number | null | undefined) {
	if (percent === null || percent === undefined) return "dim";
	if (percent >= 85) return "error";
	if (percent >= 65) return "warning";
	return "muted";
}

function thinkingColor(level: ThinkingLevel) {
	switch (level) {
		case "off":
			return "thinkingOff";
		case "minimal":
			return "thinkingMinimal";
		case "low":
			return "thinkingLow";
		case "medium":
			return "thinkingMedium";
		case "high":
			return "thinkingHigh";
		case "xhigh":
			return "thinkingXhigh";
		case "max":
			return "thinkingMax";
		default:
			return "muted";
	}
}

function fitSegments(width: number, segments: string[], separator = "  ") {
	const budget = Math.max(0, width);
	let line = "";
	for (const segment of segments) {
		if (!segment) continue;
		const prefix = line ? separator : "";
		const remaining = budget - visibleWidth(line) - visibleWidth(prefix);
		if (remaining <= 0) break;

		if (visibleWidth(segment) <= remaining) {
			line += `${prefix}${segment}`;
			continue;
		}

		const truncated = truncateToWidth(segment, remaining, "…");
		if (truncated)
			line += `${prefix}${closeDanglingHyperlink(truncated)}`;
		break;
	}
	return line;
}

/**
 * Render a one-line footer with the right-hand content taking priority.
 * The right segments are fitted first, so context/cost/statuses survive
 * narrow terminals before the left-side identity/location segments do.
 */
function splitLine(width: number, leftSegments: string[], rightSegments: string[]) {
	const budget = Math.max(1, width);
	const right = fitSegments(budget, rightSegments);
	if (!right) return fitSegments(budget, leftSegments);

	const gapWidth = 1;
	const leftBudget = Math.max(0, budget - visibleWidth(right) - gapWidth);
	const left = fitSegments(leftBudget, leftSegments);
	if (!left) return truncateToWidth(right, budget, "…");

	const gap = Math.max(1, budget - visibleWidth(left) - visibleWidth(right));
	return `${left}${" ".repeat(gap)}${right}`;
}

function formatShortcutText(raw: string) {
	return raw
		.split("/")
		.map((combo) => {
			const parts = combo.split("+").filter(Boolean);
			const formatted = parts.map((part) => {
				const lower = part.toLowerCase();
				if (lower === "ctrl") return "Ctrl";
				if (lower === "alt") return "Alt";
				if (lower === "shift") return "⇧";
				if (lower === "escape" || lower === "esc") return "Esc";
				if (lower === "pageup") return "PageUp";
				if (lower === "pagedown") return "PageDown";
				if (part.length === 1) return part.toUpperCase();
				return part.charAt(0).toUpperCase() + part.slice(1);
			});
			return formatted[0] === "⇧"
				? `⇧${formatted.slice(1).join("+")}`
				: formatted.join("+");
		})
		.join("/");
}

function configuredShortcut(
	keybinding: Parameters<typeof keyText>[0],
	fallback: string,
) {
	try {
		const configured = formatShortcutText(keyText(keybinding));
		return configured || fallback;
	} catch {
		return fallback;
	}
}

function padVisible(text: string, width: number) {
	const length = visibleWidth(text);
	if (length >= width) return truncateToWidth(text, width, "");
	return `${text}${" ".repeat(width - length)}`;
}

function oneLine(text: string) {
	return text.replace(/\s+/g, " ").trim();
}

function headerShortcutHints(): Array<[string, string]> {
	return [
		[configuredShortcut("app.model.select", "Ctrl+L"), "model"],
		[configuredShortcut("app.model.cycleForward", "Ctrl+P"), "cycle"],
		[configuredShortcut("app.thinking.cycle", "⇧Tab"), "thinking"],
		["Alt+S", "stash"],
		["Alt+N", "notes"],
		["Esc Esc", "tree"],
		[configuredShortcut("app.tools.expand", "Ctrl+O"), "tools"],
	];
}

function headerShortcutBox(width: number, palette = PALETTE) {
	const shortcuts = headerShortcutHints();

	const innerWidth = Math.max(12, width - 2);
	const title = `${BOLD}${fg(palette[2] ?? SKY, "shortcuts")}${RESET}`;
	const rows = shortcuts.slice(0, 4).map(([key, label], index) => {
		const next = shortcuts[index + 4];
		const left = `${fg(palette[3] ?? ICE, key)} ${fg([110, 118, 129], label)}`;
		const right = next
			? `${fg(palette[3] ?? ICE, next[0])} ${fg([110, 118, 129], next[1])}`
			: "";
		const gap = Math.max(
			2,
			innerWidth - visibleWidth(left) - visibleWidth(right),
		);
		return `│${padVisible(`${left}${" ".repeat(gap)}${right}`, innerWidth)}│`;
	});

	return [
		`╭${"─".repeat(innerWidth)}╮`,
		`│${padVisible(title, innerWidth)}│`,
		...rows,
		`╰${"─".repeat(innerWidth)}╯`,
	];
}

function renderHeader(
	width: number,
	phase: number,
	subtitleText: string,
	palette = PALETTE,
	terminalRows = Number.POSITIVE_INFINITY,
) {
	const compact = width < 72 || terminalRows <= 24;
	if (compact) {
		const compactText = stripAnsi(
			truncateToWidth(`π ${subtitleText}`, width, "…"),
		);
		return [
			"",
			`${BOLD}${gradientText(compactText, phase, palette)}${RESET}`,
			"",
		];
	}

	const logoWidth = Math.max(...TITLE_LINES.map((line) => visibleWidth(line)));
	const leftMargin = width >= 80 ? 2 : 0;
	const gap = 3;
	const minBoxWidth = 28;
	const maxBoxWidth = 48;
	const maxLogoBlockWidth = Math.max(
		logoWidth + 4,
		width - leftMargin - gap - minBoxWidth,
	);
	const subtitleWidth = visibleWidth(`  ${subtitleText}`);
	const logoBlockWidth = Math.max(
		logoWidth + 4,
		Math.min(maxLogoBlockWidth, subtitleWidth),
	);
	const logoInset = Math.max(0, Math.floor((logoBlockWidth - logoWidth) / 2));
	const canShowBox = width >= leftMargin + logoBlockWidth + gap + 24;
	const logoLines = [
		...TITLE_LINES.map((line, row) =>
			gradientText(
				padVisible(`${" ".repeat(logoInset)}${line}`, logoBlockWidth),
				phase + row * 0.045,
				palette,
			),
		),
		`${BOLD}${gradientText(padVisible(stripAnsi(truncateToWidth(`  ${subtitleText}`, logoBlockWidth, "…")), logoBlockWidth), phase + 0.18, palette)}${RESET}`,
	];

	const prefix = " ".repeat(leftMargin);
	if (!canShowBox)
		return [
			"",
			...logoLines.map((line) =>
				truncateToWidth(`${prefix}${line}`, width, ""),
			),
			"",
		];

	const boxWidth = Math.min(
		maxBoxWidth,
		Math.max(minBoxWidth, width - leftMargin - logoBlockWidth - gap),
	);
	const boxLines = headerShortcutBox(boxWidth, palette);
	const rows = Math.max(logoLines.length, boxLines.length);
	const lines = [];
	for (let i = 0; i < rows; i++) {
		const logo = padVisible(logoLines[i] ?? "", logoBlockWidth);
		const box = boxLines[i] ?? "";
		lines.push(
			truncateToWidth(`${prefix}${logo}${" ".repeat(gap)}${box}`, width, ""),
		);
	}

	return ["", ...lines, ""];
}

function formatDuration(ms: number) {
	const totalSeconds = Math.max(0, Math.floor(ms / 1000));
	const hours = Math.floor(totalSeconds / 3600);
	const minutes = Math.floor((totalSeconds % 3600) / 60);
	const seconds = totalSeconds % 60;

	if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
	if (minutes > 0) return `${minutes}m ${seconds}s`;
	return `${seconds}s`;
}

function renderActivityBorder(
	width: number,
	label: string,
	borderColor: (text: string) => string,
	theme: any,
) {
	const maxLabelWidth = Math.max(0, width - 4);
	const displayLabel = stripAnsi(
		truncateToWidth(` ${label} `, maxLabelWidth, "…"),
	);
	const labelWidth = visibleWidth(displayLabel);
	const rails = width - labelWidth;
	if (rails < 4 || !displayLabel) return borderColor("─".repeat(Math.max(0, width)));

	const left = Math.floor(rails / 2);
	const right = rails - left;
	return `${borderColor("─".repeat(left))}${theme.fg("muted", displayLabel)}${borderColor("─".repeat(right))}`;
}

export default function (pi: ExtensionAPI) {
	let currentModelId = "no model selected";
	let currentProviderId = "";
	let currentThinking: ThinkingLevel = "?";
	let requestHeaderRender: (() => void) | undefined;
	let requestFooterRender: (() => void) | undefined;
	let stashedEditorText: string | undefined;
	let currentEditor: CustomEditor | undefined;
	let fixedEditorCompositor: TerminalSplitCompositor | undefined;
	let fixedEditorEnabled = process.env.PI_UI_FIXED_EDITOR !== "0";
	let editorGeneration = 0;
	let fixedEditorContainer: any;
	let fixedStatusContainer: any;
	let fixedWidgetContainerAbove: any;
	let fixedWidgetContainerBelow: any;
	let activeFooterTheme: any;
	let activeFooterData: any;
	let gitDirty = false;
	let lastUserMessage = "";
	let workingStartedAt: number | undefined;
	let workingPhase = "Working";
	let runningToolCalls = new Set<string>();
	let workingTimer: ReturnType<typeof setInterval> | undefined;
	let sessionStatsDirty = true;
	let cachedSessionCost = 0;

	function installHeader(ctx: ExtensionContext) {
		ctx.ui.setHeader((tui) => {
			requestHeaderRender = () => tui.requestRender();
			return {
				render(width: number) {
					return renderHeader(
						width,
						0,
						`${formatModel(currentModelId)} · ${projectName(ctx.cwd)}`,
						paletteForTheme(ctx.ui.theme.name),
						Number((tui as any).terminal?.rows) || Number.POSITIVE_INFINITY,
					);
				},
				invalidate() {
					tui.requestRender();
				},
			} satisfies Component;
		});
		ctx.ui.setTitle(
			`π ${projectName(ctx.cwd)} · ${formatModel(currentModelId)}`,
		);
	}

	function renderLastUserMessageLine(theme: any, width: number) {
		if (!lastUserMessage) return undefined;
		const label = theme.fg("dim", "You › ");
		const messageWidth = Math.max(0, width - visibleWidth(label));
		return `${label}${theme.fg("muted", truncateToWidth(lastUserMessage, messageWidth, "…"))}`;
	}

	function invalidateSessionStats() {
		sessionStatsDirty = true;
	}

	// Session entries are append-only; lifecycle handlers invalidate this cache.
	function getSessionCost(ctx: ExtensionContext) {
		if (!sessionStatsDirty) return cachedSessionCost;

		const entries = ctx.sessionManager.getEntries();
		let totalCost = 0;
		for (const entry of entries) {
			if (entry.type !== "message" || entry.message.role !== "assistant")
				continue;
			const message = entry.message as AssistantMessage;
			const cost = message.usage?.cost?.total;
			if (typeof cost === "number" && Number.isFinite(cost)) totalCost += cost;
		}

		cachedSessionCost = totalCost;
		sessionStatsDirty = false;
		return totalCost;
	}

	function extensionStatusSegments(theme: any, footerData: any) {
		const statuses = footerData.getExtensionStatuses?.() as
			| ReadonlyMap<string, string>
			| undefined;
		if (!statuses || statuses.size === 0) return [];

		return Array.from(statuses.entries())
			.sort(([left], [right]) => left.localeCompare(right))
			.map(([, text]) => oneLine(String(text ?? "")))
			.filter(Boolean)
			.map((text) => theme.fg("dim", text));
	}

	function renderFooterLines(
		ctx: ExtensionContext,
		theme: any,
		footerData: any,
		width: number,
	) {
		const lines = [renderFooterLine(ctx, theme, footerData, width)];
		const lastUserLine = renderLastUserMessageLine(theme, width);
		if (lastUserLine) lines.push(lastUserLine);
		return lines;
	}

	function renderFooterLine(
		ctx: ExtensionContext,
		theme: any,
		footerData: any,
		width: number,
	) {
		const usage = ctx.getContextUsage();
		const branch = footerData.getGitBranch();
		const context = formatContext(usage);
		const contextTheme = contextColor(usage?.percent);
		const palette = paletteForTheme(ctx.ui.theme.name);
		const sessionName = oneLine(ctx.sessionManager.getSessionName() ?? "");
		const totalCost = getSessionCost(ctx);
		const costText =
			totalCost > 0
				? totalCost < 0.001
					? "<$0.001"
					: `$${totalCost.toFixed(3)}`
				: "";

		const leftSegments = [
			renderPiLogo(palette),
			`${theme.fg("accent", providerLogo(currentProviderId))} ${theme.fg("muted", formatModel(currentModelId))}`,
			theme.fg(thinkingColor(currentThinking) as any, currentThinking),
			formatPathSegment(ctx.cwd, theme),
			...(branch
				? [
						theme.fg(
							gitDirty ? "warning" : "success",
							` ${branch}${gitDirty ? "*" : ""}`,
						),
					]
				: []),
		];

		const rightSegments = [
			theme.fg(contextTheme as any, context),
			...extensionStatusSegments(theme, footerData),
			costText ? theme.fg("dim", costText) : "",
			sessionName ? theme.fg("muted", `• ${sessionName}`) : "",
		];

		return closeDanglingHyperlink(splitLine(width, leftSegments, rightSegments));
	}

	async function refreshGitDirty(ctx: ExtensionContext) {
		try {
			const result = await (pi as any).exec?.(
				"git",
				["status", "--porcelain"],
				{ cwd: ctx.cwd },
			);
			gitDirty = Boolean(result?.stdout?.trim());
		} catch {
			gitDirty = false;
		}
		requestFooterRender?.();
	}

	function installFooter(ctx: ExtensionContext) {
		ctx.ui.setFooter((tui, theme, footerData) => {
			activeFooterTheme = theme;
			activeFooterData = footerData;
			requestFooterRender = () => tui.requestRender();
			const disposeBranch = footerData.onBranchChange(() => {
				void refreshGitDirty(ctx);
				tui.requestRender();
			});
			void refreshGitDirty(ctx);

			return {
				render(width: number) {
					return fixedEditorCompositor
						? []
						: renderFooterLines(ctx, theme, footerData, width);
				},
				invalidate() {
					tui.requestRender();
				},
				dispose() {
					disposeBranch?.();
				},
			} satisfies Component & { dispose(): void };
		});
	}

	function teardownFixedEditor(options?: {
		resetExtendedKeyboardModes?: boolean;
	}) {
		const hadCompositor = fixedEditorCompositor !== undefined;
		fixedEditorCompositor?.dispose(options);
		if (!hadCompositor && options?.resetExtendedKeyboardModes) {
			try {
				process.stdout.write(emergencyTerminalModeReset());
			} catch {
				// Best-effort terminal cleanup on shutdown.
			}
		}
		fixedEditorCompositor = undefined;
		fixedEditorContainer = undefined;
		fixedStatusContainer = undefined;
		fixedWidgetContainerAbove = undefined;
		fixedWidgetContainerBelow = undefined;
	}

	function renderFixedFooterLines(ctx: ExtensionContext, width: number) {
		if (!activeFooterData) return [];
		return renderFooterLines(
			ctx,
			activeFooterTheme ?? ctx.ui.theme,
			activeFooterData,
			width,
		);
	}

	function findContainerWithChild(
		tui: any,
		child: any,
	): { container: any; index: number } | null {
		const children = Array.isArray(tui?.children) ? tui.children : [];
		const index = children.findIndex(
			(candidate: any) =>
				Array.isArray(candidate?.children) &&
				candidate.children.includes(child),
		);
		return index === -1 ? null : { container: children[index], index };
	}

	function installFixedEditor(ctx: ExtensionContext, tui: TUI) {
		teardownFixedEditor();
		if (!ctx.hasUI || !(tui as any)?.terminal?.write || !currentEditor) return;

		const editorContainerMatch = findContainerWithChild(tui, currentEditor);
		if (!editorContainerMatch) return;

		const tuiChildren = Array.isArray((tui as any).children)
			? (tui as any).children
			: [];
		fixedEditorContainer = editorContainerMatch.container;
		const statusContainerCandidate =
			tuiChildren[editorContainerMatch.index - 2] ?? null;
		fixedStatusContainer =
			statusContainerCandidate &&
			typeof statusContainerCandidate.render === "function"
				? statusContainerCandidate
				: null;
		fixedWidgetContainerAbove =
			tuiChildren[editorContainerMatch.index - 1] ?? null;
		fixedWidgetContainerBelow =
			tuiChildren[editorContainerMatch.index + 1] ?? null;

		let compositor!: TerminalSplitCompositor;
		compositor = new TerminalSplitCompositor({
			tui,
			terminal: (tui as any).terminal,
			mouseScroll: true,
			getShowHardwareCursor: () =>
				typeof (tui as any).getShowHardwareCursor === "function" &&
				(tui as any).getShowHardwareCursor(),
			renderCluster: (width, terminalRows) =>
				renderFixedEditorCluster({
					width,
					terminalRows,
					statusLines: [
						...(fixedStatusContainer
							? compositor
									.renderHidden(fixedStatusContainer, width)
									.filter((line) => visibleWidth(line) > 0)
							: []),
						...(fixedWidgetContainerAbove
							? compositor
									.renderHidden(fixedWidgetContainerAbove, width)
									.filter((line) => visibleWidth(line) > 0)
							: []),
					],
					topLines: renderFixedFooterLines(ctx, width),
					editorLines: fixedEditorContainer
						? compositor.renderHidden(fixedEditorContainer, width)
						: [],
					secondaryLines: fixedWidgetContainerBelow
						? compositor.renderHidden(fixedWidgetContainerBelow, width)
						: [],
				}),
		});

		fixedEditorCompositor = compositor;
		if (fixedStatusContainer?.render)
			compositor.hideRenderable(fixedStatusContainer);
		if (fixedWidgetContainerAbove?.render)
			compositor.hideRenderable(fixedWidgetContainerAbove);
		compositor.hideRenderable(fixedEditorContainer);
		if (fixedWidgetContainerBelow?.render)
			compositor.hideRenderable(fixedWidgetContainerBelow);
		compositor.install();
		(tui as any).requestRender?.(true);
	}

	function installEditor(ctx: ExtensionContext) {
		const generation = editorGeneration;
		const previousFactory = ctx.ui.getEditorComponent?.();
		const ownPreviousFactory =
			previousFactory && (previousFactory as any).__piSetupFixedEditor;
		const factory = (
			tui: TUI,
			theme: EditorTheme,
			keybindings: KeybindingsManager,
		) => {
			if (previousFactory && !ownPreviousFactory)
				return previousFactory(tui, theme, keybindings);

			class FixedPiEditor extends CustomEditor {
				constructor() {
					super(tui, theme, keybindings);
					currentEditor = this;
					setTimeout(() => {
						if (fixedEditorEnabled && generation === editorGeneration)
							installFixedEditor(ctx, tui);
					}, 0);
				}

				borderColor = (text: string) =>
					ctx.ui.theme.fg(thinkingColor(currentThinking) as any, text);

				render(width: number) {
					const lines = super.render(width);
					if (!workingStartedAt || !lines[0]) return lines;
					if (lines[0].includes("↑") || lines[0].includes("↓")) return lines;
					if (visibleWidth(lines[0]) !== width) return lines;

					lines[0] = renderActivityBorder(
						width,
						`${workingPhase} · ${formatDuration(Date.now() - workingStartedAt)}`,
						(text) => this.borderColor(text),
						ctx.ui.theme,
					);
					return lines;
				}
			}

			return new FixedPiEditor();
		};
		(factory as any).__piSetupFixedEditor = true;
		ctx.ui.setEditorComponent(factory);
	}

	function setWorkingPhase(phase: string) {
		if (workingPhase === phase) return;
		workingPhase = phase;
		requestFooterRender?.();
	}

	function stopWorkingTimer() {
		if (workingTimer) {
			clearInterval(workingTimer);
			workingTimer = undefined;
		}
		workingStartedAt = undefined;
		runningToolCalls.clear();
	}

	function updateWorkingMessage(ctx: ExtensionContext) {
		if (!workingStartedAt) return;
		ctx.ui.setWorkingMessage(
			`Working for ${formatDuration(Date.now() - workingStartedAt)}`,
		);
		requestFooterRender?.();
	}

	function startWorkingTimer(ctx: ExtensionContext) {
		stopWorkingTimer();
		workingPhase = "Thinking";
		workingStartedAt = Date.now();
		updateWorkingMessage(ctx);
		workingTimer = setInterval(() => updateWorkingMessage(ctx), 1000);
	}

	function installUi(ctx: ExtensionContext) {
		if (!ctx.hasUI) return;
		invalidateSessionStats();
		workingPhase = "Working";
		runningToolCalls.clear();
		currentModelId = ctx.model?.id ?? currentModelId;
		currentProviderId = ctx.model?.provider ?? currentProviderId;
		currentThinking =
			(typeof (ctx as any).getThinkingLevel === "function"
				? (ctx as any).getThinkingLevel()
				: undefined) ??
			(typeof (pi as any).getThinkingLevel === "function"
				? (pi as any).getThinkingLevel()
				: undefined) ??
			currentThinking;
		setTimeout(() => {
			const thinking =
				(typeof (ctx as any).getThinkingLevel === "function"
					? (ctx as any).getThinkingLevel()
					: undefined) ??
				(typeof (pi as any).getThinkingLevel === "function"
					? (pi as any).getThinkingLevel()
					: undefined);
			if (thinking) {
				currentThinking = thinking;
				requestFooterRender?.();
			}
		}, 0);
		editorGeneration++;
		teardownFixedEditor();
		installHeader(ctx);
		installFooter(ctx);
		if (fixedEditorEnabled && ctx.mode === "tui") installEditor(ctx);
		else ctx.ui.setEditorComponent(undefined);
		ctx.ui.setWorkingIndicator();
	}

	pi.on("session_start", (_event, ctx) => {
		installUi(ctx);
	});

	pi.on("model_select", (event) => {
		currentModelId = event.model.id;
		currentProviderId = event.model.provider ?? currentProviderId;
		requestHeaderRender?.();
		requestFooterRender?.();
	});

	pi.on("thinking_level_select", (event) => {
		currentThinking = event.level;
		requestFooterRender?.();
	});

	pi.on("session_info_changed", (_event, ctx) => {
		if (ctx.hasUI) requestFooterRender?.();
	});

	pi.on("message_start", (event, ctx) => {
		if (!ctx.hasUI || !workingStartedAt) return;
		if (event.message.role === "assistant") setWorkingPhase("Thinking");
	});

	pi.on("message_update", (event, ctx) => {
		if (!ctx.hasUI || !workingStartedAt || runningToolCalls.size > 0) return;
		switch (event.assistantMessageEvent.type) {
			case "thinking_start":
			case "thinking_delta":
			case "thinking_end":
				setWorkingPhase("Thinking");
				break;
			case "text_start":
			case "text_delta":
			case "text_end":
				setWorkingPhase("Streaming");
				break;
			case "toolcall_start":
			case "toolcall_delta":
				setWorkingPhase("Preparing tool");
				break;
		}
	});

	pi.on("tool_execution_start", (event, ctx) => {
		if (!ctx.hasUI || !workingStartedAt) return;
		runningToolCalls.add(event.toolCallId);
		const toolName = oneLine(event.toolName ?? "") || "tool";
		setWorkingPhase(
			runningToolCalls.size === 1
				? `Running ${toolName}`
				: `Running ${runningToolCalls.size} tools`,
		);
	});

	pi.on("tool_execution_end", (event, ctx) => {
		if (!ctx.hasUI || !workingStartedAt) return;
		runningToolCalls.delete(event.toolCallId);
		if (runningToolCalls.size === 0) setWorkingPhase("Thinking");
		else
			setWorkingPhase(
				runningToolCalls.size === 1
					? "Running 1 tool"
					: `Running ${runningToolCalls.size} tools`,
			);
	});

	pi.on("message_end", (_event, ctx) => {
		invalidateSessionStats();
		if (ctx.hasUI) requestFooterRender?.();
	});

	pi.on("session_compact", (_event, ctx) => {
		invalidateSessionStats();
		if (ctx.hasUI) requestFooterRender?.();
	});

	pi.on("session_tree", (_event, ctx) => {
		invalidateSessionStats();
		if (ctx.hasUI) requestFooterRender?.();
	});

	pi.on("input", (event, ctx) => {
		if (event.source !== "interactive") return;
		const text = oneLine(event.text ?? "");
		if (!text) return;
		lastUserMessage = text;
		requestFooterRender?.();
		if (ctx.hasUI && fixedEditorCompositor) {
			setTimeout(() => requestFooterRender?.(), 0);
		}
	});

	pi.on("agent_start", (_event, ctx) => {
		if (!ctx.hasUI) return;
		startWorkingTimer(ctx);
	});

	pi.on("turn_end", (_event, ctx) => {
		invalidateSessionStats();
		if (ctx.hasUI) {
			void refreshGitDirty(ctx);
			if (workingStartedAt && runningToolCalls.size === 0)
				setWorkingPhase("Thinking");
		}
		requestFooterRender?.();
	});
	pi.on("agent_end", (_event, ctx) => {
		if (ctx.hasUI) {
			stopWorkingTimer();
			ctx.ui.setWorkingMessage();
		}
		requestFooterRender?.();
	});

	pi.on("session_shutdown", (event, ctx) => {
		if (!ctx.hasUI) return;
		stopWorkingTimer();
		ctx.ui.setWorkingMessage();
		ctx.ui.setHeader(undefined);
		editorGeneration++;
		teardownFixedEditor({
			resetExtendedKeyboardModes: event.reason === "quit",
		});
		ctx.ui.setEditorComponent(undefined);
		ctx.ui.setFooter(undefined);
		ctx.ui.setWorkingIndicator();
	});

	pi.registerShortcut("alt+s", {
		description: "Stash/restore editor text",
		handler: async (ctx) => {
			const text = ctx.ui.getEditorText();
			if (text.trim()) {
				stashedEditorText = text;
				ctx.ui.setEditorText("");
				ctx.ui.notify(
					"Editor stashed. Press Alt+S with an empty editor to restore.",
					"info",
				);
				return;
			}

			if (stashedEditorText !== undefined) {
				ctx.ui.setEditorText(stashedEditorText);
				stashedEditorText = undefined;
				ctx.ui.notify("Editor stash restored", "info");
			} else {
				ctx.ui.notify("Editor stash is empty", "info");
			}
		},
	});

	pi.registerCommand("pi-ui", {
		description: "Enable the custom Pi UI, including the fixed editor",
		handler: async (_args, ctx) => {
			fixedEditorEnabled = true;
			installUi(ctx);
			ctx.ui.notify("Enhanced custom Pi UI enabled", "info");
		},
	});

	pi.registerCommand("pi-ui-safe", {
		description: "Use the custom Pi header/footer without the fixed-editor compositor",
		handler: async (_args, ctx) => {
			fixedEditorEnabled = false;
			installUi(ctx);
			ctx.ui.notify("Safe custom Pi UI enabled", "info");
		},
	});

	pi.registerCommand("pi-ui-builtin", {
		description: "Restore Pi's built-in header/footer for this session",
		handler: async (_args, ctx) => {
			stopWorkingTimer();
			ctx.ui.setWorkingMessage();
			editorGeneration++;
			teardownFixedEditor({ resetExtendedKeyboardModes: true });
			ctx.ui.setHeader(undefined);
			ctx.ui.setEditorComponent(undefined);
			ctx.ui.setFooter(undefined);
			ctx.ui.setWorkingIndicator();
			ctx.ui.notify("Built-in Pi UI restored", "info");
		},
	});
}
