import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
	Key,
	matchesKey,
	truncateToWidth,
	visibleWidth,
} from "@singularity-forge/tui";
import { stripAnsi } from "../shared/mod.js";
import { writeExportFile } from "./export.js";
import { sfRoot } from "./paths.js";
import { loadVisualizerData } from "./visualizer-data.js";
import {
	renderAgentView,
	renderCapturesView,
	renderChangelogView,
	renderDepsView,
	renderExportView,
	renderHealthView,
	renderKnowledgeView,
	renderMetricsView,
	renderProgressView,
	renderTimelineView,
} from "./visualizer-views.js";

const TAB_COUNT = 10;
const TAB_LABELS = [
	"1 Progress",
	"2 Timeline",
	"3 Deps",
	"4 Metrics",
	"5 Health",
	"6 Agent",
	"7 Changes",
	"8 Knowledge",
	"9 Captures",
	"0 Export",
];
function buildTabBarEntries(activeTab, filterText, capturesPendingCount) {
	return TAB_LABELS.map((label, i) => {
		let displayLabel = label;
		if (i === activeTab && filterText) {
			displayLabel += " \u2731";
		}
		if (i === 8 && capturesPendingCount) {
			displayLabel += ` (${capturesPendingCount})`;
		}
		return {
			label: displayLabel,
			width: visibleWidth(displayLabel) + 2,
		};
	});
}
export class SFVisualizerOverlay {
	tui;
	theme;
	onClose;
	activeTab = 0;
	scrollOffsets = new Array(TAB_COUNT).fill(0);
	loading = true;
	disposed = false;
	cachedWidth;
	cachedLines;
	refreshTimer;
	data = null;
	basePath;
	// Filter state
	filterMode = false;
	filterText = "";
	filterField = "all";
	// Export state
	lastExportPath;
	exportStatus;
	// New state
	lastVisibleRows = 20;
	collapsedMilestones = new Set();
	showHelp = false;
	resizeHandler = null;
	constructor(tui, theme, onClose) {
		this.tui = tui;
		this.theme = theme;
		this.onClose = onClose;
		this.basePath = process.cwd();
		// Enable SGR mouse tracking
		process.stdout.write("\x1b[?1003h\x1b[?1006h");
		// Invalidate cache on terminal resize
		this.resizeHandler = () => {
			if (this.disposed) return;
			this.invalidate();
			this.tui.requestRender();
		};
		process.stdout.on("resize", this.resizeHandler);
		loadVisualizerData(this.basePath)
			.then((d) => {
				this.data = d;
				this.loading = false;
				this.tui.requestRender();
			})
			.catch(() => {
				this.loading = false;
				this.tui.requestRender();
			});
		this.refreshTimer = setInterval(() => {
			loadVisualizerData(this.basePath)
				.then((d) => {
					if (this.disposed) return;
					this.data = d;
					this.invalidate();
					this.tui.requestRender();
				})
				.catch(() => {}); // retry on next interval
		}, 30_000);
	}
	parseSGRMouse(data) {
		const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
		if (!match) return null;
		return {
			button: parseInt(match[1], 10),
			x: parseInt(match[2], 10),
			y: parseInt(match[3], 10),
			press: match[4] === "M",
		};
	}
	handleInput(data) {
		if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
			this.dispose();
			this.onClose();
			return;
		}
		// Filter mode input routing
		if (this.filterMode) {
			if (matchesKey(data, Key.enter)) {
				this.filterMode = false;
				this.invalidate();
				this.tui.requestRender();
				return;
			}
			if (matchesKey(data, Key.backspace)) {
				this.filterText = this.filterText.slice(0, -1);
				this.invalidate();
				this.tui.requestRender();
				return;
			}
			// Append printable characters
			if (data.length === 1 && data.charCodeAt(0) >= 32) {
				this.filterText += data;
				this.invalidate();
				this.tui.requestRender();
				return;
			}
			return;
		}
		// Help overlay dismissal
		if (this.showHelp) {
			if (matchesKey(data, Key.escape) || data === "?") {
				this.showHelp = false;
				this.invalidate();
				this.tui.requestRender();
				return;
			}
			return;
		}
		// Mouse handling (before keyboard checks)
		const mouse = this.parseSGRMouse(data);
		if (mouse) {
			if (mouse.button === 64) {
				// Wheel up
				this.scrollOffsets[this.activeTab] = Math.max(
					0,
					this.scrollOffsets[this.activeTab] - 3,
				);
				this.invalidate();
				this.tui.requestRender();
				return;
			}
			if (mouse.button === 65) {
				// Wheel down
				this.scrollOffsets[this.activeTab] += 3;
				this.invalidate();
				this.tui.requestRender();
				return;
			}
			if (mouse.button === 0 && mouse.press) {
				// Left click — check if on tab bar row
				if (mouse.y === 2) {
					let xPos = 3;
					const tabs = buildTabBarEntries(
						this.activeTab,
						this.filterText,
						this.data?.captures?.pendingCount,
					);
					for (let i = 0; i < tabs.length; i++) {
						const tabWidth = tabs[i].width;
						if (mouse.x >= xPos && mouse.x < xPos + tabWidth) {
							this.activeTab = i;
							this.invalidate();
							this.tui.requestRender();
							return;
						}
						xPos += tabWidth + 1;
					}
				}
			}
			return;
		}
		if (matchesKey(data, Key.shift("tab"))) {
			this.activeTab = (this.activeTab - 1 + TAB_COUNT) % TAB_COUNT;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (matchesKey(data, Key.tab)) {
			this.activeTab = (this.activeTab + 1) % TAB_COUNT;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if ("1234567890".includes(data) && data.length === 1) {
			const idx = data === "0" ? 9 : parseInt(data, 10) - 1;
			this.activeTab = idx;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		// "/" enters filter mode on any tab
		if (data === "/") {
			this.filterMode = true;
			this.filterText = "";
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		// "f" cycles filter field (limit to all/keyword on non-Progress tabs)
		if (data === "f") {
			if (this.activeTab === 0) {
				const fields = ["all", "status", "risk", "keyword"];
				const idx = fields.indexOf(this.filterField);
				this.filterField = fields[(idx + 1) % fields.length];
			} else {
				this.filterField = this.filterField === "all" ? "keyword" : "all";
			}
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		// "?" toggles help overlay
		if (data === "?") {
			this.showHelp = true;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		// Enter/Space toggles collapse on Progress tab
		if (
			(matchesKey(data, Key.enter) || data === " ") &&
			this.activeTab === 0 &&
			this.data
		) {
			const viewLines = this.renderTabContent(0, 80);
			const offset = this.scrollOffsets[0];
			for (const ms of this.data.milestones) {
				const lineIdx = viewLines.findIndex((l) =>
					stripAnsi(l).includes(`${ms.id}:`),
				);
				if (lineIdx >= offset && lineIdx < offset + this.lastVisibleRows) {
					if (this.collapsedMilestones.has(ms.id)) {
						this.collapsedMilestones.delete(ms.id);
					} else {
						this.collapsedMilestones.add(ms.id);
					}
					this.invalidate();
					this.tui.requestRender();
					return;
				}
			}
			return;
		}
		// Export tab key handling
		if (this.activeTab === 9 && this.data) {
			if (data === "m" || data === "j" || data === "s") {
				this.handleExportKey(data);
				return;
			}
		}
		// Page Up/Down
		if (matchesKey(data, Key.pageUp)) {
			const amount = Math.max(1, this.lastVisibleRows - 2);
			this.scrollOffsets[this.activeTab] = Math.max(
				0,
				this.scrollOffsets[this.activeTab] - amount,
			);
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (matchesKey(data, Key.pageDown)) {
			const amount = Math.max(1, this.lastVisibleRows - 2);
			this.scrollOffsets[this.activeTab] += amount;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		// Half-page scroll: Ctrl+U / Ctrl+D
		if (matchesKey(data, Key.ctrl("u"))) {
			const amount = Math.max(1, Math.floor(this.lastVisibleRows / 2));
			this.scrollOffsets[this.activeTab] = Math.max(
				0,
				this.scrollOffsets[this.activeTab] - amount,
			);
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (matchesKey(data, Key.ctrl("d"))) {
			const amount = Math.max(1, Math.floor(this.lastVisibleRows / 2));
			this.scrollOffsets[this.activeTab] += amount;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (matchesKey(data, Key.down) || matchesKey(data, "j")) {
			this.scrollOffsets[this.activeTab]++;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (matchesKey(data, Key.up) || matchesKey(data, "k")) {
			this.scrollOffsets[this.activeTab] = Math.max(
				0,
				this.scrollOffsets[this.activeTab] - 1,
			);
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (data === "g") {
			this.scrollOffsets[this.activeTab] = 0;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (data === "G") {
			this.scrollOffsets[this.activeTab] = 999;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
	}
	handleExportKey(key) {
		if (!this.data) return;
		const format = key === "m" ? "markdown" : key === "j" ? "json" : "snapshot";
		if (format === "snapshot") {
			// Capture current active tab's rendered lines as snapshot
			const snapshotLines = this.renderTabContent(this.activeTab, 80);
			const timestamp = new Date()
				.toISOString()
				.replace(/[:.]/g, "-")
				.slice(0, 19);
			const exportDir = sfRoot(this.basePath);
			mkdirSync(exportDir, { recursive: true });
			const outPath = join(exportDir, `snapshot-${timestamp}.txt`);
			writeFileSync(outPath, snapshotLines.join("\n") + "\n", "utf-8");
			this.lastExportPath = outPath;
			this.exportStatus = "Snapshot saved";
		} else {
			const result = writeExportFile(this.basePath, format, this.data);
			if (result) {
				this.lastExportPath = result;
				this.exportStatus = `${format} export saved`;
			}
		}
		this.invalidate();
		this.tui.requestRender();
	}
	renderTabContent(tab, width) {
		if (!this.data) return [];
		const th = this.theme;
		switch (tab) {
			case 0: {
				const filter = this.filterText
					? { text: this.filterText, field: this.filterField }
					: undefined;
				return renderProgressView(
					this.data,
					th,
					width,
					filter,
					this.collapsedMilestones,
				);
			}
			case 1:
				return renderTimelineView(this.data, th, width);
			case 2:
				return renderDepsView(this.data, th, width);
			case 3:
				return renderMetricsView(this.data, th, width);
			case 4:
				return renderHealthView(this.data, th, width);
			case 5:
				return renderAgentView(this.data, th, width);
			case 6:
				return renderChangelogView(this.data, th, width);
			case 7:
				return renderKnowledgeView(this.data, th, width);
			case 8:
				return renderCapturesView(this.data, th, width);
			case 9:
				return renderExportView(this.data, th, width, this.lastExportPath);
			default:
				return [];
		}
	}
	renderHelpContent(_width) {
		const th = this.theme;
		const lines = [];
		lines.push(th.fg("accent", th.bold("Keyboard Shortcuts")));
		lines.push("");
		const bindings = [
			["Tab/Shift+Tab", "Next/Previous tab"],
			["1-9, 0", "Jump to tab"],
			["j/k, Up/Down", "Scroll line"],
			["PgUp/PgDn", "Scroll page"],
			["Ctrl+U/Ctrl+D", "Scroll half-page"],
			["g/G", "Top/Bottom"],
			["/", "Search/filter"],
			["f", "Cycle filter field"],
			["Enter/Space", "Toggle collapse (Progress)"],
			["Mouse wheel", "Scroll"],
			["Click tab", "Switch tab"],
			["?", "Toggle help"],
			["Esc", "Close"],
		];
		for (const [key, desc] of bindings) {
			const keyStr = th.fg("accent", key.padEnd(20));
			lines.push(`  ${keyStr} ${desc}`);
		}
		lines.push("");
		lines.push(th.fg("dim", "Press ? or Esc to dismiss"));
		return lines;
	}
	render(width) {
		if (this.cachedLines && this.cachedWidth === width) {
			return this.cachedLines;
		}
		const th = this.theme;
		const innerWidth = width - 4;
		const content = [];
		// Tab bar
		const tabEntries = buildTabBarEntries(
			this.activeTab,
			this.filterText,
			this.data?.captures?.pendingCount,
		);
		const tabs = tabEntries.map((entry, i) => {
			if (i === this.activeTab) {
				return th.fg("accent", `[${entry.label}]`);
			}
			return th.fg("dim", `[${entry.label}]`);
		});
		content.push(" " + tabs.join(" "));
		content.push("");
		// Filter bar (when in filter mode on any tab)
		if (this.filterMode) {
			content.push(
				th.fg(
					"accent",
					`Filter (${this.filterField}): ${this.filterText}\u2588`,
				),
			);
			content.push("");
		}
		if (this.showHelp) {
			content.push(...this.renderHelpContent(innerWidth));
		} else if (this.loading) {
			const loadingText = "Loading\u2026";
			const vis = visibleWidth(loadingText);
			const leftPad = Math.max(0, Math.floor((innerWidth - vis) / 2));
			content.push(" ".repeat(leftPad) + loadingText);
		} else if (this.data) {
			let viewLines = this.renderTabContent(this.activeTab, innerWidth);
			// Show export status message if present
			if (this.exportStatus && this.activeTab === 9) {
				content.push(th.fg("success", this.exportStatus));
				content.push("");
				this.exportStatus = undefined;
			}
			// Apply cross-tab filter for non-Progress tabs
			if (this.filterText && this.activeTab !== 0) {
				const lowerFilter = this.filterText.toLowerCase();
				viewLines = viewLines.filter((line) =>
					stripAnsi(line).toLowerCase().includes(lowerFilter),
				);
			}
			content.push(...viewLines);
		}
		// Apply scroll
		const viewportHeight = Math.max(
			5,
			process.stdout.rows ? process.stdout.rows - 8 : 24,
		);
		const chromeHeight = 2;
		const visibleContentRows = Math.max(1, viewportHeight - chromeHeight);
		this.lastVisibleRows = visibleContentRows;
		const totalLines = content.length;
		const maxScroll = Math.max(0, content.length - visibleContentRows);
		this.scrollOffsets[this.activeTab] = Math.min(
			this.scrollOffsets[this.activeTab],
			maxScroll,
		);
		const offset = this.scrollOffsets[this.activeTab];
		const visibleContent = content.slice(offset, offset + visibleContentRows);
		const lines = this.wrapInBox(
			visibleContent,
			width,
			offset,
			visibleContentRows,
			totalLines,
		);
		// Footer hint
		const hint = th.fg(
			"dim",
			"Tab/Shift+Tab/1-9,0 switch \u00b7 / filter \u00b7 PgUp/PgDn scroll \u00b7 ? help \u00b7 esc close",
		);
		const hintVis = visibleWidth(hint);
		const hintPad = Math.max(0, Math.floor((width - hintVis) / 2));
		lines.push(" ".repeat(hintPad) + hint);
		this.cachedWidth = width;
		this.cachedLines = lines;
		return lines;
	}
	wrapInBox(inner, width, offset, visibleRows, totalLines) {
		const th = this.theme;
		const border = (s) => th.fg("borderAccent", s);
		const innerWidth = width - 4;
		const lines = [];
		lines.push(border("\u256d" + "\u2500".repeat(width - 2) + "\u256e"));
		// Compute scroll indicator positions
		const scrollable =
			totalLines !== undefined &&
			visibleRows !== undefined &&
			totalLines > visibleRows;
		let thumbStart = -1;
		let thumbLen = 0;
		const innerRows = inner.length;
		if (scrollable && innerRows > 0 && totalLines > 0) {
			thumbStart = Math.round(((offset ?? 0) / totalLines) * innerRows);
			thumbLen = Math.max(
				1,
				Math.round((visibleRows / totalLines) * innerRows),
			);
		}
		for (let i = 0; i < inner.length; i++) {
			const line = inner[i];
			const truncated = truncateToWidth(line, innerWidth);
			const padWidth = Math.max(0, innerWidth - visibleWidth(truncated));
			const rightBorder =
				scrollable && i >= thumbStart && i < thumbStart + thumbLen
					? border("\u2503")
					: border("\u2502");
			lines.push(
				border("\u2502") +
					" " +
					truncated +
					" ".repeat(padWidth) +
					" " +
					rightBorder,
			);
		}
		lines.push(border("\u2570" + "\u2500".repeat(width - 2) + "\u256f"));
		return lines;
	}
	invalidate() {
		this.cachedWidth = undefined;
		this.cachedLines = undefined;
	}
	dispose() {
		this.disposed = true;
		clearInterval(this.refreshTimer);
		if (this.resizeHandler) {
			process.stdout.removeListener("resize", this.resizeHandler);
			this.resizeHandler = null;
		}
		// Disable SGR mouse tracking
		process.stdout.write("\x1b[?1003l\x1b[?1006l");
	}
}
