import { BadRequestException } from "@nestjs/common";
import type { FileCategory } from "@slackwsh/contracts";
import { extname } from "node:path";

export const MAX_FILE_BYTES = 250 * 1024 * 1024;

interface FileRule {
  category: FileCategory;
  mime: string;
  maxBytes: number;
  signature: "jpeg" | "png" | "gif" | "webp" | "pdf" | "zip" | "ole" | "mp4" | "webm" | "mp3" | "wav" | "ogg" | "text";
  aliases?: string[];
}

const MB = 1024 * 1024;
const rules: Record<string, FileRule> = {
  ".jpg": { category: "image", mime: "image/jpeg", maxBytes: 25 * MB, signature: "jpeg", aliases: ["image/jpg"] },
  ".jpeg": { category: "image", mime: "image/jpeg", maxBytes: 25 * MB, signature: "jpeg" },
  ".png": { category: "image", mime: "image/png", maxBytes: 25 * MB, signature: "png" },
  ".gif": { category: "image", mime: "image/gif", maxBytes: 25 * MB, signature: "gif" },
  ".webp": { category: "image", mime: "image/webp", maxBytes: 25 * MB, signature: "webp" },
  ".pdf": { category: "pdf", mime: "application/pdf", maxBytes: 50 * MB, signature: "pdf" },
  ".mp4": { category: "video", mime: "video/mp4", maxBytes: 250 * MB, signature: "mp4", aliases: ["application/mp4"] },
  ".m4v": { category: "video", mime: "video/mp4", maxBytes: 250 * MB, signature: "mp4", aliases: ["video/x-m4v"] },
  ".mov": { category: "video", mime: "video/quicktime", maxBytes: 250 * MB, signature: "mp4" },
  ".webm": { category: "video", mime: "video/webm", maxBytes: 250 * MB, signature: "webm", aliases: ["audio/webm"] },
  ".m4a": { category: "audio", mime: "audio/mp4", maxBytes: 100 * MB, signature: "mp4", aliases: ["audio/m4a", "audio/x-m4a", "audio/aac"] },
  ".mp3": { category: "audio", mime: "audio/mpeg", maxBytes: 100 * MB, signature: "mp3", aliases: ["audio/mp3"] },
  ".wav": { category: "audio", mime: "audio/wav", maxBytes: 100 * MB, signature: "wav", aliases: ["audio/x-wav"] },
  ".ogg": { category: "audio", mime: "audio/ogg", maxBytes: 100 * MB, signature: "ogg", aliases: ["video/ogg"] },
  ".zip": { category: "archive", mime: "application/zip", maxBytes: 100 * MB, signature: "zip", aliases: ["application/x-zip-compressed"] },
  ".docx": { category: "document", mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", maxBytes: 50 * MB, signature: "zip" },
  ".xlsx": { category: "document", mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", maxBytes: 50 * MB, signature: "zip" },
  ".pptx": { category: "document", mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation", maxBytes: 50 * MB, signature: "zip" },
  ".doc": { category: "document", mime: "application/msword", maxBytes: 50 * MB, signature: "ole" },
  ".xls": { category: "document", mime: "application/vnd.ms-excel", maxBytes: 50 * MB, signature: "ole" },
  ".ppt": { category: "document", mime: "application/vnd.ms-powerpoint", maxBytes: 50 * MB, signature: "ole" },
  ".txt": { category: "text", mime: "text/plain", maxBytes: 10 * MB, signature: "text" },
  ".md": { category: "text", mime: "text/markdown", maxBytes: 10 * MB, signature: "text", aliases: ["text/plain"] },
  ".csv": { category: "text", mime: "text/csv", maxBytes: 10 * MB, signature: "text", aliases: ["application/vnd.ms-excel", "text/plain"] },
  ".json": { category: "text", mime: "application/json", maxBytes: 10 * MB, signature: "text", aliases: ["text/json", "text/plain"] },
  ".xml": { category: "text", mime: "application/xml", maxBytes: 10 * MB, signature: "text", aliases: ["text/xml", "text/plain"] },
};

function starts(buffer: Uint8Array, bytes: number[]) {
  return bytes.every((value, index) => buffer[index] === value);
}

function ascii(buffer: Uint8Array, start: number, length: number) {
  return Buffer.from(buffer.subarray(start, start + length)).toString("ascii");
}

function signatureMatches(kind: FileRule["signature"], bytes: Uint8Array): boolean {
  switch (kind) {
    case "jpeg": return starts(bytes, [0xff, 0xd8, 0xff]);
    case "png": return starts(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
    case "gif": return ascii(bytes, 0, 6) === "GIF87a" || ascii(bytes, 0, 6) === "GIF89a";
    case "webp": return ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WEBP";
    case "pdf": return ascii(bytes, 0, 5) === "%PDF-";
    case "zip": return starts(bytes, [0x50, 0x4b, 0x03, 0x04]) || starts(bytes, [0x50, 0x4b, 0x05, 0x06]);
    case "ole": return starts(bytes, [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]);
    case "mp4": return ascii(bytes, 4, 4) === "ftyp";
    case "webm": return starts(bytes, [0x1a, 0x45, 0xdf, 0xa3]);
    case "mp3": return ascii(bytes, 0, 3) === "ID3" || (bytes[0] === 0xff && (bytes[1]! & 0xe0) === 0xe0);
    case "wav": return ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WAVE";
    case "ogg": return ascii(bytes, 0, 4) === "OggS";
    case "text": return !bytes.subarray(0, 4096).some((byte) => byte === 0);
  }
}

export function validateFileDeclaration(name: string, declaredType: string, size: number) {
  const extension = extname(name).toLowerCase();
  const rule = rules[extension];
  if (!rule) throw new BadRequestException(`unsupported file type: ${extension || "no extension"}`);
  if (size <= 0 || size > rule.maxBytes) {
    throw new BadRequestException(`${extension} files must be smaller than ${Math.round(rule.maxBytes / MB)} MB`);
  }
  const normalized = declaredType.split(";")[0]!.trim().toLowerCase();
  const allowed = new Set([rule.mime, ...(rule.aliases ?? []), "application/octet-stream"]);
  if (!allowed.has(normalized)) throw new BadRequestException(`file extension ${extension} does not match MIME type ${declaredType}`);
  const category = normalized.startsWith("audio/") ? "audio" as FileCategory : rule.category;
  return { ...rule, extension, category };
}

export function validateStoredFile(name: string, declaredType: string, size: number, head: Uint8Array) {
  const rule = validateFileDeclaration(name, declaredType, size);
  if (!signatureMatches(rule.signature, head)) {
    throw new BadRequestException(`file contents do not match ${rule.extension}`);
  }
  return rule;
}
