"use client";

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

import { ApiDaSala, ErroDaSala } from "@/lib/live/cliente/api";
import { QuadroDeAnotacoes, type RecadoDaCaneta } from "@/lib/live/anotacoes";
import { CanetaDaSala } from "@/lib/live/cliente/caneta";
import { ouvirSala, type CanalDaSala } from "@/lib/live/cliente/canal";
import { ConexaoDeVideo, type Desejo, type SituacaoDaConexao, type TrilhaRecebida } from "@/lib/live/cliente/conexao";
import type { EstadoPublico, MensagemDaSala, Oferta, PapelNaSala } from "@/lib/live/evento";

/*
 * O coração da tela da sala: o estado que o servidor manda, o bate-papo, as
 * reações, a conexão de vídeo e o sinal de vida. A tela só desenha.
 */

export type Entrada = {
  eu: { id: string; credencial: string; papel: PapelNaSala; nome: string; foto: string | null; podeFalar: boolean };
  sala: {
    id: string;
    titulo: string;
    comecaEm: string;
    duracaoMin: number;
    participacao: "mao" | "so_apresentador" | "livre";
    camerasAlunos: boolean;
    acesso: "paga" | "cadastro" | "aberta";
    produtoId: string | null;
    apresentador: string | null;
    gravar: boolean;
    gravacaoPara: "compradores" | "ninguem";
    gravacaoModo: "palco" | "sala";
    categoria: "venda" | "reuniao";
    codigo: string;
    slug: string;
  };
  canal: string;
  chavePublica: JsonWebKey;
  horaDoServidor: number;
  /* Quando a resposta da entrada chegou neste aparelho (para acertar o relógio). */
  recebidaEm: number;
  sfu: boolean;
  estado: EstadoPublico;
  mensagens: MensagemDaSala[];
  itens: Oferta[];
  vitrine: Oferta[];
};

export type Aviso = { texto: string; tom?: "bom" | "ruim" } | null;
export type Saida = { motivo: "outro_lugar" | "tirado" | "saiu"; texto: string } | null;

const ehEquipe = (papel: PapelNaSala) => papel === "apresentador" || papel === "convidado";

/* Quantas câmeras de quem participa cada um recebe (o resto aparece como "mais N pessoas"). */
const CARINHAS_PARA_ALUNO = 6;
const CARINHAS_PARA_EQUIPE = 12;
const CARINHAS_NA_REUNIAO = 16;

