import "server-only";

import { createHash, randomUUID } from "node:crypto";
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import path from "node:path";

export const MAX_PROPOSAL_DOCUMENT_BYTES = 20 * 1024 * 1024;

const PROPOSAL_STORAGE_ROOT = path.join(process.cwd(), "storage", "proposals");

const EXTENSION_BY_MIME: Record<string, string> = {
  "application/pdf": "pdf",
  "application/msword": "doc",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
  "image/jpeg": "jpg",
  "image/png": "png",
};

export function isAllowedProposalMimeType(mimeType: string) {
  return mimeType in EXTENSION_BY_MIME;
}

function normalizeExtension(fileName: string, mimeType: string) {
  const fromMime = EXTENSION_BY_MIME[mimeType] ?? "";
  const fromName = fileName.includes(".") ? fileName.split(".").pop()?.toLowerCase() ?? "" : "";
  return fromMime || fromName || "bin";
}

function sanitizeFileName(fileName: string, extension: string) {
  const withoutExtension = fileName.replace(/\.[^/.]+$/, "");
  const safeBase = withoutExtension.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-") || "document";
  const safeExtension = extension.replace(/[^a-z0-9]+/g, "") || "bin";
  return `${safeBase}.${safeExtension}`;
}

export type StoredProposalDocument = {
  fileName: string;
  storedFileName: string;
  filePath: string;
  mimeType: string;
  sizeBytes: number;
  checksumSha256: string;
};

export async function storeProposalDocument(userId: string, proposalId: string, originalFileName: string, mimeType: string, buffer: Buffer) {
  const extension = normalizeExtension(originalFileName, mimeType);
  const safeOriginalName = sanitizeFileName(originalFileName, extension);
  const targetDirectory = path.join(PROPOSAL_STORAGE_ROOT, userId, proposalId);
  const storedFileName = `${Date.now()}-${randomUUID()}.${extension}`;
  const absolutePath = path.join(targetDirectory, storedFileName);

  await mkdir(targetDirectory, { recursive: true });
  await writeFile(absolutePath, buffer);

  return {
    fileName: safeOriginalName,
    storedFileName,
    filePath: path.join("storage", "proposals", userId, proposalId, storedFileName).replace(/\\/g, "/"),
    mimeType,
    sizeBytes: buffer.byteLength,
    checksumSha256: createHash("sha256").update(buffer).digest("hex"),
  } satisfies StoredProposalDocument;
}

export async function removeStoredProposalDocument(relativeFilePath: string | null | undefined) {
  if (!relativeFilePath) {
    return;
  }

  const storageRoot = path.resolve(process.cwd(), "storage");
  const absolutePath = path.resolve(process.cwd(), relativeFilePath);

  if (!absolutePath.startsWith(`${storageRoot}${path.sep}`)) {
    return;
  }

  try {
    await unlink(absolutePath);
  } catch {
    // Ignore delete errors.
  }
}

export async function readStoredProposalDocument(relativeFilePath: string, maxBytes = MAX_PROPOSAL_DOCUMENT_BYTES) {
  const storageRoot = path.resolve(process.cwd(), "storage");
  const absolutePath = path.resolve(process.cwd(), relativeFilePath);

  if (!absolutePath.startsWith(`${storageRoot}${path.sep}`)) {
    throw new Error("PROPOSAL_DOCUMENT_INVALID_PATH");
  }

  try {
    const buffer = await readFile(absolutePath);

    if (!buffer.byteLength) {
      throw new Error("PROPOSAL_DOCUMENT_EMPTY");
    }

    if (buffer.byteLength > maxBytes) {
      throw new Error("PROPOSAL_DOCUMENT_TOO_LARGE");
    }

    return buffer;
  } catch (error) {
    if (
      error &&
      typeof error === "object" &&
      "code" in error &&
      (error as NodeJS.ErrnoException).code === "ENOENT"
    ) {
      throw new Error("PROPOSAL_DOCUMENT_NOT_FOUND");
    }

    throw error;
  }
}
