// ABOUTME: Task Board Viewer — opens a GUI browser window showing a live Kanban board of agent work.
// ABOUTME: Polls Commander MCP tools for tasks, agents, messages, and groups. Auto-refreshes every 3 seconds.

import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
import { outputLine } from "./lib/output-box.ts";
import { applyExtensionDefaults } from "./lib/themeMap.ts";
import { generateBoardViewerHTML } from "./lib/board-viewer-html.ts";
import { registerActiveViewer, clearActiveViewer, notifyViewerOpen } from "./lib/viewer-session.ts";

// ── Types ────────────────────────────────────────────────────────────

interface BoardResult {
	action: "closed";
}

interface BoardData {
	tasks: any[];
	agents: any[];
	messages: any[];
	groups: any[];
	readyTasks: any[];
	connected: boolean;
	timestamp: string;
	error?: string;
	localMode?: boolean;
	localTitle?: string;
}

// ── Commander Data Helpers ───────────────────────────────────────────

/**
 * Call a Commander MCP tool via the global client set by commander-mcp.ts.
 * Returns the parsed result or null on failure.
 */
async function callCommander(toolName: string, params: Record<string, unknown>): Promise<any> {
	const g = globalThis as any;
	const client = g.__piCommanderClient;
	if (!client) return null;

	try {
		const result = await client.callTool(toolName, params, 8000);
		// MCP results come as { content: [{ type: "text", text: "..." }] }
		if (result?.content?.[0]?.text) {
			try {
				return JSON.parse(result.content[0].text);
			} catch {
				return result.content[0].text;
			}
		}
		return result;
	} catch {
		return null;
	}
}

/**
 * Read local tasks from the tasks extension (globalThis.__piTaskList).
 * Always available regardless of Commander status.
 */
function getLocalTasks(): { tasks: any[]; title?: string } {
	const g = globalThis as any;
	const taskList = g.__piTaskList as { tasks: { id: number; text: string; status: string }[]; title?: string; remaining: number; total: number } | undefined;
	const now = new Date().toISOString();
	const statusMap: Record<string, string> = { idle: "pending", inprogress: "working", done: "completed" };

	const tasks = (taskList?.tasks || []).map((t) => ({
		task_id: t.id,
		description: t.text,
		status: statusMap[t.status] || t.status,
		created_at: now,
		updated_at: now,
	}));

	return { tasks, title: taskList?.title };
}

/**
 * Gather board data — always local-first.
 * Local tasks are the primary data source. Commander data is layered in when available.
 */
async function gatherBoardData(): Promise<BoardData> {
	const g = globalThis as any;
	const local = getLocalTasks();

	// Always return local tasks — this is the local-first board
	return {
		tasks: local.tasks,
		agents: [],
		messages: [],
		groups: [],
		readyTasks: [],
		connected: false,
		localMode: true,
		localTitle: local.title,
		timestamp: new Date().toISOString(),
	};
}

// ── HTTP Server ──────────────────────────────────────────────────────

function startBoardServer(
	title: string,
): Promise<{ port: number; server: Server; waitForResult: () => Promise<BoardResult> }> {
	return new Promise((resolveSetup) => {
		let resolveResult: (result: BoardResult) => void;
		let settled = false;
		const settle = (result: BoardResult) => {
			if (settled) return;
			settled = true;
			resolveResult!(result);
		};
		const resultPromise = new Promise<BoardResult>((res) => {
			resolveResult = res;
		});

		const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
			res.setHeader("Access-Control-Allow-Origin", "*");
			res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
			res.setHeader("Access-Control-Allow-Headers", "Content-Type");

			if (req.method === "OPTIONS") {
				res.writeHead(204);
				res.end();
				return;
			}

			const url = new URL(req.url || "/", `http://localhost`);

			// Serve the main HTML page
			if (req.method === "GET" && url.pathname === "/") {
				const port = (server.address() as any)?.port || 0;
				const html = generateBoardViewerHTML({ title, port });
				res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
				res.end(html);
				return;
			}

			// Serve the logo image
			if (req.method === "GET" && url.pathname === "/logo.png") {
				try {
					const logoPath = join(dirname(fileURLToPath(import.meta.url)), "assets", "agent-logo.png");
					const logoData = readFileSync(logoPath);
					res.writeHead(200, { "Content-Type": "image/png", "Cache-Control": "public, max-age=3600" });
					res.end(logoData);
				} catch {
					res.writeHead(404);
					res.end();
				}
				return;
			}

			// ── Main data endpoint ──────────────────────────────
			if (req.method === "GET" && url.pathname === "/api/board-data") {
				try {
					const data = await gatherBoardData();
					res.writeHead(200, {
						"Content-Type": "application/json",
						"Cache-Control": "no-cache",
					});
					res.end(JSON.stringify(data));
				} catch (err: any) {
					res.writeHead(500, { "Content-Type": "application/json" });
					res.end(JSON.stringify({
						tasks: [], agents: [], messages: [], groups: [], readyTasks: [],
						connected: false,
						timestamp: new Date().toISOString(),
						error: err.message,
					}));
				}
				return;
			}

			// ── Close the viewer ────────────────────────────────
			if (req.method === "POST" && url.pathname === "/result") {
				let body = "";
				req.on("data", (chunk: string) => { body += chunk; });
				req.on("end", () => {
					res.writeHead(200, { "Content-Type": "application/json" });
					res.end(JSON.stringify({ ok: true }));
					settle({ action: "closed" });
				});
				return;
			}

			// 404
			res.writeHead(404);
			res.end("Not found");
		});

		server.on("close", () => {
			settle({ action: "closed" });
		});

		server.listen(0, "127.0.0.1", () => {
			const addr = server.address() as any;
			resolveSetup({
				port: addr.port,
				server,
				waitForResult: () => resultPromise,
			});
		});
	});
}

