import puppeteer from "puppeteer-core";
import {
  bringPageWindowToFront,
  buildKnownVehicleAnswerResolver,
  composeKnownAnswerResolvers,
  fillDescribedFields,
  fillRequestedAnswers,
  hasDescribedFields,
  learnOrReplayActionPath,
  learnOrReplayFormPath,
} from "./etrzxtbsmrmtpfpyetfs.mjs";
import { attachClientInfoOverlay } from "./hkwnbsimckiekhsfxcto.mjs";
import {
  captureGeneraliSearchTemplate,
  fetchGeneraliSearchPolicies,
  mapGeneraliSearchPolicies,
} from "./tnodnwhtpiozxvtuvvlq.mjs";
import {
  prepareGeneraliPortfolioPdfEntries,
  runGeneraliPortfolioPdfBatch,
} from "./qhqzbzmeixaozmvkosla.mjs";
import {
  assertPortfolioMappingComplete,
  createPortfolioEmissionResult,
  createPortfolioMappingCollector,
} from "./fgroazrhswhewnaqxvgl.mjs";
import {
  countUnskippedPortfolioPdfSelectors,
} from "./abywezhrsiqkqfhujfvq.mjs";

const GENERALI_URL = "https://portal.generali.pl/";
const GENERALI_SEARCH_URL = "https://portal.generali.pl/frontend/search";
const PORTAL_TIMEOUT_MS = 3 * 60 * 1000;

function manualInteractionRequired(message) {
  const error = new Error(message);
  error.code = "MANUAL_INTERACTION_REQUIRED";
  return error;
}

function delay(milliseconds) {
  return new Promise((resolve) => globalThis.setTimeout(resolve, milliseconds));
}

function normalizeText(value) {
  return String(value || "")
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .replace(/\s+/g, " ")
    .trim()
    .toLowerCase();
}

async function getCombinedPageText(page) {
  const parts = [];
  for (const frame of page.frames()) {
    try {
      const text = await frame.evaluate(() => String(document.body?.innerText || "").slice(0, 20000));
      if (text) parts.push(text);
    } catch {
    }
  }
  return parts.join(" ").slice(0, 50000);
}

async function frameHasSelector(page, selector) {
  for (const frame of page.frames()) {
    try {
      if (await frame.$(selector)) return true;
    } catch {
    }
  }
  return false;
}

async function detectAuthenticationInterruption(page) {
  const pageText = normalizeText(await getCombinedPageText(page));
  if (
    pageText.includes("nieprawidlowa nazwa uzytkownika")
    || pageText.includes("nieprawidlowy login")
    || pageText.includes("nieprawidlowe haslo")
    || pageText.includes("bledne dane logowania")
  ) {
    return { type: "error", message: "Generali odrzuciło login lub hasło." };
  }
  if (
    pageText.includes("kod jednorazowy")
    || pageText.includes("kod bezpieczenstwa")
    || pageText.includes("kod sms")
    || pageText.includes("kod weryfikacyjny")
    || pageText.includes("uwierzytelnianie dwuskladnikowe")
    || pageText.includes("token mfa")
  ) {
    return {
      type: "action",
      title: "Potwierdź logowanie do Generali",
      detail: "Wpisz kod potwierdzający w otwartym oknie.",
    };
  }
  if (pageText.includes("captcha") || await frameHasSelector(page, "iframe[src*='captcha'], [class*='captcha']")) {
    return {
      type: "action",
      title: "Generali wymaga potwierdzenia CAPTCHA",
      detail: "Rozwiąż CAPTCHA w otwartym oknie.",
    };
  }
  return null;
}

export async function replaceGeneraliLoginField(handle, value) {
  if (!handle) return false;
  const expected = String(value ?? "");
  return handle.evaluate((element, nextValue) => {
    if (!(element instanceof HTMLInputElement) && !(element instanceof HTMLTextAreaElement)) {
      return false;
    }
    const prototype = element instanceof HTMLTextAreaElement
      ? HTMLTextAreaElement.prototype
      : HTMLInputElement.prototype;
    const nativeSetter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
    element.focus();
    if (nativeSetter) nativeSetter.call(element, nextValue);
    else element.value = nextValue;
    element.dispatchEvent(new Event("input", { bubbles: true }));
    element.dispatchEvent(new Event("change", { bubbles: true }));
    return element.value === nextValue;
  }, expected);
}

async function loginIfNeeded(page, credentials, onProgress) {
  const startedAt = Date.now();
  let loginFormReady = false;
  while (Date.now() - startedAt < 60000) {
    const hasUsername = await frameHasSelector(page, "#username, input[name='username']");
    const hasPassword = await frameHasSelector(page, "#password, input[name='password']");
    if (hasUsername && hasPassword) {
      loginFormReady = true;
      break;
    }
    const url = page.url();
    if (!url.includes("/iam/realms/") && !url.includes("/login/cas")) return;
    await delay(400);
  }
  if (!loginFormReady) throw new Error("Nie udało się otworzyć formularza logowania Generali.");

  let submitted = false;
  for (const frame of page.frames()) {
    try {
      const username = await frame.$("#username, input[name='username']");
      const password = await frame.$("#password, input[name='password']");
      const submit = await frame.$("#kc-login, input[name='login'], button[type='submit'], input[type='submit']");
      if (!username || !password || !submit) continue;
      const usernameReplaced = await replaceGeneraliLoginField(username, credentials.username);
      const passwordReplaced = await replaceGeneraliLoginField(password, credentials.password);
      if (!usernameReplaced || !passwordReplaced) continue;
      onProgress({
        status: "running",
        title: "Logowanie do Generali…",
        detail: "",
      });
      await submit.click();
      submitted = true;
      break;
    } catch {
    }
  }
  if (!submitted) throw new Error("Nie udało się obsłużyć formularza logowania Generali.");
}

async function waitForPortal(page, onProgress, targetFields, requestInput, { allowUserInteraction = true } = {}) {
  let startedAt = Date.now();
  let lastActionMessage = "";
  let mfaRound = 0;
  while (Date.now() - startedAt < PORTAL_TIMEOUT_MS) {
    if (await hasDescribedFields(page, targetFields)) return;
    const authenticationInterruption = await detectAuthenticationInterruption(page);
    if (authenticationInterruption?.type === "error") throw new Error(authenticationInterruption.message);
    if (authenticationInterruption?.type === "action") {
      const isMfaPrompt = authenticationInterruption.title.includes("Potwierdź logowanie");
      if (isMfaPrompt && typeof requestInput === "function" && mfaRound < 3) {
        const smsFields = (await collectGeneraliFormFields(page)).fields
          .filter((field) => ["text", "number"].includes(field.kind))
          .slice(0, 3);
        if (smsFields.length > 0) {
          mfaRound += 1;
          onProgress({
            status: "running",
            title: "Generali prosi o kod SMS…",
            detail: "Przepisz kod z telefonu w formularzu Bazy Polis.",
          });
          const answers = await requestInput({
            title: "Kod SMS do logowania Generali",
            round: mfaRound,
            fields: smsFields,
          });
          if (answers && typeof answers === "object") {
            
            
            startedAt = Date.now();
            await applyGeneraliAnswers(page, smsFields, answers);
            await clickGeneraliControlBySelector(
              page,
              "#kc-login, input[name='login'], button[type='submit'], input[type='submit']",
              8000,
            );
            await delay(1500);
            continue;
          }
        }
      }
      
      
      if (!allowUserInteraction) {
        throw manualInteractionRequired(`${authenticationInterruption.title}. Kalkulacja w tle została wstrzymana - użyj „Popraw ręcznie”, aby otworzyć okno Chrome.`);
      }
      if (lastActionMessage !== authenticationInterruption.title) {
        lastActionMessage = authenticationInterruption.title;
        onProgress({
          status: "action_required",
          title: authenticationInterruption.title,
          detail: authenticationInterruption.detail,
        });
      }
    } else {
      const loginVisible = await frameHasSelector(page, "#username, input[name='username']");
      const currentUrl = page.url();
      if (!loginVisible && !currentUrl.includes("/iam/realms/") && !currentUrl.includes("/login/cas")) return;
    }
    await delay(500);
  }
  
  if (await hasDescribedFields(page, targetFields)) return;
  if (!allowUserInteraction) {
    throw manualInteractionRequired("Panel Generali nie otworzył się w tle - użyj „Popraw ręcznie”, aby dokończyć w oknie Chrome.");
  }
  throw new Error("Panel Generali nie otworzył się w ciągu 3 minut.");
}

async function waitForGeneraliSelector(page, selector, timeout = 60000) {
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    if (await frameHasSelector(page, selector)) return true;
    await delay(350);
  }
  return false;
}

