// ABOUTME: Completion Report Viewer — opens a GUI browser window showing work summary, file diffs, and rollback controls.
// ABOUTME: Gathers git diff data, renders interactive report with per-file rollback capability.

import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
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 { generateCompletionReportHTML, type ReportData, type ChangedFile } from "./lib/completion-report-html.ts";
import { createCompletionReportStandaloneExport, saveStandaloneExport } from "./lib/viewer-standalone-export.ts";
import { upsertPersistedReport } from "./lib/report-index.ts";
import { registerActiveViewer, clearActiveViewer, notifyViewerOpen } from "./lib/viewer-session.ts";

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

interface ReportResult {
	action: "done" | "rollback" | "closed";
	rolledBackFiles: string[];
}

// ── Git Helpers ──────────────────────────────────────────────────────

function execGit(cmd: string, cwd: string): string {
	try {
		return execSync(cmd, { cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }).trim();
	} catch {
		return "";
	}
}

function isGitRepo(cwd: string): boolean {
	return execGit("git rev-parse --is-inside-work-tree", cwd) === "true";
}

/**
 * Auto-detect the best base ref to diff against.
 * Priority:
 * 1. Explicit base_ref parameter
 * 2. If there are staged/unstaged changes, diff against HEAD
 * 3. HEAD~1 (last commit)
 */
function resolveBaseRef(cwd: string, explicitRef?: string): string {
	if (explicitRef) return explicitRef;

	// Check if there are uncommitted changes (staged or unstaged)
	const status = execGit("git status --porcelain", cwd);
	if (status.length > 0) {
		return "HEAD";
	}

	// Default to last commit
	return "HEAD~1";
}

/**
 * Parse `git diff --numstat` output into file stats.
 */
function parseNumstat(output: string): Array<{ path: string; additions: number; deletions: number }> {
	if (!output.trim()) return [];
	return output.split("\n").filter(Boolean).map((line) => {
		const [add, del, ...pathParts] = line.split("\t");
		const path = pathParts.join("\t"); // handle paths with tabs (renames show as old\tnew)
		return {
			path: path.replace(/.*=> /, "").replace(/[{}]/g, "").trim(),
			additions: add === "-" ? 0 : parseInt(add, 10),
			deletions: del === "-" ? 0 : parseInt(del, 10),
		};
	});
}

/**
 * Detect file status (added, modified, deleted, renamed).
 */
function getFileStatuses(cwd: string, baseRef: string): Map<string, { status: ChangedFile["status"]; oldPath?: string }> {
	const statusMap = new Map<string, { status: ChangedFile["status"]; oldPath?: string }>();

	// For uncommitted changes
	if (baseRef === "HEAD") {
		// Unstaged changes
		const unstaged = execGit("git diff --name-status", cwd);
		for (const line of unstaged.split("\n").filter(Boolean)) {
			const [status, ...parts] = line.split("\t");
			const filePath = parts[parts.length - 1];
			if (status.startsWith("R")) {
				statusMap.set(filePath, { status: "renamed", oldPath: parts[0] });
			} else if (status === "A") {
				statusMap.set(filePath, { status: "added" });
			} else if (status === "D") {
				statusMap.set(filePath, { status: "deleted" });
			} else {
				statusMap.set(filePath, { status: "modified" });
			}
		}

		// Staged changes
		const staged = execGit("git diff --cached --name-status", cwd);
		for (const line of staged.split("\n").filter(Boolean)) {
			const [status, ...parts] = line.split("\t");
			const filePath = parts[parts.length - 1];
			if (!statusMap.has(filePath)) {
				if (status.startsWith("R")) {
					statusMap.set(filePath, { status: "renamed", oldPath: parts[0] });
				} else if (status === "A") {
					statusMap.set(filePath, { status: "added" });
				} else if (status === "D") {
					statusMap.set(filePath, { status: "deleted" });
				} else {
					statusMap.set(filePath, { status: "modified" });
				}
			}
		}

		// Untracked files
		const untracked = execGit("git ls-files --others --exclude-standard", cwd);
		for (const filePath of untracked.split("\n").filter(Boolean)) {
			if (!statusMap.has(filePath)) {
				statusMap.set(filePath, { status: "added" });
			}
		}
	} else {
		// Committed changes
		const output = execGit(`git diff --name-status ${baseRef}`, cwd);
		for (const line of output.split("\n").filter(Boolean)) {
			const [status, ...parts] = line.split("\t");
			const filePath = parts[parts.length - 1];
			if (status.startsWith("R")) {
				statusMap.set(filePath, { status: "renamed", oldPath: parts[0] });
			} else if (status === "A") {
				statusMap.set(filePath, { status: "added" });
			} else if (status === "D") {
				statusMap.set(filePath, { status: "deleted" });
			} else {
				statusMap.set(filePath, { status: "modified" });
			}
		}
	}

	return statusMap;
}

