import { createChildLogger } from '../utils/logger.js';
const log = createChildLogger('integrations:browser-session');
const sessions = new Map();
const IDLE_MS = 15 * 60 * 1000;
const CLOSE_GRACE_MS = 2_000;
// (Round-7 fix) Per-tenant async lock so concurrent getOrCreateSession /
// closeSession calls don't race. Without this, two simultaneous tool calls
// for the same tenant on a cold start could both try to launch Chromium,
// leaving an orphaned browser process. Likewise, getOrCreate+close could
// interleave and leave a session map entry pointing at a closed browser.
const sessionLocks = new Map();
async function withSessionLock(tenantId, fn) {
    while (sessionLocks.has(tenantId)) {
        try {
            await sessionLocks.get(tenantId);
        }
        catch { /* prior holder failed; we still get a turn */ }
    }
    const p = (async () => fn())();
    sessionLocks.set(tenantId, p);
    try {
        return await p;
    }
    finally {
        if (sessionLocks.get(tenantId) === p)
            sessionLocks.delete(tenantId);
    }
}
let exitHandlerInstalled = false;
function installExitHandler() {
    if (exitHandlerInstalled)
        return;
    exitHandlerInstalled = true;
    const closeAll = async () => {
        const ids = [...sessions.keys()];
        await Promise.all(ids.map((id) => closeSession(id, { force: true }).catch(() => undefined)));
    };
    // (Round-7 fix) Use async handlers and wait for closeAll() to settle
    // before letting Node exit. Previously `void closeAll()` returned
    // immediately and the process exited before browsers were closed,
    // leaving Chromium temp dirs and zombies on hard kill.
    const onExit = (signal) => async () => {
        log.info({ signal, sessions: sessions.size }, 'browser exit handler firing');
        try {
            await Promise.race([
                closeAll(),
                new Promise((resolve) => setTimeout(resolve, CLOSE_GRACE_MS + 1000)),
            ]);
        }
        catch (err) {
            log.warn({ err: err.message }, 'closeAll threw on exit');
        }
    };
    process.once('SIGTERM', () => void onExit('SIGTERM')());
    process.once('SIGINT', () => void onExit('SIGINT')());
    process.once('beforeExit', () => void onExit('beforeExit')());
}
function scheduleIdleClose(session, tenantId) {
    clearTimeout(session.idleTimer);
    session.idleTimer = setTimeout(() => {
        if (session.inFlight > 0) {
            // Defer — let the in-flight tool finish, then close. The wrapper in
            // releaseSlot() picks up `pendingClose=true` on the last release.
            session.pendingClose = true;
            log.debug({ tenantId, inFlight: session.inFlight }, 'idle timer fired but tool in flight — deferring close');
            return;
        }
        void closeSession(tenantId).catch((err) => log.debug({ err: err.message }, 'idle close failed'));
    }, IDLE_MS);
}
/**
 * Run an operation against a tenant's browser session with refcount protection.
 * Use this from every browser_* tool — it guarantees the session won't be
 * closed mid-operation by the idle timer.
 */
export async function withBrowser(tenantId, fn, options = {}) {
    const session = await getOrCreateSession(tenantId, options);
    session.inFlight++;
    try {
        return await fn(session);
    }
    finally {
        session.inFlight--;
        if (session.pendingClose && session.inFlight === 0) {
            session.pendingClose = false;
            void closeSession(tenantId).catch((err) => log.debug({ err: err.message }, 'deferred close failed'));
        }
        else if (session.inFlight === 0) {
            // Reset idle timer on every release (refresh-on-use semantics)
            scheduleIdleClose(session, tenantId);
        }
    }
}
export async function getOrCreateSession(tenantId, options = {}) {
    installExitHandler();
    // (Round-7 fix) Serialize get-or-create per tenant so concurrent callers
    // share the same browser launch instead of racing to spawn two.
    return withSessionLock(tenantId, async () => {
        const existing = sessions.get(tenantId);
        if (existing) {
            existing.lastUsed = Date.now();
            if (existing.inFlight === 0)
                scheduleIdleClose(existing, tenantId);
            return existing;
        }
        const headless = options.headless ?? (process.env.MUASSIS_BROWSER_HEADLESS === 'true');
        log.info({ tenantId, headless }, 'spawning persistent browser');
        // Lazy-import so the gateway boots fine on installs that haven't
        // downloaded Chromium yet. The browser tools surface a clear error if
        // the binary's missing.
        const playwright = await import('playwright');
        const browser = await playwright.chromium.launch({
            headless,
            args: ['--no-default-browser-check', '--disable-blink-features=AutomationControlled'],
        });
        const context = await browser.newContext({
            viewport: { width: 1366, height: 800 },
            locale: 'ar-SA',
            timezoneId: 'Asia/Riyadh',
        });
        const page = await context.newPage();
        const session = {
            browser,
            context,
            page,
            createdAt: Date.now(),
            lastUsed: Date.now(),
            idleTimer: setTimeout(() => undefined, 0), // placeholder, replaced immediately
            inFlight: 0,
            pendingClose: false,
        };
        sessions.set(tenantId, session);
        scheduleIdleClose(session, tenantId);
        return session;
    });
}
export async function closeSession(tenantId, options = {}) {
    // (Round-7 fix) Same per-tenant lock as getOrCreateSession to prevent the
    // "delete from map then create races into the gap" pattern.
    return withSessionLock(tenantId, async () => {
        const s = sessions.get(tenantId);
        if (!s)
            return;
        // If callers haven't released their refcount, only close on `force=true`
        // (process exit). Otherwise mark for deferred close — the last
        // withBrowser() release fires it.
        if (!options.force && s.inFlight > 0) {
            s.pendingClose = true;
            log.debug({ tenantId, inFlight: s.inFlight }, 'closeSession deferred — tools still in flight');
            return;
        }
        sessions.delete(tenantId);
        clearTimeout(s.idleTimer);
        log.info({ tenantId, ageMs: Date.now() - s.createdAt, forced: !!options.force }, 'closing browser session');
        try {
            await Promise.race([
                s.browser.close(),
                new Promise((resolve) => setTimeout(resolve, CLOSE_GRACE_MS)),
            ]);
        }
        catch (err) {
            log.debug({ err: err.message }, 'browser close error (ignored)');
        }
    });
}
export function isSessionActive(tenantId) {
    return sessions.has(tenantId);
}
/**
 * (Caveat fix) Pre-warm the browser session for a tenant during gateway boot.
 * Eliminates the ~10s first-call delay from launching Chromium. Opt-in via
 * MUASSIS_BROWSER_PREWARM=true to avoid the +200MB RAM cost on installs that
 * don't need browser tools.
 */
export async function prewarmBrowser(tenantId) {
    if (process.env.MUASSIS_BROWSER_PREWARM !== 'true')
        return;
    try {
        log.info({ tenantId }, 'pre-warming browser session per MUASSIS_BROWSER_PREWARM');
        await getOrCreateSession(tenantId);
    }
    catch (err) {
        log.warn({ tenantId, err: err.message }, 'browser pre-warm failed (non-fatal)');
    }
}
export function listSessions() {
    return [...sessions.entries()].map(([tenantId, s]) => ({
        tenantId,
        createdAt: s.createdAt,
        lastUsed: s.lastUsed,
        url: (() => {
            try {
                return s.page.url();
            }
            catch {
                return null;
            }
        })(),
    }));
}
//# sourceMappingURL=browser-session.js.map