


























































import puppeteer from "puppeteer-core";
import { mkdir, readFile } from "node:fs/promises";
import { join } from "node:path";

const INSURER_PROFILE_GROUPS = new Map([
  ["mtu", "ergo"],
  ["hestia", "ergo"],
  
  
  
  
  ["generali", "generali"],
  
  
  
  ["allianz", "allianz"],
  ["pzu", "reczna"],
  ["insly", "reczna"],
]);




export const INSURERS_OUTSIDE_POOL = Object.freeze([]);




const PASSWORD_MANAGER_GROUPS = new Set(["reczna"]);



const ADOPTABLE_GROUPS = new Set(["reczna"]);

export function resolveProfileGroup(insurerKey) {
  return INSURER_PROFILE_GROUPS.get(String(insurerKey || "").toLowerCase()) || "wspolna";
}






const VISIBLE_MODE_GROUP_OVERRIDES = new Map([
  ["wspolna", "reczna"],
  ["generali", "reczna"],
  
  
  
  
  ["ergo", "reczna"],
]);

export function resolveRuntimeProfileGroup(insurerKey, headless = false) {
  const baseGroup = resolveProfileGroup(insurerKey);
  if (headless === true) return baseGroup;
  return VISIBLE_MODE_GROUP_OVERRIDES.get(baseGroup) || baseGroup;
}

