










export const PORTFOLIO_MAPPING_INCOMPLETE_CODE = "PORTFOLIO_MAPPING_INCOMPLETE";
export const PORTFOLIO_MAPPING_FULL_COVERAGE = "full";
export const PORTFOLIO_MAPPING_PDF_BACKLOG_ONLY_COVERAGE = "pdf_backlog_only";
export const PORTFOLIO_EMISSION_FULL_COVERAGE = "full";
export const PORTFOLIO_EMISSION_INCREMENTAL_COVERAGE = "incremental";
export const PORTFOLIO_EMISSION_PDF_BACKLOG_ONLY_COVERAGE = "pdf_backlog_only";

const MAX_MAPPING_COUNT = 1_000_000;
const MAX_REPORT_GROUPS = 64;
const REPORT_CODE_PATTERN = /^[A-Z][A-Z0-9_]{1,63}$/;
const IDENTITY_HASH_PATTERN = /^[a-f0-9]{64}$/;

function normalizeCount(value, fieldName) {
  if (!Number.isSafeInteger(value) || value < 0 || value > MAX_MAPPING_COUNT) {
    throw new TypeError(`${fieldName} musi być nieujemną liczbą całkowitą.`);
  }
  return value;
}

function normalizeGroups(groups, fieldName, { allowIdentityHashes = false } = {}) {
  if (!Array.isArray(groups)) throw new TypeError(`${fieldName} musi być tablicą.`);
  if (groups.length > MAX_REPORT_GROUPS) {
    throw new TypeError(`Raport zawiera zbyt wiele grup ${fieldName}.`);
  }
  const normalized = [];
  const seenCodes = new Set();
  for (const group of groups) {
    const code = String(group?.code || "").trim().toUpperCase();
    if (!REPORT_CODE_PATTERN.test(code) || seenCodes.has(code)) {
      throw new TypeError(`Nieprawidłowy albo powtórzony kod w ${fieldName}.`);
    }
    seenCodes.add(code);
    const count = normalizeCount(group?.count, `${fieldName}.${code}.count`);
    const normalizedGroup = { code, count };
    if (allowIdentityHashes) {
      const identityHashes = Array.isArray(group?.identityHashes)
        ? [...new Set(group.identityHashes.map((value) => String(value || "").trim().toLowerCase()))]
        : [];
      if (
        identityHashes.length > count
        || identityHashes.length > 10_000
        || identityHashes.some((value) => !IDENTITY_HASH_PATTERN.test(value))
      ) {
        throw new TypeError(`Nieprawidłowe skróty tożsamości dla ${code}.`);
      }
      normalizedGroup.identityHashes = Object.freeze(identityHashes);
    }
    normalized.push(Object.freeze(normalizedGroup));
  }
  return Object.freeze(normalized);
}

function normalizeCoverage(value) {
  const coverage = String(value || PORTFOLIO_MAPPING_FULL_COVERAGE).trim().toLowerCase();
  if (
    coverage !== PORTFOLIO_MAPPING_FULL_COVERAGE
    && coverage !== PORTFOLIO_MAPPING_PDF_BACKLOG_ONLY_COVERAGE
  ) {
    throw new TypeError("Nieprawidłowy zakres raportu mapowania.");
  }
  return coverage;
}

function normalizeEmissionCoverage(value) {
  const coverage = String(value || PORTFOLIO_EMISSION_FULL_COVERAGE).trim().toLowerCase();
  if (
    coverage !== PORTFOLIO_EMISSION_FULL_COVERAGE
    && coverage !== PORTFOLIO_EMISSION_INCREMENTAL_COVERAGE
    && coverage !== PORTFOLIO_EMISSION_PDF_BACKLOG_ONLY_COVERAGE
  ) {
    throw new TypeError("Nieprawidłowy zakres raportu emisji.");
  }
  return coverage;
}

function normalizeDateYmd(value) {
  const match = /^(\d{4})-(\d{2})-(\d{2})(?:$|[T\s])/.exec(String(value || "").trim());
  if (!match) return "";
  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  const date = new Date(Date.UTC(year, month - 1, day));
  return date.getUTCFullYear() === year
    && date.getUTCMonth() === month - 1
    && date.getUTCDate() === day
    ? `${match[1]}-${match[2]}-${match[3]}`
    : "";
}

