/**
 * Observatory — Comprehensive observability dashboard for the Pi coding agent
 *
 * A completely passive extension that observes and records every event —
 * session starts, tool calls, agent turns, errors, context usage — into
 * an append-only JSONL log and rolling summary on disk.
 *
 * Surfaces:
 *   - Widget:  compact live dashboard with session stats, tool counts, cost
 *   - Footer:  single-line status bar with model, context %, event count
 *   - Overlay: full-screen dashboard (4 views) opened via /obs command
 *   - Export:  markdown report via /obs export
 *
 * Storage: .pi/observatory/events.jsonl, .pi/observatory/summary.json
 *
 * Commands:
 *   /obs or /observatory   — open dashboard overlay
 *   /obs clear             — wipe all observatory data
 *   /obs export            — write markdown report
 *   /obs agents            — overlay → agents view
 *   /obs tools             — overlay → tools view
 *   /obs timeline          — overlay → timeline view
 *
 * Usage: pi -e extensions/observatory.ts
 */

import type { AssistantMessage } from "@mariozechner/pi-ai";
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
import { Container, matchesKey, Text, truncateToWidth } from "@mariozechner/pi-tui";
import * as fs from "fs";
import * as path from "path";
import { applyExtensionDefaults } from "./themeMap.ts";

// ── Data Model ─────────────────────────────────────────────────────────

type ObsEventType =
	| "session_start"
	| "session_switch"
	| "session_fork"
	| "tool_call"
	| "tool_end"
	| "agent_start"
	| "agent_end"
	| "dispatch"
	| "error"
	| "blocked";

interface ObsEvent {
	ts: number;
	type: ObsEventType;
	sessionId: string;
	tool?: string;
	durationMs?: number;
	blocked?: boolean;
	blockReason?: string;
	agentTurn?: number;
	contextPercent?: number;
	tokensIn?: number;
	tokensOut?: number;
	cost?: number;
	error?: string;
	meta?: Record<string, any>;
}

interface ObsSummary {
	totalSessions: number;
	totalEvents: number;
	totalToolCalls: number;
	totalAgentTurns: number;
	totalErrors: number;
	totalBlocked: number;
	totalTokensIn: number;
	totalTokensOut: number;
	totalCost: number;
	toolCounts: Record<string, number>;
	toolDurations: Record<string, number[]>;
	toolBlocked: Record<string, number>;
	sessions: SessionSummary[];
	lastUpdated: number;
}

interface SessionSummary {
	sessionId: string;
	startedAt: number;
	endedAt?: number;
	model?: string;
	toolCalls: number;
	agentTurns: number;
	errors: number;
	blocked: number;
	tokensIn: number;
	tokensOut: number;
	cost: number;
	peakContextPercent: number;
}

// ── Helpers ────────────────────────────────────────────────────────────

function fmtDuration(ms: number): string {
	if (ms < 1000) return `${ms}ms`;
	const secs = Math.floor(ms / 1000);
	if (secs < 60) return `${secs}s`;
	const mins = Math.floor(secs / 60);
	const remSecs = secs % 60;
	if (mins < 60) return `${mins}m ${remSecs}s`;
	const hrs = Math.floor(mins / 60);
	const remMins = mins % 60;
	return `${hrs}h ${remMins}m`;
}

function fmtTokens(n: number): string {
	if (n < 1000) return `${n}`;
	return `${(n / 1000).toFixed(1)}k`;
}

function fmtCost(n: number): string {
	return `$${n.toFixed(4)}`;
}

function fmtTime(ts: number): string {
	const d = new Date(ts);
	return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}

function fmtDate(ts: number): string {
	const d = new Date(ts);
	return d.toLocaleDateString([], { year: "numeric", month: "2-digit", day: "2-digit" });
}

function shortId(): string {
	return Math.random().toString(36).slice(2, 8);
}

function emptySummary(): ObsSummary {
	return {
		totalSessions: 0,
		totalEvents: 0,
		totalToolCalls: 0,
		totalAgentTurns: 0,
		totalErrors: 0,
		totalBlocked: 0,
		totalTokensIn: 0,
		totalTokensOut: 0,
		totalCost: 0,
		toolCounts: {},
		toolDurations: {},
		toolBlocked: {},
		sessions: [],
		lastUpdated: Date.now(),
	};
}

function emptySessionSummary(sessionId: string, model?: string): SessionSummary {
	return {
		sessionId,
		startedAt: Date.now(),
		model,
		toolCalls: 0,
		agentTurns: 0,
		errors: 0,
		blocked: 0,
		tokensIn: 0,
		tokensOut: 0,
		cost: 0,
		peakContextPercent: 0,
	};
}

// ── Token/Cost computation (follows tool-counter.ts pattern) ───────────

