/**
 * File upload helpers. Bun parses `multipart/form-data` natively via
 * `Request.formData()`, yielding web `File` objects; these helpers validate and
 * persist them safely (size/type limits, filename sanitisation to prevent path
 * traversal).
 */

export class UploadError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "UploadError";
  }
}

export interface SaveUploadOptions {
  /** Destination directory. */
  dir: string;
  /** Reject files larger than this many bytes. */
  maxBytes?: number;
  /** Allowed MIME types (e.g. "image/png") and/or extensions (e.g. ".png"). */
  allowedTypes?: string[];
  /** Choose the stored filename. Default: the sanitised original name. */
  filename?: (file: File) => string;
}

export interface SavedUpload {
  path: string;
  filename: string;
  size: number;
  type: string;
}

/** All `File` values in a parsed form, with their field names. */
export function collectUploads(form: FormData): { field: string; file: File }[] {
  const out: { field: string; file: File }[] = [];
  for (const [field, value] of form) {
    if (value instanceof File && value.size > 0) out.push({ field, file: value });
  }
  return out;
}

/** Validate and write one uploaded file to disk. Throws `UploadError` on reject. */
export async function saveUpload(file: File, options: SaveUploadOptions): Promise<SavedUpload> {
  if (options.maxBytes !== undefined && file.size > options.maxBytes) {
    throw new UploadError(`File "${file.name}" exceeds the ${options.maxBytes}-byte limit`);
  }
  if (options.allowedTypes && !isAllowed(file, options.allowedTypes)) {
    throw new UploadError(`File type not allowed: ${file.type || file.name || "unknown"}`);
  }

  const filename = sanitizeFilename(
    options.filename ? options.filename(file) : file.name || "upload",
  );
  const path = `${options.dir.replace(/[/\\]+$/, "")}/${filename}`;
  await Bun.write(path, file);
  return { path, filename, size: file.size, type: file.type };
}

function isAllowed(file: File, allowed: string[]): boolean {
  const type = (file.type || "").toLowerCase();
  const name = (file.name || "").toLowerCase();
  return allowed.some((entry) => {
    const e = entry.toLowerCase();
    return e.startsWith(".") ? name.endsWith(e) : type === e;
  });
}

/** Strip directory separators, traversal, and control chars from a filename. */
export function sanitizeFilename(name: string): string {
  const base = name
    .replace(/[/\\]+/g, "_") // path separators
    .replace(/\.\.+/g, ".") // collapse traversal dots
    // eslint-disable-next-line no-control-regex -- intentionally stripping control chars
    .replace(/[\x00-\x1f<>:"|?*]/g, "") // control + illegal chars
    .replace(/^\.+/, "") // no leading dots
    .trim();
  return base.length > 0 ? base.slice(0, 255) : "upload";
}
