'use client';

import { useEffect, useId, useMemo, useRef, useState } from 'react';
import type { ReactNode } from 'react';
import { unlockAudioContext } from '../audio/audioContext';
import { getPeaksFromCache, setPeaks } from '../audio/peaksCache';
import { registerPlayer, setActivePlayer } from '../store/activePlayerBus';
import { createLevelsStore } from '../store/createLevelsStore';
import type { LevelsStore } from '../store/createLevelsStore';
import {
  getPreferences,
  setStoredMuted,
  setStoredVolume,
  subscribePreferences,
} from '../store/preferencesStore';
import { clamp } from '../utils/clamp';
import type { PlayerControls, PlayerErrorReason } from '../types';
import { AudioRefCtx } from './AudioRefContext';
import { ControlsCtx } from './ControlsContext';
import { LevelsCtx } from './LevelsContext';
import { MetaCtx, type PlayerMeta } from './MetaContext';
import { StateCtx } from './StateContext';
import { createAudioSnapshotSource, useAudioElementState } from '../hooks/useAudioElementEvents';

export type PlayerProviderProps = {
  src: string;
  peaks?: Float32Array;
  title?: string;
  artist?: string;
  album?: string;
  cover?: string;
  autoplay?: boolean;
  loop?: boolean;
  initialVolume?: number;
  muted?: boolean;
  preload?: 'none' | 'metadata' | 'auto';
  exclusive?: boolean;
  onPrev?: () => void;
  onNext?: () => void;
  onPlay?: () => void;
  onPause?: () => void;
  onEnded?: () => void;
  onError?: (reason: PlayerErrorReason, e: unknown) => void;
  children: ReactNode;
};

function createAudioElement(): HTMLAudioElement {
  const a = new Audio();
  a.crossOrigin = 'anonymous';
  a.preload = 'metadata';
  return a;
}

