import { randomBytes } from "node:crypto";
import { mkdir } from "node:fs/promises";
import {
  createPortfolioPdfDownloadCoordinator,
  PORTFOLIO_PDF_GLOBAL_FAILURE_CODES,
} from "./ivbipctpepmlmlxznfjf.mjs";
import {
  createPortfolioPdfIdentity,
  normalizePortfolioPdfInsurer,
} from "./ldpwrbpjqvuxzylkmjve.mjs";

const SAFE_CIRCUIT_CODES = new Set(PORTFOLIO_PDF_GLOBAL_FAILURE_CODES);
const DEFAULT_CONCURRENCY = 1;

function positiveInteger(value, fallback, name) {
  const candidate = value === undefined ? fallback : value;
  if (!Number.isSafeInteger(candidate) || candidate <= 0) {
    throw new TypeError(`${name} musi byc dodatnia liczba calkowita.`);
  }
  return candidate;
}

function createPdfKey(usedKeys) {
  for (let attempt = 0; attempt < 10; attempt += 1) {
    const key = randomBytes(24).toString("base64url");
    if (key.length === 32 && !usedKeys.has(key)) {
      usedKeys.add(key);
      return key;
    }
  }
  throw new Error("Nie udalo sie wygenerowac unikalnego klucza PDF.");
}

function getPolicy(entry) {
  const policy = entry?.policy ?? entry;
  if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
    throw new TypeError("Kazdy wpis musi wskazywac modyfikowalny obiekt policy.");
  }
  return policy;
}

function propertyCanBeUpdated(target, name) {
  const descriptor = Object.getOwnPropertyDescriptor(target, name);
  if (!descriptor) return Object.isExtensible(target);
  if ("value" in descriptor) return descriptor.writable === true;
  return typeof descriptor.set === "function";
}

function validatePolicyMutation(policy) {
  if (
    !propertyCanBeUpdated(policy, "pdfAvailable")
    || !propertyCanBeUpdated(policy, "pdfKey")
  ) {
    throw new TypeError("Obiekt policy nie pozwala ustawic danych dokumentu PDF.");
  }
}

function canonicalIdentity(insurer, entry, policy) {
  return createPortfolioPdfIdentity({
    insurer,
    policyNumber: entry?.policyNumber ?? policy?.policyNumber,
    validFrom: entry?.validFrom ?? policy?.validFrom,
    validUntil: entry?.validUntil ?? policy?.validUntil,
  });
}

function prepareEntries({ insurer, entries, policies }) {
  if (entries !== undefined && policies !== undefined) {
    throw new TypeError("Podaj entries albo policies, nie oba pola jednoczesnie.");
  }
  const source = entries ?? policies;
  if (!Array.isArray(source)) {
    throw new TypeError("entries albo policies musi byc tablica.");
  }

  const normalizedInsurer = normalizePortfolioPdfInsurer(insurer);
  const usedPdfKeys = new Set();
  const uniqueByIdentity = new Map();
  const prepared = [];

  for (const sourceEntry of source) {
    const policy = getPolicy(sourceEntry);
    validatePolicyMutation(policy);
    const identity = canonicalIdentity(normalizedInsurer, sourceEntry, policy);
    const existing = uniqueByIdentity.get(identity.key);
    if (existing) {
      existing.policies.push(policy);
      continue;
    }

    const pdfKey = createPdfKey(usedPdfKeys);
    const item = {
      ...sourceEntry,
      policy,
      insurer: identity.insurer,
      policyNumber: identity.policyNumber,
      validFrom: identity.validFrom,
      validUntil: identity.validUntil,
      pdfKey,
      outputPath: `${pdfKey}.pdf`,
    };
    const preparedItem = {
      identity,
      item,
      policies: [policy],
      pdfKey,
    };
    uniqueByIdentity.set(identity.key, preparedItem);
    prepared.push(preparedItem);
  }

  return { prepared };
}

function safeCircuitCode(code) {
  return SAFE_CIRCUIT_CODES.has(code) ? code : "unknown";
}

function emptyPublicResult() {
  return {
    attempted: 0,
    succeeded: 0,
    failed: 0,
    backoff: 0,
    quarantined: 0,
    unsupported: 0,
    deferred: 0,
    remaining: 0,
    batchComplete: true,
    circuitOpen: false,
    circuitCode: null,
    manualRetryAvailable: false,
  };
}