function openBrowser(url: string): void {
	try {
		execSync(`open "${url}"`, { stdio: "ignore" });
	} catch {
		try {
			execSync(`xdg-open "${url}"`, { stdio: "ignore" });
		} catch {
			try {
				execSync(`start "${url}"`, { stdio: "ignore" });
			} catch {}
		}
	}
}

// ── Tool Parameters ──────────────────────────────────────────────────

const ShowBoardParams = Type.Object({
	title: Type.Optional(Type.String({ description: "Title for the board (default: 'Task Board')" })),
});

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

export default function (pi: ExtensionAPI) {
	let activeServer: Server | null = null;
	let activeSession: { kind: "board"; title: string; url: string; server: Server; onClose: () => void } | null = null;

	function cleanupServer() {
		const server = activeServer;
		activeServer = null;
		if (server) {
			try { server.close(); } catch {}
		}
		if (activeSession) {
			clearActiveViewer(activeSession);
			activeSession = null;
		}
	}

	// ── Core board launcher ──────────────────────────────────────────

	async function launchBoard(
		ctx: ExtensionContext,
		title: string,
	): Promise<string> {
		// Clean up any previous server
		cleanupServer();

		// Start server
		const { port, server } = await startBoardServer(title);
		activeServer = server;

		const url = `http://127.0.0.1:${port}`;
		activeSession = {
			kind: "board",
			title,
			url,
			server,
			onClose: () => {
				activeServer = null;
				activeSession = null;
			},
		};
		registerActiveViewer(activeSession);

		// Open the browser
		openBrowser(url);
		notifyViewerOpen(ctx, activeSession);

		return url;
	}

	// ── show_board tool ──────────────────────────────────────────────

	pi.registerTool({
		name: "show_board",
		label: "Show Board",
		description:
			"Open a live task board in the browser. Shows a Kanban-style view of local tasks " +
			"(Pending → Working → Completed → Failed). Auto-refreshes every 3 seconds.\n\n" +
			"The board runs as a lightweight background web server. Unlike other viewers, " +
			"it stays open and keeps refreshing — close the browser tab when done.\n\n" +
			"Shows local tasks from the tasks extension — no Commander required.",
		parameters: ShowBoardParams,

		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			const { title = "Task Board" } = params as { title?: string };

			const url = await launchBoard(ctx, title);

			return {
				content: [{
					type: "text" as const,
					text: `Task board opened at ${url}\n\nThe board auto-refreshes every 3 seconds. Close the browser tab when done.\n\nFeatures:\n- Kanban columns: Pending → Working → Completed → Failed\n- Agent chips: click to filter by agent\n- Activity feed: recent mailbox messages\n- Group progress: task group completion bars\n- Keyboard: R=refresh, Esc=clear filter`,
				}],
			};
		},

		renderCall(args, theme) {
			const titleArg = (args as any).title || "Task Board";
			const text =
				theme.fg("toolTitle", theme.bold("show_board ")) +
				theme.fg("accent", titleArg);
			return new Text(outputLine(theme, "accent", text), 0, 0);
		},

		renderResult(result, _options, theme) {
			const text = result.content[0];
			const firstLine = text?.type === "text" ? text.text.split("\n")[0] : "";
			return new Text(
				outputLine(theme, "success", firstLine),
				0, 0,
			);
		},
	});

	// ── /board command ───────────────────────────────────────────────

	pi.registerCommand("board", {
		description: "Open the live task board in the browser",
		handler: async (args, ctx) => {
			if (!ctx.hasUI) {
				ctx.ui.notify("/board requires interactive mode", "error");
				return;
			}

			const title = args.trim() || "Task Board";
			const url = await launchBoard(ctx, title);
			ctx.ui.notify(`Task board opened at ${url}`, "info");
		},
	});

	// ── Session lifecycle ────────────────────────────────────────────

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

	pi.on("session_shutdown", async () => {
		cleanupServer();
	});
}
