// ABOUTME: Disk Cleanup viewer — opens a browser GUI for scanning, analyzing, and deleting junk files.
// ABOUTME: Provides /cleanup slash command and show_cleanup tool. AI analysis via Claude Agent SDK (OAuth).

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

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

interface CleanupResult {
	action: "done" | "closed";
	deletedCount?: number;
}

// ── Config ───────────────────────────────────────────────────────────

const PROTECTED_DIRS = new Set([
	"/System", "/Library", "/usr", "/bin", "/sbin",
	"/private/var/protected", "/private/etc", "/etc", "/cores",
]);

const MAX_DEPTH = Infinity;
const MAX_FILES = Infinity;

const CATEGORIES: Record<string, {
	label: string;
	extensions?: Set<string>;
	names?: Set<string>;
	directories?: Set<string>;
}> = {
	temp: {
		label: "Temporary Files",
		extensions: new Set([".tmp", ".temp", ".swp", ".swo", ".bak", ".old", ".log"]),
		names: new Set([".DS_Store", "Thumbs.db", "desktop.ini"]),
	},
	compiled: {
		label: "Compiled / Build Artifacts",
		extensions: new Set([".o", ".obj", ".pyc", ".pyo", ".class", ".dSYM"]),
		directories: new Set([
			"node_modules", "__pycache__", "dist", "build", ".next",
			"target", ".cache", ".parcel-cache", ".turbo",
		]),
	},
	archives: {
		label: "Archives",
		extensions: new Set([
			".zip", ".tar", ".tar.gz", ".tgz", ".rar", ".7z",
			".bz2", ".xz", ".gz", ".dmg", ".iso",
		]),
	},
};

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

function formatSize(bytes: number): string {
	if (bytes === 0) return "0 B";
	const units = ["B", "KB", "MB", "GB", "TB"];
	const i = Math.floor(Math.log(bytes) / Math.log(1024));
	return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + " " + units[i];
}

function isProtected(dirPath: string): boolean {
	const resolved = path.resolve(dirPath);
	for (const p of PROTECTED_DIRS) {
		if (resolved === p || resolved.startsWith(p + "/")) return true;
	}
	return false;
}

function categorizeEntry(name: string, isDirectory: boolean): string | null {
	if (isDirectory) {
		if (CATEGORIES.compiled.directories?.has(name)) return "compiled";
		return null;
	}
	const ext = path.extname(name).toLowerCase();
	const baseName = path.basename(name);
	const doubleExt = name.includes(".tar.") ? ".tar" + ext : ext;

	if (CATEGORIES.temp.names?.has(baseName)) return "temp";
	if (CATEGORIES.temp.extensions?.has(ext)) return "temp";
	if (CATEGORIES.compiled.extensions?.has(ext)) return "compiled";
	if (CATEGORIES.archives.extensions?.has(ext) || CATEGORIES.archives.extensions?.has(doubleExt)) return "archives";
	return null;
}

// ── Scanner ──────────────────────────────────────────────────────────

interface ScanFile {
	path: string;
	name: string;
	size: number;
	sizeFormatted: string;
	modified: string;
	isDirectory: boolean;
}

