import type { SourceReference } from './command-utils';
import {
  chapterSecondsToTicks,
  DEFAULT_CHAPTER_TIME_BASE,
  parseChapterTimeBase,
} from '../media/chapter-time-base';
import {
  assertMuxerMatchesExtension,
  createPlannedInput,
  createPlannedOutput,
  diagnostic,
  workVirtualPath,
} from './command-utils';
import {
  freezeCommandPlan,
  type CommandDiagnostic,
  type FFmpegCommandPlan,
} from './command-plan';
import { validateSplitMarkers } from './split';

export interface EditableChapter {
  readonly id: string;
  readonly title: string;
  readonly startSeconds: number;
  readonly endSeconds: number;
  readonly timeBase?: string;
}

export interface ChapterValidationOptions {
  readonly durationSeconds?: number;
  readonly allowOverlap?: boolean;
}

export interface ChapterDocument {
  readonly schemaVersion: 1;
  readonly chapters: readonly EditableChapter[];
}

export function createChaptersFromMarkers(
  markers: readonly number[],
  durationSeconds: number,
  titles: readonly string[] = []
): readonly EditableChapter[] {
  const sorted = validateSplitMarkers(markers, durationSeconds);
  const boundaries = [0, ...sorted, durationSeconds];
  return Object.freeze(
    boundaries.slice(0, -1).map((startSeconds, index) =>
      Object.freeze({
        id: `chapter-${String(index + 1).padStart(3, '0')}`,
        title: titles[index]?.trim() || `Chapter ${index + 1}`,
        startSeconds,
        endSeconds: boundaries[index + 1] as number,
        timeBase: '1/1000',
      })
    )
  );
}

export function createChaptersAtIntervals(
  durationSeconds: number,
  intervalSeconds: number
): readonly EditableChapter[] {
  if (
    !Number.isFinite(durationSeconds) ||
    durationSeconds <= 0 ||
    !Number.isFinite(intervalSeconds) ||
    intervalSeconds <= 0
  ) {
    throw new RangeError('Chapter duration and interval must be positive');
  }
  const markers: number[] = [];
  for (
    let marker = intervalSeconds;
    marker < durationSeconds;
    marker += intervalSeconds
  ) {
    markers.push(marker);
    if (markers.length > 10_000) {
      throw new RangeError('Fixed interval would create too many chapters');
    }
  }
  return createChaptersFromMarkers(markers, durationSeconds);
}

export function validateChapters(
  chapters: readonly EditableChapter[],
  options: ChapterValidationOptions = {}
): readonly CommandDiagnostic[] {
  const diagnostics: CommandDiagnostic[] = [];
  const ids = new Set<string>();
  const titles = new Set<string>();
  let previousEnd = 0;
  chapters.forEach((chapter, index) => {
    if (!chapter.id || ids.has(chapter.id)) {
      diagnostics.push(
        diagnostic(
          'chapter-id',
          'error',
          'Chapter IDs must be nonempty and unique.',
          `${index}.id`
        )
      );
    }
    ids.add(chapter.id);
    if (!chapter.title.trim()) {
      diagnostics.push(
        diagnostic(
          'chapter-title',
          'error',
          'Chapter title is required.',
          `${index}.title`
        )
      );
    } else if (titles.has(chapter.title.trim())) {
      diagnostics.push(
        diagnostic(
          'duplicate-chapter-title',
          'warning',
          `Duplicate chapter title: ${chapter.title.trim()}`,
          `${index}.title`
        )
      );
    }
    titles.add(chapter.title.trim());
    if (
      !Number.isFinite(chapter.startSeconds) ||
      !Number.isFinite(chapter.endSeconds) ||
      chapter.startSeconds < 0 ||
      chapter.endSeconds <= chapter.startSeconds
    ) {
      diagnostics.push(
        diagnostic(
          'chapter-range',
          'error',
          'Chapter start must be nonnegative and before its end.',
          `${index}.startSeconds`
        )
      );
    }
    const timeBase = parseChapterTimeBase(
      chapter.timeBase ?? DEFAULT_CHAPTER_TIME_BASE
    );
    if (timeBase === undefined) {
      diagnostics.push(
        diagnostic(
          'chapter-time-base',
          'error',
          'Chapter time base must be a positive integer fraction such as 1/1000.',
          `${index}.timeBase`
        )
      );
    } else {
      const startTick = chapterSecondsToTicks(chapter.startSeconds, timeBase);
      const endTick = chapterSecondsToTicks(chapter.endSeconds, timeBase);
      if (
        startTick === undefined ||
        endTick === undefined ||
        endTick <= startTick
      ) {
        diagnostics.push(
          diagnostic(
            'chapter-time-base-resolution',
            'error',
            'Chapter time base is too coarse or large for this chapter range.',
            `${index}.timeBase`
          )
        );
      }
    }
    if (
      options.durationSeconds !== undefined &&
      chapter.endSeconds > options.durationSeconds
    ) {
      diagnostics.push(
        diagnostic(
          'chapter-bounds',
          'error',
          'Chapter extends beyond output duration.',
          `${index}.endSeconds`
        )
      );
    }
    if (chapter.startSeconds < previousEnd) {
      diagnostics.push(
        diagnostic(
          options.allowOverlap
            ? 'chapter-overlap'
            : 'chapter-overlap-forbidden',
          options.allowOverlap ? 'warning' : 'error',
          'Chapter overlaps or is out of chronological order.',
          `${index}.startSeconds`
        )
      );
    }
    previousEnd = Math.max(previousEnd, chapter.endSeconds);
  });
  return Object.freeze(diagnostics);
}

