import {
  lstat,
  mkdir,
  readdir,
  realpath,
  rmdir,
  unlink,
} from "node:fs/promises";
import {
  dirname,
  isAbsolute,
  join,
  relative,
  resolve,
} from "node:path";
import {
  PORTFOLIO_PDF_GLOBAL_FAILURE_CODES,
} from "./ivbipctpepmlmlxznfjf.mjs";

const MAX_PUBLIC_COUNT = 100_000;
const JOB_ID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
const SAFE_CIRCUIT_CODES = new Set(PORTFOLIO_PDF_GLOBAL_FAILURE_CODES);

function clampCount(value) {
  if (!Number.isFinite(value) || value <= 0) return 0;
  return Math.min(MAX_PUBLIC_COUNT, Math.trunc(value));
}

function safeIsoDate(value) {
  if (typeof value !== "string" || value.length > 40) return null;
  const timestamp = Date.parse(value);
  if (!Number.isFinite(timestamp)) return null;
  const canonical = new Date(timestamp).toISOString();
  return canonical === value ? canonical : null;
}

export function serializePortfolioPdfRetryStatus(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  const circuitOpen = value.circuitOpen === true;
  const rawCircuitCode = typeof value.circuitCode === "string"
    ? value.circuitCode
    : "";
  return {
    attempted: clampCount(value.attempted),
    succeeded: clampCount(value.succeeded),
    failed: clampCount(value.failed),
    backoff: clampCount(value.backoff),
    quarantined: clampCount(value.quarantined),
    unsupported: clampCount(value.unsupported),
    deferred: clampCount(value.deferred),
    remaining: clampCount(value.remaining),
    earliestNextRetryAt: safeIsoDate(value.earliestNextRetryAt),
    batchComplete: value.batchComplete === true,
    circuitOpen,
    circuitCode: circuitOpen && SAFE_CIRCUIT_CODES.has(rawCircuitCode)
      ? rawCircuitCode
      : null,
    manualRetryAvailable: value.manualRetryAvailable === true,
  };
}

function isPathInside(basePath, candidatePath) {
  const difference = relative(basePath, candidatePath);
  return difference === ""
    || (!difference.startsWith("..\\")
      && !difference.startsWith("../")
      && difference !== ".."
      && !isAbsolute(difference));
}

function samePath(left, right) {
  return resolve(left).toLocaleLowerCase("en") === resolve(right).toLocaleLowerCase("en");
}

export async function validatePortfolioPdfJobDirectory({
  filesDirectory,
  jobId,
  jobDirectory,
} = {}) {
  const normalizedFilesDirectory = typeof filesDirectory === "string"
    ? filesDirectory.trim()
    : "";
  if (
    !normalizedFilesDirectory
    || normalizedFilesDirectory.length > 32_000
    || normalizedFilesDirectory.includes("\0")
    || !JOB_ID_PATTERN.test(String(jobId || ""))
    || typeof jobDirectory !== "string"
    || !jobDirectory.trim()
  ) {
    throw new TypeError("Nieprawidlowy katalog lub identyfikator joba PDF.");
  }

  const configuredFilesDirectory = resolve(normalizedFilesDirectory);
  const expectedDirectory = resolve(
    configuredFilesDirectory,
    "tmp",
    "portfolio-pdf-jobs",
    String(jobId).toLowerCase(),
  );
  if (!samePath(expectedDirectory, jobDirectory)) {
    throw new Error("Niebezpieczny katalog roboczy PDF.");
  }

  const lexicalSegments = [
    configuredFilesDirectory,
    resolve(configuredFilesDirectory, "tmp"),
    resolve(configuredFilesDirectory, "tmp", "portfolio-pdf-jobs"),
    expectedDirectory,
  ];
  let canonicalParent = "";
  for (let index = 0; index < lexicalSegments.length; index += 1) {
    const lexicalPath = lexicalSegments[index];
    const info = await lstat(lexicalPath);
    if (info.isSymbolicLink() || !info.isDirectory()) {
      throw new Error("Niebezpieczny katalog roboczy PDF.");
    }
    const canonicalPath = await realpath(lexicalPath);
    if (index > 0 && !samePath(dirname(canonicalPath), canonicalParent)) {
      throw new Error("Niebezpieczny katalog roboczy PDF.");
    }
    canonicalParent = canonicalPath;
  }
  return canonicalParent;
}

async function ensureSafeChildDirectory(parentPath, childName, canonicalFilesDirectory) {
  const childPath = join(parentPath, childName);
  try {
    await mkdir(childPath);
  } catch (error) {
    if (error?.code !== "EEXIST") throw error;
  }
  const info = await lstat(childPath);
  if (info.isSymbolicLink() || !info.isDirectory()) {
    throw new Error("Niebezpieczny katalog roboczy PDF.");
  }
  const canonicalPath = await realpath(childPath);
  if (!isPathInside(canonicalFilesDirectory, canonicalPath)) {
    throw new Error("Niebezpieczny katalog roboczy PDF.");
  }
  return canonicalPath;
}

