import { useEffect, useRef } from 'react';
import { usePlaybackStore } from '../store/usePlaybackStore';
import { useVibeStore } from '../store/useVibeStore';
import { trackService } from '../services/trackService';
import { advancePastUnplayableVibeTrack, reportVibeEvent } from '../services/vibeSession';
import type { Track } from '../types';

// Threshold (seconds) above which a store position change is treated as a user
// scrub and applied to the audio element. Keeps the timeupdate -> setPosition ->
// effect loop from fighting itself.
const SEEK_THRESHOLD = 1;

// If a track reaches this fraction of its duration, treat it as "effectively
// completed" even if the user clicks Next before the very end.
const COMPLETION_THRESHOLD = 0.95;

// Relative seek increment (seconds) for MediaSession seekforward/seekbackward.
const SEEK_INCREMENT = 10;

/** Build the artwork URLs for MediaSession metadata (OS media controls). */
function buildArtwork(track: Track): MediaImage[] {
  const sizes = [96, 128, 192, 256, 384, 512];
  const artwork = track.artwork_id;
  if (!artwork) return [];
  // artwork_id is either a full URL (external) or a relative path served by us.
  const url = artwork.startsWith('http')
    ? artwork
    : `${window.location.origin}${artwork}`;
  return sizes.map((s) => ({ src: url, sizes: `${s}x${s}`, type: 'image/jpeg' }));
}