export function escapeFFMetadata(value: string): string {
  if (value.includes('\0')) {
    throw new TypeError('FFMETADATA values cannot contain NUL');
  }
  return value
    .replace(/\\/gu, '\\\\')
    .replace(/\r\n?|\n/gu, '\\\n')
    .replace(/([=;#])/gu, '\\$1');
}

export function serializeFFMetadata(
  chapters: readonly EditableChapter[],
  options: ChapterValidationOptions = {}
): string {
  const diagnostics = validateChapters(chapters, options);
  const errors = diagnostics.filter((entry) => entry.severity === 'error');
  if (errors.length > 0) {
    throw new RangeError(errors.map((entry) => entry.message).join('; '));
  }
  const lines = [';FFMETADATA1'];
  for (const chapter of chapters) {
    const timeBase = parseChapterTimeBase(
      chapter.timeBase ?? DEFAULT_CHAPTER_TIME_BASE
    );
    if (timeBase === undefined) {
      throw new TypeError('Chapter time base failed validation');
    }
    const startTick = chapterSecondsToTicks(chapter.startSeconds, timeBase);
    const endTick = chapterSecondsToTicks(chapter.endSeconds, timeBase);
    if (startTick === undefined || endTick === undefined) {
      throw new RangeError('Chapter time cannot be represented safely');
    }
    lines.push(
      '[CHAPTER]',
      `TIMEBASE=${timeBase.normalized}`,
      `START=${startTick}`,
      `END=${endTick}`,
      `title=${escapeFFMetadata(chapter.title)}`
    );
  }
  return `${lines.join('\n')}\n`;
}

export function serializeChapterJson(
  chapters: readonly EditableChapter[]
): string {
  const document: ChapterDocument = { schemaVersion: 1, chapters };
  const errors = validateChapters(chapters).filter(
    (entry) => entry.severity === 'error'
  );
  if (errors.length > 0) {
    throw new TypeError(errors.map((entry) => entry.message).join('; '));
  }
  return `${JSON.stringify(document, null, 2)}\n`;
}

export function parseChapterJson(json: string): ChapterDocument {
  let value: unknown;
  try {
    value = JSON.parse(json) as unknown;
  } catch {
    throw new TypeError('Chapter JSON is malformed');
  }
  if (
    !isRecord(value) ||
    value.schemaVersion !== 1 ||
    !Array.isArray(value.chapters)
  ) {
    throw new TypeError('Chapter document must use schemaVersion 1');
  }
  const chapters = value.chapters.map((entry, index) =>
    parseChapter(entry, index)
  );
  const errors = validateChapters(chapters).filter(
    (entry) => entry.severity === 'error'
  );
  if (errors.length > 0) {
    throw new TypeError(errors.map((entry) => entry.message).join('; '));
  }
  return Object.freeze({ schemaVersion: 1, chapters: Object.freeze(chapters) });
}

export interface ChapterMuxOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly chapters: readonly EditableChapter[];
  readonly targetExtension: string;
  readonly targetMuxer: string;
  readonly durationSeconds?: number;
}

export function buildChapterMuxPlan(
  options: ChapterMuxOptions
): FFmpegCommandPlan {
  assertMuxerMatchesExtension(options.targetMuxer, options.targetExtension);
  const metadataContent = serializeFFMetadata(options.chapters, {
    durationSeconds: options.durationSeconds,
  });
  const input = createPlannedInput(options.jobId, options.source);
  const metadataPath = workVirtualPath(options.jobId, 'chapters.ffmeta');
  const output = createPlannedOutput(options.jobId, {
    baseName: options.source.fileName,
    operation: 'chapters',
    extension: options.targetExtension,
  });
  return freezeCommandPlan({
    id: `${options.jobId}:chapters`,
    operation: 'chapters',
    inputs: [input],
    temporaryFiles: [
      { path: metadataPath, content: metadataContent, purpose: 'metadata' },
    ],
    args: [
      '-i',
      input.path,
      '-i',
      metadataPath,
      '-map',
      '0',
      '-map_metadata',
      '0',
      '-map_chapters',
      '1',
      '-c',
      'copy',
      output.path,
    ],
    outputs: [output],
    ...(options.durationSeconds !== undefined
      ? { expectedDurationSeconds: options.durationSeconds }
      : {}),
    requiredCapabilities: { muxers: [options.targetMuxer] },
    diagnostics: [
      diagnostic(
        'chapter-round-trip',
        'info',
        'Re-probe the output to verify chapter count, times, and titles.'
      ),
    ],
  });
}

function parseChapter(value: unknown, index: number): EditableChapter {
  if (!isRecord(value)) {
    throw new TypeError(`chapters.${index} must be an object`);
  }
  if (
    typeof value.id !== 'string' ||
    typeof value.title !== 'string' ||
    typeof value.startSeconds !== 'number' ||
    typeof value.endSeconds !== 'number' ||
    (value.timeBase !== undefined && typeof value.timeBase !== 'string')
  ) {
    throw new TypeError(`chapters.${index} has invalid fields`);
  }
  return {
    id: value.id,
    title: value.title,
    startSeconds: value.startSeconds,
    endSeconds: value.endSeconds,
    ...(typeof value.timeBase === 'string' ? { timeBase: value.timeBase } : {}),
  };
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}
