const CLICKABLE_SELECTOR = [
  "a",
  "button",
  "[role='button']",
  "[role='menuitem']",
  "[role='option']",
  "[data-testid]",
  "[data-test]",
  "[data-qa]",
  "[data-cy]",
  "[onclick]",
  "[ng-click]",
  "[data-ng-click]",
  "summary",
  "label",
  "input[type='button']",
  "input[type='submit']",
  "input[type='radio']",
  "input[type='checkbox']",
  "[tabindex]:not([tabindex='-1'])",
].join(",");
const FORM_INPUT_SELECTOR = "input:not([type='hidden']):not([type='checkbox']):not([type='radio']):not([type='button']):not([type='submit']), textarea";
const TECHNICAL_MAP_SELECTOR = [
  "input:not([type='hidden'])",
  "textarea",
  "select",
  "button",
  "a",
  "[role]",
  "[data-testid]",
  "[data-test]",
  "[data-qa]",
  "[data-cy]",
  "[contenteditable='true']",
].join(",");
let navigationCaptureSequence = 0;

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




const BUSY_INDICATOR_SELECTOR = [
  "[class*='spinner']",
  "[class*='Spinner']",
  "[class*='loader']",
  "[class*='Loader']",
  "[class*='loading']",
  "[class*='Loading']",
  ".MuiCircularProgress-root",
  "[class*='progress-circular']",
  ".blockUI",
  "[aria-busy='true']",
].join(",");

export async function waitForUiQuiet(page, { timeout = 6000, quietChecks = 2, interval = 250 } = {}) {
  const deadline = Date.now() + Math.max(500, timeout);
  let quietStreak = 0;
  while (Date.now() < deadline) {
    let busy = false;
    for (const frame of page.frames().slice(0, 20)) {
      try {
        busy = await frame.evaluate((busySelector) => {
          for (const element of document.querySelectorAll(busySelector)) {
            if (!(element instanceof HTMLElement)) continue;
            const rects = element.getClientRects();
            if (rects.length === 0) continue;
            const rect = rects[0];
            if (rect.width < 4 || rect.height < 4) continue;
            const style = getComputedStyle(element);
            if (style.visibility === "hidden" || Number(style.opacity || "1") === 0) continue;
            
            
            
            if ((element.innerText || "").trim().length > 80) continue;
            return true;
          }
          return false;
        }, BUSY_INDICATOR_SELECTOR);
      } catch {
      }
      if (busy) break;
    }
    if (!busy) {
      quietStreak += 1;
      if (quietStreak >= quietChecks) return true;
    } else {
      quietStreak = 0;
    }
    await delay(interval);
  }
  return false;
}

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 updateTargetFormDetection(previousCount, targetFormReady) {
  const count = targetFormReady
    ? Math.min(2, Math.max(0, Math.trunc(Number(previousCount) || 0)) + 1)
    : 0;
  return { count, stable: count >= 2 };
}

function normalizeNavigationStep(step) {
  const normalized = {
    tag: String(step?.tag || "").trim().toLowerCase().slice(0, 20),
    type: String(step?.type || "").trim().toLowerCase().slice(0, 40),
    id: String(step?.id || "").trim().slice(0, 100),
    name: String(step?.name || "").trim().slice(0, 100),
    className: String(step?.className || "").replace(/\s+/g, " ").trim().slice(0, 160),
    dataTestId: String(step?.dataTestId || "").trim().slice(0, 120),
    role: String(step?.role || "").trim().toLowerCase().slice(0, 40),
    text: String(step?.text || "").replace(/\s+/g, " ").trim().slice(0, 160),
    ariaLabel: String(step?.ariaLabel || "").replace(/\s+/g, " ").trim().slice(0, 160),
    title: String(step?.title || "").replace(/\s+/g, " ").trim().slice(0, 160),
    hrefPath: String(step?.hrefPath || "").trim().slice(0, 220),
    frameHint: String(step?.frameHint || "").trim().slice(0, 220),
    selector: String(step?.selector || "").trim().slice(0, 500),
  };
  if (!normalized.text && !normalized.ariaLabel && !normalized.title
    && !normalized.id && !normalized.name && !normalized.dataTestId && !normalized.hrefPath) return null;
  return normalized;
}

function normalizeLearnedFieldDefinitions(fields) {
  return (Array.isArray(fields) ? fields : []).map((field) => ({
    key: String(field?.key || "").trim().slice(0, 60),
    selector: String(field?.selector || "").trim().slice(0, 500),
    frameHint: String(field?.frameHint || "").trim().slice(0, 220),
    tag: String(field?.tag || "").trim().toLowerCase().slice(0, 20),
    type: String(field?.type || "").trim().toLowerCase().slice(0, 40),
    id: String(field?.id || "").trim().slice(0, 160),
    name: String(field?.name || "").trim().slice(0, 160),
    dataTestId: String(field?.dataTestId || "").trim().slice(0, 160),
    ariaLabel: String(field?.ariaLabel || "").replace(/\s+/g, " ").trim().slice(0, 200),
    placeholder: String(field?.placeholder || "").replace(/\s+/g, " ").trim().slice(0, 200),
    labelText: String(field?.labelText || "").replace(/\s+/g, " ").trim().slice(0, 240),
  })).filter((field) => field.key && field.selector);
}

function normalizeNavigationPath(path) {
  const normalizedPath = [];
  for (const rawStep of Array.isArray(path) ? path.slice(0, 30) : []) {
    const step = normalizeNavigationStep(rawStep);
    if (!step) continue;
    normalizedPath.push(step);
  }
  return normalizedPath;
}

function normalizeFieldDefinitions(fields) {
  return (Array.isArray(fields) ? fields : []).map((field) => ({
    key: String(field?.key || "").trim().slice(0, 60),
    patterns: (Array.isArray(field?.patterns) ? field.patterns : [])
      .map(normalizeText)
      .filter(Boolean)
      .slice(0, 20),
    value: String(field?.value ?? ""),
    pressEnter: field?.pressEnter === true,
    commitWithKeyboard: field?.commitWithKeyboard === true,
  })).filter((field) => field.key && field.patterns.length > 0);
}