async function scanDirectory(rootDir: string, enabledCategories: string[]) {
	const results: Record<string, ScanFile[]> = { temp: [], compiled: [], archives: [] };
	let fileCount = 0;

	async function getDirSize(dir: string, depth: number): Promise<number> {
		if (depth > 5) return 0;
		let total = 0;
		try {
			const entries = await fsp.readdir(dir, { withFileTypes: true });
			for (const entry of entries) {
				const full = path.join(dir, entry.name);
				try {
					const stat = await fsp.lstat(full);
					if (stat.isSymbolicLink()) continue;
					if (stat.isDirectory()) total += await getDirSize(full, depth + 1);
					else total += stat.size;
				} catch { continue; }
			}
		} catch { /* permission denied */ }
		return total;
	}

	async function walk(dir: string, depth: number) {
		if (depth > MAX_DEPTH || fileCount >= MAX_FILES) return;
		if (isProtected(dir)) return;

		let entries;
		try { entries = await fsp.readdir(dir, { withFileTypes: true }); }
		catch { return; }

		for (const entry of entries) {
			if (fileCount >= MAX_FILES) return;
			const fullPath = path.join(dir, entry.name);

			try {
				const stat = await fsp.lstat(fullPath);
				if (stat.isSymbolicLink()) continue;
			} catch { continue; }

			const isDir = entry.isDirectory();
			const category = categorizeEntry(entry.name, isDir);

			if (category && enabledCategories.includes(category)) {
				try {
					let size = 0;
					let mtime: Date;
					if (isDir) {
						size = await getDirSize(fullPath, 0);
						const stat = await fsp.stat(fullPath);
						mtime = stat.mtime;
					} else {
						const stat = await fsp.stat(fullPath);
						size = stat.size;
						mtime = stat.mtime;
					}
					results[category].push({
						path: fullPath, name: entry.name, size,
						sizeFormatted: formatSize(size),
						modified: mtime.toISOString(), isDirectory: isDir,
					});
					fileCount++;
				} catch { /* stat failed */ }
				if (isDir) continue;
			}

			if (isDir) await walk(fullPath, depth + 1);
		}
	}

	await walk(rootDir, 0);

	for (const cat of Object.keys(results)) {
		results[cat].sort((a, b) => b.size - a.size);
	}

	return results;
}

// ── Deletion Log ─────────────────────────────────────────────────────

const DELETION_LOG = path.join(os.homedir(), ".cleanup-deletion-log.json");

async function appendDeletionLog(entry: Record<string, unknown>) {
	try { await fsp.appendFile(DELETION_LOG, JSON.stringify(entry) + "\n"); }
	catch { /* non-critical */ }
}

async function readDeletionLog(): Promise<Record<string, unknown>[]> {
	try {
		const data = await fsp.readFile(DELETION_LOG, "utf-8");
		return data.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)).reverse().slice(0, 100);
	} catch { return []; }
}

// ── AI Analysis (Agent SDK with OAuth) ───────────────────────────────

async function streamAIAnalysis(
	summary: Record<string, unknown>,
	sampleFiles: Record<string, unknown>,
	res: ServerResponse,
) {
	const prompt = `You are a disk cleanup advisor. Analyze these scan results and provide concise, actionable recommendations.

SCAN RESULTS:
${JSON.stringify(summary, null, 2)}

SAMPLE FILES (largest per category):
${JSON.stringify(sampleFiles, null, 2)}

Respond with:
1. A brief safety assessment for each category
2. Which files/directories are safe to delete and why
3. Any files that might need caution (e.g., archives that might contain important data)
4. Estimated space savings
5. A clear recommendation

Keep it concise and practical. No emojis. Use plain text formatting with dashes for lists.`;

	try {
		const { query } = await import("@anthropic-ai/claude-agent-sdk");
		const stream = query({
			prompt,
			options: {
				tools: [],
				maxTurns: 1,
				systemPrompt: "You are a concise disk cleanup advisor. Provide practical, safety-conscious recommendations for file deletion. Be direct and clear. No emojis. Use elegant, minimal formatting.",
			},
		});

		for await (const message of stream) {
			if ((message as any).type === "assistant") {
				for (const block of (message as any).message.content) {
					if (block.type === "text") {
						res.write(`data: ${JSON.stringify({ text: block.text })}\n\n`);
					}
				}
			} else if ((message as any).type === "result") {
				res.write(`data: ${JSON.stringify({ done: true, result: (message as any).result })}\n\n`);
			}
		}
	} catch (err: any) {
		res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
	}

	res.write("data: [DONE]\n\n");
	res.end();
}

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

