






import { createHash } from "node:crypto";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { dirname } from "node:path";

const MAX_STATE_KEYS = 200;
const MAX_ISSUES_PER_KEY = 10_000;
const STATE_KEY_PATTERN = /^[a-z0-9][a-z0-9:_-]{0,119}$/;
const ISSUE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{1,63}$/;
const ISSUE_HASH_PATTERN = /^[a-f0-9]{64}$/;

function normalizeStateKey(value) {
  const key = String(value || "").trim().toLowerCase();
  if (!STATE_KEY_PATTERN.test(key)) throw new TypeError("Nieprawidłowy klucz portfela.");
  return key;
}

function normalizeIssue(issue) {
  const identityHash = String(issue?.identityHash || "").trim().toLowerCase();
  const code = String(issue?.code || "").trim().toUpperCase();
  if (!ISSUE_HASH_PATTERN.test(identityHash) || !ISSUE_CODE_PATTERN.test(code)) {
    throw new TypeError("Nieprawidłowa pozycja kolejki wyjaśnienia.");
  }
  return { identityHash, code };
}

function normalizeIdentityDate(value) {
  const text = String(value || "").trim().slice(0, 10);
  const polish = text.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
  return polish ? `${polish[3]}-${polish[2]}-${polish[1]}` : text;
}

function normalizeStoredState(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new TypeError("Nieprawidłowy dokument kolejki wyjaśnienia.");
  }
  const stateEntries = Object.entries(value);
  if (stateEntries.length > MAX_STATE_KEYS) {
    throw new TypeError("Kolejka wyjaśnienia zawiera zbyt wiele portfeli.");
  }
  const result = {};
  for (const [rawKey, rawSection] of stateEntries) {
    const key = normalizeStateKey(rawKey);
    if (
      !rawSection
      || typeof rawSection !== "object"
      || Array.isArray(rawSection)
      || !Array.isArray(rawSection.issues)
      || rawSection.issues.length > MAX_ISSUES_PER_KEY
    ) {
      throw new TypeError("Nieprawidłowa sekcja kolejki wyjaśnienia.");
    }
    const normalizedIssues = [];
    const seen = new Set();
    for (const rawIssue of rawSection.issues) {
      const issue = normalizeIssue(rawIssue);
      const uniqueKey = `${issue.identityHash}:${issue.code}`;
      if (seen.has(uniqueKey)) continue;
      seen.add(uniqueKey);
      normalizedIssues.push({
        ...issue,
        firstSeenAt: typeof rawIssue?.firstSeenAt === "string" ? rawIssue.firstSeenAt : "",
        lastSeenAt: typeof rawIssue?.lastSeenAt === "string" ? rawIssue.lastSeenAt : "",
        occurrences: Number.isSafeInteger(rawIssue?.occurrences) && rawIssue.occurrences > 0
          ? Math.min(1_000_000, rawIssue.occurrences)
          : 1,
      });
    }
    if (normalizedIssues.length > 0) result[key] = { issues: normalizedIssues };
  }
  return result;
}

export function createPortfolioIssueIdentityHash({
  insurer = "",
  sourceId = "",
  policyNumber = "",
  validFrom = "",
  validUntil = "",
} = {}) {
  const normalizedInsurer = String(insurer || "").trim().toLocaleLowerCase("pl");
  const normalizedSourceId = String(sourceId || "").trim();
  const normalizedPolicyNumber = String(policyNumber || "")
    .trim()
    .toLocaleUpperCase("pl")
    .replace(/[^0-9A-Z]/g, "");
  const normalizedValidFrom = normalizeIdentityDate(validFrom);
  const normalizedValidUntil = normalizeIdentityDate(validUntil);
  if (!normalizedInsurer || (!normalizedSourceId && !normalizedPolicyNumber)) {
    throw new TypeError("Nie można zbudować tożsamości rekordu bez TU i identyfikatora.");
  }
  return createHash("sha256")
    .update([
      normalizedInsurer,
      normalizedSourceId,
      normalizedPolicyNumber,
      normalizedValidFrom,
      normalizedValidUntil,
    ].join("\0"), "utf8")
    .digest("hex");
}

