import { spawn, execFileSync } from 'child_process';
import { platform } from 'os';
import path from 'path';
import { pathToFileURL } from 'url';
import fs from 'fs';
import AnsiToHtml from 'ansi-to-html';
// Load package.json for version
import packageJson from './package.json' with { type: 'json' };
export class MessageBoxHandle {
    result;
    pid;
    child;
    _closed = false;
    constructor(child, resultPromise) {
        this.child = child;
        this.pid = child.pid;
        this.result = resultPromise;
    }
    close() {
        if (this._closed) {
            return false;
        }
        this._closed = true;
        return this.child.kill();
    }
    get closed() {
        return this._closed;
    }
}
const activeDialogs = new Map();
export function closeMessageBox(pid) {
    const handle = activeDialogs.get(pid);
    if (handle) {
        return handle.close();
    }
    // Fallback to process.kill()
    try {
        process.kill(pid);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Internal function to create a MessageBoxHandle with child process
 * @param options Message box configuration
 * @returns MessageBoxHandle with result promise
 */
/** Per-user bin dir for timestamped exe copies. Storing them OUTSIDE
 *  node_modules avoids npm's EPERM cleanup warnings on upgrade — npm tries to
 *  delete the previous package's `.msger-<rand>/` aside-dir but the running
 *  exe inside it is locked. Keeping timestamped copies here means npm only
 *  manages the stable `msgernative.exe` (which isn't the one being executed),
 *  so it's never locked. */
/** App name for per-user bin dir + per-app exe filename. Set via `setAppName()`
 *  so each app using msger gets its own binary path
 *  (`%LOCALAPPDATA%\<appName>\bin\<appName>.exe` on Windows) and thus its own
 *  taskbar identity, Task Manager process name, and pin target. Defaults to
 *  "msger" for standalone use, which preserves the original `msgernative.exe`
 *  behavior. */
let _appName = "msger";
/** Optional per-app icon path. Set via `setAppIcon()`. When set, msger copies
 *  it next to the per-app exe so Rust's default-icon search picks it up — no
 *  need to pass `icon` on every spawn call. PNG and ICO accepted. */
let _appIcon;
/** Set the app name used for the per-user bin dir AND the per-app exe filename.
 *  Call before showService/showMessageBox — e.g. `setAppName("mailx")` makes
 *  the running process appear as `mailx.exe` in Task Manager and the taskbar. */
export function setAppName(name) { _appName = name; }
/** Set a per-app icon (PNG or ICO). Copied next to the per-app exe at first
 *  provision so Rust's window icon search picks it up. Per-call `icon` in
 *  `MessageBoxOptions` still takes precedence. */
export function setAppIcon(iconPath) { _appIcon = iconPath; }
function getUserBinDirFor(appName) {
    const isWindows = platform() === 'win32';
    if (isWindows) {
        const base = process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || '.', 'AppData', 'Local');
        return path.join(base, appName, 'bin');
    }
    const xdg = process.env.XDG_DATA_HOME;
    const base = xdg || path.join(process.env.HOME || '.', '.local', 'share');
    return path.join(base, appName, 'bin');
}
function getUserBinDir() { return getUserBinDirFor(_appName); }
/** Locate the msger-managed source binary that per-app exes hardlink to.
 *  Prefers `%LOCALAPPDATA%\msger\bin\msgernative.exe` (postinstall's stable
 *  name), falls back to a per-user timestamped copy, then to the package's
 *  bundled binary. Returns null only if msger truly isn't installed. */
function findMsgerSource() {
    const isWindows = platform() === 'win32';
    const isDarwin = platform() === 'darwin';
    const arch = process.arch;
    const ext = isWindows ? '.exe' : '';
    let baseName;
    if (isWindows) {
        baseName = 'msgernative';
    }
    else if (isDarwin) {
        // Mac binaries are built infrequently on a Mac and synced via OneDrive.
        // Names are explicit so they don't collide with the Pi/Linux ARM binaries.
        baseName = arch === 'arm64' ? 'msgernative-darwin-arm64' : 'msgernative-darwin-x64';
    }
    else if (arch === 'arm64') {
        baseName = 'msgernative-linux-aarch64';
    }
    else {
        baseName = 'msgernative';
    }
    const msgerBin = getUserBinDirFor('msger');
    const pkgBin = path.join(import.meta.dirname, 'msger-native', 'bin');
    const stable = path.join(msgerBin, `${baseName}${ext}`);
    if (fs.existsSync(stable))
        return stable;
    const pattern = new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-(\\d+)${ext.replace('.', '\\.')}$`);
    for (const dir of [msgerBin, pkgBin]) {
        try {
            const matches = fs.readdirSync(dir).filter(f => pattern.test(f)).sort().reverse();
            if (matches.length > 0)
                return path.join(dir, matches[0]);
        }
        catch { /* dir doesn't exist — keep looking */ }
    }
    const bundled = path.join(pkgBin, `${baseName}${ext}`);
    if (fs.existsSync(bundled))
        return bundled;
    return null;
}
/** Sweep stale `.aside-*` / `.old-*` siblings of `perAppExe`. Best-effort —
 *  files held open by an older instance throw EPERM and stay until the next
 *  launch when nothing has them mapped. */
function sweepAsideSiblings(perAppExe) {
    try {
        const dir = path.dirname(perAppExe);
        const base = path.basename(perAppExe);
        const pat = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.(?:old|aside|tmp)-(\\d+)$`);
        for (const f of fs.readdirSync(dir)) {
            const m = pat.exec(f);
            if (!m)
                continue;
            try {
                fs.unlinkSync(path.join(dir, f));
            }
            catch { /* still locked, try next time */ }
        }
    }
    catch { /* fall through */ }
}
/** Inject `.ico` into `exePath` as the IDI_ICON1 / RT_GROUP_ICON resource so
 *  the per-app exe shows the app's own icon in Explorer, Task Manager, the
 *  taskbar, and pinned shortcuts — every Windows context that reads the exe
 *  resource (which `with_window_icon` doesn't reach). Sync — invokes the
 *  shipped `msgernative.exe --update-icon` subcommand (no external deps;
 *  msger stays self-contained). Returns true on success. Failures don't
 *  break launch — the launcher just runs without the embedded icon. */
