import {
  safeFileName,
  safeOutputFileName,
  splitFileName,
} from '../media/safe-file-name';

const DEFAULT_OUTPUT_NAME = 'output';
const MAX_OUTPUT_NAME_LENGTH = 120;

export interface OutputNameOptions {
  /**
   * When provided, this extension replaces an extension in `requestedName`.
   * Both `mp4` and `.mp4` are accepted.
   */
  readonly extension?: string;
  readonly fallback?: string;
}

export interface DerivedOutputNameOptions extends OutputNameOptions {
  readonly sourceName: string;
  readonly operation: string;
  readonly partIndex?: number;
  readonly partCount?: number;
}

/**
 * Converts an untrusted display name into a portable leaf filename. Directory
 * components, control characters, reserved device names and unsafe punctuation
 * are removed by `safeFileName`.
 */
export function safeExportFileName(
  requestedName: string,
  options: OutputNameOptions = {}
): string {
  const leafName = requestedName.split(/[\\/]/u).at(-1) ?? '';
  const fallback = safeFileName(options.fallback ?? DEFAULT_OUTPUT_NAME, {
    fallback: DEFAULT_OUTPUT_NAME,
    maxLength: 48,
    preserveExtension: false,
  });

  if (options.extension !== undefined) {
    const { stem } = splitFileName(leafName);
    return safeOutputFileName(stem, options.extension, fallback);
  }

  return safeFileName(leafName, {
    fallback,
    maxLength: MAX_OUTPUT_NAME_LENGTH,
  });
}

/**
 * Derives a predictable name from a source and operation. A numbered suffix is
 * included only for multi-output operations.
 */
export function deriveOutputFileName(
  options: DerivedOutputNameOptions
): string {
  validatePartOptions(options.partIndex, options.partCount);
  const sourceLeaf = safeExportFileName(options.sourceName, {
    fallback: 'source',
  });
  const sourceStem = splitFileName(sourceLeaf).stem;
  const operation = safeFileName(options.operation, {
    fallback: 'export',
    maxLength: 40,
    preserveExtension: false,
  });
  const partSuffix =
    options.partIndex === undefined
      ? ''
      : `-${String(options.partIndex + 1).padStart(
          Math.max(3, String(options.partCount ?? 1).length),
          '0'
        )}`;

  return safeExportFileName(`${sourceStem}-${operation}${partSuffix}`, {
    extension:
      options.extension ?? splitFileName(sourceLeaf).extension.slice(1),
    fallback: options.fallback,
  });
}

/**
 * Produces case-insensitively unique leaf filenames while retaining input
 * order. This avoids silent overwrites when an archive is extracted on a
 * case-insensitive filesystem.
 */
export function uniqueExportFileNames(
  requestedNames: readonly string[],
  fallback = DEFAULT_OUTPUT_NAME
): readonly string[] {
  const used = new Set<string>();
  const nextSuffix = new Map<string, number>();
  const names: string[] = [];

  for (const requestedName of requestedNames) {
    const safeName = safeExportFileName(requestedName, { fallback });
    const key = comparisonKey(safeName);
    if (!used.has(key)) {
      used.add(key);
      nextSuffix.set(key, 2);
      names.push(safeName);
      continue;
    }

    const { stem, extension } = splitFileName(safeName);
    let suffix = nextSuffix.get(key) ?? 2;
    let candidate: string;
    do {
      candidate = safeExportFileName(`${stem}-${suffix}${extension}`, {
        fallback,
      });
      suffix += 1;
    } while (used.has(comparisonKey(candidate)));

    nextSuffix.set(key, suffix);
    used.add(comparisonKey(candidate));
    names.push(candidate);
  }

  return Object.freeze(names);
}

export function isSafeArchiveLeafName(fileName: string): boolean {
  return (
    fileName.length > 0 &&
    fileName.length <= MAX_OUTPUT_NAME_LENGTH &&
    fileName !== '.' &&
    fileName !== '..' &&
    !fileName.includes('/') &&
    !fileName.includes('\\') &&
    !hasControlCharacters(fileName) &&
    safeExportFileName(fileName) === fileName
  );
}

function comparisonKey(fileName: string): string {
  return fileName.normalize('NFKC').toLocaleLowerCase('en-US');
}

function hasControlCharacters(value: string): boolean {
  return Array.from(value).some((character) => {
    const codePoint = character.codePointAt(0) ?? 0;
    return codePoint < 32 || codePoint === 127;
  });
}

function validatePartOptions(
  partIndex: number | undefined,
  partCount: number | undefined
): void {
  if (
    partCount !== undefined &&
    (!Number.isSafeInteger(partCount) || partCount < 1)
  ) {
    throw new RangeError('Part count must be a positive safe integer.');
  }
  if (
    partIndex !== undefined &&
    (!Number.isSafeInteger(partIndex) ||
      partIndex < 0 ||
      (partCount !== undefined && partIndex >= partCount))
  ) {
    throw new RangeError('Part index must be within the output part count.');
  }
}
