import type { MediaProbe, MediaStreamProbe } from './media.types';
import { mimeTypeFromFileName } from './mime-types';

export type BrowserPlaybackSupport =
  'probably' | 'maybe' | 'unsupported' | 'unknown';

export interface BrowserPlaybackAssessment {
  support: BrowserPlaybackSupport;
  candidates: string[];
  selectedMimeType?: string;
  reason: string;
  needsPreviewProxy: boolean;
}

export type CanPlayType = (mimeType: string) => CanPlayTypeResult;

export interface PlaybackCandidateOptions {
  fileName?: string;
  declaredMimeType?: string;
}

export type GeneratedPreviewKind = 'audio' | 'video' | 'image' | 'other';

export interface GeneratedPreviewInput {
  readonly fileName: string;
  readonly mimeType: string;
  readonly probe?: MediaProbe;
}

export interface GeneratedPreviewAssessment {
  readonly kind: GeneratedPreviewKind;
  readonly previewable: boolean;
  readonly support: BrowserPlaybackSupport;
  readonly candidates: readonly string[];
  readonly reason: string;
}

const BROWSER_IMAGE_PREVIEW_TYPES = new Set([
  'image/avif',
  'image/gif',
  'image/jpeg',
  'image/png',
  'image/svg+xml',
  'image/webp',
]);

export function buildPlaybackMimeCandidates(
  probe: MediaProbe,
  options: PlaybackCandidateOptions = {}
): string[] {
  const hasVideo = probe.streams.some(
    (stream) =>
      stream.type === 'video' && stream.disposition.attached_pic !== true
  );
  const baseMimeType =
    normalizeMimeType(options.declaredMimeType) ??
    (options.fileName ? mimeTypeFromFileName(options.fileName) : undefined) ??
    mimeTypeFromFormats(probe.formatNames, hasVideo);

  if (baseMimeType === undefined) {
    return [];
  }

  const codecNames = playbackCodecNames(probe.streams);
  const withCodecs =
    codecNames.length > 0
      ? `${baseMimeType}; codecs="${codecNames.join(', ')}"`
      : undefined;
  return [...new Set([withCodecs, baseMimeType].filter(isString))];
}

export function assessBrowserPlayback(
  probe: MediaProbe,
  canPlayType: CanPlayType,
  options: PlaybackCandidateOptions = {}
): BrowserPlaybackAssessment {
  const candidates = buildPlaybackMimeCandidates(probe, options);
  if (candidates.length === 0) {
    return {
      support: 'unknown',
      candidates,
      reason: 'The source container or MIME type could not be identified.',
      needsPreviewProxy: false,
    };
  }

  let maybeCandidate: string | undefined;
  for (const candidate of candidates) {
    const result = canPlayType(candidate);
    if (result === 'probably') {
      return {
        support: 'probably',
        candidates,
        selectedMimeType: candidate,
        reason:
          'The browser reports that it can probably decode this container and codec combination.',
        needsPreviewProxy: false,
      };
    }
    if (result === 'maybe' && maybeCandidate === undefined) {
      maybeCandidate = candidate;
    }
  }

  if (maybeCandidate !== undefined) {
    return {
      support: 'maybe',
      candidates,
      selectedMimeType: maybeCandidate,
      reason:
        'The browser may decode this source; actual playback can still fail.',
      needsPreviewProxy: false,
    };
  }

  return {
    support: 'unsupported',
    candidates,
    reason:
      'The browser reports no native support. FFmpeg may still be able to read the source.',
    needsPreviewProxy: true,
  };
}

/**
 * Checks a generated result against the browser before a Preview action is
 * offered. A broad audio/video MIME prefix is not enough: when a round-trip
 * probe is available, its container and codecs are included in the query.
 */
export function assessGeneratedResultPreview(
  input: GeneratedPreviewInput,
  canPlayType: CanPlayType
): GeneratedPreviewAssessment {
  const mimeType = input.mimeType.trim().toLowerCase().split(';')[0]?.trim();
  const kind = previewKind(mimeType);

  if (kind === 'image') {
    const previewable =
      mimeType !== undefined && BROWSER_IMAGE_PREVIEW_TYPES.has(mimeType);
    return {
      kind,
      previewable,
      support: previewable ? 'maybe' : 'unsupported',
      candidates: mimeType ? [mimeType] : [],
      reason: previewable
        ? 'The result uses a commonly supported browser image format; an actual decode error will still be reported by the preview.'
        : 'This generated image format is not in the browser-preview allowlist.',
    };
  }
  if (kind !== 'audio' && kind !== 'video') {
    return {
      kind,
      previewable: false,
      support: 'unknown',
      candidates: [],
      reason: 'This result type has no in-browser media preview.',
    };
  }

  const playback = input.probe
    ? assessBrowserPlayback(input.probe, canPlayType, {
        fileName: input.fileName,
        declaredMimeType: mimeType,
      })
    : assessDeclaredPlayback(mimeType, canPlayType);
  return {
    kind,
    previewable:
      playback.support === 'probably' || playback.support === 'maybe',
    support: playback.support,
    candidates: playback.candidates,
    reason: playback.reason,
  };
}

