import { randomUUID } from "node:crypto";
import {
  lstat,
  mkdir,
  open,
  readFile,
  readdir,
  rename,
  rm,
} from "node:fs/promises";
import { dirname, join, resolve } from "node:path";

const SCHEMA_VERSION = 2;
const LEGACY_SCHEMA_VERSION = 1;
const MAX_ATTEMPTS = 3;
const STATUS_VALUES = new Set(["retry", "quarantined", "unsupported"]);
const TRANSIENT_ERROR_CODES = new Set([
  "network",
  "timeout",
  "authentication",
  "authorization",
  "portal_unavailable",
  "navigation",
  "download_failed",
  "invalid_pdf",
  "write_failed",
  "not_found",
  "rate_limited",
  "session_expired",
  "unknown",
]);
const UNSUPPORTED_ERROR_CODES = new Set([
  "unsupported_product",
  "unsupported_portal",
  "document_not_available",
  "document_not_generated",
  "not_applicable",
]);



const QUARANTINE_ELIGIBLE_ERROR_CODES = new Set([
  "download_failed",
  "invalid_pdf",
  "not_found",
]);
const DEFAULT_POLICY = Object.freeze({
  baseDelayMs: 15 * 60_000,
  maximumDelayMs: 24 * 60 * 60_000,
  maxRecords: 50_000,
  maxFileBytes: 16 * 1024 * 1024,
  maxBatchSize: 10_000,
});

const INSURER_ALIASES = Object.freeze({
  pzu: "pzu",
  "pzu-sa": "pzu",
  "powszechny-zaklad-ubezpieczen": "pzu",
  warta: "warta",
  "warta-sa": "warta",
  "tui-warta": "warta",
  "tuir-warta": "warta",
  "towarzystwo-ubezpieczen-i-reasekuracji-warta": "warta",
  mtu: "mtu",
  mtu24: "mtu",
  "mtu24-pl": "mtu",
  "moje-towarzystwo-ubezpieczen": "mtu",
  hestia: "hestia",
  "ergo-hestia": "hestia",
  "sopockie-towarzystwo-ubezpieczen-ergo-hestia": "hestia",
  generali: "generali",
  "generali-polska": "generali",
  trasti: "trasti",
  wiener: "wiener",
  "wiener-tu": "wiener",
});



const PROCESS_WRITE_QUEUES = new Map();

function getProcessWriteQueue(storagePath) {
  const key = resolve(storagePath).toLocaleLowerCase("en");
  let queue = PROCESS_WRITE_QUEUES.get(key);
  if (!queue) {
    queue = { tail: Promise.resolve() };
    PROCESS_WRITE_QUEUES.set(key, queue);
  }
  return queue;
}

export class PortfolioPdfRetryStoreError extends Error {
  constructor(code) {
    const messages = {
      invalid_insurer: "Nieprawidłowy identyfikator towarzystwa.",
      invalid_policy_number: "Nieprawidłowy numer polisy.",
      invalid_period_date: "Nieprawidłowa data okresu polisy.",
      invalid_batch: "Nieprawidłowa partia stanu ponowień PDF.",
      invalid_unsupported_code: "Nieprawidłowy kod braku obsługi dokumentu PDF.",
      record_limit: "Magazyn ponowień PDF osiągnął limit rekordów.",
      corrupt_store: "Plik stanu ponowień PDF jest uszkodzony.",
      unsafe_store: "Plik stanu ponowień PDF jest dowiązaniem.",
      io_error: "Nie udało się odczytać lub zapisać stanu ponowień PDF.",
    };
    super(messages[code] || messages.io_error);
    this.name = "PortfolioPdfRetryStoreError";
    this.code = code;
  }
}

function stripDiacritics(value) {
  return String(value)
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[łŁ]/g, (character) => (character === "Ł" ? "L" : "l"));
}

export function normalizePortfolioPdfInsurer(value) {
  const raw = String(value ?? "").trim();
  if (!raw || raw.length > 160) throw new PortfolioPdfRetryStoreError("invalid_insurer");
  const slug = stripDiacritics(raw)
    .toLowerCase()
    .replace(/&/g, " i ")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .replace(/-+/g, "-");
  const withoutLegalSuffix = slug.replace(/-(?:s-a|sa)$/g, "");
  const normalized = INSURER_ALIASES[slug] || INSURER_ALIASES[withoutLegalSuffix] || withoutLegalSuffix;
  if (!/^[a-z0-9][a-z0-9-]{1,39}$/.test(normalized)) {
    throw new PortfolioPdfRetryStoreError("invalid_insurer");
  }
  return normalized;
}

