import type { ExportPreset } from './convert';
import { buildConvertPlan } from './convert';
import {
  assertMuxerMatchesExtension,
  createPlannedInput,
  createPlannedOutput,
  diagnostic,
  escapeFilterValue,
  normalizeSeconds,
  type SourceReference,
} from './command-utils';
import {
  CommandPlanError,
  freezeCommandPlan,
  type CommandDiagnostic,
  type FFmpegCommandPlan,
} from './command-plan';
import { validateRemuxCompatibility, type RemuxStream } from './remux';

export type SubtitleFormat = 'srt' | 'vtt' | 'ass';
export type SubtitleClassification = 'text' | 'bitmap' | 'unknown';

export interface SubtitleStreamDescription {
  readonly index: number;
  readonly codec: string;
  readonly language?: string;
  readonly title?: string;
  readonly default?: boolean;
  readonly forced?: boolean;
}

const TEXT_CODECS = new Set([
  'subrip',
  'srt',
  'webvtt',
  'ass',
  'ssa',
  'mov_text',
  'text',
]);
const BITMAP_CODECS = new Set([
  'dvd_subtitle',
  'hdmv_pgs_subtitle',
  'dvb_subtitle',
  'xsub',
]);

export function classifySubtitleCodec(codec: string): SubtitleClassification {
  if (TEXT_CODECS.has(codec)) {
    return 'text';
  }
  if (BITMAP_CODECS.has(codec)) {
    return 'bitmap';
  }
  return 'unknown';
}

export function validateSoftSubtitleCompatibility(
  container: 'mp4' | 'webm' | 'matroska',
  codec: string
): readonly CommandDiagnostic[] {
  const compatible =
    container === 'mp4'
      ? codec === 'mov_text'
      : container === 'webm'
        ? codec === 'webvtt'
        : classifySubtitleCodec(codec) !== 'unknown';
  return compatible
    ? Object.freeze([])
    : Object.freeze([
        diagnostic(
          'subtitle-container-incompatible',
          'error',
          `${codec} subtitles are not soft-mux compatible with ${container}; choose an explicit text conversion or burn-in.`
        ),
      ]);
}

export interface SubtitleExtractionOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly stream: SubtitleStreamDescription;
  readonly outputFormat: SubtitleFormat;
}

export function buildSubtitleExtractionPlan(
  options: SubtitleExtractionOptions
): FFmpegCommandPlan {
  if (!Number.isSafeInteger(options.stream.index) || options.stream.index < 0) {
    throw new RangeError('Invalid subtitle stream index');
  }
  const classification = classifySubtitleCodec(options.stream.codec);
  if (classification !== 'text') {
    throw new CommandPlanError(
      classification === 'bitmap'
        ? 'Bitmap subtitles cannot be converted to text automatically'
        : 'Unknown subtitle codecs cannot be converted to text automatically',
      [
        diagnostic(
          classification === 'bitmap'
            ? 'bitmap-not-text'
            : 'unknown-subtitle-codec',
          'error',
          classification === 'bitmap'
            ? 'Extraction can preserve bitmap data in a compatible format, but it is not OCR or text conversion.'
            : 'Only a recognized text subtitle codec can be converted to SRT, WebVTT, or ASS.'
        ),
      ]
    );
  }
  const input = createPlannedInput(options.jobId, options.source);
  const output = createPlannedOutput(options.jobId, {
    baseName: options.source.fileName,
    operation: 'subtitle',
    suffix: options.stream.language ?? `stream-${options.stream.index}`,
    extension: options.outputFormat,
    role: 'subtitle',
  });
  const encoder = subtitleEncoder(options.outputFormat);
  return freezeCommandPlan({
    id: `${options.jobId}:subtitle-extract`,
    operation: 'subtitle-extract',
    inputs: [input],
    temporaryFiles: [],
    args: [
      '-i',
      input.path,
      '-map',
      `0:${options.stream.index}`,
      '-c:s',
      encoder,
      output.path,
    ],
    outputs: [output],
    requiredCapabilities: {
      encoders: [encoder],
      muxers: [subtitleMuxer(options.outputFormat)],
    },
    diagnostics: [
      diagnostic(
        'subtitle-extraction',
        'info',
        `Text subtitle stream ${options.stream.index} is converted to ${options.outputFormat.toUpperCase()}.`
      ),
    ],
  });
}

