/**
 * Human-readable file size (binary units).
 * @param bytes
 */
export function formatFileSize(bytes: number): string {
  if (bytes === 0) return '0 B';
  const units = ['B', 'KB', 'MB', 'GB', 'TB'] as const;
  const i = Math.min(
    Math.floor(Math.log(bytes) / Math.log(1024)),
    units.length - 1,
  );
  const n = bytes / 1024 ** i;
  return `${n < 10 && i > 0 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
}

function normalizeMime(mime: string): boolean {
  return mime.includes('/');
}

/**
 * Whether a file satisfies an HTML-like `accept` string (extensions and/or MIME, comma-separated).
 * @param file
 * @param accept
 */
export function fileMatchesAccept(file: File, accept: string): boolean {
  const tokens = accept
    .split(',')
    .map((t) => t.trim())
    .filter(Boolean);
  if (tokens.length === 0) return true;

  const name = file.name.toLowerCase();
  const type = file.type.toLowerCase();

  return tokens.some((token) => {
    const t = token.toLowerCase();
    if (t.startsWith('.')) {
      return name.endsWith(t);
    }
    if (normalizeMime(t)) {
      if (t.endsWith('/*')) {
        const prefix = t.slice(0, -1);
        return type.startsWith(prefix);
      }
      return type === t;
    }
    return false;
  });
}

/**
 * Constraints applied when validating an uploaded file.
 */
export interface FileValidationOptions {
  /** HTML-like `accept` string (extensions and/or MIME types, comma-separated). */
  accept?: string;
  /** Maximum allowed file size in bytes. */
  maxBytes?: number;
}

/**
 * A file that failed validation together with a human-readable reason.
 */
export interface FileValidationError {
  /** The file that failed validation. */
  file: File;
  /** Human-readable reason why the file failed. */
  message: string;
}

/**
 * Outcome of validating a batch of files against {@link FileValidationOptions}.
 */
export interface FileValidationResult {
  /** Files that passed validation. */
  valid: File[];
  /** Files that failed validation together with a reason. */
  errors: FileValidationError[];
}

/**
 * Validates a single file against the provided constraints.
 * @param file
 * @param options
 */
export function validateFile(
  file: File,
  options: FileValidationOptions,
): string | null {
  if (options.maxBytes != null && file.size > options.maxBytes) {
    return `File exceeds maximum size of ${formatFileSize(options.maxBytes)}`;
  }
  if (
    options.accept &&
    options.accept.trim() !== '' &&
    !fileMatchesAccept(file, options.accept)
  ) {
    return 'Unsupported file type';
  }
  return null;
}

/**
 * Validates a batch of files, partitioning them into `valid` and `errors`.
 * @param files
 * @param options
 */
export function validateFiles(
  files: File[],
  options: FileValidationOptions,
): FileValidationResult {
  const valid: File[] = [];
  const errors: FileValidationError[] = [];
  for (const file of files) {
    const msg = validateFile(file, options);
    if (msg) errors.push({ file, message: msg });
    else valid.push(file);
  }
  return { valid, errors };
}
