'use client';

import { useQuery, useQueryClient } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { createContext, useCallback, useEffect, useRef, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';

import { useStores } from '@/app/(root)/AppProviders';
import { useApiClient } from '@/lib/apiClient';
import { ActionName, EventNames } from '@/utils/event-names';

import { STATION_ID } from './constants';
import { SongData } from './interfaces';
import { getCurrentSong, getCurrentSongPosition } from './util';

// import { MOCK_SONGS } from './fixtures';

export const LiveRadioContext = createContext<{
  isLiveRadio: boolean;
  setIsLiveRadio: (isLiveRadio: boolean) => void;
  isPlaying: boolean;
  setIsPlaying: (isPlaying: boolean) => void;
  togglePlay: () => void;
  isLoading: boolean;
  sourceNode: React.RefObject<MediaElementAudioSourceNode | null>;
  currentSong: SongData | null;
  volume: number;
  setVolume: (volume: number) => void;
  isAudioContextPostUserInteraction: boolean;
  setAudioElement?: (element: HTMLAudioElement) => void;
  setSourceNode?: (node: MediaElementAudioSourceNode) => void;
}>({
  isLiveRadio: false,
  setIsLiveRadio: () => {},
  isPlaying: false,
  setIsPlaying: () => {},
  togglePlay: () => {},
  isLoading: false,
  sourceNode: { current: null },
  currentSong: null,
  volume: 100,
  setVolume: () => {},
  isAudioContextPostUserInteraction: false,
  setAudioElement: undefined,
  setSourceNode: undefined,
});

const DESYNC_ALLOWANCE = 2000; // ms

const SWAP_DESYNC_ALLOWANCE = 5000; // ms

export const LIVING_RADIO_QUERY_KEY = [
  'livingRadio',
  'songList',
  STATION_ID,
] as const;

export const LiveRadioProvider = observer(
  ({ children }: { children: React.ReactNode }) => {
    const [isPlaying, setIsPlaying] = useState(false);
    const [isLoading, setIsLoading] = useState(false);
    const [
      isAudioContextPostUserInteraction,
      setIsAudioContextPostUserInteraction,
    ] = useState(false);
    const [volume, setVolume] = useState(100);
    const audioTrack = useRef<HTMLAudioElement>(null);
    const sourceNode = useRef<MediaElementAudioSourceNode>(null);

    const actionIndex = useRef(-1);
    const songSessionId = useRef('');
    const previousSongSessionId = useRef('');
    const lastDetectedSongIdRef = useRef<string | null>(null);
    const startTime = useRef(0);
    const endTime = useRef(0);
    const firstPlayOccurred = useRef(false);
    const apiClient = useApiClient();
    const { session, logger, playbar } = useStores();

    const queryClient = useQueryClient();

    // Setter functions for audio elements
    const setAudioElement = useCallback((element: HTMLAudioElement) => {
      audioTrack.current = element;
    }, []);

    const setSourceNode = useCallback((node: MediaElementAudioSourceNode) => {
      sourceNode.current = node;
    }, []);
    const getAudioPlayerEvent = (actionName: string): Record<string, any> => {
      actionIndex.current = actionIndex.current + 1;
      const cachedSongs =
        queryClient.getQueryData<SongData[]>(LIVING_RADIO_QUERY_KEY) ?? [];
      return {
        songSessionId: songSessionId.current,
        hasClip: cachedSongs.length > 0,
        songId: cachedSongs.length > 0 ? cachedSongs[0].song_id : undefined,
        contextId: STATION_ID,
        contextType: 'living_radio',
        isSongAutoplayQueue: false,
        startTime: startTime.current,
        endTime: endTime.current,
        isPlaying: isPlaying,
        playDuration: endTime.current - startTime.current,
        isAudioElementNull: !audioTrack.current,
        audioElementCurrentTime: audioTrack.current?.currentTime,
        actionName: actionName,
        isUserSongOwner: false, // Radio songs are not owned by the user
        volume: (audioTrack.current?.volume || 1) * 100,
        clickSourceUrl:
          typeof location !== 'undefined' ? location.pathname : '',
        isShuffleOn: false,
        isAutoplayOn: false,
        isRepeatOn: false,
        userId: session?.userId,
        previousSongSessionId: previousSongSessionId.current,
        actionIndex: actionIndex.current,
        songLength: cachedSongs.length > 0 ? cachedSongs[0].duration : 0,
        metadata: {
          surfaceType: 'living_radio',
          surfaceId: STATION_ID,
          browserHistory: [],
        },
      };
    };

    // Ensure the first element always represents the "current" song according to schedule.
    // We prune any songs that have already completed (or where the next song's start_time
    // is already in the past) so consumers can treat index 0 as the canonical now-playing.
    const dequeueSongs = useCallback(() => {
      queryClient.setQueryData<SongData[]>(
        LIVING_RADIO_QUERY_KEY,
        (previous) => {
          if (!previous || previous.length === 0) return previous;
          const now = Date.now();
          const pruned = [...previous];
          let startIndex = 0;
          // We first make the first song in the list the current song
          while (
            pruned.length - startIndex > 1 &&
            Date.parse(pruned[startIndex + 1].start_time) <
              now + DESYNC_ALLOWANCE
          ) {
            startIndex++; // if the next song is already playing, shift current song to the next one
          }
          // Check if current song is over (skip this check for streaming songs with duration = 0)
          if (
            pruned.length > 0 &&
            pruned[startIndex].duration > 0 && // Only check if song is over for non-streaming songs
            Date.parse(pruned[startIndex].start_time) +
              pruned[startIndex].duration * 1000 <
              now - DESYNC_ALLOWANCE
          ) {
            startIndex++; // if the current song is over, shift it to the next one
          }
          return pruned.slice(startIndex);
        }
      );
      // do not set local state; currentSong is derived from query data
    }, [queryClient]);

    const playNextSong = useCallback(
      (isAutoPlay: boolean = false) => {
        // We are already loading a song, so we don't need to load another one.
        if (isLoading) {
          return;
        }

        dequeueSongs();

        const cachedSongs =
          queryClient.getQueryData<SongData[]>(LIVING_RADIO_QUERY_KEY) ?? [];
        if (cachedSongs.length === 0) {
          setIsPlaying(false); // nothing to play, pause playback
          return;
        }

        // Okay, now that current song is the first song in the list, we need to load the audio
        if (audioTrack.current) {
          // If the audio track is not the same as the current song, we need to load the new audio
          audioTrack.current.pause();
          audioTrack.current.currentTime = 0;
          audioTrack.current.src = cachedSongs[0].song_url;
          audioTrack.current.load();

          setIsLoading(true);
          audioTrack.current
            .play()
            .then(() => {
              setIsLoading(false);
              syncAudio();
              startTime.current = audioTrack.current?.currentTime || 0;
              endTime.current = audioTrack.current?.currentTime || 0;
              if (isAutoPlay) {
                previousSongSessionId.current = songSessionId.current;
                songSessionId.current = uuidv4();
                actionIndex.current = -1;
                logger.segmentTrack(
                  EventNames.audioPlayerEvent,
                  getAudioPlayerEvent(ActionName.autoPlayNewSong),
                  session
                );
              } else if (!firstPlayOccurred.current) {
                previousSongSessionId.current = songSessionId.current;
                songSessionId.current = uuidv4();
                actionIndex.current = -1;
                firstPlayOccurred.current = true;
                logger.segmentTrack(
                  EventNames.audioPlayerEvent,
                  getAudioPlayerEvent(ActionName.playNewSong),
                  session
                );
              } else {
                logger.segmentTrack(
                  EventNames.audioPlayerEvent,
                  getAudioPlayerEvent(ActionName.playSong),
                  session
                );
              }
            })
            .catch((e) => {
              console.error('Error playing audio', e);
              setIsLoading(false);
              setIsPlaying(false);
            });
        }
      },
      [isLoading, dequeueSongs, queryClient, session]
    );

    // This is to check if playback is desynced and if so jump to that position and play the next song. Ideally, we should never need this. This gets called on timeupdate.
    const syncAudio = useCallback(() => {
      const cachedSongs =
        queryClient.getQueryData<SongData[]>(LIVING_RADIO_QUERY_KEY) ?? [];
      if (cachedSongs.length === 0) {
        // We should never be here. If we are, there are no songs to play, so we pause.
        setIsPlaying(false);
        return;
      }
      // the next song is already playing, we need to swap.
      if (
        cachedSongs.length >= 2 &&
        Date.parse(cachedSongs[1].start_time) <
          new Date().getTime() - SWAP_DESYNC_ALLOWANCE
      ) {
        playNextSong();
        return;
      }
      // okay, calculate canonical position of current song and sync if needed.
      const currentSong = cachedSongs[0];
      const currentSongPosition = getCurrentSongPosition(currentSong);
      if (
        audioTrack.current &&
        Math.abs(audioTrack.current.currentTime - currentSongPosition) >
          DESYNC_ALLOWANCE / 1000
      ) {
        audioTrack.current.currentTime = currentSongPosition;
      }
    }, [queryClient]);

    const handleAudioError = useCallback(() => {
      setIsPlaying(false);
    }, []);

    const handleAudioPlay = useCallback(() => {
      setIsPlaying(true);
    }, []);

    const handleAudioPause = useCallback(() => {
      setIsPlaying(false);
    }, []);

    const handleAudioEnded = useCallback(() => {
      // Log SongEnd event when a song ends naturally
      if (audioTrack.current) {
        endTime.current = audioTrack.current.currentTime;
        logger.segmentTrack(
          EventNames.audioPlayerEvent,
          getAudioPlayerEvent(ActionName.songEnd),
          session
        );
      }
      playNextSong(true);
    }, [session]);

    const togglePlay = useCallback(() => {
      setIsAudioContextPostUserInteraction(true);
      setIsPlaying((prev) => !prev);
    }, []);

    const { data: mergedSongs } = useQuery<SongData[], Error>({
      queryKey: LIVING_RADIO_QUERY_KEY,
      queryFn: async () => {
        // return MOCK_SONGS;
        const { data, error } = await apiClient.GET(
          '/api/living_radio/{station_id}/song-list',
          {
            params: { path: { station_id: STATION_ID } },
          }
        );
        if (error) {
          throw new Error('Failed to fetch song list');
        }
        const previous =
          queryClient.getQueryData<SongData[]>(LIVING_RADIO_QUERY_KEY) ?? [];
        const incoming = data ?? [];

        if (previous.length === 0) {
          return incoming;
        }

        const lastPrev = previous[previous.length - 1];
        const prevEndMs =
          Date.parse(lastPrev.start_time) + lastPrev.duration * 1000;
        // Only append songs that start at or after the last known song's end.
        const newSongs = incoming.filter((song) => {
          const songStartTime = Date.parse(song.start_time);
          return songStartTime >= prevEndMs;
        });

        if (newSongs.length === 0) {
          return previous;
        }

        if (
          previous.length > 0 &&
          newSongs.length > 0 &&
          previous[previous.length - 1].song_id === newSongs[0].song_id
        ) {
          // When we are streaming last previous item may match the first new item;
          // drop the duplicate tail before appending.
          // The new version may have a duration (when streaming duration is 0)
          return [...previous.slice(0, -1), ...newSongs];
        }
        return [...previous, ...newSongs];
      },
      enabled: playbar.isLivingRadioMode,
      // Treat data as fresh until shortly before the last known song ends, then allow refetch.
      // This avoids unnecessary refetches mid-song and refreshes the queue just in time.
      staleTime: (query) => {
        const songs = (query.state.data as SongData[] | undefined) ?? [];
        if (songs.length === 0) return 0;
        const last = songs[songs.length - 1];
        const endMs = Date.parse(last.start_time) + last.duration * 1000;
        const ms = endMs - Date.now() - 15000; // refresh ~15s before we run out
        return ms > 0 ? ms : 0;
      },
      // Poll on a dynamic interval so we fetch near the boundary of upcoming songs.
      // We always impose a 1s minimum to keep the UI responsive when the list is empty.
      refetchInterval: (query) => {
        const songs = (query.state.data as SongData[] | undefined) ?? [];
        if (songs.length === 0) return 1000;
        const last = songs[songs.length - 1];
        const endMs = Date.parse(last.start_time) + last.duration * 1000;
        const ms = endMs - Date.now() - 15000; // poll a bit before the tail ends
        return Math.max(ms, 1000);
      },
      // Continue polling even if the tab is backgrounded so the queue doesn't fall behind.
      refetchIntervalInBackground: true,
    });

    // Prune list after data changes to keep first element as current song
    useEffect(() => {
      if (!mergedSongs) return;
      dequeueSongs();
    }, [mergedSongs, dequeueSongs]);

    // Song change detection when paused (we want to keep the radio banner description in sync)
    useEffect(() => {
      if (!playbar.isLivingRadioMode) return;

      // Reset the last detected song on (re)activation of living radio mode
      lastDetectedSongIdRef.current = null;

      const detectSongChanges = () => {
        const cachedSongs =
          queryClient.getQueryData<SongData[]>(LIVING_RADIO_QUERY_KEY) ?? [];
        if (cachedSongs.length === 0) return;

        const expectedCurrentSong = getCurrentSong(cachedSongs);
        if (
          expectedCurrentSong &&
          expectedCurrentSong.song_id !== lastDetectedSongIdRef.current
        ) {
          // Song has changed - update the queue to reflect the new current song
          lastDetectedSongIdRef.current = expectedCurrentSong.song_id;

          const expectedIndex = cachedSongs.findIndex(
            (song) => song.song_id === expectedCurrentSong.song_id
          );

          if (expectedIndex > 0) {
            // The expected song is not first in our list, so we need to update queue
            queryClient.setQueryData<SongData[]>(
              LIVING_RADIO_QUERY_KEY,
              cachedSongs.slice(expectedIndex)
            );
          }
        } else if (!expectedCurrentSong && lastDetectedSongIdRef.current) {
          lastDetectedSongIdRef.current = null;
        }
      };

      // Check for song changes every second
      const interval = setInterval(detectSongChanges, 1000);

      return () => clearInterval(interval);
    }, [queryClient, playbar.isLivingRadioMode]);

    useEffect(() => {
      if (isPlaying) {
        if (audioTrack.current) {
          audioTrack.current.addEventListener('timeupdate', syncAudio);
          audioTrack.current.addEventListener('ended', handleAudioEnded);
          audioTrack.current.addEventListener('error', handleAudioError);
          playNextSong();
        }
      } else {
        if (
          audioTrack.current &&
          !audioTrack.current.paused &&
          mergedSongs &&
          mergedSongs.length > 0
        ) {
          // Set end time and log pause if we have an active song
          endTime.current = audioTrack.current.currentTime;
          logger.segmentTrack(
            EventNames.audioPlayerEvent,
            getAudioPlayerEvent(ActionName.pauseSong),
            session
          );
          audioTrack.current.pause();
        }
      }
      return () => {
        if (audioTrack.current) {
          audioTrack.current.removeEventListener('timeupdate', syncAudio);
          audioTrack.current.removeEventListener('ended', handleAudioEnded);
          audioTrack.current.removeEventListener('error', handleAudioError);
        }
      };
    }, [isPlaying]);

    useEffect(() => {
      if (audioTrack.current) {
        audioTrack.current.volume = volume / 100;
      }
    }, [volume]);

    useEffect(() => {
      const supportsMediaSession =
        typeof navigator !== 'undefined' &&
        'mediaSession' in navigator &&
        !!navigator.mediaSession;
      const supportsMediaMetadata =
        typeof window !== 'undefined' && 'MediaMetadata' in window;

      if (playbar.isLivingRadioMode) {
        // Set up media session if supported
        if (supportsMediaSession) {
          navigator.mediaSession.setActionHandler('play', handleAudioPlay);
          navigator.mediaSession.setActionHandler('pause', handleAudioPause);
          navigator.mediaSession.setActionHandler('nexttrack', () => {});
          navigator.mediaSession.setActionHandler('previoustrack', () => {});
          navigator.mediaSession.setActionHandler('seekto', () => {});
          if (supportsMediaMetadata) {
            navigator.mediaSession.metadata = new MediaMetadata({
              title: '[Suno Radio] Deep Focus Beats',
              artist: 'Suno',
              artwork: [
                {
                  src: 'https://cdn-o.suno.com/suno-living-radio-poster.webp',
                  sizes: '256x256',
                  type: 'image/webp',
                },
              ],
            });
          }
        }
        if (playbar.isPlaying) {
          playbar.togglePlay(false);
        }
      } else {
        // Clean up media session if supported
        if (supportsMediaSession) {
          navigator.mediaSession.setActionHandler('play', null);
          navigator.mediaSession.setActionHandler('pause', null);
          navigator.mediaSession.setActionHandler('nexttrack', null);
          navigator.mediaSession.setActionHandler('previoustrack', null);
          navigator.mediaSession.setActionHandler('seekto', null);
          if (supportsMediaMetadata) {
            navigator.mediaSession.metadata = null;
          }
        }
        setIsPlaying(false);
      }
    }, [playbar.isLivingRadioMode]);

    return (
      <LiveRadioContext.Provider
        value={{
          isLiveRadio: playbar.isLivingRadioMode,
          setIsLiveRadio: playbar.setIsLivingRadioMode,
          isPlaying,
          setIsPlaying,
          togglePlay,
          isLoading,
          sourceNode,
          currentSong:
            mergedSongs && mergedSongs.length > 0 ? mergedSongs[0] : null,
          volume,
          setVolume,
          isAudioContextPostUserInteraction,
          setAudioElement,
          setSourceNode,
        }}
      >
        {children}
      </LiveRadioContext.Provider>
    );
  }
);