function injectIcon(msgerNative, exePath, icoPath) {
    try {
        execFileSync(msgerNative, ["--update-icon", exePath, icoPath], { stdio: "pipe" });
        return true;
    }
    catch (e) {
        const stderr = (e?.stderr ?? "").toString().trim();
        console.error(`  msger: --update-icon failed for ${exePath}: ${e?.message || e}${stderr ? ` (${stderr})` : ""}`);
        return false;
    }
}
/** Ensure a per-app exe is present at `perAppExe`, derived from `sourceExe`,
 *  with the per-app icon resource injected. Two modes:
 *
 *  - `_appIcon` is set (the per-app-identity case): the per-app exe is a
 *    *real copy*, not a hardlink, because `--update-icon` rewrites the resource
 *    section and we don't want that change to bleed back into the shared
 *    `msgernative.exe` (which a hardlink would). Staleness compared via
 *    mtime of source exe + mtime of source icon. Refresh uses the
 *    rename-aside pattern so a running instance doesn't block the update.
 *
 *  - `_appIcon` is unset: legacy hardlink behavior — `mailx.exe` shares an
 *    inode with `msgernative.exe`, no resource injection. Same as before.
 */
function ensureFresh(perAppExe, sourceExe) {
    fs.mkdirSync(path.dirname(perAppExe), { recursive: true });
    // Hardlink path (callers that didn't `setAppIcon`) — unchanged behavior.
    if (!_appIcon || !fs.existsSync(_appIcon)) {
        if (!fs.existsSync(perAppExe)) {
            try {
                fs.linkSync(sourceExe, perAppExe);
            }
            catch {
                fs.copyFileSync(sourceExe, perAppExe);
            }
            return;
        }
        if (fs.statSync(perAppExe).ino === fs.statSync(sourceExe).ino)
            return;
        const aside = `${perAppExe}.aside-${Date.now()}`;
        try {
            fs.renameSync(perAppExe, aside);
        }
        catch {
            return;
        }
        try {
            fs.linkSync(sourceExe, perAppExe);
        }
        catch {
            fs.copyFileSync(sourceExe, perAppExe);
        }
        sweepAsideSiblings(perAppExe);
        return;
    }
    // Copy + --update-icon path. Stale when the per-app exe is missing OR the source
    // exe is newer (msger updated) OR the source icon is newer (icon changed).
    const srcMtime = fs.statSync(sourceExe).mtimeMs;
    const icoMtime = fs.statSync(_appIcon).mtimeMs;
    let stale = !fs.existsSync(perAppExe);
    if (!stale) {
        const dstMtime = fs.statSync(perAppExe).mtimeMs;
        stale = dstMtime < srcMtime || dstMtime < icoMtime;
    }
    if (!stale)
        return;
    // Build the new exe out-of-band so a running instance never sees a
    // half---update-icon'd file. Sequence: copy → --update-icon tmp → rename old aside →
    // rename tmp into place. All renames work even on locked files; only
    // delete is blocked, and we defer that to the next launch's sweep.
    const stamp = Date.now();
    const tmpExe = `${perAppExe}.tmp-${stamp}`;
    const isIco = _appIcon.toLowerCase().endsWith(".ico");
    try {
        fs.copyFileSync(sourceExe, tmpExe);
        let injected = false;
        if (isIco) {
            injected = injectIcon(sourceExe, tmpExe, _appIcon);
        }
        if (!isIco || !injected) {
            // No .ico available, or the resource update failed — fall back
            // to a plain copy. Window-icon path still gets set via Tao at
            // launch; we just don't get the EXE-level resource.
        }
        if (fs.existsSync(perAppExe)) {
            try {
                fs.renameSync(perAppExe, `${perAppExe}.aside-${stamp}`);
            }
            catch { /* held open — leave the old file in place; we'll retry */
                try {
                    fs.unlinkSync(tmpExe);
                }
                catch { /* */ }
                return;
            }
        }
        fs.renameSync(tmpExe, perAppExe);
        sweepAsideSiblings(perAppExe);
    }
    catch (e) {
        console.error(`  msger: per-app exe provision failed (${perAppExe}): ${e?.message || e}`);
        try {
            fs.unlinkSync(tmpExe);
        }
        catch { /* */ }
    }
}
/** Copy the per-app icon next to the per-app exe so Rust's default-icon
 *  search (`load_icon` in main.rs) picks it up. Rust looks for `msger.ico`
 *  / `msger.png` adjacent to the exe — we drop our icon under that name so
 *  no Rust changes are needed. Only re-copies when source is newer. */
