const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
const COMBINING_MARKS = /[\u0300-\u036f]/g;
const UNSAFE_CHARACTERS = /[^A-Za-z0-9._-]+/g;
const REPEATED_SEPARATORS = /[-_]{2,}/g;

export interface SafeFileNameOptions {
  fallback?: string;
  maxLength?: number;
  preserveExtension?: boolean;
}

export function safeFileName(
  originalName: string,
  options: SafeFileNameOptions = {}
): string {
  const fallback = sanitizePart(options.fallback ?? 'file') || 'file';
  const maxLength = options.maxLength ?? 120;
  if (!Number.isInteger(maxLength) || maxLength < 8 || maxLength > 255) {
    throw new RangeError('Maximum filename length must be from 8 to 255.');
  }

  const basename = originalName.split(/[\\/]/).at(-1) ?? '';
  const normalized = removeControlCharacters(
    basename.normalize('NFKD').replace(COMBINING_MARKS, '')
  ).trim();
  const { stem, extension } =
    options.preserveExtension === false
      ? { stem: normalized, extension: '' }
      : splitFileName(normalized);
  const safeStem =
    sanitizePart(stem)
      .replace(/\.{2,}/g, '.')
      .replace(/^\.+|\.+$/g, '') || fallback;
  const safeExtension =
    options.preserveExtension === false ? '' : sanitizeExtension(extension);
  const reservedStem = WINDOWS_RESERVED_NAME.test(safeStem)
    ? `_${safeStem}`
    : safeStem;

  const availableStemLength = Math.max(1, maxLength - safeExtension.length);
  const truncatedStem =
    reservedStem.slice(0, availableStemLength).replace(/[._-]+$/g, '') ||
    fallback.slice(0, availableStemLength);
  return `${truncatedStem}${safeExtension}`.slice(0, maxLength);
}

export function createSafeVirtualFileName(
  originalName: string,
  index: number,
  prefix = 'source'
): string {
  if (!Number.isSafeInteger(index) || index < 0) {
    throw new RangeError('File index must be a non-negative safe integer.');
  }
  const sanitized = safeFileName(originalName, {
    fallback: prefix,
    maxLength: 96,
  });
  const { extension } = splitFileName(sanitized);
  return `${safeFileName(prefix, {
    preserveExtension: false,
    maxLength: 40,
  })}-${index}${extension}`;
}

export function safeOutputFileName(
  requestedStem: string,
  extension: string,
  fallback = 'output'
): string {
  const normalizedExtension = sanitizeExtension(
    extension.startsWith('.') ? extension : `.${extension}`
  );
  const stem = safeFileName(requestedStem, {
    fallback,
    preserveExtension: false,
    maxLength: Math.max(8, 120 - normalizedExtension.length),
  });
  return `${stem}${normalizedExtension}`;
}

export function splitFileName(name: string): {
  stem: string;
  extension: string;
} {
  const lastDot = name.lastIndexOf('.');
  if (lastDot <= 0 || lastDot === name.length - 1) {
    return { stem: name, extension: '' };
  }
  const extension = name.slice(lastDot);
  if (extension.length > 17) {
    return { stem: name, extension: '' };
  }
  return { stem: name.slice(0, lastDot), extension };
}

function sanitizePart(value: string): string {
  return value
    .normalize('NFKD')
    .replace(COMBINING_MARKS, '')
    .replace(UNSAFE_CHARACTERS, '-')
    .replace(REPEATED_SEPARATORS, (separator) => separator[0] ?? '-')
    .replace(/^[._-]+|[._-]+$/g, '');
}

function sanitizeExtension(extension: string): string {
  if (!extension) {
    return '';
  }
  const parts = extension
    .replace(/^\.+/, '')
    .normalize('NFKD')
    .replace(COMBINING_MARKS, '')
    .split('.')
    .map((part) => part.replace(/[^A-Za-z0-9]/g, '').slice(0, 16))
    .filter(Boolean);
  return parts.length > 0 ? `.${parts.join('.').toLowerCase()}` : '';
}

function removeControlCharacters(value: string): string {
  return Array.from(value)
    .filter((character) => {
      const codePoint = character.codePointAt(0) ?? 0;
      return codePoint >= 32 && codePoint !== 127;
    })
    .join('');
}