export function PlayerProvider(props: PlayerProviderProps) {
  const {
    src,
    peaks: peaksProp,
    title,
    artist,
    album,
    cover,
    autoplay = false,
    loop = false,
    initialVolume,
    muted: mutedProp,
    preload = 'metadata',
    exclusive = true,
    onPrev,
    onNext,
    onPlay,
    onPause,
    onEnded,
    onError,
    children,
  } = props;

  // Explicit props win and opt the player out of the persisted preference sync.
  // Otherwise we read defaults from the shared store and stay subscribed so a
  // change in one player is reflected in every other player on the page.
  const volumeIsControlled = initialVolume !== undefined;
  const mutedIsControlled = mutedProp !== undefined;

  const playerId = useId();

  // <audio>, snapshot source, levels store — created exactly once per
  // component instance via useState lazy initializer (StrictMode-safe: the
  // initializer runs once even though render runs twice).
  const [audio] = useState<HTMLAudioElement>(createAudioElement);
  const [source] = useState(() => createAudioSnapshotSource(audio));
  const [levelsStore] = useState<LevelsStore>(() => createLevelsStore());

  // Seed peaks-from-prop into the module cache so other consumers benefit too.
  if (peaksProp && getPeaksFromCache(src) === undefined) {
    setPeaks(src, peaksProp);
  }
  const initialPeaks = peaksProp ?? getPeaksFromCache(src);
  const [activePeaks, setActivePeaks] = useState<Float32Array | undefined>(initialPeaks);

  // Apply src / preload / loop / volume / muted imperatively.
  useEffect(() => {
    if (audio.src !== src) {
      audio.src = src;
      audio.load();
    }
  }, [audio, src]);

  useEffect(() => {
    audio.preload = preload;
  }, [audio, preload]);

  useEffect(() => {
    audio.loop = loop;
  }, [audio, loop]);

  // Initial volume / muted: prop > stored > defaults.
  useEffect(() => {
    const stored = getPreferences();
    audio.volume = clamp(volumeIsControlled ? (initialVolume as number) : stored.volume, 0, 1);
    audio.muted = mutedIsControlled ? Boolean(mutedProp) : stored.muted;
    // Run once at mount; later updates flow through the subscription below
    // (or, for controlled players, through the prop effects).
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [audio]);

  // Re-apply when controlled props change explicitly.
  useEffect(() => {
    if (!volumeIsControlled || initialVolume === undefined) return;
    audio.volume = clamp(initialVolume, 0, 1);
  }, [audio, volumeIsControlled, initialVolume]);

  useEffect(() => {
    if (!mutedIsControlled || mutedProp === undefined) return;
    audio.muted = mutedProp;
  }, [audio, mutedIsControlled, mutedProp]);

  // Subscribe uncontrolled players to the shared preferences store so they
  // stay in sync with each other and across tabs.
  useEffect(() => {
    if (volumeIsControlled && mutedIsControlled) return;
    return subscribePreferences((prefs) => {
      if (!volumeIsControlled && audio.volume !== prefs.volume) {
        audio.volume = prefs.volume;
      }
      if (!mutedIsControlled && audio.muted !== prefs.muted) {
        audio.muted = prefs.muted;
      }
    });
  }, [audio, volumeIsControlled, mutedIsControlled]);

  // When src changes, swap in cached peaks if available; clear otherwise.
  useEffect(() => {
    setActivePeaks(getPeaksFromCache(src));
  }, [src]);

  // Lifecycle event hooks.
  useEffect(() => {
    const handlePlay = () => onPlay?.();
    const handlePause = () => onPause?.();
    const handleEnded = () => onEnded?.();
    const handleError = (e: Event) => {
      const code = audio.error?.code ?? null;
      const reason: PlayerErrorReason =
        code === 2 ? 'network' : code === 3 ? 'decode' : code === 4 ? 'unsupported' : 'unknown';
      onError?.(reason, e);
    };
    audio.addEventListener('play', handlePlay);
    audio.addEventListener('pause', handlePause);
    audio.addEventListener('ended', handleEnded);
    audio.addEventListener('error', handleError);
    return () => {
      audio.removeEventListener('play', handlePlay);
      audio.removeEventListener('pause', handlePause);
      audio.removeEventListener('ended', handleEnded);
      audio.removeEventListener('error', handleError);
    };
  }, [audio, onEnded, onError, onPause, onPlay]);

  // Refs let controls.play() claim the active slot immediately, without
  // re-memoizing on every prop change.
  const exclusiveRef = useRef(exclusive);
  exclusiveRef.current = exclusive;
  const playerIdRef = useRef(playerId);
  playerIdRef.current = playerId;

  // Single-active-player coordination.
  useEffect(() => {
    if (!exclusive) return;
    const unregister = registerPlayer(playerId, () => {
      audio.pause();
    });
    // Backstop: claim the slot whenever the element starts playing, even if
    // playback was triggered outside our controls (MediaSession, native UI).
    const onPlayHandler = () => setActivePlayer(playerId);
    audio.addEventListener('play', onPlayHandler);
    return () => {
      audio.removeEventListener('play', onPlayHandler);
      unregister();
    };
  }, [audio, exclusive, playerId]);

  // Hard cleanup on unmount: stop playback, release the network buffer.
  // The element + cached MediaElementSourceNode are GC'd together via the
  // WeakMap. In StrictMode (dev) the first mount-then-unmount pair fires this
  // immediately, so the discarded audio never keeps playing in the background.
  useEffect(() => {
    return () => {
      try {
        audio.pause();
        audio.removeAttribute('src');
        audio.load();
      } catch {
        // ignore
      }
    };
  }, [audio]);

  // Controls — memoized once. Functions close over `audio` (stable ref).
  const controls = useMemo<PlayerControls>(() => {
    const playFn = async () => {
      // Claim the active slot synchronously so siblings get paused before our
      // audio.play() resolves. Backstop listener on `play` event covers
      // playback started outside this function.
      if (exclusiveRef.current) setActivePlayer(playerIdRef.current);
      try {
        await unlockAudioContext();
      } catch {
        // ignored — playback may still work without AudioContext.
      }
      try {
        await audio.play();
      } catch (e) {
        // Caller decides; surface via onError when it's a real failure.
        const isAbort = e instanceof DOMException && e.name === 'AbortError';
        if (!isAbort) onError?.('unknown', e);
      }
    };
    const pauseFn = () => {
      audio.pause();
    };
    return {
      play: playFn,
      pause: pauseFn,
      toggle: async () => {
        if (audio.paused || audio.ended) await playFn();
        else pauseFn();
      },
      seek: (seconds: number) => {
        if (!Number.isFinite(seconds)) return;
        const dur = Number.isFinite(audio.duration) ? audio.duration : 0;
        audio.currentTime = clamp(seconds, 0, dur || seconds);
      },
      seekTo: (ratio: number) => {
        const r = clamp(ratio, 0, 1);
        const dur = Number.isFinite(audio.duration) ? audio.duration : 0;
        if (dur > 0) audio.currentTime = r * dur;
      },
      setVolume: (v: number) => {
        const next = clamp(v, 0, 1);
        audio.volume = next;
        if (next > 0 && audio.muted) audio.muted = false;
        if (!volumeIsControlled) setStoredVolume(next);
        if (!mutedIsControlled && next > 0) setStoredMuted(false);
      },
      toggleMute: () => {
        const next = !audio.muted;
        audio.muted = next;
        if (!mutedIsControlled) setStoredMuted(next);
      },
      toggleLoop: () => {
        audio.loop = !audio.loop;
      },
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [audio, volumeIsControlled, mutedIsControlled]);

  // Autoplay attempt (best-effort; suspended context will reject silently).
  useEffect(() => {
    if (!autoplay) return;
    void controls.play();
  }, [autoplay, controls]);

  const state = useAudioElementState(source, Boolean(audio.src), activePeaks);

  const meta = useMemo<PlayerMeta>(
    () => ({
      src,
      title,
      artist,
      album,
      cover,
      hasPrev: Boolean(onPrev),
      hasNext: Boolean(onNext),
    }),
    [src, title, artist, album, cover, onPrev, onNext],
  );

  return (
    <AudioRefCtx.Provider value={audio}>
      <MetaCtx.Provider value={meta}>
        <StateCtx.Provider value={state}>
          <ControlsCtx.Provider value={controls}>
            <LevelsCtx.Provider value={levelsStore}>{children}</LevelsCtx.Provider>
          </ControlsCtx.Provider>
        </StateCtx.Provider>
      </MetaCtx.Provider>
    </AudioRefCtx.Provider>
  );
}

// Internal helper for parts that want to expose peaks-update from the lazy hook.
export function usePeaksSetter() {
  // The provider seeds peaks via setPeaks() in the module cache; consumers
  // re-read on src change. usePeaks hook re-uses the cache directly.
}