async function setGeneraliTextInput(page, selector, value) {
  for (const frame of page.frames()) {
    try {
      const updated = await frame.evaluate(({ expectedSelector, expectedValue }) => {
        const visible = (element) => element instanceof HTMLElement
          && element.getClientRects().length > 0
          && getComputedStyle(element).visibility !== "hidden";
        const input = [...document.querySelectorAll(expectedSelector)].find(visible);
        if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLTextAreaElement)) return false;
        input.focus();
        const prototype = input instanceof HTMLTextAreaElement
          ? HTMLTextAreaElement.prototype
          : HTMLInputElement.prototype;
        const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
        if (setter) setter.call(input, expectedValue);
        else input.value = expectedValue;
        input.dispatchEvent(new Event("input", { bubbles: true }));
        input.dispatchEvent(new Event("change", { bubbles: true }));
        input.dispatchEvent(new Event("blur", { bubbles: true }));
        return input.value === expectedValue;
      }, { expectedSelector: selector, expectedValue: String(value || "") });
      if (updated) return true;
    } catch {
    }
  }
  return false;
}

async function clickGeneraliControlBySelector(page, selector, timeout = 20000) {
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    for (const frame of page.frames()) {
      let candidates = [];
      try {
        candidates = await frame.$$(selector);
        for (const candidate of candidates) {
          const state = await candidate.evaluate((element) => ({
            visible: element instanceof HTMLElement
              && element.getClientRects().length > 0
              && getComputedStyle(element).visibility !== "hidden",
            disabled: Boolean(element.disabled) || element.getAttribute("aria-disabled") === "true",
          }));
          if (!state.visible || state.disabled) continue;
          await candidate.click();
          return true;
        }
      } catch {
      } finally {
        await Promise.all(candidates.map((candidate) => candidate.dispose().catch(() => {})));
      }
    }
    await delay(300);
  }
  return false;
}

const GENERALI_IGNORED_FIELD_PATTERN = /cepikdataform|regon \/ pesel|szukaj|search|filtr/i;

function filterGeneraliVehicleFields(request) {
  const fields = (Array.isArray(request?.fields) ? request.fields : [])
    .filter((field) => !GENERALI_IGNORED_FIELD_PATTERN.test(`${field.selector || ""} ${field.label || ""}`));
  return { title: request?.title || "", fields };
}

async function collectGeneraliFormFields(page) {
  const collected = [];
  let screenTitle = "";
  for (const frame of page.frames().slice(0, 20)) {
    let frameHint = "";
    try {
      const frameUrl = new URL(frame.url());
      frameHint = `${frameUrl.hostname}${frameUrl.pathname}`.slice(0, 220);
    } catch {
    }
    try {
      const frameResult = await frame.evaluate((hint) => {
        const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
        const normalize = (value) => compact(value)
          .normalize("NFD")
          .replace(/[̀-ͯ]/g, "")
          .toLowerCase();
        const escapeAttribute = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
        const visible = (element) => element instanceof HTMLElement
          && element.getClientRects().length > 0
          && getComputedStyle(element).visibility !== "hidden";
        const stableSelector = (element) => {
          if (element.id) return `#${CSS.escape(element.id)}`;
          const tag = element.tagName.toLowerCase();
          const name = element.getAttribute("name");
          if (name) {
            const selector = `${tag}[name="${escapeAttribute(name)}"]`;
            try {
              if (document.querySelectorAll(selector).length === 1) return selector;
            } catch {
            }
          }
          const path = [];
          let current = element;
          for (let depth = 0; current instanceof Element && depth < 7; depth += 1, current = current.parentElement) {
            let part = current.tagName.toLowerCase();
            if (current.id) {
              path.unshift(`#${CSS.escape(current.id)}`);
              break;
            }
            const siblings = current.parentElement
              ? [...current.parentElement.children].filter((sibling) => sibling.tagName === current.tagName)
              : [];
            if (siblings.length > 1) part += `:nth-of-type(${siblings.indexOf(current) + 1})`;
            path.unshift(part);
          }
          return path.join(" > ");
        };
        const rowOf = (element) => element.closest(".form-group, [class*='panel-row'], fieldset, .row")
          || element.parentElement;
        const answerPattern = /^(tak|nie|--?\s*wybierz\s*--?|wybierz|brak)$/i;
        const yesNoPattern = /^(tak|nie)$/;
        const rowLabelFor = (element) => {
          let current = element.parentElement;
          for (let depth = 0; current instanceof HTMLElement && depth < 7; depth += 1, current = current.parentElement) {
            const candidates = [...current.querySelectorAll("label, legend, .control-label, [class*='label']")]
              .filter(visible)
              .map((node) => compact(node.innerText))
              .filter((text) => text.length >= 3 && !answerPattern.test(normalize(text)));
            if (candidates.length > 0) {
              return candidates.sort((left, right) => left.length - right.length)[0].slice(0, 160);
            }
          }
          return "";
        };
        const emptySelect = (select) => {
          const selectedText = normalize(select.selectedOptions?.[0]?.textContent);
          return !select.value || /wybierz|^-+$|^\.*$/.test(selectedText);
        };
        const looksChosen = (element) => element.getAttribute("aria-checked") === "true"
          || /(^|\s)(active|checked|selected|btn-primary)(\s|$)/.test(String(element.className || ""))
          || Boolean(element.querySelector("input:checked"));

        const heading = [...document.querySelectorAll("h1, h2, h3, h4")]
          .find((element) => visible(element) && compact(element.innerText).length > 2);
        const results = [];
        const usedRows = new Set();
        const seenRadioGroups = new Set();
        const controlSelector = [
          "select",
          "input:not([type='hidden']):not([type='submit']):not([type='button']):not([type='image']):not([type='file'])",
          "textarea",
          ".v-select",
          ".fe-vue-select",
          "[role='radio']",
          "button",
        ].join(", ");

        for (const element of document.querySelectorAll(controlSelector)) {
          if (results.length >= 40) break;
          const tag = element.tagName.toLowerCase();

          if (tag === "select") {
            if (element.disabled) continue;
            const row = rowOf(element);
            const rowVisible = row instanceof HTMLElement && row.getClientRects().length > 0;
            if (!visible(element) && !(rowVisible && row.querySelector(".v-select, .fe-vue-select"))) continue;
            if (!emptySelect(element)) continue;
            if (row && usedRows.has(row)) continue;
            const options = [...element.options]
              .filter((option) => !option.disabled)
              .slice(0, 60)
              .map((option) => ({
                value: option.value || "",
                text: compact(option.textContent).slice(0, 120),
                selector: "",
              }))
              .filter((option) => option.value || option.text);
            if (options.length === 0) continue;
            if (row) usedRows.add(row);
            results.push({
              kind: "select",
              selector: stableSelector(element),
              label: rowLabelFor(element) || compact(element.getAttribute("name")).slice(0, 160),
              options,
              currentValue: "",
              invalid: false,
              required: element.required === true,
              frameHint: hint,
              tag: "select",
              type: "select",
            });
            continue;
          }

          if (tag === "input" && (element.getAttribute("type") || "").toLowerCase() === "radio") {
            const groupName = element.getAttribute("name") || "";
            if (!groupName || seenRadioGroups.has(groupName)) continue;
            seenRadioGroups.add(groupName);
            let groupRadios = [];
            try {
              groupRadios = [...document.querySelectorAll(`input[type='radio'][name="${escapeAttribute(groupName)}"]`)]
                .filter((candidate) => visible(candidate) || visible(candidate.closest("label")));
            } catch {
            }
            if (groupRadios.length === 0) continue;
            if (groupRadios.some((candidate) => candidate.checked)) continue;
            const row = rowOf(groupRadios[0]);
            if (row) usedRows.add(row);
            results.push({
              kind: "radio",
              selector: stableSelector(groupRadios[0]),
              label: rowLabelFor(groupRadios[0]) || compact(groupName).slice(0, 160),
              options: groupRadios.slice(0, 40).map((candidate) => {
                const radioLabels = candidate.labels ? [...candidate.labels].map((label) => label.innerText).join(" ") : "";
                return {
                  value: candidate.value || "",
                  text: compact(radioLabels || candidate.closest("label")?.innerText || candidate.value).slice(0, 120),
                  selector: stableSelector(candidate),
                };
              }),
              currentValue: "",
              invalid: false,
              required: groupRadios.some((candidate) => candidate.required),
              frameHint: hint,
              tag: "input",
              type: "radio",
            });
            continue;
          }

          if (tag === "textarea" || (tag === "input" && ["text", "number", "tel", "email", "password", ""].includes((element.getAttribute("type") || "").toLowerCase()))) {
            if (!visible(element) || element.disabled || element.readOnly) continue;
            if (element.classList.contains("vs__search") || element.closest(".v-select, .fe-vue-select")) continue;
            if (compact(element.value)) continue;
            const row = rowOf(element);
            if (row && usedRows.has(row)) continue;
            const label = rowLabelFor(element)
              || compact(element.getAttribute("aria-label") || element.getAttribute("placeholder") || element.getAttribute("name"));
            if (!label) continue;
            if (row) usedRows.add(row);
            results.push({
              kind: (element.getAttribute("type") || "").toLowerCase() === "number" ? "number" : "text",
              selector: stableSelector(element),
              label: label.slice(0, 160),
              options: [],
              currentValue: "",
              invalid: false,
              required: element.required === true,
              frameHint: hint,
              tag,
              type: element.getAttribute("type") || "text",
            });
            continue;
          }

          if (element.matches(".v-select, .fe-vue-select")) {
            if (!visible(element)) continue;
            if (element.classList.contains("vs--disabled") || element.getAttribute("aria-disabled") === "true") continue;
            if (element.querySelector(".vs__search")?.disabled) continue;
            const row = rowOf(element);
            if (row && usedRows.has(row)) continue;
            const selectedText = [...element.querySelectorAll(".vs__selected")]
              .map((node) => compact(node.innerText))
              .find((text) => text && !answerPattern.test(normalize(text)));
            if (selectedText) continue;
            if (row) usedRows.add(row);
            results.push({
              kind: "combobox",
              selector: stableSelector(element),
              label: rowLabelFor(element) || "Pole listy rozwijanej",
              options: [...element.querySelectorAll(".vs__dropdown-option, [role='option']")]
                .slice(0, 60)
                .map((option) => ({ value: "", text: compact(option.innerText).slice(0, 120), selector: "" }))
                .filter((option) => option.text),
              currentValue: "",
              invalid: false,
              required: false,
              frameHint: hint,
              tag: "div",
              type: "combobox",
            });
            continue;
          }

          if ((tag === "button" || element.getAttribute("role") === "radio") && visible(element)) {
            if (!yesNoPattern.test(normalize(element.innerText))) continue;
            const row = rowOf(element);
            if (!row || usedRows.has(row)) continue;
            const rowChoices = [...row.querySelectorAll("button, [role='radio'], label")]
              .filter(visible)
              .filter((candidate) => yesNoPattern.test(normalize(candidate.innerText)));
            if (rowChoices.length < 2) continue;
            if (rowChoices.some(looksChosen)) continue;
            const label = rowLabelFor(element) || compact(row.innerText).slice(0, 160);
            if (!label) continue;
            usedRows.add(row);
            results.push({
              kind: "radio",
              selector: stableSelector(rowChoices[0]),
              label: label.slice(0, 160),
              options: rowChoices.slice(0, 6).map((candidate) => ({
                value: compact(candidate.innerText).slice(0, 120),
                text: compact(candidate.innerText).slice(0, 120),
                selector: stableSelector(candidate),
              })),
              currentValue: "",
              invalid: false,
              required: false,
              frameHint: hint,
              tag,
              type: "yes-no",
            });
            continue;
          }
        }

        return { heading: compact(heading?.innerText).slice(0, 140), fields: results };
      }, frameHint);
      if (!screenTitle && frameResult?.heading) screenTitle = frameResult.heading;
      if (Array.isArray(frameResult?.fields)) collected.push(...frameResult.fields);
    } catch {
    }
  }
  const labelCounts = new Map();
  const fields = collected
    .slice(0, 25)
    .map((field, index) => {
      const count = (labelCounts.get(field.label) || 0) + 1;
      labelCounts.set(field.label, count);
      return {
        ...field,
        label: count > 1 ? `${field.label} (${count})` : field.label,
        id: `pole-${index + 1}`,
      };
    });
  return { title: screenTitle, fields };
}