function startCleanupServer(defaultDir: string): Promise<{
	port: number;
	server: Server;
	waitForResult: () => Promise<CleanupResult>;
}> {
	return new Promise((resolveSetup) => {
		let resolveResult: (result: CleanupResult) => void;
		let settled = false;
		const settle = (result: CleanupResult) => {
			if (settled) return;
			settled = true;
			resolveResult!(result);
		};
		const resultPromise = new Promise<CleanupResult>((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 main page
			if (req.method === "GET" && url.pathname === "/") {
				const port = (server.address() as any)?.port || 0;
				const html = generateCleanupViewerHTML({ port, defaultDir });
				res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
				res.end(html);
				return;
			}

			// Logo
			if (req.method === "GET" && url.pathname === "/logo.png") {
				try {
					const logoPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "assets", "agent-logo.png");
					const logoData = fs.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;
			}

			// Scan
			if (req.method === "POST" && url.pathname === "/scan") {
				let body = "";
				req.on("data", (chunk) => { body += chunk; });
				req.on("end", async () => {
					try {
						const data = JSON.parse(body);
						const dir = data.directory || "/Users/";
						const cats = data.categories || ["temp", "compiled", "archives"];

						try {
							const realDir = await fsp.realpath(dir);
							if (isProtected(realDir)) {
								res.writeHead(400, { "Content-Type": "application/json" });
								res.end(JSON.stringify({ error: "Cannot scan protected system directory." }));
								return;
							}
							const stat = await fsp.stat(realDir);
							if (!stat.isDirectory()) {
								res.writeHead(400, { "Content-Type": "application/json" });
								res.end(JSON.stringify({ error: "Path is not a directory." }));
								return;
							}
						} catch (err: any) {
							res.writeHead(400, { "Content-Type": "application/json" });
							res.end(JSON.stringify({ error: `Invalid path: ${err.message}` }));
							return;
						}

						const start = Date.now();
						const results = await scanDirectory(dir, cats);
						const elapsed = Date.now() - start;

						const summary: Record<string, any> = {};
						let totalFiles = 0;
						let totalSize = 0;

						for (const [cat, files] of Object.entries(results)) {
							const catSize = files.reduce((s, f) => s + f.size, 0);
							summary[cat] = { count: files.length, size: catSize, sizeFormatted: formatSize(catSize) };
							totalFiles += files.length;
							totalSize += catSize;
						}

						res.writeHead(200, { "Content-Type": "application/json" });
						res.end(JSON.stringify({
							results, summary, totalFiles, totalSize,
							totalSizeFormatted: formatSize(totalSize),
							scanTime: elapsed, directory: dir,
						}));
					} catch (err: any) {
						res.writeHead(500, { "Content-Type": "application/json" });
						res.end(JSON.stringify({ error: err.message }));
					}
				});
				return;
			}

			// Delete
			if (req.method === "POST" && url.pathname === "/delete") {
				let body = "";
				req.on("data", (chunk) => { body += chunk; });
				req.on("end", async () => {
					try {
						const data = JSON.parse(body);
						const files: string[] = data.files || [];
						if (files.length === 0) {
							res.writeHead(400, { "Content-Type": "application/json" });
							res.end(JSON.stringify({ error: "No files specified." }));
							return;
						}

						const results: any[] = [];
						for (const filePath of files) {
							try {
								const real = await fsp.realpath(filePath);
								if (isProtected(real)) {
									results.push({ path: filePath, success: false, error: "Protected path" });
									continue;
								}
								const stat = await fsp.stat(real);
								const size = stat.isDirectory()
									? await (async function getSize(d: string): Promise<number> {
										let t = 0;
										try {
											const ents = await fsp.readdir(d, { withFileTypes: true });
											for (const e of ents) {
												const fp = path.join(d, e.name);
												try {
													const s = await fsp.lstat(fp);
													if (s.isDirectory()) t += await getSize(fp);
													else t += s.size;
												} catch { /* skip */ }
											}
										} catch { /* skip */ }
										return t;
									})(real)
									: stat.size;

								if (stat.isDirectory()) {
									await fsp.rm(real, { recursive: true, force: true });
								} else {
									await fsp.unlink(real);
								}

								results.push({ path: filePath, success: true, size });
								await appendDeletionLog({ path: filePath, size, timestamp: new Date().toISOString(), success: true });
							} catch (err: any) {
								results.push({ path: filePath, success: false, error: err.message });
							}
						}

						const deleted = results.filter((r) => r.success);
						const freedBytes = deleted.reduce((s: number, r: any) => s + (r.size || 0), 0);

						res.writeHead(200, { "Content-Type": "application/json" });
						res.end(JSON.stringify({
							results, deletedCount: deleted.length,
							failedCount: results.length - deleted.length,
							freedBytes, freedFormatted: formatSize(freedBytes),
						}));
					} catch (err: any) {
						res.writeHead(500, { "Content-Type": "application/json" });
						res.end(JSON.stringify({ error: err.message }));
					}
				});
				return;
			}

			// AI Analyze
			if (req.method === "POST" && url.pathname === "/analyze") {
				let body = "";
				req.on("data", (chunk) => { body += chunk; });
				req.on("end", async () => {
					res.setHeader("Content-Type", "text/event-stream");
					res.setHeader("Cache-Control", "no-cache");
					res.setHeader("Connection", "keep-alive");
					res.flushHeaders();

					try {
						const data = JSON.parse(body);
						await streamAIAnalysis(data.summary, data.sampleFiles, res);
					} catch (err: any) {
						res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
						res.write("data: [DONE]\n\n");
						res.end();
					}
				});
				return;
			}

			// History
			if (req.method === "GET" && url.pathname === "/history") {
				const entries = await readDeletionLog();
				res.writeHead(200, { "Content-Type": "application/json" });
				res.end(JSON.stringify(entries));
				return;
			}

			// Result (done/close)
			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", deletedCount: data.deletedCount });
					} catch {
						res.writeHead(400, { "Content-Type": "application/json" });
						res.end(JSON.stringify({ error: "Invalid JSON" }));
					}
				});
				return;
			}

			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 { /* no browser */ }
		}
	}
}

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

