import type { LogEvent, ProgressEvent } from '@ffmpeg/ffmpeg';
import { queryCapabilities } from './capabilities';
import {
  detectEngineEnvironment,
  selectEngineMode,
  supportsMultithread,
} from './engine-mode';
import { EngineError, toEngineError } from './ffmpeg-error';
import type {
  EngineJobRequest,
  EngineJobResult,
  EngineJobContext,
  EngineLogListener,
  EngineMode,
  EnginePreference,
  EngineProgressListener,
  EngineState,
  EngineStateListener,
  FFmpegAdapter,
  FFmpegCapabilities,
  ProbeResult,
} from './ffmpeg.types';
import { createLoadConfig } from './load-config';
import { LogBuffer } from './log-buffer';
import { normalizeProgress } from './progress-parser';
import { describeMountedInputAccess, mountJobFiles } from './virtual-fs';
import {
  parseFFmpegProgress,
  progressUpdateFromFFmpeg,
} from '../jobs/progress';

type FFmpegFactory = () => Promise<FFmpegAdapter>;

export const DEFAULT_JOB_TIMEOUT_MILLISECONDS = 4 * 60 * 60 * 1_000;
export const MIN_PROBE_TIMEOUT_MILLISECONDS = 30_000;
export const MAX_PROBE_TIMEOUT_MILLISECONDS = 5 * 60_000;
export const PROBE_BUDGET_BYTES_PER_SECOND = 2 * 1024 * 1024;
export const MULTITHREAD_CODEC_THREAD_LIMIT = 2;
/**
 * The pinned Emscripten core can retain native heap state across heterogeneous
 * FFmpeg invocations even after every virtual file has been removed. Recycling
 * the idle worker before this many media jobs bounds that accumulation without
 * interrupting an active command or discarding application-level results.
 */
export const MAX_EXECUTIONS_PER_ENGINE = 16;
/** The pinned tile filter aborts on a fourth consecutive contact-sheet job. */
export const MAX_CONTACT_SHEETS_PER_ENGINE = 3;

export function shouldRecycleExecutionCore(
  completedExecutions: number,
  nextOperation?: string,
  completedContactSheets = 0
): boolean {
  return (
    completedExecutions >= MAX_EXECUTIONS_PER_ENGINE ||
    (nextOperation === 'contact-sheet' &&
      completedContactSheets >= MAX_CONTACT_SHEETS_PER_ENGINE)
  );
}

export function probeTimeoutMilliseconds(inputBytes: number): number {
  if (!Number.isFinite(inputBytes) || inputBytes <= 0) {
    return MIN_PROBE_TIMEOUT_MILLISECONDS;
  }
  const sizeAwareBudget = Math.ceil(
    (inputBytes / PROBE_BUDGET_BYTES_PER_SECOND) * 1_000
  );
  return Math.min(
    MAX_PROBE_TIMEOUT_MILLISECONDS,
    Math.max(MIN_PROBE_TIMEOUT_MILLISECONDS, sizeAwareBudget)
  );
}

async function defaultFFmpegFactory(): Promise<FFmpegAdapter> {
  const { FFmpeg } = await import('@ffmpeg/ffmpeg');
  return new FFmpeg();
}

function textFromFileData(data: Uint8Array | string): string {
  return typeof data === 'string' ? data : new TextDecoder().decode(data);
}

function bytesFromFileData(data: Uint8Array | string): Uint8Array {
  return typeof data === 'string' ? new TextEncoder().encode(data) : data;
}

export class EngineManager {
  readonly #factory: FFmpegFactory;
  readonly #stateListeners = new Set<EngineStateListener>();
  readonly #logListeners = new Set<EngineLogListener>();
  readonly #progressListeners = new Set<EngineProgressListener>();
  readonly #logs = new LogBuffer();
  #engine: FFmpegAdapter | null = null;
  #state: EngineState = { status: 'idle' };
  #mode: EngineMode | null = null;
  #preference: EnginePreference = 'automatic';
  #capabilities: FFmpegCapabilities | null = null;
  #warning: Extract<EngineState, { status: 'ready' }>['warning'];
  #loadPromise: Promise<void> | null = null;
  #activeJobId: string | null = null;
  #activeExpectedDurationSeconds: number | undefined;
  #activeStartedAtMilliseconds = 0;
  #machineProgressLines: string[] = [];
  #machineProgressSeen = false;
  #cancelledJobIds = new Set<string>();
  #executionsSinceLoad = 0;
  #contactSheetsSinceLoad = 0;
  #generation = 0;

