// SF Extension — Desktop Notification Helper
// Cross-platform desktop notifications for autonomous mode events.
import { execFileSync } from "node:child_process";
import {
	CmuxClient,
	emitOsc777Notification,
	resolveCmuxConfig,
} from "./cmux/index.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
/**
 * Send a native desktop notification. Non-blocking, non-fatal.
 * macOS: osascript, Linux: notify-send, Windows: skipped.
 */
export function sendDesktopNotification(
	title,
	message,
	level = "info",
	kind = "complete",
	projectName,
) {
	// When a projectName is provided and the title is the default "SF",
	// replace it with a project-qualified title for multi-project clarity.
	if (projectName && title === "SF") {
		title = formatNotificationTitle(projectName);
	}
	const loaded = loadEffectiveSFPreferences()?.preferences;
	if (!shouldSendDesktopNotification(kind, loaded?.notifications)) return;
	const cmux = resolveCmuxConfig(loaded);
	if (cmux.notifications) {
		const delivered = CmuxClient.fromPreferences(loaded).notify(title, message);
		if (delivered) return;
		emitOsc777Notification(title, message);
	}
	try {
		const command = buildDesktopNotificationCommand(
			process.platform,
			title,
			message,
			level,
		);
		if (!command) return;
		execFileSync(command.file, command.args, {
			timeout: 3000,
			stdio: "ignore",
		});
	} catch {
		// Non-fatal — desktop notifications are best-effort
	}
}
/**
 * Check if a desktop notification should be sent based on notification kind
 * and user preferences. Defaults to preferences from config if not provided.
 */
export function shouldSendDesktopNotification(
	kind,
	preferences = loadEffectiveSFPreferences()?.preferences.notifications,
) {
	if (preferences?.enabled === false) return false;
	switch (kind) {
		case "error":
			return preferences?.on_error ?? true;
		case "budget":
			return preferences?.on_budget ?? true;
		case "milestone":
			return preferences?.on_milestone ?? true;
		case "attention":
			return preferences?.on_attention ?? true;
		default:
			return preferences?.on_complete ?? true;
	}
}
/**
 * Format a notification title that includes the project name for context.
 * Returns "SF — projectName" when a project name is available, otherwise "SF".
 */
export function formatNotificationTitle(projectName) {
	const trimmed = projectName?.trim();
	if (trimmed) return `SF — ${trimmed}`;
	return "SF";
}
/**
 * Build platform-specific command for desktop notification. Uses terminal-notifier
 * on macOS, notify-send on Linux, and returns null on Windows.
 */
export function buildDesktopNotificationCommand(
	platform,
	title,
	message,
	level = "info",
) {
	const normalizedTitle = normalizeNotificationText(title);
	const normalizedMessage = normalizeNotificationText(message);
	if (platform === "darwin") {
		// Prefer terminal-notifier: registers as its own Notification Center app,
		// so it gets a proper permission entry in System Settings → Notifications.
		// osascript notifications are silently swallowed when the calling terminal
		// (Ghostty, iTerm2, etc.) lacks notification permissions — exits 0, no error.
		// See: https://github.com/singularity-forge/sf-run/issues/2632
		const tnPath = findExecutable("terminal-notifier");
		if (tnPath) {
			const sound = level === "error" ? "Basso" : "Glass";
			return {
				file: tnPath,
				args: [
					"-title",
					normalizedTitle,
					"-message",
					normalizedMessage,
					"-sound",
					sound,
				],
			};
		}
		// Fallback: osascript (works if terminal app has notification permissions)
		const sound =
			level === "error" ? 'sound name "Basso"' : 'sound name "Glass"';
		const script = `display notification "${escapeAppleScript(normalizedMessage)}" with title "${escapeAppleScript(normalizedTitle)}" ${sound}`;
		return { file: "osascript", args: ["-e", script] };
	}
	if (platform === "linux") {
		const urgency =
			level === "error" ? "critical" : level === "warning" ? "normal" : "low";
		return {
			file: "notify-send",
			args: ["-u", urgency, normalizedTitle, normalizedMessage],
		};
	}
	return null;
}
function normalizeNotificationText(s) {
	return s.replace(/\r?\n/g, " ").trim();
}
function escapeAppleScript(s) {
	return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
/**
 * Locate an executable on PATH. Returns absolute path or null.
 * Non-fatal — returns null on any error.
 */
function findExecutable(name) {
	try {
		return (
			execFileSync("which", [name], {
				timeout: 2000,
				stdio: ["ignore", "pipe", "ignore"],
			})
				.toString()
				.trim() || null
		);
	} catch {
		return null;
	}
}
