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

export type StreamKind = 'video' | 'audio' | 'subtitle' | 'attachment' | 'data';

export interface RemuxStream {
  readonly index: number;
  readonly kind: StreamKind;
  readonly codec: string;
}

export interface RemuxInputDescription {
  readonly streams: readonly RemuxStream[];
  readonly hasChapters?: boolean;
  readonly hasMetadata?: boolean;
}

export interface RemuxOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly targetContainer: 'mp4' | 'webm' | 'matroska' | 'ogg' | 'ipod';
  readonly fileExtension: string;
  readonly description: RemuxInputDescription;
  readonly streamSelection?: StreamSelection;
  readonly metadataPolicy?: 'copy' | 'remove';
  readonly chapterPolicy?: 'keep' | 'remove';
  readonly expectedDurationSeconds?: number;
}

const CONTAINER_CODECS: Readonly<
  Record<string, Readonly<Partial<Record<StreamKind, ReadonlySet<string>>>>>
> = {
  mp4: {
    video: new Set(['h264', 'hevc', 'av1', 'mpeg4', 'mjpeg']),
    audio: new Set(['aac', 'mp3', 'alac', 'ac3', 'eac3']),
    subtitle: new Set(['mov_text']),
    attachment: new Set(),
    data: new Set(['bin_data']),
  },
  webm: {
    video: new Set(['vp8', 'vp9', 'av1']),
    audio: new Set(['opus', 'vorbis']),
    subtitle: new Set(['webvtt']),
    attachment: new Set(),
    data: new Set(),
  },
  matroska: {},
  ogg: {
    video: new Set(['theora']),
    audio: new Set(['vorbis', 'opus', 'flac']),
    subtitle: new Set(),
    attachment: new Set(),
    data: new Set(),
  },
  ipod: {
    video: new Set(),
    audio: new Set(['aac', 'alac', 'mp3']),
    subtitle: new Set(),
    attachment: new Set(),
    data: new Set(),
  },
};

export function validateRemuxCompatibility(
  description: RemuxInputDescription,
  container: RemuxOptions['targetContainer'],
  selection?: StreamSelection
): readonly CommandDiagnostic[] {
  const diagnostics: CommandDiagnostic[] = [];
  const rules = CONTAINER_CODECS[container];
  const selectedStreams = description.streams.filter((stream) =>
    isSelected(stream, selection)
  );
  if (selectedStreams.length === 0) {
    diagnostics.push(
      diagnostic(
        'no-streams',
        'error',
        'At least one media stream must be selected.'
      )
    );
  }
  for (const stream of selectedStreams) {
    const allowed = rules?.[stream.kind];
    if (allowed && !allowed.has(stream.codec)) {
      diagnostics.push(
        diagnostic(
          'incompatible-codec',
          'error',
          `${stream.kind} codec ${stream.codec} is not stream-copy compatible with ${container}.`,
          `stream.${stream.index}`
        )
      );
    }
  }
  if (
    description.hasChapters &&
    (container === 'webm' || container === 'ogg')
  ) {
    diagnostics.push(
      diagnostic(
        'chapter-support',
        'warning',
        `Chapter preservation should be verified after remuxing to ${container}.`
      )
    );
  }
  return Object.freeze(diagnostics);
}

export function buildRemuxPlan(options: RemuxOptions): FFmpegCommandPlan {
  assertMuxerMatchesExtension(options.targetContainer, options.fileExtension);
  const diagnostics = validateRemuxCompatibility(
    options.description,
    options.targetContainer,
    options.streamSelection
  );
  if (diagnostics.some((entry) => entry.severity === 'error')) {
    throw new CommandPlanError(
      'The selected streams cannot be remuxed',
      diagnostics
    );
  }
  const input = createPlannedInput(options.jobId, options.source);
  const output = createPlannedOutput(options.jobId, {
    baseName: options.source.fileName,
    operation: 'remux',
    extension: options.fileExtension,
  });
  const args: string[] = ['-i', input.path];
  if (options.streamSelection) {
    args.push(...streamMapArguments(options.streamSelection));
  } else {
    args.push('-map', '0');
  }
  args.push('-c', 'copy');
  if (options.metadataPolicy === 'remove') {
    args.push('-map_metadata', '-1');
  } else {
    args.push('-map_metadata', '0');
  }
  args.push('-map_chapters', options.chapterPolicy === 'remove' ? '-1' : '0');
  if (options.targetContainer === 'mp4' || options.targetContainer === 'ipod') {
    args.push('-movflags', '+faststart');
  }
  args.push('-progress', 'pipe:1', '-nostats', output.path);

  return freezeCommandPlan({
    id: `${options.jobId}:remux`,
    operation: 'remux',
    inputs: [input],
    temporaryFiles: [],
    args,
    outputs: [output],
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
    requiredCapabilities: { muxers: [options.targetContainer] },
    diagnostics: [
      diagnostic(
        'stream-copy',
        'info',
        'Fast remux copies compatible streams without re-encoding; it does not change their quality.'
      ),
      ...diagnostics,
    ],
  });
}

function isSelected(stream: RemuxStream, selection?: StreamSelection): boolean {
  if (!selection) {
    return true;
  }
  const indexes =
    stream.kind === 'video'
      ? selection.video
      : stream.kind === 'audio'
        ? selection.audio
        : stream.kind === 'subtitle'
          ? selection.subtitles
          : stream.kind === 'attachment'
            ? selection.attachments
            : selection.data;
  return indexes?.includes(stream.index) ?? false;
}
