
















import { inflateRawSync } from "node:zlib";

import { resolveErgoInsurerByPolicyNumber } from "./duymgadbtbrrvujgbqpsldobsrpsttffkvpiesdclzken.mjs";
import { createPortfolioIssueIdentityHash } from "./xombeztlyiegtybxtzkg.mjs";

const INSLY_HOST = "superpolisaia.insly.pl";
export const INSLY_ORIGIN = `https://${INSLY_HOST}`;
export const INSLY_DASHBOARD_URL = `${INSLY_ORIGIN}/dashboard`;
export const INSLY_WORKBOOK_INVALID_CODE = "INSLY_WORKBOOK_INVALID";
export const INSLY_POLICY_ROW_INVALID_CODE = "INSLY_POLICY_ROW_INVALID";

const INSLY_MAX_WORKBOOK_BYTES = 32 * 1024 * 1024;
const INSLY_MAX_ZIP_ENTRIES = 2048;
const INSLY_MAX_XML_ENTRY_BYTES = 24 * 1024 * 1024;
const INSLY_CRC_TABLE = (() => {
  const table = new Uint32Array(256);
  for (let index = 0; index < 256; index += 1) {
    let value = index;
    for (let bit = 0; bit < 8; bit += 1) {
      value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
    }
    table[index] = value >>> 0;
  }
  return table;
})();

function invalidInslyWorkbook() {
  const error = new Error("Eksport polis z Superpolisy jest nieprawidłowy lub niekompletny. Pobierz listę ponownie.");
  error.code = INSLY_WORKBOOK_INVALID_CODE;
  return error;
}

function invalidInslyPolicyRow() {
  const error = new Error("Eksport polis z Superpolisy zawiera niekompletny rekord polisy. Import przerwano.");
  error.code = INSLY_POLICY_ROW_INVALID_CODE;
  return error;
}

function calculateCrc32(buffer) {
  let crc = 0xffffffff;
  for (const byte of buffer) crc = INSLY_CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
  return (crc ^ 0xffffffff) >>> 0;
}

function compactText(value) {
  return String(value == null ? "" : value).replace(/\s+/g, " ").trim();
}

export function isAuthenticatedInslyUrl(value) {
  try {
    const url = new URL(value);
    if (url.hostname.toLowerCase() !== INSLY_HOST) return false;
    return !/login|logout|password|auth/i.test(url.pathname);
  } catch {
    return false;
  }
}

export function isOnInslyPortal(page) {
  try {
    return new URL(page.url()).hostname.toLowerCase() === INSLY_HOST;
  } catch {
    return false;
  }
}



function findZipEntry(buffer, wantedName) {
  
  const minOffset = Math.max(0, buffer.length - 65557);
  let eocdOffset = -1;
  for (let offset = buffer.length - 22; offset >= minOffset; offset -= 1) {
    if (buffer.readUInt32LE(offset) === 0x06054b50) {
      eocdOffset = offset;
      break;
    }
  }
  if (eocdOffset < 0 || eocdOffset + 22 > buffer.length) throw invalidInslyWorkbook();
  const entryCount = buffer.readUInt16LE(eocdOffset + 10);
  if (entryCount < 1 || entryCount > INSLY_MAX_ZIP_ENTRIES) throw invalidInslyWorkbook();
  let cursor = buffer.readUInt32LE(eocdOffset + 16);
  for (let index = 0; index < entryCount; index += 1) {
    if (cursor + 46 > buffer.length || buffer.readUInt32LE(cursor) !== 0x02014b50) throw invalidInslyWorkbook();
    const nameLength = buffer.readUInt16LE(cursor + 28);
    const extraLength = buffer.readUInt16LE(cursor + 30);
    const commentLength = buffer.readUInt16LE(cursor + 32);
    const nextCursor = cursor + 46 + nameLength + extraLength + commentLength;
    if (nextCursor > buffer.length) throw invalidInslyWorkbook();
    const name = buffer.toString("utf8", cursor + 46, cursor + 46 + nameLength);
    if (name === wantedName) {
      return {
        method: buffer.readUInt16LE(cursor + 10),
        crc32: buffer.readUInt32LE(cursor + 16),
        compressedSize: buffer.readUInt32LE(cursor + 20),
        uncompressedSize: buffer.readUInt32LE(cursor + 24),
        localHeaderOffset: buffer.readUInt32LE(cursor + 42),
      };
    }
    cursor = nextCursor;
  }
  return null;
}

