import type {
  BuiltInExportPreset,
  UserExportPreset,
} from '../presets/preset.types';
import { presetEncodingArguments } from '../presets/preset-registry';
import {
  assertMuxerMatchesExtension,
  createPlannedInput,
  createPlannedOutput,
  diagnostic,
  mergeRequirements,
  streamMapArguments,
  type SourceReference,
  type StreamSelection,
} from './command-utils';
import {
  freezeCommandPlan,
  type FFmpegCommandPlan,
  type PresetRequirements,
} from './command-plan';

export type ExportPreset = BuiltInExportPreset | UserExportPreset;

export interface ConvertOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly preset: ExportPreset;
  readonly streamSelection?: StreamSelection;
  readonly expectedDurationSeconds?: number;
  readonly timeoutMs?: number;
  readonly operation?: string;
  readonly videoFiltergraph?: string;
  readonly audioFiltergraph?: string;
  readonly additionalRequirements?: PresetRequirements;
}

export function buildConvertPlan(options: ConvertOptions): FFmpegCommandPlan {
  assertMuxerMatchesExtension(
    options.preset.container,
    options.preset.fileExtension
  );
  const operation = options.operation ?? 'convert';
  const input = createPlannedInput(options.jobId, options.source);
  const output = createPlannedOutput(options.jobId, {
    baseName: options.source.fileName,
    operation,
    extension: options.preset.fileExtension,
    role: options.preset.kind === 'audio' ? 'audio' : 'media',
  });
  const args: string[] = ['-i', input.path];
  const paletteGif = options.preset.video?.codec === 'gif';
  if (options.streamSelection && !paletteGif) {
    const mapArgs = streamMapArguments(options.streamSelection);
    if (mapArgs.length === 0) {
      throw new RangeError('At least one output stream must be selected');
    }
    args.push(...mapArgs);
  }
  const videoFilters = [
    options.videoFiltergraph,
    options.preset.video?.width || options.preset.video?.height
      ? `scale=${options.preset.video.width ?? -2}:${options.preset.video.height ?? -2}`
      : undefined,
  ].filter((entry): entry is string => Boolean(entry));
  if (paletteGif) {
    const selection = options.streamSelection;
    if (
      (selection?.video?.length ?? 1) !== 1 ||
      (selection?.audio?.length ?? 0) > 0 ||
      (selection?.subtitles?.length ?? 0) > 0 ||
      (selection?.attachments?.length ?? 0) > 0 ||
      (selection?.data?.length ?? 0) > 0
    ) {
      throw new RangeError(
        'Animated GIF export requires exactly one selected video stream'
      );
    }
    const videoIndex = selection?.video?.[0] ?? 0;
    if (!Number.isSafeInteger(videoIndex) || videoIndex < 0) {
      throw new RangeError('Invalid GIF video stream index');
    }
    const prefix = videoFilters.length > 0 ? `${videoFilters.join(',')},` : '';
    args.push(
      '-filter_complex',
      `[0:${videoIndex}]${prefix}split[gif-base][gif-palette];[gif-palette]palettegen[gif-p];[gif-base][gif-p]paletteuse[gif-out]`,
      '-map',
      '[gif-out]'
    );
  } else if (videoFilters.length > 0) {
    args.push('-vf', videoFilters.join(','));
  }
  if (options.audioFiltergraph) {
    args.push('-af', options.audioFiltergraph);
  }
  args.push(...presetEncodingArguments(options.preset));
  const subtitleHandling = subtitleArguments(options);
  args.push(...subtitleHandling.args);
  if ((options.streamSelection?.attachments?.length ?? 0) > 0) {
    args.push('-c:t', 'copy');
  }
  if ((options.streamSelection?.data?.length ?? 0) > 0) {
    args.push('-c:d', 'copy');
  }
  args.push(...metadataPolicyArguments(options.preset));
  args.push('-progress', 'pipe:1', '-nostats', output.path);

  const requirements = isBuiltIn(options.preset)
    ? options.preset.requirements
    : requirementsForUserPreset(options.preset);

  return freezeCommandPlan({
    id: `${options.jobId}:${operation}`,
    operation,
    inputs: [input],
    temporaryFiles: [],
    args,
    outputs: [output],
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
    ...(options.timeoutMs !== undefined
      ? { timeoutMs: options.timeoutMs }
      : {}),
    requiredCapabilities: mergeRequirements(
      requirements,
      options.additionalRequirements ?? {},
      subtitleHandling.requirements,
      options.preset.video?.width || options.preset.video?.height
        ? { filters: ['scale'] }
        : {}
    ),
    diagnostics: [
      diagnostic(
        'conversion-reencodes',
        'info',
        'Selected audio or video streams are re-encoded according to the preset.'
      ),
      ...(paletteGif
        ? [
            diagnostic(
              'gif-palette',
              'info' as const,
              'GIF export uses one palette-generation and palette-application filter graph.'
            ),
          ]
        : []),
    ],
  });
}