function batchResult(publicResult, manualRetryIdentities = []) {
  return {
    publicResult,
    privateResult: {
      manualRetryIdentities,
    },
  };
}

 
export async function runPortfolioPdfBatch({
  retryStore,
  insurer,
  pdfDirectory,
  entries,
  policies,
  limit,
  concurrency = DEFAULT_CONCURRENCY,
  fetchDocument,
} = {}) {
  const { prepared } = prepareEntries({ insurer, entries, policies });
  if (prepared.length === 0) return batchResult(emptyPublicResult());
  if (typeof fetchDocument !== "function") {
    throw new TypeError("fetchDocument jest wymagany.");
  }

  const workerCount = positiveInteger(
    concurrency,
    DEFAULT_CONCURRENCY,
    "concurrency",
  );
  const effectiveLimit = positiveInteger(limit, prepared.length, "limit");
  try {
    await mkdir(pdfDirectory, { recursive: true });
  } catch {
    return batchResult({
      ...emptyPublicResult(),
      remaining: prepared.length,
      batchComplete: false,
      circuitOpen: true,
      circuitCode: "write_failed",
    });
  }
  let fetchAttempts = 0;
  const coordinator = createPortfolioPdfDownloadCoordinator({
    retryStore,
    allowedBaseDir: pdfDirectory,
    concurrency: workerCount,
    fetchDocument: async (context) => {
      fetchAttempts += 1;
      return fetchDocument(context);
    },
  });
  const coordinatorEntries = prepared.map(({ item }) => item);
  const result = await coordinator.run(coordinatorEntries, {
    limit: effectiveLimit,
  });

  const planning = result?.planning;
  const outcomes = Array.isArray(result?.outcomes) ? result.outcomes : [];
  const committedIndexes = new Set();
  const unsupportedIndexes = new Set();
  const failedIndexes = new Set();
  const manualRetryIndexes = new Set();

  for (const outcome of outcomes) {
    const index = outcome?.index;
    if (!Number.isSafeInteger(index) || index < 0 || index >= prepared.length) {
      continue;
    }
    if (outcome.type === "success" && outcome.committed === true) {
      committedIndexes.add(index);
    } else if (outcome.type === "unsupported") {
      unsupportedIndexes.add(index);
    } else {
      failedIndexes.add(index);
    }
    if (outcome.type === "document_failure") {
      manualRetryIndexes.add(index);
    }
  }
  for (const disposition of planning?.dispositions || []) {
    if (
      (disposition?.disposition === "backoff"
        || disposition?.disposition === "quarantined")
      && Number.isSafeInteger(disposition.index)
      && disposition.index >= 0
      && disposition.index < prepared.length
    ) {
      manualRetryIndexes.add(disposition.index);
    }
  }

  for (const index of committedIndexes) {
    const current = prepared[index];
    for (const policy of current.policies) {
      policy.pdfAvailable = true;
      policy.pdfKey = current.pdfKey;
    }
  }

  const remainingIndexes = [];
  for (let index = 0; index < prepared.length; index += 1) {
    if (!committedIndexes.has(index) && !unsupportedIndexes.has(index)) {
      remainingIndexes.push(index);
    }
  }

  const circuitOpen = result?.circuit?.open === true;
  const publicResult = {
    attempted: fetchAttempts,
    succeeded: committedIndexes.size,
    failed: failedIndexes.size,
    backoff: Number.isSafeInteger(planning?.backoff) ? planning.backoff : 0,
    quarantined: Number.isSafeInteger(planning?.quarantined)
      ? planning.quarantined
      : 0,
    unsupported: unsupportedIndexes.size,
    deferred: Number.isSafeInteger(planning?.deferredByLimit)
      ? planning.deferredByLimit
      : 0,
    remaining: remainingIndexes.length,
    batchComplete: result?.batchComplete === true
      && remainingIndexes.length === 0,
    circuitOpen,
    circuitCode: circuitOpen
      ? safeCircuitCode(result?.circuit?.code)
      : null,
    manualRetryAvailable: manualRetryIndexes.size > 0,
  };
  const manualRetryIdentities = [...manualRetryIndexes]
    .sort((left, right) => left - right)
    .map((index) => ({
      insurer: prepared[index].identity.insurer,
      policyNumber: prepared[index].identity.policyNumber,
      ...(prepared[index].identity.validFrom
        ? { validFrom: prepared[index].identity.validFrom }
        : {}),
      ...(prepared[index].identity.validUntil
        ? { validUntil: prepared[index].identity.validUntil }
        : {}),
    }));
  return batchResult(publicResult, manualRetryIdentities);
}