async function readGeneraliVueOptions(page, field) {
  const frames = [...page.frames()].sort((left, right) => {
    const hintOf = (frame) => {
      try {
        const frameUrl = new URL(frame.url());
        return `${frameUrl.hostname}${frameUrl.pathname}`.slice(0, 220);
      } catch {
        return "";
      }
    };
    return Number(hintOf(right) === field.frameHint) - Number(hintOf(left) === field.frameHint);
  });
  for (const frame of frames) {
    try {
      const options = await frame.evaluate(async ({ selector }) => {
        const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
        const visible = (element) => element instanceof HTMLElement
          && element.getClientRects().length > 0
          && getComputedStyle(element).visibility !== "hidden";
        let root = null;
        try {
          root = document.querySelector(selector);
        } catch {
        }
        if (!(root instanceof HTMLElement)) return null;
        const readList = () => {
          const search = root.querySelector(".vs__search");
          const controlledId = search?.getAttribute("aria-controls");
          const list = (controlledId && document.getElementById(controlledId))
            || root.querySelector(".vs__dropdown-menu, [role='listbox']");
          return [...(list?.querySelectorAll(".vs__dropdown-option, [role='option']") || [])]
            .filter((element) => visible(element)
              && !element.classList.contains("vs__dropdown-option--disabled")
              && element.getAttribute("aria-disabled") !== "true")
            .map((element) => compact(element.innerText).slice(0, 120))
            .filter(Boolean);
        };
        let texts = readList();
        if (texts.length === 0) {
          const opener = root.querySelector(".vs__search") || root.querySelector(".vs__dropdown-toggle") || root;
          opener.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
          opener.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
          if (typeof opener.click === "function") opener.click();
          opener.focus?.();
          
          
          for (let attempt = 0; attempt < 20 && texts.length === 0; attempt += 1) {
            await new Promise((resolve) => setTimeout(resolve, 400));
            texts = readList();
          }
          const search = root.querySelector(".vs__search");
          search?.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }));
          search?.blur?.();
        }
        return texts;
      }, { selector: field.selector });
      if (Array.isArray(options) && options.length > 0) {
        return [...new Set(options)].slice(0, 60).map((text) => ({ value: "", text, selector: "" }));
      }
    } catch {
    }
  }
  return [];
}

async function setGeneraliSelectAnswer(page, field, value) {
  for (const frame of page.frames()) {
    try {
      const done = await frame.evaluate(({ selector, wanted }) => {
        const normalize = (input) => String(input || "")
          .replace(/\s+/g, " ")
          .trim()
          .normalize("NFD")
          .replace(/[̀-ͯ]/g, "")
          .toLowerCase();
        let select = null;
        try {
          select = document.querySelector(selector);
        } catch {
        }
        if (!(select instanceof HTMLSelectElement) || select.disabled) return false;
        const wantedNormalized = normalize(wanted);
        const option = [...select.options].find((candidate) => candidate.value === wanted)
          || [...select.options].find((candidate) => normalize(candidate.textContent) === wantedNormalized)
          || [...select.options].find((candidate) => wantedNormalized && normalize(candidate.textContent).includes(wantedNormalized));
        if (!option) return false;
        const setter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set;
        if (setter) setter.call(select, option.value);
        else select.value = option.value;
        select.dispatchEvent(new Event("input", { bubbles: true }));
        select.dispatchEvent(new Event("change", { bubbles: true }));
        if (typeof window.jQuery === "function") window.jQuery(select).trigger("change");
        return true;
      }, { selector: field.selector, wanted: String(value) });
      if (done) return true;
    } catch {
    }
  }
  return false;
}

async function setGeneraliVueAnswer(page, field, wantedText) {
  const deadline = Date.now() + 25000;
  let typedIntoSearch = false;
  while (Date.now() < deadline) {
    for (const frame of page.frames()) {
      try {
        const outcome = await frame.evaluate(({ selector, wanted, allowFirstOption }) => {
          const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
          const normalize = (value) => compact(value)
            .normalize("NFD")
            .replace(/[̀-ͯ]/g, "")
            .toLowerCase();
          const visible = (element) => element instanceof HTMLElement
            && element.getClientRects().length > 0
            && getComputedStyle(element).visibility !== "hidden";
          let root = null;
          try {
            root = document.querySelector(selector);
          } catch {
          }
          if (!(root instanceof HTMLElement)) return { status: "missing" };
          if (root.classList.contains("vs--disabled") || root.getAttribute("aria-disabled") === "true") {
            return { status: "not-ready" };
          }
          const wantedNormalized = normalize(wanted);
          const current = [...root.querySelectorAll(".vs__selected")]
            .filter(visible)
            .map((element) => compact(element.innerText))
            .find((text) => text && !normalize(text).includes("wybierz"));
          if (current && (normalize(current) === wantedNormalized
            || (allowFirstOption && normalize(current).includes(wantedNormalized))
            || (allowFirstOption && wantedNormalized.includes(normalize(current))))) {
            return { status: "selected", text: current };
          }

          const search = root.querySelector(".vs__search");
          const controlledId = search?.getAttribute("aria-controls");
          const list = (controlledId && document.getElementById(controlledId))
            || root.querySelector(".vs__dropdown-menu, [role='listbox']");
          const candidates = [...(list?.querySelectorAll(".vs__dropdown-option, [role='option']") || [])]
            .filter((element) => visible(element)
              && !element.classList.contains("vs__dropdown-option--disabled")
              && element.getAttribute("aria-disabled") !== "true")
            .map((element) => ({ element, text: compact(element.innerText) }))
            .filter((entry) => entry.text);

          if (candidates.length === 0) {
            const opener = search || root.querySelector(".vs__dropdown-toggle") || root;
            opener.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
            opener.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
            if (typeof opener.click === "function") opener.click();
            opener.focus?.();
            return { status: "opened" };
          }

          
          
          
          const match = candidates.find((entry) => normalize(entry.text) === wantedNormalized)
            || candidates.find((entry) => wantedNormalized && normalize(entry.text).includes(wantedNormalized))
            || candidates.find((entry) => wantedNormalized && wantedNormalized.includes(normalize(entry.text)))
            || (allowFirstOption ? candidates[0] : null);
          if (!match) return { status: "no-option" };
          match.element.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
          match.element.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
          match.element.click();
          return { status: "selected", text: match.text };
        }, { selector: field.selector, wanted: String(wantedText), allowFirstOption: typedIntoSearch });
        if (outcome?.status === "selected") return true;
        if ((outcome?.status === "opened" || outcome?.status === "no-option") && !typedIntoSearch) {
          
          
          typedIntoSearch = await frame.evaluate(({ selector, wanted }) => {
            let root = null;
            try {
              root = document.querySelector(selector);
            } catch {
            }
            const search = root?.querySelector(".vs__search");
            if (!(search instanceof HTMLInputElement) || search.disabled) return false;
            search.focus();
            const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
            if (setter) setter.call(search, wanted);
            else search.value = wanted;
            search.dispatchEvent(new Event("input", { bubbles: true }));
            search.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }));
            return true;
          }, { selector: field.selector, wanted: String(wantedText) }).catch(() => false);
        }
      } catch {
      }
    }
    await delay(400);
  }
  return false;
}