function provisionAppIcon(perAppDir) {
    if (!_appIcon || !fs.existsSync(_appIcon))
        return;
    const iconExt = path.extname(_appIcon).toLowerCase();
    if (iconExt !== '.png' && iconExt !== '.ico')
        return;
    const dest = path.join(perAppDir, `msger${iconExt}`);
    try {
        if (fs.existsSync(dest) && fs.statSync(dest).mtimeMs >= fs.statSync(_appIcon).mtimeMs)
            return;
        fs.copyFileSync(_appIcon, dest);
    }
    catch { /* non-fatal — caller can still pass icon per-call */ }
}
/** Resolve the native binary path. Two modes:
 *  - `_appName === "msger"` (default, standalone msger use): unchanged from
 *    the original — find `msgernative.exe` in user bin, fall back to legacy
 *    timestamped copies, finally to the package-bundled binary.
 *  - `_appName !== "msger"` (per-app, e.g. mailx): provision a hardlink at
 *    `%LOCALAPPDATA%\<_appName>\bin\<_appName>.exe` pointing at the msger
 *    source binary, refresh the link if msger has been updated, copy the
 *    icon next to it, return the per-app path. This is what gives mailx its
 *    own taskbar identity. */
function resolveBinaryPath() {
    const isWindows = platform() === 'win32';
    const isDarwin = platform() === 'darwin';
    const arch = process.arch;
    const pkgBinDir = path.join(import.meta.dirname, 'msger-native', 'bin');
    const userBinDir = getUserBinDir();
    let baseName;
    let ext;
    if (isWindows) {
        baseName = 'msgernative';
        ext = '.exe';
    }
    else if (isDarwin) {
        baseName = arch === 'arm64' ? 'msgernative-darwin-arm64' : 'msgernative-darwin-x64';
        ext = '';
    }
    else if (arch === 'arm64') {
        baseName = 'msgernative-linux-aarch64';
        ext = '';
    }
    else {
        baseName = 'msgernative';
        ext = '';
    }
    // Per-app mode: provision and use a renamed hardlink so taskbar / Task
    // Manager / pinned shortcuts see the app's own identity.
    if (_appName !== 'msger') {
        const source = findMsgerSource();
        if (source) {
            const perAppExe = path.join(userBinDir, `${_appName}${ext}`);
            try {
                ensureFresh(perAppExe, source);
                provisionAppIcon(userBinDir);
                if (fs.existsSync(perAppExe))
                    return perAppExe;
            }
            catch {
                // Provisioning failed — fall through to the source binary so
                // the app at least launches, just with msger's identity.
            }
            return source;
        }
        // No source found — fall through to the legacy lookup below, which
        // will return the package-bundled binary as a last resort.
    }
    // Default msger-itself path (or per-app fallback if findMsgerSource failed).
    const stableLink = path.join(userBinDir, `${baseName}${ext}`);
    if (fs.existsSync(stableLink))
        return stableLink;
    const pattern = new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-(\\d+)${ext.replace('.', '\\.')}$`);
    // Fallback: per-user timestamped copy (pre-hardlink installs)
    try {
        const matches = fs.readdirSync(userBinDir)
            .filter(f => pattern.test(f))
            .sort()
            .reverse();
        if (matches.length > 0)
            return path.join(userBinDir, matches[0]);
    }
    catch { /* user dir doesn't exist yet — fall through */ }
    // Legacy: timestamped copies inside the package (older installs that ran
    // the old postinstall). Still honored so a half-migrated tree keeps working.
    try {
        const matches = fs.readdirSync(pkgBinDir)
            .filter(f => pattern.test(f))
            .sort()
            .reverse();
        if (matches.length > 0)
            return path.join(pkgBinDir, matches[0]);
    }
    catch { /* fall through */ }
    return path.join(pkgBinDir, `${baseName}${ext}`);
}
function createMessageBoxHandle(options) {
    const binaryPath = resolveBinaryPath();
    // Validate the binary up front and synchronously. Doing this inside the
    // Promise executor used to leave `child` undefined when the binary was
    // missing, then `new MessageBoxHandle(child, ...)` blew up on `child.pid`
    // with a cryptic "cannot read properties of undefined" — masking the real
    // "binary not found" message. Throw directly here so callers see the
    // actual cause.
    if (!fs.existsSync(binaryPath)) {
        throw new Error(`msger native binary not found at: ${binaryPath}\n` +
            `Platform: ${platform()} ${process.arch}. ` +
            `If you just installed @bobfrankston/msger, the published package may be missing the binary for this platform.`);
    }
    if (platform() !== 'win32') {
        try {
            fs.accessSync(binaryPath, fs.constants.X_OK);
        }
        catch {
            throw new Error(`msger native binary is not executable: ${binaryPath}\n` +
                `Please run: chmod +x "${binaryPath}"`);
        }
    }
    const childEnv = { ...process.env };
    if ('NODE_OPTIONS' in childEnv) {
        delete childEnv.NODE_OPTIONS;
    }
    // Spawn before constructing the Promise so `child` is guaranteed defined
    // by the time we hand it to MessageBoxHandle.
    const child = spawn(binaryPath, [], {
        stdio: options.detach ? ['pipe', 'ignore', 'ignore'] : ['pipe', 'pipe', 'pipe'],
        detached: options.detach || false,
        env: childEnv
    });
    let stdout = '';
    let stderr = '';
    const resultPromise = new Promise((resolve, reject) => {
        // If detached, unref the child so parent can exit
        if (options.detach) {
            child.unref();
        }
        // Collect stdout (only if not detached)
        child.stdout?.on('data', (data) => {
            stdout += data.toString();
        });
        // Collect stderr (only if not detached)
        child.stderr?.on('data', (data) => {
            stderr += data.toString();
        });
        // Handle process completion
        child.on('close', (code) => {
            // code is null when process is killed via handle.close()
            if (code === null) {
                resolve({ button: 'closed', closed: true });
                return;
            }
            if (code !== 0) {
                reject(new Error(`msger exited with code ${code}: ${stderr}`));
                return;
            }
            try {
                const result = JSON.parse(stdout.trim());
                resolve(result);
            }
            catch (error) {
                reject(new Error(`Failed to parse result: ${error.message}\nOutput: ${stdout}`));
            }
        });
        // Handle process errors
        child.on('error', (error) => {
            reject(new Error(`Failed to spawn msger: ${error.message}`));
        });
        // Handle stdin errors (e.g., EPIPE when child dies early)
        child.stdin.on('error', (error) => {
            // Don't reject here - the 'close' or 'error' event will handle it
            // This prevents unhandled EPIPE errors
        });
        // Send options to stdin
        try {
            // If autoSize is not explicitly set, enable it when no size is specified
            const optionsToSend = { ...options };
            if (optionsToSend.autoSize === undefined && !optionsToSend.size) {
                optionsToSend.autoSize = true;
            }
            // Convert ANSI escape sequences to HTML if message is provided
            // ansi-to-html will escape HTML characters and convert ANSI codes to colored spans
            // Only do this if the message contains actual ANSI codes to preserve plain text as-is
            if (optionsToSend.message && !optionsToSend.html) {
                // Check if message actually contains ANSI codes
                if (optionsToSend.message.includes('\x1b[')) {
                    const convert = new AnsiToHtml({ fg: '#000000', bg: '#ffffff' });
                    let htmlContent = convert.toHtml(optionsToSend.message);
                    // ansi-to-html preserves newlines, so we need to convert them to <br> for HTML
                    htmlContent = htmlContent.replace(/\r?\n/g, '<br>');
                    // Move the converted content to html field and clear message
                    optionsToSend.html = htmlContent;
                    delete optionsToSend.message;
                }
            }
            // Convert URL to absolute file:// URL if it's a file path. Skip
            // when contentDir is set — the URL is meant for the custom
            // protocol, not file://.
            if (optionsToSend.url && !optionsToSend.contentDir && !optionsToSend.url.startsWith('http://') && !optionsToSend.url.startsWith('https://') && !optionsToSend.url.startsWith('file://') && !optionsToSend.url.startsWith('msger://')) {
                // It's a file path, convert to absolute file:// URL
                // Resolve to absolute path (handles both / and \ separators)
                const absolutePath = path.resolve(optionsToSend.url);
                // Check if file exists
                if (!fs.existsSync(absolutePath)) {
                    throw new Error(`File not found: ${absolutePath}`);
                }
                // Convert to file:// URL and normalize for Windows
                let fileUrl = pathToFileURL(absolutePath).href;
                // Ensure proper URL encoding
                optionsToSend.url = fileUrl;
                // Debug logging (uncomment if needed)
                // console.error(`[DEBUG] Path conversion: ${optionsToSend.url} → ${absolutePath} → ${fileUrl}`);
            }
            // Resolve contentDir to absolute path so the Rust custom-protocol
            // handler doesn't have to guess what cwd was at spawn time.
            if (optionsToSend.contentDir) {
                optionsToSend.contentDir = path.resolve(optionsToSend.contentDir);
            }
            // Append hash to URL if specified
            if (optionsToSend.hash && optionsToSend.url) {
                // Strip any existing hash from the base URL
                optionsToSend.url = optionsToSend.url.split('#')[0];
                // Remove leading # from hash if present to avoid ##
                const hashValue = optionsToSend.hash.startsWith('#') ? optionsToSend.hash.substring(1) : optionsToSend.hash;
                optionsToSend.url = `${optionsToSend.url}#${hashValue}`;
                // Remove hash from options as it's now part of URL
                delete optionsToSend.hash;
            }
            else if (optionsToSend.hash && !optionsToSend.url) {
                throw new Error('hash option requires url to be set');
            }
            // Flatten size object for Rust binary (expects width/height as top-level fields)
            const rustOptions = { ...optionsToSend };
            // If no title was provided, send a default that uses the live JS-side
            // package.json version. The Rust binary's own default_title() embeds
            // the version at compile time (build.rs), so it goes stale whenever
            // the JS package bumps without a Rust rebuild — this keeps the title
            // current without requiring a rebuild.
            if (rustOptions.title === undefined || rustOptions.title === null) {
                rustOptions.title = `msger ${packageJson.version}`;
            }
            // Add version to title only if showVersion flag is set
            if (rustOptions.showVersion) {
                if (rustOptions.title) {
                    rustOptions.title = `${rustOptions.title} [msger v${packageJson.version}]`;
                }
                else {
                    rustOptions.title = `Message [msger v${packageJson.version}]`;
                }
                delete rustOptions.showVersion; // Don't send this to Rust binary
            }
            if (rustOptions.size) {
                rustOptions.width = rustOptions.size.width;
                rustOptions.height = rustOptions.size.height;
                delete rustOptions.size;
            }
            if (rustOptions.pos) {
                // Rust expects pos fields directly
                rustOptions.pos = {
                    x: rustOptions.pos.x,
                    y: rustOptions.pos.y,
                    screen: rustOptions.pos.screen
                };
            }
            // Normalize aumid: both appUserModelId and aumid → aumid (Rust field name)
            if (!rustOptions.aumid && rustOptions.appUserModelId) {
                rustOptions.aumid = rustOptions.appUserModelId;
            }
            delete rustOptions.appUserModelId;
            // Pass app_name so the Rust side scopes its WebView2 user-data
            // dir per app (one wedged profile no longer takes out every
            // msger-hosted app's state). _appName defaults to "msger" so
            // legacy callers retain the old shared profile path.
            if (!rustOptions.app_name)
                rustOptions.app_name = _appName;
            const jsonToSend = JSON.stringify(rustOptions);
            // If detached, resolve immediately after stdin is closed
            if (options.detach) {
                child.stdin.write(jsonToSend);
                child.stdin.end(() => {
                    resolve({ button: 'detached', dismissed: false });
                });
            }
            else {
                child.stdin.write(jsonToSend);
                child.stdin.end();
            }
        }
        catch (error) {
            reject(new Error(`Failed to write to stdin: ${error.message}`));
        }
    });
    return new MessageBoxHandle(child, resultPromise);
}
/**
 * Show a message box dialog using native Rust implementation (extended version)
 * @param options Message box configuration
 * @returns MessageBoxHandle with result promise and control methods
 */