export function normalizePortfolioPdfPolicyNumber(value) {
  const raw = String(value ?? "").trim();
  if (!raw || raw.length > 200) throw new PortfolioPdfRetryStoreError("invalid_policy_number");
  const normalized = stripDiacritics(raw).toUpperCase().replace(/[^A-Z0-9]/g, "");
  if (!/^[A-Z0-9]{2,64}$/.test(normalized)) {
    throw new PortfolioPdfRetryStoreError("invalid_policy_number");
  }
  return normalized;
}

function normalizedCalendarDate(yearValue, monthValue, dayValue) {
  const year = Number(yearValue);
  const month = Number(monthValue);
  const day = Number(dayValue);
  if (
    !Number.isInteger(year)
    || !Number.isInteger(month)
    || !Number.isInteger(day)
    || year < 1900
    || year > 2200
    || month < 1
    || month > 12
    || day < 1
    || day > 31
  ) {
    throw new PortfolioPdfRetryStoreError("invalid_period_date");
  }
  const candidate = new Date(Date.UTC(year, month - 1, day));
  if (
    candidate.getUTCFullYear() !== year
    || candidate.getUTCMonth() !== month - 1
    || candidate.getUTCDate() !== day
  ) {
    throw new PortfolioPdfRetryStoreError("invalid_period_date");
  }
  return [
    String(year).padStart(4, "0"),
    String(month).padStart(2, "0"),
    String(day).padStart(2, "0"),
  ].join("-");
}

export function normalizePortfolioPdfPeriodDate(value) {
  const raw = String(value ?? "").trim();
  if (!raw) return "";
  if (raw.length > 64) throw new PortfolioPdfRetryStoreError("invalid_period_date");

  let match = raw.match(/^(\d{4})[-./](\d{1,2})[-./](\d{1,2})(?:[T\s].*)?$/);
  if (match) return normalizedCalendarDate(match[1], match[2], match[3]);

  match = raw.match(/^(\d{1,2})[.\-/](\d{1,2})[.\-/](\d{4})$/);
  if (match) return normalizedCalendarDate(match[3], match[2], match[1]);

  throw new PortfolioPdfRetryStoreError("invalid_period_date");
}

function createRecordKey(insurer, policyNumber, validFrom, validUntil) {
  if (!validFrom && !validUntil) return `${insurer}:${policyNumber}`;
  return `${insurer}:${policyNumber}:${validFrom}:${validUntil}`;
}

export function createPortfolioPdfIdentity(input) {
  const insurer = normalizePortfolioPdfInsurer(input?.insurer);
  const policyNumber = normalizePortfolioPdfPolicyNumber(input?.policyNumber);
  const validFrom = normalizePortfolioPdfPeriodDate(input?.validFrom);
  const validUntil = normalizePortfolioPdfPeriodDate(input?.validUntil);
  return {
    insurer,
    policyNumber,
    validFrom,
    validUntil,
    key: createRecordKey(insurer, policyNumber, validFrom, validUntil),
  };
}

const normalizeIdentity = createPortfolioPdfIdentity;

function normalizeCode(value, allowed, fallback) {
  const candidate = String(value ?? "")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .slice(0, 64);
  return allowed.has(candidate) ? candidate : fallback;
}

function normalizeTransientErrorCode(value) {
  return normalizeCode(value, TRANSIENT_ERROR_CODES, "unknown");
}

function normalizeUnsupportedErrorCode(value) {
  const candidate = String(value ?? "")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .slice(0, 64);
  if (!UNSUPPORTED_ERROR_CODES.has(candidate)) {
    throw new PortfolioPdfRetryStoreError("invalid_unsupported_code");
  }
  return candidate;
}

function asValidDate(value) {
  const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
  if (!Number.isFinite(date.getTime())) throw new PortfolioPdfRetryStoreError("io_error");
  return date;
}

function isIsoDate(value) {
  return typeof value === "string"
    && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
    && Number.isFinite(Date.parse(value))
    && new Date(value).toISOString() === value;
}

