import {
  isAudioMimeType,
  isVideoMimeType,
  mimeTypeFromFileName,
} from './mime-types';

export type BrowserMediaKind = 'audio' | 'video' | 'unknown';

export type BrowserMetadataErrorCode = 'aborted' | 'load-error' | 'timeout';

export interface BrowserMediaFileInfo {
  readonly name: string;
  readonly sizeBytes: number;
  readonly declaredMimeType?: string;
  readonly inferredMimeType?: string;
  readonly lastModified: number;
}

/**
 * Lightweight metadata exposed by the browser's native media element.
 *
 * This intentionally does not pretend to replace ffprobe: codec names,
 * complete stream lists, chapters, tags and dispositions remain unknown.
 */
export interface BrowserMediaMetadata {
  readonly source: 'browser';
  readonly file: BrowserMediaFileInfo;
  readonly kind: BrowserMediaKind;
  readonly durationSeconds?: number;
  readonly width?: number;
  readonly height?: number;
  readonly hasAudio?: boolean;
  readonly hasVideo?: boolean;
}

export interface BrowserMetadataOptions {
  readonly signal?: AbortSignal;
  readonly timeoutMilliseconds?: number;
  /**
   * Test seam for the DOM-owned media element. Production callers normally
   * leave this unset.
   */
  readonly createMediaElement?: (
    kind: Exclude<BrowserMediaKind, 'unknown'>
  ) => HTMLMediaElement;
}

export class BrowserMetadataError extends Error {
  readonly code: BrowserMetadataErrorCode;

  constructor(code: BrowserMetadataErrorCode, message: string) {
    super(message);
    this.name = 'BrowserMetadataError';
    this.code = code;
  }
}

const DEFAULT_BROWSER_METADATA_TIMEOUT_MILLISECONDS = 15_000;

interface MediaElementWithAudioHints extends HTMLMediaElement {
  readonly audioTracks?: { readonly length: number };
  readonly mozHasAudio?: boolean;
}

export function browserMediaFileInfo(file: File): BrowserMediaFileInfo {
  return {
    name: file.name,
    sizeBytes: file.size,
    declaredMimeType: normalizeMimeType(file.type),
    inferredMimeType: mimeTypeFromFileName(file.name),
    lastModified: file.lastModified,
  };
}

export function inferBrowserMediaKind(file: File): BrowserMediaKind {
  const declaredMimeType = normalizeMimeType(file.type);
  const inferredMimeType = mimeTypeFromFileName(file.name);
  const mimeType =
    declaredMimeType !== undefined &&
    (isAudioMimeType(declaredMimeType) || isVideoMimeType(declaredMimeType))
      ? declaredMimeType
      : inferredMimeType;

  if (mimeType === undefined) return 'unknown';
  if (isVideoMimeType(mimeType)) return 'video';
  if (isAudioMimeType(mimeType)) return 'audio';
  return 'unknown';
}

/**
 * Reads only metadata the browser can expose without FFmpeg. The supplied
 * object URL remains owned by the caller and is never revoked here.
 */