function throwPortfolioEmissionIncomplete() {
  const error = new Error("Nie udało się potwierdzić kompletności rekordów przekazanych do zapisu.");
  error.code = PORTFOLIO_MAPPING_INCOMPLETE_CODE;
  throw error;
}

export function createPortfolioEmissionReport({
  mappedCount,
  freshCount,
  backlogCount,
  beforeCheckpointCount,
  emittedCount,
  coverage = PORTFOLIO_EMISSION_FULL_COVERAGE,
} = {}) {
  const normalizedMappedCount = normalizeCount(mappedCount, "mappedCount");
  const normalizedFreshCount = normalizeCount(freshCount, "freshCount");
  const normalizedBacklogCount = normalizeCount(backlogCount, "backlogCount");
  const normalizedBeforeCheckpointCount = normalizeCount(
    beforeCheckpointCount,
    "beforeCheckpointCount",
  );
  const normalizedEmittedCount = normalizeCount(emittedCount, "emittedCount");
  const normalizedCoverage = normalizeEmissionCoverage(coverage);

  const emittedAccountedCount = normalizedFreshCount + normalizedBacklogCount;
  if (emittedAccountedCount !== normalizedEmittedCount) {
    throw new TypeError("Suma freshCount i backlogCount nie zgadza się z emittedCount.");
  }

  if (normalizedCoverage === PORTFOLIO_EMISSION_PDF_BACKLOG_ONLY_COVERAGE) {
    if (
      normalizedMappedCount !== 0
      || normalizedFreshCount !== 0
      || normalizedBeforeCheckpointCount !== 0
      || normalizedBacklogCount !== normalizedEmittedCount
    ) {
      throw new TypeError("Nieprawidłowy bilans emisji wyłącznie dla zaległych PDF-ów.");
    }
  } else {
    const mappedAccountedCount = emittedAccountedCount + normalizedBeforeCheckpointCount;
    if (mappedAccountedCount !== normalizedMappedCount) {
      throw new TypeError("Bilans emisji nie rozlicza wszystkich zmapowanych rekordów.");
    }
    if (
      normalizedCoverage === PORTFOLIO_EMISSION_FULL_COVERAGE
      && (
        normalizedBacklogCount !== 0
        || normalizedBeforeCheckpointCount !== 0
        || normalizedFreshCount !== normalizedMappedCount
      )
    ) {
      throw new TypeError("Pełny raport emisji musi przekazywać wszystkie zmapowane rekordy.");
    }
  }

  return Object.freeze({
    mappedCount: normalizedMappedCount,
    freshCount: normalizedFreshCount,
    backlogCount: normalizedBacklogCount,
    beforeCheckpointCount: normalizedBeforeCheckpointCount,
    emittedCount: normalizedEmittedCount,
    coverage: normalizedCoverage,
    complete: normalizedCoverage !== PORTFOLIO_EMISSION_PDF_BACKLOG_ONLY_COVERAGE,
  });
}

export function assertPortfolioEmissionReport(report, {
  expectedCoverage = "",
} = {}) {
  let canonical = null;
  try {
    if (!report || typeof report !== "object") throw new TypeError("Brak raportu emisji.");
    for (const fieldName of [
      "mappedCount",
      "freshCount",
      "backlogCount",
      "beforeCheckpointCount",
      "emittedCount",
      "coverage",
      "complete",
    ]) {
      if (!Object.prototype.hasOwnProperty.call(report, fieldName)) {
        throw new TypeError(`Brak pola ${fieldName}.`);
      }
    }
    canonical = createPortfolioEmissionReport({
      mappedCount: report.mappedCount,
      freshCount: report.freshCount,
      backlogCount: report.backlogCount,
      beforeCheckpointCount: report.beforeCheckpointCount,
      emittedCount: report.emittedCount,
      coverage: report.coverage,
    });
  } catch {
    canonical = null;
  }
  const normalizedExpectedCoverage = expectedCoverage
    ? (() => {
        try {
          return normalizeEmissionCoverage(expectedCoverage);
        } catch {
          return "";
        }
      })()
    : "";
  if (
    !canonical
    || report.complete !== canonical.complete
    || report.coverage !== canonical.coverage
    || report.mappedCount !== canonical.mappedCount
    || report.freshCount !== canonical.freshCount
    || report.backlogCount !== canonical.backlogCount
    || report.beforeCheckpointCount !== canonical.beforeCheckpointCount
    || report.emittedCount !== canonical.emittedCount
    || (expectedCoverage && normalizedExpectedCoverage !== canonical.coverage)
  ) {
    throwPortfolioEmissionIncomplete();
  }
  return report;
}