export function showMessageBoxEx(options) {
    const handle = createMessageBoxHandle(options);
    // Register in activeDialogs with auto-cleanup
    if (handle.pid !== undefined) {
        activeDialogs.set(handle.pid, handle);
        // Auto-cleanup when result resolves
        handle.result.finally(() => {
            if (handle.pid !== undefined) {
                activeDialogs.delete(handle.pid);
            }
        });
    }
    return handle;
}
/**
 * Show a message box dialog using native Rust implementation
 * @param options Message box configuration
 * @returns Promise that resolves with user's response
 */
export async function showMessageBox(options) {
    return showMessageBoxEx(options).result;
}
/** Handle for a service-mode msger instance. Bidirectional IPC with the WebView. */
export class ServiceHandle {
    child;
    _closed = false;
    _onRequest = null;
    closed;
    constructor(child) {
        this.child = child;
        // Parse stdout as newline-delimited JSON (requests from WebView)
        let buffer = "";
        child.stdout?.on("data", (data) => {
            buffer += data.toString();
            let newline;
            while ((newline = buffer.indexOf("\n")) >= 0) {
                const line = buffer.slice(0, newline).trim();
                buffer = buffer.slice(newline + 1);
                if (!line)
                    continue;
                try {
                    const msg = JSON.parse(line);
                    if (this._onRequest)
                        this._onRequest(msg);
                }
                catch { /* ignore malformed lines */ }
            }
        });
        this.closed = new Promise((resolve) => {
            child.on("close", () => { this._closed = true; resolve(); });
        });
    }
    /** Register handler for requests from the WebView (_action messages) */
    onRequest(handler) {
        this._onRequest = handler;
    }
    /** Send a response or event to the WebView (written to child's stdin) */
    send(msg) {
        if (this._closed)
            return;
        this.child.stdin?.write(JSON.stringify(msg) + "\n");
    }
    /** Close the window */
    close() {
        if (this._closed)
            return;
        this._closed = true;
        this.child.kill();
    }
}
/**
 * Open msger in service mode — bidirectional IPC, no HTTP.
 * Parent process handles requests via onRequest, sends responses via send().
 */