export function requirementsForUserPreset(
  preset: UserExportPreset
): PresetRequirements {
  const encoders = [
    ...(preset.video ? [preset.video.codec] : []),
    ...(preset.audio ? [preset.audio.codec] : []),
  ];
  return {
    muxers: [preset.container],
    ...(encoders.length > 0 ? { encoders } : {}),
    ...(preset.subtitlePolicy === 'burn-in' ? { filters: ['subtitles'] } : {}),
  };
}

export function metadataPolicyArguments(
  preset: Pick<
    ExportPreset,
    'metadataPolicy' | 'chapterPolicy' | 'subtitlePolicy'
  >
): readonly string[] {
  const args: string[] = [];
  if (preset.metadataPolicy === 'remove') {
    args.push('-map_metadata', '-1');
  } else {
    args.push('-map_metadata', '0');
  }
  args.push('-map_chapters', preset.chapterPolicy === 'remove' ? '-1' : '0');
  if (preset.subtitlePolicy === 'none') {
    args.push('-sn');
  }
  return Object.freeze(args);
}

function isBuiltIn(preset: ExportPreset): preset is BuiltInExportPreset {
  return 'builtIn' in preset && preset.builtIn;
}

function subtitleArguments(options: ConvertOptions): {
  readonly args: readonly string[];
  readonly requirements: PresetRequirements;
} {
  const count = options.streamSelection?.subtitles?.length ?? 0;
  if (count === 0) {
    return { args: [], requirements: {} };
  }
  if (
    options.preset.subtitlePolicy === 'none' ||
    options.preset.subtitlePolicy === 'burn-in'
  ) {
    throw new RangeError(
      `Preset subtitle policy ${options.preset.subtitlePolicy} cannot emit selected soft-subtitle streams`
    );
  }
  return softSubtitlePolicyConfiguration(options.preset);
}

export function softSubtitlePolicyConfiguration(
  preset: Pick<ExportPreset, 'container' | 'subtitlePolicy'>
): {
  readonly args: readonly string[];
  readonly requirements: PresetRequirements;
} {
  if (preset.subtitlePolicy === 'none') {
    return { args: [], requirements: {} };
  }
  if (preset.subtitlePolicy === 'burn-in') {
    throw new TypeError(
      'Burn-in requires an explicit local subtitle filter input'
    );
  }
  if (preset.subtitlePolicy === 'copy-compatible') {
    return { args: ['-c:s', 'copy'], requirements: {} };
  }
  const encoder =
    preset.container === 'mp4' || preset.container === 'ipod'
      ? 'mov_text'
      : preset.container === 'webm'
        ? 'webvtt'
        : undefined;
  if (!encoder && preset.container === 'matroska') {
    return { args: ['-c:s', 'copy'], requirements: {} };
  }
  if (!encoder) {
    throw new TypeError(
      `No reviewed subtitle conversion policy exists for ${preset.container}`
    );
  }
  return {
    args: ['-c:s', encoder],
    requirements: { encoders: [encoder] },
  };
}