async function applyGeneraliAnswers(page, fields, answers) {
  let filledCount = 0;
  const failedFields = [];
  for (const field of Array.isArray(fields) ? fields : []) {
    const answer = answers?.[field.id];
    if (answer === null || answer === undefined) continue;
    const value = typeof answer === "object" ? String(answer.value ?? "") : String(answer);
    if (!value.trim() && field.kind !== "checkbox") continue;
    let filled = false;
    if (field.kind === "select") filled = await setGeneraliSelectAnswer(page, field, value.trim());
    else if (field.kind === "combobox") filled = await setGeneraliVueAnswer(page, field, value.trim());
    if (!filled) filled = (await fillRequestedAnswers(page, [field], { [field.id]: answer })) > 0;
    if (filled) filledCount += 1;
    else failedFields.push(field);
    await delay(250);
  }
  return { filledCount, failedFields };
}

async function completeGeneraliVehicleStep(page, pesel, onProgress, requestInput, knownVehicleData = {}) {
  const ownerFormSelector = "#owner-drivingLicenceIssueYear, select[id*='drivingLicenceIssueYear'], select[name*='drivingLicenceIssueYear']";
  
  
  const knownValueForField = composeKnownAnswerResolvers(
    knownVehicleData?.extraResolver,
    buildKnownVehicleAnswerResolver({
      vin: knownVehicleData?.vin,
      vehicleInfo: knownVehicleData?.vehicleInfo,
      pesel,
    }),
  );
  if (!(await frameHasSelector(page, ownerFormSelector))) {
    await setGeneraliTextInput(
      page,
      "input[name='cepikDataForm.identifier'], input[data-vv-as='REGON / PESEL']",
      pesel,
    );
    onProgress({
      status: "running",
      title: "Pobieranie danych pojazdu…",
      detail: "",
    });
    const fetched = await clickGeneraliControlBySelector(
      page,
      "button[name='_eventId_getCepikData'], input[name='_eventId_getCepikData']",
      30000,
    );
    if (!fetched) throw new Error("Nie udało się uruchomić przycisku „Pobierz dane” w Generali.");
    if (!(await waitForGeneraliSelector(page, ownerFormSelector, 90000))) {
      throw new Error("Generali nie wyświetliło danych właściciela i pojazdu po użyciu „Pobierz dane”.");
    }
  }

  
  
  
  onProgress({
    status: "running",
    title: "Czekanie na wczytanie danych pojazdu…",
    detail: "",
  });
  await page.waitForNetworkIdle({ idleTime: 1500, timeout: 25000 }).catch(() => {});
  await delay(2000);

  if (typeof requestInput !== "function") {
    throw new Error("Formularz pojazdu Generali wymaga uzupełnienia pól w Bazie Polis, ale kanał pytań jest niedostępny.");
  }

  const clickNext = () => clickGeneraliControlBySelector(
    page,
    "button[name='_eventId_next'], input[name='_eventId_next']",
    20000,
  );
  const signatureOf = (fields) => fields
    .map((field) => `${field.frameHint}|${field.selector}`)
    .sort()
    .join("\n");
  
  
  let deadline = Date.now() + 45 * 60 * 1000;
  let answeredSignature = "";
  const failedFieldKeys = new Set();
  const fieldKeyOf = (field) => `${field.frameHint}|${field.selector}`;
  for (let round = 1; round <= 8 && Date.now() < deadline; round += 1) {
    if (!(await frameHasSelector(page, ownerFormSelector))) return;
    onProgress({
      status: "running",
      title: "Sprawdzanie formularza…",
      detail: "",
    });
    let request = filterGeneraliVehicleFields(await collectGeneraliFormFields(page));
    
    
    const emptyScanAttempts = round === 1 ? 5 : 1;
    for (let attempt = 0; attempt < emptyScanAttempts && request.fields.length === 0; attempt += 1) {
      await delay(2000);
      request = filterGeneraliVehicleFields(await collectGeneraliFormFields(page));
    }
    if (request.fields.length === 0 || signatureOf(request.fields) === answeredSignature) {
      onProgress({
        status: "running",
        title: "Przechodzenie dalej w kalkulacji…",
        detail: "",
      });
      await page.waitForNetworkIdle({ idleTime: 800, timeout: 8000 }).catch(() => {});
      if (!(await clickNext())) throw new Error("Nie udało się kliknąć „Dalej” w kalkulatorze Generali.");
      await delay(1200);
      await page.waitForNetworkIdle({ idleTime: 700, timeout: 7000 }).catch(() => {});
      if (!(await frameHasSelector(page, ownerFormSelector))) return;
      answeredSignature = "";
      continue;
    }
    const cascadeFields = request.fields.filter((field) => field.kind === "combobox");
    if (cascadeFields.length > 1) {
      const firstCascade = cascadeFields[0];
      request = {
        ...request,
        fields: request.fields.filter((field) => field.kind !== "combobox" || field === firstCascade),
      };
    }
    for (const field of request.fields) {
      if (field.kind === "combobox" && field.options.length === 0) {
        field.options = await readGeneraliVueOptions(page, field);
      }
    }
    const autoAnswers = {};
    for (const field of request.fields) {
      const known = knownValueForField(field);
      if (known) autoAnswers[field.id] = { value: known };
    }
    if (Object.keys(autoAnswers).length > 0) {
      onProgress({
        status: "running",
        title: "Uzupełnianie znanych danych pojazdu…",
        detail: "",
      });
      const autoFields = request.fields.filter((field) => autoAnswers[field.id]);
      const { failedFields: autoFailed } = await applyGeneraliAnswers(page, autoFields, autoAnswers);
      const autoFailedKeys = new Set(autoFailed.map((field) => fieldKeyOf(field)));
      request = {
        ...request,
        fields: request.fields.filter((field) => !autoAnswers[field.id] || autoFailedKeys.has(fieldKeyOf(field))),
      };
      if (request.fields.length === 0) {
        answeredSignature = "";
        await delay(1000);
        continue;
      }
    }
    const askableFields = request.fields.filter((field) => !(field.kind === "combobox" && field.options.length === 0));
    if (askableFields.length > 0 && askableFields.length < request.fields.length) {
      request = { ...request, fields: askableFields };
    }
    for (const field of request.fields) {
      if (failedFieldKeys.has(fieldKeyOf(field))) field.invalid = true;
    }
    
    const askStartedAt = Date.now();
    const answers = await requestInput({ ...request, round });
    deadline += Date.now() - askStartedAt;
    if (!answers || typeof answers !== "object") {
      throw new Error("Pola formularza Generali nie zostały uzupełnione w Bazie Polis (brak odpowiedzi).");
    }
    const { failedFields } = await applyGeneraliAnswers(page, request.fields, answers);
    onProgress({
      status: "running",
      title: "Uzupełnianie danych w Generali…",
      detail: "",
    });
    for (const field of request.fields) failedFieldKeys.delete(fieldKeyOf(field));
    if (failedFields.length === 0) {
      answeredSignature = signatureOf(request.fields);
    } else {
      
      
      answeredSignature = "";
      for (const field of failedFields) failedFieldKeys.add(fieldKeyOf(field));
      onProgress({
        status: "running",
        title: "Część pól nie została przyjęta - poproszę o nie jeszcze raz…",
        detail: "",
      });
    }
    await delay(1200);
  }
  throw new Error("Formularz pojazdu Generali nie został ukończony w wyznaczonym czasie.");
}

