/**
 * WezTerm Theme Sync Extension
 *
 * Syncs pi theme with WezTerm terminal colors on startup.
 *
 * How it works:
 *   1. Finds the WezTerm config directory (via $WEZTERM_CONFIG_DIR or defaults)
 *   2. Runs the config through luajit to extract effective colors
 *   3. Maps ANSI palette slots to pi theme colors
 *   4. Writes a pi theme file and activates it
 *
 * Supports:
 *   - Inline `config.colors = { ... }` definitions
 *   - Lua theme modules loaded via require()
 *   - Any config structure as long as `config.colors` is set
 *
 * ANSI slots (consistent across themes):
 *   0: black    8: bright black (gray/muted)
 *   1: red      9: bright red
 *   2: green   10: bright green
 *   3: yellow  11: bright yellow
 *   4: blue    12: bright blue
 *   5: magenta 13: bright magenta
 *   6: cyan    14: bright cyan
 *   7: white   15: bright white
 *
 * Requirements:
 *   - WezTerm installed and running (sets $WEZTERM_CONFIG_DIR)
 *   - luajit or lua available in PATH
 */

import { execSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readdirSync, unlinkSync, watch, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

interface WeztermColors {
	background: string;
	foreground: string;
	palette: Record<number, string>;
}

/**
 * Find the WezTerm config directory.
 * Checks $WEZTERM_CONFIG_DIR, then standard locations.
 */
function findConfigDir(): string | null {
	if (process.env.WEZTERM_CONFIG_DIR && existsSync(process.env.WEZTERM_CONFIG_DIR)) {
		return process.env.WEZTERM_CONFIG_DIR;
	}

	const candidates = [
		join(homedir(), ".config", "wezterm"),
		join(homedir(), ".wezterm"),
	];

	for (const dir of candidates) {
		if (existsSync(dir)) return dir;
	}

	return null;
}

/**
 * Find which Lua interpreter is available.
 */
function findLua(): string | null {
	for (const cmd of ["luajit", "lua5.4", "lua5.3", "lua"]) {
		try {
			execSync(`which ${cmd}`, { stdio: "pipe" });
			return cmd;
		} catch {
			// Try next
		}
	}
	return null;
}

/**
 * Extract colors from WezTerm config by evaluating it with a mocked wezterm module.
 * Writes a temporary Lua helper script, runs it with luajit, then cleans up.
 */
function getWeztermColors(configDir: string, lua: string): WeztermColors | null {
	const configFile = join(configDir, "wezterm.lua");
	if (!existsSync(configFile)) return null;

	const tmpScript = join(configDir, ".pi-extract-colors.lua");
	const extractScript = `
-- Mock wezterm module with commonly used functions
local mock_wezterm = {
	font = function(name) return name end,
	font_with_fallback = function(names) return names end,
	hostname = function() return "mock" end,
	home_dir = ${JSON.stringify(homedir())},
	config_dir = ${JSON.stringify(configDir)},
	target_triple = "x86_64-unknown-linux-gnu",
	version = "mock",
	log_info = function() end,
	log_warn = function() end,
	log_error = function() end,
	on = function() end,
	add_to_config_reload_watch_list = function() end,
	action = setmetatable({}, {
		__index = function(_, k)
			return function(...) return { action = k, args = {...} } end
		end
	}),
	action_callback = function(fn) return fn end,
	color = {
		parse = function(c) return c end,
		get_builtin_schemes = function() return {} end,
	},
	gui = {
		get_appearance = function() return "Dark" end,
	},
	GLOBAL = {},
	nerdfonts = setmetatable({}, { __index = function() return "" end }),
}
mock_wezterm.plugin = { require = function() return {} end }
package.loaded["wezterm"] = mock_wezterm

-- Add config dir to Lua search path
package.path = ${JSON.stringify(configDir)} .. "/?.lua;" ..
               ${JSON.stringify(configDir)} .. "/?/init.lua;" ..
               package.path

-- Try to load the config
local ok, config = pcall(dofile, ${JSON.stringify(configFile)})
if not ok then
	io.stderr:write("Failed to load config: " .. tostring(config) .. "\\n")
	os.exit(1)
end

if type(config) ~= "table" then
	io.stderr:write("Config did not return a table\\n")
	os.exit(1)
end

local colors = config.colors
if not colors then
	if config.color_scheme then
		io.stderr:write("color_scheme=" .. tostring(config.color_scheme) .. "\\n")
	end
	io.stderr:write("No inline colors found in config\\n")
	os.exit(1)
end

if type(colors) == "table" then
	if colors.background then print("background=" .. colors.background) end
	if colors.foreground then print("foreground=" .. colors.foreground) end
	if colors.ansi then
		for i, c in ipairs(colors.ansi) do
			print("ansi" .. (i-1) .. "=" .. c)
		end
	end
	if colors.brights then
		for i, c in ipairs(colors.brights) do
			print("bright" .. (i-1) .. "=" .. c)
		end
	end
end
`;

	try {
		writeFileSync(tmpScript, extractScript);
		const output = execSync(`${lua} ${JSON.stringify(tmpScript)}`, {
			encoding: "utf-8",
			timeout: 5000,
			cwd: configDir,
			stdio: ["pipe", "pipe", "pipe"],
		});
		return parseWeztermOutput(output);
	} catch (err: any) {
		if (err.stderr) {
			console.error(`[wezterm-theme-sync] ${err.stderr.trim()}`);
		}
		return null;
	} finally {
		try { unlinkSync(tmpScript); } catch { /* ignore */ }
	}
}

function parseWeztermOutput(output: string): WeztermColors {
	const colors: WeztermColors = {
		background: "#1e1e1e",
		foreground: "#d4d4d4",
		palette: {},
	};

	for (const line of output.split("\n")) {
		const match = line.match(/^(\w+)=(.+)$/);
		if (!match) continue;

		const [, key, value] = match;
		const color = normalizeColor(value.trim());

		if (key === "background") {
			colors.background = color;
		} else if (key === "foreground") {
			colors.foreground = color;
		} else {
			const ansiMatch = key.match(/^ansi(\d+)$/);
			const brightMatch = key.match(/^bright(\d+)$/);
			if (ansiMatch) {
				const idx = parseInt(ansiMatch[1], 10);
				if (idx >= 0 && idx <= 7) colors.palette[idx] = color;
			} else if (brightMatch) {
				const idx = parseInt(brightMatch[1], 10);
				if (idx >= 0 && idx <= 7) colors.palette[idx + 8] = color;
			}
		}
	}

	return colors;
}

function normalizeColor(color: string): string {
	const trimmed = color.trim();
	if (trimmed.startsWith("#")) {
		if (trimmed.length === 4) {
			return `#${trimmed[1]}${trimmed[1]}${trimmed[2]}${trimmed[2]}${trimmed[3]}${trimmed[3]}`;
		}
		return trimmed.toLowerCase();
	}
	if (/^[0-9a-fA-F]{6}$/.test(trimmed)) {
		return `#${trimmed}`.toLowerCase();
	}
	return `#${trimmed}`.toLowerCase();
}

function hexToRgb(hex: string): { r: number; g: number; b: number } {
	const h = hex.replace("#", "");
	return {
		r: parseInt(h.substring(0, 2), 16),
		g: parseInt(h.substring(2, 4), 16),
		b: parseInt(h.substring(4, 6), 16),
	};
}

function rgbToHex(r: number, g: number, b: number): string {
	const clamp = (n: number) => Math.round(Math.min(255, Math.max(0, n)));
	return `#${clamp(r).toString(16).padStart(2, "0")}${clamp(g).toString(16).padStart(2, "0")}${clamp(b).toString(16).padStart(2, "0")}`;
}

function getLuminance(hex: string): number {
	const { r, g, b } = hexToRgb(hex);
	return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}

function adjustBrightness(hex: string, amount: number): string {
	const { r, g, b } = hexToRgb(hex);
	return rgbToHex(r + amount, g + amount, b + amount);
}

function mixColors(color1: string, color2: string, weight: number): string {
	const c1 = hexToRgb(color1);
	const c2 = hexToRgb(color2);
	return rgbToHex(
		c1.r * weight + c2.r * (1 - weight),
		c1.g * weight + c2.g * (1 - weight),
		c1.b * weight + c2.b * (1 - weight),
	);
}

function generatePiTheme(colors: WeztermColors, themeName: string): object {
	const bg = colors.background;
	const fg = colors.foreground;
	const isDark = getLuminance(bg) < 0.5;

	// ANSI color slots - trust the standard for semantic colors
	const error = colors.palette[1] || "#cc6666";
	const success = colors.palette[2] || "#98c379";
	const warning = colors.palette[3] || "#e5c07b";
	const link = colors.palette[4] || "#61afef";
	const accent = colors.palette[5] || "#c678dd";
	const accentAlt = colors.palette[6] || "#56b6c2";

	// Derive neutrals from bg/fg for consistent readability
	const muted = mixColors(fg, bg, 0.65);
	const dim = mixColors(fg, bg, 0.45);
	const borderMuted = mixColors(fg, bg, 0.25);

	// Derive backgrounds
	const bgShift = isDark ? 12 : -12;
	const selectedBg = adjustBrightness(bg, bgShift);
	const userMsgBg = adjustBrightness(bg, Math.round(bgShift * 0.7));
	const toolPendingBg = adjustBrightness(bg, Math.round(bgShift * 0.4));
	const toolSuccessBg = mixColors(bg, success, 0.88);
	const toolErrorBg = mixColors(bg, error, 0.88);
	const customMsgBg = mixColors(bg, accent, 0.92);

	return {
		$schema:
			"https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
		name: themeName,
		vars: {
			bg,
			fg,
			accent,
			accentAlt,
			link,
			error,
			success,
			warning,
			muted,
			dim,
			borderMuted,
			selectedBg,
			userMsgBg,
			toolPendingBg,
			toolSuccessBg,
			toolErrorBg,
			customMsgBg,
		},
		colors: {
			accent: "accent",
			border: "link",
			borderAccent: "accent",
			borderMuted: "borderMuted",
			success: "success",
			error: "error",
			warning: "warning",
			muted: "muted",
			dim: "dim",
			text: "",
			thinkingText: "muted",
			selectedBg: "selectedBg",
			userMessageBg: "userMsgBg",
			userMessageText: "",
			customMessageBg: "customMsgBg",
			customMessageText: "",
			customMessageLabel: "accent",
			toolPendingBg: "toolPendingBg",
			toolSuccessBg: "toolSuccessBg",
			toolErrorBg: "toolErrorBg",
			toolTitle: "",
			toolOutput: "muted",
			mdHeading: "warning",
			mdLink: "link",
			mdLinkUrl: "dim",
			mdCode: "accent",
			mdCodeBlock: "success",
			mdCodeBlockBorder: "muted",
			mdQuote: "muted",
			mdQuoteBorder: "muted",
			mdHr: "muted",
			mdListBullet: "accent",
			toolDiffAdded: "success",
			toolDiffRemoved: "error",
			toolDiffContext: "muted",
			syntaxComment: "muted",
			syntaxKeyword: "accent",
			syntaxFunction: "link",
			syntaxVariable: "accentAlt",
			syntaxString: "success",
			syntaxNumber: "accent",
			syntaxType: "accentAlt",
			syntaxOperator: "fg",
			syntaxPunctuation: "muted",
			thinkingOff: "borderMuted",
			thinkingMinimal: "muted",
			thinkingLow: "link",
			thinkingMedium: "accentAlt",
			thinkingHigh: "accent",
			thinkingXhigh: "accent",
			bashMode: "success",
		},
		export: {
			pageBg: isDark ? adjustBrightness(bg, -8) : adjustBrightness(bg, 8),
			cardBg: bg,
			infoBg: mixColors(bg, warning, 0.88),
		},
	};
}

function computeThemeHash(colors: WeztermColors): string {
	const parts: string[] = [];
	parts.push(`bg=${colors.background}`);
	parts.push(`fg=${colors.foreground}`);
	for (let i = 0; i <= 15; i++) {
		parts.push(`p${i}=${colors.palette[i] ?? ""}`);
	}
	return createHash("sha1").update(parts.join("\n")).digest("hex").slice(0, 8);
}

function cleanupOldThemes(themesDir: string, keepFile: string): void {
	try {
		for (const file of readdirSync(themesDir)) {
			if (file === keepFile) continue;
			if (file.startsWith("wezterm-sync-") && file.endsWith(".json")) {
				unlinkSync(join(themesDir, file));
			}
		}
	} catch {
		// Best-effort cleanup
	}
}

function syncTheme(ctx: any) {
	const configDir = findConfigDir();
	if (!configDir) return;

	const lua = findLua();
	if (!lua) return;

	const colors = getWeztermColors(configDir, lua);
	if (!colors) return;

	const themesDir = join(homedir(), ".pi", "agent", "themes");
	if (!existsSync(themesDir)) {
		mkdirSync(themesDir, { recursive: true });
	}

	const hash = computeThemeHash(colors);
	const themeName = `wezterm-sync-${hash}`;
	const themeFile = `${themeName}.json`;
	const themePath = join(themesDir, themeFile);

	// Skip if already on the correct synced theme (avoids repaint)
	if (ctx.ui.theme.name === themeName) {
		return;
	}

	const themeJson = generatePiTheme(colors, themeName);
	writeFileSync(themePath, JSON.stringify(themeJson, null, 2));

	// Remove old generated themes
	cleanupOldThemes(themesDir, themeFile);

	// Set by name so pi loads from the file we just wrote
	const result = ctx.ui.setTheme(themeName);
	if (!result.success) {
		ctx.ui.notify(`WezTerm theme sync failed: ${result.error}`, "error");
	}
}

export default function (pi: ExtensionAPI) {
	let currentCtx: any = null;

	pi.on("session_start", async (_event, ctx) => {
		currentCtx = ctx;
		syncTheme(ctx);
	});

	// Watch theme-state file for dark/light toggle changes
	const stateFile = join(homedir(), ".config", "theme-state");
	try {
		watch(stateFile, { persistent: false }, (_event) => {
			// Debounce: wait a tick for the file write to complete
			setTimeout(() => {
				if (currentCtx) {
					syncTheme(currentCtx);
				}
			}, 100);
		});
	} catch {
		// File may not exist yet — non-fatal
	}
}