export function inspectBrowserMediaMetadata(
  file: File,
  objectUrl: string,
  options: BrowserMetadataOptions = {}
): Promise<BrowserMediaMetadata> {
  const declaredKind = inferBrowserMediaKind(file);
  const elementKind = declaredKind === 'audio' ? 'audio' : 'video';
  const createMediaElement =
    options.createMediaElement ??
    ((kind: 'audio' | 'video') => document.createElement(kind));
  const element = createMediaElement(elementKind);
  const timeoutMilliseconds =
    options.timeoutMilliseconds ??
    DEFAULT_BROWSER_METADATA_TIMEOUT_MILLISECONDS;

  if (options.signal?.aborted === true) {
    return Promise.reject(abortedError());
  }

  return new Promise((resolve, reject) => {
    let settled = false;

    const cleanup = () => {
      clearTimeout(timeoutId);
      element.removeEventListener('loadedmetadata', handleLoadedMetadata);
      element.removeEventListener('error', handleError);
      element.removeEventListener('abort', handleElementAbort);
      options.signal?.removeEventListener('abort', handleSignalAbort);
      element.removeAttribute('src');
      try {
        element.load();
      } catch {
        // The caller owns the object URL; releasing this detached element is
        // best-effort and must not turn successful metadata into a failure.
      }
    };
    const finish = (
      outcome:
        | { readonly ok: true; readonly value: BrowserMediaMetadata }
        | { readonly ok: false; readonly error: BrowserMetadataError }
    ) => {
      if (settled) return;
      settled = true;
      cleanup();
      if (outcome.ok) resolve(outcome.value);
      else reject(outcome.error);
    };
    const handleLoadedMetadata = () => {
      finish({
        ok: true,
        value: metadataFromElement(file, declaredKind, element),
      });
    };
    const handleError = () => {
      finish({
        ok: false,
        error: new BrowserMetadataError(
          'load-error',
          `The browser could not read metadata from ${file.name}.`
        ),
      });
    };
    const handleElementAbort = () => {
      finish({
        ok: false,
        error: new BrowserMetadataError(
          'load-error',
          `The browser stopped reading metadata from ${file.name}.`
        ),
      });
    };
    const handleSignalAbort = () => {
      finish({ ok: false, error: abortedError() });
    };

    element.addEventListener('loadedmetadata', handleLoadedMetadata);
    element.addEventListener('error', handleError);
    element.addEventListener('abort', handleElementAbort);
    options.signal?.addEventListener('abort', handleSignalAbort, {
      once: true,
    });
    const timeoutId = setTimeout(
      () => {
        finish({
          ok: false,
          error: new BrowserMetadataError(
            'timeout',
            `Browser metadata loading exceeded ${String(
              Math.ceil(timeoutMilliseconds / 1_000)
            )} seconds.`
          ),
        });
      },
      Math.max(0, timeoutMilliseconds)
    );

    element.preload = 'metadata';
    element.src = objectUrl;
    try {
      element.load();
    } catch {
      finish({
        ok: false,
        error: new BrowserMetadataError(
          'load-error',
          `The browser could not start reading metadata from ${file.name}.`
        ),
      });
    }
  });
}

function metadataFromElement(
  file: File,
  declaredKind: BrowserMediaKind,
  element: HTMLMediaElement
): BrowserMediaMetadata {
  const isVideoElement = element instanceof HTMLVideoElement;
  const width = isVideoElement
    ? positiveFiniteNumber(element.videoWidth)
    : undefined;
  const height = isVideoElement
    ? positiveFiniteNumber(element.videoHeight)
    : undefined;
  // A zero-sized <video> can still be the correct, playable element for an
  // audio-only MP4 or another video container. Treat missing dimensions as
  // inconclusive instead of claiming that no video-style player is usable.
  const hasVideo = isVideoElement
    ? width !== undefined
      ? true
      : undefined
    : false;
  const hasAudio = audioPresence(element, isVideoElement);
  const kind =
    hasVideo === true
      ? 'video'
      : hasAudio === true || element instanceof HTMLAudioElement
        ? 'audio'
        : declaredKind;

  return {
    source: 'browser',
    file: browserMediaFileInfo(file),
    kind,
    durationSeconds: nonNegativeFiniteNumber(element.duration),
    width,
    height,
    hasAudio,
    hasVideo,
  };
}

function audioPresence(
  element: HTMLMediaElement,
  isVideoElement: boolean
): boolean | undefined {
  if (!isVideoElement) return true;

  const withAudioHints = element as MediaElementWithAudioHints;
  if (withAudioHints.audioTracks !== undefined) {
    return withAudioHints.audioTracks.length > 0;
  }
  if (typeof withAudioHints.mozHasAudio === 'boolean') {
    return withAudioHints.mozHasAudio;
  }
  return undefined;
}

function normalizeMimeType(mimeType: string): string | undefined {
  const normalized = mimeType.trim().toLowerCase().split(';')[0]?.trim();
  return normalized === undefined || normalized.length === 0
    ? undefined
    : normalized;
}

function positiveFiniteNumber(value: number): number | undefined {
  return Number.isFinite(value) && value > 0 ? value : undefined;
}

function nonNegativeFiniteNumber(value: number): number | undefined {
  return Number.isFinite(value) && value >= 0 ? value : undefined;
}

function abortedError(): BrowserMetadataError {
  return new BrowserMetadataError(
    'aborted',
    'Browser metadata loading was cancelled.'
  );
}
