import { assetUrl } from "@/lib/asset";
import { BASE_PATH } from "@/const";
import { trpc } from "@/lib/trpc";
import {
  episodioToPlayable,
  faixaToPlayable,
  vinhetaToPlayable,
  type PlayableItem,
} from "@shared/playable";
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
  type ReactNode,
} from "react";
import { toast } from "sonner";

export type RepeatMode = "off" | "one" | "all";

/**
 * DIAGNÓSTICO DE CONTINUIDADE (jul/2026, temporário) — registra cada
 * etapa da troca de faixa num arquivo de log no servidor, com hora exata
 * e se a aba estava visível ou escondida. Existe só para descobrir, com
 * prova, onde a troca de faixa trava com a tela apagada — em vez de mais
 * uma tentativa de correção às cegas.
 *
 * Usa `navigator.sendBeacon` de propósito, não `fetch`: o beacon é
 * projetado especificamente para sobreviver a uma aba sendo suspensa ou
 * fechada — o navegador garante a entrega em segundo plano de um jeito
 * que uma chamada fetch() comum não garante.
 */
function diag(mensagem: string) {
  try {
    const visibilidade = typeof document !== "undefined" ? document.visibilityState : "desconhecido";
    const corpo = JSON.stringify({ mensagem: `[vis:${visibilidade}] ${mensagem}` });
    const blob = new Blob([corpo], { type: "application/json" });
    navigator.sendBeacon?.(`${BASE_PATH}/api/debug-log`, blob);
  } catch {
    // nunca deixa o diagnóstico quebrar o player em si
  }
}

// A cada quantas faixas de Música tocadas em sequência entram 2 vinhetas
// (avisos da plataforma) — confirmado com o Marsan: conta o total real
// ouvido na sessão, mesmo trocando de álbum/artista no meio.
const INTERVALO_VINHETA_MUSICA = 3;

/**
 * Limite de tempo pras consultas de rede feitas dentro de handleEnded
 * (AutoDJ, rodízio de vinhetas). Com a tela apagada, o sistema pode
 * atrasar bastante requisições em segundo plano — sem esse limite, a
 * troca de faixa ficaria esperando indefinidamente. Se estourar, cai
 * pro comportamento padrão (segue sem AutoDJ/vinheta) em vez de travar.
 */
const TIMEOUT_REDE_MS = 6000;

function comTimeout<T>(promise: Promise<T>, ms: number): Promise<T | null> {
  return Promise.race([
    promise,
    new Promise<null>(resolve => setTimeout(() => resolve(null), ms)),
  ]);
}

type PlayerState = {
  queue: PlayableItem[];
  queueIndex: number;
  currentItem: PlayableItem | null;
  // Itens a tocar em sequência assim que o item atual terminar — usado
  // para encaixar vinhetas (1 ou mais) antes de retomar o conteúdo real.
  // Generalização do antigo "pendingNext" (que só suportava 1 vinheta).
  pendingQueue: PlayableItem[];
  // Quando a fila de vinhetas acabar e chegar no conteúdo real, sincroniza
  // queue/queueIndex com isto — sem isso, depois de uma pausa de vinhetas
  // a fila ficava "presa" na posição de antes da pausa, e o próximo
  // intervalo calculava a próxima faixa errado (repetindo ou pulando).
  pendingQueueSync: { queue: PlayableItem[]; index: number } | null;
  isPlaying: boolean;
  currentTime: number;
  duration: number;
  volume: number;
  repeat: RepeatMode;
  shuffle: boolean;
};

type PlayerContextValue = PlayerState & {
  audioRef: React.RefObject<HTMLAudioElement | null>;
  play: (item: PlayableItem, queue?: PlayableItem[], queueIndex?: number) => void;
  togglePlayPause: () => void;
  seek: (time: number) => void;
  setVolume: (volume: number) => void;
  playNext: () => void;
  playPrevious: () => void;
  toggleRepeat: () => void;
  toggleShuffle: () => void;
};

const PlayerContext = createContext<PlayerContextValue | null>(null);

const initialState: PlayerState = {
  queue: [],
  queueIndex: -1,
  currentItem: null,
  pendingQueue: [],
  pendingQueueSync: null,
  isPlaying: false,
  currentTime: 0,
  duration: 0,
  volume: 1,
  repeat: "off",
  shuffle: false,
};