  readonly #handleLog = (event: LogEvent): void => {
    this.#logs.append(event);
    if (this.#state.status === 'running') {
      for (const line of event.message.split(/\r?\n/gu)) {
        if (/^(?:frame|fps|out_time_us|out_time|speed|progress)=/u.test(line)) {
          this.#machineProgressLines.push(line);
        }
        if (/^progress=(?:continue|end)$/u.test(line)) {
          const parsed = parseFFmpegProgress(
            this.#machineProgressLines.join('\n')
          );
          const update = progressUpdateFromFFmpeg(
            parsed,
            this.#activeExpectedDurationSeconds
          );
          this.#machineProgressLines = [];
          this.#machineProgressSeen = true;
          this.#setState({
            ...this.#state,
            ...(update.progress !== undefined
              ? { progress: update.progress }
              : {}),
            ...(update.speed !== undefined ? { speed: update.speed } : {}),
            ...(update.frame !== undefined ? { frame: update.frame } : {}),
            elapsedSeconds: Math.max(
              0,
              (performance.now() - this.#activeStartedAtMilliseconds) / 1_000
            ),
          });
        }
      }
    }
    for (const listener of this.#logListeners) listener(event);
  };

  readonly #handleProgress = (event: ProgressEvent): void => {
    const normalized = normalizeProgress(event);
    if (
      this.#state.status === 'running' &&
      !this.#machineProgressSeen &&
      normalized.progress !== this.#state.progress
    ) {
      this.#setState({
        ...this.#state,
        progress: Math.min(0.99, normalized.progress),
        elapsedSeconds: Math.max(
          0,
          (performance.now() - this.#activeStartedAtMilliseconds) / 1_000
        ),
      });
    }
    for (const listener of this.#progressListeners) listener(normalized);
  };

  constructor(factory: FFmpegFactory = defaultFFmpegFactory) {
    this.#factory = factory;
  }

  get state(): EngineState {
    return this.#state;
  }

  get preference(): EnginePreference {
    return this.#preference;
  }

  setPreference(preference: EnginePreference): void {
    if (this.#state.status === 'running') {
      throw new EngineError(
        'busy',
        'Engine mode cannot be changed while a job is running.'
      );
    }
    if (preference !== this.#preference) {
      this.#preference = preference;
      this.dispose();
    }
  }

  subscribe(listener: EngineStateListener): () => void {
    this.#stateListeners.add(listener);
    listener(this.#state);
    return () => this.#stateListeners.delete(listener);
  }

  onLog(listener: EngineLogListener): () => void {
    this.#logListeners.add(listener);
    return () => this.#logListeners.delete(listener);
  }

  onProgress(listener: EngineProgressListener): () => void {
    this.#progressListeners.add(listener);
    return () => this.#progressListeners.delete(listener);
  }

  async initialize(): Promise<void> {
    if (this.#activeJobId || this.#state.status === 'running') {
      throw new EngineError(
        'busy',
        'The FFmpeg engine cannot be initialized while another operation is active.'
      );
    }
    if (this.#state.status === 'ready') return;
    if (this.#loadPromise) return this.#loadPromise;
    this.#loadPromise = this.#initializeInternal().finally(() => {
      this.#loadPromise = null;
    });
    return this.#loadPromise;
  }

  async #initializeInternal(): Promise<void> {
    const environment = detectEngineEnvironment();
    if (!environment.secureContext || typeof Worker === 'undefined') {
      const error = new EngineError(
        'unsupported-environment',
        'FFmpeg requires a secure browser context with Web Worker support.'
      );
      this.#setState({ status: 'error', error });
      throw error;
    }

    const requestedMode = selectEngineMode(environment, this.#preference);
    const unsupportedMultithread =
      this.#preference === 'prefer-multithread' &&
      !supportsMultithread(environment);
    const initialWarning = unsupportedMultithread
      ? {
          code: 'multithread-unavailable' as const,
          message:
            'Multithreading is unavailable in this browser context; using the single-thread compatibility core.',
        }
      : undefined;

    try {
      await this.#loadMode(requestedMode, initialWarning);
    } catch (firstError) {
      if (requestedMode !== 'multithread') {
        const details = describeInitializationFailure(firstError);
        this.#terminateEngine();
        const error = new EngineError(
          'load-failed',
          'The FFmpeg compatibility core could not be loaded.',
          { cause: firstError, details }
        );
        this.#setState({ status: 'error', error });
        throw error;
      }

      this.#terminateEngine();
      const warning = {
        code: 'multithread-load-failed' as const,
        message:
          'The multithread core failed to load. The app recovered with the single-thread compatibility core.',
      };
      this.#setState({
        status: 'recovering',
        previousMode: 'multithread',
        warning,
      });
      try {
        await this.#loadMode('single-thread', warning);
      } catch (fallbackError) {
        const details = [
          `Multithread core: ${describeInitializationFailure(firstError)}`,
          `Single-thread core: ${describeInitializationFailure(fallbackError)}`,
        ].join('\n');
        this.#terminateEngine();
        const error = new EngineError(
          'load-failed',
          'Neither FFmpeg execution core could be loaded.',
          {
            cause: fallbackError,
            details,
          }
        );
        this.#setState({ status: 'error', error });
        throw error;
      }
    }
  }

  async #loadMode(
    mode: EngineMode,
    warning?: Extract<EngineState, { status: 'ready' }>['warning']
  ): Promise<void> {
    const generation = ++this.#generation;
    this.#setState({
      status: 'loading',
      mode,
      progress: 0.05,
      stage: 'Loading FFmpeg wrapper',
    });
    const engine = await this.#factory();
    if (generation !== this.#generation) return;
    this.#engine = engine;
    this.#mode = mode;
    engine.on('log', this.#handleLog);
    engine.on('progress', this.#handleProgress);
    this.#setState({
      status: 'loading',
      mode,
      progress: 0.2,
      stage: 'Loading local WebAssembly core',
    });
    await engine.load(createLoadConfig(mode));
    if (generation !== this.#generation) return;
    this.#setState({
      status: 'loading',
      mode,
      progress: 0.75,
      stage: 'Inspecting codec capabilities',
    });
    const capabilities = await queryCapabilities(engine);
    if (generation !== this.#generation) return;
    this.#capabilities = capabilities;
    this.#executionsSinceLoad = 0;
    this.#contactSheetsSinceLoad = 0;
    this.#warning = warning;
    this.#setState({ status: 'ready', mode, capabilities, warning });
  }

  async runJob(request: EngineJobRequest): Promise<EngineJobResult> {
    if (this.#activeJobId) {
      throw new EngineError(
        'busy',
        'Another FFmpeg operation is already running.'
      );
    }
    await this.initialize();
    const coreWorkload = request.coreWorkload ?? request.operation;
    await this.#recycleExecutionCoreIfNeeded(coreWorkload);
    const engine = this.#engine;
    const mode = this.#mode;
    if (!engine || !mode || this.#state.status !== 'ready') {
      throw new EngineError('load-failed', 'The FFmpeg engine is not ready.');
    }

    const mounted = await mountJobFiles(
      engine,
      request.id,
      request.inputs,
      request.outputs.map(({ name }) => name)
    );
    const generation = this.#generation;
    const temporaryPaths = (request.temporaryFiles ?? []).map(
      (temporary, index) => {
        const extensionMatch = /\.([a-zA-Z0-9]{1,10})$/.exec(temporary.name);
        const extension = extensionMatch?.[1]?.toLowerCase() ?? 'tmp';
        return `${mounted.workDirectory}/temporary-${index}.${extension}`;
      }
    );
    this.#activeJobId = request.id;
    this.#activeExpectedDurationSeconds = request.expectedDurationSeconds;
    this.#activeStartedAtMilliseconds = performance.now();
    this.#machineProgressLines = [];
    this.#machineProgressSeen = false;
    this.#logs.clear();
    this.#setState({
      status: 'running',
      mode,
      jobId: request.id,
      operation: request.operation,
      inputMode: mounted.inputMode,
      inputBytes: mounted.inputBytes,
      copiedInputBytes: mounted.copiedInputBytes,
      progress: 0,
    });
    this.#appendInternalLog({
      type: mounted.inputMode === 'workerfs' ? 'stdout' : 'stderr',
      message: describeMountedInputAccess(mounted),
    });

    let completedResult: Omit<EngineJobResult, 'timings'> | undefined;
    let conversionMilliseconds: number;
    let outputReadMilliseconds: number;
    let cleanupMilliseconds: number;
    let coreAborted = false;
    try {
      const context: EngineJobContext = {
        inputPaths: mounted.inputPaths,
        temporaryPaths,
        outputPaths: mounted.outputPaths,
        workDirectory: mounted.workDirectory,
      };
      for (const [index, temporary] of (
        request.temporaryFiles ?? []
      ).entries()) {
        const path = temporaryPaths[index];
        if (path) {
          const content =
            typeof temporary.content === 'function'
              ? temporary.content(context)
              : temporary.content;
          await engine.writeFile(
            path,
            typeof content === 'string' ? content : new Uint8Array(content)
          );
        }
      }
      const args = prepareEngineArguments(
        request.buildArguments(context),
        mode,
        mounted.inputPaths,
        mounted.outputPaths
      );
      const conversionStartedAt = performance.now();
      const exitCode = await engine.exec(
        args,
        engineJobTimeoutMilliseconds(request)
      );
      if (generation === this.#generation && this.#engine === engine) {
        this.#executionsSinceLoad += 1;
        if (coreWorkload === 'contact-sheet') {
          this.#contactSheetsSinceLoad += 1;
        }
      }
      conversionMilliseconds = performance.now() - conversionStartedAt;
      if (this.#cancelledJobIds.delete(request.id)) {
        throw new EngineError('cancelled', 'The FFmpeg job was cancelled.');
      }
      if (exitCode !== 0) {
        coreAborted = /(?:^|\n)Aborted\(\)(?:\n|$)/u.test(this.#logs.text());
        throw new EngineError(
          'execution-failed',
          `FFmpeg exited with code ${exitCode}.`,
          {
            exitCode,
            details: this.#logs.text(),
          }
        );
      }
      const outputReadStartedAt = performance.now();
      const outputs = await Promise.all(
        mounted.outputPaths.map(async (path, index) => {
          const bytes = bytesFromFileData(await engine.readFile(path));
          if (bytes.byteLength === 0) {
            throw new EngineError(
              'execution-failed',
              `FFmpeg produced an empty expected output: ${request.outputs[index]?.name ?? `output-${index}`}.`,
              { details: this.#logs.text() }
            );
          }
          return {
            name: request.outputs[index]?.name ?? `output-${index}`,
            mimeType:
              request.outputs[index]?.mimeType ?? 'application/octet-stream',
            bytes,
          };
        })
      );
      outputReadMilliseconds = performance.now() - outputReadStartedAt;
      completedResult = {
        id: request.id,
        exitCode: 0,
        inputMode: mounted.inputMode,
        inputBytes: mounted.inputBytes,
        copiedInputBytes: mounted.copiedInputBytes,
        outputs,
        logs: this.#logs.snapshot(),
      };
    } catch (error) {
      if (this.#cancelledJobIds.delete(request.id)) {
        throw new EngineError('cancelled', 'The FFmpeg job was cancelled.', {
          cause: error,
        });
      }
      throw toEngineError(error);
    } finally {
      const cleanupStartedAt = performance.now();
      this.#activeJobId = null;
      this.#activeExpectedDurationSeconds = undefined;
      this.#machineProgressLines = [];
      this.#machineProgressSeen = false;
      if (generation === this.#generation && this.#engine === engine) {
        for (const path of temporaryPaths) {
          try {
            await engine.deleteFile(path);
          } catch {
            // Cleanup below will recover or surface a later filesystem fault.
          }
        }
        const cleanupSucceeded = await mounted.cleanup();
        if (
          (!cleanupSucceeded || coreAborted) &&
          generation === this.#generation &&
          this.#engine === engine
        ) {
          this.#logs.append({
            type: 'stderr',
            message: coreAborted
              ? 'The FFmpeg WebAssembly core aborted; recreating it before the next job.'
              : 'Filesystem cleanup could not be verified; recreating the FFmpeg engine.',
          });
          this.#terminateEngine();
          this.#setState({ status: 'recovering', previousMode: mode });
          try {
            await this.initialize();
          } catch {
            // initialize() publishes the controlled error state.
          }
        } else if (this.#isRunning()) {
          const capabilities = this.#capabilities;
          if (capabilities) {
            this.#setState({
              status: 'ready',
              mode,
              capabilities,
              warning: this.#warning,
            });
          }
        }
      }
      cleanupMilliseconds = performance.now() - cleanupStartedAt;
    }
    if (!completedResult) {
      throw new EngineError(
        'execution-failed',
        'FFmpeg completed without returning an execution result.'
      );
    }
    return {
      ...completedResult,
      timings: Object.freeze({
        conversionMilliseconds,
        outputReadMilliseconds,
        cleanupMilliseconds,
      }),
    };
  }

  async probe(
    file: File,
    timeoutMilliseconds = probeTimeoutMilliseconds(file.size),
    signal?: AbortSignal
  ): Promise<ProbeResult> {
    if (signal?.aborted) {
      throw new EngineError('cancelled', 'The media probe was cancelled.');
    }
    await this.initialize();
    if (signal?.aborted) {
      throw new EngineError('cancelled', 'The media probe was cancelled.');
    }
    const engine = this.#engine;
    const mode = this.#mode;
    if (!engine || !mode || this.#state.status !== 'ready') {
      throw new EngineError('load-failed', 'The FFmpeg engine is not ready.');
    }
    if (this.#activeJobId) {
      throw new EngineError(
        'busy',
        'Another FFmpeg operation is already running.'
      );
    }

    const id = crypto.randomUUID();
    const mounted = await mountJobFiles(engine, id, [file], ['probe.json']);
    if (signal?.aborted) {
      await mounted.cleanup();
      throw new EngineError('cancelled', 'The media probe was cancelled.');
    }
    const probeOutput = mounted.outputPaths[0];
    const inputPath = mounted.inputPaths[0];
    if (!probeOutput || !inputPath) {
      throw new EngineError(
        'filesystem-failed',
        'Could not allocate probe paths.'
      );
    }
    const activeProbeId = `probe-${id}`;
    this.#activeJobId = activeProbeId;
    this.#activeExpectedDurationSeconds = undefined;
    this.#activeStartedAtMilliseconds = performance.now();
    this.#machineProgressLines = [];
    this.#machineProgressSeen = false;
    this.#logs.clear();
    this.#setState({
      status: 'running',
      mode,
      jobId: this.#activeJobId,
      operation: 'Inspect media',
      inputMode: mounted.inputMode,
      inputBytes: mounted.inputBytes,
      copiedInputBytes: mounted.copiedInputBytes,
    });
    this.#appendInternalLog({
      type: mounted.inputMode === 'workerfs' ? 'stdout' : 'stderr',
      message: describeMountedInputAccess(mounted),
    });
    let timedOut = false;
    const watchdog = setTimeout(() => {
      timedOut = true;
      void this.cancelActive();
    }, timeoutMilliseconds);
    const cancelOnAbort = () => {
      void this.cancelActive();
    };
    signal?.addEventListener('abort', cancelOnAbort, { once: true });
    try {
      const exitCode = await engine.ffprobe([
        '-v',
        'error',
        '-show_format',
        '-show_streams',
        '-show_chapters',
        '-of',
        'json',
        inputPath,
        '-o',
        probeOutput,
      ]);
      let parsed: unknown;
      try {
        parsed = JSON.parse(
          textFromFileData(await engine.readFile(probeOutput))
        ) as unknown;
      } catch (readError) {
        throw new EngineError(
          'probe-failed',
          exitCode === 0
            ? 'ffprobe did not produce valid JSON.'
            : `ffprobe exited with code ${exitCode} without a valid report.`,
          {
            cause: readError,
            exitCode,
            details: this.#logs.text(),
          }
        );
      }
      if (!isProbeReportShape(parsed)) {
        throw new EngineError(
          'probe-failed',
          'ffprobe produced JSON that does not have a media report shape.',
          { exitCode, details: this.#logs.text() }
        );
      }
      if (!probeReportHasStreams(parsed)) {
        throw new EngineError(
          'probe-failed',
          'ffprobe did not find any media streams.',
          { exitCode, details: this.#logs.text() }
        );
      }
      if (exitCode !== 0) {
        // The pinned 0.12.10 browser core is observed to return -1 after
        // successfully writing a complete report. The file is the source of
        // truth only in this narrow, validated case; absent/malformed output
        // above still fails.
        this.#logs.append({
          type: 'stderr',
          message: `Compatibility note: ffprobe returned ${exitCode}, but its complete JSON report was validated and accepted.`,
        });
      }
      return {
        json: parsed,
        inputMode: mounted.inputMode,
        inputBytes: mounted.inputBytes,
        copiedInputBytes: mounted.copiedInputBytes,
        logs: this.#logs.snapshot(),
      };
    } catch (error) {
      if (this.#cancelledJobIds.delete(activeProbeId)) {
        if (timedOut) {
          throw new EngineError(
            'probe-failed',
            `Media inspection exceeded its ${String(Math.ceil(timeoutMilliseconds / 1_000))}-second safety limit.`,
            { cause: error }
          );
        }
        throw new EngineError('cancelled', 'The media probe was cancelled.', {
          cause: error,
        });
      }
      throw error instanceof EngineError
        ? error
        : new EngineError('probe-failed', 'The media could not be inspected.', {
            cause: error,
            details: this.#logs.text(),
          });
    } finally {
      signal?.removeEventListener('abort', cancelOnAbort);
      clearTimeout(watchdog);
      this.#activeJobId = null;
      this.#machineProgressLines = [];
      this.#machineProgressSeen = false;
      if (this.#engine === engine) {
        const cleanupSucceeded = await mounted.cleanup();
        if (!cleanupSucceeded && this.#engine === engine) {
          this.#logs.append({
            type: 'stderr',
            message:
              'Probe filesystem cleanup could not be verified; recreating the FFmpeg engine.',
          });
          this.#terminateEngine();
          this.#setState({ status: 'recovering', previousMode: mode });
          try {
            await this.initialize();
          } catch {
            // initialize() publishes the controlled error state.
          }
        } else if (this.#isRunning()) {
          const capabilities = this.#capabilities;
          if (capabilities) {
            this.#setState({
              status: 'ready',
              mode,
              capabilities,
              warning: this.#warning,
            });
          }
        }
      }
    }
  }

  async cancelActive(): Promise<void> {
    const activeJobId = this.#activeJobId;
    const previousMode = this.#mode;
    if (!activeJobId || !previousMode) return;
    this.#cancelledJobIds.add(activeJobId);
    this.#terminateEngine();
    this.#activeJobId = null;
    this.#activeExpectedDurationSeconds = undefined;
    this.#machineProgressLines = [];
    this.#machineProgressSeen = false;
    this.#setState({ status: 'recovering', previousMode });
    try {
      await this.initialize();
    } catch {
      // initialize() already publishes the controlled error state.
    }
  }

  dispose(): void {
    this.#terminateEngine();
    this.#activeJobId = null;
    this.#setState({ status: 'idle' });
  }

  #terminateEngine(): void {
    this.#generation += 1;
    if (this.#engine) {
      this.#engine.off('log', this.#handleLog);
      this.#engine.off('progress', this.#handleProgress);
      this.#engine.terminate();
    }
    this.#engine = null;
    this.#mode = null;
    this.#capabilities = null;
    this.#warning = undefined;
    this.#executionsSinceLoad = 0;
    this.#contactSheetsSinceLoad = 0;
  }

  async #recycleExecutionCoreIfNeeded(nextOperation: string): Promise<void> {
    if (
      !shouldRecycleExecutionCore(
        this.#executionsSinceLoad,
        nextOperation,
        this.#contactSheetsSinceLoad
      )
    ) {
      return;
    }
    const previousMode = this.#mode;
    if (
      !previousMode ||
      this.#activeJobId ||
      this.#state.status === 'running'
    ) {
      return;
    }
    const contactSheetGuard =
      nextOperation === 'contact-sheet' &&
      this.#contactSheetsSinceLoad >= MAX_CONTACT_SHEETS_PER_ENGINE;
    this.#appendInternalLog({
      type: 'stderr',
      message: contactSheetGuard
        ? `Refreshing the idle FFmpeg WebAssembly core before contact sheet ${this.#contactSheetsSinceLoad + 1} to release retained tile-filter state.`
        : `Refreshing the idle FFmpeg WebAssembly core after ${this.#executionsSinceLoad} media jobs to release retained native heap state.`,
    });
    this.#terminateEngine();
    this.#setState({ status: 'recovering', previousMode });
    await this.initialize();
  }

  #setState(state: EngineState): void {
    this.#state = state;
    for (const listener of this.#stateListeners) listener(state);
  }

  #appendInternalLog(event: LogEvent): void {
    this.#logs.append(event);
    for (const listener of this.#logListeners) listener(event);
  }

  #isRunning(): boolean {
    return this.#state.status === 'running';
  }
}