function readZipEntryText(buffer, wantedName, { required = false } = {}) {
  const entry = findZipEntry(buffer, wantedName);
  if (!entry) {
    if (required) throw invalidInslyWorkbook();
    return "";
  }
  if (![0, 8].includes(entry.method)
    || entry.compressedSize > INSLY_MAX_WORKBOOK_BYTES
    || entry.uncompressedSize > INSLY_MAX_XML_ENTRY_BYTES) {
    throw invalidInslyWorkbook();
  }
  const headerOffset = entry.localHeaderOffset;
  if (headerOffset + 30 > buffer.length || buffer.readUInt32LE(headerOffset) !== 0x04034b50) {
    throw invalidInslyWorkbook();
  }
  const nameLength = buffer.readUInt16LE(headerOffset + 26);
  const extraLength = buffer.readUInt16LE(headerOffset + 28);
  const dataStart = headerOffset + 30 + nameLength + extraLength;
  if (dataStart > buffer.length || dataStart + entry.compressedSize > buffer.length) throw invalidInslyWorkbook();
  const data = buffer.subarray(dataStart, dataStart + entry.compressedSize);
  try {
    const bytes = entry.method === 0
      ? data
      : inflateRawSync(data, { maxOutputLength: INSLY_MAX_XML_ENTRY_BYTES });
    if (bytes.length !== entry.uncompressedSize
      || bytes.length > INSLY_MAX_XML_ENTRY_BYTES
      || calculateCrc32(bytes) !== entry.crc32) {
      throw invalidInslyWorkbook();
    }
    return bytes.toString("utf8");
  } catch (error) {
    if (error?.code === INSLY_WORKBOOK_INVALID_CODE) throw error;
    throw invalidInslyWorkbook();
  }
}