const ShowCleanupParams = Type.Object({
	directory: Type.Optional(Type.String({ description: "Directory to scan (default: /Users/)" })),
});

// ── 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;
		}
	}

	async function launchCleanup(dir: string, ctx?: any): Promise<string> {
		cleanupServer();

		const { port, server, waitForResult } = await startCleanupServer(dir);
		activeServer = server;

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

		try {
			const result = await waitForResult();
			const msg = result.action === "done"
				? "Disk cleanup session complete."
				: "Disk cleanup viewer closed.";
			return msg;
		} finally {
			cleanupServer();
		}
	}

	// ── show_cleanup tool ────────────────────────────────────────────

	pi.registerTool({
		name: "show_cleanup",
		label: "Disk Cleanup",
		description:
			"Open a disk cleanup viewer in the browser. " +
			"Scans for temporary files, compiled artifacts, and archives. " +
			"Includes AI-powered analysis via Claude Agent SDK. " +
			"User can select and delete files with confirmation.",
		parameters: ShowCleanupParams,

		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			const { directory } = params as { directory?: string };
			const dir = directory || "/Users/";
			const msg = await launchCleanup(dir, ctx);
			return { content: [{ type: "text" as const, text: msg }] };
		},

		renderCall(args, theme) {
			const dir = (args as any).directory || "~";
			const text =
				theme.fg("toolTitle", theme.bold("show_cleanup ")) +
				theme.fg("accent", dir);
			return new Text(outputLine(theme, "accent", text), 0, 0);
		},

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

	// ── /cleanup command ─────────────────────────────────────────────

	pi.registerCommand("cleanup", {
		description: "Open the disk cleanup viewer to scan and delete junk files",
		handler: async (args, ctx) => {
			if (!ctx.hasUI) {
				ctx.ui.notify("/cleanup requires interactive mode", "error");
				return;
			}

			const dir = args.trim() || "/Users/";
			const msg = await launchCleanup(dir, ctx);
			ctx.ui.notify(msg, "info");
		},
	});

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

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

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