function normalizeFreshnessDates(value) {
  const values = Array.isArray(value) ? value : [value];
  return values.map(normalizeDateYmd).filter(Boolean);
}





export function createPortfolioEmissionResult(mappedPolicies, {
  sinceDate = "",
  sourceAlreadyFilteredToCheckpoint = false,
  getFreshnessDates = (policy) => policy?.issueDate,
  isBacklogPolicy = () => false,
} = {}) {
  if (!Array.isArray(mappedPolicies)) {
    throw new TypeError("mappedPolicies musi być tablicą.");
  }
  const normalizedSinceDate = String(sinceDate || "").trim();
  if (normalizedSinceDate && !normalizeDateYmd(normalizedSinceDate)) {
    throw new TypeError("sinceDate musi być poprawną datą YYYY-MM-DD.");
  }
  if (typeof getFreshnessDates !== "function" || typeof isBacklogPolicy !== "function") {
    throw new TypeError("Klasyfikatory emisji muszą być funkcjami.");
  }

  const fullCoverage = !normalizedSinceDate;
  const freshPolicies = [];
  const backlogPolicies = [];
  let beforeCheckpointCount = 0;

  for (const policy of mappedPolicies) {
    const freshnessDates = fullCoverage || sourceAlreadyFilteredToCheckpoint
      ? []
      : normalizeFreshnessDates(getFreshnessDates(policy));
    
    const isFresh = fullCoverage
      || sourceAlreadyFilteredToCheckpoint
      || freshnessDates.length === 0
      || freshnessDates.some((date) => date >= normalizedSinceDate);
    if (isFresh) {
      freshPolicies.push(policy);
    } else if (isBacklogPolicy(policy) === true) {
      backlogPolicies.push(policy);
    } else {
      beforeCheckpointCount += 1;
    }
  }

  const emittedPolicies = [...freshPolicies, ...backlogPolicies];
  const emissionReport = createPortfolioEmissionReport({
    mappedCount: mappedPolicies.length,
    freshCount: freshPolicies.length,
    backlogCount: backlogPolicies.length,
    beforeCheckpointCount,
    emittedCount: emittedPolicies.length,
    coverage: fullCoverage
      ? PORTFOLIO_EMISSION_FULL_COVERAGE
      : PORTFOLIO_EMISSION_INCREMENTAL_COVERAGE,
  });
  return Object.freeze({ emittedPolicies, emissionReport });
}

export function createPortfolioPdfBacklogOnlyEmissionResult(backlogPolicies) {
  if (!Array.isArray(backlogPolicies)) {
    throw new TypeError("backlogPolicies musi być tablicą.");
  }
  const emittedPolicies = [...backlogPolicies];
  const emissionReport = createPortfolioEmissionReport({
    mappedCount: 0,
    freshCount: 0,
    backlogCount: emittedPolicies.length,
    beforeCheckpointCount: 0,
    emittedCount: emittedPolicies.length,
    coverage: PORTFOLIO_EMISSION_PDF_BACKLOG_ONLY_COVERAGE,
  });
  return Object.freeze({ emittedPolicies, emissionReport });
}