function decodeXmlEntities(value) {
  return String(value || "")
    .replace(/&#x([0-9a-f]+);/gi, (match, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
    .replace(/&#(\d+);/g, (match, code) => String.fromCodePoint(Number(code)))
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">")
    .replace(/&amp;/g, "&");
}

function parseSharedStrings(xml) {
  
  return [...xml.matchAll(/<si>([\s\S]*?)<\/si>/g)].map((match) => decodeXmlEntities(
    [...match[1].matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((run) => run[1]).join(""),
  ));
}

function columnLetterToIndex(letters) {
  let index = 0;
  for (const letter of letters) index = index * 26 + (letter.charCodeAt(0) - 64);
  return index - 1;
}



function parseWorksheetRows(xml, sharedStrings) {
  const rows = [];
  for (const rowMatch of xml.matchAll(/<row[^>]*>([\s\S]*?)<\/row>/g)) {
    const cells = [];
    for (const cellMatch of rowMatch[1].matchAll(/<c\s([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/g)) {
      const attributes = cellMatch[1] || "";
      const body = cellMatch[2] || "";
      const reference = /r="([A-Z]+)\d+"/.exec(attributes);
      if (!reference) continue;
      const columnIndex = columnLetterToIndex(reference[1]);
      const type = /t="([a-zA-Z]+)"/.exec(attributes)?.[1] || "n";
      let value = null;
      if (type === "inlineStr") {
        value = decodeXmlEntities([...body.matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((run) => run[1]).join(""));
      } else {
        const rawValue = /<v>([\s\S]*?)<\/v>/.exec(body)?.[1];
        if (rawValue === undefined) value = null;
        else if (type === "s") value = sharedStrings[Number(rawValue)] ?? "";
        else value = decodeXmlEntities(rawValue);
      }
      if (value !== null) cells[columnIndex] = { type, value };
    }
    rows.push(cells);
  }
  return rows;
}



export function parseInslyWorkbookPolicies(buffer) {
  if (!Buffer.isBuffer(buffer)
    || buffer.length < 100
    || buffer.length > INSLY_MAX_WORKBOOK_BYTES
    || buffer.readUInt32LE(0) !== 0x04034b50) {
    throw invalidInslyWorkbook();
  }
  const contentTypesXml = readZipEntryText(buffer, "[Content_Types].xml", { required: true });
  const workbookXml = readZipEntryText(buffer, "xl/workbook.xml", { required: true });
  if (!/<Types\b/i.test(contentTypesXml) || !/<workbook\b/i.test(workbookXml)) {
    throw invalidInslyWorkbook();
  }
  const sharedStringsXml = readZipEntryText(buffer, "xl/sharedStrings.xml");
  const sharedStrings = parseSharedStrings(sharedStringsXml);
  const sheetXml = readZipEntryText(buffer, "xl/worksheets/sheet1.xml", { required: true });
  if (!/<worksheet\b/i.test(sheetXml) || !/<sheetData\b/i.test(sheetXml)) throw invalidInslyWorkbook();
  const rows = parseWorksheetRows(sheetXml, sharedStrings);
  const headerRowIndex = rows.findIndex((cells) => {
    const texts = cells.filter(Boolean).map((cell) => normalizeForMatching(cell.value));
    return texts.includes("polisa")
      && texts.some((text) => text.startsWith("towarzystwo ubezpieczen"))
      && texts.includes("klient")
      && texts.includes("okres ubezpieczenia");
  });
  if (headerRowIndex < 0) throw invalidInslyWorkbook();
  const headers = rows[headerRowIndex].map((cell) => (cell ? compactText(cell.value) : ""));
  const records = [];
  for (const cells of rows.slice(headerRowIndex + 1)) {
    const record = {};
    let hasValue = false;
    headers.forEach((header, columnIndex) => {
      if (!header || !cells[columnIndex]) return;
      record[header] = cells[columnIndex];
      if (compactText(cells[columnIndex].value)) hasValue = true;
    });
    if (hasValue) records.push(record);
  }
  return records;
}






const INSLY_INSURER_KEYWORDS = [
  ["ergo hestia", "Hestia"],
  ["hestia", "Hestia"],
  ["you can drive", "You Can Drive"],
  ["mtu", "MTU"],
  ["pzu", "PZU"],
  ["warta", "Warta"],
  ["hdi", "HDI"],
  ["generali", "Generali"],
  ["proama", "Proama"],
  ["wiener", "Wiener"],
  ["uniqa", "Uniqa"],
  ["compensa", "Compensa"],
  ["interrisk", "Interrisk"],
  ["inter risk", "Interrisk"],
  ["link4", "Link4"],
  ["link 4", "Link4"],
  ["trasti", "Trasti"],
  ["tuz", "TUZ"],
  ["tuw", "TUW"],
  ["balcia", "Balcia"],
  ["euroins", "Euroins"],
  ["allianz", "Allianz"],
  ["axa", "AXA Partners"],
  ["agro", "Agro Ubezpieczenia"],
  ["ominimo", "Ominimo"],
  ["direct", "Direct"],
];

function normalizeForMatching(value) {
  return compactText(value)
    .toLowerCase()
    .normalize("NFD")
    .replace(/[̀-ͯ]/g, "")
    .replace(/[^a-z0-9]+/g, " ")
    .trim();
}

export function mapInslyInsurerName(rawName) {
  const normalized = normalizeForMatching(rawName);
  if (!normalized) return "";
  const padded = ` ${normalized} `;
  for (const [keyword, canonicalName] of INSLY_INSURER_KEYWORDS) {
    if (padded.includes(` ${keyword} `)) return canonicalName;
  }
  
  
  let cleaned = compactText(rawName);
  for (let round = 0; round < 4; round += 1) {
    const shorter = cleaned.replace(/\s+(?:TUW?|TU|S\.?A\.?|Towarzystwo Ubezpiecze[nń][^,]*|Sp\.? z o\.?o\.?)\.?$/i, "");
    if (shorter === cleaned) break;
    cleaned = shorter;
  }
  return cleaned.slice(0, 80);
}

function cellText(cell) {
  return cell ? compactText(cell.value) : "";
}


function excelSerialToIsoDate(serialValue) {
  const serial = Number(serialValue);
  if (!Number.isFinite(serial) || serial < 60 || serial > 200000) return "";
  return new Date(Math.round((serial - 25569) * 86400000)).toISOString().slice(0, 10);
}

function polishDateToIso(value) {
  const match = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(compactText(value));
  return match ? `${match[3]}-${match[2]}-${match[1]}` : "";
}


function cellDateToIso(cell) {
  if (!cell) return "";
  const text = cellText(cell);
  return polishDateToIso(text) || excelSerialToIsoDate(text) || "";
}

function parseInsurancePeriod(cell) {
  const parts = cellText(cell).split(/\s*[-–]\s*/);
  return {
    validFrom: polishDateToIso(parts[0]) || "",
    validUntil: polishDateToIso(parts[1]) || "",
  };
}

function formatInslyPremium(cell) {
  if (!cell) return "";
  const raw = cellText(cell).replace(/\s/g, "").replace(",", ".");
  const value = Number(raw);
  if (!Number.isFinite(value) || value <= 0) return "";
  const fixed = value.toFixed(2);
  return fixed.endsWith(".00") ? fixed.slice(0, -3) : fixed.replace(".", ",");
}

function peselChecksumValid(pesel) {
  if (!/^\d{11}$/.test(pesel)) return false;
  const weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3];
  const sum = weights.reduce((total, weight, index) => total + weight * Number(pesel[index]), 0);
  return (10 - (sum % 10)) % 10 === Number(pesel[10]);
}



export function normalizeInslyIdentityNumber(cell) {
  const digits = cellText(cell).replace(/\D/g, "");
  if (!digits) return "";
  if (cell?.type === "n" && digits.length === 10 && peselChecksumValid(`0${digits}`)) return `0${digits}`;
  return digits.slice(0, 14);
}



const INSLY_REGISTRATION_PATTERN = /^[A-Z][A-Z0-9]{3,9}$/;

function looksLikeRegistrationPlate(value) {
  return INSLY_REGISTRATION_PATTERN.test(value) && /\d/.test(value);
}




export function parseInslyInsuranceSubject(rawValue) {
  const raw = compactText(rawValue);
  const separatorIndex = raw.indexOf(" / ");
  const productLabel = separatorIndex >= 0 ? raw.slice(0, separatorIndex) : raw;
  const subject = separatorIndex >= 0 ? raw.slice(separatorIndex + 3) : "";
  const subjectParts = subject.split(",").map((part) => compactText(part)).filter(Boolean);
  const plateCandidate = (subjectParts[0] || "").replace(/\s+/g, "").toUpperCase();
  if (looksLikeRegistrationPlate(plateCandidate)) {
    return {
      registration: plateCandidate,
      vehicleMake: (subjectParts[1] || "").slice(0, 80),
      vehicleModel: subjectParts.slice(2).join(", ").slice(0, 120),
      clientAddress: "",
    };
  }
  const normalizedProduct = normalizeForMatching(productLabel);
  let registration = "Inne";
  if (/komunikacyj|moto|pojazd|oc ppm|autocasco/.test(normalizedProduct)) registration = "Auto";
  else if (/mieszka|dom|majat|nieruchomo/.test(normalizedProduct)) registration = "Mieszkanie";
  else if (/podroz|turyst|travel/.test(normalizedProduct)) registration = "Podróż";
  else if (/zycie|life/.test(normalizedProduct)) registration = "Życie";
  else if (/firm|biznes|dzialalno/.test(normalizedProduct)) registration = "Firma";
  else if (/rolne|rolnik|agro|gospodarstw/.test(normalizedProduct)) registration = "Rolne";
  else if (/nnw/.test(normalizedProduct)) registration = "NNW";
  
  const looksLikeAddress = /\d{2}-\d{3}/.test(subject) || /ul\.|ulica|os\.|al\./i.test(subject);
  return {
    registration,
    vehicleMake: "",
    vehicleModel: "",
    clientAddress: looksLikeAddress ? subject.slice(0, 160) : "",
  };
}

function buildInslyContact(row) {
  const lines = [];
  const mobilePhone = cellText(row["Telefon komórkowy"]);
  const landlinePhone = cellText(row["Telefon stacjonarny"]);
  if (mobilePhone) lines.push(`Telefon: ${mobilePhone}`);
  if (landlinePhone && landlinePhone !== mobilePhone) lines.push(`Telefon stacjonarny: ${landlinePhone}`);
  const note = cellText(row["Notatka"]);
  if (note) lines.push(note.slice(0, 200));
  return lines.join("\n").slice(0, 400);
}

const INSLY_SKIPPED_STATUSES = /anulowan|storno|wypowiedzian|odst[aą]pi/i;





export function mapInslyPolicyRows(rows, {
  minEndDate = "",
  mappingCollector = null,
} = {}) {
  const records = [];
  const seen = new Set();
  for (const row of Array.isArray(rows) ? rows : []) {
    const policyNumber = cellText(row["Polisa"]);
    const clientName = cellText(row["Klient"]);
    const status = cellText(row["Status"]);
    if (INSLY_SKIPPED_STATUSES.test(status)) {
      mappingCollector?.recordFiltered("CANCELLED_POLICY");
      continue;
    }
    const rawInsurer = cellText(row["Towarzystwo Ubezpieczeń"]);
    const mappedInsurer = mapInslyInsurerName(rawInsurer);
    
    
    const insurer = mappedInsurer === "Hestia" || mappedInsurer === "MTU"
      ? resolveErgoInsurerByPolicyNumber(policyNumber, mappedInsurer)
      : mappedInsurer;
    const { validFrom, validUntil } = parseInsurancePeriod(row["Okres ubezpieczenia"]);
    if (!policyNumber && !clientName && !insurer) {
      mappingCollector?.recordRejected("MISSING_REQUIRED_FIELDS");
      continue;
    }
    const normalizedPolicyNumber = normalizeInslyPolicyKey(policyNumber);
    if (!normalizedPolicyNumber || !clientName || !insurer) {
      if (!mappingCollector) throw invalidInslyPolicyRow();
      let identityHash = "";
      try {
        identityHash = createPortfolioIssueIdentityHash({
          insurer: insurer || mappedInsurer || rawInsurer,
          policyNumber,
          validFrom,
          validUntil,
        });
      } catch {
      }
      mappingCollector.recordRejected("MISSING_REQUIRED_FIELDS", { identityHash });
      continue;
    }
    if (minEndDate && validUntil && validUntil < minEndDate) {
      mappingCollector?.recordFiltered("OUTSIDE_DATE_RANGE");
      continue;
    }
    const key = `${normalizeForMatching(insurer)}\0${normalizedPolicyNumber}\0${validFrom}\0${validUntil}`;
    if (seen.has(key)) {
      mappingCollector?.recordDuplicate();
      continue;
    }
    seen.add(key);
    const subject = parseInslyInsuranceSubject(cellText(row["Przedmiot ubezpieczenia"]));

    records.push({
      insurer,
      policyNumber,
      clientName: clientName.slice(0, 160),
      identityNumber: normalizeInslyIdentityNumber(row["PESEL/REGON"]),
      contact: buildInslyContact(row),
      registration: subject.registration,
      vehicleMake: subject.vehicleMake,
      vehicleModel: subject.vehicleModel,
      clientAddress: subject.clientAddress,
      validFrom,
      validUntil,
      premium: formatInslyPremium(row["Składka"]),
      issueDate: cellDateToIso(row["Data wystawienia"]),
      addedDate: cellDateToIso(row["Data wprowadzenia"]),
    });
    mappingCollector?.recordMapped();
  }
  return records;
}




export function filterInslyRecordsSince(records, sinceDate) {
  const normalizedSince = compactText(sinceDate);
  if (!/^\d{4}-\d{2}-\d{2}$/.test(normalizedSince)) return Array.isArray(records) ? [...records] : [];
  return (Array.isArray(records) ? records : []).filter((record) => {
    if (!record.issueDate && !record.addedDate) return true;
    return (record.issueDate && record.issueDate >= normalizedSince)
      || (record.addedDate && record.addedDate >= normalizedSince);
  });
}







export function extractInslyAjaxHtml(text) {
  const raw = String(text || "");
  const trimmed = raw.trim();
  if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return raw;
  try {
    const parsed = JSON.parse(trimmed);
    if (typeof parsed === "string") return parsed;
    if (parsed && !Array.isArray(parsed) && typeof parsed === "object"
      && typeof parsed.content === "string") return parsed.content;
  } catch {
  }
  return "";
}

function stripHtmlTags(value) {
  return compactText(decodeXmlEntities(String(value || "").replace(/<[^>]*>/g, " ")));
}

export function normalizeInslyPolicyKey(value) {
  return String(value || "").replace(/[^0-9A-Za-z]/g, "").toUpperCase();
}

export function makeInslyPolicyRecordKey(policy) {
  return `${normalizeForMatching(policy?.insurer)}\0${normalizeInslyPolicyKey(policy?.policyNumber)}`;
}



export function findInslyAmbiguousPdfPolicyNumbers(policies, oidMap = null) {
  const counts = new Map();
  for (const policy of Array.isArray(policies) ? policies : []) {
    const numberKey = normalizeInslyPolicyKey(policy?.policyNumber);
    if (!numberKey) continue;
    counts.set(numberKey, (counts.get(numberKey) || 0) + 1);
  }
  const ambiguous = new Set([...counts].filter(([, count]) => count > 1).map(([numberKey]) => numberKey));
  if (oidMap instanceof Map) {
    for (const [numberKey, oid] of oidMap) {
      if (oid === null && counts.has(numberKey)) ambiguous.add(numberKey);
    }
  }
  return ambiguous;
}



export function parseInslyPolicyOidMap(html, targetMap = new Map()) {
  for (const match of String(html || "").matchAll(/<a\b[^>]*href="[^"]*\/policy\/view\/policy=(\d+)\/?[^"]*"[^>]*>([\s\S]*?)<\/a>/gi)) {
    const oid = match[1];
    const numberKey = normalizeInslyPolicyKey(stripHtmlTags(match[2]));
    if (!oid || numberKey.length < 3) continue;
    if (!targetMap.has(numberKey)) {
      targetMap.set(numberKey, oid);
    } else if (targetMap.get(numberKey) !== oid) {
      
      
      targetMap.set(numberKey, null);
    }
  }
  return targetMap;
}



export function parseInslyListPageLinks(html) {
  const paths = [];
  const seen = new Set();
  for (const match of String(html || "").matchAll(/href="((?:https?:\/\/[^"/]+)?(\/policy\/[^"]*(?:page|offset|start)=\d+[^"]*))"/gi)) {
    const path = decodeXmlEntities(match[2]);
    if (!seen.has(path)) {
      seen.add(path);
      paths.push(path);
    }
  }
  return paths.slice(0, 80);
}

function normalizeInslyDocumentPath(rawHref) {
  try {
    const url = new URL(decodeXmlEntities(rawHref), INSLY_ORIGIN);
    if (url.origin !== INSLY_ORIGIN || !url.pathname.startsWith("/document/get/")) return "";
    return `${url.pathname}${url.search}`;
  } catch {
    return "";
  }
}



export function pickInslyPolicyDocumentPath(fragmentHtml) {
  const html = String(fragmentHtml || "");
  const rowPattern = /<tr[\s\S]*?<\/tr>/gi;
  const policyRows = [];
  for (const rowMatch of html.match(rowPattern) || []) {
    const cellTexts = [...rowMatch.matchAll(/<t[dh]\b[^>]*>([\s\S]*?)<\/t[dh]>/gi)]
      .map((match) => normalizeForMatching(stripHtmlTags(match[1])));
    if (!cellTexts.includes("polisa")) continue;
    const paths = new Set(
      [...rowMatch.matchAll(/href="([^"]+)"/gi)]
        .map((match) => normalizeInslyDocumentPath(match[1]))
        .filter(Boolean),
    );
    policyRows.push(paths.size === 1 ? [...paths][0] : "");
  }
  return policyRows.length === 1 ? policyRows[0] : "";
}
