import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { useContentCatalogPayload } from '@/business/content-catalog/hooks';
import { playBuiltInSound } from '@/business/classroom-timer/audio';
import { playRecipe } from '@/business/classroom-timer/audio-recipe';
import {
  useAudioFiles,
  useDeleteAudioFile,
  useGenerateAudioFile,
} from '@/business/audio-files/hooks';
import { canManageAudioFiles } from '@/business/audio-files/selectors';
import { getFirstQueryError, isAnyLoading } from '@/shared/business/queryState';
import {
  createTimerParticles,
  formatTimerTime,
  getTimerProgress,
  getTimerUrgencyColor,
  parseBoundedInteger,
} from '@/business/classroom-timer/selectors';
import {
  DEFAULT_CUSTOM_MINUTES,
  DEFAULT_CUSTOM_SECONDS,
  DEFAULT_TIMER_SECONDS,
  FULLSCREEN_PARTICLE_COUNT,
  TIMER_CARD_PARTICLE_COUNT,
  TIMER_FINISH_REPEAT_DELAY_MS,
  TIMER_PREVIEW_DURATION_MS,
} from '@/shared/constants/classroomTimer';
import { CONTENT_CATALOG_TYPES } from '@/shared/constants/contentCatalog';
import {
  TIMER_SOUND_GROUP_LABELS,
  TIMER_GENERATED_SOUND_ICON,
  TIMER_UPLOADED_SOUND_ICON,
} from '@/shared/constants/classroomTimer';
import type {
  ClassroomTimerTip,
  PresetTimerOption,
  SensoryBackground,
  SensoryBackgroundId,
  TimerSound,
  TimerSoundGroup,
  TimerSoundOption,
} from '@/shared/types/classroomTimer';
import type { AudioFileDto } from '@/shared/types/audioFiles';
import type { UserRole } from '@/shared/types/app';

declare global {
  interface Window {
    readonly webkitAudioContext?: typeof AudioContext;
  }
}