/**
 * The pinned core-mt build can stall on Chromium when FFmpeg auto-selects a
 * large codec thread count. Keep the WASM pthread core, but bound decoder and
 * encoder codec threads to the smallest upstream-reported stable setting.
 */
export function prepareEngineArguments(
  original: readonly string[],
  mode: EngineMode,
  inputPaths: readonly string[],
  outputPaths: readonly string[]
): string[] {
  const args = [...original];
  if (mode !== 'multithread' || args.length === 0) return args;

  const constrained: string[] = [];
  const inputPathSet = new Set(inputPaths);
  const outputPathSet = new Set(outputPaths);
  let constrainedOutput = false;

  for (const [index, argument] of args.entries()) {
    if (argument === '-i' && inputPathSet.has(args[index + 1] ?? '')) {
      constrained.push(
        '-threads',
        String(MULTITHREAD_CODEC_THREAD_LIMIT),
        '-i'
      );
      continue;
    }
    if (outputPathSet.has(argument)) {
      constrained.push(
        '-threads',
        String(MULTITHREAD_CODEC_THREAD_LIMIT),
        argument
      );
      constrainedOutput = true;
      continue;
    }
    constrained.push(argument);
  }

  // Segment/image sequence plans can use a pattern instead of an exact
  // expected output path. Every typed command plan keeps its output URL last.
  if (!constrainedOutput && constrained.length > 0) {
    constrained.splice(
      constrained.length - 1,
      0,
      '-threads',
      String(MULTITHREAD_CODEC_THREAD_LIMIT)
    );
  }
  return constrained;
}