/**
 * Gather all data needed for the completion report.
 */
function shouldSuppressReportFile(filePath: string): boolean {
	const normalized = filePath.replace(/\\/g, "/");
	return normalized.startsWith(".context/test-exports/") ||
		normalized.startsWith(".context/reports/") ||
		normalized === "agent/extensions/lib/marked.min.js";
}

function summarizeSuppressedFile(filePath: string): string {
	return [
		"@@ -0,0 +1,1 @@",
		`+Diff preview suppressed for generated or bulky artifact: ${filePath}`,
		"+Use copy/save/export or open the file directly if you need to inspect the full contents.",
	].join("\n");
}

function gatherReportData(cwd: string, title: string, summary: string, baseRef: string): ReportData {
	const resolvedRef = resolveBaseRef(cwd, baseRef);

	// Get diff stats
	let numstatOutput: string;
	if (resolvedRef === "HEAD") {
		// Combine staged + unstaged + untracked
		const unstaged = execGit("git diff --numstat", cwd);
		const staged = execGit("git diff --cached --numstat", cwd);
		numstatOutput = [unstaged, staged].filter(Boolean).join("\n");
	} else {
		numstatOutput = execGit(`git diff --numstat ${resolvedRef}`, cwd);
	}

	const stats = parseNumstat(numstatOutput);
	const statuses = getFileStatuses(cwd, resolvedRef);

	// Get per-file diffs
	const files: ChangedFile[] = [];

	for (const stat of stats) {
		const statusInfo = statuses.get(stat.path) || { status: "modified" as const };
		let diff: string;

		if (resolvedRef === "HEAD") {
			// Try unstaged first, then staged
			diff = execGit(`git diff -- "${stat.path}"`, cwd);
			if (!diff) {
				diff = execGit(`git diff --cached -- "${stat.path}"`, cwd);
			}
		} else {
			diff = execGit(`git diff ${resolvedRef} -- "${stat.path}"`, cwd);
		}

		files.push({
			path: stat.path,
			status: statusInfo.status,
			additions: stat.additions,
			deletions: stat.deletions,
			diff: shouldSuppressReportFile(stat.path) ? summarizeSuppressedFile(stat.path) : diff,
			oldPath: statusInfo.oldPath,
		});
	}

	// Also add untracked files if diffing against HEAD
	if (resolvedRef === "HEAD") {
		const untracked = execGit("git ls-files --others --exclude-standard", cwd);
		for (const filePath of untracked.split("\n").filter(Boolean)) {
			if (!files.some((f) => f.path === filePath)) {
				if (shouldSuppressReportFile(filePath)) {
					files.push({
						path: filePath,
						status: "added",
						additions: 1,
						deletions: 0,
						diff: summarizeSuppressedFile(filePath),
					});
					continue;
				}

				// Read file content to show as "all added"
				let content = "";
				try {
					content = readFileSync(join(cwd, filePath), "utf-8");
				} catch {
					content = "(binary or unreadable file)";
				}
				const lines = content.split("\n");
				const diff = lines.map((l) => `+${l}`).join("\n");
				files.push({
					path: filePath,
					status: "added",
					additions: lines.length,
					deletions: 0,
					diff: `@@ -0,0 +1,${lines.length} @@\n${diff}`,
				});
			}
		}
	}

	// Sort: modified first, then added, then deleted, then renamed
	const statusOrder: Record<string, number> = { modified: 0, added: 1, deleted: 2, renamed: 3 };
	files.sort((a, b) => (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9));

	const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
	const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);

	// Read task markdown if it exists
	let taskMarkdown: string | undefined;
	const todoPath = join(cwd, ".context", "todo.md");
	if (existsSync(todoPath)) {
		try {
			taskMarkdown = readFileSync(todoPath, "utf-8");
		} catch {}
	}

	return {
		title,
		summary,
		files,
		baseRef: resolvedRef,
		totalAdditions,
		totalDeletions,
		taskMarkdown,
	};
}

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

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

		const server = createServer((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 = generateCompletionReportHTML({ report, 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;
			}

			// Handle rollback
			if (req.method === "POST" && url.pathname === "/rollback") {
				let body = "";
				req.on("data", (chunk) => { body += chunk; });
				req.on("end", () => {
					try {
						const data = JSON.parse(body);
						const files: string[] = data.files || [];
						const baseRef: string = data.baseRef || "HEAD";
						const errors: string[] = [];

						for (const filePath of files) {
							try {
								if (baseRef === "HEAD") {
									// For uncommitted changes, checkout from HEAD
									execSync(`git checkout HEAD -- "${filePath}"`, { cwd, encoding: "utf-8" });
								} else {
									// For committed changes, checkout from the base ref
									execSync(`git checkout ${baseRef} -- "${filePath}"`, { cwd, encoding: "utf-8" });
								}
							} catch (err: any) {
								errors.push(`${filePath}: ${err.message}`);
							}
						}

						if (errors.length > 0) {
							res.writeHead(200, { "Content-Type": "application/json" });
							res.end(JSON.stringify({ ok: false, error: errors.join("; ") }));
						} else {
							res.writeHead(200, { "Content-Type": "application/json" });
							res.end(JSON.stringify({ ok: true }));
						}
					} catch {
						res.writeHead(400, { "Content-Type": "application/json" });
						res.end(JSON.stringify({ error: "Invalid JSON" }));
					}
				});
				return;
			}

			// Handle result (done)
			if (req.method === "POST" && url.pathname === "/result") {
				let body = "";
				req.on("data", (chunk) => { body += chunk; });
				req.on("end", () => {
					try {
						const data = JSON.parse(body);
						res.writeHead(200, { "Content-Type": "application/json" });
						res.end(JSON.stringify({ ok: true }));
						settle({
							action: data.action || "done",
							rolledBackFiles: data.rolledBackFiles || [],
						});
					} catch {
						res.writeHead(400, { "Content-Type": "application/json" });
						res.end(JSON.stringify({ error: "Invalid JSON" }));
					}
				});
				return;
			}

			// Handle save to desktop
			if (req.method === "POST" && url.pathname === "/save") {
				let body = "";
				req.on("data", (chunk) => { body += chunk; });
				req.on("end", () => {
					try {
						const data = JSON.parse(body);
						const desktop = join(homedir(), "Desktop");
						if (!existsSync(desktop)) mkdirSync(desktop, { recursive: true });
						const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
						const fileName = `report-${ts}.md`;
						const filePath = join(desktop, fileName);
						writeFileSync(filePath, data.content, "utf-8");
						res.writeHead(200, { "Content-Type": "application/json" });
						res.end(JSON.stringify({ ok: true, message: `Saved to ~/Desktop/${fileName}` }));
					} catch (err: any) {
						res.writeHead(500, { "Content-Type": "application/json" });
						res.end(JSON.stringify({ error: err.message }));
					}
				});
				return;
			}

			if (req.method === "POST" && url.pathname === "/export-standalone") {
				try {
					const html = createCompletionReportStandaloneExport(report);
					const saved = saveStandaloneExport({ filePrefix: "report-readonly", html });
					res.writeHead(200, { "Content-Type": "application/json" });
					res.end(JSON.stringify({ ok: true, message: `Standalone export saved to ~/Desktop/${saved.fileName}` }));
				} catch (err: any) {
					res.writeHead(500, { "Content-Type": "application/json" });
					res.end(JSON.stringify({ error: err.message }));
				}
				return;
			}

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

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

		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 ShowReportParams = Type.Object({
	title: Type.Optional(Type.String({ description: "Title for the report (default: 'Completion Report')" })),
	summary: Type.Optional(Type.String({ description: "Markdown summary of the work done" })),
	base_ref: Type.Optional(Type.String({ description: "Git ref to diff against (default: auto-detect — HEAD for uncommitted changes, HEAD~1 for committed)" })),
});

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

export default function (pi: ExtensionAPI) {
	let activeServer: Server | null = null;
	let activeSession: { kind: "report"; 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;
		}
	}

	// ── show_report tool ─────────────────────────────────────────────

	pi.registerTool({
		name: "show_report",
		label: "Show Report",
		description:
			"Open a completion report viewer in the browser. Shows a summary of work done, " +
			"files changed with unified diffs, and per-file rollback controls.\n\n" +
			"Automatically gathers git diff data from the working directory. " +
			"Includes task completion data from .context/todo.md if available.\n\n" +
			"The user can review diffs, rollback individual files or all changes, " +
			"copy the report, or save it to the desktop.",
		parameters: ShowReportParams,

		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			const {
				title = "Completion Report",
				summary = "",
				base_ref,
			} = params as { title?: string; summary?: string; base_ref?: string };

			const cwd = ctx.cwd || process.cwd();

			// Check if we're in a git repo
			if (!isGitRepo(cwd)) {
				return {
					content: [{ type: "text" as const, text: "Error: Not a git repository. The completion report requires git to gather file changes." }],
				};
			}

			// Gather report data
			const report = gatherReportData(cwd, title, summary, base_ref || "");

			if (report.files.length === 0) {
				return {
					content: [{ type: "text" as const, text: "No file changes detected. Nothing to report." }],
				};
			}

			// Clean up any previous server
			cleanupServer();

			// Start server and open browser
			const { port, server, waitForResult } = await startReportServer(report, cwd);
			activeServer = server;

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

			// Wait for user to close the report
			try {
				const result = await waitForResult();

				try {
					upsertPersistedReport({
						category: "completion",
						title,
						summary,
						sourcePath: join(cwd, ".context", "todo.md"),
						viewerPath: join(cwd, ".context", "todo.md"),
						viewerLabel: title,
						tags: ["completion", "git", "diff"],
						metadata: {
							baseRef: report.baseRef,
							fileCount: report.files.length,
							totalAdditions: report.totalAdditions,
							totalDeletions: report.totalDeletions,
							action: result.action,
							rolledBackFiles: result.rolledBackFiles,
						},
					});
				} catch {}

				const rolledBack = result.rolledBackFiles.length;
				const summary = rolledBack > 0
					? `Report closed. ${rolledBack} file${rolledBack > 1 ? "s" : ""} rolled back: ${result.rolledBackFiles.join(", ")}`
					: "Report closed. No files were rolled back.";

				return {
					content: [{ type: "text" as const, text: summary }],
					details: {
						action: result.action,
						rolledBackFiles: result.rolledBackFiles,
						totalFiles: report.files.length,
						totalAdditions: report.totalAdditions,
						totalDeletions: report.totalDeletions,
					},
				};
			} finally {
				cleanupServer();
			}
		},

		renderCall(args, theme) {
			const titleArg = (args as any).title || "Completion Report";
			const text =
				theme.fg("toolTitle", theme.bold("show_report ")) +
				theme.fg("success", titleArg);
			return new Text(outputLine(theme, "success", text), 0, 0);
		},

		renderResult(result, _options, theme) {
			const details = ((result as any).details || result) as any;
			if (!details || (details.totalFiles === undefined && !details.content)) {
				const text = result.content[0];
				return new Text(text?.type === "text" ? text.text : "", 0, 0);
			}

			const fileCount = details.totalFiles ?? 0;
			const totalAdditions = details.totalAdditions ?? 0;
			const totalDeletions = details.totalDeletions ?? 0;
			const rolledBack = (details.rolledBackFiles || []).length;

			let info = `${fileCount} files · +${totalAdditions} -${totalDeletions}`;
			if (rolledBack > 0) {
				info += ` · ${rolledBack} rolled back`;
				return new Text(
					outputLine(theme, "warning", `Report closed — ${info}`),
					0, 0,
				);
			}

			return new Text(
				outputLine(theme, "success", `Report closed — ${info}`),
				0, 0,
			);
		},
	});

	// ── /report command ──────────────────────────────────────────────

	pi.registerCommand("report", {
		description: "Open the completion report viewer for current git changes",
		handler: async (args, ctx) => {
			if (!ctx.hasUI) {
				ctx.ui.notify("/report requires interactive mode", "error");
				return;
			}

			const cwd = ctx.cwd || process.cwd();

			if (!isGitRepo(cwd)) {
				ctx.ui.notify("Not a git repository", "error");
				return;
			}

			// Parse optional base ref from args
			const baseRef = args.trim() || "";
			const report = gatherReportData(cwd, "Completion Report", "", baseRef);

			if (report.files.length === 0) {
				ctx.ui.notify("No file changes detected", "info");
				return;
			}

			cleanupServer();

			const { port, server, waitForResult } = await startReportServer(report, cwd);
			activeServer = server;

			const url = `http://127.0.0.1:${port}`;
			activeSession = {
				kind: "report",
				title: "Completion Report",
				url,
				server,
				onClose: () => {
					activeServer = null;
					activeSession = null;
				},
			};
			registerActiveViewer(activeSession);
			openBrowser(url);
			notifyViewerOpen(ctx, activeSession);

			const result = await waitForResult();
			cleanupServer();

			if (result.rolledBackFiles.length > 0) {
				ctx.ui.notify(
					`Report closed — ${result.rolledBackFiles.length} file(s) rolled back`,
					"info",
				);
			} else {
				ctx.ui.notify("Report closed", "info");
			}
		},
	});

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

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

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