import type { ExportPreset } from './convert';
import { presetEncodingArguments } from '../presets/preset-registry';
import {
  metadataPolicyArguments,
  softSubtitlePolicyConfiguration,
} from './convert';
import {
  createPlannedInput,
  createPlannedOutput,
  diagnostic,
  mergeRequirements,
  muxerForExtension,
  normalizeSeconds,
  validateRange,
  type SourceReference,
} from './command-utils';
import { freezeCommandPlan, type FFmpegCommandPlan } from './command-plan';
import {
  requirementsForUserPreset,
  type ExportPreset as Preset,
} from './convert';
import {
  GENERATED_OUTPUT_LIMIT_EXPLANATION,
  MAX_GENERATED_OUTPUT_FILES,
  MAX_SPLIT_MARKERS,
} from '../limits';

export interface TimeRange {
  readonly startSeconds: number;
  readonly endSeconds: number;
  readonly label?: string;
}

export type SplitDefinition =
  | { readonly type: 'markers'; readonly markers: readonly number[] }
  | { readonly type: 'equal-duration'; readonly segmentDurationSeconds: number }
  | { readonly type: 'part-count'; readonly parts: number }
  | { readonly type: 'chapters'; readonly chapters: readonly TimeRange[] }
  | { readonly type: 'clips'; readonly clips: readonly TimeRange[] };

export interface SplitOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly durationSeconds: number;
  readonly definition: SplitDefinition;
  readonly mode: 'fast' | 'accurate';
  readonly targetExtension: string;
  readonly preset?: ExportPreset;
}

export function rangesForSplit(
  definition: SplitDefinition,
  durationSeconds: number
): readonly TimeRange[] {
  if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
    throw new RangeError('durationSeconds must be positive');
  }
  let ranges: TimeRange[];
  if (definition.type === 'markers') {
    const markers = validateSplitMarkers(definition.markers, durationSeconds);
    const boundaries = [0, ...markers, durationSeconds];
    ranges = boundaries.slice(0, -1).map((start, index) => ({
      startSeconds: start,
      endSeconds: boundaries[index + 1] as number,
    }));
  } else if (definition.type === 'equal-duration') {
    if (
      !Number.isFinite(definition.segmentDurationSeconds) ||
      definition.segmentDurationSeconds <= 0
    ) {
      throw new RangeError('segmentDurationSeconds must be positive');
    }
    if (
      Math.ceil(durationSeconds / definition.segmentDurationSeconds) >
      MAX_GENERATED_OUTPUT_FILES
    ) {
      throw new RangeError(
        `A split operation can create at most ${MAX_GENERATED_OUTPUT_FILES} outputs. ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`
      );
    }
    ranges = [];
    for (
      let start = 0;
      start < durationSeconds;
      start += definition.segmentDurationSeconds
    ) {
      ranges.push({
        startSeconds: start,
        endSeconds: Math.min(
          durationSeconds,
          start + definition.segmentDurationSeconds
        ),
      });
    }
  } else if (definition.type === 'part-count') {
    if (
      !Number.isSafeInteger(definition.parts) ||
      definition.parts < 1 ||
      definition.parts > MAX_GENERATED_OUTPUT_FILES
    ) {
      throw new RangeError(
        `parts must be an integer between 1 and ${MAX_GENERATED_OUTPUT_FILES}. ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`
      );
    }
    const segmentDuration = durationSeconds / definition.parts;
    ranges = Array.from({ length: definition.parts }, (_, index) => ({
      startSeconds: segmentDuration * index,
      endSeconds:
        index === definition.parts - 1
          ? durationSeconds
          : segmentDuration * (index + 1),
    }));
  } else {
    ranges = [
      ...(definition.type === 'chapters'
        ? definition.chapters
        : definition.clips),
    ];
  }
  if (ranges.length === 0 || ranges.length > MAX_GENERATED_OUTPUT_FILES) {
    throw new RangeError(
      `A split operation must create 1–${MAX_GENERATED_OUTPUT_FILES} outputs. ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`
    );
  }
  ranges.forEach((range) =>
    validateRange(range.startSeconds, range.endSeconds, durationSeconds)
  );
  return Object.freeze(ranges.map((range) => Object.freeze({ ...range })));
}