export interface SubtitleConversionOptions {
  readonly jobId: string;
  readonly subtitle: SourceReference;
  readonly outputFormat: SubtitleFormat;
  readonly offsetSeconds?: number;
}

export function buildSubtitleConversionPlan(
  options: SubtitleConversionOptions
): FFmpegCommandPlan {
  const input = createPlannedInput(options.jobId, options.subtitle, 'subtitle');
  const output = createPlannedOutput(options.jobId, {
    baseName: options.subtitle.fileName,
    operation: 'subtitle',
    extension: options.outputFormat,
    role: 'subtitle',
  });
  const encoder = subtitleEncoder(options.outputFormat);
  const args: string[] = [];
  if (options.offsetSeconds !== undefined) {
    if (
      !Number.isFinite(options.offsetSeconds) ||
      Math.abs(options.offsetSeconds) > 86_400
    ) {
      throw new RangeError('Subtitle offset must be within 24 hours');
    }
    args.push('-itsoffset', signedSeconds(options.offsetSeconds));
  }
  args.push('-i', input.path, '-map', '0:s:0', '-c:s', encoder, output.path);
  return freezeCommandPlan({
    id: `${options.jobId}:subtitle-convert`,
    operation: 'subtitle-convert',
    inputs: [input],
    temporaryFiles: [],
    args,
    outputs: [output],
    requiredCapabilities: {
      encoders: [encoder],
      muxers: [subtitleMuxer(options.outputFormat)],
    },
    diagnostics: [],
  });
}

export interface SubtitleMuxOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly subtitle: SourceReference;
  readonly subtitleCodec: string;
  readonly sourceStreams: readonly RemuxStream[];
  readonly targetContainer: 'mp4' | 'webm' | 'matroska';
  readonly targetExtension: string;
  readonly language?: string;
  readonly title?: string;
  readonly default?: boolean;
  readonly forced?: boolean;
  readonly expectedDurationSeconds?: number;
}

export function buildSubtitleMuxPlan(
  options: SubtitleMuxOptions
): FFmpegCommandPlan {
  assertMuxerMatchesExtension(options.targetContainer, options.targetExtension);
  const outputCodec =
    options.targetContainer === 'mp4'
      ? 'mov_text'
      : options.targetContainer === 'webm'
        ? 'webvtt'
        : options.subtitleCodec;
  const diagnostics = [
    ...validateSoftSubtitleCompatibility(options.targetContainer, outputCodec),
    ...validateRemuxCompatibility(
      { streams: options.sourceStreams },
      options.targetContainer
    ),
  ];
  validateSourceStreams(options.sourceStreams);
  if (options.sourceStreams.length === 0) {
    throw new CommandPlanError('No source media streams were declared', [
      diagnostic(
        'no-source-streams',
        'error',
        'Soft-subtitle muxing requires inspected source streams.'
      ),
    ]);
  }
  const existingSubtitleCount = options.sourceStreams.filter(
    (stream) => stream.kind === 'subtitle'
  ).length;
  if (diagnostics.some((entry) => entry.severity === 'error')) {
    throw new CommandPlanError(
      'Subtitle is incompatible with target container',
      diagnostics
    );
  }
  if (
    classifySubtitleCodec(options.subtitleCodec) === 'bitmap' &&
    outputCodec !== options.subtitleCodec
  ) {
    throw new CommandPlanError('Bitmap subtitle conversion is not supported', [
      diagnostic(
        'bitmap-not-text',
        'error',
        'Bitmap subtitles cannot be converted to a text subtitle codec automatically.'
      ),
    ]);
  }
  const mediaInput = createPlannedInput(options.jobId, options.source);
  const subtitleInput = createPlannedInput(
    options.jobId,
    options.subtitle,
    'subtitle'
  );
  const output = createPlannedOutput(options.jobId, {
    baseName: options.source.fileName,
    operation: 'subtitled',
    extension: options.targetExtension,
  });
  const requiresConversion = outputCodec !== options.subtitleCodec;
  if (
    requiresConversion &&
    classifySubtitleCodec(options.subtitleCodec) !== 'text'
  ) {
    throw new CommandPlanError(
      'Only text subtitles can be converted for this target container',
      [
        diagnostic(
          'subtitle-conversion-unsupported',
          'error',
          'Choose a recognized SRT, WebVTT, ASS/SSA, mov_text, or text subtitle input.'
        ),
      ]
    );
  }
  const args = [
    '-i',
    mediaInput.path,
    '-i',
    subtitleInput.path,
    ...[...options.sourceStreams]
      .sort((left, right) => left.index - right.index)
      .flatMap((stream) => ['-map', `0:${stream.index}`]),
    '-map',
    '1:s:0',
    '-c',
    'copy',
    `-c:s:${existingSubtitleCount}`,
    requiresConversion ? outputCodec : 'copy',
  ];
  if (options.language) {
    args.push(
      `-metadata:s:s:${existingSubtitleCount}`,
      `language=${safeSubtitleTag(options.language)}`
    );
  }
  if (options.title) {
    const title = safeSubtitleTag(options.title);
    args.push(`-metadata:s:s:${existingSubtitleCount}`, `title=${title}`);
    if (options.targetContainer === 'mp4') {
      args.push(
        `-metadata:s:s:${existingSubtitleCount}`,
        `handler_name=${title}`
      );
    }
  }
  const dispositions =
    [
      options.default ? 'default' : undefined,
      options.forced ? 'forced' : undefined,
    ]
      .filter((value): value is string => Boolean(value))
      .join('+') || '0';
  args.push(
    `-disposition:s:${existingSubtitleCount}`,
    dispositions,
    output.path
  );
  return freezeCommandPlan({
    id: `${options.jobId}:subtitle-mux`,
    operation: 'subtitle-mux',
    inputs: [mediaInput, subtitleInput],
    temporaryFiles: [],
    args,
    outputs: [output],
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
    requiredCapabilities: {
      muxers: [options.targetContainer],
      ...(requiresConversion ? { encoders: [outputCodec] } : {}),
    },
    diagnostics,
  });
}

