/**
 * Native desktop notifications by shelling out to per-platform binaries.
 *
 * No external dependency: the industry-standard `node-notifier` is
 * effectively abandoned (last publish April 2022), and its active fork
 * `toasted-notifier` does what this file does — subprocess to platform
 * tools — with similar footprint. Rolling our own keeps the surface tight
 * and makes the shell-escape behavior explicit.
 *
 * Platform matrix (April 2026):
 *
 *   macOS   → `osascript -e 'display notification …'`. Universal since
 *             10.8. No action buttons (AppleScript limitation).
 *   Linux   → `notify-send` (libnotify). Installed by default on most
 *             modern DEs; absent on minimal containers / WSL. We degrade
 *             to a log warning.
 *   Windows → `powershell.exe` invoking the built-in `[Windows.UI.Notifications]`
 *             WinRT API. No module install required on Win10+.
 *
 * All calls are fire-and-forget — a failed notifier should never break
 * the caller. Errors are logged at debug.
 */
import { spawn } from "child_process";
import { logger } from "../utils/logger.js";
/** Escape a string for inclusion in an AppleScript double-quoted literal.
 *  AppleScript strings can carry literal newlines and other control chars
 *  that — even when the outer quote-escape is correct — allow breaking out
 *  of the `display notification "…" with title …` clause and injecting
 *  e.g. a second `with title` or a `do shell script`. Strip every control
 *  char up to ASCII 0x1F plus DEL (0x7F); they have no legitimate place in
 *  a toast notification body. */
function escAppleScript(s) {
    return s
        .replace(/[\x00-\x1f\x7f]/g, " ")
        .replace(/\\/g, "\\\\")
        .replace(/"/g, '\\"');
}
/** PowerShell single-quoted literal: double internal single quotes. */
function escPowerShell(s) {
    return s.replace(/'/g, "''");
}
/** HTML/XML escape for content embedded in the Windows toast XML. The toast
 *  payload is parsed as XML by ToastNotificationManager; raw `<` `>` `&`
 *  break the parse or expand entities. */
function escXml(s) {
    return s
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");
}
/** UI-008: hard ceiling on a notifier subprocess. PowerShell toasts and a
 *  notify-send against a missing DBus session can hang indefinitely; kill the
 *  child after this many ms and treat it as a failure. */
const NOTIFIER_TIMEOUT_MS = 3000;
function defaultRunner(cmd, args) {
    return new Promise((resolve) => {
        try {
            const child = spawn(cmd, args, { stdio: "ignore", detached: false });
            let settled = false;
            const done = (code) => {
                if (settled)
                    return;
                settled = true;
                clearTimeout(timer);
                resolve({ code });
            };
            const timer = setTimeout(() => {
                try {
                    child.kill("SIGKILL");
                }
                catch { /* already gone */ }
                done(-1);
            }, NOTIFIER_TIMEOUT_MS);
            timer.unref?.();
            child.on("error", () => done(-1));
            child.on("close", (code) => done(code ?? 0));
        }
        catch {
            resolve({ code: -1 });
        }
    });
}
export class DesktopNotifier {
    platform;
    run;
    constructor(deps = {}) {
        this.platform = deps.platform ?? process.platform;
        this.run = deps.runner ?? defaultRunner;
    }
    /** Dispatch a notification. Resolves when the subprocess exits (fast). */
    async notify(n) {
        try {
            if (this.platform === "darwin")
                return await this.notifyMac(n);
            if (this.platform === "linux")
                return await this.notifyLinux(n);
            if (this.platform === "win32")
                return await this.notifyWindows(n);
            return { ok: false, platform: this.platform, reason: "unsupported_platform" };
        }
        catch (err) {
            logger.debug("DesktopNotifier dispatch failed", "DesktopNotifier", err);
            return { ok: false, platform: this.platform, reason: err.message };
        }
    }
    async notifyMac(n) {
        // AppleScript: display notification "body" with title "title" [subtitle "sub"] [sound name "name"]
        let script = `display notification "${escAppleScript(n.body)}" with title "${escAppleScript(n.title)}"`;
        if (n.subtitle)
            script += ` subtitle "${escAppleScript(n.subtitle)}"`;
        if (typeof n.sound === "string")
            script += ` sound name "${escAppleScript(n.sound)}"`;
        const { code } = await this.run("osascript", ["-e", script]);
        return code === 0 ? { ok: true, platform: "darwin" } : { ok: false, platform: "darwin", reason: `osascript exit ${code}` };
    }
    async notifyLinux(n) {
        const args = ["--app-name=mailpouch"];
        if (n.subtitle) {
            // notify-send doesn't have a subtitle; fold it into the body.
            args.push(n.title, `${n.subtitle}\n\n${n.body}`);
        }
        else {
            args.push(n.title, n.body);
        }
        const { code } = await this.run("notify-send", args);
        if (code === 0)
            return { ok: true, platform: "linux" };
        if (code === -1)
            return { ok: false, platform: "linux", reason: "notify-send not available (libnotify not installed?)" };
        return { ok: false, platform: "linux", reason: `notify-send exit ${code}` };
    }
    async notifyWindows(n) {
        // Inline PowerShell script uses the built-in WinRT toast API. No
        // module install required on Windows 10+; the AppID is fine as a
        // generic label since we don't care about Action Center grouping
        // for a single mailpouch deployment.
        const xml = `<toast><visual><binding template='ToastGeneric'>` +
            `<text>${escXml(n.title)}</text>` +
            (n.subtitle ? `<text>${escXml(n.subtitle)}</text>` : "") +
            `<text>${escXml(n.body)}</text>` +
            `</binding></visual>` +
            (n.sound === false ? `<audio silent='true'/>` : "") +
            `</toast>`;
        const ps = `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null;` +
            `$x = New-Object Windows.Data.Xml.Dom.XmlDocument;` +
            `$x.LoadXml('${escPowerShell(xml)}');` +
            `$t = [Windows.UI.Notifications.ToastNotification]::new($x);` +
            `[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('mailpouch').Show($t);`;
        const { code } = await this.run("powershell.exe", ["-NoProfile", "-Command", ps]);
        return code === 0 ? { ok: true, platform: "win32" } : { ok: false, platform: "win32", reason: `powershell exit ${code}` };
    }
}
//# sourceMappingURL=desktop.js.map