export function showService(options) {
    const serviceOptions = { ...options, service: true, detach: false };
    const binaryPath = resolveBinaryPath();
    const childEnv = { ...process.env };
    if ("NODE_OPTIONS" in childEnv)
        delete childEnv.NODE_OPTIONS;
    const child = spawn(binaryPath, [], {
        stdio: ["pipe", "pipe", "pipe"],
        env: childEnv,
    });
    child.stderr?.on("data", (data) => {
        // Forward Rust stderr to parent stderr (debug messages)
        process.stderr.write(data);
    });
    child.stdin?.on("error", () => { });
    // Send options as first line of stdin (Rust reads one line for options, then keeps reading)
    const rustOptions = { ...serviceOptions };
    if (rustOptions.size) {
        rustOptions.width = rustOptions.size.width;
        rustOptions.height = rustOptions.size.height;
        delete rustOptions.size;
    }
    // Convert local paths to file:// URLs when no custom protocol (contentDir) is used
    if (!rustOptions.contentDir && rustOptions.url && !rustOptions.url.startsWith("http://") && !rustOptions.url.startsWith("https://") && !rustOptions.url.startsWith("file://")) {
        const absolutePath = path.resolve(rustOptions.url);
        if (!fs.existsSync(absolutePath)) {
            throw new Error(`File not found: ${absolutePath}`);
        }
        rustOptions.url = pathToFileURL(absolutePath).href;
    }
    // Resolve contentDir to absolute path
    if (rustOptions.contentDir) {
        rustOptions.contentDir = path.resolve(rustOptions.contentDir);
    }
    // Normalize aumid: both appUserModelId and aumid → aumid (Rust field name)
    if (!rustOptions.aumid && rustOptions.appUserModelId) {
        rustOptions.aumid = rustOptions.appUserModelId;
    }
    delete rustOptions.appUserModelId;
    // Per-app WebView2 profile dir — see corresponding showMessageBox path
    // for rationale. Pass app_name so the Rust side scopes its profile
    // under `%LOCALAPPDATA%\<appName>\webview2` instead of the shared
    // `%LOCALAPPDATA%\msger\webview2`.
    if (!rustOptions.app_name)
        rustOptions.app_name = _appName;
    const jsonToSend = JSON.stringify(rustOptions);
    if (process.env.MSGER_DEBUG)
        console.log(`[service] Sending to Rust: ${jsonToSend.slice(0, 200)}`);
    child.stdin?.write(jsonToSend + "\n");
    return new ServiceHandle(child);
}