async function fillFieldWithRealKeyboard(page, field, resolvedField) {
  if (!resolvedField?.selector) return false;
  const isoDate = String(field?.value || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
  const resolvedDateDescriptor = normalizeText([
    resolvedField.type,
    resolvedField.name,
    resolvedField.id,
    resolvedField.ariaLabel,
    resolvedField.placeholder,
    resolvedField.labelText,
    field?.key,
  ].filter(Boolean).join(" "));
  const isDateField = Boolean(isoDate) && (
    ["date", "datetime-local", "month"].includes(String(resolvedField.type || "").toLowerCase())
    || /date|birth|urodz|datetimes?/.test(resolvedDateDescriptor)
  );
  const polishDate = isoDate ? `${isoDate[3]}.${isoDate[2]}.${isoDate[1]}` : "";
  const keyboardCandidates = isDateField
    ? [...new Set([
      String(resolvedField.type || "").toLowerCase() === "date" ? field.value : polishDate,
      polishDate?.replace(/\./g, ""),
      field.value,
      String(field.value || "").replace(/\D/g, ""),
    ].filter(Boolean))]
    : [...new Set([field.value, String(field.value || "").replace(/\D/g, "")].filter(Boolean))];
  const comparableDate = (value) => {
    const text = String(value || "").trim();
    const iso = text.match(/^(\d{4})[-./](\d{1,2})[-./](\d{1,2})$/);
    if (iso) return `${iso[1]}${iso[2].padStart(2, "0")}${iso[3].padStart(2, "0")}`;
    const polish = text.match(/^(\d{1,2})[-./](\d{1,2})[-./](\d{4})$/);
    if (polish) return `${polish[3]}${polish[2].padStart(2, "0")}${polish[1].padStart(2, "0")}`;
    return "";
  };
  const expectedDate = comparableDate(field.value);
  const valueAccepted = (actual, typed) => {
    if (actual === typed || actual === field.value) return true;
    if (expectedDate && comparableDate(actual) === expectedDate) return true;
    return false;
  };
  const frames = [...page.frames()].sort((left, right) => {
    const leftHint = getFrameHint(left);
    const rightHint = getFrameHint(right);
    return Number(rightHint === resolvedField.frameHint) - Number(leftHint === resolvedField.frameHint);
  });
  for (const frame of frames) {
    let handle = null;
    try {
      handle = await frame.$(resolvedField.selector);
      if (!handle) continue;
      const usable = await handle.evaluate((element) => element instanceof HTMLElement
        && element.getClientRects().length > 0
        && getComputedStyle(element).visibility !== "hidden"
        && !element.disabled
        && !element.readOnly);
      if (!usable) continue;
      const readValue = () => handle.evaluate((element) => String(element.value ?? ""));
      const typeAttempt = async (text) => {
        await handle.click({ clickCount: 3 });
        await handle.evaluate((element) => {
          if (element instanceof HTMLInputElement) element.setSelectionRange?.(0, element.value.length);
        }).catch(() => {});
        await handle.press("Backspace");
        await handle.type(text, { delay: 30 });
        return readValue();
      };
      for (const candidate of keyboardCandidates) {
        const typedValue = await typeAttempt(candidate);
        if (!valueAccepted(typedValue, candidate)) continue;
        await handle.press("Tab");
        await delay(300);
        const settledValue = await readValue();
        if (valueAccepted(settledValue, candidate)) return true;
      }
      return false;
    } catch {
    } finally {
      await handle?.dispose().catch(() => {});
    }
  }
  return false;
}

function getFrameHint(frame) {
  try {
    const frameUrl = new URL(frame.url());
    return `${frameUrl.hostname}${frameUrl.pathname}`.slice(0, 220);
  } catch {
    return "";
  }
}

async function captureDescribedField(frame, field, learnedField) {
  try {
    return await frame.evaluate(({ definition, exactField, inputSelector, frameHint }) => {
      const normalize = (value) => String(value || "")
        .normalize("NFD")
        .replace(/[\u0300-\u036f]/g, "")
        .replace(/\s+/g, " ")
        .trim()
        .toLowerCase();
      const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
      const escapeAttribute = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
      const stableSelector = (element) => {
        if (element.id) return `#${CSS.escape(element.id)}`;
        const tag = element.tagName.toLowerCase();
        for (const attribute of ["data-testid", "data-test", "data-qa", "data-cy"]) {
          const value = element.getAttribute(attribute);
          if (value) return `${tag}[${attribute}="${escapeAttribute(value)}"]`;
        }
        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) {
          const currentTag = current.tagName.toLowerCase();
          let part = currentTag;
          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 inputs = [...document.querySelectorAll(inputSelector)]
        .filter((input) => input.getClientRects().length && !input.disabled && !input.readOnly);
      
      
      
      
      const isTopmost = (input) => {
        const rect = input.getBoundingClientRect();
        if (!rect.width || !rect.height) return true;
        const cx = rect.left + rect.width / 2;
        const cy = rect.top + rect.height / 2;
        if (cx < 0 || cy < 0 || cx >= window.innerWidth || cy >= window.innerHeight) return true;
        const hit = document.elementFromPoint(cx, cy);
        if (!hit) return true;
        return hit === input || input.contains(hit) || hit.contains(input)
          || Boolean(hit.closest("label")?.contains(input));
      };
      let target = null;
      if (exactField?.selector) {
        try {
          const exact = document.querySelector(exactField.selector);
          if (inputs.includes(exact) && isTopmost(exact)) target = exact;
        } catch {
        }
      }
      const describeInput = (input, withContainer) => {
        const labels = input.labels ? [...input.labels].map((label) => label.innerText).join(" ") : "";
        const parts = [
          input.id,
          input.name,
          input.placeholder,
          input.getAttribute("aria-label"),
          input.getAttribute("data-dtname"),
          input.getAttribute("ng-model"),
          input.getAttribute("ng-change"),
          input.closest("[approval-field]")?.getAttribute("approval-field"),
          labels,
          input.closest("label")?.innerText,
        ];
        if (withContainer) {
          parts.push(input.closest("[approval-field], .widget-tile-row, .row")?.innerText);
        }
        return normalize(parts.filter(Boolean).join(" "));
      };
      const matchesPatterns = (input, withContainer) => definition.patterns
        .some((pattern) => describeInput(input, withContainer).includes(pattern));
      target ||= inputs.find((input) => matchesPatterns(input, false) && isTopmost(input))
        || inputs.find((input) => matchesPatterns(input, true) && isTopmost(input))
        || inputs.find((input) => matchesPatterns(input, false))
        || inputs.find((input) => matchesPatterns(input, true));
      if (!target) return null;
      const labels = target.labels ? [...target.labels].map((label) => label.innerText).join(" ") : "";
      return {
        key: definition.key,
        selector: stableSelector(target),
        frameHint,
        tag: target.tagName.toLowerCase(),
        type: target.getAttribute("type") || "",
        id: target.id || "",
        name: target.getAttribute("name") || "",
        dataTestId: target.getAttribute("data-testid")
          || target.getAttribute("data-test")
          || target.getAttribute("data-qa")
          || target.getAttribute("data-cy")
          || "",
        ariaLabel: compact(target.getAttribute("aria-label")),
        placeholder: compact(target.getAttribute("placeholder")),
        labelText: compact(labels || target.closest("label")?.innerText),
      };
    }, {
      definition: field,
      exactField: learnedField,
      inputSelector: FORM_INPUT_SELECTOR,
      frameHint: getFrameHint(frame),
    });
  } catch {
    return null;
  }
}

async function captureDescribedFields(page, fields, learnedFields = []) {
  const normalizedFields = normalizeFieldDefinitions(fields);
  const normalizedLearnedFields = normalizeLearnedFieldDefinitions(learnedFields);
  const captured = [];
  const usedTargets = new Set();
  for (const field of normalizedFields) {
    const learnedField = normalizedLearnedFields.find((candidate) => candidate.key === field.key);
    const frames = [...page.frames()].sort((left, right) => {
      const leftHint = getFrameHint(left);
      const rightHint = getFrameHint(right);
      return Number(rightHint === learnedField?.frameHint) - Number(leftHint === learnedField?.frameHint);
    });
    for (const frame of frames) {
      const exactField = !learnedField?.frameHint || getFrameHint(frame) === learnedField.frameHint
        ? learnedField
        : null;
      const metadata = await captureDescribedField(frame, field, exactField);
      if (!metadata) continue;
      const targetKey = `${metadata.frameHint}\n${metadata.selector}`;
      if (usedTargets.has(targetKey)) continue;
      usedTargets.add(targetKey);
      captured.push(metadata);
      break;
    }
  }
  return normalizeLearnedFieldDefinitions(captured);
}

async function captureTechnicalPageMap(page, reason) {
  const frames = [];
  for (const frame of page.frames().slice(0, 20)) {
    try {
      const frameMap = await frame.evaluate(({ technicalSelector, frameHint }) => {
        const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
        const redact = (value) => compact(value)
          .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "[EMAIL]")
          .replace(/\b\d{11}\b/g, "[PESEL]")
          .replace(/\b\d{7,}\b/g, "[NUMER]")
          .slice(0, 240);
        const escapeAttribute = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
        const stableSelector = (element) => {
          if (element.id) return `#${CSS.escape(element.id)}`;
          const tag = element.tagName.toLowerCase();
          for (const attribute of ["data-testid", "data-test", "data-qa", "data-cy"]) {
            const value = element.getAttribute(attribute);
            if (value) return `${tag}[${attribute}="${escapeAttribute(value)}"]`;
          }
          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 < 6; depth += 1, current = current.parentElement) {
            const currentTag = current.tagName.toLowerCase();
            let part = currentTag;
            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);
            if (current.id) {
              path[0] = `#${CSS.escape(current.id)}`;
              break;
            }
          }
          return path.join(" > ");
        };
        const controls = [...document.querySelectorAll(technicalSelector)]
          .filter((element) => element instanceof HTMLElement && element.getClientRects().length)
          .slice(0, 600)
          .map((element) => {
            const labels = element.labels ? [...element.labels].map((label) => label.innerText).join(" ") : "";
            const rect = element.getBoundingClientRect();
            return {
              selector: stableSelector(element),
              tag: element.tagName.toLowerCase(),
              type: element.getAttribute("type") || "",
              id: element.id || "",
              name: element.getAttribute("name") || "",
              className: typeof element.className === "string" ? compact(element.className).slice(0, 240) : "",
              dataTestId: element.getAttribute("data-testid")
                || element.getAttribute("data-test")
                || element.getAttribute("data-qa")
                || element.getAttribute("data-cy")
                || "",
              role: element.getAttribute("role") || "",
              ariaLabel: redact(element.getAttribute("aria-label")),
              placeholder: redact(element.getAttribute("placeholder")),
              labelText: redact(labels || element.closest("label")?.innerText),
              text: element.matches("input,textarea,select") ? "" : redact(element.innerText || element.textContent),
              disabled: Boolean(element.disabled) || element.getAttribute("aria-disabled") === "true",
              readOnly: Boolean(element.readOnly),
              position: {
                x: Math.round(rect.x),
                y: Math.round(rect.y),
                width: Math.round(rect.width),
                height: Math.round(rect.height),
              },
            };
          });
        return {
          frameHint,
          title: redact(document.title),
          controls,
          truncated: document.querySelectorAll(technicalSelector).length > controls.length,
        };
      }, { technicalSelector: TECHNICAL_MAP_SELECTOR, frameHint: getFrameHint(frame) });
      frames.push(frameMap);
    } catch {
    }
  }
  return {
    capturedAt: new Date().toISOString(),
    reason: String(reason || "recording").slice(0, 60),
    frames,
  };
}

export async function hasDescribedFields(page, fields, learnedFields = []) {
  const normalizedFields = normalizeFieldDefinitions(fields);
  if (normalizedFields.length === 0) return false;
  const captured = await captureDescribedFields(page, normalizedFields, learnedFields);
  return captured.length === normalizedFields.length;
}

export async function fillDescribedFields(page, fields, learnedFields = []) {
  const normalizedFields = normalizeFieldDefinitions(fields);
  if (normalizedFields.length === 0) return false;
  const resolvedFields = await captureDescribedFields(page, normalizedFields, learnedFields);
  if (resolvedFields.length !== normalizedFields.length) return false;
  for (let fieldIndex = 0; fieldIndex < normalizedFields.length; fieldIndex += 1) {
    const field = normalizedFields[fieldIndex];
    const learnedField = resolvedFields.find((candidate) => candidate.key === field.key);
    let filled = false;
    if (field.commitWithKeyboard) {
      filled = await fillFieldWithRealKeyboard(page, field, learnedField);
      if (filled) continue;
    }
    const enterMarker = `baza-polis-enter-${Date.now()}-${fieldIndex}`;
    const frames = [...page.frames()].sort((left, right) => {
      const leftHint = getFrameHint(left);
      const rightHint = getFrameHint(right);
      return Number(rightHint === learnedField?.frameHint) - Number(leftHint === learnedField?.frameHint);
    });
    for (const frame of frames) {
      try {
        const exactField = !learnedField?.frameHint || getFrameHint(frame) === learnedField.frameHint
          ? learnedField
          : null;
        filled = await frame.evaluate((definition) => {
          const normalize = (value) => String(value || "")
            .normalize("NFD")
            .replace(/[\u0300-\u036f]/g, "")
            .replace(/\s+/g, " ")
            .trim()
            .toLowerCase();
          const inputs = [...document.querySelectorAll(definition.inputSelector)]
            .filter((input) => input.getClientRects().length && !input.disabled && !input.readOnly);
          
          
          
          const isTopmost = (input) => {
            const rect = input.getBoundingClientRect();
            if (!rect.width || !rect.height) return true;
            const cx = rect.left + rect.width / 2;
            const cy = rect.top + rect.height / 2;
            if (cx < 0 || cy < 0 || cx >= window.innerWidth || cy >= window.innerHeight) return true;
            const hit = document.elementFromPoint(cx, cy);
            if (!hit) return true;
            return hit === input || input.contains(hit) || hit.contains(input)
              || Boolean(hit.closest("label")?.contains(input));
          };
          let target = null;
          if (definition.learnedField?.selector) {
            try {
              const exact = document.querySelector(definition.learnedField.selector);
              if (inputs.includes(exact) && isTopmost(exact)) target = exact;
            } catch {
            }
          }
          const describeInput = (input, withContainer) => {
            const labels = input.labels ? [...input.labels].map((label) => label.innerText).join(" ") : "";
            const parts = [
              input.id,
              input.name,
              input.placeholder,
              input.getAttribute("aria-label"),
              input.getAttribute("data-dtname"),
              input.getAttribute("ng-model"),
              input.getAttribute("ng-change"),
              input.closest("[approval-field]")?.getAttribute("approval-field"),
              labels,
              input.closest("label")?.innerText,
            ];
            if (withContainer) {
              parts.push(input.closest("[approval-field], .widget-tile-row, .row")?.innerText);
            }
            return normalize(parts.filter(Boolean).join(" "));
          };
          const matchesPatterns = (input, withContainer) => definition.patterns
            .some((pattern) => describeInput(input, withContainer).includes(pattern));
          target ||= inputs.find((input) => matchesPatterns(input, false) && isTopmost(input))
            || inputs.find((input) => matchesPatterns(input, true) && isTopmost(input))
            || inputs.find((input) => matchesPatterns(input, false))
            || inputs.find((input) => matchesPatterns(input, true));
          if (!target) return false;
          let valueToSet = definition.value;
          if (target instanceof HTMLInputElement && target.type === "date") {
            const polishDate = String(valueToSet).match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
            if (polishDate) valueToSet = `${polishDate[3]}-${polishDate[2]}-${polishDate[1]}`;
          }
          const prototype = target instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
          const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
          if (setter) setter.call(target, valueToSet);
          else target.value = valueToSet;
          target.dispatchEvent(new Event("input", { bubbles: true }));
          target.dispatchEvent(new Event("change", { bubbles: true }));
          target.dispatchEvent(new Event("blur", { bubbles: true }));
          if (definition.pressEnter) target.setAttribute("data-baza-polis-enter-target", definition.enterMarker);
          else target.setAttribute("data-baza-polis-fill-check", definition.enterMarker);
          return true;
        }, { ...field, enterMarker, learnedField: exactField, inputSelector: FORM_INPUT_SELECTOR });
        if (filled && !field.pressEnter) {
          
          
          
          await delay(200);
          filled = await frame.evaluate(({ marker, expected }) => {
            const element = document.querySelector(`[data-baza-polis-fill-check="${marker}"]`);
            
            
            if (!element) return true;
            element.removeAttribute("data-baza-polis-fill-check");
            const squeeze = (value) => String(value || "").replace(/[^0-9a-z]/gi, "").toLowerCase();
            return squeeze(element.value) === squeeze(expected);
          }, { marker: enterMarker, expected: field.value }).catch(() => true);
        }
        if (filled && field.pressEnter) {
          const target = await frame.$(`[data-baza-polis-enter-target="${enterMarker}"]`);
          if (!target) {
            filled = false;
          } else {
            await target.focus();
            await target.press("Enter");
            await target.evaluate((element) => element.removeAttribute("data-baza-polis-enter-target")).catch(() => {});
          }
        }
      } catch {
        filled = false;
      }
      if (filled) break;
    }
    if (!filled) return false;
  }
  return true;
}