export function engineJobTimeoutMilliseconds(
  request: Pick<
    EngineJobRequest,
    'expectedDurationSeconds' | 'timeoutMilliseconds'
  >
): number {
  const inferred =
    request.expectedDurationSeconds === undefined
      ? DEFAULT_JOB_TIMEOUT_MILLISECONDS
      : Math.max(120_000, request.expectedDurationSeconds * 30_000);
  return Math.min(
    request.timeoutMilliseconds ?? inferred,
    DEFAULT_JOB_TIMEOUT_MILLISECONDS
  );
}

export const engineManager = new EngineManager();

function describeInitializationFailure(error: unknown): string {
  if (error instanceof EngineError) {
    return error.details ? `${error.message}\n${error.details}` : error.message;
  }
  if (error instanceof Error) {
    return error.stack ?? error.message;
  }
  return String(error);
}

function isProbeReportShape(value: unknown): boolean {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    return false;
  }
  const report = value as Record<string, unknown>;
  return (
    (Array.isArray(report.streams) || report.format !== undefined) &&
    (report.format === undefined ||
      (typeof report.format === 'object' && report.format !== null))
  );
}

function probeReportHasStreams(value: unknown): boolean {
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
    return false;
  }
  const streams = (value as Record<string, unknown>).streams;
  return Array.isArray(streams) && streams.length > 0;
}
