import puppeteer from "puppeteer-core";
import {
  bringPageWindowToFront,
  buildKnownVehicleAnswerResolver,
  composeKnownAnswerResolvers,
  fillDescribedFields,
  learnOrReplayActionPath,
  learnOrReplayFormPath,
} from "./etrzxtbsmrmtpfpyetfs.mjs";
import { attachClientInfoOverlay } from "./hkwnbsimckiekhsfxcto.mjs";
import {
  fetchTrastiPolicyDetailsBatch,
  fetchTrastiPolicyItems,
  hasTrastiApiToken,
  mapTrastiPolicyItems,
} from "./iepmqtwbjbswhxmtyeym.mjs";
import {
  prepareTrastiPortfolioPdfEntries,
  runTrastiPortfolioPdfBatch,
} from "./dhnpcgzxifcodoxghblz.mjs";
import {
  assertPortfolioMappingComplete,
  createPortfolioEmissionResult,
  createPortfolioMappingCollector,
} from "./fgroazrhswhewnaqxvgl.mjs";
import {
  countUnskippedPortfolioPdfSelectors,
} from "./abywezhrsiqkqfhujfvq.mjs";

const TRASTI_URL = "https://portal.trasti.pl/";
const PORTAL_TIMEOUT_MS = 3 * 60 * 1000;
const LOGIN_SELECTOR = "#email, input[type='email'], input[name='Adres e-mail']";
const PASSWORD_SELECTOR = "#password, input[type='password']";
const SUBMIT_SELECTOR = "#next, button[type='submit'], input[type='submit']";

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

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

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

export function isAuthenticatedTrastiPortalUrl(value) {
  try {
    const url = new URL(value);
    return url.hostname.toLowerCase() === "portal.trasti.pl"
      && (url.pathname === "/management" || url.pathname.startsWith("/management/"));
  } catch {
    return false;
  }
}

function isAuthenticatedPortal(page) {
  return isAuthenticatedTrastiPortalUrl(page.url());
}

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 findVisibleElement(page, selector) {
  for (const frame of page.frames()) {
    let elements = [];
    try {
      elements = await frame.$$(selector);
      for (const element of elements) {
        const visible = await element.evaluate((candidate) => candidate instanceof HTMLElement
          && candidate.getClientRects().length > 0
          && getComputedStyle(candidate).visibility !== "hidden");
        if (visible) {
          await Promise.all(elements
            .filter((candidate) => candidate !== element)
            .map((candidate) => candidate.dispose().catch(() => {})));
          return element;
        }
      }
    } catch {
    }
    await Promise.all(elements.map((element) => element.dispose().catch(() => {})));
  }
  return null;
}

async function frameHasSelector(page, selector) {
  const element = await findVisibleElement(page, selector);
  const found = Boolean(element);
  await element?.dispose().catch(() => {});
  return found;
}

const COOKIE_BANNER_SELECTOR = "#cookiescript_accept";

function isCookieBannerStep(step) {
  return /cookiescript/i.test(`${step?.id || ""} ${step?.selector || ""} ${step?.className || ""}`);
}

async function dismissCookieBannerIfPresent(page, timeoutMs = 4000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const banner = await findVisibleElement(page, COOKIE_BANNER_SELECTOR);
    if (banner) {
      try {
        await banner.evaluate((element) => element.click());
      } finally {
        await banner.dispose().catch(() => {});
      }
      return true;
    }
    await delay(300);
  }
  return false;
}

const VIN_PROMPT_INPUT_MARKER = "data-baza-polis-vin-input";
const VIN_PROMPT_SUBMIT_MARKER = "data-baza-polis-vin-submit";

export async function findVinVerificationPrompt(page) {
  for (const frame of page.frames()) {
    let prompt = null;
    try {
      prompt = await frame.evaluate(({ inputMarker, submitMarker }) => {
        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";
        for (const marked of document.querySelectorAll(`[${inputMarker}], [${submitMarker}]`)) {
          marked.removeAttribute(inputMarker);
          marked.removeAttribute(submitMarker);
        }
        const vinLike = (text) => /(^|[^a-z])vin([^a-z]|$)/.test(text) || text.includes("numer oferty");
        const findVerifyButton = (root) => [...root.querySelectorAll("button, [role='button'], input[type='submit']")]
          .find((candidate) => visible(candidate) && normalize(candidate.innerText || candidate.value) === "weryfikuj")
          || null;
        const inputs = [...document.querySelectorAll("input:not([type='hidden']):not([type='checkbox']):not([type='radio']):not([type='button']):not([type='submit']), textarea")]
          .filter((input) => visible(input) && !input.disabled && !input.readOnly);
        for (const input of inputs) {
          const control = input.closest("[class*='FormControl'], [class*='TextField'], [class*='field'], [class*='Field']") || input.parentElement;
          const description = normalize([
            input.getAttribute("placeholder"),
            input.getAttribute("aria-label"),
            input.labels ? [...input.labels].map((label) => label.innerText).join(" ") : "",
            control?.querySelector("label")?.innerText,
          ].filter(Boolean).join(" "));
          const isValidationField = normalize(input.getAttribute("name")) === "validationfield";
          if (!isValidationField && !vinLike(description)) continue;
          if (isValidationField && !vinLike(description)) {
            const pageText = normalize(document.body?.innerText || "");
            if (!vinLike(pageText) && !pageText.includes("zweryfikuj oferte")) continue;
          }
          let submit = null;
          let container = input;
          for (let level = 0; level < 9 && container && !submit; level += 1, container = container.parentElement) {
            submit = findVerifyButton(container);
          }
          if (!submit && isValidationField) submit = findVerifyButton(document);
          if (!submit && !isValidationField) continue;
          input.setAttribute(inputMarker, "1");
          if (submit) submit.setAttribute(submitMarker, "1");
          const invalid = input.getAttribute("aria-invalid") === "true"
            || /Mui-error/.test(String(control?.className || ""));
          return { value: compact(input.value), invalid, hasSubmit: Boolean(submit) };
        }
        return null;
      }, { inputMarker: VIN_PROMPT_INPUT_MARKER, submitMarker: VIN_PROMPT_SUBMIT_MARKER });
    } catch {
    }
    if (prompt) return { frame, ...prompt };
  }
  return null;
}

async function submitVinVerification(page, frame, value) {
  const input = await frame.$(`[${VIN_PROMPT_INPUT_MARKER}='1']`);
  if (!input) return false;
  try {
    const filled = await input.evaluate((element, wanted) => {
      const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
      const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
      element.focus();
      if (setter) setter.call(element, wanted);
      else element.value = wanted;
      element.dispatchEvent(new Event("input", { bubbles: true }));
      element.dispatchEvent(new Event("change", { bubbles: true }));
      element.dispatchEvent(new Event("blur", { bubbles: true }));
      const settled = String(element.value || "").replace(/\s+/g, "").toLowerCase();
      return settled === String(wanted || "").replace(/\s+/g, "").toLowerCase();
    }, value);
    if (!filled) return false;
    await delay(250);
    const submit = await frame.$(`[${VIN_PROMPT_SUBMIT_MARKER}='1']`);
    if (submit) {
      try {
        await submit.evaluate((element) => element.click());
      } finally {
        await submit.dispose().catch(() => {});
      }
    } else {
      await input.press("Enter");
    }
  } catch {
    return false;
  } finally {
    await input.evaluate((element, marker) => element.removeAttribute(marker), VIN_PROMPT_INPUT_MARKER).catch(() => {});
    await input.dispose().catch(() => {});
  }
  const deadline = Date.now() + 25000;
  while (Date.now() < deadline) {
    await delay(600);
    const prompt = await findVinVerificationPrompt(page);
    if (!prompt) {
      await delay(1200);
      if (!(await findVinVerificationPrompt(page))) return true;
      continue;
    }
    if (prompt.invalid) return false;
  }
  return false;
}