async function installNavigationCapture(page, onStep) {
  navigationCaptureSequence += 1;
  const bindingName = `__bazaPolisRememberNavigationStep${navigationCaptureSequence}`;
  const installedFlag = `__bazaPolisNavigationCaptureInstalled${navigationCaptureSequence}`;
  await page.exposeFunction(bindingName, (rawStep) => {
    const step = normalizeNavigationStep(rawStep);
    if (step) onStep(step, { recordingStarted: rawStep?.trainingStarted === true });
  });
  return async () => {
    for (const frame of page.frames()) {
      try {
        await frame.evaluate(({ callbackName, clickableSelector, captureInstalledFlag }) => {
          if (window[captureInstalledFlag]) return;
          window[captureInstalledFlag] = true;
          const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
          const escapeAttribute = (value) => String(value || "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
          const stableSelector = (element) => {
            if (element.id) return `#${CSS.escape(element.id)}`;
            const tag = element.tagName.toLowerCase();
            for (const attribute of ["data-testid", "data-test", "data-qa", "data-cy"]) {
              const value = element.getAttribute(attribute);
              if (value) return `${tag}[${attribute}="${escapeAttribute(value)}"]`;
            }
            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) {
              const currentTag = current.tagName.toLowerCase();
              let part = currentTag;
              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(" > ");
          };
          window.addEventListener("click", (event) => {
            const source = event.target instanceof Element ? event.target : null;
            if (source?.closest("[data-baza-polis-control]")) return;
            let target = source?.closest(clickableSelector);
            if (!target) {
              let candidate = source;
              for (let level = 0; level < 6 && candidate; level += 1, candidate = candidate.parentElement) {
                if (!(candidate instanceof HTMLElement) || !candidate.getClientRects().length) continue;
                const hasStableAttribute = Boolean(candidate.id
                  || candidate.getAttribute("data-testid")
                  || candidate.getAttribute("data-test")
                  || candidate.getAttribute("data-qa")
                  || candidate.getAttribute("data-cy"));
                if (hasStableAttribute || getComputedStyle(candidate).cursor === "pointer") {
                  target = candidate;
                  break;
                }
              }
            }
            if (!(target instanceof HTMLElement) || !target.getClientRects().length) return;
            let hrefPath = "";
            const rawHref = target.getAttribute("href");
            if (rawHref) {
              try {
                const href = new URL(rawHref, location.href);
                hrefPath = `${href.hostname}${href.pathname}`;
              } catch {
              }
            }
            const step = {
              tag: target.tagName.toLowerCase(),
              type: target.getAttribute("type") || "",
              id: target.id || "",
              name: target.getAttribute("name") || "",
              className: typeof target.className === "string" ? target.className : "",
              dataTestId: target.getAttribute("data-testid")
                || target.getAttribute("data-test")
                || target.getAttribute("data-qa")
                || target.getAttribute("data-cy")
                || "",
              role: target.getAttribute("role") || "",
              text: compact(target.innerText || target.value || target.textContent).slice(0, 160),
              ariaLabel: compact(target.getAttribute("aria-label")).slice(0, 160),
              title: compact(target.getAttribute("title")).slice(0, 160),
              hrefPath,
              frameHint: `${location.hostname}${location.pathname}`.slice(0, 220),
              selector: stableSelector(target).slice(0, 500),
              trainingStarted: window.__bazaPolisTrainingRecordingStarted === true,
            };
            const callback = window[callbackName];
            if (typeof callback === "function") Promise.resolve(callback(step)).catch(() => {});
          }, true);
        }, { callbackName: bindingName, clickableSelector: CLICKABLE_SELECTOR, captureInstalledFlag: installedFlag });
      } catch {
      }
    }
  };
}

async function refreshTrainingFinishControl(page, {
  insurerName,
  errorMessage = "",
  recordingActive = false,
} = {}) {
  try {
    return await page.mainFrame().evaluate(({ companyName, currentError, isRecording }) => {
      const hostId = "baza-polis-training-control";
      window.__bazaPolisTrainingRecordingStarted = window.__bazaPolisTrainingRecordingStarted === true || isRecording;
      let host = document.getElementById(hostId);
      if (!host) {
        host = document.createElement("div");
        host.id = hostId;
        host.dataset.bazaPolisControl = "finish-learning";
        host.style.cssText = "position:fixed;z-index:2147483647;right:18px;top:18px;width:330px;max-width:calc(100vw - 36px);font-family:Arial,sans-serif;color:#0b2138;";
        const shadow = host.attachShadow({ mode: "open" });
        shadow.innerHTML = `
          <style>
            .box{box-sizing:border-box;border:2px solid #00a86b;border-radius:14px;background:#fff;box-shadow:0 12px 35px rgba(0,0,0,.28);padding:14px}
            strong{display:block;font-size:15px;margin-bottom:6px}.info{font-size:12px;line-height:1.4;color:#3d5368;margin-bottom:10px}
            .error{display:none;color:#a41515;background:#fff0f0;border-radius:8px;padding:7px;margin-bottom:8px;font-size:11px;line-height:1.35}
            button{box-sizing:border-box;width:100%;border:0;border-radius:9px;background:#008f5b;color:#fff;font-weight:700;font-size:13px;padding:11px 12px;cursor:pointer}
            .buttons{display:grid;gap:8px}.start{background:#0b5fa5}.start:hover{background:#084c84}
            button:hover{background:#00794d}button:disabled{opacity:.58;cursor:not-allowed}
          </style>
          <div class="box">
            <strong>Nagrywanie trasy: <span class="company"></span></strong>
            <div class="info">Po zalogowaniu, przed wyborem rekordu, kliknij „Rozpocznij nagrywanie”. Potem wykonaj wszystkie kroki - również Analizę potrzeb Klienta. Po wykryciu numeru rejestracyjnego i PESEL nagrywanie zakończy się automatycznie.</div>
            <div class="error"></div>
            <div class="buttons">
              <button class="start" type="button">Rozpocznij nagrywanie</button>
              <button class="finish" type="button" disabled>Tu jest formularz - zakończ nagrywanie</button>
            </div>
          </div>`;
        Object.assign(shadow.querySelector(".box").style, {
          boxSizing: "border-box",
          border: "2px solid #00a86b",
          borderRadius: "14px",
          background: "#fff",
          boxShadow: "0 12px 35px rgba(0,0,0,.28)",
          padding: "14px",
        });
        Object.assign(shadow.querySelector("strong").style, {
          display: "block",
          fontSize: "15px",
          marginBottom: "6px",
        });
        Object.assign(shadow.querySelector(".info").style, {
          fontSize: "12px",
          lineHeight: "1.4",
          color: "#3d5368",
          marginBottom: "10px",
        });
        Object.assign(shadow.querySelector(".error").style, {
          display: "none",
          color: "#a41515",
          background: "#fff0f0",
          borderRadius: "8px",
          padding: "7px",
          marginBottom: "8px",
          fontSize: "11px",
          lineHeight: "1.35",
        });
        Object.assign(shadow.querySelector(".buttons").style, {
          display: "grid",
          gap: "8px",
        });
        for (const button of shadow.querySelectorAll("button")) {
          Object.assign(button.style, {
            boxSizing: "border-box",
            width: "100%",
            border: "0",
            borderRadius: "9px",
            background: "#008f5b",
            color: "#fff",
            fontWeight: "700",
            fontSize: "13px",
            padding: "11px 12px",
            cursor: "pointer",
          });
        }
        shadow.querySelector(".start").style.background = "#0b5fa5";
        shadow.querySelector(".start").addEventListener("click", () => {
          window.__bazaPolisTrainingRecordingStarted = true;
          const startButton = shadow.querySelector(".start");
          const finishButton = shadow.querySelector(".finish");
          startButton.disabled = true;
          startButton.textContent = "Nagrywanie aktywne";
          finishButton.disabled = false;
        });
        const requestFinish = (event) => {
          event.preventDefault();
          event.stopPropagation();
          if (window.__bazaPolisTrainingRecordingStarted !== true) return;
          window.__bazaPolisTrainingFinishRequested = true;
          const button = shadow.querySelector(".finish");
          button.disabled = true;
          button.textContent = "Sprawdzam formularz…";
        };
        shadow.querySelector(".finish").addEventListener("pointerdown", requestFinish, true);
        shadow.querySelector(".finish").addEventListener("click", requestFinish, true);
        document.documentElement.appendChild(host);
      }
      const shadow = host.shadowRoot;
      shadow.querySelector(".company").textContent = companyName;
      const error = shadow.querySelector(".error");
      error.textContent = currentError;
      error.style.display = currentError ? "block" : "none";
      const started = window.__bazaPolisTrainingRecordingStarted === true;
      const startButton = shadow.querySelector(".start");
      const finishButton = shadow.querySelector(".finish");
      startButton.disabled = started;
      startButton.textContent = started ? "Nagrywanie aktywne" : "Rozpocznij nagrywanie";
      if (window.__bazaPolisTrainingFinishRequested !== true) finishButton.disabled = !started;
      return {
        startRequested: started,
        finishRequested: window.__bazaPolisTrainingFinishRequested === true,
      };
    }, { companyName: insurerName, currentError: errorMessage, isRecording: recordingActive });
  } catch {
    return { startRequested: recordingActive, finishRequested: false };
  }
}

async function resetTrainingFinishControl(page, errorMessage) {
  try {
    await page.mainFrame().evaluate((message) => {
      window.__bazaPolisTrainingFinishRequested = false;
      const shadow = document.getElementById("baza-polis-training-control")?.shadowRoot;
      if (!shadow) return;
      const error = shadow.querySelector(".error");
      error.textContent = message;
      error.style.display = "block";
      const button = shadow.querySelector(".finish");
      button.disabled = window.__bazaPolisTrainingRecordingStarted !== true;
      button.textContent = "Tu jest formularz - zakończ nagrywanie";
    }, errorMessage);
  } catch {
  }
}

async function removeTrainingFinishControl(page) {
  try {
    await page.mainFrame().evaluate(() => {
      document.getElementById("baza-polis-training-control")?.remove();
      window.__bazaPolisTrainingFinishRequested = false;
      window.__bazaPolisTrainingRecordingStarted = false;
    });
  } catch {
  }
}

async function clickNavigationStep(page, step, timeout = 15000) {
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    for (const frame of page.frames()) {
      try {
        const frameHint = (() => {
          try {
            const frameUrl = new URL(frame.url());
            return `${frameUrl.hostname}${frameUrl.pathname}`.slice(0, 220);
          } catch {
            return "";
          }
        })();
        const clicked = await frame.evaluate(({ expected, clickableSelector, currentFrameHint }) => {
          const normalize = (value) => String(value || "")
            .normalize("NFD")
            .replace(/[\u0300-\u036f]/g, "")
            .replace(/\s+/g, " ")
            .trim()
            .toLowerCase();
          const compact = (value) => String(value || "").replace(/\s+/g, " ").trim();
          const expectedText = normalize(expected.text);
          const expectedAria = normalize(expected.ariaLabel);
          const expectedTitle = normalize(expected.title);
          const extraTag = /^[a-z][a-z0-9-]{0,30}$/.test(expected.tag) ? `,${expected.tag}` : "";
          let exactElement = null;
          if (expected.selector && (!expected.frameHint || expected.frameHint === currentFrameHint)) {
            try {
              exactElement = document.querySelector(expected.selector);
            } catch {
            }
          }
          const candidates = [...new Set([
            ...(exactElement ? [exactElement] : []),
            ...document.querySelectorAll(`${clickableSelector}${extraTag}`),
          ])]
            .filter((element) => element instanceof HTMLElement
              && element.getClientRects().length
              && !element.disabled
              && element.getAttribute("aria-disabled") !== "true")
            .map((element) => {
              const text = normalize(element.innerText || element.value || element.textContent);
              const ariaLabel = normalize(element.getAttribute("aria-label"));
              const title = normalize(element.getAttribute("title"));
              let hrefPath = "";
              const rawHref = element.getAttribute("href");
              if (rawHref) {
                try {
                  const href = new URL(rawHref, location.href);
                  hrefPath = `${href.hostname}${href.pathname}`;
                } catch {
                }
              }
              let semantic = 0;
              if (exactElement === element) semantic += 140;
              if (expected.id && element.id === expected.id) semantic += 90;
              const dataTestId = element.getAttribute("data-testid")
                || element.getAttribute("data-test")
                || element.getAttribute("data-qa")
                || element.getAttribute("data-cy")
                || "";
              if (expected.dataTestId && dataTestId === expected.dataTestId) semantic += 88;
              if (expected.name && element.getAttribute("name") === expected.name) semantic += 70;
              if (expectedAria && ariaLabel === expectedAria) semantic += 65;
              else if (expectedAria && ariaLabel.includes(expectedAria)) semantic += 30;
              if (expectedText && text === expectedText) semantic += 60;
              else if (expectedText && text.includes(expectedText)) semantic += 28;
              if (expectedTitle && title === expectedTitle) semantic += 45;
              if (expected.hrefPath && hrefPath === expected.hrefPath) semantic += 45;
              let score = semantic;
              if (expected.tag && element.tagName.toLowerCase() === expected.tag) score += 8;
              if (expected.type && element.getAttribute("type") === expected.type) score += 5;
              if (expected.role && element.getAttribute("role") === expected.role) score += 5;
              if (expected.className && typeof element.className === "string") {
                const expectedClasses = expected.className.split(/\s+/).filter(Boolean);
                const actualClasses = new Set(element.className.split(/\s+/).filter(Boolean));
                score += Math.min(20, expectedClasses.filter((className) => actualClasses.has(className)).length * 5);
              }
              if (expected.frameHint && expected.frameHint === currentFrameHint) score += 12;
              return { element, score, semantic, textLength: compact(element.innerText || element.value || element.textContent).length };
            })
            .filter((candidate) => candidate.score >= 28 && candidate.semantic > 0)
            .sort((left, right) => right.score - left.score || left.textLength - right.textLength);
          const target = candidates[0]?.element;
          if (!target) return false;
          target.click();
          return true;
        }, { expected: step, clickableSelector: CLICKABLE_SELECTOR, currentFrameHint: frameHint });
        if (clicked) return true;
      } catch {
      }
    }
    await delay(300);
  }
  return false;
}