export function createPortfolioMappingReport({
  sourceCount,
  mappedCount,
  filteredCount = 0,
  duplicateCount = 0,
  filters = [],
  rejections = [],
  coverage = PORTFOLIO_MAPPING_FULL_COVERAGE,
} = {}) {
  const normalizedSourceCount = normalizeCount(sourceCount, "sourceCount");
  const normalizedMappedCount = normalizeCount(mappedCount, "mappedCount");
  const normalizedFilteredCount = normalizeCount(filteredCount, "filteredCount");
  const normalizedDuplicateCount = normalizeCount(duplicateCount, "duplicateCount");
  const normalizedFilters = normalizeGroups(filters, "filters");
  const groupedFilteredCount = normalizedFilters.reduce((sum, filter) => sum + filter.count, 0);
  if (groupedFilteredCount !== normalizedFilteredCount) {
    throw new TypeError("Suma grup filtrów nie zgadza się z filteredCount.");
  }
  const normalizedRejections = normalizeGroups(rejections, "rejections", {
    allowIdentityHashes: true,
  });
  const normalizedCoverage = normalizeCoverage(coverage);
  const rejectedCount = normalizedRejections.reduce((sum, rejection) => sum + rejection.count, 0);
  if (rejectedCount > MAX_MAPPING_COUNT) {
    throw new TypeError("Łączna liczba odrzuceń jest zbyt duża.");
  }

  const accountedCount = normalizedMappedCount
    + normalizedFilteredCount
    + normalizedDuplicateCount
    + rejectedCount;
  const unexplainedCount = Math.max(0, normalizedSourceCount - accountedCount);
  const overcount = Math.max(0, accountedCount - normalizedSourceCount);
  const balanced = unexplainedCount === 0 && overcount === 0;

  return Object.freeze({
    sourceCount: normalizedSourceCount,
    mappedCount: normalizedMappedCount,
    filteredCount: normalizedFilteredCount,
    duplicateCount: normalizedDuplicateCount,
    filters: normalizedFilters,
    rejectedCount,
    rejections: normalizedRejections,
    accountedCount,
    unexplainedCount,
    overcount,
    coverage: normalizedCoverage,
    complete: normalizedCoverage === PORTFOLIO_MAPPING_FULL_COVERAGE && balanced,
  });
}



export function assertPortfolioMappingComplete(report) {
  let canonical = null;
  try {
    if (!report || typeof report !== "object") throw new TypeError("Brak raportu.");
    for (const fieldName of [
      "sourceCount",
      "mappedCount",
      "filteredCount",
      "duplicateCount",
      "filters",
      "rejections",
    ]) {
      if (!Object.prototype.hasOwnProperty.call(report, fieldName)) {
        throw new TypeError(`Brak pola ${fieldName}.`);
      }
    }
    canonical = createPortfolioMappingReport({
      sourceCount: report.sourceCount,
      mappedCount: report.mappedCount,
      filteredCount: report.filteredCount,
      duplicateCount: report.duplicateCount,
      filters: report.filters || [],
      rejections: report.rejections,
      coverage: report.coverage || PORTFOLIO_MAPPING_FULL_COVERAGE,
    });
  } catch {
    canonical = null;
  }
  if (
    !canonical
    || canonical.complete !== true
    || report.complete !== true
    || report.accountedCount !== canonical.accountedCount
    || report.unexplainedCount !== canonical.unexplainedCount
    || report.overcount !== canonical.overcount
    || report.rejectedCount !== canonical.rejectedCount
  ) {
    const error = new Error("Nie udało się potwierdzić kompletności mapowania portfela.");
    error.code = PORTFOLIO_MAPPING_INCOMPLETE_CODE;
    error.sourceCount = Number.isSafeInteger(canonical?.sourceCount) ? canonical.sourceCount : null;
    error.accountedCount = Number.isSafeInteger(canonical?.accountedCount)
      ? canonical.accountedCount
      : null;
    error.unexplainedCount = Number.isSafeInteger(canonical?.unexplainedCount)
      ? canonical.unexplainedCount
      : null;
    throw error;
  }
  return report;
}

export function getPortfolioMappingRejectionCount(report) {
  return Number.isSafeInteger(report?.rejectedCount) && report.rejectedCount >= 0
    ? report.rejectedCount
    : 0;
}



