// SF Extension — Notification History Overlay
// Scrollable panel showing all persisted notifications with severity filtering.
// Toggled with Ctrl+Alt+N (⌃⌥N on macOS), Ctrl+Shift+N fallback, or /notifications.
import {
	Key,
	matchesKey,
	truncateToWidth,
	visibleWidth,
} from "@singularity-forge/tui";
import { joinColumns, padRight } from "../shared/mod.js";
import {
	clearNotifications,
	markAllRead,
	onNotificationStoreChange,
	readNotifications,
} from "./notification-store.js";
import { formattedShortcutPair } from "./shortcut-defs.js";

/**
 * Cycle of filter modes used when cycling through filter states with 'f' key.
 */
const FILTER_CYCLE = ["all", "error", "warning", "info"];
/**
 * Returns a single-character icon representing the notification severity.
 */
function severityIcon(severity) {
	switch (severity) {
		case "error":
			return "✗";
		case "warning":
			return "⚠";
		case "success":
			return "✓";
		default:
			return "●";
	}
}
/**
 * Word-wrap plain text to fit within maxWidth columns. Splits on whitespace,
 * handles single words longer than maxWidth by truncating with ellipsis.
 */
function wrapText(text, maxWidth) {
	if (text.length <= maxWidth) return [text];
	const words = text.split(/\s+/);
	const lines = [];
	let current = "";
	for (const word of words) {
		if (current.length === 0) {
			current = word;
		} else if (current.length + 1 + word.length <= maxWidth) {
			current += " " + word;
		} else {
			lines.push(current);
			current = word;
		}
	}
	if (current.length > 0) lines.push(current);
	// If a single word exceeds maxWidth, truncate it
	return lines.map((l) =>
		l.length > maxWidth ? l.slice(0, maxWidth - 1) + "…" : l,
	);
}
/**
 * Format an ISO timestamp for display as relative time ("just now", "5m ago", etc.).
 * Falls back to HH:MM:SS if parsing fails.
 */
function formatTimestamp(ts) {
	try {
		const d = new Date(ts);
		const now = Date.now();
		const diffMs = now - d.getTime();
		if (diffMs < 60_000) return "just now";
		if (diffMs < 3600_000) return `${Math.floor(diffMs / 60_000)}m ago`;
		if (diffMs < 86400_000) return `${Math.floor(diffMs / 3600_000)}h ago`;
		return `${Math.floor(diffMs / 86400_000)}d ago`;
	} catch {
		return ts.slice(11, 19); // fallback: HH:MM:SS
	}
}
/**
 * Compute a string signature of notification entries for change detection.
 * Used to detect mutations across processes without re-reading entire file.
 */