export async function replayNavigationPath(page, path, fields, learnedFields = [], {
  stepTimeout = 15000,
  completionTimeout = 25000,
} = {}) {
  const normalizedPath = normalizeNavigationPath(path);
  const expectedFieldCount = normalizeFieldDefinitions(fields).length;
  const completedSteps = [];
  let fieldsFrameHint = null;
  for (const step of normalizedPath) {
    if (fieldsFrameHint === null) {
      const captured = await captureDescribedFields(page, fields, learnedFields);
      if (expectedFieldCount > 0 && captured.length === expectedFieldCount) {
        fieldsFrameHint = captured[0]?.frameHint || "";
      }
    }
    if (fieldsFrameHint !== null) {
      if (step.frameHint && step.frameHint !== fieldsFrameHint) continue;
      if (!(await clickNavigationStep(page, step, 2500))) continue;
      completedSteps.push(step);
      await delay(650);
      continue;
    }
    if (!(await clickNavigationStep(page, step, stepTimeout))) return { success: false, completedSteps };
    completedSteps.push(step);
    await delay(650);
    
    
    await waitForUiQuiet(page, { timeout: 5000 });
  }
  const success = await (async () => {
    const deadline = Date.now() + completionTimeout;
    while (Date.now() < deadline) {
      if (await hasDescribedFields(page, fields, learnedFields)) return true;
      await delay(350);
    }
    return false;
  })();
  return { success, completedSteps };
}

async function waitForCompletion(isComplete, timeout = 15000) {
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    if (await isComplete()) return true;
    await delay(350);
  }
  return false;
}

export async function replayActionPath(page, path, isComplete, {
  stepTimeout = 15000,
  completionTimeout = 25000,
  skipMissing = false,
  afterStep = null,
  beforeStep = null,
} = {}) {
  const normalizedPath = normalizeNavigationPath(path);
  const completedSteps = [];
  for (const step of normalizedPath) {
    if (await isComplete()) return { success: true, completedSteps };
    if (typeof beforeStep === "function") {
      const directive = await beforeStep(step).catch(() => null);
      if (directive === "skip") continue;
    }
    if (!(await clickNavigationStep(page, step, stepTimeout))) {
      if (!skipMissing) return { success: false, completedSteps };
      continue;
    }
    completedSteps.push(step);
    await delay(650);
    
    await waitForUiQuiet(page, { timeout: 5000 });
    if (typeof afterStep === "function") await afterStep(step).catch(() => {});
  }
  return {
    success: await waitForCompletion(isComplete, completionTimeout),
    completedSteps,
  };
}

const BLOCKED_FIELD_SELECTOR = "input:not([type='hidden']):not([type='button']):not([type='submit']):not([type='image']):not([type='file']), select, textarea, [role='combobox'], [aria-haspopup='listbox']";

function compactFieldText(value, maximum = 160) {
  return String(value || "").replace(/\s+/g, " ").trim().slice(0, maximum);
}

function normalizeDateMask(value) {
  return compactFieldText(value, 80)
    .replace(/[dD]/g, "_")
    .replace(/[mM]/g, "_")
    .replace(/[rRyY]/g, "_");
}





export function normalizeBlockedFieldMetadata(rawField) {
  const field = rawField && typeof rawField === "object" ? rawField : {};
  const type = compactFieldText(field.type, 40).toLowerCase();
  const name = compactFieldText(field.name, 120);
  const placeholder = compactFieldText(field.placeholder, 120);
  const rawLabel = compactFieldText(field.label, 160);
  const normalizedLabel = normalizeText(rawLabel);
  const normalizedName = normalizeText(name);
  const normalizedPlaceholder = normalizeDateMask(placeholder);
  const technicalDateName = /^(date|dates|datetime|datetimes|birthdate|birth-date|dateofbirth)$/i.test(name);
  const dateMask = /^_{4}[-./]_{2}[-./]_{2}$/.test(normalizedPlaceholder)
    || /^_{2}[-./]_{2}[-./]_{4}$/.test(normalizedPlaceholder);
  const dateLike = ["date", "datetime-local", "month"].includes(type)
    || technicalDateName
    || dateMask
    || /^(date|dates|datetime|datetimes)$/.test(normalizedLabel)
    || /(^| )data( |$)|date of|birth date/.test(normalizedLabel);

  let kind = compactFieldText(field.kind, 40).toLowerCase() || "text";
  if (dateLike) {
    kind = type === "datetime-local" ? "datetime" : type === "month" ? "month" : "date";
  } else if (!["text", "number", "select", "checkbox", "radio", "combobox"].includes(kind)) {
    kind = "text";
  }

  let label = rawLabel;
  const technicalLabel = /^(date|dates|datetime|datetimes)$/i.test(label)
    || (technicalDateName && normalizeText(label) === normalizedName);
  if (/^datetimes?$/i.test(name) || /^datetimes?$/i.test(label)) {
    label = "Data urodzenia";
  } else if (technicalLabel) {
    label = "Data";
  } else if (!label && technicalDateName) {
    label = /^datetimes?$/i.test(name) ? "Data urodzenia" : "Data";
  }
  if (!label) label = placeholder && !dateMask ? placeholder : "Pole wymagane przez kalkulator";

  let formatHint = compactFieldText(field.formatHint, 120);
  if (!formatHint && kind === "date") {
    if (/^_{2}\._{2}\._{4}$/.test(normalizedPlaceholder)) formatHint = "DD.MM.RRRR";
    else if (/^_{2}-_{2}-_{4}$/.test(normalizedPlaceholder)) formatHint = "DD-MM-RRRR";
    else formatHint = "RRRR-MM-DD";
  } else if (!formatHint && kind === "datetime") {
    formatHint = "RRRR-MM-DD GG:MM";
  } else if (!formatHint && kind === "month") {
    formatHint = "RRRR-MM";
  }

  return {
    ...field,
    kind,
    type,
    name,
    placeholder,
    label: compactFieldText(label, 160),
    formatHint,
  };
}

