// useSyncExternalStore over a low-frequency set of HTMLAudioElement events.
// Snapshot is cached; consumers only re-render when one of the tracked fields
// actually changes (Object.is on the snapshot). See ADR-001 / research 03 §2.

import { useSyncExternalStore } from 'react';
import type { PlayerErrorReason, PlayerState, PlayerStateKind } from '../types';

const TRACKED = [
  'play',
  'pause',
  'ended',
  'loadstart',
  'loadedmetadata',
  'durationchange',
  'emptied',
  'waiting',
  'canplay',
  'error',
] as const;

// A detached <audio> whose `src` 404s (or points at a dead host) often never
// fires an `error` event — it stalls in NETWORK_LOADING indefinitely. After
// this long without metadata/canplay/error we synthesize a network error so
// the UI can surface it instead of showing a stuck skeleton forever.
const LOAD_WATCHDOG_MS = 12_000;
const SYNTHETIC_NETWORK_ERROR = 2;

type Snapshot = {
  paused: boolean;
  ended: boolean;
  duration: number;
  ready: boolean;
  errorCode: number | null;
};

function readSnapshot(audio: HTMLAudioElement, stalled: boolean): Snapshot {
  const nativeCode = audio.error?.code ?? null;
  return {
    paused: audio.paused,
    ended: audio.ended,
    duration: Number.isFinite(audio.duration) ? audio.duration : 0,
    ready: audio.readyState >= 2,
    errorCode: nativeCode ?? (stalled ? SYNTHETIC_NETWORK_ERROR : null),
  };
}

function snapshotsEqual(a: Snapshot, b: Snapshot): boolean {
  return (
    a.paused === b.paused &&
    a.ended === b.ended &&
    a.duration === b.duration &&
    a.ready === b.ready &&
    a.errorCode === b.errorCode
  );
}

export function createAudioSnapshotSource(audio: HTMLAudioElement) {
  let stalled = false;
  let cached: Snapshot = readSnapshot(audio, stalled);
  return {
    subscribe(cb: () => void): () => void {
      let watchdog: ReturnType<typeof setTimeout> | null = null;

      const clearWatchdog = () => {
        if (watchdog !== null) {
          clearTimeout(watchdog);
          watchdog = null;
        }
      };
      const refresh = () => {
        const next = readSnapshot(audio, stalled);
        if (!snapshotsEqual(cached, next)) {
          cached = next;
          cb();
        }
      };
      const armWatchdog = () => {
        clearWatchdog();
        if (audio.readyState >= 2 || audio.error) return;
        watchdog = setTimeout(() => {
          watchdog = null;
          if (audio.readyState < 2 && !audio.error) {
            stalled = true;
            refresh();
          }
        }, LOAD_WATCHDOG_MS);
      };
      const onProgress = (e: Event) => {
        if (e.type === 'loadstart' || e.type === 'emptied') {
          // New load cycle — drop any prior stall and restart the watchdog.
          stalled = false;
          armWatchdog();
        } else if (audio.readyState >= 2 || audio.error) {
          // Real progress (or a native error) clears a pending stall timer.
          clearWatchdog();
          if (stalled) stalled = false;
        }
        refresh();
      };

      for (const e of TRACKED) audio.addEventListener(e, onProgress);
      // Re-sync immediately: events (error, loadedmetadata, …) may have fired
      // between source creation and this subscription. Without this, a stale
      // snapshot from construction time would never update — e.g. a `src` that
      // already errored would stay stuck on `loading`.
      armWatchdog();
      refresh();
      return () => {
        clearWatchdog();
        for (const e of TRACKED) audio.removeEventListener(e, onProgress);
      };
    },
    getSnapshot(): Snapshot {
      return cached;
    },
    getServerSnapshot(): Snapshot {
      return cached;
    },
  };
}

function errorCodeToReason(code: number | null): PlayerErrorReason {
  // MediaError codes: 1 ABORT, 2 NETWORK, 3 DECODE, 4 SRC_NOT_SUPPORTED.
  if (code === 2) return 'network';
  if (code === 3) return 'decode';
  if (code === 4) return 'unsupported';
  return 'unknown';
}

function snapshotToState(snap: Snapshot, hasSrc: boolean, peaks?: Float32Array): PlayerState {
  if (snap.errorCode !== null) {
    return { kind: 'error', reason: errorCodeToReason(snap.errorCode), duration: snap.duration };
  }
  if (!hasSrc) return { kind: 'idle' };
  if (!snap.ready) return { kind: 'loading' };
  const kind: PlayerStateKind = snap.ended
    ? 'ended'
    : snap.paused
      ? 'paused'
      : 'playing';
  return { kind, duration: snap.duration, peaks };
}

export function useAudioElementState(
  source: ReturnType<typeof createAudioSnapshotSource>,
  hasSrc: boolean,
  peaks?: Float32Array,
): PlayerState {
  const snap = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getServerSnapshot);
  return snapshotToState(snap, hasSrc, peaks);
}