export async function createPortfolioPdfJobDirectory({
  filesDirectory,
  jobId,
} = {}) {
  const normalizedFilesDirectory = typeof filesDirectory === "string"
    ? filesDirectory.trim()
    : "";
  if (
    !normalizedFilesDirectory
    || normalizedFilesDirectory.length > 32_000
    || normalizedFilesDirectory.includes("\0")
    || !JOB_ID_PATTERN.test(String(jobId || ""))
  ) {
    throw new TypeError("Nieprawidlowy katalog lub identyfikator joba PDF.");
  }

  const configuredFilesDirectory = resolve(normalizedFilesDirectory);
  const filesInfo = await lstat(configuredFilesDirectory);
  if (filesInfo.isSymbolicLink() || !filesInfo.isDirectory()) {
    throw new Error("Niebezpieczny katalog roboczy PDF.");
  }
  const canonicalFilesDirectory = await realpath(configuredFilesDirectory);
  const temporaryDirectory = await ensureSafeChildDirectory(
    configuredFilesDirectory,
    "tmp",
    canonicalFilesDirectory,
  );
  const jobsDirectory = await ensureSafeChildDirectory(
    temporaryDirectory,
    "portfolio-pdf-jobs",
    canonicalFilesDirectory,
  );
  return ensureSafeChildDirectory(
    jobsDirectory,
    String(jobId).toLowerCase(),
    canonicalFilesDirectory,
  );
}

export async function cleanupPortfolioPdfJobDirectory({
  filesDirectory,
  jobId,
  jobDirectory,
} = {}) {
  let canonicalDirectory;
  try {
    canonicalDirectory = await validatePortfolioPdfJobDirectory({
      filesDirectory,
      jobId,
      jobDirectory,
    });
  } catch (error) {
    if (error?.code === "ENOENT") return false;
    throw error;
  }
  const entries = await readdir(canonicalDirectory, { withFileTypes: true });
  for (const entry of entries) {
    if (
      !entry.isFile()
      || !/^(?:[0-9A-Za-z_-]{32}\.pdf|\.portfolio-pdf-download-[0-9a-f-]{36}\.tmp)$/i.test(entry.name)
    ) {
      throw new Error("Niebezpieczna zawartosc katalogu roboczego PDF.");
    }
  }
  for (const entry of entries) {
    const revalidatedDirectory = await validatePortfolioPdfJobDirectory({
      filesDirectory,
      jobId,
      jobDirectory,
    });
    await unlink(join(revalidatedDirectory, entry.name));
  }
  const revalidatedDirectory = await validatePortfolioPdfJobDirectory({
    filesDirectory,
    jobId,
    jobDirectory,
  });
  await rmdir(revalidatedDirectory);
  return true;
}





export async function sweepOrphanedPortfolioPdfJobDirectories({
  filesDirectory,
  liveJobIds,
} = {}) {
  const summary = { removed: 0, skipped: 0, failed: 0 };
  const normalizedFilesDirectory = typeof filesDirectory === "string"
    ? filesDirectory.trim()
    : "";
  if (
    !normalizedFilesDirectory
    || normalizedFilesDirectory.length > 32_000
    || normalizedFilesDirectory.includes("\0")
  ) {
    throw new TypeError("Nieprawidlowy katalog plikow aplikacji.");
  }
  const liveDirectoryNames = new Set(
    [...(liveJobIds || [])].map((jobId) => String(jobId).toLowerCase()),
  );
  const jobsDirectory = resolve(normalizedFilesDirectory, "tmp", "portfolio-pdf-jobs");
  let entries;
  try {
    const info = await lstat(jobsDirectory);
    if (info.isSymbolicLink() || !info.isDirectory()) return summary;
    entries = await readdir(jobsDirectory, { withFileTypes: true });
  } catch (error) {
    if (error?.code === "ENOENT") return summary;
    throw error;
  }
  for (const entry of entries) {
    if (
      !entry.isDirectory()
      || !JOB_ID_PATTERN.test(entry.name)
      || liveDirectoryNames.has(entry.name.toLowerCase())
    ) {
      summary.skipped += 1;
      continue;
    }
    try {
      await cleanupPortfolioPdfJobDirectory({
        filesDirectory: normalizedFilesDirectory,
        jobId: entry.name,
        jobDirectory: join(jobsDirectory, entry.name.toLowerCase()),
      });
      summary.removed += 1;
    } catch {
      summary.failed += 1;
    }
  }
  return summary;
}

export const PORTFOLIO_PDF_PUBLIC_CIRCUIT_CODES = Object.freeze(
  [...SAFE_CIRCUIT_CODES],
);