export async function collectBlockedFormFields(page) {
  const fields = [];
  let screenTitle = "";
  for (const frame of page.frames().slice(0, 20)) {
    try {
      const frameResult = await frame.evaluate(({ candidateSelector, frameHint }) => {
        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 stableSelector = (element) => {
          if (element.id) return `#${CSS.escape(element.id)}`;
          const tag = element.tagName.toLowerCase();
          for (const attribute of ["data-testid", "data-test", "data-qa", "data-cy"]) {
            const value = element.getAttribute(attribute);
            if (value) return `${tag}[${attribute}="${escapeAttribute(value)}"]`;
          }
          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 visible = (element) => element instanceof HTMLElement
          && element.getClientRects().length > 0
          && getComputedStyle(element).visibility !== "hidden";
        const hasErrorMarker = (element) => {
          if (element.getAttribute("aria-invalid") === "true") return true;
          let current = element;
          for (let depth = 0; current instanceof HTMLElement && depth < 4; depth += 1, current = current.parentElement) {
            const className = String(current.className || "");
            if (className.includes("Mui-error")) return true;
            if (/(^|[\s_-])(error|invalid|has-error)([\s_-]|$)/i.test(className)) return true;
          }
          return false;
        };
        const labelFor = (element) => {
          const labels = element.labels ? [...element.labels].map((label) => label.innerText).join(" ") : "";
          const container = element.closest("[class*='field'], [class*='Field'], [class*='form-group'], [class*='FormControl'], [class*='row']");
          return compact(labels
            || element.getAttribute("aria-label")
            || element.getAttribute("placeholder")
            || element.closest("label")?.innerText
            || container?.querySelector("label, legend, [class*='label'], [class*='Label']")?.innerText
            || element.getAttribute("name")
            || element.id).slice(0, 160);
        };
        const placeholderPattern = /^(wybierz|select|szukaj|-+|\.+)?$/i;
        const heading = [...document.querySelectorAll("h1, h2, h3, h4")]
          .find((element) => visible(element) && compact(element.innerText).length > 2);
        const seenRadioGroups = new Set();
        const results = [];
        const controls = [...document.querySelectorAll(candidateSelector)]
          .filter((element) => visible(element) && !element.disabled && !element.readOnly && element.getAttribute("aria-disabled") !== "true");
        for (const element of controls) {
          if (results.length >= 40) break;
          const tag = element.tagName.toLowerCase();
          const type = (element.getAttribute("type") || (tag === "select" ? "select" : "text")).toLowerCase();
          const invalid = hasErrorMarker(element);
          const required = element.required === true || element.getAttribute("aria-required") === "true";
          let entry = null;
          if (tag !== "input" && tag !== "select" && tag !== "textarea") {
            const valueText = compact(element.innerText);
            if (!placeholderPattern.test(normalize(valueText)) && !invalid) continue;
            entry = {
              kind: "combobox",
              selector: stableSelector(element),
              label: labelFor(element) || valueText.slice(0, 80),
              options: [],
              currentValue: placeholderPattern.test(normalize(valueText)) ? "" : valueText.slice(0, 120),
              invalid,
              required,
            };
          } else if (type === "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(visible);
            } catch {
            }
            if (groupRadios.length === 0) continue;
            const checked = groupRadios.some((radio) => radio.checked);
            const groupInvalid = groupRadios.some(hasErrorMarker);
            if (checked && !groupInvalid) continue;
            entry = {
              kind: "radio",
              selector: stableSelector(element),
              label: compact(element.closest("fieldset")?.querySelector("legend")?.innerText).slice(0, 160) || labelFor(element),
              options: groupRadios.slice(0, 40).map((radio) => {
                const radioLabels = radio.labels ? [...radio.labels].map((label) => label.innerText).join(" ") : "";
                return {
                  value: radio.value || "",
                  text: compact(radioLabels || radio.closest("label")?.innerText || radio.value).slice(0, 120),
                  selector: stableSelector(radio),
                };
              }),
              currentValue: "",
              invalid: groupInvalid,
              required: required || groupRadios.some((radio) => radio.required),
            };
          } else if (type === "checkbox") {
            if (element.checked && !invalid) continue;
            if (!invalid && !required) continue;
            entry = {
              kind: "checkbox",
              selector: stableSelector(element),
              label: labelFor(element),
              options: [],
              currentValue: element.checked ? "tak" : "",
              invalid,
              required,
            };
          } else if (tag === "select") {
            const selectedText = compact(element.selectedOptions?.[0]?.innerText);
            const empty = !element.value || placeholderPattern.test(normalize(selectedText));
            if (!empty && !invalid) continue;
            entry = {
              kind: "select",
              selector: stableSelector(element),
              label: labelFor(element),
              options: [...element.options].slice(0, 60).map((option) => ({
                value: option.value || "",
                text: compact(option.innerText).slice(0, 120),
                selector: "",
              })),
              currentValue: element.value || "",
              invalid,
              required,
            };
          } else {
            const value = compact(element.value);
            if (value && !invalid) continue;
            
            
            
            const comboboxLike = element.getAttribute("role") === "combobox"
              || element.getAttribute("aria-haspopup") === "listbox"
              || element.getAttribute("aria-autocomplete") === "list"
              || Boolean(element.closest("[role='combobox'], [class*='autocomplete' i], [class*='Autocomplete']"));
            entry = {
              kind: comboboxLike ? "combobox" : type === "number" ? "number" : "text",
              selector: stableSelector(element),
              label: labelFor(element),
              options: [],
              currentValue: value.slice(0, 120),
              invalid,
              required,
            };
          }
          if (!entry?.selector || !entry.label) continue;
          results.push({
            ...entry,
            frameHint,
            tag,
            type,
            name: element.getAttribute("name") || "",
            placeholder: element.getAttribute("placeholder") || "",
            inputMode: element.getAttribute("inputmode") || "",
            pattern: element.getAttribute("pattern") || "",
          });
        }
        return { heading: compact(heading?.innerText).slice(0, 140), fields: results };
      }, { candidateSelector: BLOCKED_FIELD_SELECTOR, frameHint: getFrameHint(frame) });
      if (!screenTitle && frameResult?.heading) screenTitle = frameResult.heading;
      if (Array.isArray(frameResult?.fields)) fields.push(...frameResult.fields);
    } catch {
    }
  }
  
  
  
  const navigationFieldPattern = /szukaj|wyszuk|search|filtr|filter/i;
  
  
  const marketingFieldPattern = /(^|[^a-z])vip([^a-z]|$)|platinum/i;
  const prioritized = fields
    .map(normalizeBlockedFieldMetadata)
    .filter((field) => field.invalid === true
      || !(navigationFieldPattern.test(field.label) || navigationFieldPattern.test(field.selector)
        || marketingFieldPattern.test(field.label)))
    .sort((left, right) => Number(right.invalid) - Number(left.invalid)
      || Number(right.required) - Number(left.required))
    .slice(0, 25)
    .map((field, index) => ({ ...field, id: `pole-${index + 1}` }));
  return { title: screenTitle, fields: prioritized };
}





async function harvestComboboxOptions(page, field) {
  const optionSelector = "[role='option'], [role='menuitem'], li[data-value]";
  const collectVisibleOptionTexts = async () => {
    const texts = new Set();
    for (const frame of page.frames().slice(0, 20)) {
      try {
        const frameTexts = await frame.evaluate(({ selector }) => [...document.querySelectorAll(selector)]
          .filter((element) => element instanceof HTMLElement && element.getClientRects().length)
          .map((element) => String(element.innerText || "").replace(/\s+/g, " ").trim())
          .filter(Boolean)
          .slice(0, 200), { selector: optionSelector });
        for (const text of frameTexts) texts.add(text);
      } catch {
      }
    }
    return texts;
  };
  const frames = [...page.frames()].sort((left, right) => {
    const leftHint = getFrameHint(left);
    const rightHint = getFrameHint(right);
    return Number(rightHint === field.frameHint) - Number(leftHint === field.frameHint);
  });
  const before = await collectVisibleOptionTexts();
  let openedFrame = null;
  for (const frame of frames) {
    try {
      const opened = await frame.evaluate(({ selector }) => {
        const element = document.querySelector(selector);
        if (!(element instanceof HTMLElement) || !element.getClientRects().length) return false;
        element.focus?.();
        element.click();
        
        
        element.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }));
        return true;
      }, { selector: field.selector });
      if (opened) {
        openedFrame = frame;
        break;
      }
    } catch {
    }
  }
  if (!openedFrame) return [];
  const deadline = Date.now() + 3000;
  let fresh = [];
  while (Date.now() < deadline) {
    await delay(300);
    const current = await collectVisibleOptionTexts();
    fresh = [...current].filter((text) => !before.has(text));
    if (fresh.length > 0) {
      
      await delay(300);
      const settled = await collectVisibleOptionTexts();
      const settledFresh = [...settled].filter((text) => !before.has(text));
      if (settledFresh.length >= fresh.length) fresh = settledFresh;
      break;
    }
  }
  await openedFrame.evaluate(({ selector }) => {
    const element = document.querySelector(selector);
    if (element instanceof HTMLElement) {
      element.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
      element.blur?.();
    }
  }, { selector: field.selector }).catch(() => {});
  const placeholderPattern = /^(wybierz|select|szukaj|brak wyników|no results|ładowanie|loading)([\s.…-]|$)|^[-.…\s]+$/i;
  return fresh
    .filter((text) => !placeholderPattern.test(text))
    .slice(0, 60)
    .map((text) => ({ value: text.slice(0, 120), text: text.slice(0, 120), selector: "" }));
}