async function detectAuthenticationInterruption(page) {
  const pageText = normalizeText(await getCombinedPageText(page));
  if (
    pageText.includes("nieprawidlowy adres e-mail lub haslo")
    || pageText.includes("nieprawidlowe haslo")
    || pageText.includes("nie mozemy zalogowac cie przy uzyciu tych poswiadczen")
    || pageText.includes("konto lub haslo jest nieprawidlowe")
  ) {
    return { type: "error", message: "Trasti odrzuciło adres e-mail lub hasło." };
  }
  if (
    pageText.includes("kod weryfikacyjny")
    || pageText.includes("kod jednorazowy")
    || pageText.includes("uwierzytelnianie wieloskladnikowe")
    || pageText.includes("zatwierdz zadanie logowania")
  ) {
    return {
      type: "action",
      title: "Potwierdź logowanie do Trasti",
      detail: "Zatwierdź logowanie w otwartym oknie.",
    };
  }
  if (pageText.includes("captcha") || await frameHasSelector(page, "iframe[src*='captcha'], [class*='captcha']")) {
    return {
      type: "action",
      title: "Trasti wymaga potwierdzenia CAPTCHA",
      detail: "Rozwiąż CAPTCHA w otwartym oknie.",
    };
  }
  return null;
}

async function fillLoginForm(page, credentials, onProgress) {
  const login = await findVisibleElement(page, LOGIN_SELECTOR);
  const password = await findVisibleElement(page, PASSWORD_SELECTOR);
  const submit = await findVisibleElement(page, SUBMIT_SELECTOR);
  if (!login || !password || !submit) {
    await Promise.all([login, password, submit].filter(Boolean).map((element) => element.dispose().catch(() => {})));
    return false;
  }
  try {
    await login.click({ clickCount: 3 });
    await login.type(credentials.username, { delay: 18 });
    await password.click({ clickCount: 3 });
    await password.type(credentials.password, { delay: 18 });
    const valuesWereEntered = await Promise.all([
      login.evaluate((element, expected) => element instanceof HTMLInputElement && element.value === expected, credentials.username),
      password.evaluate((element, expected) => element instanceof HTMLInputElement && element.value === expected, credentials.password),
    ]);
    if (!valuesWereEntered.every(Boolean)) {
      throw new Error("Nie udało się wpisać adresu e-mail lub hasła do formularza logowania Trasti.");
    }
    onProgress({
      status: "running",
      title: "Logowanie do Trasti…",
      detail: "",
    });
    await submit.click();
    return true;
  } finally {
    await Promise.all([login, password, submit].map((element) => element.dispose().catch(() => {})));
  }
}

async function loginIfNeeded(page, credentials, onProgress) {
  const deadline = Date.now() + 60000;
  while (Date.now() < deadline) {
    if (isAuthenticatedPortal(page)) return;
    if (await frameHasSelector(page, LOGIN_SELECTOR) && await frameHasSelector(page, PASSWORD_SELECTOR)) {
      if (!(await fillLoginForm(page, credentials, onProgress))) {
        throw new Error("Nie udało się obsłużyć formularza logowania Trasti.");
      }
      return;
    }
    const interruption = await detectAuthenticationInterruption(page);
    if (interruption?.type === "error") throw new Error(interruption.message);
    await delay(350);
  }
  throw new Error("Nie udało się otworzyć formularza logowania Trasti.");
}

async function waitForPortal(page, onProgress, { allowUserInteraction = true } = {}) {
  const startedAt = Date.now();
  let lastActionTitle = "";
  while (Date.now() - startedAt < PORTAL_TIMEOUT_MS) {
    if (isAuthenticatedPortal(page) && !(await frameHasSelector(page, LOGIN_SELECTOR))) {
      await delay(800);
      return;
    }
    const interruption = await detectAuthenticationInterruption(page);
    if (interruption?.type === "error") throw new Error(interruption.message);
    if (interruption?.type === "action" && !allowUserInteraction) {
      throw manualInteractionRequired(`${interruption.title}. Kalkulacja w tle została wstrzymana - użyj „Popraw ręcznie”, aby otworzyć okno Chrome.`);
    }
    if (interruption?.type === "action" && interruption.title !== lastActionTitle) {
      lastActionTitle = interruption.title;
      onProgress({ status: "action_required", title: interruption.title, detail: interruption.detail });
    }
    await delay(500);
  }
  throw new Error("Panel Trasti nie otworzył się w ciągu 3 minut.");
}







async function waitForTrastiApiToken(page, { timeoutMs = 45000 } = {}) {
  const deadline = Date.now() + timeoutMs;
  for (;;) {
    if (await hasTrastiApiToken(page)) return true;
    if (Date.now() >= deadline) return false;
    await delay(1000);
  }
}

const TRASTI_AMOUNT_SOURCE = "(?:\\d{1,3}(?:[ .\\u00a0]\\d{3})+|\\d{1,6})(?:,\\d{2})?";

function trastiAmountToNumber(value) {
  const rawValue = String(value || "").trim();
  if (!/\d/.test(rawValue)) return null;
  const normalized = rawValue
    .replace(/[ .\u00a0]/g, "")
    .replace(",", ".")
    .replace(/[^\d.]/g, "");
  const amount = Number(normalized);
  return Number.isFinite(amount) ? amount : null;
}

function cleanTrastiSentence(value) {
  const compact = String(value || "")
    .replace(/\s+/g, " ")
    .replace(/^[\s–—|:•-]+|[\s–—|:•-]+$/g, "")
    .trim();
  if (!compact) return "";
  const sentence = compact.charAt(0).toUpperCase() + compact.slice(1);
  return /[.!?]$/.test(sentence) ? sentence : `${sentence}.`;
}