export function createPortfolioMappingCollector(sourceCount) {
  const normalizedSourceCount = normalizeCount(sourceCount, "sourceCount");
  let mappedCount = 0;
  let duplicateCount = 0;
  const filtersByCode = new Map();
  const rejectionsByCode = new Map();

  function addCount(current, amount, fieldName) {
    return normalizeCount(current + normalizeCount(amount, fieldName), fieldName);
  }

  function recordMapped(amount = 1) {
    mappedCount = addCount(mappedCount, amount, "mappedCount");
  }

  function recordFiltered(code, amount = 1) {
    const normalizedCode = String(code || "").trim().toUpperCase();
    if (!REPORT_CODE_PATTERN.test(normalizedCode)) {
      throw new TypeError("Nieprawidłowy kod filtra.");
    }
    filtersByCode.set(
      normalizedCode,
      addCount(
        filtersByCode.get(normalizedCode) || 0,
        amount,
        `filters.${normalizedCode}.count`,
      ),
    );
  }

  function recordDuplicate(amount = 1) {
    duplicateCount = addCount(duplicateCount, amount, "duplicateCount");
  }

  function recordRejected(code, { identityHash = "", count = 1 } = {}) {
    const normalizedCode = String(code || "").trim().toUpperCase();
    if (!REPORT_CODE_PATTERN.test(normalizedCode)) {
      throw new TypeError("Nieprawidłowy kod odrzucenia.");
    }
    const normalizedHash = String(identityHash || "").trim().toLowerCase();
    if (normalizedHash && !IDENTITY_HASH_PATTERN.test(normalizedHash)) {
      throw new TypeError("Nieprawidłowy skrót tożsamości odrzucenia.");
    }
    const current = rejectionsByCode.get(normalizedCode)
      || { count: 0, identityHashes: new Set() };
    current.count = addCount(
      current.count,
      count,
      `rejections.${normalizedCode}.count`,
    );
    if (normalizedHash) current.identityHashes.add(normalizedHash);
    rejectionsByCode.set(normalizedCode, current);
  }

  function createReport({ coverage = PORTFOLIO_MAPPING_FULL_COVERAGE } = {}) {
    const filters = [...filtersByCode].map(([code, count]) => ({ code, count }));
    const rejections = [...rejectionsByCode].map(([code, group]) => ({
      code,
      count: group.count,
      identityHashes: [...group.identityHashes],
    }));
    return createPortfolioMappingReport({
      sourceCount: normalizedSourceCount,
      mappedCount,
      filteredCount: filters.reduce((sum, group) => sum + group.count, 0),
      duplicateCount,
      filters,
      rejections,
      coverage,
    });
  }

  return Object.freeze({
    sourceCount: normalizedSourceCount,
    recordMapped,
    recordFiltered,
    recordDuplicate,
    recordRejected,
    createReport,
  });
}

export function createPortfolioPdfBacklogOnlyMappingReport() {
  return createPortfolioMappingReport({
    sourceCount: 0,
    mappedCount: 0,
    filteredCount: 0,
    duplicateCount: 0,
    rejections: [],
    coverage: PORTFOLIO_MAPPING_PDF_BACKLOG_ONLY_COVERAGE,
  });
}

export function assertPortfolioPdfBacklogOnlyMappingReport(report) {
  const canonical = createPortfolioPdfBacklogOnlyMappingReport();
  if (
    !report
    || report.coverage !== canonical.coverage
    || report.complete !== false
    || report.sourceCount !== 0
    || report.mappedCount !== 0
    || report.filteredCount !== 0
    || report.duplicateCount !== 0
    || report.rejectedCount !== 0
    || report.accountedCount !== 0
    || report.unexplainedCount !== 0
    || report.overcount !== 0
    || !Array.isArray(report.filters)
    || report.filters.length !== 0
    || !Array.isArray(report.rejections)
    || report.rejections.length !== 0
  ) {
    const error = new Error("Nie udało się potwierdzić zakresu pobierania zaległych PDF-ów.");
    error.code = PORTFOLIO_MAPPING_INCOMPLETE_CODE;
    throw error;
  }
  return report;
}