async function fillSingleAnswer(page, field, value, checked) {
  const frames = [...page.frames()].sort((left, right) => {
    const leftHint = getFrameHint(left);
    const rightHint = getFrameHint(right);
    return Number(rightHint === field.frameHint) - Number(leftHint === field.frameHint);
  });
  for (const frame of frames) {
    try {
      if (field.kind === "combobox") {
        const opened = await frame.evaluate(({ selector, wanted }) => {
          const element = document.querySelector(selector);
          if (!(element instanceof HTMLElement) || !element.getClientRects().length) return false;
          element.click();
          
          
          
          if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
            const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
            const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
            if (setter) setter.call(element, wanted);
            else element.value = wanted;
            element.dispatchEvent(new Event("input", { bubbles: true }));
          }
          return true;
        }, { selector: field.selector, wanted: String(value ?? "") });
        if (!opened) continue;
        
        
        
        const optionsDeadline = Date.now() + 3000;
        let pickedOption = false;
        while (!pickedOption && Date.now() < optionsDeadline) {
          await delay(300);
          for (const optionFrame of page.frames()) {
            pickedOption = await optionFrame.evaluate(({ wanted }) => {
              const normalize = (input) => String(input || "")
                .normalize("NFD")
                .replace(/[̀-ͯ]/g, "")
                .replace(/\s+/g, " ")
                .trim()
                .toLowerCase();
              const target = normalize(wanted);
              if (!target) return false;
              const options = [...document.querySelectorAll("[role='option'], [role='menuitem'], li[data-value]")]
                .filter((option) => option instanceof HTMLElement && option.getClientRects().length);
              const exact = options.find((option) => normalize(option.innerText) === target)
                || options.find((option) => normalize(option.innerText).includes(target))
                || options.find((option) => normalize(option.getAttribute("data-value")) === target);
              if (!exact) return false;
              exact.click();
              return true;
            }, { wanted: value }).catch(() => false);
            if (pickedOption) break;
          }
        }
        if (pickedOption) return true;
        await frame.evaluate(({ selector }) => {
          const element = document.querySelector(selector);
          if (!(element instanceof HTMLElement)) return;
          element.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
          
          
          if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
            const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
            const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
            if (setter) setter.call(element, "");
            else element.value = "";
            element.dispatchEvent(new Event("input", { bubbles: true }));
          }
        }, { selector: field.selector }).catch(() => {});
        continue;
      }
      const filled = await frame.evaluate(({ definition, wanted, wantedChecked }) => {
        const normalize = (input) => String(input || "")
          .normalize("NFD")
          .replace(/[̀-ͯ]/g, "")
          .replace(/\s+/g, " ")
          .trim()
          .toLowerCase();
        const dispatchAll = (element) => {
          element.dispatchEvent(new Event("input", { bubbles: true }));
          element.dispatchEvent(new Event("change", { bubbles: true }));
          element.dispatchEvent(new Event("blur", { bubbles: true }));
        };
        if (definition.kind === "radio") {
          const wantedNormalized = normalize(wanted);
          for (const option of definition.options || []) {
            if (normalize(option.value) !== wantedNormalized && normalize(option.text) !== wantedNormalized
              && !normalize(option.text).includes(wantedNormalized)) continue;
            let radio = null;
            try {
              radio = document.querySelector(option.selector);
            } catch {
            }
            if (radio instanceof HTMLElement && radio.getClientRects().length) {
              radio.click();
              return true;
            }
          }
          return false;
        }
        let element = null;
        try {
          element = document.querySelector(definition.selector);
        } catch {
        }
        if (!(element instanceof HTMLElement)) return false;
        
        const bootstrapWrapperVisible = definition.kind === "select"
          && Boolean(element.closest(".bootstrap-select")?.getClientRects().length);
        if (!element.getClientRects().length && !bootstrapWrapperVisible) return false;
        if (definition.kind === "checkbox") {
          if (element.checked !== wantedChecked) element.click();
          return true;
        }
        if (definition.kind === "select") {
          const wantedNormalized = normalize(wanted);
          let option = [...element.options].find((candidate) => candidate.value === wanted)
            || [...element.options].find((candidate) => normalize(candidate.innerText) === wantedNormalized)
            || [...element.options].find((candidate) => normalize(candidate.innerText).includes(wantedNormalized));
          if (!option) {
            
            
            const isoDate = String(wanted).match(/^(\d{4})-\d{1,2}-\d{1,2}$/);
            const polishDate = String(wanted).match(/^\d{1,2}[.-]\d{1,2}[.-](\d{4})$/);
            const year = isoDate ? isoDate[1] : polishDate ? polishDate[1] : "";
            if (year) option = [...element.options].find((candidate) => candidate.innerText.trim() === year);
          }
          if (!option) {
            const numericValue = Number(String(wanted).replace(/\s/g, "").replace(",", "."));
            if (Number.isFinite(numericValue) && numericValue > 0 && /^[\d\s.,]+$/.test(String(wanted))) {
              option = [...element.options].find((candidate) => {
                const rangeMatch = String(candidate.innerText).match(/(\d[\d\s]*)\s*-\s*(\d[\d\s]*)/);
                if (!rangeMatch) return false;
                const low = Number(rangeMatch[1].replace(/\s/g, ""));
                const high = Number(rangeMatch[2].replace(/\s/g, ""));
                return Number.isFinite(low) && Number.isFinite(high) && low <= numericValue && numericValue <= high;
              });
            }
          }
          if (!option) return false;
          if (element.value === option.value) return true;
          element.value = option.value;
          dispatchAll(element);
          try {
            window.jQuery?.(element).selectpicker?.("render");
          } catch {
          }
          return true;
        }
        const prototype = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
        const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
        let valueToSet = wanted;
        
        
        if (element instanceof HTMLInputElement && element.type === "date") {
          const polishDate = String(valueToSet).match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
          if (polishDate) valueToSet = `${polishDate[3]}-${polishDate[2].padStart(2, "0")}-${polishDate[1].padStart(2, "0")}`;
        }
        
        
        
        if (element instanceof HTMLInputElement && element.type !== "date"
          && /^_{4}[-._]_{2}[-._]_{2}$/.test(String(element.placeholder || ""))) {
          const polishDate = String(valueToSet).match(/^(\d{1,2})[.-](\d{1,2})[.-](\d{4})$/);
          if (polishDate) valueToSet = `${polishDate[3]}-${polishDate[2].padStart(2, "0")}-${polishDate[1].padStart(2, "0")}`;
        }
        
        
        
        if (element instanceof HTMLInputElement && element.type !== "date"
          && /^_{2}([-.])_{2}\1_{4}$/.test(String(element.placeholder || ""))) {
          const separator = String(element.placeholder)[2];
          const isoDate = String(valueToSet).match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
          const polishDate = String(valueToSet).match(/^(\d{1,2})[.-](\d{1,2})[.-](\d{4})$/);
          if (isoDate) {
            valueToSet = `${isoDate[3].padStart(2, "0")}${separator}${isoDate[2].padStart(2, "0")}${separator}${isoDate[1]}`;
          } else if (polishDate) {
            valueToSet = `${polishDate[1].padStart(2, "0")}${separator}${polishDate[2].padStart(2, "0")}${separator}${polishDate[3]}`;
          }
        }
        if (setter) setter.call(element, valueToSet);
        else element.value = valueToSet;
        dispatchAll(element);
        
        
        const appliedValue = String(element.value ?? "");
        if (appliedValue === String(valueToSet)) return true;
        const appliedDigits = appliedValue.replace(/\D/g, "");
        const wantedDigits = String(valueToSet).replace(/\D/g, "");
        return wantedDigits.length > 0 && appliedDigits === wantedDigits;
      }, { definition: field, wanted: value, wantedChecked: checked });
      if (filled) return true;
    } catch {
    }
  }
  return false;
}

export async function fillRequestedAnswers(page, fields, answers) {
  let filledCount = 0;
  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);
    const checked = typeof answer === "object"
      ? answer.checked === true
      : /^(true|tak|1)$/i.test(value.trim());
    if (field.kind === "checkbox") {
      if (typeof answer === "object" && answer.checked === undefined) continue;
    } else if (!value.trim()) {
      continue;
    }
    if (await fillSingleAnswer(page, field, value.trim(), checked)) filledCount += 1;
    await delay(200);
  }
  return filledCount;
}







function createKnownAnswerFieldResolver(entries) {
  return (field) => {
    
    
    if (field?.kind === "checkbox") return "";
    const label = normalizeText(field?.label);
    if (!label) return "";
    for (const entry of entries) {
      if (!entry.pattern.test(label) || (entry.reject && entry.reject.test(label))) continue;
      const value = typeof entry.value === "function" ? entry.value(label) : entry.value;
      if (value) return value;
    }
    return "";
  };
}