function assessDeclaredPlayback(
  mimeType: string | undefined,
  canPlayType: CanPlayType
): BrowserPlaybackAssessment {
  if (
    mimeType === undefined ||
    !/^(?:audio|video)\/[a-z0-9.+-]+$/.test(mimeType)
  ) {
    return {
      support: 'unknown',
      candidates: [],
      reason: 'The generated media type could not be identified.',
      needsPreviewProxy: false,
    };
  }
  const result = canPlayType(mimeType);
  if (result === 'probably' || result === 'maybe') {
    return {
      support: result,
      candidates: [mimeType],
      selectedMimeType: mimeType,
      reason:
        result === 'probably'
          ? 'The browser reports that it can probably decode this generated result.'
          : 'The browser may decode this generated result; actual playback can still fail.',
      needsPreviewProxy: false,
    };
  }
  return {
    support: 'unsupported',
    candidates: [mimeType],
    reason:
      'The browser reports no native playback support for this generated result.',
    needsPreviewProxy: false,
  };
}

function previewKind(mimeType: string | undefined): GeneratedPreviewKind {
  if (mimeType?.startsWith('audio/')) return 'audio';
  if (mimeType?.startsWith('video/')) return 'video';
  if (mimeType?.startsWith('image/')) return 'image';
  return 'other';
}

function mimeTypeFromFormats(
  formats: readonly string[],
  hasVideo: boolean
): string | undefined {
  const normalized = new Set(formats.map((format) => format.toLowerCase()));
  if (
    normalized.has('mp4') ||
    normalized.has('mov') ||
    normalized.has('m4a') ||
    normalized.has('3gp') ||
    normalized.has('3g2') ||
    normalized.has('mj2')
  ) {
    return hasVideo ? 'video/mp4' : 'audio/mp4';
  }
  if (normalized.has('webm')) {
    return hasVideo ? 'video/webm' : 'audio/webm';
  }
  if (normalized.has('ogg')) {
    return hasVideo ? 'video/ogg' : 'audio/ogg';
  }
  if (normalized.has('matroska')) {
    return hasVideo ? 'video/x-matroska' : 'audio/x-matroska';
  }
  if (normalized.has('mp3')) {
    return 'audio/mpeg';
  }
  if (normalized.has('wav')) {
    return 'audio/wav';
  }
  if (normalized.has('flac')) {
    return 'audio/flac';
  }
  return undefined;
}

function playbackCodecNames(streams: readonly MediaStreamProbe[]): string[] {
  const codecs: string[] = [];
  for (const stream of streams) {
    if (
      (stream.type !== 'video' && stream.type !== 'audio') ||
      stream.disposition.attached_pic === true
    ) {
      continue;
    }
    const codec = browserCodecName(stream.codecName);
    if (codec && !codecs.includes(codec)) {
      codecs.push(codec);
    }
  }
  return codecs;
}

function browserCodecName(codecName: string | undefined): string | undefined {
  switch (codecName?.toLowerCase()) {
    case 'h264':
      return 'avc1.42E01E';
    case 'hevc':
    case 'h265':
      return 'hvc1';
    case 'av1':
      return 'av01.0.05M.08';
    case 'vp8':
      return 'vp8';
    case 'vp9':
      return 'vp09.00.10.08';
    case 'aac':
      return 'mp4a.40.2';
    case 'mp3':
      return 'mp3';
    case 'opus':
      return 'opus';
    case 'vorbis':
      return 'vorbis';
    default:
      return undefined;
  }
}

function normalizeMimeType(value: string | undefined): string | undefined {
  const mimeType = value?.trim().toLowerCase().split(';')[0]?.trim();
  return mimeType && /^(?:audio|video)\/[a-z0-9.+-]+$/.test(mimeType)
    ? mimeType
    : undefined;
}

function isString(value: string | undefined): value is string {
  return value !== undefined;
}