// Headless audio engine: one shared <audio> element driven by the playback store.
// State -> DOM via store subscriptions; DOM -> state via media events.
export const AudioEngine = () => {
  const audioRef = useRef<HTMLAudioElement>(null);

  // Track which id is currently loaded into the element, and whether it ended
  // naturally (so we record COMPLETED, not skip, on the resulting track change).
  const loadedIdRef = useRef<string | null>(null);
  const endedNaturallyRef = useRef(false);
  // Track whether the current track has crossed the completion threshold.
  const crossedThresholdRef = useRef(false);
  const lastProgressSecondRef = useRef(-1);
  const streamErrorTrackIdRef = useRef<string | null>(null);

  // --- DOM -> store: media events -----------------------------------------
  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const store = usePlaybackStore.getState;

    const onTimeUpdate = () => {
      store().setPosition(audio.currentTime);
      // Mark as effectively completed if we cross the threshold.
      if (
        !crossedThresholdRef.current &&
        audio.duration &&
        audio.currentTime / audio.duration >= COMPLETION_THRESHOLD
      ) {
        crossedThresholdRef.current = true;
      }
      const vibe = useVibeStore.getState();
      const track = store().currentTrack;
      const elapsed = Math.floor(audio.currentTime);
      if (vibe.activeSessionId && store().queueOwner === 'vibe' && track && elapsed > 0 && elapsed % 30 === 0 && elapsed !== lastProgressSecondRef.current) {
        lastProgressSecondRef.current = elapsed;
        void reportVibeEvent('progress', track.id, Math.round(audio.currentTime * 1000), Math.round((audio.duration || 0) * 1000)).catch(() => undefined);
      }
    };
    const onLoadedMetadata = () => {
      if (Number.isFinite(audio.duration)) store().setDuration(audio.duration);
    };
    const onPlay = () => {
      if (!store().isPlaying) store().play();
    };
    const onPause = () => {
      // Ignore the pause that fires as part of ending a track.
      if (audio.ended) return;
      if (store().isPlaying) store().pause();
    };
    const onEnded = () => {
      // Just flag it — applyTrack (below) is the single place that sends
      // feedback, on the resulting track-change, so completion is recorded
      // exactly once per track.
      endedNaturallyRef.current = true;
      store().nextWithReason('completed');
    };
    const onError = () => {
      const playback = store();
      const track = playback.currentTrack;
      const vibe = useVibeStore.getState();
      // Metadata can be available while the stream itself is no longer
      // readable. Vibe must advance that exact durable cursor, not fall back
      // to ordinary queue navigation or feedback-driven replanning.
      if (!track || !vibe.activeSessionId || playback.queueOwner !== 'vibe' || streamErrorTrackIdRef.current === track.id) return;
      streamErrorTrackIdRef.current = track.id;
      void advancePastUnplayableVibeTrack(track.id)
        .catch(() => undefined)
        .finally(() => { streamErrorTrackIdRef.current = null; });
    };

    audio.addEventListener('timeupdate', onTimeUpdate);
    audio.addEventListener('loadedmetadata', onLoadedMetadata);
    audio.addEventListener('play', onPlay);
    audio.addEventListener('pause', onPause);
    audio.addEventListener('ended', onEnded);
    audio.addEventListener('error', onError);

    return () => {
      audio.removeEventListener('timeupdate', onTimeUpdate);
      audio.removeEventListener('loadedmetadata', onLoadedMetadata);
      audio.removeEventListener('play', onPlay);
      audio.removeEventListener('pause', onPause);
      audio.removeEventListener('ended', onEnded);
      audio.removeEventListener('error', onError);
    };
  }, []);

  // --- store -> DOM: react to currentTrack changes ------------------------
  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const applyTrack = (id: string | null) => {
      if (id === loadedIdRef.current) return;

      // The durable Vibe controller owns normal next/ended navigation. It
      // records the outcome, receives a new plan revision, then calls the raw
      // advance method. Do not emit a second event here after that transition.
      const prevId = loadedIdRef.current;
      const completed = endedNaturallyRef.current || crossedThresholdRef.current;
      const playback = usePlaybackStore.getState();
      const inVibePlayback = !!useVibeStore.getState().activeSessionId && playback.queueOwner === 'vibe';
      if (prevId && inVibePlayback && !playback.vibeAdvanceHandler) {
        void reportVibeEvent(completed ? 'completed' : 'skipped', prevId).catch(() => undefined);
      }
      endedNaturallyRef.current = false;
      crossedThresholdRef.current = false;
      lastProgressSecondRef.current = -1;
      loadedIdRef.current = id;

      if (!id) {
        audio.removeAttribute('src');
        audio.load();
        return;
      }

      audio.src = trackService.getStreamUrl(id);
      audio.load();
      if (inVibePlayback) void reportVibeEvent('playback_started', id).catch(() => undefined);
      if (usePlaybackStore.getState().isPlaying) {
        void audio.play().catch(() => {});
      }
    };

    // Apply the current value immediately, then subscribe to future changes.
    applyTrack(usePlaybackStore.getState().currentTrack?.id ?? null);
    const unsub = usePlaybackStore.subscribe((state) => {
      applyTrack(state.currentTrack?.id ?? null);
    });
    return unsub;
  }, []);

  // --- store -> DOM: isPlaying ------------------------------------------------
  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const apply = (isPlaying: boolean) => {
      if (isPlaying) {
        if (audio.paused) void audio.play().catch(() => {});
      } else {
        if (!audio.paused) audio.pause();
      }
    };

    apply(usePlaybackStore.getState().isPlaying);
    return usePlaybackStore.subscribe((state) => apply(state.isPlaying));
  }, []);

  // --- store -> DOM: volume --------------------------------------------------
  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const apply = (volume: number) => {
      audio.volume = Math.min(1, Math.max(0, volume));
    };

    apply(usePlaybackStore.getState().volume);
    return usePlaybackStore.subscribe((state) => apply(state.volume));
  }, []);

  // --- store -> DOM: external seeks (user scrubbing) -------------------------
  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const apply = (position: number) => {
      if (Math.abs(audio.currentTime - position) > SEEK_THRESHOLD) {
        audio.currentTime = position;
      }
    };

    return usePlaybackStore.subscribe((state) => apply(state.position));
  }, []);

  // --- MediaSession: hardware media keys + OS media controls ------------------
  //
  // Without this, the browser's default media-key handler toggles the <audio>
  // element directly, bypassing the store — causing the UI and audio to
  // desync. By registering action handlers we route all media-key input
  // through the store, so isPlaying stays consistent. We also publish track
  // metadata so the OS "now playing" widget shows title/artist/artwork.
  useEffect(() => {
    if (!('mediaSession' in navigator)) return;

    const store = usePlaybackStore.getState;

    const handlers: Partial<Record<MediaSessionAction, (details: MediaSessionActionDetails) => void>> = {
      play: () => store().play(),
      pause: () => store().pause(),
      previoustrack: () => store().prev(),
      nexttrack: () => store().next(),
      seekbackward: (details) => {
        const audio = audioRef.current;
        if (!audio) return;
        const delta = details.seekOffset ?? SEEK_INCREMENT;
        audio.currentTime = Math.max(0, audio.currentTime - delta);
      },
      seekforward: (details) => {
        const audio = audioRef.current;
        if (!audio) return;
        const delta = details.seekOffset ?? SEEK_INCREMENT;
        audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + delta);
      },
      seekto: (details) => {
        const audio = audioRef.current;
        if (!audio || details.seekTime == null) return;
        audio.currentTime = details.seekTime;
      },
      stop: () => {
        store().pause();
        store().setPosition(0);
      },
    };

    for (const [action, handler] of Object.entries(handlers)) {
      try {
        navigator.mediaSession.setActionHandler(
          action as MediaSessionAction,
          handler ?? null,
        );
      } catch {
        // Some actions aren't supported on every browser/OS — ignore.
      }
    }

    // Clean up handlers on unmount so they don't outlive the engine.
    return () => {
      for (const action of Object.keys(handlers)) {
        try {
          navigator.mediaSession.setActionHandler(
            action as MediaSessionAction,
            null,
          );
        } catch {
          /* ignore */
        }
      }
    };
  }, []);

  // --- MediaSession: publish metadata + playback state ------------------------
  useEffect(() => {
    if (!('mediaSession' in navigator)) return;

    const update = (track: Track | null, isPlaying: boolean) => {
      if (track) {
        navigator.mediaSession.metadata = new MediaMetadata({
          title: track.title || 'Unknown',
          artist: track.artist || 'Unknown',
          album: '',
          artwork: buildArtwork(track),
        });
      }
      navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
    };

    // Publish immediately for the current state.
    update(usePlaybackStore.getState().currentTrack, usePlaybackStore.getState().isPlaying);

    // Subscribe to future changes of currentTrack or isPlaying.
    return usePlaybackStore.subscribe((state) => {
      update(state.currentTrack, state.isPlaying);
    });
  }, []);

  return <audio ref={audioRef} hidden />;
};