export function buildGeneraliIncludedRiskRows(values = []) {
  const rows = [];
  const seen = new Set();
  for (const value of Array.isArray(values) ? values : []) {
    const text = String(value || "").replace(/\s+/g, " ").trim();
    const normalized = normalizeText(text);
    if (!normalized
      || /\b(?:nie zawiera|brak ochrony|wylacz|opcjonal|dobierz|dodaj|rozszerz)\b/.test(normalized)) {
      continue;
    }
    let key = "";
    let label = "";
    if (/\bnnw\b/.test(normalized)) {
      key = "nnw";
      label = "NNW";
    } else if (/\bassistance\b/.test(normalized)) {
      key = "assistance";
      const name = text
        .replace(/^.*?\bassistance\b/i, "")
        .replace(/\b(?:w cenie|tak|zawiera|ochrona)\b/gi, " ")
        .replace(/^[\s:;,./+–—-]+|[\s:;,./+–—-]+$/g, "")
        .replace(/\s+/g, " ")
        .trim();
      label = name ? `Assistance (${name.slice(0, 36)})` : "Assistance";
    } else if (/\bszyb(?:y|a|ami|owe)?\b/.test(normalized)) {
      key = "szyby";
      label = "Szyby";
    } else if (/\bautocasco\b|(^|[^a-z])ac([^a-z]|$)/.test(normalized)) {
      key = "ac";
      label = /\baso\b|\bserwis/.test(normalized)
        ? "AC (ASO/serwisowy)"
        : /\bkosztorys/.test(normalized)
          ? "AC (kosztorys)"
          : "AC";
    } else if (/^oc\b|oc posiadaczy|odpowiedzialnosc cywilna/.test(normalized)) {
      key = "oc";
      label = "OC";
    }
    if (!key || seen.has(key)) continue;
    seen.add(key);
    rows.push({ label, amount: "w cenie" });
  }
  return rows;
}

export function normalizeGeneraliPremiumVariants(premiums) {
  const entries = Array.isArray(premiums) ? premiums.map((premium) => {
    const { includedRiskTexts, ...publicPremium } = premium || {};
    const componentsList = Array.isArray(premium?.componentsList)
      ? premium.componentsList.map((component) => ({ ...component }))
      : [];
    const existingRisks = new Set(componentsList.map((component) => (
      normalizeText(component?.label).match(/\b(?:nnw|assistance|szyb|autocasco|ac|oc)\b/)?.[0] || ""
    )));
    for (const component of buildGeneraliIncludedRiskRows(includedRiskTexts)) {
      const risk = normalizeText(component.label).match(/\b(?:nnw|assistance|szyb|autocasco|ac|oc)\b/)?.[0] || "";
      if (!risk || existingRisks.has(risk)) continue;
      existingRisks.add(risk);
      componentsList.push(component);
    }
    return {
      ...publicPremium,
      components: { ...(premium?.components || {}) },
      componentsList,
    };
  }) : [];
  const ocCandidates = entries.filter((premium) => (
    /pakiet oc|rozszerzenia pakiet/i.test(normalizeText(premium?.label))
  ));
  if (ocCandidates.length === 0) return entries;

  const amountValue = (premium) => {
    const parsed = Number.parseFloat(
      String(premium?.amount || "").replace(/\s/g, "").replace(",", ".").replace(/[^\d.]/g, ""),
    );
    return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
  };
  const ocVariantOrder = new Map(
    [...ocCandidates]
      .sort((first, second) => amountValue(first) - amountValue(second))
      .map((premium, index) => [premium, index + 1]),
  );
  return entries.map((premium) => {
    if (!/pakiet oc|rozszerzenia pakiet/i.test(normalizeText(premium?.label))) return premium;
    const amount = String(premium.amount || "").trim();
    const otherComponents = premium.componentsList.filter((component) => (
      !/^oc\b(?!\s*[/+])/i.test(String(component?.label || "").trim())
    ));
    return {
      ...premium,
      label: ocCandidates.length > 1
        ? `Pakiet OC – wariant ${ocVariantOrder.get(premium)}`
        : "Pakiet OC",
      details: "",
      components: {
        ...premium.components,
        oc: amount,
      },
      componentsList: amount
        ? [{ label: "OC", amount }, ...otherComponents]
        : otherComponents,
    };
  });
}

export async function extractGeneraliPremiums(page) {
  const collected = [];
  for (const frame of page.frames()) {
    try {
      const premiums = await frame.evaluate(() => {
        const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
        const normalize = (value) => compact(value)
          .normalize("NFD")
          .replace(/[̀-ͯ]/g, "")
          .toLowerCase();
        const amountPattern = /(\d{1,3}(?:[ . ]\d{3})*|\d+)(?:,([0-9]{2}))?\s*(?:zł|PLN)/i;
        const visible = (element) => element instanceof HTMLElement
          && element.getClientRects().length > 0
          && getComputedStyle(element).visibility !== "hidden";
        const pageText = normalize(document.body?.innerText || "");
        if (!/(skladk|ofert|wariant|pakiet|rata|roczn|do zaplaty)/.test(pageText)) return [];
        const sumLikeContext = /suma|sumie|sum ubezpieczenia|wartość pojazdu|wartosc pojazdu|limit|franszyza|udział własny|udzial wlasny|zniżk|znizk|bonus|przebieg/i;
        const premiumLikeContext = /składk|skladk|do zapłaty|do zaplaty|cena|koszt|łącznie|lacznie|rocznie|za rok|miesięcznie|miesiecznie|rata|raty|kup|wybierz ofert/i;
        const componentPattern = /(^|[^a-ząćęłńóśźż])(oc|ac|autocasco|nnw|assistance|zielona karta|szyby|opony|bagaż|bagaz|ochrona zniżki|ochrona znizki)([^a-ząćęłńóśźż]|$)/i;
        const digitFontSize = (element) => {
          let max = 0;
          const measure = (candidate) => {
            if (!/\d/.test(candidate.textContent || "")) return;
            const size = Number.parseFloat(getComputedStyle(candidate).fontSize) || 0;
            if (size > max) max = size;
          };
          measure(element);
          for (const child of element.querySelectorAll("*")) measure(child);
          return max;
        };

        const candidates = [...document.querySelectorAll("[class*='price'], [class*='premium'], [class*='sklad'], [class*='amount'], strong, b, td, span, div, p, h1, h2, h3, h4")]
          .filter(visible)
          
          .filter((element) => !element.closest("select, option, optgroup, [role='listbox'], [role='option']"))
          .map((element, index) => ({ element, index, text: compact(element.innerText || element.textContent) }))
          .filter(({ text }) => text.length > 0 && text.length <= 140 && amountPattern.test(text))
          
          
          .filter(({ text }) => (text.match(new RegExp(amountPattern.source, "gi")) || []).length < 2)
          .sort((left, right) => left.text.length - right.text.length)
          .slice(0, 60);

        const entries = [];
        const used = new Set();
        for (const { element, index, text } of candidates) {
          const amountMatch = text.match(amountPattern);
          if (!amountMatch) continue;
          const numericAmount = Number(amountMatch[1].replace(/[ . ]/g, "")) + Number(amountMatch[2] || "0") / 100;
          if (!Number.isFinite(numericAmount) || numericAmount < 100 || numericAmount > 100000) continue;
          if ([...used].some((saved) => saved.contains(element) || element.contains(saved))) continue;

          let rowContext = text;
          let rowNode = element;
          for (let level = 0; level < 3 && rowNode.parentElement; level += 1) {
            const parentText = compact(rowNode.parentElement.innerText || rowNode.parentElement.textContent);
            if (parentText.length > 160) break;
            rowNode = rowNode.parentElement;
            rowContext = parentText;
          }
          if (sumLikeContext.test(rowContext) && !premiumLikeContext.test(rowContext)) continue;
          
          
          const roundThousandSum = !amountMatch[2] && numericAmount >= 1000 && numericAmount % 1000 === 0;
          if (roundThousandSum && !premiumLikeContext.test(rowContext)) continue;

          let container = element;
          for (let level = 0; level < 5 && container.parentElement; level += 1) {
            const parentText = compact(container.parentElement.innerText || container.parentElement.textContent);
            if (parentText.length > 900) break;
            container = container.parentElement;
            if (/(offer|ofert|variant|wariant|package|pakiet|product|produkt|tile|card|summary|podsum)/i.test(container.className || "")) break;
          }
          used.add(element);

          const heading = [...container.querySelectorAll("h1, h2, h3, h4, [class*='title'], [class*='name'], strong, b")]
            .find((candidate) => visible(candidate) && !amountPattern.test(compact(candidate.textContent)));
          let label = compact(heading?.innerText || heading?.textContent || "");
          if (!label) {
            
            
            label = compact(rowContext.replace(new RegExp(amountPattern.source, "gi"), " ")).replace(/^[\s–—|:•-]+|[\s–—|:•-]+$/g, "").slice(0, 100);
          }

          
          const componentsList = [];
          const includedRiskTexts = [];
          const seenComponentRows = new Set();
          for (const rowElement of container.querySelectorAll("li, tr, [class*='row'], [class*='item'], p, div")) {
            if (componentsList.length >= 10) break;
            if (!visible(rowElement)) continue;
            const rowText = compact(rowElement.innerText);
            if (!rowText || rowText.length > 90 || seenComponentRows.has(rowText)) continue;
            if (rowElement.querySelector("li, tr, [class*='row'], [class*='item']")) continue;
            const rowAmounts = [...rowText.matchAll(new RegExp(amountPattern.source, "gi"))];
            const rowAmount = rowAmounts.at(-1);
            if (!rowAmount || !componentPattern.test(rowText)) continue;
            if (rowAmounts.length === 1 && sumLikeContext.test(rowText) && !premiumLikeContext.test(rowText)) continue;
            seenComponentRows.add(rowText);
            const componentLabel = compact(
              `${rowText.slice(0, rowAmount.index)} ${rowText.slice(rowAmount.index + rowAmount[0].length)}`,
            ).replace(/^[\s–—|:•-]+|[\s–—|:•-]+$/g, "").slice(0, 60);
            if (!componentLabel) continue;
            componentsList.push({
              label: componentLabel,
              amount: rowAmount[0].replace(/PLN/i, "zł").replace(/ /g, " ").replace(/\s+/g, " ").trim(),
            });
          }
          for (const rowElement of container.querySelectorAll("li, tr, [class*='row'], [class*='item'], p, div, span, label")) {
            if (includedRiskTexts.length >= 12) break;
            if (!visible(rowElement)) continue;
            const rowText = compact(rowElement.innerText || rowElement.textContent);
            if (!rowText || rowText.length > 100 || seenComponentRows.has(rowText)) continue;
            if (amountPattern.test(rowText) || !componentPattern.test(rowText)) continue;
            if (/\b(?:nie zawiera|brak ochrony|wyłącz|opcjonal|dobierz|dodaj|rozszerz)\b/i.test(rowText)) continue;
            if (rowElement.matches(
              "[aria-disabled='true'], [aria-checked='false'], [data-included='false'], [class*='disabled'], [class*='excluded'], [class*='unavailable']",
            )) {
              continue;
            }
            const controls = [...rowElement.querySelectorAll(
              'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [aria-checked]',
            )];
            if (controls.length > 0 && !controls.some((control) => (
              control.checked === true || control.getAttribute("aria-checked") === "true"
            ))) {
              continue;
            }
            if (!includedRiskTexts.includes(rowText)) includedRiskTexts.push(rowText);
          }

          entries.push({
            index,
            fontSize: digitFontSize(element),
            insurer: "Generali",
            label: label || `Wariant ${entries.length + 1}`,
            amount: amountMatch[0].replace(/PLN/i, "zł").replace(/ /g, " ").replace(/\s+/g, " ").trim(),
            details: "",
            componentsList,
            includedRiskTexts,
            components: { oc: "", ac: "", vehicleValue: "", assistance: "", nnw: "" },
          });
        }
        if (entries.length === 0) return [];
        
        
        const maxFontSize = Math.max(...entries.map((entry) => entry.fontSize));
        const prominent = entries.filter((entry) => entry.fontSize >= maxFontSize * 0.75);
        return (prominent.length > 0 ? prominent : entries)
          .sort((left, right) => left.index - right.index)
          .map(({ index, fontSize, ...premium }) => premium);
      });
      collected.push(...premiums);
    } catch {
    }
  }

  const unique = [];
  const seen = new Set();
  const seenAmounts = new Set();
  for (const premium of collected) {
    const key = `${normalizeText(premium.label)}|${premium.amount}`;
    if (seen.has(key)) continue;
    
    
    const genericLabel = /^(cena|koszt|oferta|wariant \d+|)$/i.test(normalizeText(premium.label));
    if (genericLabel && seenAmounts.has(premium.amount)) continue;
    seen.add(key);
    seenAmounts.add(premium.amount);
    unique.push(premium);
  }
  return normalizeGeneraliPremiumVariants(unique.slice(0, 6));
}