export function createBrowserPool({
  chromeExecutablePath,
  profilesDirectory,
  launcher = puppeteer,
  ensureProfileDirectory = (directory) => mkdir(directory, { recursive: true }),
  pageCloseTimeoutMs = 4000,
  browserCloseTimeoutMs = 8000,
  browserKillWaitTimeoutMs = 2000,
}) {
  
  const instances = new Map();
  
  
  
  const groupQueues = new Map();
  
  
  
  
  const groupQueueWrites = new Map();
  
  const pendingGroupCloses = new Map();

  function runExclusively(group, task) {
    groupQueueWrites.set(group, (groupQueueWrites.get(group) || 0) + 1);
    const previous = groupQueues.get(group) || Promise.resolve();
    const next = previous.catch(() => {}).then(task);
    groupQueues.set(group, next.catch(() => {}));
    return next;
  }

  function profileDirectoryFor(group) {
    return join(profilesDirectory, `chrome-${group}-profile`);
  }

  
  
  
  async function connectToRunningProfileBrowser(group) {
    if (!ADOPTABLE_GROUPS.has(group)) return null;
    let raw;
    try {
      raw = await readFile(join(profileDirectoryFor(group), "DevToolsActivePort"), "utf8");
    } catch {
      return null;
    }
    const [portLine, pathLine] = String(raw).split(/\r?\n/);
    const port = Number.parseInt(String(portLine || "").trim(), 10);
    const wsPath = String(pathLine || "").trim();
    if (!Number.isInteger(port) || port <= 0 || port > 65535 || !wsPath.startsWith("/devtools/browser/")) {
      return null;
    }
    const connect = typeof launcher.connect === "function"
      ? launcher.connect.bind(launcher)
      : puppeteer.connect.bind(puppeteer);
    try {
      return await connect({
        browserWSEndpoint: `ws://127.0.0.1:${port}${wsPath}`,
        defaultViewport: null,
        protocolTimeout: 300000,
      });
    } catch {
      return null;
    }
  }

  async function launchInstance(group, headlessRequested) {
    if (!chromeExecutablePath) throw new Error("Nie znaleziono Google Chrome.");
    const headless = headlessRequested === true;
    const profileDirectory = profileDirectoryFor(group);
    await ensureProfileDirectory(profileDirectory);
    const args = [
      headless ? "--window-size=1440,1100" : "--start-maximized",
      "--disable-sync",
    ];
    if (!PASSWORD_MANAGER_GROUPS.has(group)) {
      
      
      args.push(
        "--disable-save-password-bubble",
        "--disable-features=PasswordLeakDetection,PasswordManagerOnboarding",
      );
    }
    const adopted = await connectToRunningProfileBrowser(group);
    if (adopted) {
      
      
      const version = await adopted.version().catch(() => "");
      return {
        browser: adopted,
        headless: /headless/i.test(version),
        adopted: true,
        pagesByInsurer: new Map(),
      };
    }
    const browser = await launcher.launch({
      executablePath: chromeExecutablePath,
      headless,
      userDataDir: profileDirectory,
      defaultViewport: headless ? { width: 1440, height: 1100 } : null,
      
      
      protocolTimeout: 300000,
      args,
    });
    return { browser, headless, adopted: false, pagesByInsurer: new Map() };
  }

  async function settleWithin(action, timeoutMs) {
    let timeoutId = null;
    const operation = Promise.resolve()
      .then(action)
      .then(() => true, () => true);
    const timeout = new Promise((resolve) => {
      timeoutId = setTimeout(() => resolve(false), Math.max(1, timeoutMs));
    });
    try {
      return await Promise.race([operation, timeout]);
    } finally {
      if (timeoutId !== null) clearTimeout(timeoutId);
    }
  }

  function createBrowserCloseTimeoutError(message, metadata = {}) {
    const error = new Error(message);
    error.code = "BROWSER_CLOSE_TIMEOUT";
    Object.assign(error, metadata);
    return error;
  }

  async function waitForProcessExit(chromeProcess) {
    if (!chromeProcess) return false;
    if (chromeProcess.exitCode !== null && chromeProcess.exitCode !== undefined) return true;
    let timeoutId = null;
    return new Promise((resolve) => {
      let settled = false;
      const finish = (exited) => {
        if (settled) return;
        settled = true;
        if (timeoutId !== null) clearTimeout(timeoutId);
        chromeProcess.removeListener?.("exit", onExit);
        chromeProcess.removeListener?.("close", onExit);
        resolve(exited);
      };
      const onExit = () => finish(true);
      chromeProcess.once?.("exit", onExit);
      chromeProcess.once?.("close", onExit);
      timeoutId = setTimeout(() => finish(false), Math.max(1, browserKillWaitTimeoutMs));
      if (chromeProcess.exitCode !== null && chromeProcess.exitCode !== undefined) finish(true);
    });
  }

  async function closeInstance(instance) {
    if (!instance) return;
    instance.pagesByInsurer.clear();
    const closedInTime = await settleWithin(
      () => instance.browser.close(),
      browserCloseTimeoutMs,
    );
    if (closedInTime && instance.browser.connected !== true) return;
    
    
    
    let processExited = false;
    if (instance.adopted !== true) {
      try {
        const chromeProcess = instance.browser.process?.();
        if (chromeProcess) {
          if (chromeProcess.exitCode === null || chromeProcess.exitCode === undefined) {
            chromeProcess.kill();
          }
          processExited = await waitForProcessExit(chromeProcess);
        }
      } catch {
      }
    }
    try {
      instance.browser.disconnect?.();
    } catch {
    }
    if (!processExited) {
      throw createBrowserCloseTimeoutError(
        "Google Chrome nie zakończył procesu w wyznaczonym czasie.",
        { scope: "browser" },
      );
    }
  }

  function countLivePages(instance) {
    let live = 0;
    for (const pages of instance?.pagesByInsurer?.values() || []) {
      for (const page of pages) {
        if (page?.isClosed?.() !== true) live += 1;
      }
    }
    return live;
  }

  function createBrowserModeConflictError(group, current, requestedHeadless, livePages) {
    const actualMode = current.headless ? "tła" : "widoczny";
    const requestedMode = requestedHeadless ? "tła" : "widoczny";
    const error = new Error(
      `Pula przeglądarek: grupa ${group} ma aktywny tryb ${actualMode},`
      + ` a żądany jest tryb ${requestedMode}. Poczekaj na zakończenie aktywnego biegu.`,
    );
    error.code = "BROWSER_MODE_CONFLICT";
    error.browserGroup = group;
    error.actualHeadless = current.headless;
    error.requestedHeadless = requestedHeadless;
    error.livePages = livePages;
    return error;
  }

  
  
  
  
  
  
  
  
  async function ensureInstance(group, headlessRequested) {
    const headless = headlessRequested === true;
    const current = instances.get(group);
    if (current?.browser?.connected && current.headless === headless) return current;
    if (current?.browser?.connected) {
      const livePages = countLivePages(current);
      if (livePages > 0 || current.adopted === true) {
        console.error(
          `Pula przeglądarek: grupa ${group} pracuje w trybie ${current.headless ? "tła" : "widocznym"}`
          + ` i ma ${livePages} żywych kart - nie przełączam trybu, żeby nie przerwać biegu.`,
        );
        throw createBrowserModeConflictError(group, current, headless, livePages);
      }
      instances.delete(group);
      await closeInstance(current);
    }
    const instance = await launchInstance(group, headless);
    instances.set(group, instance);
    
    
    if (instance.headless !== headless) {
      throw createBrowserModeConflictError(group, instance, headless, countLivePages(instance));
    }
    return instance;
  }

  function trackPage(instance, insurerKey, page) {
    const key = String(insurerKey || "").toLowerCase();
    if (!instance.pagesByInsurer.has(key)) instance.pagesByInsurer.set(key, new Set());
    const pages = instance.pagesByInsurer.get(key);
    pages.add(page);
    
    
    page.once?.("close", () => pages.delete(page));
  }

  
  async function openPage({ insurerKey, headless = false, reuseExisting = false }) {
    const group = resolveRuntimeProfileGroup(insurerKey, headless);
    return runExclusively(group, async () => {
      const instance = await ensureInstance(group, headless);
      if (reuseExisting) {
        const key = String(insurerKey || "").toLowerCase();
        const existingPages = [...(instance.pagesByInsurer.get(key) || [])];
        const existingPage = existingPages.reverse().find((page) => page?.isClosed?.() !== true);
        if (existingPage) return existingPage;
      }
      const page = await instance.browser.newPage();
      trackPage(instance, insurerKey, page);
      return page;
    });
  }

  
  
  
  async function getBrowser({ insurerKey, headless = false }) {
    const group = resolveRuntimeProfileGroup(insurerKey, headless);
    return runExclusively(group, async () => (await ensureInstance(group, headless)).browser);
  }

  
  
  function adoptPage(insurerKey, page) {
    if (!page || page.isClosed?.() === true) return;
    const instance = instances.get(resolveProfileGroup(insurerKey));
    if (!instance?.browser?.connected) return;
    const key = String(insurerKey || "").toLowerCase();
    if (instance.pagesByInsurer.get(key)?.has(page)) return;
    trackPage(instance, insurerKey, page);
  }

  function listPages(insurerKey) {
    const key = String(insurerKey || "").toLowerCase();
    const pages = [];
    for (const instance of instances.values()) {
      if (!instance?.browser?.connected) continue;
      for (const page of instance.pagesByInsurer.get(key) || []) {
        if (page?.isClosed?.() !== true) pages.push(page);
      }
    }
    return pages;
  }

  function isHeadless(insurerKey) {
    const key = String(insurerKey || "").toLowerCase();
    let trackedMode = null;
    for (const instance of instances.values()) {
      if (!instance?.browser?.connected) continue;
      const hasLivePage = [...(instance.pagesByInsurer.get(key) || [])]
        .some((page) => page?.isClosed?.() !== true);
      if (!hasLivePage) continue;
      if (instance.headless === false) return false;
      trackedMode = true;
    }
    return trackedMode;
  }

  
  
  async function closeInsurerPagesInGroup(group, key) {
    const instance = instances.get(group);
    if (!instance?.browser?.connected) return;
    if (!instance.pagesByInsurer.has(key)) return;
    const pages = instance.pagesByInsurer.get(key);
    await Promise.all([...(pages || [])].map(async (page) => {
      if (page?.isClosed?.() === true) {
        pages.delete(page);
        return;
      }
      await settleWithin(
        () => page.close?.({ runBeforeUnload: false }),
        pageCloseTimeoutMs,
      );
      if (page?.isClosed?.() === true) pages.delete(page);
    }));
    
    
    if (pages?.size > 0) {
      throw createBrowserCloseTimeoutError(
        `Nie udało się zamknąć ${pages.size} karty/kart Chrome dla ${key}.`,
        { scope: "page", insurerKey: key, livePages: pages.size },
      );
    }
    instance.pagesByInsurer.delete(key);
  }

  
  async function releaseInstanceIfEmpty(group) {
    const instance = instances.get(group);
    if (!instance?.browser?.connected) return;
    const stillUsed = [...instance.pagesByInsurer.values()].some((set) => set.size > 0);
    if (stillUsed) return;
    
    
    
    
    
    if (instance.headless === true) return;
    
    
    
    
    if (ADOPTABLE_GROUPS.has(group) && typeof instance.browser.pages === "function") {
      const foreignPages = (await instance.browser.pages().catch(() => []))
        .filter((page) => page?.isClosed?.() !== true && page?.url?.() !== "about:blank");
      if (foreignPages.length > 0) return;
    }
    
    
    instances.delete(group);
    await closeInstance(instance);
  }

  function createCloseSettler() {
    const settler = {};
    settler.promise = new Promise((resolve, reject) => {
      settler.resolve = resolve;
      settler.reject = reject;
    });
    
    settler.promise.catch(() => {});
    return settler;
  }

  
  
  
  
  
  function enqueueInsurerCloseInGroup(group, key) {
    const pending = pendingGroupCloses.get(group);
    if (pending && !pending.sealed
      && groupQueueWrites.get(group) === pending.queueWritesAtEnqueue) {
      if (!pending.keys.has(key)) pending.keys.set(key, createCloseSettler());
      return pending.keys.get(key).promise;
    }
    const batch = {
      keys: new Map([[key, createCloseSettler()]]),
      sealed: false,
      queueWritesAtEnqueue: 0,
    };
    pendingGroupCloses.set(group, batch);
    const run = runExclusively(group, async () => {
      batch.sealed = true;
      if (pendingGroupCloses.get(group) === batch) pendingGroupCloses.delete(group);
      const keyErrors = new Map();
      await Promise.all([...batch.keys.keys()].map(async (batchKey) => {
        try {
          await closeInsurerPagesInGroup(group, batchKey);
        } catch (error) {
          keyErrors.set(batchKey, error);
        }
      }));
      
      
      let epilogueError = null;
      try {
        await releaseInstanceIfEmpty(group);
      } catch (error) {
        epilogueError = error;
      }
      for (const [batchKey, settler] of batch.keys) {
        const error = keyErrors.get(batchKey) || epilogueError;
        if (error) settler.reject(error);
        else settler.resolve();
      }
    });
    
    
    batch.queueWritesAtEnqueue = groupQueueWrites.get(group);
    run.catch(() => {});
    return batch.keys.get(key).promise;
  }

  
  
  async function closeInsurer(insurerKey) {
    const key = String(insurerKey || "").toLowerCase();
    
    
    
    const groups = [...new Set([
      resolveRuntimeProfileGroup(insurerKey, true),
      resolveRuntimeProfileGroup(insurerKey, false),
      ...[...instances.entries()]
        .filter(([, instance]) => instance.pagesByInsurer.has(key))
        .map(([group]) => group),
    ])];
    await Promise.all(groups.map((group) => enqueueInsurerCloseInGroup(group, key)));
  }

  
  
  
  
  async function prewarm(groups) {
    for (const group of groups || []) {
      await runExclusively(group, async () => {
        if (instances.get(group)?.browser?.connected) return;
        try {
          instances.set(group, await launchInstance(group, true));
        } catch (error) {
          console.error(
            `Pula przeglądarek: rozgrzanie grupy ${group} nie powiodło się (${error?.message || error}).`,
          );
        }
      });
    }
  }

  async function closeAll() {
    
    
    
    const groups = [...new Set([...instances.keys(), ...groupQueues.keys()])];
    await Promise.all(groups.map((group) => runExclusively(group, async () => {
      const instance = instances.get(group);
      if (!instance) return;
      instances.delete(group);
      await closeInstance(instance);
    })));
  }

  function describe() {
    return [...instances.entries()].map(([group, instance]) => ({
      group,
      headless: instance.headless,
      connected: instance.browser?.connected === true,
      insurers: [...instance.pagesByInsurer.entries()]
        .filter(([, pages]) => pages.size > 0)
        .map(([insurerKey, pages]) => ({ insurerKey, pages: pages.size })),
    }));
  }

  return { openPage, getBrowser, adoptPage, closeInsurer, closeAll, listPages, isHeadless, describe, prewarm };
}