export function validateSplitMarkers(
  markers: readonly number[],
  durationSeconds: number
): readonly number[] {
  if (markers.length > MAX_SPLIT_MARKERS) {
    throw new RangeError(
      `A split operation accepts at most ${MAX_SPLIT_MARKERS} markers (${MAX_GENERATED_OUTPUT_FILES} outputs). ${GENERATED_OUTPUT_LIMIT_EXPLANATION}`
    );
  }
  const sorted = [...markers].sort((left, right) => left - right);
  for (let index = 0; index < sorted.length; index += 1) {
    const marker = sorted[index] as number;
    if (!Number.isFinite(marker) || marker <= 0 || marker >= durationSeconds) {
      throw new RangeError('Split markers must be inside the source duration');
    }
    if (index > 0 && Math.abs(marker - (sorted[index - 1] as number)) < 0.04) {
      throw new RangeError(
        'Split markers must be unique and at least 0.04 seconds apart'
      );
    }
  }
  return Object.freeze(sorted);
}

export function buildSplitPlan(options: SplitOptions): FFmpegCommandPlan {
  const ranges = rangesForSplit(options.definition, options.durationSeconds);
  if (options.mode === 'accurate' && !options.preset) {
    throw new TypeError('Accurate split requires an export preset');
  }
  if (
    options.mode === 'accurate' &&
    options.preset &&
    options.targetExtension.replace(/^\.+/u, '').toLowerCase() !==
      options.preset.fileExtension
  ) {
    throw new TypeError(
      `Accurate split extension must match the preset (.${options.preset.fileExtension})`
    );
  }
  const input = createPlannedInput(options.jobId, options.source);
  const outputs = ranges.map((range, index) =>
    createPlannedOutput(options.jobId, {
      id: `segment-${index + 1}`,
      baseName: options.source.fileName,
      operation: 'split',
      suffix: range.label
        ? `${String(index + 1).padStart(3, '0')}-${range.label}`
        : String(index + 1).padStart(3, '0'),
      extension: options.targetExtension,
      timeRange: {
        startSeconds: range.startSeconds,
        endSeconds: range.endSeconds,
      },
    })
  );
  const args: string[] = [];
  if (options.mode === 'fast') {
    // Each output needs its own demuxer cursor. Reusing one input while
    // stream-copying several timestamp windows can leave later outputs without
    // video when keyframes are sparse.
    ranges.forEach((range) => {
      args.push('-ss', normalizeSeconds(range.startSeconds), '-i', input.path);
    });
  } else {
    args.push('-i', input.path);
  }
  args.push('-progress', 'pipe:1', '-nostats');
  ranges.forEach((range, index) => {
    if (options.mode === 'accurate') {
      args.push('-ss', normalizeSeconds(range.startSeconds));
    }
    args.push(
      '-t',
      normalizeSeconds(range.endSeconds - range.startSeconds),
      '-map',
      options.mode === 'fast' ? String(index) : '0'
    );
    if (options.mode === 'fast') {
      args.push(
        '-c',
        'copy',
        '-map_metadata',
        String(index),
        '-map_chapters',
        '-1',
        '-avoid_negative_ts',
        'make_zero'
      );
    } else {
      args.push(...presetEncodingArguments(options.preset as Preset));
      args.push(
        ...softSubtitlePolicyConfiguration(options.preset as Preset).args
      );
      args.push(
        ...metadataPolicyArguments(options.preset as Preset),
        '-map_chapters',
        '-1'
      );
    }
    const output = outputs[index];
    if (!output) {
      throw new TypeError(`Missing planned split output ${index + 1}`);
    }
    args.push(output.path);
  });
  const requirements =
    options.mode === 'accurate' && options.preset
      ? mergeRequirements(
          'builtIn' in options.preset
            ? options.preset.requirements
            : requirementsForUserPreset(options.preset),
          softSubtitlePolicyConfiguration(options.preset).requirements
        )
      : {
          ...(muxerForExtension(options.targetExtension)
            ? { muxers: [muxerForExtension(options.targetExtension)!] }
            : {}),
        };
  return freezeCommandPlan({
    id: `${options.jobId}:split`,
    operation: options.mode === 'fast' ? 'split-fast' : 'split-accurate',
    inputs: [input],
    temporaryFiles: [],
    args,
    outputs,
    expectedDurationSeconds: options.durationSeconds,
    requiredCapabilities: requirements,
    diagnostics: [
      diagnostic(
        options.mode === 'fast' ? 'split-keyframes' : 'split-reencode',
        options.mode === 'fast' ? 'warning' : 'info',
        options.mode === 'fast'
          ? 'Fast split boundaries can move to nearby keyframes.'
          : 'Accurate split re-encodes each segment at the requested boundaries.'
      ),
      diagnostic(
        'split-chapters-omitted',
        'info',
        'Source chapters are omitted because their timestamps do not automatically match individual split outputs.'
      ),
    ],
  });
}