function parseStoredKey(key, { legacyOnly = false } = {}) {
  if (typeof key !== "string" || key.length > 130) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  const parts = key.split(":");
  if (
    (parts.length !== 2 && parts.length !== 4)
    || (legacyOnly && parts.length !== 2)
  ) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  const [insurer, policyNumber, validFrom = "", validUntil = ""] = parts;
  try {
    const identity = createPortfolioPdfIdentity({
      insurer,
      policyNumber,
      validFrom,
      validUntil,
    });
    if (identity.key !== key) {
      throw new Error("non-canonical");
    }
  } catch {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  return { insurer, policyNumber, validFrom, validUntil };
}

function validateStoredRecord(record) {
  if (!record || typeof record !== "object" || Array.isArray(record)) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  if (!Number.isInteger(record.attempts) || record.attempts < 0 || record.attempts > MAX_ATTEMPTS) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  if (!STATUS_VALUES.has(record.status)) throw new PortfolioPdfRetryStoreError("corrupt_store");
  if (record.lastAttemptAt !== null && !isIsoDate(record.lastAttemptAt)) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  if (record.nextRetryAt !== null && !isIsoDate(record.nextRetryAt)) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  if (typeof record.lastErrorCode !== "string" || record.lastErrorCode.length > 64) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  if (record.status === "retry") {
    if (record.attempts >= MAX_ATTEMPTS || !isIsoDate(record.nextRetryAt)) {
      throw new PortfolioPdfRetryStoreError("corrupt_store");
    }
    const allowedError = record.attempts === 0
      ? record.lastErrorCode === ""
      : QUARANTINE_ELIGIBLE_ERROR_CODES.has(record.lastErrorCode);
    if (!allowedError) throw new PortfolioPdfRetryStoreError("corrupt_store");
  } else if (record.status === "quarantined") {
    if (
      record.attempts !== MAX_ATTEMPTS
      || record.nextRetryAt !== null
      || !QUARANTINE_ELIGIBLE_ERROR_CODES.has(record.lastErrorCode)
      || !isIsoDate(record.lastAttemptAt)
    ) {
      throw new PortfolioPdfRetryStoreError("corrupt_store");
    }
  } else if (
    record.attempts !== 0
    || record.nextRetryAt !== null
    || !UNSUPPORTED_ERROR_CODES.has(record.lastErrorCode)
    || !isIsoDate(record.lastAttemptAt)
  ) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
}

function validateState(parsed, policy) {
  const sourceVersion = parsed?.version;
  if (
    !parsed
    || typeof parsed !== "object"
    || Array.isArray(parsed)
    || (sourceVersion !== LEGACY_SCHEMA_VERSION && sourceVersion !== SCHEMA_VERSION)
    || !isIsoDate(parsed.updatedAt)
    || !parsed.records
    || typeof parsed.records !== "object"
    || Array.isArray(parsed.records)
  ) {
    throw new PortfolioPdfRetryStoreError("corrupt_store");
  }
  const entries = Object.entries(parsed.records);
  if (entries.length > policy.maxRecords) throw new PortfolioPdfRetryStoreError("corrupt_store");
  for (const [key, record] of entries) {
    parseStoredKey(key, { legacyOnly: sourceVersion === LEGACY_SCHEMA_VERSION });
    validateStoredRecord(record);
  }
  return {
    version: SCHEMA_VERSION,
    updatedAt: parsed.updatedAt,
    records: Object.fromEntries(entries.map(([key, record]) => [key, {
      attempts: record.attempts,
      lastErrorCode: record.lastErrorCode,
      lastAttemptAt: record.lastAttemptAt,
      nextRetryAt: record.nextRetryAt,
      status: record.status,
    }])),
  };
}

function emptyState(nowDate) {
  return {
    version: SCHEMA_VERSION,
    updatedAt: nowDate.toISOString(),
    records: {},
  };
}

async function syncDirectory(path) {
  try {
    const handle = await open(path, "r");
    try {
      await handle.sync();
    } finally {
      await handle.close();
    }
  } catch {
    
  }
}

function publicDecision(record, nowDate) {
  if (!record) {
    return {
      shouldAttempt: true,
      reason: "new",
      status: "retry",
      attempts: 0,
      lastErrorCode: "",
      lastAttemptAt: null,
      nextRetryAt: nowDate.toISOString(),
    };
  }
  if (record.status === "quarantined") {
    return {
      shouldAttempt: false,
      reason: "quarantined",
      ...record,
    };
  }
  if (record.status === "unsupported") {
    return {
      shouldAttempt: false,
      reason: "unsupported",
      ...record,
    };
  }
  const due = Date.parse(record.nextRetryAt) <= nowDate.getTime();
  return {
    shouldAttempt: due,
    reason: due ? "ready" : "backoff",
    ...record,
  };
}

function summarize(state, nowDate) {
  const summary = {
    total: 0,
    retry: 0,
    dueRetry: 0,
    quarantined: 0,
    unsupported: 0,
    earliestNextRetryAt: null,
  };
  for (const record of Object.values(state.records)) {
    summary.total += 1;
    summary[record.status] += 1;
    if (record.status !== "retry") continue;
    if (Date.parse(record.nextRetryAt) <= nowDate.getTime()) summary.dueRetry += 1;
    if (!summary.earliestNextRetryAt || record.nextRetryAt < summary.earliestNextRetryAt) {
      summary.earliestNextRetryAt = record.nextRetryAt;
    }
  }
  return summary;
}

function validatePolicy(policyOverrides) {
  const policy = { ...DEFAULT_POLICY, ...policyOverrides };
  for (const field of ["baseDelayMs", "maximumDelayMs", "maxRecords", "maxFileBytes", "maxBatchSize"]) {
    if (!Number.isSafeInteger(policy[field]) || policy[field] <= 0) {
      throw new TypeError(`Nieprawidłowa polityka: ${field}.`);
    }
  }
  if (policy.maximumDelayMs < policy.baseDelayMs) {
    throw new TypeError("maximumDelayMs nie może być mniejsze niż baseDelayMs.");
  }
  return policy;
}

export function createPortfolioPdfRetryStore({
  storagePath,
  now = () => new Date(),
  policy: policyOverrides = {},
} = {}) {
  if (!storagePath) throw new TypeError("storagePath jest wymagany.");
  if (typeof now !== "function") throw new TypeError("now musi być funkcją.");
  const policy = validatePolicy(policyOverrides);
  const processWriteQueue = getProcessWriteQueue(storagePath);

  function currentDate() {
    return asValidDate(now());
  }

  async function cleanOrphanedTemporaryFiles() {
    const storageDirectory = dirname(storagePath);
    let entries;
    try {
      entries = await readdir(storageDirectory, { withFileTypes: true });
    } catch (error) {
      if (error?.code === "ENOENT") return;
      throw new PortfolioPdfRetryStoreError("io_error");
    }
    await Promise.all(entries
      .filter((entry) => (
        (entry.isFile() || entry.isSymbolicLink())
        && entry.name.startsWith(".portfolio-pdf-retry-")
        && entry.name.endsWith(".tmp")
      ))
      .map((entry) => rm(join(storageDirectory, entry.name), { force: true })
        .catch(() => {
          throw new PortfolioPdfRetryStoreError("io_error");
        })));
  }

  async function readState() {
    await cleanOrphanedTemporaryFiles();
    let info;
    try {
      info = await lstat(storagePath);
    } catch (error) {
      if (error?.code === "ENOENT") return emptyState(currentDate());
      throw new PortfolioPdfRetryStoreError("io_error");
    }
    if (info.isSymbolicLink()) throw new PortfolioPdfRetryStoreError("unsafe_store");
    if (!info.isFile() || info.size > policy.maxFileBytes) {
      throw new PortfolioPdfRetryStoreError("corrupt_store");
    }
    let raw;
    try {
      raw = await readFile(storagePath, "utf8");
    } catch {
      throw new PortfolioPdfRetryStoreError("io_error");
    }
    let parsed;
    try {
      parsed = JSON.parse(raw);
    } catch {
      throw new PortfolioPdfRetryStoreError("corrupt_store");
    }
    return validateState(parsed, policy);
  }

  async function writeState(state, nowDate) {
    state.updatedAt = nowDate.toISOString();
    const contents = `${JSON.stringify(state, null, 2)}\n`;
    if (Buffer.byteLength(contents, "utf8") > policy.maxFileBytes) {
      throw new PortfolioPdfRetryStoreError("record_limit");
    }
    const storageDirectory = dirname(storagePath);
    await mkdir(storageDirectory, { recursive: true });
    const temporaryPath = join(storageDirectory, `.portfolio-pdf-retry-${randomUUID()}.tmp`);
    let handle;
    try {
      handle = await open(temporaryPath, "wx", 0o600);
      await handle.writeFile(contents, "utf8");
      await handle.sync();
      await handle.close();
      handle = null;
      await rename(temporaryPath, storagePath);
      await syncDirectory(storageDirectory);
    } catch (error) {
      await handle?.close().catch(() => {});
      await rm(temporaryPath, { force: true }).catch(() => {});
      if (error instanceof PortfolioPdfRetryStoreError) throw error;
      throw new PortfolioPdfRetryStoreError("io_error");
    }
  }

  function enqueueMutation(mutator) {
    const operation = processWriteQueue.tail
      .catch(() => {})
      .then(async () => {
        const nowDate = currentDate();
        const state = await readState();
        const result = await mutator(state, nowDate);
        if (result.changed) await writeState(state, nowDate);
        return result.value;
      });
    processWriteQueue.tail = operation.catch(() => {});
    return operation;
  }

  async function readFresh(reader) {
    await processWriteQueue.tail.catch(() => {});
    const nowDate = currentDate();
    return reader(await readState(), nowDate);
  }

  function delayForAttempt(attempts) {
    return Math.min(policy.maximumDelayMs, policy.baseDelayMs * (2 ** Math.max(0, attempts - 1)));
  }

  function applyFailure(state, identity, errorCode, nowDate) {
    const current = state.records[identity.key];
    if (current?.status === "quarantined" || current?.status === "unsupported") {
      return { changed: false, quarantinedNow: false, ignoredSystemFailure: false };
    }
    if (!QUARANTINE_ELIGIBLE_ERROR_CODES.has(errorCode)) {
      return { changed: false, quarantinedNow: false, ignoredSystemFailure: true };
    }
    if (!current && Object.keys(state.records).length >= policy.maxRecords) {
      throw new PortfolioPdfRetryStoreError("record_limit");
    }
    const attempts = Math.min(MAX_ATTEMPTS, Number(current?.attempts || 0) + 1);
    const quarantined = attempts >= MAX_ATTEMPTS;
    state.records[identity.key] = {
      attempts,
      lastErrorCode: errorCode,
      lastAttemptAt: nowDate.toISOString(),
      nextRetryAt: quarantined
        ? null
        : new Date(nowDate.getTime() + delayForAttempt(attempts)).toISOString(),
      status: quarantined ? "quarantined" : "retry",
    };
    return { changed: true, quarantinedNow: quarantined, ignoredSystemFailure: false };
  }

  function getDecision(input) {
    const identity = normalizeIdentity(input);
    return readFresh((state, nowDate) => publicDecision(state.records[identity.key], nowDate));
  }

  function getSummary() {
    return readFresh((state, nowDate) => summarize(state, nowDate));
  }

  function getDecisions(entries) {
    if (!Array.isArray(entries) || entries.length === 0 || entries.length > policy.maxBatchSize) {
      return Promise.reject(new PortfolioPdfRetryStoreError("invalid_batch"));
    }
    let identities;
    try {
      identities = entries.map(normalizeIdentity);
    } catch (error) {
      return Promise.reject(error);
    }
    return readFresh((state, nowDate) => ({
      decisions: identities.map((identity) => publicDecision(state.records[identity.key], nowDate)),
      summary: summarize(state, nowDate),
    }));
  }

  function recordFailure(input) {
    const identity = normalizeIdentity(input);
    const errorCode = normalizeTransientErrorCode(input?.errorCode);
    return enqueueMutation((state, nowDate) => {
      const result = applyFailure(state, identity, errorCode, nowDate);
      return {
        changed: result.changed,
        value: {
          ...publicDecision(state.records[identity.key], nowDate),
          ignoredSystemFailure: result.ignoredSystemFailure,
        },
      };
    });
  }

  function recordFailures(entries) {
    if (!Array.isArray(entries) || entries.length === 0 || entries.length > policy.maxBatchSize) {
      return Promise.reject(new PortfolioPdfRetryStoreError("invalid_batch"));
    }
    let normalizedEntries;
    try {
      const byKey = new Map();
      for (const entry of entries) {
        const identity = normalizeIdentity(entry);
        byKey.set(identity.key, {
          identity,
          errorCode: normalizeTransientErrorCode(entry?.errorCode),
        });
      }
      normalizedEntries = [...byKey.values()];
    } catch (error) {
      return Promise.reject(error);
    }
    return enqueueMutation((state, nowDate) => {
      let changed = 0;
      let quarantinedNow = 0;
      let ignoredSystemFailures = 0;
      for (const entry of normalizedEntries) {
        const result = applyFailure(state, entry.identity, entry.errorCode, nowDate);
        if (result.changed) changed += 1;
        if (result.quarantinedNow) quarantinedNow += 1;
        if (result.ignoredSystemFailure) ignoredSystemFailures += 1;
      }
      return {
        changed: changed > 0,
        value: {
          processed: normalizedEntries.length,
          changed,
          quarantinedNow,
          ignoredSystemFailures,
          summary: summarize(state, nowDate),
        },
      };
    });
  }

  function markUnsupported(input) {
    const identity = normalizeIdentity(input);
    const errorCode = normalizeUnsupportedErrorCode(input?.errorCode);
    return enqueueMutation((state, nowDate) => {
      if (!state.records[identity.key] && Object.keys(state.records).length >= policy.maxRecords) {
        throw new PortfolioPdfRetryStoreError("record_limit");
      }
      state.records[identity.key] = {
        attempts: 0,
        lastErrorCode: errorCode,
        lastAttemptAt: nowDate.toISOString(),
        nextRetryAt: null,
        status: "unsupported",
      };
      return {
        changed: true,
        value: publicDecision(state.records[identity.key], nowDate),
      };
    });
  }

  function requestManualRetry(input) {
    const identity = normalizeIdentity(input);
    return enqueueMutation((state, nowDate) => {
      if (!state.records[identity.key] && Object.keys(state.records).length >= policy.maxRecords) {
        throw new PortfolioPdfRetryStoreError("record_limit");
      }
      const previous = state.records[identity.key];
      state.records[identity.key] = {
        attempts: 0,
        lastErrorCode: "",
        lastAttemptAt: previous?.lastAttemptAt ?? null,
        nextRetryAt: nowDate.toISOString(),
        status: "retry",
      };
      return {
        changed: true,
        value: publicDecision(state.records[identity.key], nowDate),
      };
    });
  }

  function requestManualRetries(entries) {
    if (!Array.isArray(entries) || entries.length === 0 || entries.length > policy.maxBatchSize) {
      return Promise.reject(new PortfolioPdfRetryStoreError("invalid_batch"));
    }
    let identities;
    try {
      const byKey = new Map();
      for (const entry of entries) {
        const identity = normalizeIdentity(entry);
        byKey.set(identity.key, identity);
      }
      identities = [...byKey.values()];
    } catch (error) {
      return Promise.reject(error);
    }
    return enqueueMutation((state, nowDate) => {
      let reset = 0;
      let skipped = 0;
      for (const identity of identities) {
        const previous = state.records[identity.key];
        
        
        
        if (!previous || previous.status === "unsupported") {
          skipped += 1;
          continue;
        }
        state.records[identity.key] = {
          attempts: 0,
          lastErrorCode: "",
          lastAttemptAt: previous.lastAttemptAt,
          nextRetryAt: nowDate.toISOString(),
          status: "retry",
        };
        reset += 1;
      }
      return {
        changed: reset > 0,
        value: {
          processed: identities.length,
          reset,
          skipped,
          summary: summarize(state, nowDate),
        },
      };
    });
  }

  function clearRecord(input, resultField) {
    const identity = normalizeIdentity(input);
    return enqueueMutation((state, nowDate) => {
      const existed = Object.hasOwn(state.records, identity.key);
      if (existed) delete state.records[identity.key];
      return {
        changed: existed,
        value: {
          [resultField]: existed,
          summary: summarize(state, nowDate),
        },
      };
    });
  }

  function recordSuccesses(entries) {
    if (!Array.isArray(entries) || entries.length === 0 || entries.length > policy.maxBatchSize) {
      return Promise.reject(new PortfolioPdfRetryStoreError("invalid_batch"));
    }
    let identities;
    try {
      const byKey = new Map();
      for (const entry of entries) {
        const identity = normalizeIdentity(entry);
        byKey.set(identity.key, identity);
      }
      identities = [...byKey.values()];
    } catch (error) {
      return Promise.reject(error);
    }
    return enqueueMutation((state, nowDate) => {
      let cleared = 0;
      for (const identity of identities) {
        if (!Object.hasOwn(state.records, identity.key)) continue;
        delete state.records[identity.key];
        cleared += 1;
      }
      return {
        changed: cleared > 0,
        value: {
          processed: identities.length,
          cleared,
          summary: summarize(state, nowDate),
        },
      };
    });
  }

  return Object.freeze({
    getDecision,
    getDecisions,
    getSummary,
    recordFailure,
    recordFailures,
    recordSuccesses,
    markUnsupported,
    requestManualRetry,
    requestManualRetries,
    recordSuccess: (input) => clearRecord(input, "cleared"),
    reset: (input) => clearRecord(input, "reset"),
  });
}

export const PORTFOLIO_PDF_RETRY_STORE_SCHEMA_VERSION = SCHEMA_VERSION;
export const PORTFOLIO_PDF_RETRY_MAX_ATTEMPTS = MAX_ATTEMPTS;