function emptyGeneraliPdfRetry() {
  return {
    attempted: 0,
    succeeded: 0,
    failed: 0,
    backoff: 0,
    quarantined: 0,
    unsupported: 0,
    deferred: 0,
    remaining: 0,
    batchComplete: true,
    circuitOpen: false,
    circuitCode: null,
    manualRetryAvailable: false,
  };
}

function failedGeneraliPdfRetry(remaining, circuitCode = "unknown", deferred = 0) {
  return {
    ...emptyGeneraliPdfRetry(),
    deferred: Math.max(0, deferred),
    remaining: Math.max(0, remaining),
    batchComplete: false,
    circuitOpen: true,
    circuitCode,
  };
}

export function buildGeneraliImportCompletion({
  freshCount = 0,
  downloadPdfs = false,
  pdfOutcome = null,
  pdfExpectedCount = freshCount,
} = {}) {
  const normalizedFreshCount = Number.isSafeInteger(freshCount) && freshCount >= 0
    ? freshCount
    : 0;
  const normalizedExpectedCount = Number.isSafeInteger(pdfExpectedCount) && pdfExpectedCount >= 0
    ? pdfExpectedCount
    : normalizedFreshCount;
  if (!downloadPdfs) {
    return {
      freshCount: normalizedFreshCount,
      pdfSaved: null,
      pdfRemaining: null,
      pdfRetry: null,
      pdfRetryIdentities: [],
    };
  }
  const hasPdfOutcome = pdfOutcome
    && typeof pdfOutcome === "object"
    && pdfOutcome.pdfRetry
    && typeof pdfOutcome.pdfRetry === "object";
  if (!hasPdfOutcome) {
    return {
      freshCount: normalizedFreshCount,
      pdfSaved: 0,
      pdfRemaining: normalizedExpectedCount,
      pdfRetry: failedGeneraliPdfRetry(normalizedExpectedCount, "write_failed"),
      pdfRetryIdentities: [],
    };
  }
  return {
    freshCount: normalizedFreshCount,
    pdfSaved: Number.isSafeInteger(pdfOutcome.saved) ? pdfOutcome.saved : 0,
    pdfRemaining: Number.isSafeInteger(pdfOutcome.remaining) ? pdfOutcome.remaining : 0,
    pdfRetry: pdfOutcome.pdfRetry,
    pdfRetryIdentities: Array.isArray(pdfOutcome.pdfRetryIdentities)
      ? pdfOutcome.pdfRetryIdentities
      : [],
  };
}

export async function runGeneraliPortfolioPdfPhase({
  page,
  retryStore,
  pdfDirectory,
  policies,
  onProgress = () => {},
  limit = 50,
  skipPolicyNumbers = [],
  knownPolicyNumbers = [],
} = {}) {
  if (!Number.isSafeInteger(limit) || limit <= 0) {
    throw new TypeError("limit musi byc dodatnia liczba calkowita.");
  }
  const eligible = prepareGeneraliPortfolioPdfEntries({
    policies,
    skipPolicyNumbers,
    knownPolicyNumbers,
  });
  const pdfDirectoryReady = typeof pdfDirectory === "string"
    && pdfDirectory.trim().length > 0
    && !pdfDirectory.includes("\0");
  if (!pdfDirectoryReady) {
    const pdfRetry = failedGeneraliPdfRetry(
      eligible.length,
      "write_failed",
      Math.max(0, eligible.length - Math.min(eligible.length, limit)),
    );
    onProgress({
      status: "running",
      title: "Pobieranie PDF-ow przerwane",
      detail: "Faza dokumentow zostala bezpiecznie zatrzymana; polisy nadal zostana zaimportowane.",
    });
    return {
      attempted: 0,
      saved: 0,
      remaining: pdfRetry.remaining,
      pdfRetry,
      pdfRetryIdentities: [],
    };
  }
  if (eligible.length === 0) {
    onProgress({
      status: "running",
      title: "Pobieranie PDF-ow pominiete",
      detail: "Wszystkie polisy Generali maja juz podpiete pliki.",
    });
    return {
      attempted: 0,
      saved: 0,
      remaining: 0,
      pdfRetry: emptyGeneraliPdfRetry(),
      pdfRetryIdentities: [],
    };
  }

  const planned = Math.min(eligible.length, limit);
  onProgress({
    status: "running",
    title: "Pobieranie PDF-ow...",
    detail: `Do rozpatrzenia w tej partii: ${planned} plikow.`,
  });
  const retryStoreReady = retryStore
    && typeof retryStore.getDecisions === "function"
    && typeof retryStore.recordFailures === "function"
    && typeof retryStore.recordSuccesses === "function"
    && typeof retryStore.markUnsupported === "function";
  let batch;
  if (!retryStoreReady) {
    batch = {
      publicResult: failedGeneraliPdfRetry(
        eligible.length,
        "store_failed",
        Math.max(0, eligible.length - planned),
      ),
      privateResult: {
        manualRetryIdentities: [],
      },
    };
  } else {
    try {
      batch = await runGeneraliPortfolioPdfBatch({
        page,
        retryStore,
        pdfDirectory,
        policies,
        limit,
        skipPolicyNumbers,
        knownPolicyNumbers,
        onProgress({ processed, planned: batchPlanned }) {
          if (processed % 5 === 0 || processed === batchPlanned) {
            onProgress({
              status: "running",
              title: `Pobieranie PDF-ow... (${processed}/${batchPlanned})`,
              detail: "",
            });
          }
        },
      });
    } catch {
      
      
      batch = {
        publicResult: failedGeneraliPdfRetry(
          eligible.length,
          "unknown",
          Math.max(0, eligible.length - planned),
        ),
        privateResult: {
          manualRetryIdentities: [],
        },
      };
    }
  }

  const pdfRetry = batch.publicResult;
  
  
  console.error(
    `Generali PDF partia: rozmiar=${planned} attempted=${pdfRetry.attempted} ok=${pdfRetry.succeeded}`
    + ` failed=${pdfRetry.failed} backoff=${pdfRetry.backoff} unsupported=${pdfRetry.unsupported}`
    + ` obwod=${pdfRetry.circuitOpen ? pdfRetry.circuitCode || "unknown" : "-"}`
    + ` pozostalo=${pdfRetry.remaining}`,
  );
  onProgress({
    status: "running",
    title: pdfRetry.circuitOpen
      ? "Pobieranie PDF-ow przerwane"
      : "PDF-y pobrane",
    detail: pdfRetry.circuitOpen
      ? "Faza dokumentow zostala bezpiecznie zatrzymana; polisy nadal zostana zaimportowane."
      : `Zapisano ${pdfRetry.succeeded} z ${pdfRetry.attempted} rozpoczetych plikow.`,
  });
  return {
    attempted: pdfRetry.attempted,
    saved: pdfRetry.succeeded,
    remaining: pdfRetry.remaining,
    pdfRetry,
    pdfRetryIdentities: batch.privateResult.manualRetryIdentities,
  };
}