export function useClassroomTimer(userRole: UserRole) {
  const backgroundsQuery = useContentCatalogPayload<readonly SensoryBackground[]>(
    CONTENT_CATALOG_TYPES.classroomTimerBackgrounds,
    [],
  );
  const soundsQuery = useContentCatalogPayload<readonly TimerSoundOption[]>(
    CONTENT_CATALOG_TYPES.classroomTimerSounds,
    [],
  );
  const audioFilesQuery = useAudioFiles();
  const generateMutation = useGenerateAudioFile();
  const deleteMutation = useDeleteAudioFile();
  const canManageAudio = canManageAudioFiles(userRole);
  const presetsQuery = useContentCatalogPayload<readonly PresetTimerOption[]>(
    CONTENT_CATALOG_TYPES.classroomTimerPresets,
    [],
  );
  const tipsQuery = useContentCatalogPayload<readonly ClassroomTimerTip[]>(
    CONTENT_CATALOG_TYPES.classroomTimerTips,
    [],
  );
  const [totalSeconds, setTotalSeconds] = useState(DEFAULT_TIMER_SECONDS);
  const [remainingSeconds, setRemainingSeconds] = useState(DEFAULT_TIMER_SECONDS);
  const [isRunning, setIsRunning] = useState(false);
  const [isFinished, setIsFinished] = useState(false);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [selectedBackgroundId, setSelectedBackgroundId] = useState<SensoryBackgroundId | null>(null);
  const [selectedSoundKey, setSelectedSoundKey] = useState<string | null>(null);
  const [soundEnabled, setSoundEnabled] = useState(true);
  const [customMinutes, setCustomMinutes] = useState(DEFAULT_CUSTOM_MINUTES);
  const [customSeconds, setCustomSeconds] = useState(DEFAULT_CUSTOM_SECONDS);
  const [previewPlaying, setPreviewPlaying] = useState(false);

  const intervalRef = useRef<number | null>(null);
  const audioCtxRef = useRef<AudioContext | null>(null);
  const fullscreenRef = useRef<HTMLDivElement>(null);

  const displayParticles = useMemo(
    () => createTimerParticles(TIMER_CARD_PARTICLE_COUNT, 6, 3),
    [],
  );
  const fullscreenParticles = useMemo(
    () => createTimerParticles(FULLSCREEN_PARTICLE_COUNT, 8, 4),
    [],
  );
  const backgrounds = backgroundsQuery.payload;
  const builtInOptions = soundsQuery.payload;
  const presets = presetsQuery.payload;
  const tips = tipsQuery.payload;
  const contentQueries = [backgroundsQuery, soundsQuery, presetsQuery, tipsQuery];
  const selectedBackground = useMemo(
    () => backgrounds.find((background) => background.id === selectedBackgroundId) ?? backgrounds[0] ?? null,
    [backgrounds, selectedBackgroundId],
  );

  const audioRows = useMemo<readonly AudioFileDto[]>(
    () => audioFilesQuery.data ?? [],
    [audioFilesQuery.data],
  );

  // Built-in synthesized sounds (hardcoded) + the audio library, grouped by
  // origin for a clear picker structure.
  const soundGroups = useMemo<readonly TimerSoundGroup[]>(() => {
    const builtinSounds: TimerSound[] = builtInOptions.map((option) => ({
      key: `builtin:${option.id}`,
      name: option.name,
      icon: option.icon,
      kind: 'builtin',
      soundType: option.id,
      recipe: null,
      url: null,
      audioFileId: null,
      canDelete: false,
    }));

    const toAudioSound = (row: AudioFileDto): TimerSound => ({
      key: `audio:${row.id}`,
      name: row.title,
      icon: row.kind === 'recipe' ? TIMER_GENERATED_SOUND_ICON : TIMER_UPLOADED_SOUND_ICON,
      kind: row.kind,
      soundType: null,
      recipe: row.recipe,
      url: row.url,
      audioFileId: row.id,
      // A global default (no organization) is read-only; own-org rows are
      // deletable by managers. The backend enforces this regardless.
      canDelete: canManageAudio && row.organizationId !== null,
    });

    const generatedSounds = audioRows
      .filter((row) => row.kind === 'recipe')
      .map(toAudioSound);
    const uploadedSounds = audioRows
      .filter((row) => row.kind === 'file' || row.kind === 'url')
      .map(toAudioSound);

    return [
      { id: 'builtin', label: TIMER_SOUND_GROUP_LABELS.builtin, sounds: builtinSounds },
      { id: 'generated', label: TIMER_SOUND_GROUP_LABELS.generated, sounds: generatedSounds },
      { id: 'uploaded', label: TIMER_SOUND_GROUP_LABELS.uploaded, sounds: uploadedSounds },
    ];
  }, [audioRows, builtInOptions, canManageAudio]);

  const allSounds = useMemo<readonly TimerSound[]>(
    () => soundGroups.flatMap((group) => group.sounds),
    [soundGroups],
  );

  const selectedSound = useMemo(
    () =>
      allSounds.find((sound) => sound.key === selectedSoundKey) ?? allSounds[0] ?? null,
    [allSounds, selectedSoundKey],
  );

  const getAudioContext = useCallback((): AudioContext => {
    if (!audioCtxRef.current) {
      const AudioContextConstructor = window.AudioContext || window.webkitAudioContext;

      if (!AudioContextConstructor) {
        throw new Error('Web Audio API is not supported in this browser.');
      }

      audioCtxRef.current = new AudioContextConstructor();
    }

    if (audioCtxRef.current.state === 'suspended') {
      void audioCtxRef.current.resume();
    }

    return audioCtxRef.current;
  }, []);

  // Plays any sound regardless of origin: a built-in is synthesized by id, a
  // recipe is synthesized from parameters, a file/url is played from its URL.
  const playTimerSound = useCallback(
    (sound: TimerSound) => {
      if (sound.kind === 'builtin' && sound.soundType) {
        playBuiltInSound(sound.soundType, getAudioContext());
        return;
      }
      if (sound.kind === 'recipe' && sound.recipe) {
        playRecipe(sound.recipe, getAudioContext());
        return;
      }
      if (sound.url) {
        const audio = new Audio(sound.url);
        void audio.play().catch(() => undefined);
      }
    },
    [getAudioContext],
  );

  useEffect(() => {
    if (isRunning && remainingSeconds > 0) {
      intervalRef.current = window.setInterval(() => {
        setRemainingSeconds((previousValue) => {
          if (previousValue <= 1) {
            setIsRunning(false);
            setIsFinished(true);
            return 0;
          }

          return previousValue - 1;
        });
      }, 1000);
    }

    return () => {
      if (intervalRef.current) {
        window.clearInterval(intervalRef.current);
      }
    };
  }, [isRunning, remainingSeconds]);

  useEffect(() => {
    if (!isFinished || !soundEnabled || !selectedSound) {
      return;
    }

    playTimerSound(selectedSound);
    const repeatSoundTimeout = window.setTimeout(() => {
      playTimerSound(selectedSound);
    }, TIMER_FINISH_REPEAT_DELAY_MS);

    return () => window.clearTimeout(repeatSoundTimeout);
  }, [playTimerSound, isFinished, selectedSound, soundEnabled]);

  useEffect(() => {
    const handleFullscreenChange = () => {
      if (!document.fullscreenElement) {
        setIsFullscreen(false);
      }
    };

    document.addEventListener('fullscreenchange', handleFullscreenChange);
    return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
  }, []);

  const handleStart = useCallback(() => {
    if (remainingSeconds <= 0) {
      return;
    }

    getAudioContext();
    setIsRunning(true);
    setIsFinished(false);
  }, [getAudioContext, remainingSeconds]);

  const handlePause = useCallback(() => {
    setIsRunning(false);
  }, []);

  const handleReset = useCallback(() => {
    setIsRunning(false);
    setIsFinished(false);
    setRemainingSeconds(totalSeconds);
  }, [totalSeconds]);

  const handleSetTime = useCallback((seconds: number) => {
    setIsRunning(false);
    setIsFinished(false);
    setTotalSeconds(seconds);
    setRemainingSeconds(seconds);
  }, []);

  const handleCustomTime = useCallback(() => {
    const total = customMinutes * 60 + customSeconds;
    if (total > 0) {
      handleSetTime(total);
    }
  }, [customMinutes, customSeconds, handleSetTime]);

  const toggleFullscreen = useCallback(async () => {
    if (!isFullscreen) {
      if (fullscreenRef.current) {
        await fullscreenRef.current.requestFullscreen();
      }
      setIsFullscreen(true);
      return;
    }

    if (document.fullscreenElement) {
      await document.exitFullscreen();
    }
    setIsFullscreen(false);
  }, [isFullscreen]);

  const previewSound = useCallback((sound: TimerSound) => {
    if (previewPlaying) {
      return;
    }

    setPreviewPlaying(true);
    playTimerSound(sound);
    window.setTimeout(() => setPreviewPlaying(false), TIMER_PREVIEW_DURATION_MS);
  }, [playTimerSound, previewPlaying]);

  const generateSound = useCallback(async (name?: string) => {
    const created = await generateMutation.mutateAsync(name);
    setSelectedSoundKey(`audio:${created.id}`);
  }, [generateMutation]);

  const deleteSound = useCallback(
    async (audioFileId: string) => {
      await deleteMutation.mutateAsync(audioFileId);
      setSelectedSoundKey((current) =>
        current === `audio:${audioFileId}` ? null : current,
      );
    },
    [deleteMutation],
  );

  const progress = getTimerProgress(totalSeconds, remainingSeconds);
  const formattedTime = formatTimerTime(remainingSeconds);
  const urgencyColor = selectedBackground
    ? getTimerUrgencyColor(isFinished, progress, selectedBackground)
    : 'text-cyan-100';

  return {
    state: {
      backgrounds,
      soundGroups,
      presets,
      tips,
      totalSeconds,
      remainingSeconds,
      isRunning,
      isFinished,
      isFullscreen,
      selectedBackground,
      selectedSound,
      soundEnabled,
      customMinutes,
      customSeconds,
      previewPlaying,
      canManageAudio,
      isGeneratingSound: generateMutation.isPending,
      isDeletingSound: deleteMutation.isPending,
      progress,
      formattedTime,
      urgencyColor,
      displayParticles,
      fullscreenParticles,
      isContentLoading: isAnyLoading(...contentQueries),
      contentError: getFirstQueryError(...contentQueries),
    },
    refs: {
      fullscreenRef,
    },
    actions: {
      handleStart,
      handlePause,
      handleReset,
      handleSetTime,
      handleCustomTime,
      toggleFullscreen,
      previewSound,
      generateSound,
      deleteSound,
      setSelectedBackground: (background: SensoryBackground) => setSelectedBackgroundId(background.id),
      setSelectedSound: (sound: TimerSound) => setSelectedSoundKey(sound.key),
      setSoundEnabled,
      setCustomMinutes,
      setCustomSeconds,
      parseCustomMinutes: (value: string) => setCustomMinutes(parseBoundedInteger(value, 0, 120)),
      parseCustomSeconds: (value: string) => setCustomSeconds(parseBoundedInteger(value, 0, 59)),
    },
  };
}