export function parseTrastiAnnualPremiumContext(rawContext, expectedCandidateAmount = null) {
  const context = String(rawContext || "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
  if (!context || !/\bpakiet\b/i.test(context)) return null;

  const suffixPattern = new RegExp(
    `(?<!\\d)(${TRASTI_AMOUNT_SOURCE})\\s*(?:zł|PLN)\\s*(?:\\/\\s*rok\\b|rocznie\\b)`,
    "gi",
  );
  const prefixPattern = new RegExp(
    `(?:łączna\\s+składka|laczna\\s+skladka|składka\\s+(?:roczna|łączna)|skladka\\s+(?:roczna|laczna)|łącznie\\s+do\\s+zapłaty|lacznie\\s+do\\s+zaplaty|do\\s+zapłaty|do\\s+zaplaty|za\\s+rok)`
      + `([^\\d]{0,30})(${TRASTI_AMOUNT_SOURCE})\\s*(?:zł|PLN)`,
    "gi",
  );
  const suffixMatches = [...context.matchAll(suffixPattern)]
    .filter((match) => {
      const leftContext = context.slice(Math.max(0, match.index - 45), match.index);
      return !/(?:\braty?\s*(?:od)?|\bmiesięcznie|\bmiesiecznie)\s*$/i.test(leftContext);
    })
    .map((match) => ({
      type: "suffix",
      index: match.index,
      end: match.index + match[0].length,
      rawAmount: match[1],
      numericAmount: trastiAmountToNumber(match[1]),
    }));
  const prefixMatches = [...context.matchAll(prefixPattern)]
    .filter((match) => !/\b(?:rata|raty|miesięcznie|miesiecznie)\b/i.test(match[1]))
    .map((match) => ({
      type: "prefix",
      index: match.index,
      end: match.index + match[0].length,
      rawAmount: match[2],
      numericAmount: trastiAmountToNumber(match[2]),
    }));
  const expectedAmount = trastiAmountToNumber(expectedCandidateAmount);
  const validAmount = (candidate) => (
    candidate.numericAmount !== null
    && candidate.numericAmount >= 100
    && candidate.numericAmount <= 100000
    && (expectedAmount === null || Math.abs(candidate.numericAmount - expectedAmount) <= 0.001)
  );
  const preferredMatches = suffixMatches.filter(validAmount);
  const fallbackMatches = prefixMatches.filter(validAmount);
  const matches = preferredMatches.length > 0 ? suffixMatches : prefixMatches;
  const chosen = preferredMatches[0] || fallbackMatches[0] || null;
  if (!chosen) return null;

  const chosenIndex = matches.indexOf(chosen);
  let segmentStart = 0;
  let segmentEnd = context.length;
  if (chosen.type === "suffix") {
    segmentStart = chosen.index;
    segmentEnd = matches[chosenIndex + 1]?.index ?? context.length;
  } else {
    segmentStart = matches[chosenIndex - 1]?.end ?? 0;
    segmentEnd = chosen.end;
  }
  const localContext = context.slice(segmentStart, segmentEnd).trim() || context;
  const amount = `${chosen.rawAmount.replace(/\u00a0/g, " ").replace(/\s+/g, " ")} zł`;

  const knownPackage = localContext.match(/\bpakiet\s+(podstawowy|polecany|premium)\b/i);
  const packageLabel = knownPackage
    ? `Pakiet ${knownPackage[1].toLocaleLowerCase("pl-PL")}`
    : "";
  const descriptionMatch = localContext.match(
    /\bpakiet\s+(?:podstawowy|polecany|premium)\s+zawiera\s+(.{3,180}?)(?=\s+(?:Dostosuj|ŁĄCZNA\s+SKŁADKA|LACZNA\s+SKLADKA|Raty?\s+od)\b|$)/i,
  );
  const installmentMatch = localContext.match(
    new RegExp(`\\braty?\\s+od\\s+(${TRASTI_AMOUNT_SOURCE})\\s*(?:zł|PLN)`, "i"),
  );
  const detailParts = [];
  const description = cleanTrastiSentence(descriptionMatch?.[1]);
  if (description) detailParts.push(description);
  if (installmentMatch?.[1]) {
    detailParts.push(`Raty od ${installmentMatch[1].replace(/\u00a0/g, " ")} zł.`);
  }

  return {
    amount,
    numericAmount: chosen.numericAmount,
    packageLabel,
    details: detailParts.join(" ").slice(0, 180) || "Trasti – składka roczna.",
  };
}

function normalizeTrastiPremiumCandidate(premium, index) {
  const rawContext = String(premium?.rawContext || "");
  if (rawContext) {
    const annual = parseTrastiAnnualPremiumContext(rawContext, premium.amount);
    if (!annual) return null;
    const candidateAmount = trastiAmountToNumber(premium.amount);
    if (candidateAmount === null || Math.abs(candidateAmount - annual.numericAmount) > 0.001) return null;

    const existingLabel = String(premium.label || "").replace(/\s+/g, " ").trim();
    const safeExistingLabel = existingLabel.length >= 2
      && existingLabel.length <= 80
      && !/^(?:\/|rok\b|rata\b|raty\b|od\b)/i.test(existingLabel)
      && !/\b(?:rata|raty)\s+od\b|(?:zł|PLN)/i.test(existingLabel)
      ? existingLabel
      : "";
    const componentsList = Array.isArray(premium?.componentsList) && premium.componentsList.length > 0
      ? premium.componentsList
      : buildTrastiIncludedRiskRowsFromContext(rawContext);
    return {
      ...premium,
      label: annual.packageLabel || safeExistingLabel || `Wariant Trasti ${index + 1}`,
      amount: annual.amount,
      details: annual.details,
      componentsList,
    };
  }
  return premium;
}

const TRASTI_API_PRODUCT = Object.freeze({
  oc: "OC",
  ac: new Set(["AC_ALL_RISK", "AC_ALL_RISK2", "AC_FIT", "AC_MINI"]),
  assistance: new Set(["A_K", "A_K2", "A_H", "A_H_PLUS"]),
  glass: "A_S",
  nnw: new Set(["NNW", "NNW2", "NNW3", "NNW_AUTO"]),
  repairVariant: "Wariant naprawy pojazdu",
  insuranceSum: "S_U",
});

export function buildTrastiIncludedRiskRowsFromContext(value) {
  const context = String(value || "").replace(/\s+/g, " ").trim();
  if (!context) return [];
  const normalized = normalizeText(context);
  const rows = [];
  if (/(^|[^a-z])oc([^a-z]|$)/.test(normalized)) {
    rows.push({ label: "OC", amount: "w cenie" });
  }
  if (/\b(?:autocasco|ac)\b/.test(normalized)) {
    rows.push({ label: "AC", amount: "w cenie" });
  }
  if (/\bnnw\b/.test(normalized)) {
    rows.push({ label: "NNW", amount: "w cenie" });
  }
  const assistanceMatch = context.match(
    /\bAssistance\b\s*([^|•]{0,48}?)(?=\s+(?:ŁĄCZNA\s+SKŁADKA|LACZNA\s+SKLADKA|NNW|AC|Szyby|Raty?\s+od)\b|$)/i,
  );
  if (assistanceMatch) {
    const name = String(assistanceMatch[1] || "")
      .replace(/^[\s:;,./+–—-]+|[\s:;,./+–—-]+$/g, "")
      .trim();
    rows.push({
      label: name ? `Assistance (${name.slice(0, 36)})` : "Assistance",
      amount: "w cenie",
    });
  }
  if (/\bszyb(?:y|a|ami|owe)?\b/.test(normalized)) {
    rows.push({ label: "Szyby", amount: "w cenie" });
  }
  return rows;
}

function trastiArray(value) {
  return Array.isArray(value) ? value : [];
}

function trastiEnumIs(value, numericValue, textValue) {
  return value === numericValue || normalizeText(value) === normalizeText(textValue);
}

function unwrapTrastiMultiOfferResponse(payload) {
  const queue = [payload];
  const visited = new Set();
  while (queue.length > 0) {
    const candidate = queue.shift();
    if (!candidate || typeof candidate !== "object" || visited.has(candidate)) continue;
    visited.add(candidate);
    if (Array.isArray(candidate.products)
      && Array.isArray(candidate.options)
      && Array.isArray(candidate.combinations)
      && Array.isArray(candidate.grillConfigurations)) {
      return candidate;
    }
    for (const key of ["data", "result", "value", "response"]) {
      if (candidate[key] && typeof candidate[key] === "object") queue.push(candidate[key]);
    }
  }
  return null;
}

function buildTrastiProductViews(multiOffer, grill) {
  const brandId = grill?.brandId;
  const selectedProducts = trastiArray(grill?.selectedProducts);
  const optionDefinitions = trastiArray(multiOffer?.options)
    .filter((option) => option?.brandId === brandId);
  const views = trastiArray(multiOffer?.products)
    .filter((product) => product?.brandId === brandId)
    .map((product) => {
      const selectedProduct = selectedProducts.find((selected) => selected?.code === product?.code);
      const selectedOptions = trastiArray(selectedProduct?.selectedOption);
      const options = trastiArray(product?.optionKeys).map((optionCode) => {
        const definition = optionDefinitions.find((option) => (
          option?.code === optionCode && option?.productCode === product?.code
        ));
        if (!definition) return null;
        const packetOption = selectedOptions.find((option) => option?.key === optionCode);
        const values = trastiArray(definition?.values);
        let selectedValue = packetOption?.value;
        if (!values.some((option) => option?.value === selectedValue)) {
          selectedValue = values.find((option) => option?.value === definition?.defaultValue)?.value
            ?? values[0]?.value;
        }
        return {
          ...definition,
          code: optionCode,
          selected: Boolean(packetOption),
          selectedValue,
        };
      }).filter(Boolean);

      
      
      
      const dependency = product?.code === "AC_ALL_RISK2"
        ? options.find((option) => option.code === TRASTI_API_PRODUCT.repairVariant)
        : null;
      const normalizedOptions = options.map((option) => {
        if (!dependency?.selectedValue || option === dependency) return option;
        const allowedValues = trastiArray(option.values).filter((value) => (
          !value?.additionalRequiredName
          || !trastiArray(value?.additionalRequiredValues).length
          || (value.additionalRequiredName === dependency.code
            && value.additionalRequiredValues.includes(dependency.selectedValue))
        ));
        if (allowedValues.length === 0 || allowedValues.some((value) => value?.value === option.selectedValue)) {
          return option;
        }
        return { ...option, selectedValue: allowedValues[0]?.value };
      });
      return {
        code: product?.code,
        brandId: product?.brandId,
        selected: Boolean(selectedProduct) && product?.isAvailable !== false,
        options: normalizedOptions,
      };
    });

  
  if (views.some((view) => view.code === "GWC" && view.selected)) {
    const ouz = views.find((view) => view.code === "OUZ");
    if (ouz) {
      ouz.selected = false;
      ouz.options = ouz.options.map((option) => ({ ...option, selected: false }));
    }
  }
  return views;
}

function findTrastiRequiredProductCombination(type, productCode, views, combinations, brandId) {
  const selectedCodes = views.filter((view) => view.selected).map((view) => view.code);
  const selectedProductOptions = views.filter((view) => view.selected)
    .flatMap((view) => view.options)
    .filter((option) => option.selected && trastiEnumIs(option.type, 2, "Product"))
    .map((option) => option.code);
  const availableCodes = selectedCodes.concat(selectedProductOptions);
  return combinations
    .filter((combination) => trastiEnumIs(combination?.type, type, ["Feature", "Risk", "OC", "Product"][type]))
    .filter((combination) => combination?.productCode === productCode)
    .filter((combination) => combination?.brandId === brandId)
    .filter((combination) => trastiArray(combination?.requiredProductCodes)
      .every((requiredCode) => availableCodes.includes(requiredCode)))
    .sort((left, right) => (
      trastiArray(right?.requiredProductCodes).length - trastiArray(left?.requiredProductCodes).length
    ))[0];
}

function calculateTrastiProductPrices(views, sourceCombinations) {
  const combinations = trastiArray(sourceCombinations);
  return views.map((view) => {
    if (!view.selected) return { code: view.code, price: 0 };
    let price = 0;
    const add = (combination) => {
      const amount = Number(combination?.fullAmount);
      if (Number.isFinite(amount)) price += amount;
    };
    if (view.code === TRASTI_API_PRODUCT.oc) {
      add(findTrastiRequiredProductCombination(2, view.code, views, combinations, view.brandId));
      return { code: view.code, price };
    }
    if (view.code === "OUZ") {
      add(findTrastiRequiredProductCombination(3, view.code, views, combinations, view.brandId));
      return { code: view.code, price };
    }
    if (view.code === "GWC") {
      add(findTrastiRequiredProductCombination(3, view.code, views, combinations, view.brandId));
      add(findTrastiRequiredProductCombination(3, "OUZ", views, combinations, view.brandId));
      return { code: view.code, price };
    }

    const productCombinations = combinations.filter((combination) => (
      trastiEnumIs(combination?.type, 3, "Product") && combination?.brandId === view.brandId
    ));
    add(productCombinations.find((combination) => combination?.productCode === view.code));
    for (const productOption of view.options.filter((option) => (
      option.selected && trastiEnumIs(option.type, 2, "Product")
    ))) {
      add(productCombinations.find((combination) => combination?.productCode === productOption.code));
    }
    const selectedFeatures = view.options
      .filter((option) => trastiEnumIs(option.type, 0, "Feature"))
      .map((option) => `${option.code}=${option.selectedValue}`);
    add(combinations.find((combination) => (
      trastiEnumIs(combination?.type, 0, "Feature")
      && combination?.brandId === view.brandId
      && combination?.productCode === view.code
      && trastiArray(combination?.featureCodes).every((featureCode) => selectedFeatures.includes(featureCode))
    )));
    const selectedRisks = view.options.filter((option) => (
      option.selected && trastiEnumIs(option.type, 1, "Risk")
    ));
    for (const combination of combinations.filter((candidate) => (
      trastiEnumIs(candidate?.type, 1, "Risk")
      && candidate?.brandId === view.brandId
      && selectedRisks.some((risk) => (
        candidate?.productCode === risk.code && candidate?.coverageLimit === risk.selectedValue
      ))
    ))) {
      add(combination);
    }
    return { code: view.code, price };
  });
}

function formatTrastiApiMoney(value) {
  const amount = Number(value);
  if (!Number.isFinite(amount)) return "";
  return `${new Intl.NumberFormat("pl-PL", {
    minimumFractionDigits: Number.isInteger(amount) ? 0 : 2,
    maximumFractionDigits: 2,
    useGrouping: "always",
  }).format(amount).replace(/[  ]/g, " ")} zł`;
}

function formatTrastiCoverageValue(value) {
  if (typeof value === "number" && Number.isFinite(value)) return formatTrastiApiMoney(value);
  const text = String(value ?? "").replace(/[  ]/g, " ").replace(/\s+/g, " ").trim();
  if (!text) return "";
  const numeric = Number(text.replace(/\s/g, "").replace(",", ".").replace(/[^\d.-]/g, ""));
  if (Number.isFinite(numeric) && numeric > 0) return formatTrastiApiMoney(numeric);
  return text.slice(0, 60);
}

function getTrastiSelectedOption(view, code) {
  return view?.options?.find((option) => option?.code === code && option?.selected)?.selectedValue
    ?? view?.options?.find((option) => option?.code === code)?.selectedValue;
}

function getTrastiPackageLabel(name, index) {
  const normalized = normalizeText(name);
  if (normalized === "basic") return "Pakiet podstawowy";
  if (normalized === "recommended") return "Pakiet polecany";
  if (normalized === "best") return "Pakiet premium";
  return `Wariant Trasti ${index + 1}`;
}

 
export function buildTrastiPremiumsFromMultiOfferResponse(payload) {
  const multiOffer = unwrapTrastiMultiOfferResponse(payload);
  if (!multiOffer) return [];
  const grills = [...trastiArray(multiOffer.grillConfigurations)]
    .filter((grill) => ["basic", "recommended", "best"].includes(normalizeText(grill?.name)))
    .sort((left, right) => Number(left?.priority || 0) - Number(right?.priority || 0));
  return grills.map((grill, index) => {
    const views = buildTrastiProductViews(multiOffer, grill);
    const prices = calculateTrastiProductPrices(views, multiOffer.combinations);
    const selectedViews = views.filter((view) => view.selected);
    const priceFor = (code) => prices.find((price) => price.code === code)?.price || 0;
    const buckets = {
      oc: { price: 0, labels: [] },
      ac: { price: 0, labels: [] },
      nnw: { price: 0, labels: [] },
      assistance: { price: 0, labels: [] },
      szyby: { price: 0, labels: [] },
    };
    for (const view of selectedViews) {
      let bucket = "";
      if (view.code === TRASTI_API_PRODUCT.oc) bucket = "oc";
      else if (TRASTI_API_PRODUCT.ac.has(view.code)) bucket = "ac";
      else if (TRASTI_API_PRODUCT.nnw.has(view.code)) bucket = "nnw";
      else if (TRASTI_API_PRODUCT.assistance.has(view.code)) bucket = "assistance";
      else if (view.code === TRASTI_API_PRODUCT.glass) bucket = "szyby";
      if (!bucket) continue;
      buckets[bucket].price += priceFor(view.code);
      buckets[bucket].labels.push(view.code);
    }

    const acView = selectedViews.find((view) => TRASTI_API_PRODUCT.ac.has(view.code));
    const glassView = selectedViews.find((view) => view.code === TRASTI_API_PRODUCT.glass);
    const acRepairVariant = String(
      getTrastiSelectedOption(acView, TRASTI_API_PRODUCT.repairVariant) || "",
    ).trim();
    const vehicleValue = formatTrastiCoverageValue(
      getTrastiSelectedOption(acView, TRASTI_API_PRODUCT.insuranceSum)
        ?? multiOffer.carPrice
        ?? multiOffer.acCoverageLimit,
    );
    const glassSum = formatTrastiCoverageValue(
      getTrastiSelectedOption(glassView, TRASTI_API_PRODUCT.insuranceSum),
    );
    const assistanceNames = [...new Set(buckets.assistance.labels.map((code) => ({
      A_K: "awaria i wypadek",
      A_K2: "Auto – awaria lub wypadek",
      A_H: "Holowanie",
      A_H_PLUS: "Holowanie Plus",
    })[code]).filter(Boolean))];
    const assistanceLabel = assistanceNames.length > 0
      ? `Assistance (${assistanceNames.join(" + ")})`
      : "Assistance";
    const rows = [];
    if (buckets.oc.labels.length > 0) rows.push({ label: "OC", amount: formatTrastiApiMoney(buckets.oc.price) });
    if (buckets.ac.labels.length > 0) {
      rows.push({
        label: vehicleValue ? `AC SU: ${vehicleValue}` : "AC",
        amount: formatTrastiApiMoney(buckets.ac.price),
      });
    }
    if (buckets.nnw.labels.length > 0) rows.push({ label: "NNW", amount: formatTrastiApiMoney(buckets.nnw.price) });
    if (buckets.assistance.labels.length > 0) {
      rows.push({ label: assistanceLabel, amount: formatTrastiApiMoney(buckets.assistance.price) });
    }
    if (buckets.szyby.labels.length > 0) {
      rows.push({
        label: glassSum ? `Szyby SU: ${glassSum}` : "Szyby",
        amount: formatTrastiApiMoney(buckets.szyby.price),
      });
    }
    const total = prices.reduce((sum, product) => sum + product.price, 0);
    return {
      insurer: "Trasti",
      label: getTrastiPackageLabel(grill?.name, index),
      amount: formatTrastiApiMoney(total),
      details: "",
      components: {
        oc: formatTrastiApiMoney(buckets.oc.price),
        ac: formatTrastiApiMoney(buckets.ac.price),
        acVariant: acRepairVariant,
        vehicleValue,
        assistance: formatTrastiApiMoney(buckets.assistance.price),
        nnw: formatTrastiApiMoney(buckets.nnw.price),
        szyby: formatTrastiApiMoney(buckets.szyby.price),
      },
      componentsList: rows,
    };
  }).filter((premium) => premium.amount && premium.componentsList.length > 0);
}

export function enrichTrastiPremiumsWithMultiOffer(premiums, payload) {
  const apiPremiums = buildTrastiPremiumsFromMultiOfferResponse(payload);
  const sourcePremiums = Array.isArray(premiums) ? premiums : [];
  if (apiPremiums.length === 0 || sourcePremiums.length === 0) return sourcePremiums;
  const available = [...apiPremiums];
  return sourcePremiums.map((premium) => {
    const normalizedLabel = normalizeText(premium?.label);
    const amount = trastiAmountToNumber(premium?.amount);
    const sameAmount = (candidate) => {
      const candidateAmount = trastiAmountToNumber(candidate?.amount);
      return amount !== null && candidateAmount !== null && Math.abs(candidateAmount - amount) < 0.01;
    };
    let matchIndex = available.findIndex((candidate) => (
      normalizeText(candidate.label) === normalizedLabel && sameAmount(candidate)
    ));
    if (matchIndex < 0) matchIndex = available.findIndex(sameAmount);
    let exactAmount = matchIndex >= 0;
    if (matchIndex < 0) {
      matchIndex = available.findIndex((candidate) => normalizeText(candidate.label) === normalizedLabel);
      exactAmount = false;
    }
    const match = matchIndex >= 0 ? available.splice(matchIndex, 1)[0] : null;
    if (!match) return premium;
    if (!exactAmount) {
      const components = {
        ...match.components,
        oc: match.components?.oc ? "w cenie" : "",
        ac: match.components?.ac ? "w cenie" : "",
        assistance: match.components?.assistance ? "w cenie" : "",
        nnw: match.components?.nnw ? "w cenie" : "",
        szyby: match.components?.szyby ? "w cenie" : "",
      };
      return {
        ...premium,
        components,
        componentsList: match.componentsList.map((component) => ({
          ...component,
          amount: "w cenie",
        })),
      };
    }
    return {
      ...premium,
      components: match.components,
      componentsList: match.componentsList,
    };
  });
}

export async function extractTrastiPremiums(page) {
  const collected = [];
  for (const frame of page.frames()) {
    try {
      const framePremiums = await frame.evaluate(() => {
        const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
        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 sumLikeContext = /suma|sumie|sum ubezpieczenia|wartość pojazdu|wartosc pojazdu|limit|franszyza|udział własny|udzial wlasny|zniżk|znizk/i;
        const premiumLikeContext = /składk|skladk|do zapłaty|do zaplaty|cena|koszt|łącznie|lacznie|rocznie|za rok|\/\s*rok/i;
        const componentPattern = /(^|[^a-ząćęłńóśźż])(oc|ac|autocasco|nnw|assistance|minicasco|szyby)([^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*='amount'], [class*='sklad'], strong, b, td, span, div, p, h1, h2, h3, h4")]
          .filter(visible)
          .map((element, index) => ({ element, index, text: compact(element.innerText || element.textContent) }))
          .filter(({ text }) => text.length > 0 && text.length <= 140 && amountPattern.test(text))
          .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 match = text.match(amountPattern);
          if (!match) continue;
          const numericAmount = Number(match[1].replace(/[ . ]/g, "")) + Number(match[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;
          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;
          }
          const context = compact(container.innerText || container.textContent).slice(0, 500);
          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 = context.replace(amountPattern, "").replace(/^[\s–—|:•-]+|[\s–—|:•-]+$/g, "").slice(0, 100);
          const componentsList = [];
          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 > 120 || 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;
            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;
            seenComponentRows.add(rowText);
            componentsList.push({
              label: componentLabel,
              amount: rowAmount[0].replace(/PLN/i, "zł").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim(),
            });
          }
          entries.push({
            index,
            fontSize: digitFontSize(element),
            insurer: "Trasti",
            label: label || `Wariant ${entries.length + 1}`,
            amount: match[0].replace(/PLN/i, "zł").replace(/ /g, " ").replace(/\s+/g, " ").trim(),
            details: context || "Trasti - kalkulacja komunikacyjna",
            rawContext: context,
            componentsList,
          });
        }
        if (entries.length === 0) return [];
        return entries
          .sort((left, right) => left.index - right.index)
          .map(({ index, fontSize, ...premium }) => ({
            ...premium,
            components: { oc: "", ac: "", vehicleValue: "", assistance: "", nnw: "" },
          }));
      });
      for (const premium of framePremiums) {
        const normalized = normalizeTrastiPremiumCandidate(premium, collected.length);
        if (normalized) collected.push(normalized);
      }
    } catch {
    }
  }
  const unique = [];
  const seen = new Set();
  for (const premium of collected) {
    const key = `${normalizeText(premium.label)}|${premium.amount}`;
    if (seen.has(key)) continue;
    seen.add(key);
    const { rawContext, ...publicPremium } = premium;
    unique.push(publicPremium);
  }
  
  
  
  return unique.slice(0, 100);
}

async function collectTrastiPremiumDebug(page) {
  const frames = [];
  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 candidates = await frame.evaluate(() => {
        const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
        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 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;
        };
        return [...document.querySelectorAll("[class*='price'], [class*='premium'], [class*='amount'], [class*='sklad'], strong, b, td, span, div, p, h1, h2, h3, h4")]
          .filter(visible)
          .map((element) => ({ element, text: compact(element.innerText || element.textContent) }))
          .filter(({ text }) => text.length > 0 && text.length <= 140 && amountPattern.test(text))
          .slice(0, 40)
          .map(({ element, text }) => {
            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;
            }
            return {
              fontSizePx: Math.round(digitFontSize(element)),
              hasAmount: amountPattern.test(rowContext),
              containerClass: compact(String(element.parentElement?.className || "")).slice(0, 120),
            };
          });
      });
      frames.push({ frameHint, candidates });
    } catch {
    }
  }
  return {
    capturedAt: new Date().toISOString(),
    reason: "trasti-skladki-nieodczytane",
    frames,
  };
}