export function PlayerProvider({ children }: { children: ReactNode }) {
  const audioRef = useRef<HTMLAudioElement | null>(null);
  const [state, setState] = useState<PlayerState>(initialState);
  const stateRef = useRef(state);
  stateRef.current = state;

  // CRÍTICO: a dependência é currentItem?.id (string estável, ex:
  // "episodio-42"), NUNCA o objeto PlayableItem inteiro. episodioToPlayable()
  // e vinhetaToPlayable() criam um NOVO OBJETO a cada chamada — se algum
  // efeito dependesse do objeto, dispararia a cada re-render que recriasse
  // esse objeto, causando play()/pause() em sucessão rápida (loop de
  // AbortError). Esta foi a causa raiz confirmada do bug anterior — nunca
  // reverter para depender do objeto currentItem completo.
  const currentItemId = state.currentItem?.id ?? null;

  const loadedSrcRef = useRef<string | null>(null);
  const utils = trpc.useUtils();
  const reproduzirMutation = trpc.podcast.reproduzir.useMutation();
  const reproduzirMusicaMutation = trpc.musica.reproduzir.useMutation();

  // Contador de faixas de Música tocadas nesta sessão (não persiste entre
  // recarregamentos — reinicia do zero a cada nova sessão, o que é
  // aceitável para um intervalo de vinhetas).
  const musicaContagemRef = useRef(0);

  const play = useCallback(
    (item: PlayableItem, queue?: PlayableItem[], queueIndex?: number) => {
      if (!item.arquivo && !item.streamUrl) {
        toast.error("Este conteúdo ainda não possui um arquivo de áudio.");
        return;
      }

      setState(prev => ({
        ...prev,
        queue: queue ?? [item],
        queueIndex: queueIndex ?? 0,
        currentItem: item,
        pendingQueue: [],
        pendingQueueSync: null,
        isPlaying: true,
        currentTime: 0,
        duration: 0,
      }));

      // Estatística de reprodução — cada módulo tem seu próprio contador.
      if (item.kind === "episodio") {
        reproduzirMutation.mutate({ episodioId: item.sourceId });
      } else if (item.kind === "musica") {
        reproduzirMusicaMutation.mutate({ faixaId: item.sourceId });
      }
    },
    [reproduzirMutation, reproduzirMusicaMutation]
  );

  /**
   * Toca o primeiro item de uma sequência (tipicamente vinheta(s) + o
   * conteúdo real que viria a seguir), guardando o restante em
   * `pendingQueue` para continuar automaticamente quando cada um terminar.
   * Não mexe no histórico/estatística de "queue" normal — é só uma
   * interposição temporária no que toca a seguir.
   *
   * `sync`, se informado, é aplicado a `queue`/`queueIndex` assim que a
   * fila de pendências esvaziar (ou seja, quando chegamos no conteúdo
   * real, depois das vinhetas) — sem isso, a fila principal ficava presa
   * na posição de antes da pausa.
   */
  const playFila = useCallback(
    (itens: PlayableItem[], sync?: { queue: PlayableItem[]; index: number }) => {
      if (itens.length === 0) return;
      const [primeiro, ...resto] = itens;

      if (!primeiro.arquivo && !primeiro.streamUrl) {
        // Item sem áudio no meio da fila (não deveria acontecer) — pula
        // pro próximo, sem travar a reprodução.
        playFila(resto, sync);
        return;
      }

      const chegouNoConteudoReal = resto.length === 0;

      setState(prev => ({
        ...prev,
        currentItem: primeiro,
        pendingQueue: resto,
        pendingQueueSync: chegouNoConteudoReal ? null : (sync ?? prev.pendingQueueSync),
        queue: chegouNoConteudoReal && sync ? sync.queue : prev.queue,
        queueIndex: chegouNoConteudoReal && sync ? sync.index : prev.queueIndex,
        isPlaying: true,
        currentTime: 0,
        duration: 0,
      }));
    },
    []
  );

  const togglePlayPause = useCallback(() => {
    setState(prev => {
      if (!prev.currentItem) return prev;
      return { ...prev, isPlaying: !prev.isPlaying };
    });
  }, []);

  const seek = useCallback((time: number) => {
    if (audioRef.current) {
      audioRef.current.currentTime = time;
    }
    setState(prev => ({ ...prev, currentTime: time }));
  }, []);

  const setVolume = useCallback((volume: number) => {
    if (audioRef.current) {
      audioRef.current.volume = volume;
    }
    setState(prev => ({ ...prev, volume }));
  }, []);

  const playNext = useCallback(() => {
    const s = stateRef.current;
    if (s.queue.length === 0) return;

    let nextIdx: number;
    if (s.shuffle && s.queue.length > 1) {
      do {
        nextIdx = Math.floor(Math.random() * s.queue.length);
      } while (nextIdx === s.queueIndex);
    } else {
      nextIdx = s.queueIndex + 1;
      if (nextIdx >= s.queue.length) {
        if (s.repeat === "all") nextIdx = 0;
        else return;
      }
    }

    play(s.queue[nextIdx], s.queue, nextIdx);
  }, [play]);

  const playPrevious = useCallback(() => {
    const s = stateRef.current;
    if (s.queue.length === 0) return;

    const prevIdx = s.queueIndex - 1;
    if (prevIdx < 0) return;

    play(s.queue[prevIdx], s.queue, prevIdx);
  }, [play]);

  const toggleRepeat = useCallback(() => {
    setState(prev => {
      const next: RepeatMode = prev.repeat === "off" ? "all" : prev.repeat === "all" ? "one" : "off";
      return { ...prev, repeat: next };
    });
  }, []);

  const toggleShuffle = useCallback(() => {
    setState(prev => ({ ...prev, shuffle: !prev.shuffle }));
  }, []);

  /**
   * PREFETCH (jul/2026) — técnica usada no SpotOnFide antigo em PHP pra
   * lidar com tela apagada: em vez de descobrir "o que toca depois" só
   * quando a faixa ATUAL termina (exatamente o momento em que a tela pode
   * estar apagada e a rede mais lenta/suspensa), a decisão é buscada
   * assim que a faixa COMEÇA a tocar — com bastante folga antes de
   * precisar dela de verdade. Quando "ended" disparar, o resultado já
   * está pronto na maior parte das vezes, sem depender de rede naquele
   * instante crítico.
   */
  const prefetchAutoDJRef = useRef<{
    paraItemId: string;
    vinhetas: PlayableItem[];
    proximoEpisodio: PlayableItem | null;
  } | null>(null);

  const prefetchVinhetaMusicaRef = useRef<{
    paraItemId: string;
    vinhetas: PlayableItem[];
  } | null>(null);

  useEffect(() => {
    const item = state.currentItem;
    prefetchAutoDJRef.current = null;
    prefetchVinhetaMusicaRef.current = null;
    if (!item) return;

    let cancelado = false;

    // Prefetch do AutoDJ de Podcast — só faz sentido se a série tem o
    // interruptor "tocar vinheta"/AutoDJ ligado, mas isso o próprio
    // endpoint já resolve (devolve vinhetas só quando aplicável).
    if (item.kind === "episodio" && item.programaId) {
      utils.client.podcast.getProximoConteudoAutoDJ
        .query({ programaId: item.programaId, episodioAtualId: item.sourceId })
        .then(result => {
          if (cancelado) return;
          const vinhetasComIdUnico = (result?.vinhetas ?? []).map((v, idx) => {
            const vi = vinhetaToPlayable(v);
            return { ...vi, id: `${vi.id}::${idx}` };
          });
          prefetchAutoDJRef.current = {
            paraItemId: item.id,
            vinhetas: vinhetasComIdUnico,
            proximoEpisodio: result?.proximoEpisodio ? episodioToPlayable(result.proximoEpisodio) : null,
          };
        })
        .catch(() => {
          prefetchAutoDJRef.current = null;
        });
    }

    // Prefetch do intervalo de vinhetas de Música — só busca se ESTA
    // faixa, ao terminar, vai completar o intervalo (musicaContagemRef + 1).
    if (item.kind === "musica") {
      const vaiCompletarIntervalo =
        (musicaContagemRef.current + 1) % INTERVALO_VINHETA_MUSICA === 0;
      if (vaiCompletarIntervalo) {
        utils.client.podcast.getProximasVinhetas
          .query({ quantidade: 2 })
          .then(vinhetas => {
            if (cancelado) return;
            const vinhetasComIdUnico = vinhetas.map((v, idx) => {
              const vi = vinhetaToPlayable(v);
              return { ...vi, id: `${vi.id}::${idx}` };
            });
            prefetchVinhetaMusicaRef.current = { paraItemId: item.id, vinhetas: vinhetasComIdUnico };
          })
          .catch(() => {
            prefetchVinhetaMusicaRef.current = null;
          });
      }
    }

    return () => {
      cancelado = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [currentItemId, utils]);

  /**
   * Busca as próximas 2 vinhetas do rodízio (compartilhado entre todos os
   * ouvintes, ver server/podcast-db.ts) e devolve como PlayableItem[].
   * Nunca lança erro — se falhar, devolve [] e a reprodução segue direto
   * pro conteúdo real, sem travar por causa de um aviso da plataforma.
   *
   * BUG CORRIGIDO (jul/2026): quando existem poucas vinhetas cadastradas
   * (ex: só 1 na categoria "transicao"), o rodízio pode devolver a MESMA
   * vinheta duas vezes seguidas. Como o Efeito 1/2 do player dependem do
   * `id` do PlayableItem pra saber se precisam trocar a fonte de áudio
   * (ver comentário mais abaixo — é a mesma regra que corrigiu o loop de
   * AbortError), dois itens com o mesmo `id` em sequência faziam o player
   * achar que "nada mudou" e nunca chamava play() de novo — o áudio
   * simplesmente parava depois da vinheta. Aqui cada ocorrência ganha um
   * sufixo (`::0`, `::1`...) garantindo um id único mesmo repetindo a
   * mesma vinheta.
   */
  const buscarVinhetasDoRodizio = useCallback(async (): Promise<PlayableItem[]> => {
    try {
      const vinhetas = await comTimeout(
        utils.client.podcast.getProximasVinhetas.query({ quantidade: 2 }),
        TIMEOUT_REDE_MS
      );
      if (!vinhetas) return [];
      return vinhetas.map((v, idx) => {
        const item = vinhetaToPlayable(v);
        return { ...item, id: `${item.id}::${idx}` };
      });
    } catch {
      return [];
    }
  }, [utils]);

  // AutoDJ / avanço de fila quando a faixa atual termina.
  const handleEnded = useCallback(async () => {
    diag(`handleEnded INICIOU — item atual: ${stateRef.current.currentItem?.titulo ?? "nenhum"}`);
    const s = stateRef.current;

    // 1. Existe uma sequência pendente (ex: vinheta 1 → vinheta 2 → próximo
    //    conteúdo real) — continua por ela antes de qualquer outra lógica.
    if (s.pendingQueue.length > 0) {
      diag("branch: pendingQueue — continuando sequência de vinhetas/conteúdo pendente");
      playFila(s.pendingQueue, s.pendingQueueSync ?? undefined);
      return;
    }

    // 2. Repetir a faixa atual.
    if (s.repeat === "one" && s.currentItem) {
      diag("branch: repeat=one — reiniciando a mesma faixa");
      const audio = audioRef.current;
      if (audio) {
        audio.currentTime = 0;
        audio.play().catch(() => {});
      }
      return;
    }

    // 3. Fila explícita (playlist/álbum/lista) tem mais itens.
    if (s.queue.length > 1 && s.queueIndex < s.queue.length - 1) {
      const proximoItem = s.queue[s.queueIndex + 1];
      const proximoIdx = s.queueIndex + 1;
      diag(`branch: fila explícita tem mais itens — próximo: ${proximoItem.titulo}`);

      // Intervalo automático de vinhetas em Música — a cada N faixas
      // tocadas (contando o total da sessão, não por álbum).
      if (s.currentItem?.kind === "musica") {
        musicaContagemRef.current += 1;
        if (musicaContagemRef.current % INTERVALO_VINHETA_MUSICA === 0) {
          // Usa o resultado pré-buscado (enquanto a faixa ainda tocava) se
          // já estiver pronto — evita depender de rede bem na hora crítica.
          const prefetch = prefetchVinhetaMusicaRef.current;
          const prefetchPronto = prefetch?.paraItemId === s.currentItem.id;
          diag(`intervalo de vinheta de música completado — prefetch pronto? ${prefetchPronto}`);
          const vinhetas = prefetchPronto ? prefetch!.vinhetas : await buscarVinhetasDoRodizio();

          if (vinhetas.length > 0) {
            diag(`tocando ${vinhetas.length} vinheta(s) antes de: ${proximoItem.titulo}`);
            playFila([...vinhetas, proximoItem], { queue: s.queue, index: proximoIdx });
            return;
          }
          diag("busca de vinhetas voltou vazia — pulando direto pro próximo item");
        }
      }

      diag(`chamando play() direto para: ${proximoItem.titulo}`);
      play(proximoItem, s.queue, proximoIdx);
      return;
    }

    if (s.queue.length > 1 && s.repeat === "all") {
      diag("branch: repeat=all — voltando ao início da fila");
      play(s.queue[0], s.queue, 0);
      return;
    }

    // 3.5. Fila de Música acabou (fim do álbum/playlist) — o DJ continua
    //      sozinho com mais faixas, tipo rádio automática, em vez de
    //      simplesmente parar. Busca as mais tocadas, evitando repetir
    //      imediatamente o que acabou de tocar nessa fila.
    if (s.currentItem?.kind === "musica" && s.repeat !== "all") {
      diag("branch: fim de fila de música — buscando mais faixas (getTopFaixas)");
      try {
        const maisFaixas = await comTimeout(
          utils.client.musica.getTopFaixas.query(),
          TIMEOUT_REDE_MS
        );
        diag(`getTopFaixas respondeu: ${maisFaixas ? maisFaixas.length + " faixas" : "null/timeout"}`);

        if (maisFaixas && maisFaixas.length > 0) {
          const idsJaNaFila = new Set(
            s.queue.filter(i => i.kind === "musica").map(i => i.sourceId)
          );
          const semRepetir = maisFaixas.filter(f => !idsJaNaFila.has(f.id));
          const pool = (semRepetir.length > 0 ? semRepetir : maisFaixas).map(faixaToPlayable);

          if (pool.length > 0) {
            diag(`continuando com mais música — próximo: ${pool[0].titulo}`);
            play(pool[0], pool, 0);
            return;
          }
        }
      } catch (err) {
        diag(`getTopFaixas ERRO: ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    // 4. AutoDJ: aplica-se apenas a episódios de Podcast (kind === "episodio"),
    //    e só quando a série tem o interruptor "tocar vinheta" ligado no
    //    admin (ver server/podcast-db.ts — configVinheta).
    if (s.currentItem?.kind === "episodio" && s.currentItem.programaId) {
      diag("branch: AutoDJ de podcast — verificando prefetch");
      // Usa o resultado pré-buscado (enquanto o episódio ainda tocava) se
      // já estiver pronto — esse é o caminho que evita depender de rede
      // bem na hora em que a tela pode estar apagada.
      const prefetch = prefetchAutoDJRef.current;
      let vinhetasComIdUnico: PlayableItem[] = [];
      let proximoItem: PlayableItem | null = null;

      if (prefetch?.paraItemId === s.currentItem.id) {
        diag("AutoDJ: prefetch estava pronto, usando sem chamar rede");
        vinhetasComIdUnico = prefetch.vinhetas;
        proximoItem = prefetch.proximoEpisodio;
      } else {
        diag("AutoDJ: prefetch NÃO estava pronto — chamando rede agora (ponto crítico)");
        try {
          const result = await comTimeout(
            utils.client.podcast.getProximoConteudoAutoDJ.query({
              programaId: s.currentItem.programaId,
              episodioAtualId: s.currentItem.sourceId,
            }),
            TIMEOUT_REDE_MS
          );
          diag(`AutoDJ getProximoConteudoAutoDJ respondeu: ${result ? "ok" : "null/timeout"}`);

          if (result?.proximoEpisodio) {
            proximoItem = episodioToPlayable(result.proximoEpisodio);
            vinhetasComIdUnico = (result.vinhetas ?? []).map((v, idx) => {
              const item = vinhetaToPlayable(v);
              return { ...item, id: `${item.id}::${idx}` };
            });
          }
        } catch (err) {
          diag(`AutoDJ ERRO: ${err instanceof Error ? err.message : String(err)}`);
        }
      }

      if (proximoItem) {
        diag(`AutoDJ: tocando próximo — ${proximoItem.titulo} (${vinhetasComIdUnico.length} vinheta(s) antes)`);
        if (vinhetasComIdUnico.length > 0) {
          playFila([...vinhetasComIdUnico, proximoItem]);
        } else {
          play(proximoItem);
        }
        return;
      }
      diag("AutoDJ: sem próximo item encontrado");
    }

    diag("handleEnded TERMINOU sem encontrar próximo conteúdo — parando reprodução (isPlaying: false)");
    setState(prev => ({ ...prev, isPlaying: false }));
  }, [play, playFila, buscarVinhetasDoRodizio, utils]);

  // ── Efeito 1 — troca de fonte ──────────────────────────────────────────
  // (currentItemId já declarado no início do componente, ver comentário lá)

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const item = stateRef.current.currentItem;
    if (!item) return;

    const rawSrc = item.kind === "radio" ? item.streamUrl : item.arquivo;
    const src = assetUrl(rawSrc);

    if (!src) return;

    if (loadedSrcRef.current !== src) {
      loadedSrcRef.current = src;
      audio.src = src;
      audio.currentTime = 0;
    }
    // Se já é o mesmo item carregado, não mexe em audio.src nem reseta currentTime.
  }, [currentItemId]);

  // ── Efeito 2 — play/pause ──────────────────────────────────────────────
  // Depende apenas do booleano isPlaying, nunca do objeto currentItem.
  // Isso evita que qualquer recriação de objeto dispare play()/pause()
  // indevidamente.
  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;
    if (!stateRef.current.currentItem) return;

    if (state.isPlaying) {
      diag(`Efeito2: chamando audio.play() para "${stateRef.current.currentItem?.titulo ?? "?"}"`);
      audio.play().then(
        () => diag(`Efeito2: audio.play() teve SUCESSO`),
        (err: unknown) => {
          // AbortError é esperado quando play() é interrompido por uma troca
          // legítima de faixa — não é um erro real, apenas log informativo.
          const name = err instanceof Error ? err.name : "";
          diag(`Efeito2: audio.play() FALHOU — nome do erro: ${name || "desconhecido"}`);
          if (name !== "AbortError") {
            console.error("[SpotOnFide Player] Erro ao reproduzir:", err, {
              src: audio.src,
              item: stateRef.current.currentItem?.titulo,
            });
          }
          setState(prev => (prev.isPlaying ? { ...prev, isPlaying: false } : prev));
        }
      );
    } else {
      audio.pause();
    }
  }, [state.isPlaying, currentItemId]);

  // Listeners nativos do <audio>, anexados uma única vez.
  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const atualizarPositionState = () => {
      if (!("mediaSession" in navigator) || typeof navigator.mediaSession.setPositionState !== "function") {
        return;
      }
      if (!isFinite(audio.duration) || audio.duration <= 0) return;
      try {
        navigator.mediaSession.setPositionState({
          duration: audio.duration,
          playbackRate: audio.playbackRate || 1,
          position: Math.min(audio.currentTime, audio.duration),
        });
      } catch {
        // Android mais antigo / navegador sem suporte — ignora.
      }
    };

    const onTimeUpdate = () => {
      setState(prev => ({ ...prev, currentTime: audio.currentTime }));
      atualizarPositionState();
    };
    const onLoadedMetadata = () => {
      setState(prev => ({ ...prev, duration: audio.duration || 0 }));
      atualizarPositionState();
    };
    const onEnded = () => {
      diag("evento nativo 'ended' disparou no <audio>");
      void handleEnded();
    };

    audio.addEventListener("timeupdate", onTimeUpdate);
    audio.addEventListener("loadedmetadata", onLoadedMetadata);
    audio.addEventListener("ended", onEnded);

    return () => {
      audio.removeEventListener("timeupdate", onTimeUpdate);
      audio.removeEventListener("loadedmetadata", onLoadedMetadata);
      audio.removeEventListener("ended", onEnded);
    };
  }, [handleEnded]);

  // ── Media Session API ───────────────────────────────────────────────
  // Nunca tinha sido implementada. Sem isso, o navegador/SO não tem como
  // saber que existe uma reprodução de áudio "de propósito" acontecendo —
  // em mobile, isso é um dos motivos de o áudio não continuar avançando
  // pra próxima faixa com a tela apagada (o JS da aba fica sujeito a
  // suspensão agressiva de economia de energia sem esse sinal). Também dá
  // os controles de play/pause/anterior/próxima na tela de bloqueio.
  //
  // REFORÇADA (jul/2026): a primeira versão só tinha metadata + 4
  // handlers. O Android usa `setPositionState` (posição/duração
  // atualizadas) como parte de como valida se uma sessão de mídia é
  // "real" — sem isso, a notificação de mídia pode ficar incompleta e o
  // sistema tratar a aba com menos prioridade. Adicionados também os
  // handlers `stop` e `seekto`, e múltiplos tamanhos de artwork (o
  // Android é exigente com isso — uma imagem só, com tamanho declarado
  // errado, pode fazer a notificação de mídia inteira falhar).
  useEffect(() => {
    if (!("mediaSession" in navigator) || !state.currentItem) return;

    const capaUrl = assetUrl(state.currentItem.capa) || "";

    navigator.mediaSession.metadata = new MediaMetadata({
      title: state.currentItem.titulo,
      artist: state.currentItem.subtitulo ?? "SpotOnFide",
      album: "SpotOnFide",
      artwork: capaUrl
        ? [
            { src: capaUrl, sizes: "96x96", type: "image/jpeg" },
            { src: capaUrl, sizes: "192x192", type: "image/jpeg" },
            { src: capaUrl, sizes: "256x256", type: "image/jpeg" },
            { src: capaUrl, sizes: "384x384", type: "image/jpeg" },
            { src: capaUrl, sizes: "512x512", type: "image/jpeg" },
          ]
        : [],
    });

    navigator.mediaSession.setActionHandler("play", () => togglePlayPause());
    navigator.mediaSession.setActionHandler("pause", () => togglePlayPause());
    navigator.mediaSession.setActionHandler("previoustrack", () => playPrevious());
    navigator.mediaSession.setActionHandler("nexttrack", () => playNext());
    navigator.mediaSession.setActionHandler("stop", () => togglePlayPause());
    try {
      navigator.mediaSession.setActionHandler("seekto", details => {
        if (details.seekTime != null) seek(details.seekTime);
      });
    } catch {
      // nem todo navegador suporta "seekto" — não é crítico, ignora.
    }

    return () => {
      navigator.mediaSession.setActionHandler("play", null);
      navigator.mediaSession.setActionHandler("pause", null);
      navigator.mediaSession.setActionHandler("previoustrack", null);
      navigator.mediaSession.setActionHandler("nexttrack", null);
      navigator.mediaSession.setActionHandler("stop", null);
      try {
        navigator.mediaSession.setActionHandler("seekto", null);
      } catch {
        // idem
      }
    };
  }, [currentItemId, togglePlayPause, playPrevious, playNext, seek]);

  useEffect(() => {
    if (!("mediaSession" in navigator)) return;
    navigator.mediaSession.playbackState = state.isPlaying ? "playing" : "paused";
  }, [state.isPlaying]);

  // setPositionState — atualizado junto com o progresso real do áudio
  // (via evento nativo, não via re-render do React, pra não disparar
  // isso a cada frame). Ver o listener "timeupdate" mais abaixo.

  const value: PlayerContextValue = {
    ...state,
    audioRef,
    play,
    togglePlayPause,
    seek,
    setVolume,
    playNext,
    playPrevious,
    toggleRepeat,
    toggleShuffle,
  };

  return (
    <PlayerContext.Provider value={value}>
      {children}
      {/*
       * ── ELEMENTO DE ÁUDIO ────────────────────────────────────────────
       * CORREÇÃO (jul/2026, ADR-004): antes vivia dentro de Player.tsx,
       * duplicado em dois `return` mutuamente exclusivos (player
       * expandido mobile vs. recolhido/desktop) — cada troca de
       * `isExpanded` desmontava o <audio> de um lugar e criava um novo
       * do zero no outro, perdendo currentTime, estado de reprodução e a
       * vinculação com a Media Session (a peça que segura a reprodução
       * em segundo plano). Isso violava a própria regra do ADR-004 na
       * prática, mesmo citando ele no comentário de lá.
       *
       * Aqui dentro do Provider — que é montado uma única vez em App.tsx
       * e nunca desmonta — o <audio> tem uma posição fixa na árvore,
       * imune a qualquer estado de UI do Player.tsx.
       */}
      <audio ref={audioRef} />
    </PlayerContext.Provider>
  );
}

export function usePlayer(): PlayerContextValue {
  const ctx = useContext(PlayerContext);
  if (!ctx) {
    throw new Error("usePlayer deve ser usado dentro de um PlayerProvider");
  }
  return ctx;
}