export interface SubtitleBurnOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly subtitle: SourceReference;
  readonly preset: ExportPreset;
  readonly expectedDurationSeconds?: number;
}

export function buildSubtitleBurnPlan(
  options: SubtitleBurnOptions
): FFmpegCommandPlan {
  if (!options.preset.video) {
    throw new TypeError('Subtitle burn-in requires a video export preset');
  }
  const subtitleInput = createPlannedInput(
    options.jobId,
    options.subtitle,
    'subtitle'
  );
  const base = buildConvertPlan({
    jobId: options.jobId,
    source: options.source,
    preset: options.preset,
    operation: 'subtitle-burn',
    expectedDurationSeconds: options.expectedDurationSeconds,
    videoFiltergraph: `subtitles=filename='${escapeFilterValue(subtitleInput.path)}'`,
    additionalRequirements: { filters: ['subtitles'] },
  });
  return freezeCommandPlan({
    ...base,
    inputs: [...base.inputs, subtitleInput],
    diagnostics: [
      diagnostic(
        'subtitle-burn-in',
        'info',
        'Subtitle pixels are rendered locally into the video using the bundled subtitle filter.'
      ),
    ],
  });
}

function subtitleEncoder(format: SubtitleFormat): string {
  return format === 'srt' ? 'srt' : format === 'vtt' ? 'webvtt' : 'ass';
}

function subtitleMuxer(format: SubtitleFormat): string {
  return format === 'vtt' ? 'webvtt' : format;
}

function signedSeconds(value: number): string {
  return value < 0
    ? `-${normalizeSeconds(Math.abs(value))}`
    : normalizeSeconds(value);
}

function safeSubtitleTag(value: string): string {
  if (
    value.includes('\0') ||
    value.includes('\n') ||
    value.includes('\r') ||
    value.length > 128
  ) {
    throw new TypeError('Subtitle tag contains unsupported characters');
  }
  return value;
}

function validateSourceStreams(streams: readonly RemuxStream[]): void {
  const indexes = new Set<number>();
  for (const stream of streams) {
    if (
      !Number.isSafeInteger(stream.index) ||
      stream.index < 0 ||
      indexes.has(stream.index)
    ) {
      throw new RangeError(
        'Source stream indexes must be unique nonnegative integers'
      );
    }
    if (!stream.codec.trim()) {
      throw new TypeError(`Source stream ${stream.index} codec is required`);
    }
    indexes.add(stream.index);
  }
}