function emptyTrastiPdfRetry() {
  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 failedTrastiPdfRetry(remaining, circuitCode = "unknown", deferred = 0) {
  return {
    ...emptyTrastiPdfRetry(),
    deferred: Math.max(0, deferred),
    remaining: Math.max(0, remaining),
    batchComplete: false,
    circuitOpen: true,
    circuitCode,
  };
}

export function buildTrastiImportCompletion({
  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: failedTrastiPdfRetry(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 runTrastiPortfolioPdfPhase({
  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 = prepareTrastiPortfolioPdfEntries({
    policies,
    skipPolicyNumbers,
    knownPolicyNumbers,
  });
  const pdfDirectoryReady = typeof pdfDirectory === "string"
    && pdfDirectory.trim().length > 0
    && !pdfDirectory.includes("\0");
  if (!pdfDirectoryReady) {
    const pdfRetry = failedTrastiPdfRetry(
      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 Trasti maja juz podpiete pliki.",
    });
    return {
      attempted: 0,
      saved: 0,
      remaining: 0,
      pdfRetry: emptyTrastiPdfRetry(),
      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: failedTrastiPdfRetry(
        eligible.length,
        "store_failed",
        Math.max(0, eligible.length - planned),
      ),
      privateResult: {
        manualRetryIdentities: [],
      },
    };
  } else {
    try {
      batch = await runTrastiPortfolioPdfBatch({
        page: page && typeof page.evaluate === "function" ? page : null,
        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: failedTrastiPdfRetry(
          eligible.length,
          "unknown",
          Math.max(0, eligible.length - planned),
        ),
        privateResult: {
          manualRetryIdentities: [],
        },
      };
    }
  }
  const pdfRetry = batch.publicResult;
  
  
  console.error(
    `Trasti 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 finalizeTrastiPortfolioApiImport({
  policies,
  mappingReport = null,
  sinceDate = "",
  sourceAlreadyFilteredToCheckpoint = false,
  downloadPdfs = false,
  page,
  retryStore,
  pdfDirectory = "",
  pdfPolicyLimit = 50,
  skipPdfPolicyNumbers = [],
  knownPdfPolicyNumbers = [],
  onProgress = () => {},
  onCompletion,
} = {}) {
  if (!Array.isArray(policies)) {
    throw new TypeError("policies musi byc tablica.");
  }
  const pdfExpectedCount = downloadPdfs
    ? prepareTrastiPortfolioPdfEntries({
      policies,
      skipPolicyNumbers: skipPdfPolicyNumbers,
      knownPolicyNumbers: knownPdfPolicyNumbers,
    }).length
    : 0;
  const pdfOutcome = downloadPdfs
    ? await runTrastiPortfolioPdfPhase({
      page,
      retryStore,
      pdfDirectory,
      policies,
      onProgress,
      limit: pdfPolicyLimit,
      skipPolicyNumbers: skipPdfPolicyNumbers,
      knownPolicyNumbers: knownPdfPolicyNumbers,
    })
    : null;
  const { emittedPolicies, emissionReport } = createPortfolioEmissionResult(policies, {
    sinceDate,
    sourceAlreadyFilteredToCheckpoint,
    getFreshnessDates: (policy) => policy?.issueDate,
    isBacklogPolicy: (policy) => (
      String(policy?.insurer || "").trim().toUpperCase() === "TRASTI"
      && policy?.pdfAvailable === true
    ),
  });
  const completion = buildTrastiImportCompletion({
    freshCount: emissionReport.freshCount,
    downloadPdfs,
    pdfOutcome,
    pdfExpectedCount,
  });
  const completionMappingReport = assertPortfolioMappingComplete(mappingReport);
  onCompletion?.({
    ...completion,
    mappingReport: completionMappingReport,
    emissionReport,
  });
  return emittedPolicies;
}

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

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

  
  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);
    let latestMultiOfferPayload = null;
    let latestMultiOfferSequence = 0;
    let responseSequence = 0;
    const multiOfferResponseTasks = new Set();
    const captureMultiOfferResponse = (response) => {
      const url = String(response?.url?.() || "");
      if (!/\/api\/[^?#]*multi[-_]?offer(?:[/?#]|$)/i.test(url)) return;
      const sequence = ++responseSequence;
      const task = response.json()
        .then((payload) => {
          if (!unwrapTrastiMultiOfferResponse(payload) || sequence < latestMultiOfferSequence) return;
          latestMultiOfferPayload = payload;
          latestMultiOfferSequence = sequence;
        })
        .catch(() => {})
        .finally(() => multiOfferResponseTasks.delete(task));
      multiOfferResponseTasks.add(task);
    };
    const enrichWithCapturedMultiOffer = async (premiums) => {
      if (multiOfferResponseTasks.size > 0) {
        await Promise.allSettled([...multiOfferResponseTasks]);
      }
      return enrichTrastiPremiumsWithMultiOffer(premiums, latestMultiOfferPayload);
    };
    page.on("response", captureMultiOfferResponse);
    if (!headless) {
      lastVisiblePage = page;
      await bringPageWindowToFront(page);
      await attachClientInfoOverlay(page, vehicleInfo, { onAction: onOverlayAction }).catch(() => {});
    }
    try {
    const targetFields = [
      {
        key: "pesel",
        patterns: ["pesel", "identity", "identyfikator klienta", "numer klienta", "regon / pesel"],
        value: pesel,
      },
      {
        key: "registration",
        patterns: [
          "numer rejestracyjny",
          "nr rejestracyjny",
          "nr rej",
          "rejestracj",
          "registrationnumber",
          "registration-number",
          "registration",
        ],
        value: registration,
      },
    ];

    const vinFromBase = (() => {
      const normalized = String(vin || "").replace(/[^A-Z0-9]/gi, "").toUpperCase();
      return normalized.length >= 5 ? normalized : "";
    })();
    let vinPromptUserRounds = 0;
    let vinAutoFillUsed = false;
    let vinPromptBusy = false;
    let lastRejectedVinValue = "";
    const handleVinPrompt = async () => {
      if (vinPromptBusy) return;
      vinPromptBusy = true;
      try {
        for (let iteration = 0; iteration < 6; iteration += 1) {
          const prompt = await findVinVerificationPrompt(page);
          if (!prompt) return;
          let candidate = "";
          let fromBase = false;
          if (prompt.value && !lastRejectedVinValue) {
            candidate = prompt.value;
          } else if (vinFromBase && !vinAutoFillUsed) {
            vinAutoFillUsed = true;
            candidate = vinFromBase;
            fromBase = true;
          } else if (typeof onInputRequested === "function" && vinPromptUserRounds < 3) {
            vinPromptUserRounds += 1;
            onProgress({
              status: "running",
              title: "Trasti prosi o numer VIN…",
              detail: "Trasti ma już ofertę dla tego numeru rejestracyjnego i wymaga VIN lub numeru oferty.",
            });
            
            
            
            const answers = await onInputRequested({
              title: lastRejectedVinValue
                ? `Zweryfikuj ofertę - numer „${lastRejectedVinValue}” został odrzucony`
                : "Zweryfikuj ofertę",
              round: vinPromptUserRounds,
              fields: [{
                id: "trasti-vin",
                kind: "text",
                label: "VIN lub numer oferty (Trasti ma już ofertę dla tego nr rejestracyjnego)",
                currentValue: "",
                required: true,
                invalid: Boolean(lastRejectedVinValue) || prompt.invalid,
              }],
            });
            const rawAnswer = answers?.["trasti-vin"];
            candidate = String((rawAnswer && typeof rawAnswer === "object" ? rawAnswer.value : rawAnswer) ?? "").trim();
            if (!candidate) return;
          } else {
            return;
          }
          onProgress({
            status: "running",
            title: "Weryfikowanie oferty…",
            detail: "",
          });
          if (await submitVinVerification(page, prompt.frame, candidate)) {
            lastRejectedVinValue = "";
            onProgress({
              status: "running",
              title: "Oferta zweryfikowana",
              detail: "",
            });
            return;
          }
          lastRejectedVinValue = candidate;
          if (fromBase) {
            onProgress({
              status: "running",
              title: "Numer VIN nie został przyjęty",
              detail: "Uzupełnij go ręcznie w kolejnym kroku.",
            });
          }
        }
      } finally {
        vinPromptBusy = false;
      }
    };
    const isVinInputRequest = (request) => (Array.isArray(request?.fields) ? request.fields : [])
      .some((field) => {
        const label = normalizeText(field?.label);
        return /(^|[^a-z])vin([^a-z]|$)/.test(label) || label.includes("numer oferty");
      });
    
    
    
    
    const isLoginInputRequest = (request) => (Array.isArray(request?.fields) ? request.fields : [])
      .some((field) => String(field?.type || "").toLowerCase() === "password");
    const handleLoginInterruption = async () => {
      await loginIfNeeded(page, { username, password }, onProgress).catch(() => {});
      await waitForPortal(page, onProgress, { allowUserInteraction: !headless }).catch(() => {});
      const error = new Error("Sesja Trasti wygasła w trakcie kalkulacji - zalogowano ponownie.");
      error.code = "TRASTI_SESSION_RELOGIN";
      throw error;
    };
    const onInputRequestedWithVin = typeof onInputRequested === "function"
      ? async (request) => {
        if (isLoginInputRequest(request)) return handleLoginInterruption();
        if (isVinInputRequest(request)) {
          await handleVinPrompt();
          if (!(await findVinVerificationPrompt(page))) return {};
          return null;
        }
        return onInputRequested(request);
      }
      : onInputRequested;

    onProgress({
      status: "running",
      title: headless ? "Kalkulacja Trasti działa w tle…" : "Otwieranie kalkulatora Trasti…",
      detail: "",
    });
    await page.goto(TRASTI_URL, { waitUntil: "domcontentloaded", timeout: 60000 });
    await loginIfNeeded(page, { username, password }, onProgress);
    await waitForPortal(page, onProgress, { allowUserInteraction: !headless });
    await dismissCookieBannerIfPresent(page);
    
    
    
    
    
    
    
    if (!(await waitForTrastiApiToken(page, { timeoutMs: 20000 }))) {
      onProgress({ status: "running", title: "Sesja Trasti wygasła - loguję ponownie…", detail: "" });
      await page.evaluate(() => {
        try {
          localStorage.clear();
          sessionStorage.clear();
        } catch {
        }
      }).catch(() => {});
      await page.goto(TRASTI_URL, { waitUntil: "domcontentloaded", timeout: 60000 });
      await loginIfNeeded(page, { username, password }, onProgress);
      await waitForPortal(page, onProgress, { allowUserInteraction: !headless });
      await dismissCookieBannerIfPresent(page);
      if (!(await waitForTrastiApiToken(page))) {
        throw manualInteractionRequired(
          "Trasti nie wydało tokenu sesji po ponownym logowaniu.",
        );
      }
    }

    onProgress({
      status: "running",
      title: "Zalogowano do Trasti",
      detail: "",
    });
    const knownAnswerResolver = composeKnownAnswerResolvers(
      extraKnownAnswerResolver,
      buildKnownVehicleAnswerResolver({ vin, vehicleInfo, pesel }),
    );
    let formNavigation = null;
    try {
      formNavigation = await learnOrReplayFormPath({
        page,
        savedPath: navigationPath,
        savedFields: formFields,
        fields: targetFields,
        insurerName: "Trasti",
        ignoreStep: isCookieBannerStep,
        manualInstruction: "Logowanie jest zakończone. Kliknij „Rozpocznij nagrywanie”, przejdź do kalkulatora komunikacyjnego i zakończ nagrywanie na ekranie z polami PESEL i numeru rejestracyjnego.",
        onPathLearned,
        onDiagnosticsCaptured,
        onProgress,
        requestInput: onInputRequestedWithVin,
        knownAnswerResolver,
        allowManual: !headless && !manualStartOnly,
        patientReplay: !manualStartOnly,
      });
    } catch (error) {
      if (!manualStartOnly) throw error;
    }
    if (manualStartOnly) {
      onProgress({
        status: "running",
        title: "Karta Trasti jest gotowa do ręcznego liczenia",
        detail: "Wpisz dane w formularzu samodzielnie.",
      });
      return [];
    }

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

    let premiums = await enrichWithCapturedMultiOffer(await extractTrastiPremiums(page));
    const resultsReady = async () => {
      premiums = await enrichWithCapturedMultiOffer(await extractTrastiPremiums(page));
      return premiums.length > 0;
    };
    await learnOrReplayActionPath({
      page,
      savedPath: resultNavigationPath,
      isComplete: resultsReady,
      requestInput: onInputRequestedWithVin,
      knownAnswerResolver,
      beforeStep: handleVinPrompt,
      afterStep: handleVinPrompt,
      stageTitle: "od formularza do składek Trasti",
      manualInstruction: "PESEL i numer rejestracyjny są już wpisane. Klikaj dalej jak zwykle, aż pojawią się składki Trasti. Aplikacja zapamięta te czynności.",
      retryInstruction: "Zapamiętana droga do wyników Trasti przestała pasować. Klikaj ręcznie aż pojawią się składki, aby zapisać nową sekwencję.",
      onPathLearned: onResultPathLearned,
      onProgress,
      allowManual: !headless,
    });
    premiums = await enrichWithCapturedMultiOffer(await extractTrastiPremiums(page));
    if (premiums.length === 0) {
      try {
        await onDiagnosticsCaptured?.(await collectTrastiPremiumDebug(page));
      } catch {
      }
      throw new Error("Nie udało się odczytać składek z kalkulatora Trasti w ciągu 3 minut.");
    }
    return premiums;
    } finally {
      page.off("response", captureMultiOfferResponse);
      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 Trasti 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 });
    try {
      return await calculateQuoteAttempt(options, { headless: true });
    } catch (error) {
      
      
      
      
      if (error?.code !== "TRASTI_SESSION_RELOGIN") throw error;
      options?.onProgress?.({
        status: "running",
        title: "Sesja Trasti odnowiona - ponawiam kalkulację…",
        detail: "",
      });
      return calculateQuoteAttempt(options, { headless: true });
    }
  }

  async function importPortfolioAttempt({
    username,
    password,
    downloadPdfs = false,
    pdfDirectory = "",
    pdfPolicyLimit = 50,
    skipPdfPolicyNumbers = [],
    knownPdfPolicyNumbers = [],
    sinceDate = "",
    onCompletion,
    onProgress,
  }, { headless = false } = {}) {
    const page = await openPage(headless);
    page.setDefaultTimeout(12000);
    page.setDefaultNavigationTimeout(60000);
    if (!headless) {
      lastVisiblePage = page;
      await bringPageWindowToFront(page);
    }
    try {
      onProgress({
        status: "running",
        title: headless ? "Import polis Trasti działa w tle…" : "Otwieranie portalu Trasti…",
        detail: "",
      });
      await page.goto(TRASTI_URL, { waitUntil: "domcontentloaded", timeout: 60000 });
      await loginIfNeeded(page, { username, password }, onProgress);
      await waitForPortal(page, onProgress, { allowUserInteraction: !headless });
      await dismissCookieBannerIfPresent(page);
      
      
      
      
      
      if (!(await waitForTrastiApiToken(page, { timeoutMs: 20000 }))) {
        onProgress({ status: "running", title: "Sesja Trasti wygasła - loguję ponownie…", detail: "" });
        await page.evaluate(() => {
          try {
            localStorage.clear();
            sessionStorage.clear();
          } catch {
          }
        }).catch(() => {});
        await page.goto(TRASTI_URL, { waitUntil: "domcontentloaded", timeout: 60000 });
        await loginIfNeeded(page, { username, password }, onProgress);
        await waitForPortal(page, onProgress, { allowUserInteraction: !headless });
        await dismissCookieBannerIfPresent(page);
        if (!(await waitForTrastiApiToken(page))) {
          
          throw manualInteractionRequired(
            "Trasti nie wydało tokenu sesji po ponownym logowaniu w tle.",
          );
        }
      }

      onProgress({
        status: "running",
        title: "Zalogowano do Trasti",
        detail: "Pobieranie polis…",
      });
      
      
      const pdfBacklogCount = countUnskippedPortfolioPdfSelectors({
        known: knownPdfPolicyNumbers,
        skipped: skipPdfPolicyNumbers,
        insurer: "trasti",
      });
      const fetchFullForPdfBacklog = downloadPdfs === true && pdfBacklogCount > 0;
      const items = await fetchTrastiPolicyItems(page, {
        
        
        issueDateFrom: fetchFullForPdfBacklog ? "" : sinceDate,
        onProgress: ({ collected, page: pageNumber }) => {
          onProgress({
            status: "running",
            title: "Pobieranie polis…",
            detail: `Pobrano ${collected} polis.`,
          });
        },
      });
      const backlogMode = fetchFullForPdfBacklog && Boolean(sinceDate);
      const sourceAlreadyFilteredToCheckpoint = Boolean(sinceDate) && !backlogMode;
      if (items.length === 0) {
        if (sinceDate) {
          const mappingReport = assertPortfolioMappingComplete(
            createPortfolioMappingCollector(0).createReport(),
          );
          const { emittedPolicies, emissionReport } = createPortfolioEmissionResult([], {
            sinceDate,
            sourceAlreadyFilteredToCheckpoint,
          });
          onCompletion?.({
            freshCount: emissionReport.freshCount,
            pdfSaved: null,
            pdfRemaining: null,
            mappingReport,
            emissionReport,
          });
          return emittedPolicies;
        }
        throw new Error("Nie znaleziono żadnej polisy. Sprawdź, czy konto ma dostęp do listy polis.");
      }
      
      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 detailTargets = items.filter((item) => {
        const status = String(item?.status || "").toUpperCase();
        if (status === "DRAFT") {
          mappingCollector.recordFiltered("DRAFT_OFFER");
          return false;
        }
        if (/^(?:CANCELED|CANCELLED|ANULOWANA|STORNO)$/.test(status)) {
          mappingCollector.recordFiltered("CANCELLED_POLICY");
          return false;
        }
        return true;
      });
      const { detailsById } = await fetchTrastiPolicyDetailsBatch(page, detailTargets, {
        allowPartialForReview: true,
        onProgress: ({
          detailsProcessed,
          detailsRequested,
          detailFailures,
          detailFetchFailures,
          detailNoValidUntil,
        }) => {
          if (detailsProcessed % 10 === 0 || detailsProcessed === detailsRequested) {
            const failureParts = [];
            if (detailFetchFailures > 0) {
              failureParts.push(`nieudane pobranie: ${detailFetchFailures}`);
            }
            if (detailNoValidUntil > 0) {
              failureParts.push(`brak daty końca: ${detailNoValidUntil}`);
            }
            const failureSuffix = detailFailures > 0
              ? ` Problemy szczegółów: ${failureParts.join(", ")}.`
              : "";
            onProgress({
              status: "running",
              title: "Pobieranie szczegółów polis…",
              detail: `Sprawdzono ${detailsProcessed} z ${detailsRequested} polis.${failureSuffix}`,
            });
          }
        },
      });

      const policies = mapTrastiPolicyItems(detailTargets, detailsById, {
        minEndDate,
        requireConfirmedValidUntil: true,
        mappingCollector,
      });
      const mappingReport = assertPortfolioMappingComplete(mappingCollector.createReport());
      const previewEmission = createPortfolioEmissionResult(policies, {
        sinceDate,
        sourceAlreadyFilteredToCheckpoint,
        getFreshnessDates: (policy) => policy?.issueDate,
      });
      if (policies.length === 0) {
        if (sinceDate) {
          onCompletion?.({
            freshCount: previewEmission.emissionReport.freshCount,
            pdfSaved: null,
            pdfRemaining: null,
            mappingReport,
            emissionReport: previewEmission.emissionReport,
          });
          return previewEmission.emittedPolicies;
        }
        throw new Error("Nie znaleziono żadnej polisy do pobrania.");
      }
      onProgress({
        status: "running",
        title: "Porządkowanie pobranych polis…",
        detail: sinceDate
          ? `Nowych polis: ${previewEmission.emissionReport.freshCount}.`
          : `Pobrano ${policies.length} polis.`,
      });
      
      
      return await finalizeTrastiPortfolioApiImport({
        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 };
}