function buildKnownVehicleAnswerEntries({ vin, vehicleInfo, pesel } = {}) {
  const info = vehicleInfo && typeof vehicleInfo === "object" ? vehicleInfo : {};
  const digitsOnly = (value) => String(value || "").replace(/\D/g, "");
  const mileageOf = (value) => {
    const text = String(value || "").trim();
    const valueBeforeUnit = text.match(/(\d(?:[\d\s\u00a0\u202f.,']*\d)?)\s*km\b/i)?.[1];
    const leadingValue = text.match(/^\s*(\d(?:[\d\s\u00a0\u202f.,']*\d)?)/)?.[1];
    return digitsOnly(valueBeforeUnit || leadingValue || "");
  };
  const enginePowerForField = (value, normalizedLabel) => {
    const text = String(value || "").trim();
    const measurements = [...text.matchAll(/(\d+(?:[.,]\d+)?)\s*(kw|km)(?=$|[^a-z])/gi)]
      .map((match) => ({
        value: Number(match[1].replace(",", ".")),
        unit: match[2].toLowerCase(),
      }))
      .filter((measurement) => Number.isFinite(measurement.value));
    const labelHasKw = /(^|[^a-z])kw([^a-z]|$)/.test(normalizedLabel);
    const labelHasKm = /(^|[^a-z])km([^a-z]|$)/.test(normalizedLabel);
    const targetUnit = labelHasKw !== labelHasKm ? (labelHasKw ? "kw" : "km") : "";
    let result = targetUnit
      ? measurements.find((measurement) => measurement.unit === targetUnit)?.value
      : measurements[0]?.value;
    if (!Number.isFinite(result) && targetUnit) {
      const source = measurements.find((measurement) => measurement.unit !== targetUnit);
      if (source) {
        result = targetUnit === "km"
          ? source.value * 1.3596216173
          : source.value * 0.73549875;
      }
    }
    
    
    return Number.isFinite(result) ? String(Math.round(result)) : "";
  };
  const yearOf = (value) => {
    const match = String(value || "").match(/(19|20)\d{2}/);
    return match ? match[0] : "";
  };
  const dateToPolish = (value) => {
    const text = String(value || "").trim();
    const iso = text.match(/^(\d{4})-(\d{2})-(\d{2})/);
    if (iso) return `${iso[3]}.${iso[2]}.${iso[1]}`;
    const polish = text.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})/);
    return polish ? polish[0] : "";
  };
  const cleanPesel = digitsOnly(pesel || info.identityNumber).slice(0, 11);
  const birthDateFromPesel = (() => {
    if (cleanPesel.length !== 11) return "";
    let year = Number(cleanPesel.slice(0, 2));
    let month = Number(cleanPesel.slice(2, 4));
    const day = Number(cleanPesel.slice(4, 6));
    if (month >= 81) { year += 1800; month -= 80; }
    else if (month >= 61) { year += 2200; month -= 60; }
    else if (month >= 41) { year += 2100; month -= 40; }
    else if (month >= 21) { year += 2000; month -= 20; }
    else year += 1900;
    if (month < 1 || month > 12 || day < 1 || day > 31) return "";
    return `${String(day).padStart(2, "0")}.${String(month).padStart(2, "0")}.${year}`;
  })();
  const postalCode = String(info.clientPostalCode || "").match(/\b\d{2}-\d{3}\b/)?.[0]
    || String(info.clientAddress || "").match(/\b\d{2}-\d{3}\b/)?.[0]
    || "";
  const licenseDate = dateToPolish(info.driverLicenseDate);
  const licenseYear = yearOf(info.driverLicenseDate);
  
  
  
  const otherPersonPattern = /wspo[lł]w[lł]asciciel|wspo[lł]uzytkownik|wspo[lł]ubezpieczon|ma[lł]zonk|kierowc|najm[lł]odsz|najstarsz/;
  
  
  const maidenNameRejectPattern = new RegExp(`${otherPersonPattern.source}|matki|ojca`);
  
  
  const leadingNumberOf = (value) => digitsOnly(
    String(value || "").trim().match(/^\s*(\d(?:[\d\s\u00a0\u202f.,']*\d)?)/)?.[1] || "",
  );
  const weightOf = (value) => {
    const text = String(value || "").trim();
    const valueBeforeUnit = text.match(/(\d(?:[\d\s\u00a0\u202f.,']*\d)?)\s*kg\b/i)?.[1];
    return digitsOnly(valueBeforeUnit || "") || leadingNumberOf(text);
  };
  return [
    { technical: true, pattern: /(^|[^a-z])vin([^a-z]|$)|numer nadwozia|nr nadwozia/, value: String(vin || info.vin || "").trim() },
    { technical: true, pattern: /przebieg/, reject: /ubezpiecz|szkod/, value: mileageOf(info.mileage) },
    { technical: true, pattern: /(^|[^a-z])marka([^a-z]|$)|marka pojazdu/, reject: /poprzedni/, value: String(info.vehicleMake || "").trim() },
    { technical: true, pattern: /(^|[^a-z])model([^a-z]|$)|model pojazdu/, reject: /poprzedni/, value: String(info.vehicleModel || "").trim() },
    { technical: true, pattern: /data pierwszej rejestracji|pierwszej rejestracji|pierwsza rejestracja/, reject: /polsce/, value: dateToPolish(info.firstRegistrationDate) },
    { pattern: /rok (wydania|uzyskania|zrobienia|otrzymania) prawa jazdy|prawo jazdy.*rok|prawa jazdy.*rok/, reject: otherPersonPattern, value: licenseYear },
    { pattern: /prawa jazdy|prawo jazdy/, reject: otherPersonPattern, value: licenseDate || licenseYear },
    {
      technical: true,
      pattern: /moc silnika|moc pojazdu|moc \(km\)|moc \(kw\)|(^|[^a-z])moc([^a-z]|$)/,
      value: (label) => enginePowerForField(info.enginePower, label),
    },
    { technical: true, pattern: /pojemnosc/, value: digitsOnly(info.engineCapacity) },
    { technical: true, pattern: /rok produkcji|(^|[^a-z])rocznik([^a-z]|$)/, value: yearOf(info.productionYear) },
    { technical: true, pattern: /rodzaj paliwa|typ paliwa|(^|[^a-z])paliw(o|a)([^a-z]|$)/, reject: /zuzyci|spalan|koszt/, value: String(info.fuelType || "").trim() },
    { technical: true, pattern: /liczba miejsc|ilosc miejsc/, value: leadingNumberOf(info.seats) },
    { technical: true, pattern: /(^|[^a-z])dmc([^a-z]|$)|dopuszczalna masa|masa calkowita/, reject: /wlasna/, value: weightOf(info.grossWeight) },
    { technical: true, pattern: /rodzaj pojazdu|typ pojazdu/, value: String(info.vehicleType || "").trim() },
    { pattern: /data urodzenia/, reject: otherPersonPattern, value: birthDateFromPesel },
    { pattern: /nazwisko rodowe|rodowe nazwisko|nazwisko panienskie|panienskie nazwisko/, reject: maidenNameRejectPattern, value: String(info.clientMaidenName || "").trim() },
    { pattern: /(^|[^a-z])pesel([^a-z]|$)/, reject: otherPersonPattern, value: cleanPesel },
    
    
    
    
    { pattern: /imie i nazwisko|nazwisko i imie/, reject: otherPersonPattern, value: String(info.clientName || "").trim() },
    { pattern: /(^|[^a-z])nazwisko([^a-z]|$)/, reject: new RegExp(`${otherPersonPattern.source}|rodowe|panienskie|poprzedni|matki|ojca`), value: String(info.clientLastName || "").trim() },
    { pattern: /(^|[^a-z])imie([^a-z]|$)/, reject: new RegExp(`${otherPersonPattern.source}|drugie|nazwisko`), value: String(info.clientFirstName || "").trim() },
    { technical: true, pattern: /numer rejestracyjny|nr rejestracyjny|tablic rejestracyjnych|tablice rejestracyjne/, reject: /poprzedni/, value: String(info.registration || "").trim() },
    
    
    
    { pattern: /kod pocztowy|^_{2}-_{3}$/, reject: /korespondencyj/, value: postalCode },
  ];
}

export function buildKnownVehicleAnswerResolver(context = {}) {
  return createKnownAnswerFieldResolver(buildKnownVehicleAnswerEntries(context));
}




export function buildVehicleTechnicalAnswerResolver(context = {}) {
  return createKnownAnswerFieldResolver(
    buildKnownVehicleAnswerEntries(context).filter((entry) => entry.technical === true),
  );
}




export function composeKnownAnswerResolvers(...resolvers) {
  const usable = resolvers.filter((resolver) => typeof resolver === "function");
  return (field) => {
    for (const resolver of usable) {
      try {
        const value = resolver(field);
        if (value) return value;
      } catch {
      }
    }
    return "";
  };
}

async function resolveBlockedActionPath({
  page,
  path,
  isComplete,
  requestInput,
  onProgress,
  stageTitle,
  collectFields,
  maxRounds,
  afterStep,
  beforeStep,
  knownAnswerResolver,
}) {
  const collect = typeof collectFields === "function" ? collectFields : collectBlockedFormFields;
  const roundsLimit = Number.isFinite(maxRounds) && maxRounds > 0 ? maxRounds : 8;
  const fieldsSignature = (fields) => fields
    .map((field) => `${field.frameHint}|${field.selector}`)
    .sort()
    .join("\n");
  const replayRemainingSteps = ({ quick = false } = {}) => replayActionPath(page, path, isComplete, {
    
    
    stepTimeout: quick ? 1800 : 6000,
    completionTimeout: quick ? 8000 : 20000,
    skipMissing: true,
    afterStep,
    beforeStep,
  });
  
  
  
  let deadline = Date.now() + 45 * 60 * 1000;
  let answeredSignature = "";
  let emptyReplayStrikes = 0;
  const autoFilledOnce = new Set();
  const comboboxOptionsCache = new Map();
  for (let round = 1; round <= roundsLimit && Date.now() < deadline; round += 1) {
    if (await isComplete()) return true;
    
    
    await waitForUiQuiet(page, { timeout: 8000 });
    
    
    if (await isComplete()) return true;
    let request = await collect(page);
    if (!request.fields.length) {
      await delay(1500);
      request = await collect(page);
    }
    if (!request.fields.length) {
      const replay = await replayRemainingSteps({ quick: true });
      if (replay.success) return true;
      const nextScreen = await collect(page);
      if (!nextScreen.fields.length) {
        emptyReplayStrikes += 1;
        if (emptyReplayStrikes >= 3) return false;
        await delay(1000);
        continue;
      }
      emptyReplayStrikes = 0;
      continue;
    }
    emptyReplayStrikes = 0;
    if (fieldsSignature(request.fields) === answeredSignature) {
      const replay = await replayRemainingSteps();
      if (replay.success) return true;
      request = await collect(page);
      if (!request.fields.length) continue;
      answeredSignature = "";
    }
    if (typeof knownAnswerResolver === "function") {
      
      
      const autoFilledFields = [];
      for (const field of request.fields) {
        
        
        
        
        if (field.kind === "checkbox" || field.keepForUser === true) continue;
        const fieldKey = `${field.frameHint}|${field.selector}`;
        
        
        if (autoFilledOnce.has(fieldKey)) continue;
        const knownValue = knownAnswerResolver(field);
        if (!knownValue) continue;
        if (await fillSingleAnswer(page, field, knownValue, false)) {
          autoFilledFields.push(field);
          autoFilledOnce.add(fieldKey);
        }
        await delay(200);
      }
      if (autoFilledFields.length > 0) {
        onProgress?.({
          status: "running",
          title: "Uzupełnianie znanych danych pojazdu…",
          detail: "",
        });
        const autoFilledSet = new Set(autoFilledFields);
        request = { ...request, fields: request.fields.filter((field) => !autoFilledSet.has(field)) };
        if (request.fields.length === 0) {
          answeredSignature = "";
          await delay(1200);
          continue;
        }
      }
    }
    
    
    if (await isComplete()) return true;
    
    
    
    
    let harvestedCombobox = 0;
    for (const field of request.fields) {
      if (field.kind !== "combobox" || (Array.isArray(field.options) && field.options.length > 0)) continue;
      if (harvestedCombobox >= 8) break;
      const cacheKey = `${field.frameHint}|${field.selector}`;
      if (!comboboxOptionsCache.has(cacheKey)) {
        harvestedCombobox += 1;
        comboboxOptionsCache.set(cacheKey, await harvestComboboxOptions(page, field).catch(() => []));
      }
      const harvestedOptions = comboboxOptionsCache.get(cacheKey);
      if (harvestedOptions.length > 0) field.options = harvestedOptions;
    }
    onProgress?.({
      status: "running",
      title: `Kalkulator zatrzymał się na etapie: ${stageTitle}`,
      detail: "Wykryto brakujące pola. Możesz je uzupełnić w Bazie Polis.",
    });
    
    const askStartedAt = Date.now();
    const answers = await requestInput({ ...request, round });
    deadline += Date.now() - askStartedAt;
    if (!answers || typeof answers !== "object") return false;
    const filledCount = await fillRequestedAnswers(page, request.fields, answers);
    onProgress?.({
      status: "running",
      title: "Uzupełnianie danych…",
      detail: "",
    });
    answeredSignature = fieldsSignature(request.fields);
    await delay(1200);
    
    await waitForUiQuiet(page, { timeout: 10000 });
  }
  return false;
}

export async function learnOrReplayActionPath({
  page,
  savedPath,
  isComplete,
  stageTitle,
  manualInstruction,
  retryInstruction,
  onPathLearned,
  onProgress,
  requestInput,
  collectFields,
  inputMaxRounds,
  afterStep,
  beforeStep,
  knownAnswerResolver,
  allowManual = true,
  timeout = 6 * 60 * 1000,
}) {
  if (await isComplete()) return { learned: false, path: [] };
  let prefix = [];
  const normalizedSavedPath = normalizeNavigationPath(savedPath);
  if (normalizedSavedPath.length > 0) {
    onProgress({
      status: "running",
      title: `Odtwarzanie zapamiętanego etapu: ${stageTitle}…`,
      detail: "",
    });
    
    
    const replayResult = await replayActionPath(page, normalizedSavedPath, isComplete, {
      stepTimeout: allowManual ? 15000 : 12000,
      completionTimeout: 25000,
      afterStep,
      beforeStep,
    });
    if (replayResult.success) return { learned: false, path: normalizedSavedPath };
    prefix = replayResult.completedSteps;
    if (typeof requestInput === "function") {
      const resolvedWithInput = await resolveBlockedActionPath({
        page,
        path: normalizedSavedPath,
        isComplete,
        requestInput,
        onProgress,
        stageTitle,
        collectFields,
        maxRounds: inputMaxRounds,
        afterStep,
        beforeStep,
        knownAnswerResolver,
      });
      if (resolvedWithInput) return { learned: false, path: normalizedSavedPath };
    }
  }

  if (!allowManual) {
    throw manualInteractionRequired(`Zapamiętany etap „${stageTitle}” wymaga ponownego pokazania w widocznym oknie Chrome.`);
  }

  const capturedSteps = [];
  const refreshCapture = await installNavigationCapture(page, (step) => {
    const normalized = normalizeNavigationPath([...capturedSteps, step]);
    capturedSteps.splice(0, capturedSteps.length, ...normalized);
  });
  onProgress({
    status: "action_required",
    title: `Pokaż aplikacji etap: ${stageTitle}`,
    detail: normalizedSavedPath.length > 0 ? retryInstruction : manualInstruction,
  });
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    await refreshCapture();
    if (await isComplete()) {
      await delay(350);
      const learnedPath = normalizeNavigationPath([...prefix, ...capturedSteps]);
      if (learnedPath.length > 0) await onPathLearned?.(learnedPath);
      onProgress({
        status: "running",
        title: `Zapamiętano etap: ${stageTitle}`,
        detail: "Przy następnej kalkulacji aplikacja wykona go automatycznie.",
      });
      return { learned: learnedPath.length > 0, path: learnedPath };
    }
    await delay(400);
  }
  throw new Error(`Nie udało się zakończyć etapu „${stageTitle}” w ciągu ${Math.round(timeout / 60000)} minut.`);
}

export async function learnOrReplayFormPath({
  page,
  savedPath,
  savedFields,
  fields,
  insurerName,
  onPathLearned,
  onDiagnosticsCaptured,
  onProgress,
  ignoreStep,
  manualInstruction,
  requestInput,
  collectFields,
  inputMaxRounds,
  knownAnswerResolver,
  requireManualFinish = false,
  allowManual = true,
  patientReplay = true,
  timeout = 6 * 60 * 1000,
}) {
  let prefix = [];
  const normalizedSavedPath = normalizeNavigationPath(savedPath);
  const normalizedSavedFields = normalizeLearnedFieldDefinitions(savedFields);
  const finishSavedPath = async () => {
    const currentFields = await captureDescribedFields(page, fields, normalizedSavedFields);
    const formFields = currentFields.length > 0 ? currentFields : normalizedSavedFields;
    if (currentFields.length > 0 && JSON.stringify(currentFields) !== JSON.stringify(normalizedSavedFields)) {
      await onPathLearned?.(normalizedSavedPath, currentFields);
    }
    return { learned: false, path: normalizedSavedPath, formFields };
  };
  if (normalizedSavedPath.length > 0 && await hasDescribedFields(page, fields, normalizedSavedFields)) {
    return finishSavedPath();
  }
  if (normalizedSavedPath.length > 0) {
    onProgress({
      status: "running",
      title: `Odtwarzanie zapamiętanej trasy ${insurerName}…`,
      detail: "",
    });
    let replayResult = await replayNavigationPath(page, normalizedSavedPath, fields, normalizedSavedFields, {
      stepTimeout: allowManual ? 15000 : 12000,
      completionTimeout: 25000,
    });
    if (replayResult.success) return finishSavedPath();
    prefix = replayResult.completedSteps;
    
    
    
    
    
    
    if (!allowManual && patientReplay && prefix.length === 0) {
      const retryDeadline = Date.now() + 120000;
      while (prefix.length === 0 && Date.now() < retryDeadline) {
        onProgress({
          status: "running",
          title: `Portal ${insurerName} wolno się otwiera…`,
          detail: "Czekam, aż strona będzie gotowa, i ponawiam zapamiętaną trasę.",
        });
        await waitForUiQuiet(page, { timeout: 10000 });
        replayResult = await replayNavigationPath(page, normalizedSavedPath, fields, normalizedSavedFields, {
          stepTimeout: 25000,
          completionTimeout: 25000,
        });
        if (replayResult.success) return finishSavedPath();
        prefix = replayResult.completedSteps;
      }
    }
    
    
    
    
    
    
    if (typeof requestInput === "function" && !allowManual) {
      
      
      
      const remainingPath = normalizedSavedPath.slice(prefix.length);
      
      
      
      
      const blockedPreview = await (typeof collectFields === "function" ? collectFields : collectBlockedFormFields)(page);
      const looksLikeQuestionScreen = prefix.length > 0
        || blockedPreview.fields.some((field) => field.invalid === true || field.required === true
          || field.kind === "radio" || field.kind === "checkbox");
      if (looksLikeQuestionScreen) {
        const resolvedWithInput = await resolveBlockedActionPath({
          page,
          path: remainingPath,
          isComplete: () => hasDescribedFields(page, fields, normalizedSavedFields),
          requestInput,
          onProgress,
          stageTitle: `Droga do formularza ${insurerName}`,
          collectFields,
          maxRounds: inputMaxRounds,
          knownAnswerResolver,
        });
        if (resolvedWithInput) return finishSavedPath();
        
        if (await hasDescribedFields(page, fields, normalizedSavedFields)) return finishSavedPath();
      }
    }
  }

  if (!allowManual) {
    
    
    
    
    throw manualInteractionRequired(normalizedSavedPath.length > 0 && prefix.length === 0
      ? `Portal ${insurerName} nie doszedł w tle do formularza mimo ponowień. Ponów kalkulację za chwilę albo kliknij „Popraw ręcznie”.`
      : `Zapamiętana trasa ${insurerName} przestała pasować i wymaga widocznego okna Chrome.`);
  }

  const capturedSteps = [];
  const technicalSnapshots = [];
  let recordingActive = false;
  let lastSnapshotStepCount = -1;
  let consecutiveTargetDetections = 0;
  const refreshCapture = await installNavigationCapture(page, (step, captureState) => {
    if (!recordingActive && captureState?.recordingStarted) {
      recordingActive = true;
      capturedSteps.splice(0, capturedSteps.length);
    }
    if (!recordingActive || ignoreStep?.(step)) return;
    const normalized = normalizeNavigationPath([...capturedSteps, step]);
    capturedSteps.splice(0, capturedSteps.length, ...normalized);
  });
  const finishHint = requireManualFinish
    ? "Nagrywanie zakończysz zielonym przyciskiem „Tu jest formularz - zakończ nagrywanie”."
    : "Nagrywanie zakończy się po rozpoznaniu formularza.";
  onProgress({
    status: "action_required",
    title: `Uruchom nagrywanie drogi do formularza ${insurerName}`,
    detail: normalizedSavedPath.length > 0
      ? `Zapamiętana trasa przestała pasować. Kliknij w otwartym oknie „Rozpocznij nagrywanie” i wykonaj wszystkie kroki. ${finishHint}`
      : manualInstruction || `Przed wyborem rekordu kliknij w otwartym oknie „Rozpocznij nagrywanie” i wykonaj wszystkie kroki, w tym Analizę potrzeb Klienta. ${finishHint}`,
  });
  const deadline = Date.now() + timeout;
  let controlError = "";
  while (Date.now() < deadline) {
    await refreshCapture();
    const controlState = await refreshTrainingFinishControl(page, {
      insurerName,
      errorMessage: controlError,
      recordingActive,
    });
    if (controlState.startRequested && !recordingActive) {
      recordingActive = true;
      capturedSteps.splice(0, capturedSteps.length);
      onProgress({
        status: "action_required",
        title: `Nagrywanie trasy ${insurerName} jest aktywne`,
        detail: requireManualFinish
          ? "Wykonaj po kolei wszystkie kliknięcia (w tym Analizę potrzeb Klienta), a na końcu użyj zielonego przycisku „Tu jest formularz - zakończ nagrywanie”."
          : "Wykonaj po kolei wszystkie kliknięcia. Po rozpoznaniu formularza nagrywanie zakończy się automatycznie.",
      });
    }
    if (recordingActive && capturedSteps.length !== lastSnapshotStepCount) {
      const reason = capturedSteps.length === 0 ? "recording-start" : `after-step-${capturedSteps.length}`;
      technicalSnapshots.push(await captureTechnicalPageMap(page, reason));
      lastSnapshotStepCount = capturedSteps.length;
    }
    let targetFormReady = false;
    let automaticFinishRequested = false;
    if (recordingActive) {
      targetFormReady = await hasDescribedFields(page, fields);
      const targetDetection = updateTargetFormDetection(
        consecutiveTargetDetections,
        targetFormReady,
      );
      consecutiveTargetDetections = targetDetection.count;
      automaticFinishRequested = targetDetection.stable && !requireManualFinish;
    }
    if (recordingActive && (controlState.finishRequested || automaticFinishRequested)) {
      if (!targetFormReady) {
        controlError = "Na tym ekranie nie udało się rozpoznać obu pól: numeru rejestracyjnego i PESEL. Przejdź do właściwego formularza i spróbuj ponownie.";
        await resetTrainingFinishControl(page, controlError);
        await delay(400);
        continue;
      }
      await delay(350);
      const learnedPath = normalizeNavigationPath([...prefix, ...capturedSteps]);
      const formFields = await captureDescribedFields(page, fields);
      technicalSnapshots.push(await captureTechnicalPageMap(page, "target-form"));
      await onPathLearned?.(learnedPath, formFields);
      try {
        await onDiagnosticsCaptured?.({
          version: 1,
          insurer: String(insurerName || "").slice(0, 80),
          capturedAt: new Date().toISOString(),
          note: "Mapa techniczna nie zawiera wartości pól, haseł, cookies ani treści zapytań sieciowych.",
          formFields,
          snapshots: technicalSnapshots,
        });
      } catch {
      }
      await removeTrainingFinishControl(page);
      onProgress({
        status: "running",
        title: `Zapamiętano trasę do formularza ${insurerName}`,
        detail: "Przy następnej kalkulacji aplikacja odtworzy ją automatycznie.",
      });
      return { learned: learnedPath.length > 0, path: learnedPath, formFields };
    }
    await delay(400);
  }
  await removeTrainingFinishControl(page);
  throw new Error(`Nie udało się dotrzeć do formularza ${insurerName} w ciągu ${Math.round(timeout / 60000)} minut.`);
}

export async function bringPageWindowToFront(page) {
  if (!page || page.isClosed()) return false;
  try {
    const session = await page.createCDPSession();
    try {
      const { windowId } = await session.send("Browser.getWindowForTarget");
      const { bounds } = await session.send("Browser.getWindowBounds", { windowId });
      const restoredState = bounds?.windowState === "maximized" ? "maximized" : "normal";
      if (bounds?.windowState !== "minimized") {
        await session.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "minimized" } });
        await delay(120);
      }
      await session.send("Browser.setWindowBounds", { windowId, bounds: { windowState: restoredState } });
    } finally {
      await session.detach().catch(() => {});
    }
    await page.bringToFront();
    return true;
  } catch {
    try {
      await page.bringToFront();
      return true;
    } catch {
      return false;
    }
  }
}
