import type { ExportPreset } from './convert';
import { buildConvertPlan } from './convert';
import {
  assertMuxerMatchesExtension,
  createPlannedInput,
  createPlannedOutput,
  diagnostic,
  streamMapArguments,
  type SourceReference,
  type StreamSelection,
} from './command-utils';
import {
  CommandPlanError,
  freezeCommandPlan,
  type FFmpegCommandPlan,
} from './command-plan';

export interface AudioExtractionOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  /** Absolute ffprobe stream index. */
  readonly audioStreamIndex: number;
  readonly expectedDurationSeconds?: number;
  readonly mode: 'copy' | 'encode';
  readonly inputCodec?: string;
  readonly targetMuxer?: string;
  readonly targetExtension?: string;
  readonly preset?: ExportPreset;
}

export function buildAudioExtractionPlan(
  options: AudioExtractionOptions
): FFmpegCommandPlan {
  const audioStreamIndex = options.audioStreamIndex;
  if (!Number.isSafeInteger(audioStreamIndex) || audioStreamIndex < 0) {
    throw new RangeError('Invalid audio stream index');
  }
  if (options.mode === 'encode') {
    if (!options.preset || options.preset.video) {
      throw new TypeError(
        'Encoded audio extraction requires an audio-only preset'
      );
    }
    return buildConvertPlan({
      jobId: options.jobId,
      source: options.source,
      preset: options.preset,
      streamSelection: { audio: [audioStreamIndex] },
      operation: 'audio',
      expectedDurationSeconds: options.expectedDurationSeconds,
    });
  }
  if (!options.inputCodec || !options.targetMuxer || !options.targetExtension) {
    throw new TypeError(
      'Stream-copy audio extraction requires codec, target muxer, and extension'
    );
  }
  assertMuxerMatchesExtension(options.targetMuxer, options.targetExtension);
  if (!isAudioCopyCompatible(options.inputCodec, options.targetMuxer)) {
    throw new CommandPlanError(
      'Audio codec is not compatible with the target container',
      [
        diagnostic(
          'audio-copy-incompatible',
          'error',
          `${options.inputCodec} cannot be safely copied into ${options.targetMuxer}.`
        ),
      ]
    );
  }
  const input = createPlannedInput(options.jobId, options.source);
  const output = createPlannedOutput(options.jobId, {
    baseName: options.source.fileName,
    operation: 'audio',
    extension: options.targetExtension,
    role: 'audio',
  });
  return freezeCommandPlan({
    id: `${options.jobId}:audio-extract`,
    operation: 'audio-extract-copy',
    inputs: [input],
    temporaryFiles: [],
    args: [
      '-i',
      input.path,
      '-map',
      `0:${audioStreamIndex}`,
      '-vn',
      '-sn',
      '-dn',
      '-c:a',
      'copy',
      output.path,
    ],
    outputs: [output],
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
    requiredCapabilities: { muxers: [options.targetMuxer] },
    diagnostics: [
      diagnostic(
        'audio-stream-copy',
        'info',
        'The selected compatible audio stream is retained without re-encoding.'
      ),
    ],
  });
}

export interface StreamRemovalOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly selection: StreamSelection;
  readonly targetExtension: string;
  readonly targetMuxer: string;
  readonly expectedDurationSeconds?: number;
}

export function buildStreamRemovalPlan(
  options: StreamRemovalOptions
): FFmpegCommandPlan {
  assertMuxerMatchesExtension(options.targetMuxer, options.targetExtension);
  const mapArgs = streamMapArguments(options.selection);
  if (mapArgs.length === 0) {
    throw new RangeError('At least one output stream must remain');
  }
  const input = createPlannedInput(options.jobId, options.source);
  const output = createPlannedOutput(options.jobId, {
    baseName: options.source.fileName,
    operation: 'streams',
    extension: options.targetExtension,
  });
  return freezeCommandPlan({
    id: `${options.jobId}:stream-removal`,
    operation: 'stream-removal',
    inputs: [input],
    temporaryFiles: [],
    args: [
      '-i',
      input.path,
      ...mapArgs,
      '-c',
      'copy',
      '-map_metadata',
      '0',
      '-map_chapters',
      '0',
      output.path,
    ],
    outputs: [output],
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
    requiredCapabilities: { muxers: [options.targetMuxer] },
    diagnostics: [
      diagnostic(
        'stream-removal-copy',
        'info',
        'Only explicitly selected streams are copied to the output.'
      ),
    ],
  });
}

function isAudioCopyCompatible(codec: string, muxer: string): boolean {
  const rules: Readonly<Record<string, ReadonlySet<string>>> = {
    mp3: new Set(['mp3']),
    ipod: new Set(['aac', 'alac', 'mp3']),
    ogg: new Set(['vorbis', 'opus', 'flac']),
    opus: new Set(['opus']),
    wav: new Set(['pcm_s16le', 'pcm_s24le', 'pcm_f32le']),
    flac: new Set(['flac']),
    matroska: new Set([
      'aac',
      'alac',
      'mp3',
      'vorbis',
      'opus',
      'flac',
      'pcm_s16le',
    ]),
  };
  return rules[muxer]?.has(codec) ?? false;
}