/* `compacta`: o mini player do liveshop recebe só quem apresenta (sem as carinhas). */
export function useSalaAoVivo(entrada: Entrada, opcoes: { compacta?: boolean } = {}) {
  const compacta = Boolean(opcoes.compacta);
  const api = useMemo(() => new ApiDaSala(entrada.sala.id, entrada.eu.credencial), [entrada]);
  /* Diferença entre o relógio do servidor e o deste aparelho, medida quando a entrada chegou. */
  const diferenca = entrada.horaDoServidor - entrada.recebidaEm;
  const equipe = ehEquipe(entrada.eu.papel);

  const [estado, setEstado] = useState<EstadoPublico>(entrada.estado);
  const [mensagens, setMensagens] = useState<MensagemDaSala[]>(entrada.mensagens);
  const [recebidas, setRecebidas] = useState<TrilhaRecebida[]>([]);
  const [situacao, setSituacao] = useState<SituacaoDaConexao>("parada");
  const [canalOk, setCanalOk] = useState(false);
  const [podeFalar, setPodeFalar] = useState(entrada.eu.podeFalar);
  const [aviso, setAviso] = useState<Aviso>(null);
  const [saida, setSaida] = useState<Saida>(null);
  const [reacoes, setReacoes] = useState<Array<{ id: number; emoji: string; x: number }>>([]);
  const [falando, setFalando] = useState<Set<string>>(new Set());

  const conexao = useRef<ConexaoDeVideo | null>(null);
  const canal = useRef<CanalDaSala | null>(null);
  const versao = useRef(entrada.estado.v);
  const ultimaMensagem = useRef<string | null>(entrada.mensagens[entrada.mensagens.length - 1]?.em ?? null);
  const canalOkRef = useRef(false);
  /* Os destaques da tela (caneta, marca-texto, laser): fora do React, a camada pinta sozinha. */
  const quadro = useRef(new QuadroDeAnotacoes());
  const filaDaCaneta = useRef<Promise<unknown>>(Promise.resolve());
  /* A licença da caneta só é pedida quando a pessoa usa (quem organiza ou quem está apresentando a tela). */
  const caneta = useMemo(() => new CanetaDaSala(api, diferenca), [api, diferenca]);
  const estadoRef = useRef(entrada.estado);

  /* O que volta assinado nas respostas sai na hora pelo canal (o servidor manda de novo por trás). */
  useEffect(() => api.ouvirAvisos((avisos) => canal.current?.repassar(avisos)), [api]);

  const aplicarEstado = useCallback((novo: EstadoPublico | null | undefined) => {
    if (!novo || novo.v < versao.current) return;
    versao.current = novo.v;
    estadoRef.current = novo;
    setEstado(novo);
  }, []);

  const juntarMensagens = useCallback((novas: MensagemDaSala[]) => {
    if (!novas.length) return;
    setMensagens((atuais) => {
      const vistas = new Set(atuais.map((m) => m.id));
      const juntas = [...atuais, ...novas.filter((m) => !vistas.has(m.id))];
      juntas.sort((a, b) => a.em.localeCompare(b.em));
      return juntas.slice(-300);
    });
    const ultima = novas.reduce((maior, m) => (m.em > maior ? m.em : maior), ultimaMensagem.current ?? "");
    ultimaMensagem.current = ultima || ultimaMensagem.current;
  }, []);

  const flutuar = useCallback((emoji: string) => {
    const id = Date.now() + Math.random();
    setReacoes((atuais) => [...atuais.slice(-24), { id, emoji, x: Math.round(Math.random() * 30) }]);
    setTimeout(() => setReacoes((atuais) => atuais.filter((r) => r.id !== id)), 2700);
  }, []);

  /* O canal de avisos. */
  useEffect(() => {
    let vivo = true;
    void ouvirSala({
      topico: entrada.canal,
      sala: entrada.sala.id,
      chavePublica: entrada.chavePublica,
      diferencaDoRelogio: diferenca,
      aoMudarConexao: (ok) => {
        canalOkRef.current = ok;
        if (vivo) setCanalOk(ok);
      },
      aoReagir: (emoji) => flutuar(emoji),
      /* Só vale o risco de quem organiza ou de quem está mostrando a tela agora. */
      aoRiscar: (recado, participante) => {
        const pessoa = estadoRef.current.palco.find((p) => p.id === participante);
        if (pessoa && (ehEquipe(pessoa.papel) || pessoa.trilhas.tela)) quadro.current.aplicar(recado);
      },
      aoEvento: (evento) => {
        if (evento.t === "estado") aplicarEstado(evento.estado);
        else if (evento.t === "msg") juntarMensagens([evento.msg]);
        else if (evento.t === "msg-fora") setMensagens((atuais) => atuais.filter((m) => m.id !== evento.id));
        else if (evento.t === "fala" && evento.participante === entrada.eu.id) {
          setPodeFalar(evento.pode);
          setAviso(
            evento.pode
              ? { texto: "A loja liberou a sua fala. Ligue o microfone quando quiser.", tom: "bom" }
              : { texto: "A sua fala foi encerrada pela loja. Obrigado por participar!" },
          );
        } else if (evento.t === "fora" && evento.participante === entrada.eu.id) {
          setSaida({ motivo: "tirado", texto: "Você saiu desta live (ela foi aberta em outro aparelho ou a loja tirou você da sala)." });
        }
      },
    })
      .then((c) => {
        if (vivo) canal.current = c;
        else c.parar();
      })
      .catch(() => setAviso({ texto: "O bate-papo ao vivo não conectou. A sala segue atualizando a cada poucos segundos.", tom: "ruim" }));
    return () => {
      vivo = false;
      canal.current?.parar();
      canal.current = null;
    };
  }, [entrada, diferenca, aplicarEstado, juntarMensagens, flutuar]);

  /* O sinal de vida (e o plano B do canal). */
  useEffect(() => {
    let vivo = true;
    const bater = async () => {
      try {
        const r = await api.pedir<{ estado: EstadoPublico | null; mensagens: MensagemDaSala[] | null; podeFalar: boolean }>("sinal", {
          semCanal: !canalOkRef.current,
          desde: ultimaMensagem.current,
        });
        if (!vivo) return;
        aplicarEstado(r.estado);
        if (r.mensagens) juntarMensagens(r.mensagens);
        setPodeFalar(r.podeFalar);
      } catch (erro) {
        if (!vivo) return;
        if (erro instanceof ErroDaSala && (erro.motivo === "outro_lugar" || erro.motivo === "bloqueado" || erro.status === 401)) {
          setSaida({ motivo: erro.motivo === "outro_lugar" ? "outro_lugar" : "tirado", texto: erro.message });
        }
      }
    };
    /* Quem apresenta no modo "só perguntas" precisa ver as perguntas logo. */
    const intervalo = equipe && estado.chat === "perguntas" ? 5000 : 15000;
    const relogio = setInterval(bater, intervalo);
    return () => {
      vivo = false;
      clearInterval(relogio);
    };
  }, [api, equipe, estado.chat, aplicarEstado, juntarMensagens]);

  /* O que receber, conforme o palco. */
  const desejos = useMemo<Desejo[]>(() => {
    const lista: Desejo[] = [];
    const aoVivo = estado.status === "ao_vivo";
    const limite = entrada.sala.categoria === "reuniao" ? CARINHAS_NA_REUNIAO : equipe ? CARINHAS_PARA_EQUIPE : CARINHAS_PARA_ALUNO;
    let carinhas = 0;
    const ordenado = [...estado.palco].sort((a, b) => Number(estado.falando.includes(b.id)) - Number(estado.falando.includes(a.id)));
    for (const p of ordenado) {
      if (p.id === entrada.eu.id) continue;
      const daEquipe = ehEquipe(p.papel);
      if (daEquipe && !aoVivo && !equipe) continue;
      for (const [tipo, nome] of Object.entries(p.trilhas)) {
        if (!nome) continue;
        /* No mini player, de quem não apresenta só o som (quem ganhou a fala). */
        if (compacta && !daEquipe && tipo !== "mic") continue;
        if (!daEquipe && tipo === "cam") {
          if (carinhas >= limite) continue;
          carinhas += 1;
        }
        lista.push({ participante: p.id, tipo: tipo as Desejo["tipo"], nome });
      }
    }
    return lista;
  }, [estado, equipe, entrada.eu.id, entrada.sala.categoria, compacta]);

  const desejosRef = useRef<Desejo[]>(desejos);
  useLayoutEffect(() => {
    desejosRef.current = desejos;
  });
  const chaveDosDesejos = desejos.map((d) => `${d.participante}:${d.tipo}:${d.nome}`).sort().join("|");
  useEffect(() => {
    void conexao.current?.sincronizar(desejosRef.current).catch(() => undefined);
  }, [chaveDosDesejos]);

  /* Sem as chaves da Cloudflare no servidor, a sala funciona sem vídeo: quem apresenta fica sabendo. */
  useEffect(() => {
    if (entrada.sfu || !equipe) return;
    const t = setTimeout(
      () => setAviso({ texto: "O vídeo ao vivo ainda não foi ligado neste servidor. O bate-papo e o resto da sala funcionam.", tom: "ruim" }),
      800,
    );
    return () => clearTimeout(t);
  }, [entrada.sfu, equipe]);

  /* A conexão de vídeo. */
  useEffect(() => {
    if (!entrada.sfu || saida) return;
    const c = new ConexaoDeVideo(api);
    c.aoMudarRecebidas = (lista) => setRecebidas(lista);
    c.aoMudarSituacao = (s) => setSituacao(s);
    conexao.current = c;
    void c.iniciar().catch((erro) => {
      setSituacao("falhou");
      setAviso({ texto: erro instanceof Error ? erro.message : "O vídeo não conectou.", tom: "ruim" });
    });
    void c.sincronizar(desejosRef.current).catch(() => undefined);
    return () => {
      c.encerrar();
      conexao.current = null;
    };
  }, [api, entrada.sfu, saida]);


  /*
   * Quem está falando agora (o contorno) e quem fica em destaque. O destaque
   * só troca quando outra pessoa fala por quase um segundo e quem estava em
   * destaque já parou há um tempo: nada de a tela pular a cada "hum".
   */
  const [emDestaque, setEmDestaque] = useState<string | null>(null);
  useEffect(() => {
    let candidato: { id: string; vezes: number } | null = null;
    let atual: string | null = null;
    let atualFalouEm = 0;
    const relogio = setInterval(async () => {
      const niveis = await conexao.current?.niveisDeAudio().catch(() => null);
      if (!niveis) return;
      const lista = Array.from(niveis.entries()).filter(([, nivel]) => nivel > 0.04);
      const agora = new Set(lista.map(([id]) => id));
      setFalando((antes) => (antes.size === agora.size && Array.from(agora).every((id) => antes.has(id)) ? antes : agora));
      const instante = Date.now();
      if (atual && agora.has(atual)) atualFalouEm = instante;
      const maisAlto = lista.sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
      if (!maisAlto || maisAlto === atual) {
        candidato = null;
        return;
      }
      candidato = candidato?.id === maisAlto ? { id: maisAlto, vezes: candidato.vezes + 1 } : { id: maisAlto, vezes: 1 };
      if (candidato.vezes >= 2 && (!atual || instante - atualFalouEm > 1500)) {
        atual = candidato.id;
        atualFalouEm = instante;
        candidato = null;
        setEmDestaque(atual);
      }
    }, 400);
    return () => clearInterval(relogio);
  }, []);

  /* Fechou a aba ou recarregou: avisa a sala na hora (ninguém tenta receber de quem já foi). */
  useEffect(() => {
    const saiu = () => {
      void api.pedir("sair", {}, { manter: true }).catch(() => undefined);
    };
    /* Voltou do cache do navegador (celular): a vaga já foi solta, entra de novo. */
    const voltou = (e: PageTransitionEvent) => {
      if (e.persisted) window.location.reload();
    };
    window.addEventListener("pagehide", saiu);
    window.addEventListener("pageshow", voltou);
    return () => {
      window.removeEventListener("pagehide", saiu);
      window.removeEventListener("pageshow", voltou);
    };
  }, [api]);

  /* Avisos somem sozinhos. */
  useEffect(() => {
    if (!aviso) return;
    const t = setTimeout(() => setAviso(null), 5200);
    return () => clearTimeout(t);
  }, [aviso]);

  /* A mensagem aparece na hora para quem mandou (apagadinha) e é trocada pela de verdade quando volta. */
  const mandarMensagem = useCallback(
    async (texto: string) => {
      const provisoria: MensagemDaSala = {
        id: `tmp-${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`,
        autor: entrada.eu.id,
        nome: entrada.eu.nome,
        papel: entrada.eu.papel,
        foto: entrada.eu.foto,
        texto,
        pergunta: false,
        em: new Date(Date.now() + diferenca).toISOString(),
        pendente: true,
      };
      setMensagens((atuais) => [...atuais, provisoria].slice(-300));
      try {
        const r = await api.pedir<{ msg: MensagemDaSala }>("mensagem", { texto });
        setMensagens((atuais) => atuais.filter((m) => m.id !== provisoria.id));
        juntarMensagens([r.msg]);
      } catch (erro) {
        setMensagens((atuais) => atuais.filter((m) => m.id !== provisoria.id));
        throw erro;
      }
    },
    [api, juntarMensagens, entrada.eu, diferenca],
  );

  /* Um destaque na tela: aparece aqui na hora e vai assinado para todos. */
  const riscar = useCallback(
    (recado: RecadoDaCaneta, aplicarAqui = true) => {
      if (aplicarAqui) quadro.current.aplicar(recado);
      /* Em fila: os pedaços do risco chegam na ordem em que foram feitos. */
      filaDaCaneta.current = filaDaCaneta.current
        .then(() => caneta.assinar(recado))
        .then((risco) => canal.current?.riscar(risco))
        .catch(() => undefined);
    },
    [caneta],
  );

  const levantarMao = useCallback(
    async (levantar: boolean) => {
      const r = await api.pedir<{ estado?: EstadoPublico }>("mao", { levantar });
      aplicarEstado(r.estado);
    },
    [api, aplicarEstado],
  );

  const moderar = useCallback(
    async (acao: string, dados: Record<string, unknown> = {}) => {
      const r = await api.pedir<{ estado?: EstadoPublico }>("moderar", { acao, ...dados });
      aplicarEstado(r.estado);
      return r;
    },
    [api, aplicarEstado],
  );

  const reagir = useCallback(
    (emoji: string) => {
      flutuar(emoji);
      canal.current?.reagir(emoji);
    },
    [flutuar],
  );

  const sair = useCallback(() => {
    void api.pedir("sair", {}, { manter: true }).catch(() => undefined);
    conexao.current?.encerrar();
    setSaida({ motivo: "saiu", texto: "Você saiu da live." });
  }, [api]);

  return {
    api,
    conexao,
    equipe,
    estado,
    mensagens,
    recebidas,
    situacao,
    canalOk,
    podeFalar,
    aviso,
    setAviso,
    saida,
    reacoes,
    falando,
    emDestaque,
    diferenca,
    mandarMensagem,
    quadro,
    caneta,
    riscar,
    levantarMao,
    moderar,
    reagir,
    sair,
  };
}

export type SalaAoVivo = ReturnType<typeof useSalaAoVivo>;