function notificationSignature(entries) {
	return entries
		.map(
			(entry) =>
				`${entry.ts}|${entry.severity}|${entry.read ? 1 : 0}|${entry.repeatCount ?? 1}|${entry.lastTs ?? ""}|${entry.message}`,
		)
		.join("\n");
}
export class SFNotificationOverlay {
	tui;
	theme;
	onClose;
	cachedWidth;
	cachedLines;
	scrollOffset = 0;
	filterIndex = 0;
	entries = [];
	entriesSignature = "";
	refreshTimer;
	disposed = false;
	resizeHandler = null;
	unsubscribeStore = null;
	constructor(tui, theme, onClose) {
		this.tui = tui;
		this.theme = theme;
		this.onClose = onClose;
		// Mark all as read on open
		markAllRead();
		this.entries = readNotifications();
		this.entriesSignature = notificationSignature(this.entries);
		// Resize handler
		this.resizeHandler = () => {
			if (this.disposed) return;
			this.invalidate();
			this.tui.requestRender();
		};
		process.stdout.on("resize", this.resizeHandler);
		// Subscribe to store mutations for immediate updates
		this.unsubscribeStore = onNotificationStoreChange(() => {
			if (this.disposed) return;
			this._refreshFromDisk();
		});
		// 30s safety-net for cross-process edits (web subprocess, parallel workers)
		this.refreshTimer = setInterval(() => {
			if (this.disposed) return;
			this._refreshFromDisk();
		}, 30_000);
	}
	get filter() {
		return FILTER_CYCLE[this.filterIndex];
	}
	get filteredEntries() {
		if (this.filter === "all") return this.entries;
		return this.entries.filter((e) => e.severity === this.filter);
	}
	handleInput(data) {
		if (
			matchesKey(data, Key.escape) ||
			matchesKey(data, Key.ctrl("c")) ||
			matchesKey(data, Key.ctrlAlt("n")) ||
			matchesKey(data, Key.ctrlShift("n"))
		) {
			this.dispose();
			this.onClose();
			return;
		}
		// Scroll
		if (matchesKey(data, Key.down) || matchesKey(data, "j")) {
			this.scrollOffset++;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (matchesKey(data, Key.up) || matchesKey(data, "k")) {
			this.scrollOffset = Math.max(0, this.scrollOffset - 1);
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (data === "g") {
			this.scrollOffset = 0;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (data === "G") {
			this.scrollOffset = 999;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		// Filter cycle
		if (data === "f") {
			this.filterIndex = (this.filterIndex + 1) % FILTER_CYCLE.length;
			this.scrollOffset = 0;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		// Clear all
		if (data === "c") {
			clearNotifications();
			this.entries = [];
			this.entriesSignature = notificationSignature(this.entries);
			this.scrollOffset = 0;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
	}
	render(width) {
		if (this.cachedLines && this.cachedWidth === width) {
			return this.cachedLines;
		}
		const content = this.buildContentLines(width);
		const maxVisibleRows =
			Math.max(5, process.stdout.rows ? process.stdout.rows - 8 : 24) - 2;
		const visibleContentRows = Math.min(content.length, maxVisibleRows);
		const maxScroll = Math.max(0, content.length - visibleContentRows);
		this.scrollOffset = Math.min(this.scrollOffset, maxScroll);
		const visibleContent = content.slice(
			this.scrollOffset,
			this.scrollOffset + visibleContentRows,
		);
		const lines = this.wrapInBox(visibleContent, width);
		this.cachedWidth = width;
		this.cachedLines = lines;
		return lines;
	}
	invalidate() {
		this.cachedLines = undefined;
		this.cachedWidth = undefined;
	}
	dispose() {
		this.disposed = true;
		clearInterval(this.refreshTimer);
		if (this.unsubscribeStore) {
			this.unsubscribeStore();
			this.unsubscribeStore = null;
		}
		if (this.resizeHandler) {
			process.stdout.removeListener("resize", this.resizeHandler);
			this.resizeHandler = null;
		}
	}
	_refreshFromDisk() {
		const fresh = readNotifications();
		const signature = notificationSignature(fresh);
		if (signature !== this.entriesSignature) {
			markAllRead();
			this.entries = readNotifications();
			this.entriesSignature = notificationSignature(this.entries);
			this.invalidate();
			this.tui.requestRender();
		}
	}
	wrapInBox(inner, width) {
		const th = this.theme;
		const border = (s) => th.fg("borderAccent", s);
		const innerWidth = width - 4;
		const lines = [];
		lines.push(border("╭" + "─".repeat(width - 2) + "╮"));
		for (const line of inner) {
			const truncated = truncateToWidth(line, innerWidth);
			const padWidth = Math.max(0, innerWidth - visibleWidth(truncated));
			lines.push(
				border("│") +
					" " +
					truncated +
					" ".repeat(padWidth) +
					" " +
					border("│"),
			);
		}
		lines.push(border("╰" + "─".repeat(width - 2) + "╯"));
		return lines;
	}
	buildContentLines(width) {
		const th = this.theme;
		const shellWidth = width - 4;
		const contentWidth = Math.min(shellWidth, 128);
		const sidePad = Math.max(0, Math.floor((shellWidth - contentWidth) / 2));
		const leftMargin = " ".repeat(sidePad);
		const lines = [];
		const row = (content = "") => {
			const truncated = truncateToWidth(content, contentWidth);
			return leftMargin + padRight(truncated, contentWidth);
		};
		const blank = () => row("");
		const hr = () => row(th.fg("dim", "─".repeat(contentWidth)));
		// Header
		const title = th.fg("accent", th.bold("Notifications"));
		const filterLabel =
			this.filter === "all"
				? th.fg("dim", "all")
				: th.fg(
						this.filter === "error"
							? "error"
							: this.filter === "warning"
								? "warning"
								: "dim",
						this.filter,
					);
		const count = `${this.filteredEntries.length} entries`;
		lines.push(
			row(
				joinColumns(
					`${title}  ${th.fg("dim", "filter:")} ${filterLabel}`,
					th.fg("dim", count),
					contentWidth,
				),
			),
		);
		lines.push(hr());
		// Controls
		const closeShortcut = formattedShortcutPair("notifications");
		lines.push(
			row(
				th.fg(
					"dim",
					`↑/↓ scroll  f filter  c clear  Esc close  (${closeShortcut})`,
				),
			),
		);
		lines.push(blank());
		// Entries
		const filtered = this.filteredEntries;
		if (filtered.length === 0) {
			lines.push(blank());
			lines.push(
				row(
					th.fg(
						"dim",
						this.entries.length === 0
							? "No notifications yet."
							: `No ${this.filter} notifications.`,
					),
				),
			);
			lines.push(blank());
			return lines;
		}
		for (const entry of filtered) {
			const icon = severityIcon(entry.severity);
			const coloredIcon =
				entry.severity === "error"
					? th.fg("error", icon)
					: entry.severity === "warning"
						? th.fg("warning", icon)
						: entry.severity === "success"
							? th.fg("success", icon)
							: th.fg("dim", icon);
			const time = th.fg("dim", formatTimestamp(entry.ts));
			const sourceLabel =
				entry.metadata?.source ??
				(entry.source === "workflow-logger" ? "engine" : undefined);
			const source = sourceLabel ? th.fg("dim", ` [${sourceLabel}]`) : "";
			// Measure actual prefix width for wrapping
			const prefix = `${coloredIcon} ${time}${source}  `;
			const prefixWidth = visibleWidth(prefix);
			const msgMaxWidth = Math.max(10, contentWidth - prefixWidth);
			// Wrap long messages onto continuation lines indented to align with message start
			const repeatSuffix =
				(entry.repeatCount ?? 1) > 1
					? th.fg(
							"dim",
							` · ×${entry.repeatCount}${
								entry.lastTs ? ` (last ${formatTimestamp(entry.lastTs)})` : ""
							}`,
						)
					: "";
			const noticeKind =
				entry.noticeKind && entry.noticeKind !== "user_visible"
					? th.fg("dim", ` [${entry.noticeKind}]`)
					: "";
			const msgLines = wrapText(entry.message, msgMaxWidth);
			const indent = " ".repeat(prefixWidth);
			for (let i = 0; i < msgLines.length; i++) {
				if (i === 0) {
					lines.push(
						row(`${prefix}${msgLines[i]}${repeatSuffix}${noticeKind}`),
					);
				} else {
					lines.push(row(`${indent}${msgLines[i]}`));
				}
			}
		}
		return lines;
	}
}