export function createPortfolioImportReviewStore({ storagePath }) {
  if (typeof storagePath !== "string" || !storagePath.trim()) {
    throw new TypeError("storagePath jest wymagany.");
  }
  let writeQueue = Promise.resolve();

  async function readState() {
    let raw;
    try {
      raw = await readFile(storagePath, "utf8");
    } catch (error) {
      if (error?.code === "ENOENT") return {};
      const unreadable = new Error("Nie można bezpiecznie odczytać kolejki wyjaśnienia.");
      unreadable.code = "PORTFOLIO_IMPORT_REVIEW_STORE_UNREADABLE";
      throw unreadable;
    }
    try {
      return normalizeStoredState(JSON.parse(raw));
    } catch {
      const unreadable = new Error("Nie można bezpiecznie odczytać kolejki wyjaśnienia.");
      unreadable.code = "PORTFOLIO_IMPORT_REVIEW_STORE_UNREADABLE";
      throw unreadable;
    }
  }

  async function writeState(state) {
    await mkdir(dirname(storagePath), { recursive: true });
    const temporaryPath = `${storagePath}.tmp`;
    await writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
    await rename(temporaryPath, storagePath);
  }

  function withWrite(mutator) {
    writeQueue = writeQueue
      .catch(() => {})
      .then(async () => {
        const state = await readState();
        const result = mutator(state);
        await writeState(state);
        return result;
      });
    return writeQueue;
  }

  async function getPendingCount(key) {
    await writeQueue.catch(() => {});
    const state = await readState();
    return state[normalizeStateKey(key)]?.issues?.length || 0;
  }

  async function getPendingSummary(key) {
    await writeQueue.catch(() => {});
    const state = await readState();
    const issues = state[normalizeStateKey(key)]?.issues || [];
    const byCode = {};
    for (const issue of issues) byCode[issue.code] = (byCode[issue.code] || 0) + 1;
    return {
      pendingCount: issues.length,
      byCode,
    };
  }

  
  
  function recordRunIssues(key, issues, { fullCoverage = false } = {}) {
    const normalizedKey = normalizeStateKey(key);
    if (!Array.isArray(issues) || issues.length > MAX_ISSUES_PER_KEY) {
      return Promise.reject(new TypeError("Nieprawidłowa lista rekordów do wyjaśnienia."));
    }
    const normalizedIssues = issues.map(normalizeIssue);
    return withWrite((state) => {
      const now = new Date().toISOString();
      const previous = Array.isArray(state[normalizedKey]?.issues)
        ? state[normalizedKey].issues
        : [];
      const previousByKey = new Map(previous.map((issue) => [
        `${issue.identityHash}:${issue.code}`,
        issue,
      ]));
      const nextByKey = fullCoverage ? new Map() : new Map(previousByKey);
      for (const issue of normalizedIssues) {
        const uniqueKey = `${issue.identityHash}:${issue.code}`;
        const prior = previousByKey.get(uniqueKey);
        nextByKey.set(uniqueKey, {
          ...issue,
          firstSeenAt: prior?.firstSeenAt || now,
          lastSeenAt: now,
          occurrences: Math.min(1_000_000, (prior?.occurrences || 0) + 1),
        });
      }
      
      
      const nextIssues = [...nextByKey.values()].slice(-MAX_ISSUES_PER_KEY);
      if (nextIssues.length > 0) {
        
        
        
        
        if (!state[normalizedKey] && Object.keys(state).length >= MAX_STATE_KEYS) {
          const full = new Error(
            "Kolejka wyjaśnienia osiągnęła limit portfeli. Wyjaśnij zaległe wpisy, zanim dojdą kolejne.",
          );
          full.code = "PORTFOLIO_IMPORT_REVIEW_STORE_FULL";
          throw full;
        }
        state[normalizedKey] = { issues: nextIssues };
      } else {
        delete state[normalizedKey];
      }
      return nextIssues.length;
    });
  }

  function clear(key) {
    const normalizedKey = normalizeStateKey(key);
    return withWrite((state) => {
      const existed = Boolean(state[normalizedKey]);
      delete state[normalizedKey];
      return existed;
    });
  }

  return {
    getPendingCount,
    getPendingSummary,
    recordRunIssues,
    clear,
  };
}
