import puppeteer from "puppeteer";
import logger from "./logger.js";
const BROWSER_COUNT = parseInt(process.env.BROWSER_COUNT || "2", 10);
const PAGES_PER_BROWSER = parseInt(process.env.PAGES_PER_BROWSER || "8", 10);
const RESTART_AFTER_PDFS = 1000;
const RESTART_AFTER_MS = 60 * 60 * 1000; // 1 hour
const instances = [];
const waitingQueue = [];
let roundRobinIndex = 0;
export function getPoolStats() {
    const totalAvailable = instances.reduce((s, i) => s + i.availablePages.length, 0);
    const totalPages = instances.length * PAGES_PER_BROWSER;
    const totalPdfs = instances.reduce((s, i) => s + i.pdfCount, 0);
    return {
        poolSize: totalPages,
        totalPages,
        availablePages: totalAvailable,
        queueDepth: waitingQueue.length,
        pdfCount: totalPdfs,
        restarting: instances.some((i) => i.restarting),
        uptimeMs: Date.now() - (instances[0]?.lastRestartTime || Date.now()),
        browsers: instances.map((i) => ({
            id: i.id,
            available: i.availablePages.length,
            pdfCount: i.pdfCount,
            restarting: i.restarting,
        })),
    };
}
async function recyclePage(page) {
    try {
        const client = await page.createCDPSession();
        await client.send("Network.clearBrowserCache").catch(() => { });
        await client.detach().catch(() => { });
        const cookies = await page.cookies();
        if (cookies.length > 0) {
            await page.deleteCookie(...cookies);
        }
        await page.goto("about:blank", { timeout: 5000 }).catch(() => { });
    }
    catch {
        // ignore
    }
}
async function createPages(b, count) {
    const pages = [];
    for (let i = 0; i < count; i++) {
        const page = await b.newPage();
        pages.push(page);
    }
    return pages;
}
function pickInstance() {
    // Round-robin among instances that have available pages
    for (let i = 0; i < instances.length; i++) {
        const idx = (roundRobinIndex + i) % instances.length;
        const inst = instances[idx];
        if (inst.availablePages.length > 0 && !inst.restarting) {
            roundRobinIndex = (idx + 1) % instances.length;
            return inst;
        }
    }
    return null;
}
async function acquirePage() {
    // Check restarts
    for (const inst of instances) {
        if (!inst.restarting && (inst.pdfCount >= RESTART_AFTER_PDFS || Date.now() - inst.lastRestartTime >= RESTART_AFTER_MS)) {
            scheduleRestart(inst);
        }
    }
    const inst = pickInstance();
    if (inst) {
        const page = inst.availablePages.pop();
        return { page, instance: inst };
    }
    // All pages busy, queue with 30s timeout
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            const idx = waitingQueue.findIndex((w) => w.resolve === resolve);
            if (idx >= 0)
                waitingQueue.splice(idx, 1);
            reject(new Error("QUEUE_FULL"));
        }, 30_000);
        waitingQueue.push({
            resolve: (v) => {
                clearTimeout(timer);
                resolve(v);
            },
        });
    });
}
function releasePage(page, inst) {
    inst.pdfCount++;
    const waiter = waitingQueue.shift();
    if (waiter) {
        recyclePage(page).then(() => waiter.resolve({ page, instance: inst })).catch(() => {
            if (inst.browser && !inst.restarting) {
                inst.browser.newPage().then((p) => waiter.resolve({ page: p, instance: inst })).catch(() => {
                    waitingQueue.unshift(waiter);
                });
            }
            else {
                waitingQueue.unshift(waiter);
            }
        });
        return;
    }
    recyclePage(page).then(() => {
        inst.availablePages.push(page);
    }).catch(() => {
        if (inst.browser && !inst.restarting) {
            inst.browser.newPage().then((p) => inst.availablePages.push(p)).catch(() => { });
        }
    });
}
async function scheduleRestart(inst) {
    if (inst.restarting)
        return;
    inst.restarting = true;
    logger.info(`Scheduling browser ${inst.id} restart (pdfs=${inst.pdfCount}, uptime=${Math.round((Date.now() - inst.lastRestartTime) / 1000)}s)`);
    const drainCheck = () => new Promise((resolve) => {
        const check = () => {
            if (inst.availablePages.length === PAGES_PER_BROWSER && waitingQueue.length === 0) {
                resolve();
            }
            else {
                setTimeout(check, 100);
            }
        };
        check();
    });
    await Promise.race([drainCheck(), new Promise(r => setTimeout(r, 30000))]);
    for (const page of inst.availablePages) {
        await page.close().catch(() => { });
    }
    inst.availablePages.length = 0;
    try {
        await inst.browser.close().catch(() => { });
    }
    catch { }
    const execPath = process.env.PUPPETEER_EXECUTABLE_PATH || undefined;
    inst.browser = await puppeteer.launch({
        headless: true,
        executablePath: execPath,
        args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage"],
    });
    const pages = await createPages(inst.browser, PAGES_PER_BROWSER);
    inst.availablePages.push(...pages);
    inst.pdfCount = 0;
    inst.lastRestartTime = Date.now();
    inst.restarting = false;
    logger.info(`Browser ${inst.id} restarted successfully`);
    while (waitingQueue.length > 0 && inst.availablePages.length > 0) {
        const waiter = waitingQueue.shift();
        const p = inst.availablePages.pop();
        if (waiter && p)
            waiter.resolve({ page: p, instance: inst });
    }
}
async function launchInstance(id) {
    const execPath = process.env.PUPPETEER_EXECUTABLE_PATH || undefined;
    const browser = await puppeteer.launch({
        headless: true,
        executablePath: execPath,
        args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu", "--disable-dev-shm-usage"],
    });
    const pages = await createPages(browser, PAGES_PER_BROWSER);
    const inst = {
        browser,
        availablePages: pages,
        pdfCount: 0,
        lastRestartTime: Date.now(),
        restarting: false,
        id,
    };
    return inst;
}
export async function initBrowser() {
    for (let i = 0; i < BROWSER_COUNT; i++) {
        const inst = await launchInstance(i);
        instances.push(inst);
    }
    logger.info(`Browser pool ready (${BROWSER_COUNT} browsers × ${PAGES_PER_BROWSER} pages = ${BROWSER_COUNT * PAGES_PER_BROWSER} total)`);
}
export async function closeBrowser() {
    for (const inst of instances) {
        for (const page of inst.availablePages) {
            await page.close().catch(() => { });
        }
        inst.availablePages.length = 0;
        await inst.browser.close().catch(() => { });
    }
    instances.length = 0;
}
export async function renderPdf(html, options = {}) {
    const { page, instance } = await acquirePage();
    try {
        await page.setJavaScriptEnabled(false);
        const result = await Promise.race([
            (async () => {
                await page.setContent(html, { waitUntil: "domcontentloaded", timeout: 15_000 });
                await page.addStyleTag({ content: "* { margin: 0; padding: 0; } body { margin: 0; }" });
                const pdf = await page.pdf({
                    format: options.format || "A4",
                    landscape: options.landscape || false,
                    printBackground: options.printBackground !== false,
                    margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" },
                    headerTemplate: options.headerTemplate,
                    footerTemplate: options.footerTemplate,
                    displayHeaderFooter: options.displayHeaderFooter || false,
                });
                return Buffer.from(pdf);
            })(),
            new Promise((_, reject) => setTimeout(() => reject(new Error("PDF_TIMEOUT")), 30_000)),
        ]);
        return result;
    }
    finally {
        releasePage(page, instance);
    }
}
export async function renderUrlPdf(url, options = {}) {
    const { page, instance } = await acquirePage();
    try {
        await page.setJavaScriptEnabled(false);
        // Pin DNS resolution to prevent DNS rebinding SSRF attacks
        if (options.hostResolverRules) {
            const client = await page.createCDPSession();
            // Use Chrome DevTools Protocol to set host resolver rules per-page
            await client.send("Network.enable");
            // Extract hostname and IP from rules like "MAP hostname ip"
            const match = options.hostResolverRules.match(/^MAP\s+(\S+)\s+(\S+)$/);
            if (match) {
                const [, hostname, ip] = match;
                await page.setRequestInterception(true);
                page.on("request", (request) => {
                    const reqUrl = new URL(request.url());
                    if (reqUrl.hostname === hostname) {
                        // For HTTP, rewrite to IP with Host header
                        if (reqUrl.protocol === "http:") {
                            reqUrl.hostname = ip;
                            request.continue({
                                url: reqUrl.toString(),
                                headers: { ...request.headers(), host: hostname },
                            });
                        }
                        else {
                            // For HTTPS, we can't easily swap the IP without cert issues
                            // But we've already validated the IP, and the short window makes rebinding unlikely
                            // Combined with JS disabled, this is sufficient mitigation
                            request.continue();
                        }
                    }
                    else {
                        // Block any requests to other hosts (prevent redirects to internal IPs)
                        request.abort("blockedbyclient");
                    }
                });
            }
        }
        const result = await Promise.race([
            (async () => {
                await page.goto(url, {
                    waitUntil: options.waitUntil || "domcontentloaded",
                    timeout: 30_000,
                });
                const pdf = await page.pdf({
                    format: options.format || "A4",
                    landscape: options.landscape || false,
                    printBackground: options.printBackground !== false,
                    margin: options.margin || { top: "0", right: "0", bottom: "0", left: "0" },
                });
                return Buffer.from(pdf);
            })(),
            new Promise((_, reject) => setTimeout(() => reject(new Error("PDF_TIMEOUT")), 30_000)),
        ]);
        return result;
    }
    finally {
        releasePage(page, instance);
    }
}
