/**
 * SF Parallel Monitor Overlay
 *
 * Full-screen TUI overlay showing real-time parallel worker progress.
 * Opened via `/parallel watch`, Ctrl+Alt+P (⌃⌥P on macOS),
 * or Ctrl+Shift+P fallback.
 * Reads the same data sources as `scripts/parallel-monitor.mjs` but
 * renders as a native tui overlay with theme integration.
 */
import {
	closeSync,
	existsSync,
	openSync,
	readdirSync,
	readFileSync,
	readSync,
	statSync,
} from "node:fs";
import { join } from "node:path";
import { Key, matchesKey } from "@singularity-forge/tui";
import { formatDuration } from "../shared/mod.js";
import {
	queryParallelRecentCompletionRows,
	queryParallelSliceProgress,
} from "./parallel-monitor-store.js";
import { formattedShortcutPair } from "./shortcut-defs.js";

// ─── Data Helpers ─────────────────────────────────────────────────────────
function readJsonSafe(filePath) {
	try {
		return JSON.parse(readFileSync(filePath, "utf-8"));
	} catch {
		return null;
	}
}
function isPidAlive(pid) {
	try {
		process.kill(pid, 0);
		return true;
	} catch {
		return false;
	}
}
function tailRead(filePath, maxBytes) {
	try {
		const stat = statSync(filePath);
		const readSize = Math.min(stat.size, maxBytes);
		const fd = openSync(filePath, "r");
		const buf = Buffer.alloc(readSize);
		readSync(fd, buf, 0, readSize, Math.max(0, stat.size - readSize));
		closeSync(fd);
		return buf.toString("utf-8");
	} catch {
		return "";
	}
}
function discoverWorkers(basePath) {
	const parallelDir = join(basePath, ".sf", "parallel");
	const worktreeDir = join(basePath, ".sf", "worktrees");
	const mids = new Set();
	if (existsSync(parallelDir)) {
		try {
			for (const f of readdirSync(parallelDir)) {
				if (f.endsWith(".status.json")) mids.add(f.replace(".status.json", ""));
				const m = f.match(/^(M\d+)\.(stderr|stdout)\.log$/);
				if (m) mids.add(m[1]);
			}
		} catch {
			/* skip */
		}
	}
	if (existsSync(worktreeDir)) {
		try {
			for (const d of readdirSync(worktreeDir)) {
				if (
					d.startsWith("M") &&
					existsSync(join(worktreeDir, d, ".sf", "auto.lock"))
				) {
					mids.add(d);
				}
			}
		} catch {
			/* skip */
		}
	}
	return [...mids].sort();
}
function extractCostFromNdjson(basePath, mid) {
	const stdoutPath = join(basePath, ".sf", "parallel", `${mid}.stdout.log`);
	if (!existsSync(stdoutPath)) return 0;
	try {
		const content = readFileSync(stdoutPath, "utf-8");
		let total = 0;
		for (const line of content.split("\n")) {
			if (!line.includes("message_end")) continue;
			try {
				const obj = JSON.parse(line);
				if (obj.type === "message_end") {
					const cost = obj.message?.usage?.cost?.total;
					if (typeof cost === "number") total += cost;
				}
			} catch {
				/* skip */
			}
		}
		return total;
	} catch {
		return 0;
	}
}
function formatCompletionEvent(mid, row) {
	return `✓ ${mid}/${row.sliceId}/${row.taskId}${row.oneLiner ? ": " + row.oneLiner : ""}`;
}
async function collectWorkerData(basePath) {
	const mids = discoverWorkers(basePath);
	const parallelDir = join(basePath, ".sf", "parallel");
	const allSlices = await Promise.all(
		mids.map((mid) => queryParallelSliceProgress(basePath, mid)),
	);
	const workers = [];
	for (let i = 0; i < mids.length; i++) {
		const mid = mids[i];
		const slices = allSlices[i];
		const status = readJsonSafe(join(parallelDir, `${mid}.status.json`));
		const lock = readJsonSafe(
			join(basePath, ".sf", "worktrees", mid, ".sf", "auto.lock"),
		);
		const pid = lock?.pid || status?.pid || 0;
		const alive = pid ? isPidAlive(pid) : false;
		// Heartbeat: prefer status.json if PID matches, else use file mtime
		let heartbeatAge = Infinity;
		const statusPidMatches = status?.pid === pid && status?.lastHeartbeat;
		if (statusPidMatches) {
			heartbeatAge = Date.now() - status.lastHeartbeat;
		} else {
			const mtimes = [];
			const stdoutLog = join(parallelDir, `${mid}.stdout.log`);
			const stderrLog = join(parallelDir, `${mid}.stderr.log`);
			if (existsSync(stdoutLog)) mtimes.push(statSync(stdoutLog).mtimeMs);
			if (existsSync(stderrLog)) mtimes.push(statSync(stderrLog).mtimeMs);
			if (lock?.unitStartedAt)
				mtimes.push(new Date(lock.unitStartedAt).getTime());
			if (mtimes.length > 0) heartbeatAge = Date.now() - Math.max(...mtimes);
		}
		let cost = status?.cost || 0;
		if (cost === 0) cost = extractCostFromNdjson(basePath, mid);
		const totalTasks = slices.reduce((sum, s) => sum + s.total, 0);
		const doneTasks = slices.reduce((sum, s) => sum + s.done, 0);
		const doneSlices = slices.filter((s) => s.status === "complete").length;
		const elapsed = status?.startedAt
			? Date.now() - status.startedAt
			: lock?.startedAt
				? Date.now() - new Date(lock.startedAt).getTime()
				: 0;
		// Errors from stderr (last 4KB, only new content)
		const errors = [];
		const stderrLog = join(parallelDir, `${mid}.stderr.log`);
		if (existsSync(stderrLog)) {
			const content = tailRead(stderrLog, 4096);
			for (const line of content.trim().split("\n").slice(-5)) {
				if (
					line.includes("error") ||
					line.includes("Error") ||
					line.includes("exited")
				) {
					errors.push(line.trim());
				}
			}
		}
		workers.push({
			mid,
			pid,
			alive,
			state: alive ? "running" : status?.state || "dead",
			cost,
			heartbeatAge,
			currentUnit: lock?.unitId || null,
			unitType: lock?.unitType || null,
			unitElapsed: lock?.unitStartedAt
				? Date.now() - new Date(lock.unitStartedAt).getTime()
				: 0,
			elapsed,
			totalTasks,
			doneTasks,
			totalSlices: slices.length,
			doneSlices,
			slices,
			errors,
		});
	}
	return workers;
}
// ─── Rendering Helpers ────────────────────────────────────────────────────
function unitTypeLabel(unitType) {
	const labels = {
		"execute-task": "EXEC",
		"research-slice": "RSRCH",
		"plan-slice": "PLAN",
		"complete-slice": "DONE",
		"complete-task": "DONE",
		reassess: "ASSESS",
		validate: "VALID",
		"reassess-roadmap": "ASSESS",
	};
	return (
		labels[unitType || ""] || (unitType || "---").toUpperCase().slice(0, 5)
	);
}
function progressBar(done, total, width) {
	if (total === 0) return "░".repeat(width);
	const filled = Math.round((done / total) * width);
	return "█".repeat(filled) + "░".repeat(width - filled);
}
function healthGlyph(alive, _heartbeatAge) {
	if (!alive) return "○";
	return "●";
}
// ─── Overlay Class ────────────────────────────────────────────────────────
/**
 * Real-time overlay for parallel worker progress monitoring.
 * Displays status, slices, tasks, costs, and completion events for all workers.
 */