export async function finalizeGeneraliPortfolioApiImport({
  apiPolicies,
  mappingReport = null,
  sinceDate = "",
  sourceAlreadyFilteredToCheckpoint = false,
  downloadPdfs = false,
  page,
  retryStore,
  pdfDirectory = "",
  pdfPolicyLimit = 50,
  skipPdfPolicyNumbers = [],
  knownPdfPolicyNumbers = [],
  onProgress = () => {},
  onCompletion,
} = {}) {
  if (!Array.isArray(apiPolicies)) {
    throw new TypeError("apiPolicies musi byc tablica.");
  }
  const pdfExpectedCount = downloadPdfs
    ? prepareGeneraliPortfolioPdfEntries({
      policies: apiPolicies,
      skipPolicyNumbers: skipPdfPolicyNumbers,
      knownPolicyNumbers: knownPdfPolicyNumbers,
    }).length
    : 0;
  const pdfOutcome = downloadPdfs
    ? await runGeneraliPortfolioPdfPhase({
      page,
      retryStore,
      pdfDirectory,
      policies: apiPolicies,
      onProgress,
      limit: pdfPolicyLimit,
      skipPolicyNumbers: skipPdfPolicyNumbers,
      knownPolicyNumbers: knownPdfPolicyNumbers,
    })
    : null;
  const { emittedPolicies, emissionReport } = createPortfolioEmissionResult(apiPolicies, {
    sinceDate,
    sourceAlreadyFilteredToCheckpoint,
    getFreshnessDates: (policy) => policy?.issueDate,
    isBacklogPolicy: (policy) => (
      String(policy?.insurer || "").trim().toUpperCase() === "GENERALI"
      && policy?.pdfAvailable === true
    ),
  });
  const completion = buildGeneraliImportCompletion({
    freshCount: emissionReport.freshCount,
    downloadPdfs,
    pdfOutcome,
    pdfExpectedCount,
  });
  const completionMappingReport = assertPortfolioMappingComplete(mappingReport);
  onCompletion?.({
    ...completion,
    mappingReport: completionMappingReport,
    emissionReport,
  });
  return emittedPolicies;
}

