'use client';

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

import { createAudioBus, type ChatAudioBus } from '../core/audio/audioBus';
import { useChatAudioPrefs } from '../core/audio/preferences';
import type {
  ChatAudioConfig,
  ChatAudioEvent,
  UseChatAudioReturn,
} from '../core/audio/types';

const ALL_EVENTS: ChatAudioEvent[] = [
  'messageSent',
  'messageReceived',
  'streamStart',
  'error',
  'mention',
  'notification',
];

function readReducedMotion(): boolean {
  if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
  return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

function readReducedData(): boolean {
  if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
  return window.matchMedia('(prefers-reduced-data: reduce)').matches;
}

function readVisibilityHidden(): boolean {
  if (typeof document === 'undefined') return false;
  return document.visibilityState === 'hidden';
}

export function useChatAudio(config: ChatAudioConfig = {}): UseChatAudioReturn {
  const {
    sounds = {},
    volume: volumeOverride,
    muted: mutedOverride,
    shouldPlay,
    respectReducedMotion = true,
    respectReducedData = true,
    muteWhenHidden = true,
  } = config;

  const volume = useChatAudioPrefs((s) => (volumeOverride != null ? volumeOverride : s.volume));
  const mutedPersisted = useChatAudioPrefs((s) => s.muted);
  const muted = mutedOverride != null ? mutedOverride : mutedPersisted;
  const enabledMap = useChatAudioPrefs((s) => s.enabled);
  const setVolumePref = useChatAudioPrefs((s) => s.setVolume);
  const setMutedPref = useChatAudioPrefs((s) => s.setMuted);
  const setEventEnabledPref = useChatAudioPrefs((s) => s.setEventEnabled);

  // Refs to keep `play()` referentially-stable while still reading current prefs.
  const volumeRef = useRef(volume);
  volumeRef.current = volume;
  const mutedRef = useRef(muted);
  mutedRef.current = muted;
  const enabledRef = useRef(enabledMap);
  enabledRef.current = enabledMap;
  const reducedMotionRef = useRef(readReducedMotion());
  const reducedDataRef = useRef(readReducedData());
  const hiddenRef = useRef(readVisibilityHidden());
  const shouldPlayRef = useRef(shouldPlay);
  shouldPlayRef.current = shouldPlay;

  // Watch reduced-motion / reduced-data preference changes.
  useEffect(() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
    const mqMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
    const mqData = window.matchMedia('(prefers-reduced-data: reduce)');
    const onMotion = () => {
      reducedMotionRef.current = mqMotion.matches;
    };
    const onData = () => {
      reducedDataRef.current = mqData.matches;
    };
    mqMotion.addEventListener('change', onMotion);
    mqData.addEventListener('change', onData);
    return () => {
      mqMotion.removeEventListener('change', onMotion);
      mqData.removeEventListener('change', onData);
    };
  }, []);

  // Visibility tracking — mute while tab is hidden.
  useEffect(() => {
    if (!muteWhenHidden || typeof document === 'undefined') return;
    const onVis = () => {
      hiddenRef.current = document.visibilityState === 'hidden';
    };
    document.addEventListener('visibilitychange', onVis);
    return () => document.removeEventListener('visibilitychange', onVis);
  }, [muteWhenHidden]);

  // Bus instance — created once per provider, sounds map hot-swapped on change.
  const busRef = useRef<ChatAudioBus | null>(null);
  if (busRef.current === null) {
    busRef.current = createAudioBus({
      sounds,
      getVolume: () => volumeRef.current,
      getMuted: () => effectiveMuted(),
      isEnabled: (event) => isEnabledImpl(event),
    });
  }

  function effectiveMuted(): boolean {
    if (mutedRef.current) return true;
    if (muteWhenHidden && hiddenRef.current) return true;
    if (respectReducedMotion && reducedMotionRef.current) return true;
    if (respectReducedData && reducedDataRef.current) return true;
    return false;
  }

  function isEnabledImpl(event: ChatAudioEvent): boolean {
    if (shouldPlayRef.current && shouldPlayRef.current(event) === false) return false;
    const flag = enabledRef.current[event];
    if (flag === false) return false;
    return true;
  }

  // Hot-swap sounds when caller-provided map changes.
  useEffect(() => {
    busRef.current?.setSounds(sounds);
  }, [sounds]);

  // Preload all configured events once.
  useEffect(() => {
    const bus = busRef.current;
    if (!bus) return;
    for (const event of ALL_EVENTS) {
      bus.preload(event);
    }
  }, [sounds]);

  // Dispose on unmount.
  useEffect(() => {
    const bus = busRef.current;
    return () => {
      bus?.dispose();
    };
  }, []);

  // Reactive `isUnlocked`.
  const isUnlocked = useSyncExternalStore(
    useCallback((cb) => busRef.current?.subscribeUnlock(cb) ?? (() => undefined), []),
    () => busRef.current?.isUnlocked() ?? false,
    () => false,
  );

  const play = useCallback((event: ChatAudioEvent) => {
    busRef.current?.play(event);
  }, []);
  const preload = useCallback((event: ChatAudioEvent) => {
    busRef.current?.preload(event);
  }, []);
  const unlock = useCallback(() => {
    busRef.current?.unlock();
  }, []);

  const isEventEnabled = useCallback(
    (event: ChatAudioEvent) => isEnabledImpl(event),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [],
  );

  const api = useMemo<UseChatAudioReturn>(
    () => ({
      play,
      preload,
      unlock,
      isUnlocked,
      muted,
      setMuted: (m: boolean) => setMutedPref(m),
      volume,
      setVolume: (v: number) => setVolumePref(v),
      isEventEnabled,
      setEventEnabled: (event, enabled) => setEventEnabledPref(event, enabled),
    }),
    [play, preload, unlock, isUnlocked, muted, volume, isEventEnabled, setMutedPref, setVolumePref, setEventEnabledPref],
  );

  // We need a useState here just to register a re-render trigger when the
  // bus reports unlock changes — but we already wired `useSyncExternalStore`
  // above, so this is a no-op holder.
  void useState;

  return api;
}