function computeTokensAndCost(ctx: ExtensionContext): { tokensIn: number; tokensOut: number; cost: number } {
	let tokensIn = 0;
	let tokensOut = 0;
	let cost = 0;
	try {
		for (const entry of ctx.sessionManager.getBranch()) {
			if (entry.type === "message" && entry.message.role === "assistant") {
				const m = entry.message as AssistantMessage;
				tokensIn += m.usage.input;
				tokensOut += m.usage.output;
				cost += m.usage.cost.total;
			}
		}
	} catch {
		// Session not yet ready — return zeros
	}
	return { tokensIn, tokensOut, cost };
}

// ── Extension ──────────────────────────────────────────────────────────

export default function (pi: ExtensionAPI) {
	// ── State ──────────────────────────────────────────────────────────

	let obsDir = "";
	let eventsPath = "";
	let summaryPath = "";

	let sessionId = "";
	let sessionStartTs = 0;
	let summary: ObsSummary = emptySummary();
	let currentSession: SessionSummary | null = null;
	let sessionEvents: ObsEvent[] = [];

	let widgetCtx: ExtensionContext | null = null;

	// Tool call timing: Map<toolName, startTs[]> — FIFO queue per tool
	const toolTimers: Map<string, number[]> = new Map();

	// Agent turn tracking
	let agentTurnCount = 0;
	let agentTurnStartTs = 0;

	// Previous token/cost values for delta-based cross-session accumulation
	let prevTokensIn = 0;
	let prevTokensOut = 0;
	let prevCost = 0;

	// Debounced summary flush
	let summaryDirty = false;
	let flushTimer: ReturnType<typeof setInterval> | null = null;

	// ── Storage Layer ──────────────────────────────────────────────────

	function ensureDir() {
		try {
			if (!fs.existsSync(obsDir)) {
				fs.mkdirSync(obsDir, { recursive: true });
			}
		} catch {}
	}

	function loadSummary() {
		try {
			if (fs.existsSync(summaryPath)) {
				const raw = fs.readFileSync(summaryPath, "utf-8");
				const loaded = JSON.parse(raw) as ObsSummary;
				// Merge loaded with defaults for any missing fields
				summary = { ...emptySummary(), ...loaded };
				// Cap stored durations
				for (const tool of Object.keys(summary.toolDurations)) {
					if (summary.toolDurations[tool].length > 200) {
						summary.toolDurations[tool] = summary.toolDurations[tool].slice(-200);
					}
				}
			}
		} catch {
			summary = emptySummary();
		}
	}

	function saveSummary() {
		try {
			summary.lastUpdated = Date.now();
			fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), "utf-8");
			summaryDirty = false;
		} catch {}
	}

	function markDirty() {
		summaryDirty = true;
	}

	function startFlushTimer() {
		if (flushTimer) return;
		flushTimer = setInterval(() => {
			if (summaryDirty) saveSummary();
		}, 5000);
	}

	function stopFlushTimer() {
		if (flushTimer) {
			clearInterval(flushTimer);
			flushTimer = null;
		}
		if (summaryDirty) saveSummary();
	}

	function appendEvent(evt: ObsEvent) {
		try {
			ensureDir();
			fs.appendFileSync(eventsPath, JSON.stringify(evt) + "\n", "utf-8");
		} catch {}
		sessionEvents.push(evt);
		// Keep in-memory events bounded
		if (sessionEvents.length > 500) {
			sessionEvents = sessionEvents.slice(-400);
		}
		summary.totalEvents++;
		markDirty();
	}

	function clearAllData() {
		try {
			if (fs.existsSync(eventsPath)) fs.unlinkSync(eventsPath);
			if (fs.existsSync(summaryPath)) fs.unlinkSync(summaryPath);
		} catch {}
		summary = emptySummary();
		sessionEvents = [];
		toolTimers.clear();
		agentTurnCount = 0;
		currentSession = emptySessionSummary(sessionId, widgetCtx?.model?.id);
		summary.sessions.push(currentSession);
		summary.totalSessions = summary.sessions.length;
		markDirty();
	}

	// ── Context snapshot ───────────────────────────────────────────────

	function getContextPercent(): number {
		try {
			const usage = widgetCtx?.getContextUsage();
			return usage ? Math.round(usage.percent ?? 0) : 0;
		} catch {
			return 0;
		}
	}

	// ── Widget rendering ───────────────────────────────────────────────

	function updateWidget() {
		if (!widgetCtx) return;

		try {
			widgetCtx.ui.setWidget("observatory", (_tui, theme) => {
				const container = new Container();
				const borderFn = (s: string) => theme.fg("accent", s);

				container.addChild(new DynamicBorder(borderFn));

				const headerText = new Text("", 1, 0);
				container.addChild(headerText);

				const liveText = new Text("", 1, 0);
				container.addChild(liveText);

				const hintText = new Text("", 1, 0);
				container.addChild(hintText);

				container.addChild(new DynamicBorder(borderFn));

				return {
					render(width: number): string[] {
						const elapsed = fmtDuration(Date.now() - sessionStartTs);
						const toolCount = currentSession?.toolCalls || 0;
						const turns = currentSession?.agentTurns || 0;
						const ctxPct = getContextPercent();

						// Update peak context
						if (currentSession && ctxPct > currentSession.peakContextPercent) {
							currentSession.peakContextPercent = ctxPct;
						}

						// Line 1: session info
						const sid = theme.fg("accent", `#${sessionId}`);
						const line1 =
							theme.fg("dim", " SESSION ") + sid +
							theme.fg("dim", " │ ⏱ ") + theme.fg("success", elapsed) +
							theme.fg("dim", " │ 🔧 ") + theme.fg("accent", `${toolCount}`) + theme.fg("dim", " tools") +
							theme.fg("dim", " │ 🔄 ") + theme.fg("accent", `${turns}`) + theme.fg("dim", " turns") +
							theme.fg("dim", " │ 📊 ") + theme.fg(ctxPct > 80 ? "error" : ctxPct > 60 ? "warning" : "success", `${ctxPct}%`) + theme.fg("dim", " ctx");
						headerText.setText(truncateToWidth(line1, width - 4));

						// Line 2: tool frequency bar (top 8 tools)
						const tc = summary.toolCounts;
						const sorted = Object.entries(tc).sort((a, b) => b[1] - a[1]).slice(0, 8);
						const parts = sorted.map(
							([name, count]) =>
								theme.fg("accent", name) + theme.fg("dim", "(") + theme.fg("success", `${count}`) + theme.fg("dim", ")")
						);
						const liveLine = parts.length > 0
							? theme.fg("dim", " LIVE ") + theme.fg("success", "● ") + parts.join(theme.fg("dim", " "))
							: theme.fg("dim", " LIVE ● waiting for tools…");
						liveText.setText(truncateToWidth(liveLine, width - 4));

						// Line 3: hint + tokens + cost
						const { tokensIn, tokensOut, cost } = computeTokensAndCost(widgetCtx!);
						const hintLine =
							theme.fg("dim", " /obs") + theme.fg("muted", " — dashboard") +
							theme.fg("dim", " │ tokens: ") +
							theme.fg("success", fmtTokens(tokensIn)) + theme.fg("dim", " in · ") +
							theme.fg("accent", fmtTokens(tokensOut)) + theme.fg("dim", " out") +
							theme.fg("dim", " │ ") + theme.fg("warning", fmtCost(cost));
						hintText.setText(truncateToWidth(hintLine, width - 4));

						return container.render(width);
					},
					invalidate() {
						container.invalidate();
					},
				};
			});
		} catch {}
	}

	// ── Footer rendering ───────────────────────────────────────────────

	function updateFooter(ctx: ExtensionContext) {
		try {
			ctx.ui.setFooter((_tui, theme) => ({
				dispose: () => {},
				invalidate() {},
				render(width: number): string[] {
					const model = ctx.model?.id || "no-model";
					const ctxPct = getContextPercent();
					const evtCount = summary.totalEvents;

					const line =
						theme.fg("accent", " 🔭 Observatory") +
						theme.fg("dim", " │ ") + theme.fg("muted", model) +
						theme.fg("dim", " │ ctx: ") +
						theme.fg(ctxPct > 80 ? "error" : ctxPct > 60 ? "warning" : "success", `${ctxPct}%`) +
						theme.fg("dim", " │ ") + theme.fg("success", `${evtCount}`) + theme.fg("dim", " events") +
						theme.fg("dim", " │ /obs for dashboard ");

					return [truncateToWidth(line, width)];
				},
			}));
		} catch {}
	}

	// ── Dashboard Overlay ──────────────────────────────────────────────

	async function openOverlay(ctx: ExtensionContext, initialView: number = 0) {
		if (!ctx.hasUI) return;

		let currentView = initialView; // 0=Feed, 1=Agents, 2=Tools, 3=Timeline
		let scrollOffset = 0;

		const viewNames = ["1:Feed", "2:Agents", "3:Tools", "4:Timeline"];

		await ctx.ui.custom((_tui, theme, _kb, done) => {
			return {
				render(width: number): string[] {
					const lines: string[] = [];
					const innerW = width - 2;

					// ── Header ──
					lines.push("");
					const tabs = viewNames.map((name, i) =>
						i === currentView
							? theme.fg("accent", theme.bold(`[${name}]`))
							: theme.fg("dim", `[${name}]`)
					).join(" ");
					lines.push(truncateToWidth(
						" " + theme.fg("accent", theme.bold("🔭 Observatory Dashboard")) +
						" ".repeat(Math.max(1, innerW - 24 - viewNames.join(" ").length - 2)) +
						tabs,
						width,
					));
					lines.push(theme.fg("dim", "─".repeat(width)));

					// ── View content ──
					const contentLines = renderView(currentView, width, theme, scrollOffset);
					lines.push(...contentLines);

					// ── Footer controls ──
					lines.push("");
					lines.push(theme.fg("dim", "─".repeat(width)));
					lines.push(truncateToWidth(
						" " + theme.fg("dim", "1-4/Tab: views │ ↑↓/j/k: scroll │ PgUp/PgDn: page │ c: clear │ q/Esc: close"),
						width,
					));
					lines.push("");

					return lines;
				},
				handleInput(data: string) {
					if (matchesKey(data, "escape") || data === "q") {
						done(undefined);
						return;
					}
					if (data === "1") { currentView = 0; scrollOffset = 0; }
					else if (data === "2") { currentView = 1; scrollOffset = 0; }
					else if (data === "3") { currentView = 2; scrollOffset = 0; }
					else if (data === "4") { currentView = 3; scrollOffset = 0; }
					else if (data === "\t") { currentView = (currentView + 1) % 4; scrollOffset = 0; }
					else if (matchesKey(data, "up") || data === "k") { scrollOffset = Math.max(0, scrollOffset - 1); }
					else if (matchesKey(data, "down") || data === "j") { scrollOffset++; }
					else if (matchesKey(data, "pageUp")) { scrollOffset = Math.max(0, scrollOffset - 20); }
					else if (matchesKey(data, "pageDown")) { scrollOffset += 20; }
					else if (data === "c") {
						clearAllData();
						scrollOffset = 0;
					}
					_tui.requestRender();
				},
				invalidate() {},
			};
		}, {
			overlay: true,
			overlayOptions: { width: "90%", anchor: "center" },
		});
	}

	// ── View Renderers ─────────────────────────────────────────────────

	function renderView(view: number, width: number, theme: any, offset: number): string[] {
		switch (view) {
			case 0: return renderFeedView(width, theme, offset);
			case 1: return renderAgentsView(width, theme, offset);
			case 2: return renderToolsView(width, theme, offset);
			case 3: return renderTimelineView(width, theme, offset);
			default: return [];
		}
	}

	// ── View 1: Live Feed ──────────────────────────────────────────────

	function renderFeedView(width: number, theme: any, offset: number): string[] {
		const lines: string[] = [];
		const events = sessionEvents.slice(-100);

		if (events.length === 0) {
			lines.push("");
			lines.push(truncateToWidth("  " + theme.fg("dim", "No events yet. Start working and events will appear here."), width));
			return lines;
		}

		lines.push(truncateToWidth(
			"  " + theme.fg("muted", `Showing last ${events.length} events from session #${sessionId}`),
			width,
		));
		lines.push("");

		const iconMap: Record<string, string> = {
			session_start: "🚀", session_switch: "🔀", session_fork: "🔱",
			tool_call: "🔧", tool_end: "✅", agent_start: "🤖", agent_end: "🏁",
			dispatch: "📤", error: "❌", blocked: "🚫",
		};
		const colorMap: Record<string, string> = {
			session_start: "success", session_switch: "accent", session_fork: "accent",
			tool_call: "accent", tool_end: "success", agent_start: "warning", agent_end: "success",
			dispatch: "accent", error: "error", blocked: "error",
		};

		const visible = events.slice(offset);
		for (const evt of visible) {
			const time = theme.fg("dim", `[${fmtTime(evt.ts)}]`);
			const icon = iconMap[evt.type] || "●";
			const color = colorMap[evt.type] || "muted";
			let detail = "";

			if (evt.type === "tool_call" && evt.tool) {
				detail = theme.fg(color, `${evt.tool}`) +
					(evt.contextPercent !== undefined ? theme.fg("dim", ` ctx:${evt.contextPercent}%`) : "");
			} else if (evt.type === "tool_end" && evt.tool) {
				detail = theme.fg(color, `${evt.tool}`) +
					(evt.durationMs !== undefined ? theme.fg("dim", ` ${fmtDuration(evt.durationMs)}`) : "");
			} else if (evt.type === "agent_start") {
				detail = theme.fg(color, `turn #${evt.agentTurn || "?"}`) +
					(evt.contextPercent !== undefined ? theme.fg("dim", ` ctx:${evt.contextPercent}%`) : "");
			} else if (evt.type === "agent_end") {
				detail = theme.fg(color, `turn #${evt.agentTurn || "?"}`) +
					(evt.durationMs !== undefined ? theme.fg("dim", ` ${fmtDuration(evt.durationMs)}`) : "") +
					(evt.tokensIn !== undefined ? theme.fg("dim", ` tok:${fmtTokens(evt.tokensIn)}in`) : "") +
					(evt.cost !== undefined ? theme.fg("dim", ` ${fmtCost(evt.cost)}`) : "");
			} else if (evt.type === "error") {
				detail = theme.fg(color, evt.error || "unknown error");
			} else if (evt.type === "blocked") {
				detail = theme.fg(color, `${evt.tool || "?"}: ${evt.blockReason || "blocked"}`);
			} else {
				detail = theme.fg(color, evt.type);
			}

			lines.push(truncateToWidth(`  ${time} ${icon} ${detail}`, width));
		}

		return lines;
	}

	// ── View 2: Agent Performance ──────────────────────────────────────

	function renderAgentsView(width: number, theme: any, offset: number): string[] {
		const lines: string[] = [];

		// Cross-session summary
		lines.push(truncateToWidth(
			"  " + theme.fg("accent", theme.bold("Cross-Session Summary")),
			width,
		));
		lines.push(truncateToWidth(
			"  " +
			theme.fg("muted", "Total turns: ") + theme.fg("success", `${summary.totalAgentTurns}`) +
			theme.fg("dim", " │ ") +
			theme.fg("muted", "Total sessions: ") + theme.fg("success", `${summary.totalSessions}`) +
			theme.fg("dim", " │ ") +
			theme.fg("muted", "Total cost: ") + theme.fg("warning", fmtCost(summary.totalCost)),
			width,
		));
		lines.push("");

		// Current session agent turns
		lines.push(truncateToWidth(
			"  " + theme.fg("accent", theme.bold("Current Session Turns")) +
			theme.fg("dim", ` (#${sessionId})`),
			width,
		));
		lines.push("");

		// Collect agent turn events
		const turnStarts: ObsEvent[] = sessionEvents.filter(e => e.type === "agent_start");
		const turnEnds: ObsEvent[] = sessionEvents.filter(e => e.type === "agent_end");

		if (turnStarts.length === 0) {
			lines.push(truncateToWidth("  " + theme.fg("dim", "No agent turns recorded yet."), width));
			return lines;
		}

		// Table header
		const hdr =
			theme.fg("accent", "  Turn") +
			theme.fg("accent", "  │ Duration ") +
			theme.fg("accent", " │ Tools  ") +
			theme.fg("accent", " │ Context  ") +
			theme.fg("accent", " │ Tokens In ") +
			theme.fg("accent", " │ Cost     ");
		lines.push(truncateToWidth(hdr, width));
		lines.push(truncateToWidth("  " + theme.fg("dim", "─".repeat(Math.min(70, width - 4))), width));

		// Build turn rows — pair starts with ends
		const turnRows: string[] = [];
		let prevCtx = 0;
		for (let i = 0; i < turnStarts.length; i++) {
			const start = turnStarts[i];
			const end = turnEnds[i]; // May be undefined if still running
			const turn = start.agentTurn || (i + 1);
			const dur = end?.durationMs !== undefined ? fmtDuration(end.durationMs) : "running…";

			// Count tools between this turn start and end (or now)
			const startTs = start.ts;
			const endTs = end?.ts || Date.now();
			const toolsInTurn = sessionEvents.filter(
				e => e.type === "tool_call" && e.ts >= startTs && e.ts <= endTs
			).length;

			const ctxNow = end?.contextPercent ?? start.contextPercent ?? 0;
			const ctxDelta = ctxNow - prevCtx;
			prevCtx = ctxNow;

			const tokIn = end?.tokensIn ?? 0;
			const cost = end?.cost ?? 0;

			const row =
				theme.fg("success", `  #${String(turn).padStart(2)}`) +
				theme.fg("dim", "  │ ") + theme.fg("muted", dur.padEnd(9)) +
				theme.fg("dim", " │ ") + theme.fg("accent", String(toolsInTurn).padStart(5)) + "  " +
				theme.fg("dim", " │ ") + theme.fg(ctxDelta > 15 ? "warning" : "muted", `${ctxDelta >= 0 ? "+" : ""}${ctxDelta}%`.padStart(7)) + "  " +
				theme.fg("dim", " │ ") + theme.fg("muted", fmtTokens(tokIn).padStart(9)) + " " +
				theme.fg("dim", " │ ") + theme.fg("warning", fmtCost(cost).padStart(8));
			turnRows.push(row);
		}

		const visible = turnRows.slice(offset);
		for (const row of visible) {
			lines.push(truncateToWidth(row, width));
		}

		return lines;
	}

	// ── View 3: Tool Analytics ─────────────────────────────────────────

	function renderToolsView(width: number, theme: any, offset: number): string[] {
		const lines: string[] = [];

		lines.push(truncateToWidth(
			"  " + theme.fg("accent", theme.bold("Tool Analytics")) +
			theme.fg("dim", " (all sessions)"),
			width,
		));
		lines.push("");

		const tc = summary.toolCounts;
		const entries = Object.entries(tc).sort((a, b) => b[1] - a[1]);

		if (entries.length === 0) {
			lines.push(truncateToWidth("  " + theme.fg("dim", "No tool usage recorded yet."), width));
			return lines;
		}

		const maxCount = entries[0][1];
		const barWidth = Math.min(25, Math.floor(width * 0.3));
		const blocks = "▏▎▍▌▋▊▉█";

		const visible = entries.slice(offset);
		for (const [toolName, count] of visible) {
			// Bar
			const ratio = maxCount > 0 ? count / maxCount : 0;
			const fullBlocks = Math.floor(ratio * barWidth);
			const remainder = (ratio * barWidth) - fullBlocks;
			const partialIdx = Math.floor(remainder * 8);
			let bar = "█".repeat(fullBlocks);
			if (fullBlocks < barWidth && partialIdx > 0) {
				bar += blocks[partialIdx - 1];
			}

			// Duration stats
			const durations = summary.toolDurations[toolName] || [];
			let avgDur = "—";
			let minDur = "—";
			let maxDur = "—";
			if (durations.length > 0) {
				const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
				const min = Math.min(...durations);
				const max = Math.max(...durations);
				avgDur = fmtDuration(Math.round(avg));
				minDur = fmtDuration(min);
				maxDur = fmtDuration(max);
			}

			const blocked = summary.toolBlocked[toolName] || 0;

			const namePad = toolName.padEnd(12);
			const line =
				"  " +
				theme.fg("accent", namePad) + " " +
				theme.fg("success", bar.padEnd(barWidth)) + " " +
				theme.fg("muted", String(count).padStart(5)) +
				theme.fg("dim", "  avg: ") + theme.fg("muted", avgDur.padEnd(6)) +
				theme.fg("dim", "  min: ") + theme.fg("muted", minDur.padEnd(6)) +
				theme.fg("dim", "  max: ") + theme.fg("muted", maxDur.padEnd(6)) +
				(blocked > 0
					? theme.fg("dim", "  blocked: ") + theme.fg("error", `${blocked}`)
					: theme.fg("dim", "  blocked: ") + theme.fg("muted", "0"));

			lines.push(truncateToWidth(line, width));
		}

		return lines;
	}

	// ── View 4: Timeline ───────────────────────────────────────────────

	function renderTimelineView(width: number, theme: any, offset: number): string[] {
		const lines: string[] = [];

		lines.push(truncateToWidth(
			"  " + theme.fg("accent", theme.bold("Session Timeline")) +
			theme.fg("dim", ` (${summary.sessions.length} sessions)`),
			width,
		));
		lines.push("");

		if (summary.sessions.length === 0) {
			lines.push(truncateToWidth("  " + theme.fg("dim", "No sessions recorded yet."), width));
			return lines;
		}

		// Show sessions newest first
		const sessionsDesc = [...summary.sessions].reverse();
		const cardLines: string[] = [];

		for (const sess of sessionsDesc) {
			const dur = sess.endedAt
				? fmtDuration(sess.endedAt - sess.startedAt)
				: fmtDuration(Date.now() - sess.startedAt);
			const date = fmtDate(sess.startedAt);
			const time = fmtTime(sess.startedAt);
			const isCurrent = sess.sessionId === sessionId;

			const topBorder = theme.fg("dim", "  ┌─ ") +
				theme.fg("accent", `Session ${sess.sessionId}`) +
				(isCurrent ? theme.fg("success", " (current)") : "") +
				theme.fg("dim", ` ─ ${date} ${time} `) +
				theme.fg("dim", "─".repeat(Math.max(0, width - 50)));
			cardLines.push(truncateToWidth(topBorder, width));

			const line1 =
				theme.fg("dim", "  │ ") +
				theme.fg("muted", "Duration: ") + theme.fg("success", dur) +
				theme.fg("dim", " │ ") +
				theme.fg("muted", "Model: ") + theme.fg("accent", sess.model || "?") +
				theme.fg("dim", " │ ") +
				theme.fg("muted", "Tools: ") + theme.fg("success", `${sess.toolCalls}`) +
				theme.fg("dim", " │ ") +
				theme.fg("muted", "Turns: ") + theme.fg("success", `${sess.agentTurns}`);
			cardLines.push(truncateToWidth(line1, width));

			const line2 =
				theme.fg("dim", "  │ ") +
				theme.fg("muted", "Cost: ") + theme.fg("warning", fmtCost(sess.cost)) +
				theme.fg("dim", " │ ") +
				theme.fg("muted", "Peak Context: ") + theme.fg(
					sess.peakContextPercent > 80 ? "error" : sess.peakContextPercent > 60 ? "warning" : "success",
					`${sess.peakContextPercent}%`,
				) +
				theme.fg("dim", " │ ") +
				theme.fg("muted", "Errors: ") + theme.fg(sess.errors > 0 ? "error" : "muted", `${sess.errors}`) +
				theme.fg("dim", " │ ") +
				theme.fg("muted", "Tokens: ") +
				theme.fg("success", fmtTokens(sess.tokensIn)) + theme.fg("dim", " in / ") +
				theme.fg("accent", fmtTokens(sess.tokensOut)) + theme.fg("dim", " out");
			cardLines.push(truncateToWidth(line2, width));

			const botBorder = theme.fg("dim", "  └" + "─".repeat(Math.max(0, width - 5)) + "┘");
			cardLines.push(truncateToWidth(botBorder, width));
			cardLines.push("");
		}

		const visible = cardLines.slice(offset);
		lines.push(...visible);

		return lines;
	}

	// ── Export Report ──────────────────────────────────────────────────

	function exportReport(ctx: ExtensionContext) {
		try {
			ensureDir();
			const { tokensIn, tokensOut, cost } = computeTokensAndCost(ctx);
			const elapsed = fmtDuration(Date.now() - sessionStartTs);
			const model = ctx.model?.id || "unknown";

			const toolRows = Object.entries(summary.toolCounts)
				.sort((a, b) => b[1] - a[1])
				.map(([name, count]) => {
					const durations = summary.toolDurations[name] || [];
					const avg = durations.length > 0
						? fmtDuration(Math.round(durations.reduce((a, b) => a + b, 0) / durations.length))
						: "—";
					const blocked = summary.toolBlocked[name] || 0;
					return `| ${name} | ${count} | ${avg} | ${blocked} |`;
				})
				.join("\n");

			const sessionRows = summary.sessions.map(sess => {
				const dur = sess.endedAt
					? fmtDuration(sess.endedAt - sess.startedAt)
					: fmtDuration(Date.now() - sess.startedAt);
				return `| ${sess.sessionId} | ${fmtDate(sess.startedAt)} | ${dur} | ${sess.model || "?"} | ${sess.toolCalls} | ${sess.agentTurns} | ${fmtCost(sess.cost)} | ${sess.peakContextPercent}% |`;
			}).join("\n");

			const report = `# 🔭 Observatory Report
Generated: ${new Date().toISOString()}

## Current Session
- **ID:** ${sessionId}
- **Duration:** ${elapsed}
- **Model:** ${model}
- **Tool Calls:** ${currentSession?.toolCalls || 0}
- **Agent Turns:** ${currentSession?.agentTurns || 0}
- **Tokens:** ${fmtTokens(tokensIn)} in / ${fmtTokens(tokensOut)} out
- **Cost:** ${fmtCost(cost)}
- **Peak Context:** ${currentSession?.peakContextPercent || 0}%

## Tool Usage
| Tool | Count | Avg Duration | Blocked |
|------|-------|-------------|---------|
${toolRows || "| (none) | — | — | — |"}

## Cross-Session Summary
- **Total Sessions:** ${summary.totalSessions}
- **Total Events:** ${summary.totalEvents}
- **Total Tool Calls:** ${summary.totalToolCalls}
- **Total Agent Turns:** ${summary.totalAgentTurns}
- **Total Errors:** ${summary.totalErrors}
- **Total Blocked:** ${summary.totalBlocked}
- **Total Tokens In:** ${fmtTokens(summary.totalTokensIn)}
- **Total Tokens Out:** ${fmtTokens(summary.totalTokensOut)}
- **Total Cost:** ${fmtCost(summary.totalCost)}

## Session History
| Session | Date | Duration | Model | Tools | Turns | Cost | Peak Ctx |
|---------|------|----------|-------|-------|-------|------|----------|
${sessionRows || "| (none) | — | — | — | — | — | — | — |"}
`;

			const reportPath = path.join(obsDir, "report.md");
			fs.writeFileSync(reportPath, report, "utf-8");
			ctx.ui.notify(`📄 Report exported to .pi/observatory/report.md`, "info");
		} catch (err) {
			ctx.ui.notify(`Export failed: ${err instanceof Error ? err.message : String(err)}`, "error");
		}
	}

	// ── Commands ───────────────────────────────────────────────────────

	pi.registerCommand("obs", {
		description: "Open Observatory dashboard. Args: clear | export | agents | tools | timeline",
		handler: async (args, ctx) => {
			widgetCtx = ctx;
			const arg = (args || "").trim().toLowerCase();

			if (arg === "clear") {
				clearAllData();
				ctx.ui.notify("🔭 Observatory: All data cleared.", "info");
				updateWidget();
				return;
			}
			if (arg === "export") {
				exportReport(ctx);
				return;
			}

			let view = 0;
			if (arg === "agents") view = 1;
			else if (arg === "tools") view = 2;
			else if (arg === "timeline") view = 3;

			await openOverlay(ctx, view);
		},
	});

	pi.registerCommand("observatory", {
		description: "Open Observatory dashboard (alias for /obs)",
		handler: async (_args, ctx) => {
			widgetCtx = ctx;
			await openOverlay(ctx, 0);
		},
	});

	// ── Event Handlers ─────────────────────────────────────────────────

	pi.on("session_start", async (_event, ctx) => {
		try {
			applyExtensionDefaults(import.meta.url, ctx);

			widgetCtx = ctx;
			sessionId = shortId();
			sessionStartTs = Date.now();
			agentTurnCount = 0;
			sessionEvents = [];
			toolTimers.clear();
			prevTokensIn = 0;
			prevTokensOut = 0;
			prevCost = 0;

			// Initialize storage
			obsDir = path.join(ctx.cwd, ".pi", "observatory");
			eventsPath = path.join(obsDir, "events.jsonl");
			summaryPath = path.join(obsDir, "summary.json");
			ensureDir();
			loadSummary();

			// Create session summary
			currentSession = emptySessionSummary(sessionId, ctx.model?.id);
			summary.sessions.push(currentSession);
			summary.totalSessions = summary.sessions.length;

			// Log event
			appendEvent({
				ts: Date.now(),
				type: "session_start",
				sessionId,
				contextPercent: getContextPercent(),
				meta: { model: ctx.model?.id },
			});

			// Start flush timer
			startFlushTimer();

			// Set up UI
			updateWidget();
			updateFooter(ctx);
		} catch {}
	});

	pi.on("tool_call", async (event, _ctx) => {
		try {
			const toolName = event.toolName;

			// Record start time (FIFO queue per tool name)
			if (!toolTimers.has(toolName)) {
				toolTimers.set(toolName, []);
			}
			toolTimers.get(toolName)!.push(Date.now());

			// Update counts
			summary.toolCounts[toolName] = (summary.toolCounts[toolName] || 0) + 1;
			summary.totalToolCalls++;
			if (currentSession) currentSession.toolCalls++;

			// Log event
			appendEvent({
				ts: Date.now(),
				type: "tool_call",
				sessionId,
				tool: toolName,
				contextPercent: getContextPercent(),
			});

			updateWidget();
		} catch {}

		// IMPORTANT: Never block — completely passive
		return undefined;
	});

	pi.on("tool_execution_end", async (event) => {
		try {
			const toolName = event.toolName;

			// Calculate duration (pop earliest timer for this tool)
			let durationMs: number | undefined;
			const timers = toolTimers.get(toolName);
			if (timers && timers.length > 0) {
				const startTs = timers.shift()!;
				durationMs = Date.now() - startTs;

				// Store duration (cap at 200 per tool)
				if (!summary.toolDurations[toolName]) {
					summary.toolDurations[toolName] = [];
				}
				summary.toolDurations[toolName].push(durationMs);
				if (summary.toolDurations[toolName].length > 200) {
					summary.toolDurations[toolName] = summary.toolDurations[toolName].slice(-200);
				}
			}

			// Log event
			appendEvent({
				ts: Date.now(),
				type: "tool_end",
				sessionId,
				tool: toolName,
				durationMs,
			});

			updateWidget();
		} catch {}
	});

	pi.on("before_agent_start", async (_event, _ctx) => {
		try {
			agentTurnCount++;
			agentTurnStartTs = Date.now();

			summary.totalAgentTurns++;
			if (currentSession) currentSession.agentTurns++;

			const ctxPct = getContextPercent();

			appendEvent({
				ts: Date.now(),
				type: "agent_start",
				sessionId,
				agentTurn: agentTurnCount,
				contextPercent: ctxPct,
			});

			updateWidget();
		} catch {}

		// Don't modify the system prompt — return undefined
		return undefined;
	});

	pi.on("agent_end", async (_event, ctx) => {
		try {
			widgetCtx = ctx;
			const turnDuration = Date.now() - agentTurnStartTs;
			const { tokensIn, tokensOut, cost } = computeTokensAndCost(ctx);
			const ctxPct = getContextPercent();

			// Update session summary
			if (currentSession) {
				currentSession.tokensIn = tokensIn;
				currentSession.tokensOut = tokensOut;
				currentSession.cost = cost;
				if (ctxPct > currentSession.peakContextPercent) {
					currentSession.peakContextPercent = ctxPct;
				}
			}

			// Update global summary totals using deltas (so cross-session values accumulate)
			const deltaIn = tokensIn - prevTokensIn;
			const deltaOut = tokensOut - prevTokensOut;
			const deltaCost = cost - prevCost;
			summary.totalTokensIn += deltaIn;
			summary.totalTokensOut += deltaOut;
			summary.totalCost += deltaCost;
			prevTokensIn = tokensIn;
			prevTokensOut = tokensOut;
			prevCost = cost;

			appendEvent({
				ts: Date.now(),
				type: "agent_end",
				sessionId,
				agentTurn: agentTurnCount,
				durationMs: turnDuration,
				contextPercent: ctxPct,
				tokensIn,
				tokensOut,
				cost,
			});

			// Flush summary on every agent end
			markDirty();
			saveSummary();

			updateWidget();
			updateFooter(ctx);
		} catch {}
	});

}
