const fs = require("fs");
const path = require("path");
const { TextDecoder } = require("util");

const FILE_TYPES = Object.freeze({
  zip: {
    mime: "application/zip",
    mimes: ["application/zip", "application/x-zip-compressed", "application/octet-stream"],
    extensions: [".zip"]
  },
  png: {
    mime: "image/png",
    mimes: ["image/png"],
    extensions: [".png"]
  },
  jpeg: {
    mime: "image/jpeg",
    mimes: ["image/jpeg", "image/jpg"],
    extensions: [".jpg", ".jpeg"]
  },
  webp: {
    mime: "image/webp",
    mimes: ["image/webp"],
    extensions: [".webp"]
  },
  pdf: {
    mime: "application/pdf",
    mimes: ["application/pdf"],
    extensions: [".pdf"]
  },
  text: {
    mime: "text/plain",
    mimes: ["text/plain"],
    extensions: [".txt"]
  },
  svg: {
    mime: "image/svg+xml",
    mimes: ["image/svg+xml"],
    extensions: [".svg"]
  }
});

function flattenUploadedFiles(value) {
  if (!value) return [];
  if (Array.isArray(value)) return value.flatMap(flattenUploadedFiles);
  if (value.path) return [value];
  if (typeof value === "object") return Object.values(value).flatMap(flattenUploadedFiles);
  return [];
}

function cleanupUploadedFiles(value) {
  for (const file of flattenUploadedFiles(value)) {
    if (!file.path) continue;
    try {
      fs.rmSync(file.path, { force: true });
    } catch {
      // Cleanup is best effort; callers still receive the original upload error.
    }
  }
}

function validateUploadedFile(file, options = {}) {
  if (!file?.path) throw new Error("The uploaded file could not be read.");
  const allowedTypes = normalizeAllowedTypes(options.allowedTypes);
  const stat = fs.statSync(file.path);
  const maxBytes = Math.max(1, Number(options.maxBytes) || Number.MAX_SAFE_INTEGER);
  if (!stat.isFile() || stat.size < 1) throw new Error("The uploaded file is empty.");
  if (stat.size > maxBytes) throw new Error(`The uploaded file is larger than ${formatBytes(maxBytes)}.`);

  const originalName = safeDownloadFilename(file.originalname, "upload");
  const extension = path.extname(originalName).toLowerCase();
  const claimedMime = String(file.mimetype || "").toLowerCase();
  const buffer = allowedTypes.some((type) => type === "text" || type === "svg")
    ? fs.readFileSync(file.path)
    : readPrefix(file.path, 16);
  const detectedType = allowedTypes.find((type) => matchesType(buffer, type));
  if (!detectedType) {
    throw new Error(`The file contents do not match an allowed ${options.label || "file"} type.`);
  }

  const definition = FILE_TYPES[detectedType];
  if (claimedMime && !definition.mimes.includes(claimedMime)) {
    throw new Error("The file contents do not match the type reported by the browser.");
  }
  if (options.requireExtension && !extension) {
    throw new Error(`The uploaded file must use ${formatExtensions(allowedTypes)}.`);
  }
  if (extension && !definition.extensions.includes(extension)) {
    throw new Error("The filename extension does not match the file contents.");
  }

  file.originalname = originalName;
  file.mimetype = definition.mime;
  file.size = stat.size;
  file.detectedType = detectedType;
  return file;
}

function validateUploadedFiles(value, options = {}) {
  const files = flattenUploadedFiles(value);
  try {
    files.forEach((file) => validateUploadedFile(file, options));
    return files;
  } catch (error) {
    cleanupUploadedFiles(files);
    throw error;
  }
}

function safeDownloadFilename(value, fallback = "attachment") {
  const lastSegment = String(value || "")
    .split(/[\\/]/)
    .pop()
    .normalize("NFKC")
    .replace(/[\u0000-\u001f\u007f]/g, "")
    .replace(/["';]/g, "")
    .trim();
  const cleaned = lastSegment.replace(/^\.+/, "").slice(0, 180);
  return cleaned || fallback;
}

function normalizeAllowedTypes(value) {
  const types = Array.isArray(value) ? value : [];
  if (!types.length || types.some((type) => !FILE_TYPES[type])) {
    throw new Error("Upload validation requires known allowed file types.");
  }
  return [...new Set(types)];
}

function matchesType(buffer, type) {
  if (type === "png") return startsWith(buffer, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
  if (type === "jpeg") return startsWith(buffer, [0xff, 0xd8, 0xff]);
  if (type === "webp") return buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP";
  if (type === "pdf") return buffer.subarray(0, 5).toString("ascii") === "%PDF-";
  if (type === "zip") {
    return startsWith(buffer, [0x50, 0x4b, 0x03, 0x04]) || startsWith(buffer, [0x50, 0x4b, 0x05, 0x06]) || startsWith(buffer, [0x50, 0x4b, 0x07, 0x08]);
  }
  if (type === "text") return isPlainText(buffer);
  if (type === "svg") return isSafeSvg(buffer);
  return false;
}

function isPlainText(buffer) {
  if (buffer.includes(0)) return false;
  try {
    new TextDecoder("utf-8", { fatal: true }).decode(buffer);
    return true;
  } catch {
    return false;
  }
}

function isSafeSvg(buffer) {
  if (!isPlainText(buffer)) return false;
  const source = buffer.toString("utf8").replace(/^\uFEFF/, "").trim();
  if (!/^<\?xml\b[^>]*>\s*/i.test(source) && !/^<svg\b/i.test(source)) return false;
  if (!/<svg\b/i.test(source)) return false;
  return !/<\s*(script|foreignObject|iframe|object|embed|audio|video)\b/i.test(source)
    && !/\bon[a-z]+\s*=/i.test(source)
    && !/javascript\s*:/i.test(source)
    && !/@import\b/i.test(source)
    && !/<\s*!DOCTYPE|<\s*!ENTITY/i.test(source)
    && !/\b(?:href|xlink:href)\s*=\s*["']\s*(?!#)/i.test(source)
    && !/\burl\s*\(\s*["']?\s*(?!#)/i.test(source);
}

function startsWith(buffer, bytes) {
  return buffer.length >= bytes.length && bytes.every((byte, index) => buffer[index] === byte);
}

function readPrefix(filePath, length) {
  const descriptor = fs.openSync(filePath, "r");
  try {
    const buffer = Buffer.alloc(length);
    const bytesRead = fs.readSync(descriptor, buffer, 0, length, 0);
    return buffer.subarray(0, bytesRead);
  } finally {
    fs.closeSync(descriptor);
  }
}

function formatBytes(bytes) {
  if (bytes >= 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))} MB`;
  return `${Math.round(bytes / 1024)} KB`;
}

function formatExtensions(types) {
  return types.flatMap((type) => FILE_TYPES[type].extensions).join(" or ");
}

module.exports = {
  FILE_TYPES,
  cleanupUploadedFiles,
  flattenUploadedFiles,
  safeDownloadFilename,
  validateUploadedFile,
  validateUploadedFiles
};
