import type { FFFSType } from '@ffmpeg/ffmpeg';
import { EngineError } from './ffmpeg-error';
import type { EngineInputMode, FFmpegAdapter } from './ffmpeg.types';

const WORKER_FS = 'WORKERFS' as FFFSType;
const DEFAULT_COPY_LIMIT_BYTES = 256 * 1024 * 1024;

function safeJobId(jobId: string): string {
  const normalized = jobId
    .replaceAll(/[^a-zA-Z0-9_-]/g, '-')
    .replaceAll(/-+/g, '-')
    .replaceAll(/^[-_]+|[-_]+$/g, '')
    .slice(0, 80);
  return normalized || 'anonymous';
}

function safeExtension(fileName: string): string {
  const match = /\.([a-zA-Z0-9]{1,10})$/.exec(fileName);
  return match?.[1]?.toLowerCase() ?? 'bin';
}

async function createDirIfNeeded(
  ffmpeg: FFmpegAdapter,
  path: string
): Promise<void> {
  try {
    await ffmpeg.createDir(path);
  } catch {
    // Emscripten exposes no portable "exists" primitive. Creating an existing
    // parent is harmless, and a later mount/write will surface real failures.
  }
}

async function deleteFileIfPresent(
  ffmpeg: FFmpegAdapter,
  path: string
): Promise<void> {
  try {
    await ffmpeg.deleteFile(path);
  } catch {
    // Cleanup is intentionally idempotent.
  }
}

async function deleteDirIfEmpty(
  ffmpeg: FFmpegAdapter,
  path: string
): Promise<void> {
  try {
    await ffmpeg.deleteDir(path);
  } catch {
    // Parent or a terminated filesystem may already be gone.
  }
}

export interface MountedJobFiles {
  inputPaths: readonly string[];
  outputPaths: readonly string[];
  workDirectory: string;
  inputMode: EngineInputMode;
  /** Total size of every mounted source. */
  inputBytes: number;
  /** Exact bytes copied into MEMFS; always zero for WORKERFS. */
  copiedInputBytes: number;
  /**
   * Returns false when the per-job directories can still be observed after
   * cleanup. Callers must discard that engine instance in that case.
   */
  cleanup: () => Promise<boolean>;
}

export async function mountJobFiles(
  ffmpeg: FFmpegAdapter,
  jobId: string,
  files: readonly File[],
  outputNames: readonly string[],
  copyLimitBytes = DEFAULT_COPY_LIMIT_BYTES
): Promise<MountedJobFiles> {
  const id = safeJobId(jobId);
  const inputDirectory = `/input/job-${id}`;
  const workDirectory = `/work/job-${id}`;
  const inputPaths = files.map(
    (file, index) =>
      `${inputDirectory}/source-${index}.${safeExtension(file.name)}`
  );
  const outputPaths = outputNames.map((name, index) => {
    const extension = safeExtension(name);
    return `${workDirectory}/output-${index}.${extension}`;
  });
  const inputBytes = files.reduce((total, file) => total + file.size, 0);

  await createDirIfNeeded(ffmpeg, '/input');
  await createDirIfNeeded(ffmpeg, '/work');
  await createDirIfNeeded(ffmpeg, inputDirectory);
  await createDirIfNeeded(ffmpeg, workDirectory);

  let mounted = false;
  let inputMode: MountedJobFiles['inputMode'] = 'workerfs';
  try {
    await ffmpeg.mount(
      WORKER_FS,
      {
        blobs: files.map((file, index) => ({
          name: inputPaths[index]?.split('/').at(-1) ?? `source-${index}.bin`,
          data: file,
        })),
      },
      inputDirectory
    );
    mounted = true;
  } catch (mountError) {
    inputMode = 'memory-copy';
    if (inputBytes > copyLimitBytes) {
      throw new EngineError(
        'filesystem-failed',
        'This browser could not mount the source file without copying it, and the file exceeds the compatibility-mode memory limit.',
        {
          cause: mountError,
          details: `${inputBytes} bytes exceeds ${copyLimitBytes} bytes`,
        }
      );
    }
    try {
      for (const [index, file] of files.entries()) {
        const path = inputPaths[index];
        if (path) {
          await ffmpeg.writeFile(
            path,
            new Uint8Array(await file.arrayBuffer())
          );
        }
      }
    } catch (copyError) {
      throw new EngineError(
        'filesystem-failed',
        'The input could not be copied into the FFmpeg virtual filesystem.',
        { cause: copyError }
      );
    }
  }

  return {
    inputPaths,
    outputPaths,
    workDirectory,
    inputMode,
    inputBytes,
    copiedInputBytes: inputMode === 'memory-copy' ? inputBytes : 0,
    cleanup: async () => {
      for (const path of outputPaths) await deleteFileIfPresent(ffmpeg, path);
      if (mounted) {
        try {
          await ffmpeg.unmount(inputDirectory);
        } catch {
          // The engine may have been terminated by cancellation.
        }
      } else {
        for (const path of inputPaths) await deleteFileIfPresent(ffmpeg, path);
      }
      await deleteDirIfEmpty(ffmpeg, inputDirectory);
      await deleteDirIfEmpty(ffmpeg, workDirectory);
      try {
        const [inputEntries, workEntries] = await Promise.all([
          ffmpeg.listDir('/input'),
          ffmpeg.listDir('/work'),
        ]);
        const inputName = inputDirectory.split('/').at(-1);
        const workName = workDirectory.split('/').at(-1);
        return (
          !inputEntries.some((entry) => entry.name === inputName) &&
          !workEntries.some((entry) => entry.name === workName)
        );
      } catch {
        // A terminated engine has no inspectable filesystem. Its caller owns
        // recovery; a live caller conservatively recreates the instance.
        return false;
      }
    },
  };
}

export function describeMountedInputAccess(
  mounted: Pick<
    MountedJobFiles,
    'inputMode' | 'inputBytes' | 'copiedInputBytes'
  >
): string {
  return mounted.inputMode === 'workerfs'
    ? `Input access: ${mounted.inputBytes} bytes mounted read-only through WORKERFS; 0 bytes copied into engine memory.`
    : `Input compatibility fallback: WORKERFS mounting was unavailable, so ${mounted.copiedInputBytes} bytes were copied into engine memory.`;
}