export function createGeneraliAutomation({ browserPool, pdfRetryStore }) {
  const INSURER_KEY = "generali";
  let lastVisiblePage = null;

  
  
  async function openPage(headless) {
    return browserPool.openPage({
      insurerKey: INSURER_KEY,
      headless,
      reuseExisting: headless !== true,
    });
  }

  
  function isVisibleBrowserActive() {
    return browserPool.isHeadless(INSURER_KEY) === false;
  }

  async function calculateQuoteAttempt({
    registration,
    pesel,
    vin,
    username,
    password,
    vehicleInfo,
    navigationPath,
    formFields,
    resultNavigationPath,
    onPathLearned,
    onDiagnosticsCaptured,
    onResultPathLearned,
    onInputRequested,
    extraKnownAnswerResolver,
    onProgress,
    manualStartOnly = false,
    onOverlayAction,
  }, { headless = false } = {}) {
    const page = await openPage(headless);
    page.setDefaultTimeout(12000);
    page.setDefaultNavigationTimeout(60000);
    if (!headless) {
      lastVisiblePage = page;
      await bringPageWindowToFront(page);
      await attachClientInfoOverlay(page, vehicleInfo, { onAction: onOverlayAction }).catch(() => {});
    }
    try {
      const targetFields = [
        {
          key: "pesel",
          patterns: ["cepikdataform.identifier", "regon / pesel", "pesel", "customer-pesel", "client-pesel", "insured-pesel", "identyfikator klienta", "identity"],
          value: pesel,
        },
        {
          key: "registration",
          patterns: ["numer rejestracyjny", "nr rejestracyjny", "nr rej", "rejestracj", "registrationnumber", "registration-number", "registration"],
          value: registration,
        },
      ];

      onProgress({
        status: "running",
        title: headless ? "Kalkulacja Generali działa w tle…" : "Otwieranie kalkulatora Generali…",
        detail: "",
      });
      await page.goto(GENERALI_URL, { waitUntil: "domcontentloaded", timeout: 60000 });
      await loginIfNeeded(page, { username, password }, onProgress);
      
      
      await waitForPortal(page, onProgress, targetFields, onInputRequested, {
        allowUserInteraction: !headless,
      });
      const knownAnswerResolver = composeKnownAnswerResolvers(
        extraKnownAnswerResolver,
        buildKnownVehicleAnswerResolver({ vin, vehicleInfo, pesel }),
      );
      let formNavigation = null;
      try {
        formNavigation = await learnOrReplayFormPath({
          page,
          savedPath: navigationPath,
          savedFields: formFields,
          fields: targetFields,
          insurerName: "Generali",
          onPathLearned,
          onDiagnosticsCaptured,
          onProgress,
          requestInput: onInputRequested,
          knownAnswerResolver,
          allowManual: !headless && !manualStartOnly,
          patientReplay: !manualStartOnly,
        });
      } catch (error) {
        if (!manualStartOnly) throw error;
      }
      if (manualStartOnly) {
        onProgress({
          status: "running",
          title: "Karta Generali jest gotowa do ręcznego liczenia",
          detail: "Wpisz dane w formularzu samodzielnie.",
        });
        return [];
      }

      onProgress({
        status: "running",
        title: "Uzupełnianie danych w Generali…",
        detail: "",
      });
      if (!(await fillDescribedFields(page, targetFields, formNavigation?.formFields))) {
        throw new Error("Nie znaleziono pól numeru rejestracyjnego i PESEL w kalkulatorze Generali.");
      }
      await completeGeneraliVehicleStep(page, pesel, onProgress, onInputRequested, {
        vin,
        vehicleInfo,
        extraResolver: extraKnownAnswerResolver,
      });

      let premiums = await extractGeneraliPremiums(page);
      const areResultsReady = async () => {
        premiums = await extractGeneraliPremiums(page);
        return premiums.length > 0;
      };
      await learnOrReplayActionPath({
        page,
        savedPath: resultNavigationPath,
        isComplete: areResultsReady,
        requestInput: onInputRequested,
        knownAnswerResolver,
        stageTitle: "od formularza do składek Generali",
        manualInstruction: "Formularz pojazdu został uzupełniony odpowiedziami z Bazy i zatwierdzony. Dokończ kolejne kroki aż pojawią się składki; aplikacja zapamięta kliknięcia.",
        retryInstruction: "Zapamiętana droga po ekranie „Dalej” przestała pasować. Dokończ kolejne kroki ręcznie, aby zapisać nową sekwencję.",
        onPathLearned: onResultPathLearned,
        onProgress,
        allowManual: !headless,
      });
      premiums = await extractGeneraliPremiums(page);
      if (premiums.length === 0) throw new Error("Nie udało się odczytać składek z kalkulatora Generali w ciągu 3 minut.");
      return premiums;
    } finally {
      if (headless) await page.close().catch(() => {});
    }
  }

  
  
  
  async function calculateQuote(options) {
    const formPathReady = options?.runInBackground === true
      && Array.isArray(options?.navigationPath)
      && options.navigationPath.length > 0;
    const resultPathReady = Array.isArray(options?.resultNavigationPath)
      && options.resultNavigationPath.length > 0;
    
    
    const canRunInBackground = formPathReady && resultPathReady;
    if (formPathReady && !resultPathReady) {
      options?.onProgress?.({
        status: "running",
        title: "Nagranie trasy Generali jest niedokończone…",
        detail: "W otwartym oknie doklikaj do składek. Aplikacja zapisze drogę do wyników i następne kalkulacje pójdą w tle.",
      });
    }
    const visibleBrowserActive = isVisibleBrowserActive();
    if (!canRunInBackground || visibleBrowserActive) return calculateQuoteAttempt(options, { headless: false });
    return calculateQuoteAttempt(options, { headless: true });
  }

  
  
  
  
  
  
  
  
  async function waitForPortalHome(page, onProgress, { allowUserInteraction = true, requestInput = null } = {}) {
    let startedAt = Date.now();
    let lastActionTitle = "";
    let mfaRound = 0;
    while (Date.now() - startedAt < PORTAL_TIMEOUT_MS) {
      const url = page.url();
      const onLoginPage = url.includes("/iam/realms/") || url.includes("/login/cas") || url.includes("login-actions");
      if (url.includes("portal.generali.pl") && !onLoginPage) {
        await delay(800);
        return;
      }
      const interruption = await detectAuthenticationInterruption(page);
      if (interruption?.type === "error") throw new Error(interruption.message);
      if (interruption?.type === "action") {
        const isMfaPrompt = interruption.title.includes("Potwierdź logowanie");
        if (isMfaPrompt && typeof requestInput === "function" && mfaRound < 3) {
          const smsFields = (await collectGeneraliFormFields(page)).fields
            .filter((field) => ["text", "number"].includes(field.kind))
            .slice(0, 3);
          if (smsFields.length > 0) {
            mfaRound += 1;
            onProgress({
              status: "running",
              title: "Generali prosi o kod SMS…",
              detail: "Przepisz kod z telefonu w oknie importu.",
            });
            const answers = await requestInput({
              title: "Kod SMS do logowania Generali",
              round: mfaRound,
              fields: smsFields,
            });
            if (answers && typeof answers === "object") {
              
              startedAt = Date.now();
              await applyGeneraliAnswers(page, smsFields, answers);
              await clickGeneraliControlBySelector(
                page,
                "#kc-login, input[name='login'], button[type='submit'], input[type='submit']",
                8000,
              );
              await delay(1500);
              continue;
            }
          }
        }
        if (!allowUserInteraction && typeof requestInput !== "function") {
          throw manualInteractionRequired(`${interruption.title}. Import w tle został wstrzymany - użyj „Popraw ręcznie”, aby otworzyć okno Chrome.`);
        }
        if (interruption.title !== lastActionTitle) {
          lastActionTitle = interruption.title;
          onProgress({ status: "action_required", title: interruption.title, detail: interruption.detail });
        }
      }
      await delay(500);
    }
    throw new Error("Portal Generali nie otworzył się w ciągu 3 minut.");
  }

  
  async function importPortfolioAttempt({
    username,
    password,
    downloadPdfs = false,
    pdfDirectory = "",
    pdfPolicyLimit = 50,
    skipPdfPolicyNumbers = [],
    knownPdfPolicyNumbers = [],
    sinceDate = "",
    onCompletion,
    onProgress,
    onInputRequested = null,
  }, { headless = false } = {}) {
    const page = await openPage(headless);
    page.setDefaultTimeout(15000);
    page.setDefaultNavigationTimeout(60000);
    if (!headless) {
      lastVisiblePage = page;
      await bringPageWindowToFront(page);
    }
    try {
      onProgress({
        status: "running",
        title: headless ? "Import polis Generali działa w tle…" : "Otwieranie portalu Generali…",
        detail: "",
      });
      await page.goto(GENERALI_URL, { waitUntil: "domcontentloaded", timeout: 60000 });
      await loginIfNeeded(page, { username, password }, onProgress);
      await waitForPortalHome(page, onProgress, {
        allowUserInteraction: !headless,
        requestInput: onInputRequested,
      });

      onProgress({
        status: "running",
        title: "Zalogowano do Generali",
        detail: "Pobieranie polis…",
      });
      await page.goto(GENERALI_SEARCH_URL, { waitUntil: "networkidle2", timeout: 60000 }).catch(() => {});
      await delay(4000);
      let searchTemplateUrl = "";
      try {
        searchTemplateUrl = await captureGeneraliSearchTemplate(page);
      } catch (error) {
        
        if (headless) {
          throw manualInteractionRequired("Portal Generali wymaga potwierdzenia w oknie przeglądarki.");
        }
        throw error;
      }

      
      
      
      const pdfBacklogCount = countUnskippedPortfolioPdfSelectors({
        known: knownPdfPolicyNumbers,
        skipped: skipPdfPolicyNumbers,
        insurer: "generali",
      });
      const fetchFullForPdfBacklog = downloadPdfs === true && pdfBacklogCount > 0;
      const items = await fetchGeneraliSearchPolicies(page, {
        templateUrl: searchTemplateUrl,
        createDateFrom: fetchFullForPdfBacklog ? "" : sinceDate,
        onProgress: ({ collected, total }) => {
          onProgress({
            status: "running",
            title: "Pobieranie polis…",
            detail: `Pobrano ${collected} z ${total} pozycji.`,
          });
        },
      });

      
      const cutoff = new Date();
      cutoff.setFullYear(cutoff.getFullYear() - 1);
      const minEndDate = `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-${String(cutoff.getDate()).padStart(2, "0")}`;
      const mappingCollector = createPortfolioMappingCollector(items.length);
      const policies = mapGeneraliSearchPolicies(items, {
        minEndDate,
        mappingCollector,
      });
      const mappingReport = assertPortfolioMappingComplete(mappingCollector.createReport());
      const backlogMode = fetchFullForPdfBacklog && Boolean(sinceDate);
      const sourceAlreadyFilteredToCheckpoint = Boolean(sinceDate) && !backlogMode;
      if (policies.length === 0) {
        if (sinceDate) {
          
          
          return await finalizeGeneraliPortfolioApiImport({
            apiPolicies: [],
            mappingReport,
            sinceDate,
            sourceAlreadyFilteredToCheckpoint,
            downloadPdfs,
            page,
            retryStore: pdfRetryStore,
            pdfDirectory,
            pdfPolicyLimit,
            skipPdfPolicyNumbers,
            knownPdfPolicyNumbers,
            onProgress,
            onCompletion,
          });
        }
        throw new Error("Nie znaleziono żadnej polisy do pobrania.");
      }
      const previewEmission = createPortfolioEmissionResult(policies, {
        sinceDate,
        sourceAlreadyFilteredToCheckpoint,
        getFreshnessDates: (policy) => policy?.issueDate,
      });
      onProgress({
        status: "running",
        title: "Porządkowanie pobranych polis…",
        detail: sinceDate
          ? `Nowych polis: ${previewEmission.emissionReport.freshCount}.`
          : `Pobrano ${policies.length} polis.`,
      });

      
      
      return await finalizeGeneraliPortfolioApiImport({
        apiPolicies: policies,
        mappingReport,
        sinceDate,
        sourceAlreadyFilteredToCheckpoint,
        downloadPdfs,
        page,
        retryStore: pdfRetryStore,
        pdfDirectory,
        pdfPolicyLimit,
        skipPdfPolicyNumbers,
        knownPdfPolicyNumbers,
        onProgress,
        onCompletion,
      });
    } finally {
      if (headless) await page.close().catch(() => {});
      else lastVisiblePage = page;
    }
  }

  
  
  async function importPortfolio(options) {
    const startHeadless = isVisibleBrowserActive() ? false : true;
    if (!startHeadless) return importPortfolioAttempt(options, { headless: false });
    try {
      return await importPortfolioAttempt(options, { headless: true });
    } catch (error) {
      if (error?.code !== "MANUAL_INTERACTION_REQUIRED") throw error;
      options?.onProgress?.({
        status: "running",
        title: "Potrzebne jest okno przeglądarki…",
        detail: "Dokończ logowanie w oknie, które się otworzy.",
      });
      return importPortfolioAttempt(options, { headless: false });
    }
  }

  async function focusWindow() {
    const candidates = [];
    if (lastVisiblePage && !lastVisiblePage.isClosed()) candidates.push(lastVisiblePage);
    
    
    if (browserPool.isHeadless(INSURER_KEY) === false) {
      for (const page of [...browserPool.listPages(INSURER_KEY)].reverse()) {
        if (page && !page.isClosed?.() && !candidates.includes(page)) candidates.push(page);
      }
    }
    for (const candidate of candidates) {
      if (await bringPageWindowToFront(candidate)) return true;
    }
    return false;
  }

  
  
  async function close() {
    lastVisiblePage = null;
    await browserPool.closeInsurer(INSURER_KEY);
  }

  return { calculateQuote, importPortfolio, focusWindow, close };
}