export class ParallelMonitorOverlay {
	tui;
	theme;
	onClose;
	basePath;
	refreshTimer;
	workers = [];
	events = [];
	cachedLines;
	scrollOffset = 0;
	disposed = false;
	resizeHandler = null;
	constructor(tui, theme, onClose, basePath) {
		this.tui = tui;
		this.theme = theme;
		this.onClose = onClose;
		this.basePath = basePath || process.cwd();
		this.resizeHandler = () => {
			if (this.disposed) return;
			this.invalidate();
			this.tui.requestRender();
		};
		process.stdout.on("resize", this.resizeHandler);
		this.refresh();
		this.refreshTimer = setInterval(() => this.refresh(), 15_000);
	}
	async refresh() {
		if (this.disposed) return;
		const workers = await collectWorkerData(this.basePath);
		if (this.disposed) return;
		this.workers = workers;
		// Collect completion events in parallel across workers
		const allCompletions = await Promise.all(
			this.workers.map((wk) =>
				queryParallelRecentCompletionRows(this.basePath, wk.mid).map((row) =>
					formatCompletionEvent(wk.mid, row),
				),
			),
		);
		if (this.disposed) return;
		for (const completions of allCompletions) {
			for (const evt of completions) {
				if (!this.events.includes(evt)) this.events.push(evt);
			}
		}
		this.events = this.events.slice(-10);
		this.cachedLines = undefined;
		this.tui.requestRender();
	}
	dispose() {
		this.disposed = true;
		clearInterval(this.refreshTimer);
		if (this.resizeHandler) {
			process.stdout.removeListener("resize", this.resizeHandler);
			this.resizeHandler = null;
		}
	}
	handleInput(data) {
		if (
			matchesKey(data, Key.escape) ||
			matchesKey(data, Key.ctrlAlt("p")) ||
			matchesKey(data, Key.ctrlShift("p")) ||
			data === "q"
		) {
			this.dispose();
			this.onClose();
			return;
		}
		if (matchesKey(data, Key.down) || data === "j") {
			this.scrollOffset++;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (matchesKey(data, Key.up) || data === "k") {
			this.scrollOffset = Math.max(0, this.scrollOffset - 1);
			this.invalidate();
			this.tui.requestRender();
			return;
		}
	}
	invalidate() {
		this.cachedLines = undefined;
	}
	render(width) {
		if (this.cachedLines) return this.cachedLines;
		const t = this.theme;
		const lines = [];
		const w = Math.max(width, 60);
		// Header
		const totalCost = this.workers.reduce((s, wk) => s + wk.cost, 0);
		const aliveCount = this.workers.filter((wk) => wk.alive).length;
		const now = new Date().toLocaleTimeString();
		lines.push(t.bold(t.fg("accent", " SF Parallel Monitor ")));
		lines.push(
			t.fg(
				"muted",
				`  ${now}  │  ${aliveCount}/${this.workers.length} alive  │  Total: `,
			) +
				t.bold(`$${totalCost.toFixed(2)}`) +
				t.fg("muted", "  │  15s refresh"),
		);
		lines.push(t.fg("muted", "─".repeat(w)));
		if (this.workers.length === 0) {
			lines.push("");
			lines.push(t.fg("warning", "  No parallel workers found."));
			lines.push(t.fg("muted", "  Run /parallel start to begin."));
		} else {
			for (const wk of this.workers) {
				lines.push("");
				// Health + ID + state
				const healthColor = wk.alive ? "success" : "error";
				const glyph = healthGlyph(wk.alive, wk.heartbeatAge);
				const stateText = wk.alive
					? t.fg("success", "RUNNING")
					: t.fg("error", t.bold("DEAD"));
				const heartbeatText =
					wk.heartbeatAge === Infinity
						? "never"
						: formatDuration(wk.heartbeatAge) + " ago";
				lines.push(
					`  ${t.fg(healthColor, glyph)}  ${t.bold(wk.mid)}  ${stateText}  ` +
						t.fg(
							"muted",
							`PID ${wk.pid}  │  elapsed ${formatDuration(wk.elapsed)}  │  `,
						) +
						`cost ${t.bold("$" + wk.cost.toFixed(2))}  ` +
						t.fg("muted", "│  heartbeat ") +
						t.fg(healthColor, heartbeatText),
				);
				// Current unit
				if (wk.currentUnit) {
					const phaseColor =
						wk.unitType === "execute-task"
							? "accent"
							: wk.unitType === "research-slice"
								? "warning"
								: wk.unitType?.includes("complete")
									? "success"
									: "text";
					lines.push(
						`     ${t.fg("muted", "▸")} ${t.fg(phaseColor, unitTypeLabel(wk.unitType))}  ${wk.currentUnit}  ` +
							t.fg("muted", `(${formatDuration(wk.unitElapsed)})`),
					);
				} else if (!wk.alive) {
					lines.push(`     ${t.fg("muted", "▸")} ${t.fg("error", "stopped")}`);
				} else {
					lines.push(`     ${t.fg("muted", "▸ idle / between units")}`);
				}
				// Slice progress chips
				if (wk.slices.length > 0) {
					const chips = wk.slices.map((s) => {
						const pct = s.total > 0 ? s.done / s.total : 0;
						const color =
							s.status === "complete"
								? "success"
								: pct > 0
									? "warning"
									: "muted";
						return t.fg(color, `${s.id}:${s.done}/${s.total}`);
					});
					lines.push(`     ${t.fg("muted", "slices")}  ${chips.join("  ")}`);
					// Task progress bar
					const bar = progressBar(wk.doneTasks, wk.totalTasks, 25);
					const pct =
						wk.totalTasks > 0
							? Math.round((wk.doneTasks / wk.totalTasks) * 100)
							: 0;
					lines.push(
						`     ${t.fg("muted", "tasks")}   ${t.fg("success", bar)}  ${wk.doneTasks}/${wk.totalTasks} ` +
							t.fg(
								"muted",
								`(${pct}%)  │  slices done ${wk.doneSlices}/${wk.totalSlices}`,
							),
					);
				}
				// Errors
				for (const err of wk.errors.slice(-2)) {
					const truncated =
						err.length > w - 10 ? err.slice(0, w - 11) + "…" : err;
					lines.push(`     ${t.fg("error", "⚠ " + truncated)}`);
				}
			}
		}
		// Event feed
		lines.push("");
		lines.push(t.fg("muted", "─".repeat(w)));
		lines.push(`  ${t.bold("Recent Events")}`);
		if (this.events.length === 0) {
			lines.push(t.fg("muted", "  No events yet..."));
		} else {
			for (const evt of this.events.slice(-8)) {
				const mid = evt.match(/^✓ (M\d+)\//)?.[1] || "";
				const truncated =
					evt.length > w - 10 ? evt.slice(0, w - 11) + "…" : evt;
				lines.push(
					`  ${t.fg("muted", "│")} ${t.fg("accent", mid)} ${truncated.replace(/^✓ M\d+\//, "")}`,
				);
			}
		}
		// Footer
		lines.push("");
		const allDone =
			this.workers.length > 0 && this.workers.every((wk) => !wk.alive);
		if (allDone) {
			lines.push(t.bold(t.fg("success", "  ALL WORKERS COMPLETE")));
			for (const wk of this.workers) {
				lines.push(
					`  ${wk.mid}  $${wk.cost.toFixed(2)}  │  ${wk.doneSlices}/${wk.totalSlices} slices  ` +
						`${wk.doneTasks}/${wk.totalTasks} tasks  │  ${formatDuration(wk.elapsed)}`,
				);
			}
			lines.push(
				`  ${t.bold("Total: $" + this.workers.reduce((s, wk) => s + wk.cost, 0).toFixed(2))}`,
			);
		}
		lines.push(
			t.fg(
				"muted",
				`  ESC/q/${formattedShortcutPair("parallel")} close  │  ↑↓ scroll`,
			),
		);
		// Apply scroll — use terminal rows as height estimate
		const termHeight = process.stdout.rows || 40;
		const maxScroll = Math.max(0, lines.length - termHeight);
		this.scrollOffset = Math.min(Math.max(this.scrollOffset, 0), maxScroll);
		const visible = lines.slice(
			this.scrollOffset,
			this.scrollOffset + termHeight,
		);
		this.cachedLines = visible;
		return visible;
	}
}
