"use client";

import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
  ArrowSquareOut,
  Broadcast,
  ChatCircleText,
  Check,
  Copy,
  CornersOut,
  DotsThreeVertical,
  GearSix,
  HandWaving,
  Lightning,
  Microphone,
  MicrophoneSlash,
  MonitorArrowUp,
  PencilSimple,
  PhoneDisconnect,
  Record,
  SignOut,
  Smiley,
  Sparkle,
  ShoppingBag,
  Ticket,
  Users,
  VideoCamera,
  VideoCameraSlash,
  WifiSlash,
} from "@phosphor-icons/react";

import { entrarNaSala, ErroDaSala } from "@/lib/live/cliente/api";
import { REACOES } from "@/lib/live/cliente/canal";
import { erroParaATela } from "@/lib/live/cliente/conexao";
import { contaConferida, enderecoDoPainel, perguntarAoPainel, type ContaDoKtalogu } from "@/lib/live/cliente/identidade";
import { corDoNome, GravadorDaLive, gravacaoPossivel, type FonteDoQuadro } from "@/lib/live/cliente/gravador";
import { CameraComEfeitos } from "@/lib/live/cliente/efeitos/camera";
import { lerEfeitos, salvarEfeitos, SEM_EFEITOS, temEfeito, type Efeitos } from "@/lib/live/cliente/efeitos/preferencias";
import type { Oferta, TipoDeTrilha } from "@/lib/live/evento";
import { iniciais } from "@/lib/live/regras";

import CamadaDeDestaques, { BarraDeDestaques, type EscolhaDaCaneta } from "./CamadaDeDestaques";
import LadoDaLive, { type Aba } from "./LadoDaLive";
import PainelDeEfeitos, { useEstadoDosEfeitos } from "./PainelDeEfeitos";
import { enderecoNaLive, LojaPorTras, useLojaNaLive } from "./LojaNaLive";
import { useSalaAoVivo, type Entrada, type SalaAoVivo } from "./useSalaAoVivo";
import VideoDaTrilha, { SomDaTrilha } from "./VideoDaTrilha";

import "./live.css";

export type AcessoInicial = "apresentador" | "convidado" | "aluno" | "equipe" | "aberta" | "comprar" | "fechado";

export type PropsDaSala = {
  salaId: string;
  titulo: string;
  comecaEm: string;
  esperaMin: number;
  duracaoMin: number;
  acesso: "paga" | "cadastro" | "aberta";
  categoria: "venda" | "reuniao";
  participacao: "mao" | "so_apresentador" | "livre";
  loja: { nome: string; logo: string | null };
  capa: string | null;
  apresentador: { nome: string | null; foto: string | null };
  chaveDoApresentador: string | null;
  convite: string | null;
  acessoInicial: AcessoInicial;
  motivo: string | null;
  linkDoProduto: string | null;
  linkDeVolta: string;
  gravacaoParaQuemComprou: boolean;
  /* A hora do servidor quando montou a página (o relógio começa dela). */
  agora: number;
};

function quandoPorExtenso(iso: string) {
  try {
    return new Intl.DateTimeFormat("pt-BR", {
      weekday: "long",
      day: "2-digit",
      month: "long",
      hour: "2-digit",
      minute: "2-digit",
      timeZone: "America/Sao_Paulo",
    }).format(new Date(iso));
  } catch {
    return iso;
  }
}

function horaCurta(ms: number) {
  try {
    return new Intl.DateTimeFormat("pt-BR", { hour: "2-digit", minute: "2-digit", timeZone: "America/Sao_Paulo" }).format(new Date(ms));
  } catch {
    return "";
  }
}

function partesDoTempo(ms: number) {
  const total = Math.max(0, Math.floor(ms / 1000));
  return { dias: Math.floor(total / 86400), horas: Math.floor((total % 86400) / 3600), min: Math.floor((total % 3600) / 60), seg: total % 60 };
}

/* `agoraInicial`: a hora do servidor na primeira pintura (a mesma no servidor e no navegador). */
function Relogio({ ate, agoraInicial }: { ate: number; agoraInicial: number }) {
  const [agora, setAgora] = useState(agoraInicial);
  useEffect(() => {
    const t = setInterval(() => setAgora(Date.now()), 1000);
    return () => clearInterval(t);
  }, []);
  const p = partesDoTempo(ate - agora);
  const dois = (n: number) => String(n).padStart(2, "0");
  return (
    <div className="lv-relogio" aria-label="Contagem para a live">
      {p.dias > 0 ? (
        <div>
          <b>{dois(p.dias)}</b>
          <span>dias</span>
        </div>
      ) : null}
      <div>
        <b>{dois(p.horas)}</b>
        <span>horas</span>
      </div>
      <div>
        <b>{dois(p.min)}</b>
        <span>min</span>
      </div>
      <div>
        <b>{dois(p.seg)}</b>
        <span>seg</span>
      </div>
    </div>
  );
}

/* O nível do microfone (as barrinhas da prévia e o anel de "falando" da própria carinha). */
function useNivelDoMicrofone(track: MediaStreamTrack | null) {
  const [nivel, setNivel] = useState(0);
  useEffect(() => {
    if (!track) return;
    const contexto = new AudioContext();
    const fonte = contexto.createMediaStreamSource(new MediaStream([track]));
    const analise = contexto.createAnalyser();
    analise.fftSize = 512;
    fonte.connect(analise);
    const dados = new Uint8Array(analise.frequencyBinCount);
    const relogio = setInterval(() => {
      analise.getByteTimeDomainData(dados);
      let maior = 0;
      for (const v of dados) maior = Math.max(maior, Math.abs(v - 128));
      setNivel(Math.min(1, maior / 64));
    }, 120);
    return () => {
      clearInterval(relogio);
      void contexto.close().catch(() => undefined);
    };
  }, [track]);
  return track ? nivel : 0;
}

function Nivel({ valor }: { valor: number }) {
  return (
    <span className="lv-nivel" aria-hidden>
      {[0.15, 0.3, 0.5, 0.7, 0.9].map((corte, i) => (
        <i key={i} style={{ height: valor >= corte ? `${6 + i * 3}px` : "4px", opacity: valor >= corte ? 1 : 0.35 }} />
      ))}
    </span>
  );
}

const CHAVE_DA_CREDENCIAL = (salaId: string) => `ktalogu-live:${salaId}`;

function lerGuardado(salaId: string): string | null {
  try {
    return sessionStorage.getItem(CHAVE_DA_CREDENCIAL(salaId));
  } catch {
    return null;
  }
}

function guardar(salaId: string, credencial: string | null) {
  try {
    if (credencial) sessionStorage.setItem(CHAVE_DA_CREDENCIAL(salaId), credencial);
    else sessionStorage.removeItem(CHAVE_DA_CREDENCIAL(salaId));
  } catch {
    /* navegador sem armazenamento: só não lembra */
  }
}

/* Copiar com o plano B do navegador antigo. */
async function copiar(texto: string): Promise<boolean> {
  try {
    await navigator.clipboard.writeText(texto);
    return true;
  } catch {
    const campo = document.createElement("textarea");
    campo.value = texto;
    campo.style.position = "fixed";
    campo.style.opacity = "0";
    document.body.appendChild(campo);
    campo.select();
    const ok = document.execCommand("copy");
    campo.remove();
    return ok;
  }
}

/* ======================================================================= */
/*                              A ENTRADA                                    */
/* ======================================================================= */

/* O que vem ligado da entrada: a câmera (com os efeitos já aplicados) e o microfone. */
type Inicial = { cam: MediaStreamTrack | null; mic: MediaStreamTrack | null; camera: CameraComEfeitos | null };

/* Os efeitos da câmera escolhidos (fundo e filtro): os mesmos na prévia e na sala, salvos neste aparelho. */
function useEfeitosEscolhidos() {
  const [efeitos, setEfeitos] = useState<Efeitos>(SEM_EFEITOS);
  /* Depois de montar: o servidor não tem o armazenamento. */
  useEffect(() => {
    const t = setTimeout(() => setEfeitos(lerEfeitos()), 0);
    return () => clearTimeout(t);
  }, []);
  const mudar = useCallback((e: Efeitos) => {
    setEfeitos(e);
    salvarEfeitos(e);
  }, []);
  return [efeitos, mudar] as const;
}

export default function SalaDaLive(props: PropsDaSala) {
  const [entrada, setEntrada] = useState<Entrada | null>(null);
  const [inicial, setInicial] = useState<Inicial>({ cam: null, mic: null, camera: null });
  const [efeitos, mudarEfeitos] = useEfeitosEscolhidos();

  if (entrada) return <Sala props={props} entrada={entrada} inicial={inicial} efeitos={efeitos} aoMudarEfeitos={mudarEfeitos} />;
  return (
    <Antesala
      props={props}
      efeitos={efeitos}
      aoMudarEfeitos={mudarEfeitos}
      aoEntrar={(e, trilhas) => {
        setInicial(trilhas);
        setEntrada(e);
      }}
    />
  );
}

/* O erro do aparelho (câmera, microfone) em português, com o que fazer. */
function mensagemDoAparelho(erro: unknown, oQue: "a câmera" | "o microfone" | "a câmera e o microfone"): string {
  const nome = erro instanceof DOMException ? erro.name : "";
  if (nome === "NotAllowedError" || nome === "SecurityError") return `Libere ${oQue} no navegador: clique no cadeado ao lado do endereço e permita.`;
  if (nome === "NotReadableError" || nome === "AbortError" || nome === "TrackStartError")
    return `${oQue === "o microfone" ? "O microfone" : "A câmera"} está sendo usad${oQue === "o microfone" ? "o" : "a"} por outro programa ou outra aba (Meet, Zoom, WhatsApp, outra janela desta sala). Feche lá e tente de novo.`;
  if (nome === "NotFoundError" || nome === "OverconstrainedError") return `Não encontramos ${oQue} neste aparelho.`;
  return `Não deu para abrir ${oQue}. Tente de novo.`;
}

/* Abre câmera e microfone; se um falhar, tenta só o outro (câmera ocupada não tira o microfone). */
async function abrirAparelhos(): Promise<{ cam: MediaStreamTrack | null; mic: MediaStreamTrack | null; erro: string | null }> {
  const video = { width: { ideal: 1280 }, height: { ideal: 720 } };
  const audio = { echoCancellation: true, noiseSuppression: true, autoGainControl: true };
  try {
    const s = await navigator.mediaDevices.getUserMedia({ video, audio });
    return { cam: s.getVideoTracks()[0] ?? null, mic: s.getAudioTracks()[0] ?? null, erro: null };
  } catch (erroDosDois) {
    const [soMic, soCam] = await Promise.allSettled([navigator.mediaDevices.getUserMedia({ audio }), navigator.mediaDevices.getUserMedia({ video })]);
    const mic = soMic.status === "fulfilled" ? (soMic.value.getAudioTracks()[0] ?? null) : null;
    const cam = soCam.status === "fulfilled" ? (soCam.value.getVideoTracks()[0] ?? null) : null;
    const erro = !cam && !mic ? mensagemDoAparelho(erroDosDois, "a câmera e o microfone") : !cam ? mensagemDoAparelho(soCam.status === "rejected" ? soCam.reason : null, "a câmera") : !mic ? mensagemDoAparelho(soMic.status === "rejected" ? soMic.reason : null, "o microfone") : null;
    return { cam, mic, erro };
  }
}

function Antesala({
  props,
  efeitos,
  aoMudarEfeitos,
  aoEntrar,
}: {
  props: PropsDaSala;
  efeitos: Efeitos;
  aoMudarEfeitos: (e: Efeitos) => void;
  aoEntrar: (entrada: Entrada, trilhas: Inicial) => void;
}) {
  const reuniao = props.categoria === "reuniao";
  /* A conta do Ktalogu de quem entra (o painel diz quem é): "conferindo" enquanto pergunta. */
  const [conta, setConta] = useState<ContaDoKtalogu | "conferindo" | null>(null);
  const contaPronta = conta && conta !== "conferindo" ? conta : null;
  const organiza = props.acessoInicial === "apresentador" || Boolean(contaPronta?.dono);
  const daEquipe = organiza || props.acessoInicial === "convidado";
  const fechadoParaMim = !organiza && (props.acessoInicial === "comprar" || props.acessoInicial === "fechado");
  /* Na reunião todo mundo fala: todo mundo vê a própria câmera antes de entrar, como no Meet. */
  const comPrevia = daEquipe || (props.participacao === "livre" && !fechadoParaMim);
  const precisaDeNome = props.acessoInicial === "aberta" || props.acessoInicial === "convidado" || Boolean(contaPronta);
  const [nome, setNome] = useState("");
  const [erro, setErro] = useState<string | null>(null);
  const [entrando, setEntrando] = useState(false);
  /* A câmera com os efeitos; `cam` é a trilha que aparece na prévia (crua ou já com efeito). */
  const [camera, setCamera] = useState<CameraComEfeitos | null>(null);
  const [cam, setCam] = useState<MediaStreamTrack | null>(null);
  const [mic, setMic] = useState<MediaStreamTrack | null>(null);
  const [verEfeitos, setVerEfeitos] = useState(false);
  const estadoDosEfeitos = useEstadoDosEfeitos(camera);
  const [semAparelho, setSemAparelho] = useState<string | null>(null);
  const [camLigada, setCamLigada] = useState(true);
  /* Só no navegador dá para saber se ele grava (no servidor não há WebCodecs). */
  const [podeGravar, setPodeGravar] = useState(true);
  useEffect(() => {
    const t = setTimeout(() => setPodeGravar(gravacaoPossivel()), 0);
    return () => clearTimeout(t);
  }, []);
  const [micLigado, setMicLigado] = useState(true);
  const [agora, setAgora] = useState(props.agora);
  const nivel = useNivelDoMicrofone(mic);
  const trilhasRef = useRef<{ camera: CameraComEfeitos | null; mic: MediaStreamTrack | null }>({ camera: null, mic: null });
  const entrou = useRef(false);

  useEffect(() => {
    const t = setInterval(() => setAgora(Date.now()), 1000);
    return () => clearInterval(t);
  }, []);

  /* Quem é: o link de quem apresenta já diz; senão, pergunta ao painel do Ktalogu (uma vez por sala nesta aba). */
  useEffect(() => {
    if (props.acessoInicial === "apresentador") return;
    let vivo = true;
    /* Depois de montar (o servidor não tem o endereço com # nem o armazenamento da aba); sem relógio, que a aba de fundo segura. */
    void Promise.resolve().then(() => {
      if (!vivo) return;
      const conferida = contaConferida(props.salaId);
      if (conferida !== undefined) {
        setConta(conferida === "sem" ? null : conferida);
        if (conferida !== "sem") setNome((atual) => atual || conferida.nome);
        return;
      }
      setConta("conferindo");
      void perguntarAoPainel(props.salaId).then((foi) => {
        if (vivo && !foi) setConta(null);
      });
    });
    return () => {
      vivo = false;
    };
  }, [props.salaId, props.acessoInicial]);

  /* O nome que a pessoa usou da última vez (depois de montar: o servidor não tem o armazenamento). */
  useEffect(() => {
    const t = setTimeout(() => {
      if (props.acessoInicial === "fechado" && props.motivo) setErro(props.motivo);
      try {
        const salvo = localStorage.getItem("ktalogu-live:nome");
        if (salvo) setNome((atual) => atual || salvo);
      } catch {
        /* sem armazenamento */
      }
    }, 0);
    return () => clearTimeout(t);
  }, [props.acessoInicial, props.motivo]);

  /* A câmera e o microfone já aparecem antes de entrar. */
  useEffect(() => {
    if (!comPrevia) return;
    let vivo = true;
    if (!navigator.mediaDevices?.getUserMedia) {
      const t = setTimeout(() => setSemAparelho("Este navegador não abre câmera nem microfone. Use o Chrome, o Edge ou o Safari atualizado."), 0);
      return () => clearTimeout(t);
    }
    void abrirAparelhos().then(({ cam: v, mic: a, erro: problema }) => {
      if (!vivo) {
        v?.stop();
        a?.stop();
        return;
      }
      const c = v ? new CameraComEfeitos(v) : null;
      /* Ligou ou desligou um efeito: a prévia passa a mostrar a trilha nova. */
      if (c) c.aoTrocarSaida = (nova) => setCam(nova);
      trilhasRef.current = { camera: c, mic: a };
      setCamera(c);
      setCam(v);
      setMic(a);
      setSemAparelho(problema);
    });
    return () => {
      vivo = false;
      if (!entrou.current) {
        trilhasRef.current.camera?.encerrar();
        trilhasRef.current.mic?.stop();
      }
    };
  }, [comPrevia]);

  /* Os efeitos escolhidos valem já na prévia (e o que ficou salvo da última vez também). */
  useEffect(() => {
    if (camera) void camera.aplicar(efeitos);
  }, [camera, efeitos]);

  const comeca = new Date(props.comecaEm).getTime();
  const abre = comeca - props.esperaMin * 60000;
  const cedo = !daEquipe && !reuniao && agora < abre;

  /* Desligar na prévia só silencia a trilha (pelo ref: a trilha é do navegador, não do React). */
  const alternarCam = () => {
    const c = trilhasRef.current.camera;
    if (!c) return;
    c.definirLigada(!camLigada);
    setCamLigada(!camLigada);
  };
  const alternarMic = () => {
    const trilha = trilhasRef.current.mic;
    if (!trilha) return;
    trilha.enabled = !micLigado;
    setMicLigado(!micLigado);
  };

  const entrar = async () => {
    if (precisaDeNome && nome.trim().length < 2) {
      setErro("Diga o seu nome para entrar.");
      return;
    }
    setEntrando(true);
    setErro(null);
    try {
      if (precisaDeNome && !contaPronta) {
        try {
          localStorage.setItem("ktalogu-live:nome", nome.trim());
        } catch {
          /* sem armazenamento */
        }
      }
      const json = await entrarNaSala(props.salaId, {
        anterior: lerGuardado(props.salaId),
        apresentar: props.chaveDoApresentador,
        convite: props.convite,
        identidade: contaPronta?.token ?? null,
        nome: nome.trim(),
      });
      const e = json as unknown as Entrada;
      guardar(props.salaId, e.eu.credencial);
      entrou.current = true;
      const { camera: aCamera, mic: trilhaDoMic } = trilhasRef.current;
      const comCamera = aCamera && camLigada ? aCamera : null;
      const usar: Inicial = { cam: comCamera ? comCamera.saida : null, mic: trilhaDoMic && micLigado ? trilhaDoMic : null, camera: comCamera };
      if (aCamera && !camLigada) aCamera.encerrar();
      if (trilhaDoMic && !micLigado) trilhaDoMic.stop();
      aoEntrar(e, usar);
    } catch (erroDaEntrada) {
      if (erroDaEntrada instanceof ErroDaSala && erroDaEntrada.status === 401) guardar(props.salaId, null);
      setErro(erroDaEntrada instanceof ErroDaSala ? erroDaEntrada.message : "Não foi possível entrar agora. Confira a internet e tente de novo.");
      setEntrando(false);
    }
  };

  const nomeNaPrevia = nome.trim() || (props.acessoInicial === "apresentador" ? props.apresentador.nome || props.loja.nome : "Você");
  const fotoNaPrevia = contaPronta?.logo ?? (props.acessoInicial === "apresentador" ? props.apresentador.foto || props.loja.logo : null);

  return (
    <main className="lv-entrada">
      <div className="lv-entrada-col">
      <div className="lv-previa">
        {comPrevia && cam ? (
          camLigada ? (
            <VideoDaTrilha track={cam} espelho />
          ) : (
            <div className="lv-previa-txt">
              <div>
                <Avatar nome={nomeNaPrevia} foto={fotoNaPrevia} className="lv-avatar-grande" />
                <p>A câmera está desligada</p>
              </div>
            </div>
          )
        ) : (
          <>
            {props.capa && !comPrevia ? <span className="lv-capa" style={{ backgroundImage: `url("${props.capa}")` }} /> : null}
            <div className="lv-previa-txt">
              <div>
                {comPrevia ? <Avatar nome={nomeNaPrevia} foto={fotoNaPrevia} className="lv-avatar-grande" /> : null}
                <p>{comPrevia ? semAparelho || "Abrindo a câmera…" : "Você entra com a câmera e o microfone desligados."}</p>
              </div>
            </div>
          </>
        )}
        {comPrevia && cam && semAparelho ? <span className="lv-previa-aviso">{semAparelho}</span> : null}
        {comPrevia && (cam || mic) ? (
          <>
            <span className="lv-previa-nome">{nomeNaPrevia}</span>
            <div className="lv-previa-ctl">
              <button type="button" className={`lv-ctl redondo ${mic && micLigado ? "" : "desligado"}`} onClick={alternarMic} disabled={!mic} aria-label={micLigado ? "Desligar o microfone" : "Ligar o microfone"}>
                {mic && micLigado ? <Microphone /> : <MicrophoneSlash />}
              </button>
              <button type="button" className={`lv-ctl redondo ${cam && camLigada ? "" : "desligado"}`} onClick={alternarCam} disabled={!cam} aria-label={camLigada ? "Desligar a câmera" : "Ligar a câmera"}>
                {cam && camLigada ? <VideoCamera /> : <VideoCameraSlash />}
              </button>
            </div>
            {cam ? (
              <button
                type="button"
                className={`lv-ctl redondo lv-previa-efeitos${verEfeitos ? " ativo" : ""}${temEfeito(efeitos) ? " com-efeito" : ""}`}
                onClick={() => setVerEfeitos((v) => !v)}
                aria-label="Efeitos da câmera: fundo desfocado e filtros"
                aria-expanded={verEfeitos}
                title="Efeitos: fundo desfocado e filtros"
              >
                <Sparkle weight={temEfeito(efeitos) ? "fill" : "regular"} />
              </button>
            ) : null}
          </>
        ) : null}
      </div>
      {verEfeitos && cam ? (
        <PainelDeEfeitos variante="entrada" efeitos={efeitos} aoMudar={aoMudarEfeitos} estado={estadoDosEfeitos} aoFechar={() => setVerEfeitos(false)} />
      ) : null}
      </div>

      <section className="lv-entrada-texto">
        <h1>{daEquipe || reuniao ? "Pronto para participar?" : props.titulo}</h1>
        <p className="lv-quando">
          {daEquipe || reuniao ? `${props.titulo} · ` : ""}
          {reuniao ? props.loja.nome : `${quandoPorExtenso(props.comecaEm)} · ${props.loja.nome}`}
        </p>

        {contaPronta ? (
          <p className="lv-conta">
            <Avatar nome={contaPronta.nome} foto={contaPronta.logo} className="lv-conta-av" />
            <span>
              {contaPronta.dono ? (reuniao ? "Você organiza esta reunião" : "Você apresenta esta live") : "Entrando com a sua conta Ktalogu"}
              <small>{contaPronta.nome}</small>
            </span>
          </p>
        ) : null}

        {cedo ? (
          <>
            <p className="lv-dica">A sala de espera abre em</p>
            <Relogio ate={abre} agoraInicial={props.agora} />
            <p className="lv-dica">Deixe esta página aberta: o botão de entrar aparece sozinho.</p>
          </>
        ) : null}

        {conta === "conferindo" ? (
          <div className="lv-form">
            <button type="button" className="lv-botao" disabled>
              Conferindo a sua conta…
            </button>
          </div>
        ) : fechadoParaMim && props.acessoInicial === "comprar" ? (
          <div className="lv-form">
            <p className="lv-dica">{props.acesso === "cadastro" ? "Esta live é grátis: garanta a sua vaga com nome e e-mail." : "Garanta a sua vaga para entrar na live."}</p>
            {props.linkDoProduto ? (
              <a className="lv-botao" href={props.linkDoProduto}>
                <Ticket size={18} />
                {props.acesso === "cadastro" ? "Garantir vaga grátis" : "Garantir minha vaga"}
              </a>
            ) : null}
            <p className="lv-dica">Já garantiu? Entre pela área de membros com o seu e-mail, ou abra o link que chegou no e-mail.</p>
            <a className="lv-botao leve" href={props.linkDeVolta}>
              Ir para a área de membros
            </a>
          </div>
        ) : fechadoParaMim ? null : (
          <div className="lv-form">
            {precisaDeNome ? (
              <input
                className="lv-campo"
                placeholder="Seu nome"
                value={nome}
                maxLength={60}
                autoComplete="name"
                aria-label="Seu nome na sala"
                onChange={(e) => setNome(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === "Enter" && !cedo) void entrar();
                }}
              />
            ) : null}
            <button type="button" className="lv-botao" disabled={entrando || cedo} onClick={() => void entrar()}>
              {entrando ? "Entrando…" : cedo ? "A sala ainda não abriu" : "Participar agora"}
            </button>
            {comPrevia && mic ? (
              <p className="lv-dica lv-dica-nivel">
                <Nivel valor={micLigado ? nivel : 0} /> {micLigado ? "Fale algo: as barrinhas mostram o seu microfone." : "Microfone desligado."}
              </p>
            ) : null}
            {organiza && !reuniao ? (
              <p className="lv-dica">
                Você entra primeiro nos bastidores: quem assiste só vê você quando clicar em <b>Entrar ao vivo</b>.
                {podeGravar ? "" : " Para gravar, use o Chrome ou o Edge no computador."}
              </p>
            ) : null}
            {organiza && reuniao ? <p className="lv-dica">A reunião começa quando você entrar. Quem chegou antes está esperando.</p> : null}
            {contaPronta && !contaPronta.dono ? <p className="lv-dica">O nome aparece para todos na sala. Dá para trocar antes de entrar.</p> : null}
          </div>
        )}
        {erro ? <div className="lv-erro">{erro}</div> : null}
      </section>
    </main>
  );
}

/* A foto da pessoa, a logo da empresa ou as iniciais. A logo aparece inteira, num círculo branco. */
function Avatar({ nome, foto, className }: { nome: string; foto: string | null; className: string }) {
  if (foto) return <span className={`${className} lv-av-logo`} style={{ backgroundImage: `url("${foto}")` }} aria-hidden />;
  return (
    <span className={className} style={{ background: corDoNome(nome) }} aria-hidden>
      {iniciais(nome.replace(/ \(você\)$/, ""))}
    </span>
  );
}

/* ======================================================================= */
/*                                A SALA                                     */
/* ======================================================================= */

type Locais = { cam: MediaStreamTrack | null; mic: MediaStreamTrack | null; tela: MediaStreamTrack | null; telaAudio: MediaStreamTrack | null };

type Carinha = {
  id: string;
  nome: string;
  track: MediaStreamTrack | null;
  foto: string | null;
  mic: boolean;
  mao: boolean;
  eu: boolean;
  apresenta: boolean;
  falando: boolean;
};

type Menu = "reacoes" | "mais" | "sair" | "aparelhos" | "efeitos" | null;

function Sala({
  props,
  entrada,
  inicial,
  efeitos,
  aoMudarEfeitos,
}: {
  props: PropsDaSala;
  entrada: Entrada;
  inicial: Inicial;
  efeitos: Efeitos;
  aoMudarEfeitos: (e: Efeitos) => void;
}) {
  const s = useSalaAoVivo(entrada);
  const { estado, equipe, conexao } = s;
  const eu = entrada.eu;
  const apresentador = eu.papel === "apresentador";
  const reuniao = entrada.sala.categoria === "reuniao";
  const livre = entrada.sala.participacao === "livre";
  const [locais, setLocais] = useState<Locais>({ cam: null, mic: null, tela: null, telaAudio: null });
  const locaisRef = useRef(locais);
  useLayoutEffect(() => {
    locaisRef.current = locais;
  });
  const [menu, setMenu] = useState<Menu>(null);
  /*
   * A minha câmera com os efeitos. `locais.cam` é a trilha que sai dela (crua
   * ou com efeito); a câmera de verdade fica guardada aqui dentro.
   */
  const [camera, setCamera] = useState<CameraComEfeitos | null>(inicial.camera);
  const cameraRef = useRef<CameraComEfeitos | null>(inicial.camera);
  /* Na reunião, antes de quem organiza chegar, a câmera da entrada aparece só para mim. */
  const [camDaEntrada, setCamDaEntrada] = useState<MediaStreamTrack | null>(inicial.cam);
  /* A câmera que acabei de ligar, enquanto ela chega à sala (aparece para mim na hora). */
  const [camLigando, setCamLigando] = useState<MediaStreamTrack | null>(null);
  const efeitosRef = useRef(efeitos);
  useLayoutEffect(() => {
    efeitosRef.current = efeitos;
  });
  const estadoDosEfeitos = useEstadoDosEfeitos(camera);
  const [aba, setAba] = useState<Aba | null>(() => (!reuniao && window.innerWidth >= 1180 ? "chat" : null));
  const [confirmarFim, setConfirmarFim] = useState(false);
  const [ocupado, setOcupado] = useState<string | null>(null);
  const [gravacao, setGravacao] = useState<"parada" | "gravando" | "salvando" | "salva" | "falhou">("parada");
  const [modoDaGravacao, setModoDaGravacao] = useState<"palco" | "sala">(entrada.sala.gravacaoModo);
  const [gravar, setGravar] = useState(entrada.sala.gravar);
  const [caneta, setCaneta] = useState<EscolhaDaCaneta | null>(null);
  const [aparelhos, setAparelhos] = useState<{ mic: string | null; cam: string | null }>({ mic: null, cam: null });
  const [lidas, setLidas] = useState(s.mensagens.length);
  const [linkCopiado, setLinkCopiado] = useState(false);
  const [relogio, setRelogio] = useState(() => Date.now());
  /* Alguém já apresenta e eu quero apresentar: pergunta antes de assumir. */
  const [assumirTela, setAssumirTela] = useState<string | null>(null);
  /* Acabaram as transmissões do mês: aviso fixo para quem organiza (não some sozinho). */
  const [semVaga, setSemVaga] = useState<string | null>(null);
  /* Quando cada coisa minha foi ligada (para não confundir "ainda não chegou ao palco" com "foi cortada"). */
  const ligadoEm = useRef(new Map<TipoDeTrilha, number>());
  const gravador = useRef<GravadorDaLive | null>(null);
  const palcoRef = useRef<HTMLVideoElement | null>(null);
  const salaRef = useRef<HTMLDivElement | null>(null);
  /* Comprar sem sair da live: a sala vira janelinha e a loja abre por trás (LojaNaLive.tsx). */
  const janela = useLojaNaLive(salaRef);
  const primeiraVez = useRef(true);
  const comecouSozinha = useRef(false);
  const aoVivo = estado.status === "ao_vivo";
  const encerrada = estado.status === "encerrada" || estado.status === "cancelada";
  const maoLevantada = estado.maos.some((m) => m.id === eu.id);
  const meuNivel = useNivelDoMicrofone(locais.mic);
  const posso = {
    cam: equipe || livre || entrada.sala.camerasAlunos || (entrada.sala.participacao === "mao" && s.podeFalar),
    mic: equipe || livre || (entrada.sala.participacao === "mao" && s.podeFalar),
    tela: equipe || livre,
    mao: !equipe && entrada.sala.participacao !== "so_apresentador",
  };
  /* Reunião antes de quem organiza chegar: ninguém manda nada ainda (a câmera da entrada fica guardada). */
  const esperandoOrganizador = reuniao && !equipe && !aoVivo && !encerrada;
  const aviso = s.setAviso;

  useEffect(() => {
    const t = setInterval(() => setRelogio(Date.now()), 15000);
    return () => clearInterval(t);
  }, []);

  /* Bate-papo aberto: tudo lido. */
  const naoLidas = aba === "chat" ? 0 : Math.max(0, s.mensagens.filter((m) => !m.pendente).length - lidas);
  useEffect(() => {
    if (aba === "chat") setLidas(s.mensagens.filter((m) => !m.pendente).length);
  }, [aba, s.mensagens]);

  /* Clicar fora fecha o menu aberto. */
  useEffect(() => {
    if (!menu) return;
    const fechar = (e: PointerEvent) => {
      if (!(e.target as HTMLElement | null)?.closest?.("[data-menu]")) setMenu(null);
    };
    window.addEventListener("pointerdown", fechar);
    return () => window.removeEventListener("pointerdown", fechar);
  }, [menu]);

  /* Como cada coisa vai para a sala (a câmera de quem apresenta vai melhor). */
  const opcoesDeEnvio = useCallback(
    (tipo: TipoDeTrilha) =>
      tipo === "cam"
        ? equipe
          ? { maxBitrate: 1_200_000, dica: "motion" as const }
          : { maxBitrate: livre ? 600_000 : 300_000, maxFramerate: 20, dica: "motion" as const }
        : tipo === "tela"
          ? { maxBitrate: 2_500_000, maxFramerate: 15, dica: "detail" as const }
          : {},
    [equipe, livre],
  );

  /* Ligar e desligar o que eu mando. Devolve se deu certo. */
  const mandar = useCallback(
    async (tipo: TipoDeTrilha, track: MediaStreamTrack, chave: keyof Locais): Promise<boolean> => {
      const c = conexao.current;
      if (!c) {
        track.stop();
        aviso({ texto: "O vídeo ainda está conectando. Tente de novo em instantes.", tom: "ruim" });
        return false;
      }
      /* A câmera aparece na hora para mim, enquanto ela chega à sala. */
      const naHora = tipo === "cam" && !locaisRef.current.cam;
      if (naHora) setCamLigando(track);
      try {
        await c.publicar(tipo, track, opcoesDeEnvio(tipo));
        const antiga = locaisRef.current[chave];
        if (antiga && antiga !== track) antiga.stop();
        ligadoEm.current.set(tipo, Date.now());
        setLocais((l) => ({ ...l, [chave]: track }));
        return true;
      } catch (erro) {
        track.stop();
        aviso({ texto: erroParaATela(erro, "Não deu para ligar agora. Tente de novo em instantes."), tom: "ruim" });
        return false;
      } finally {
        if (naHora) setCamLigando((atual) => (atual === track ? null : atual));
      }
    },
    [conexao, aviso, opcoesDeEnvio],
  );

  /*
   * A câmera ligou ou desligou um efeito: a trilha que sai mudou. Se ela já
   * está na sala, troca no envio (replaceTrack, sem negociar de novo); senão,
   * só troca o que aparece para mim.
   */
  const acompanharCamera = useCallback(
    (c: CameraComEfeitos) => {
      c.aoTrocarSaida = async (nova, antiga) => {
        if (cameraRef.current !== c) return;
        if (locaisRef.current.cam === antiga) {
          const con = conexao.current;
          if (con?.estaMandando("cam")) await con.publicar("cam", nova, opcoesDeEnvio("cam")).catch(() => undefined);
          locaisRef.current = { ...locaisRef.current, cam: nova };
          setLocais((l) => (l.cam === antiga ? { ...l, cam: nova } : l));
        } else {
          setCamDaEntrada((atual) => (atual === antiga ? nova : atual));
        }
      };
    },
    [conexao, opcoesDeEnvio],
  );

  /* A câmera que veio da entrada passa a ser acompanhada pela sala. */
  useEffect(() => {
    if (inicial.camera) acompanharCamera(inicial.camera);
  }, [inicial.camera, acompanharCamera]);

  /* Os efeitos escolhidos (na entrada ou aqui dentro) valem na hora. */
  useEffect(() => {
    if (camera) void camera.aplicar(efeitos);
  }, [camera, efeitos]);

  const largarCamera = useCallback(() => {
    cameraRef.current?.encerrar();
    cameraRef.current = null;
    setCamera(null);
  }, []);

  const parar = useCallback(
    /* `deVez`: o servidor já fechou a trilha (senão câmera e microfone só pausam, e religar é na hora). */
    async (tipo: TipoDeTrilha, chave: keyof Locais, deVez = false) => {
      const track = locaisRef.current[chave];
      setLocais((l) => ({ ...l, [chave]: null }));
      track?.stop();
      if (chave === "cam") largarCamera();
      await conexao.current?.parar(tipo, { deVez }).catch(() => undefined);
    },
    [conexao, largarCamera],
  );

  const ligarMic = useCallback(
    async (dispositivo: string | null = aparelhos.mic) => {
      try {
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, ...(dispositivo ? { deviceId: { exact: dispositivo } } : {}) },
        });
        await mandar("mic", stream.getAudioTracks()[0], "mic");
      } catch (erro) {
        aviso({ texto: mensagemDoAparelho(erro, "o microfone"), tom: "ruim" });
      }
    },
    [mandar, aviso, aparelhos.mic],
  );

  const ligarCam = useCallback(
    async (dispositivo: string | null = aparelhos.cam) => {
      let bruta: MediaStreamTrack;
      try {
        const base = equipe ? { width: { ideal: 1280 }, height: { ideal: 720 } } : { width: { ideal: 640 }, height: { ideal: 360 }, frameRate: { ideal: 20 } };
        const stream = await navigator.mediaDevices.getUserMedia({ video: { ...base, ...(dispositivo ? { deviceId: { exact: dispositivo } } : {}) } });
        bruta = stream.getVideoTracks()[0];
      } catch (erro) {
        aviso({ texto: mensagemDoAparelho(erro, "a câmera"), tom: "ruim" });
        return;
      }
      /* A câmera nova já entra com os efeitos escolhidos; a antiga só desliga depois que a nova estiver na sala. */
      const nova = new CameraComEfeitos(bruta);
      await nova.aplicar(efeitosRef.current).catch(() => undefined);
      const antiga = cameraRef.current;
      cameraRef.current = nova;
      acompanharCamera(nova);
      const ok = await mandar("cam", nova.saida, "cam");
      if (ok) {
        if (antiga && antiga !== nova) antiga.encerrar();
        setCamera(nova);
      } else {
        nova.encerrar();
        cameraRef.current = antiga;
      }
    },
    [mandar, aviso, equipe, aparelhos.cam, acompanharCamera],
  );

  const pararTela = useCallback(async () => {
    setCaneta(null);
    await parar("tela", "tela");
    if (locaisRef.current.telaAudio) await parar("tela-audio", "telaAudio");
  }, [parar]);

  const mostrarTela = useCallback(async () => {
    setAssumirTela(null);
    try {
      const stream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: { ideal: 15, max: 24 }, cursor: "always" } as MediaTrackConstraints, audio: true });
      const video = stream.getVideoTracks()[0];
      const audio = stream.getAudioTracks()[0];
      video.addEventListener("ended", () => void pararTela());
      await mandar("tela", video, "tela");
      if (audio) await mandar("tela-audio", audio, "telaAudio");
      s.caneta.aquecer();
    } catch (erro) {
      /* Desistir de escolher a tela não é erro. */
      if (erro instanceof DOMException && erro.name === "NotAllowedError") return;
      aviso({ texto: "Não deu para apresentar a tela. Tente de novo.", tom: "ruim" });
    }
  }, [mandar, pararTela, s.caneta, aviso]);

  /* O que veio ligado da entrada (na reunião, só quando quem organiza chega). */
  useEffect(() => {
    if (!primeiraVez.current || s.situacao === "parada" || esperandoOrganizador) return;
    primeiraVez.current = false;
    const daEntrada = cameraRef.current && cameraRef.current === inicial.camera ? cameraRef.current.saida : null;
    if (daEntrada)
      void mandar("cam", daEntrada, "cam").then((ok) => {
        if (!ok && cameraRef.current === inicial.camera) largarCamera();
      });
    if (inicial.mic) void mandar("mic", inicial.mic, "mic");
  }, [s.situacao, inicial, mandar, esperandoOrganizador, largarCamera]);

  /* Perdeu a fala: desliga o microfone (e a câmera, se a live não deixa câmera de quem participa). */
  useEffect(() => {
    if (equipe || livre || s.podeFalar) return;
    if (locaisRef.current.mic) void parar("mic", "mic");
    if (locaisRef.current.cam && !entrada.sala.camerasAlunos) void parar("cam", "cam");
  }, [s.podeFalar, equipe, livre, parar, entrada.sala.camerasAlunos]);

  /*
   * O servidor fechou algo meu (quem organiza fechou o meu microfone, outra
   * pessoa começou a apresentar e assumiu a tela): a trilha sumiu do palco
   * num estado mais novo do que a hora em que liguei. Aí desliga aqui também.
   */
  const meuNoPalco = estado.palco.find((p) => p.id === eu.id);
  useEffect(() => {
    const versaoDoEstado = estado.v;
    const conferir: Array<[TipoDeTrilha, keyof Locais]> = [
      ["mic", "mic"],
      ["cam", "cam"],
      ["tela", "tela"],
    ];
    for (const [tipo, chave] of conferir) {
      if (!locaisRef.current[chave] || meuNoPalco?.trilhas[tipo] || !conexao.current?.estaMandando(tipo)) continue;
      const ligou = ligadoEm.current.get(tipo) ?? 0;
      /* O estado é de antes de a trilha chegar ao palco: ainda não dá para saber. */
      if (versaoDoEstado < ligou + s.diferenca + 1500) continue;
      if (tipo === "tela") {
        void pararTela();
        const outro = estado.palco.find((p) => p.id !== eu.id && p.trilhas.tela);
        aviso({ texto: outro ? `${outro.nome} começou a apresentar.` : "A sua apresentação foi encerrada." });
      } else {
        void parar(tipo, chave, true);
        if (tipo === "mic") aviso({ texto: "Quem organiza fechou o seu microfone." });
      }
    }
  }, [estado, meuNoPalco, parar, pararTela, aviso, conexao, eu.id, s.diferenca]);

  /* Saiu: desliga tudo. */
  useEffect(() => {
    if (!s.saida) return;
    for (const t of Object.values(locaisRef.current)) t?.stop();
    largarCamera();
    guardar(entrada.sala.id, null);
  }, [s.saida, entrada.sala.id, largarCamera]);

  /* Reunião: começa sozinha quando quem organizou entra (não tem bastidores, como no Meet). */
  useEffect(() => {
    if (!reuniao || !apresentador || comecouSozinha.current || estado.status !== "agendada" || s.situacao !== "conectada") return;
    comecouSozinha.current = true;
    void s.moderar("iniciar").catch((erro) => {
      if (erro instanceof ErroDaSala && erro.motivo === "sem_saldo") setSemVaga(erro.message);
      else aviso({ texto: erroParaATela(erro, "A reunião não começou. Tente de novo."), tom: "ruim" });
    });
  }, [reuniao, apresentador, estado.status, s.situacao, s, aviso]);

  /* ---------------------------------------------------------------- palco */

  const recebida = (participante: string, tipo: TipoDeTrilha) =>
    s.recebidas.find((r) => r.participante === participante && r.tipo === tipo)?.track ?? null;

  const daEquipe = (papel: string) => papel === "apresentador" || papel === "convidado";
  const quemMostraTela = estado.palco.find((p) => p.trilhas.tela && (daEquipe(p.papel) || livre)) ?? null;
  const telaNoPalco = quemMostraTela ? (quemMostraTela.id === eu.id ? locais.tela : recebida(quemMostraTela.id, "tela")) : null;
  const vePalco = aoVivo || equipe || reuniao;

  /* Trocou quem apresenta (ou parou): os destaques da tela anterior saem. */
  const idDeQuemApresenta = quemMostraTela?.id ?? null;
  useEffect(() => {
    s.quadro.current.aplicar({ k: "limpar" });
    const t = setTimeout(() => setCaneta(null), 0);
    return () => clearTimeout(t);
  }, [idDeQuemApresenta, s.quadro]);

  const carinhas = useMemo<Carinha[]>(() => {
    const lista: Carinha[] = [];
    const usadas = new Set<string>();
    for (const p of estado.palco) {
      if (!vePalco && daEquipe(p.papel)) continue;
      const souEu = p.id === eu.id;
      /* Esperando quem organiza: a minha câmera da entrada aparece só para mim. */
      const track = souEu ? (locais.cam ?? camLigando ?? (esperandoOrganizador ? camDaEntrada : null)) : recebida(p.id, "cam");
      usadas.add(p.id);
      lista.push({
        id: p.id,
        nome: souEu ? `${eu.nome} (você)` : p.nome,
        track,
        foto: p.foto,
        mic: souEu ? Boolean(locais.mic ?? (esperandoOrganizador ? inicial.mic : null)) : Boolean(p.trilhas.mic),
        mao: estado.maos.some((m) => m.id === p.id),
        eu: souEu,
        apresenta: daEquipe(p.papel),
        falando: souEu ? meuNivel > 0.12 : s.falando.has(p.id),
      });
    }
    /* Eu apareço quando ligo algo (ou sempre, na reunião). */
    if (!usadas.has(eu.id) && (locais.cam || camLigando || locais.mic || reuniao || equipe)) {
      lista.push({
        id: eu.id,
        nome: `${eu.nome} (você)`,
        track: locais.cam ?? camLigando ?? (esperandoOrganizador ? camDaEntrada : null),
        foto: eu.foto,
        mic: Boolean(locais.mic ?? (esperandoOrganizador ? inicial.mic : null)),
        mao: maoLevantada,
        eu: true,
        apresenta: equipe,
        falando: meuNivel > 0.12,
      });
    }
    /* Ordem fixa (quem apresenta primeiro, depois na ordem em que entrou): falar só acende o contorno, ninguém pula de lugar. */
    return lista.sort((a, b) => Number(b.apresenta) - Number(a.apresenta));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [estado, s.recebidas, s.falando, locais, eu.id, eu.nome, eu.foto, maoLevantada, vePalco, reuniao, equipe, meuNivel > 0.12, esperandoOrganizador, inicial, camDaEntrada, camLigando]);

  /*
   * Quem fica em destaque (como no Meet): quem falou por último, com calma
   * para trocar (useSalaAoVivo); sem ninguém falando, quem apresenta.
   */
  const destaque = useMemo(() => {
    const falou = s.emDestaque ? carinhas.find((c) => c.id === s.emDestaque && !c.eu) : null;
    return falou ?? carinhas.find((c) => c.apresenta && !c.eu) ?? carinhas.find((c) => !c.eu) ?? null;
  }, [carinhas, s.emDestaque]);

  /* Uma fila que cabe na coluna: se quem está falando ficou de fora, entra no último lugar visível. */
  const cabendo = useCallback(
    (lista: Carinha[], vagas: number, sobraFora: number) => {
      const temSobra = lista.length > vagas || sobraFora > 0;
      const cabem = temSobra ? Math.max(1, vagas - 1) : vagas;
      const visiveis = lista.slice(0, cabem);
      const falando = lista.find((c) => c.id === s.emDestaque);
      if (falando && !visiveis.includes(falando) && visiveis.length) visiveis[visiveis.length - 1] = falando;
      return { visiveis, sobra: lista.length - visiveis.length + sobraFora, resto: lista.filter((c) => !visiveis.includes(c)) };
    },
    [s.emDestaque],
  );

  const doPublico = carinhas.filter((c) => !c.apresenta).length;
  const maisPessoas = Math.max(0, estado.pessoas - doPublico);

  /* ------------------------------------------------------------- gravação */

  const fonteDoQuadro = useCallback(
    (): FonteDoQuadro => ({
      modo: modoDaGravacao,
      tela: telaNoPalco && quemMostraTela ? { track: telaNoPalco, nome: quemMostraTela.nome } : null,
      pessoas: carinhas.map((c) => ({
        id: c.id,
        nome: c.eu ? eu.nome : c.nome,
        track: c.track,
        foto: c.foto,
        falando: c.falando,
        mudo: !c.mic,
        apresenta: quemMostraTela ? c.id === quemMostraTela.id || (c.apresenta && !carinhas.some((x) => x.id === quemMostraTela.id)) : c.apresenta,
      })),
      mais: maisPessoas,
      anotacoes: s.quadro.current,
    }),
    [modoDaGravacao, telaNoPalco, quemMostraTela, carinhas, maisPessoas, s.quadro, eu.nome],
  );
  const fonteRef = useRef(fonteDoQuadro);
  useLayoutEffect(() => {
    fonteRef.current = fonteDoQuadro;
  });

  const audiosParaGravar = useCallback(() => {
    const mapa = new Map<string, MediaStreamTrack>();
    if (locaisRef.current.mic) mapa.set("eu-mic", locaisRef.current.mic);
    if (locaisRef.current.telaAudio) mapa.set("eu-tela", locaisRef.current.telaAudio);
    for (const r of s.recebidas) if (r.tipo === "mic" || r.tipo === "tela-audio") mapa.set(`${r.participante}:${r.tipo}`, r.track);
    return mapa;
  }, [s.recebidas]);

  useEffect(() => {
    gravador.current?.misturar(audiosParaGravar());
  }, [audiosParaGravar, locais]);

  const comecarGravacao = useCallback(async () => {
    /*
     * Um gravador por vez, também enquanto ele ainda está começando: o efeito
     * que liga a gravação roda de novo quando alguém entra (antes, conferia
     * `ativo`, que só fica verdadeiro no fim do começo, e em 17/10 nasceram
     * dois gravadores mandando partes para o mesmo arquivo, que se perdeu).
     */
    if (!apresentador || gravador.current) return;
    if (!gravacaoPossivel()) {
      aviso({ texto: "Este navegador não grava. Para gravar, use o Chrome ou o Edge no computador.", tom: "ruim" });
      return;
    }
    const g = new GravadorDaLive(s.api, () => fonteRef.current());
    gravador.current = g;
    try {
      await g.iniciar(audiosParaGravar());
      setGravar(true);
      setGravacao("gravando");
    } catch (erro) {
      gravador.current = null;
      setGravacao("falhou");
      aviso({ texto: erro instanceof Error ? `A gravação não começou: ${erro.message}` : "A gravação não começou.", tom: "ruim" });
    }
  }, [apresentador, s.api, audiosParaGravar, aviso]);

  const terminarGravacao = useCallback(async (deVez = false) => {
    const g = gravador.current;
    if (!g?.ativo) return;
    setGravacao("salvando");
    try {
      await g.terminar(deVez);
      if (deVez) setGravar(false);
      setGravacao("salva");
      aviso({
        texto: entrada.sala.gravacaoPara === "compradores" && !reuniao ? "Gravação salva. Ela vai para a área de membros." : "Gravação salva. Baixe no painel, em Lives e reuniões.",
        tom: "bom",
      });
    } catch {
      setGravacao("falhou");
      aviso({ texto: "Parte da gravação não foi salva. Fale com o suporte do Ktalogu.", tom: "ruim" });
    } finally {
      gravador.current = null;
    }
  }, [aviso, entrada.sala.gravacaoPara, reuniao]);

  /* Com a gravação ligada no painel, começa sozinha ao entrar ao vivo (e continua num arquivo novo se a página recarregar). */
  useEffect(() => {
    if (aoVivo && apresentador && gravar && gravacao === "parada" && s.situacao === "conectada") void comecarGravacao();
  }, [aoVivo, apresentador, gravar, gravacao, s.situacao, comecarGravacao]);

  /* Fechar a aba com a gravação no meio: o navegador pergunta antes. */
  useEffect(() => {
    if (gravacao !== "gravando" && gravacao !== "salvando") return;
    const segurar = (e: BeforeUnloadEvent) => {
      e.preventDefault();
      e.returnValue = "";
    };
    window.addEventListener("beforeunload", segurar);
    return () => window.removeEventListener("beforeunload", segurar);
  }, [gravacao]);

  const trocarModoDaGravacao = async (modo: "palco" | "sala") => {
    setModoDaGravacao(modo);
    await s.moderar("modo_gravacao", { modo }).catch(() => undefined);
  };

  /* Alguém entrou: quem apresenta manda de novo o que já está riscado na tela. */
  const pessoasAntes = useRef(estado.pessoas);
  useEffect(() => {
    const entrouGente = estado.pessoas > pessoasAntes.current;
    pessoasAntes.current = estado.pessoas;
    if (!entrouGente || !equipe || !locais.tela) return;
    const t = setTimeout(() => {
      for (const recado of s.quadro.current.exportar()) s.riscar(recado, false);
    }, 2500);
    return () => clearTimeout(t);
  }, [estado.pessoas, equipe, locais.tela, s]);

  /* --------------------------------------------------------------- ações */

  const irAoVivo = async () => {
    setOcupado("ir");
    try {
      await s.moderar("iniciar");
      setSemVaga(null);
      s.setAviso({ texto: reuniao ? "A reunião começou." : "Você está ao vivo.", tom: "bom" });
    } catch (erro) {
      if (erro instanceof ErroDaSala && erro.motivo === "sem_saldo") setSemVaga(erro.message);
      else aviso({ texto: erroParaATela(erro, "Não deu para começar agora. Tente de novo."), tom: "ruim" });
    } finally {
      setOcupado(null);
    }
  };

  const encerrar = async () => {
    setOcupado("fim");
    try {
      await terminarGravacao();
      await s.moderar("encerrar");
      setConfirmarFim(false);
    } catch (erro) {
      aviso({ texto: erro instanceof Error ? erro.message : "Não deu para encerrar.", tom: "ruim" });
    } finally {
      setOcupado(null);
    }
  };

  const telaCheia = () => {
    setMenu(null);
    const alvo = salaRef.current;
    if (!alvo) return;
    if (document.fullscreenElement) void document.exitFullscreen().catch(() => undefined);
    else void alvo.requestFullscreen?.().catch(() => undefined);
  };

  const sair = async () => {
    setMenu(null);
    if (gravador.current?.ativo) await terminarGravacao();
    s.sair();
  };

  const linkDaSala = typeof window !== "undefined" ? `${window.location.origin}/${entrada.sala.codigo}` : "";
  const copiarLink = async () => {
    const ok = await copiar(linkDaSala);
    setLinkCopiado(ok);
    setTimeout(() => setLinkCopiado(false), 1800);
  };

  const alternarAba = (nova: Aba) => setAba((atual) => (atual === nova ? null : nova));

  /* Com a live minimizada, o que estava na tela quando ela encolheu: se o
     apresentador mostrar outro produto (ou soltar outra oferta), a janelinha
     pisca "Novo produto na live" e tocar nela expande. */
  const [naHoraDeEncolher, setNaHoraDeEncolher] = useState<{ foco: string | null; oferta: string | null } | null>(null);
  useEffect(() => {
    setNaHoraDeEncolher(janela.minimizada ? { foco: estado.foco?.id ?? null, oferta: estado.oferta?.id ?? null } : null);
    // eslint-disable-next-line react-hooks/exhaustive-deps -- só no momento de encolher e expandir
  }, [janela.minimizada]);
  const novidade =
    janela.minimizada && naHoraDeEncolher && !encerrada
      ? estado.foco && estado.foco.id !== naHoraDeEncolher.foco
        ? "Novo produto na live"
        : estado.oferta && estado.oferta.id !== naHoraDeEncolher.oferta
          ? "Nova oferta na live"
          : null
      : null;

  /* Produto desta loja abre por trás da live; link de fora, numa aba nova. */
  const comprar = (destino: string | null) => {
    const naLive = enderecoNaLive(destino);
    if (naLive) janela.abrirLoja(naLive);
    else abrirDestino(destino);
  };

  /* --------------------------------------------------------------- telas */

  if (s.saida) {
    return (
      <main className="lv-entrada lv-entrada-fim">
        <section className="lv-entrada-texto">
          <h1>{s.saida.motivo === "saiu" ? (reuniao ? "Você saiu da reunião" : "Você saiu da live") : "Você saiu da sala"}</h1>
          <p className="lv-quando">{s.saida.texto}</p>
          <div className="lv-form lv-form-centro">
            {s.saida.motivo !== "tirado" ? (
              <button type="button" className="lv-botao" onClick={() => window.location.reload()}>
                Entrar de novo
              </button>
            ) : null}
            {reuniao ? null : (
              <a className="lv-botao leve" href={props.linkDeVolta}>
                Voltar
              </a>
            )}
          </div>
        </section>
      </main>
    );
  }

  const comeco = new Date(entrada.sala.comecaEm).getTime();
  const quemApresentaTela = quemMostraTela ? (quemMostraTela.id === eu.id ? "Você" : quemMostraTela.nome) : null;
  const gravando = (estado.gravando && !encerrada && gravacao !== "salvando") || gravacao === "gravando";
  /* A caneta é de quem está apresentando a tela (e de quem organiza, por cima da apresentação de qualquer um). */
  const podeDestacar = Boolean(telaNoPalco) && (equipe || quemMostraTela?.id === eu.id);

  /* O que fica no centro: a espera, o fim, a tela com a coluna, ou a galeria. */
  let centro: React.ReactNode;
  if (encerrada) {
    centro = (
      <div className="lv-aviso-centro">
        <h2>{reuniao ? "A reunião terminou" : "A live terminou"}</h2>
        <p>
          {reuniao
            ? "Obrigado por participar."
            : entrada.sala.gravar && props.gravacaoParaQuemComprou
              ? "A gravação fica na área de membros assim que estiver pronta."
              : "Obrigado por estar aqui. Até a próxima."}
        </p>
        {apresentador && gravacao === "salvando" ? <p>Salvando a gravação… não feche esta página.</p> : null}
        {apresentador && gravacao === "salva" ? <p>Gravação salva.</p> : null}
      </div>
    );
  } else if (!vePalco) {
    centro = (
      <div className="lv-aviso-centro">
        <p className="lv-aviso-sobre">{Date.now() < comeco ? "A live começa em" : "Daqui a pouco"}</p>
        <h2>{entrada.sala.titulo}</h2>
        {Date.now() < comeco ? <Relogio ate={comeco} agoraInicial={Date.now()} /> : <p>Quem apresenta já vai entrar. Enquanto isso, diga oi no bate-papo.</p>}
      </div>
    );
  } else if (telaNoPalco) {
    const { visiveis: coluna, sobra, resto } = cabendo(carinhas, 4, maisPessoas);
    centro = (
      <div className="lv-com-tela">
        <div className={`lv-palco${caneta ? " riscando" : ""}`}>
          <VideoDaTrilha ref={palcoRef} track={telaNoPalco} className="lv-tela" />
          <CamadaDeDestaques videoRef={palcoRef} quadro={s.quadro} escolha={podeDestacar ? caneta : null} riscar={s.riscar} />
          {podeDestacar && caneta ? <BarraDeDestaques escolha={caneta} aoEscolher={setCaneta} quadro={s.quadro} riscar={s.riscar} /> : null}
          <span className="lv-rotulo-palco">
            <MonitorArrowUp /> {quemApresentaTela === "Você" ? "Você está apresentando" : `Apresentação de ${quemApresentaTela}`}
          </span>
        </div>
        <div className="lv-coluna">
          {coluna.map((c) => (
            <Quadradinho key={c.id} c={c} />
          ))}
          {sobra > 0 ? <MaisPessoas n={sobra} carinhas={resto} /> : null}
        </div>
      </div>
    );
  } else if (carinhas.length >= 4 && destaque) {
    /* Muita gente e ninguém apresentando: quem fala fica grande, o resto numa coluna menor (como no Meet). */
    const { visiveis: coluna, sobra, resto } = cabendo(
      carinhas.filter((c) => c.id !== destaque.id),
      4,
      0,
    );
    centro = (
      <div className="lv-com-tela">
        <div className="lv-palco lv-palco-pessoa">
          <Quadradinho c={destaque} grande />
        </div>
        <div className="lv-coluna">
          {coluna.map((c) => (
            <Quadradinho key={c.id} c={c} />
          ))}
          {sobra > 0 ? <MaisPessoas n={sobra} carinhas={resto} /> : null}
        </div>
      </div>
    );
  } else {
    const limite = 9;
    const lista = carinhas.slice(0, carinhas.length > limite ? limite - 1 : limite);
    const sobra = carinhas.length - lista.length;
    const total = Math.max(1, lista.length + (sobra > 0 ? 1 : 0));
    centro = (
      <div className={`lv-galeria g${Math.min(total, 9)}`}>
        {lista.length ? (
          lista.map((c) => <Quadradinho key={c.id} c={c} grande={total <= 2} />)
        ) : (
          <div className="lv-aviso-centro">
            <p>{equipe && !aoVivo ? "Bastidores: só quem apresenta vê. Ligue a câmera, prepare a tela e clique em Entrar ao vivo." : "Ninguém com câmera ligada agora."}</p>
          </div>
        )}
        {sobra > 0 ? <MaisPessoas n={sobra} carinhas={carinhas.slice(lista.length)} /> : null}
      </div>
    );
  }

  return (
    <div
      className={`lv-sala${aba ? " com-lado" : ""}${janela.minimizada ? " lv-mini" : ""}${janela.arrastando ? " arrastando" : ""}`}
      ref={salaRef}
      style={janela.estiloDaJanela}
      data-situacao={s.situacao}
      data-status={estado.status}
    >
      {s.aviso ? (
        <div className={`lv-toast ${s.aviso.tom ?? ""}`} role="status">
          {s.aviso.texto}
        </div>
      ) : null}
      {/* O som de quem fala (e da tela compartilhada). */}
      {s.recebidas
        .filter((r) => r.tipo === "mic" || r.tipo === "tela-audio")
        .map((r) => (
          <SomDaTrilha key={`${r.participante}:${r.tipo}:${r.nome}`} track={r.track} />
        ))}

      <header className="lv-topo">
        <div className="lv-topo-esq">
          {aoVivo && !reuniao ? (
            <span className="lv-selo-vivo">Ao vivo</span>
          ) : encerrada ? (
            <span className="lv-selo">Encerrada</span>
          ) : !aoVivo && !reuniao ? (
            <span className="lv-selo">{equipe ? "Bastidores" : "Sala de espera"}</span>
          ) : null}
          {gravando ? (
            <span className="lv-selo lv-selo-gravando">
              <i /> Gravando
            </span>
          ) : gravacao === "salvando" ? (
            <span className="lv-selo">Salvando a gravação…</span>
          ) : null}
          {s.situacao === "reconectando" || s.situacao === "falhou" ? (
            <span className="lv-selo lv-selo-rede">
              <WifiSlash /> {s.situacao === "falhou" ? "Sem vídeo" : "Reconectando…"}
            </span>
          ) : null}
        </div>
        <div className="lv-topo-dir">
          {janela.loja && !janela.minimizada ? (
            <button type="button" className="lv-chip lv-chip-botao lv-chip-loja" onClick={() => janela.abrirLoja(janela.loja!)}>
              <ShoppingBag /> Continuar comprando
            </button>
          ) : null}
          {quemApresentaTela ? (
            <span className="lv-chip">
              <Avatar nome={quemMostraTela?.nome ?? ""} foto={quemMostraTela?.foto ?? null} className="lv-chip-av" />
              {quemApresentaTela === "Você" ? "Você está apresentando" : `${quemApresentaTela} (apresentando)`}
            </span>
          ) : null}
          <button type="button" className="lv-chip lv-chip-botao" onClick={() => alternarAba("pessoas")} aria-label="Pessoas na sala">
            <Users /> {estado.pessoas + estado.palco.filter((p) => daEquipe(p.papel)).length}
          </button>
        </div>
      </header>

      <main className="lv-area">
        <section className="lv-centro">
          {semVaga && apresentador && !aoVivo ? (
            <div className="lv-faixa-espera lv-faixa-vaga" role="alert">
              <b>{reuniao ? "A reunião ainda não começou." : "A live ainda não está no ar."}</b> {semVaga}
              <span className="lv-faixa-acoes">
                <a className="lv-botao" href={`${enderecoDoPainel()}/dashboard/assinatura?aba=digitais`} target="_blank" rel="noreferrer">
                  Comprar mais
                </a>
                <button type="button" className="lv-botao leve" onClick={() => void irAoVivo()} disabled={ocupado === "ir"}>
                  {ocupado === "ir" ? "Tentando…" : "Já comprei, tentar de novo"}
                </button>
              </span>
            </div>
          ) : null}
          {esperandoOrganizador ? (
            <div className="lv-faixa-espera" role="status">
              Esperando quem organizou a reunião entrar. A conversa começa assim que a pessoa chegar: a sua câmera e o seu microfone ligam sozinhos.
            </div>
          ) : null}
          {centro}
          {estado.oferta && !encerrada ? <OfertaNaTela oferta={estado.oferta} aoComprar={comprar} /> : null}
          {estado.foco && !encerrada ? <ProdutoEmFoco oferta={estado.foco} aoComprar={comprar} /> : null}
          <div className="lv-reacoes" aria-hidden>
            {s.reacoes.map((r) => (
              <span key={r.id} style={{ left: r.x }}>
                {r.emoji}
              </span>
            ))}
          </div>
        </section>
        {aba ? <LadoDaLive s={s} entrada={entrada} aba={aba} aoTrocar={setAba} aoFechar={() => setAba(null)} /> : null}
      </main>

      <footer className="lv-barra">
        <div className="lv-barra-esq">
          <span className="lv-hora">{horaCurta(relogio)}</span>
          <span className="lv-barra-sep" />
          <span className="lv-barra-titulo" title={entrada.sala.titulo}>
            {reuniao ? entrada.sala.codigo : entrada.sala.titulo}
          </span>
        </div>

        <div className="lv-barra-meio">
          {apresentador && !aoVivo && !encerrada && !reuniao ? (
            <button type="button" className="lv-ir" onClick={() => void irAoVivo()} disabled={ocupado === "ir"}>
              <Broadcast /> {ocupado === "ir" ? "Entrando…" : "Entrar ao vivo"}
            </button>
          ) : null}
          {posso.mic ? (
            <button
              type="button"
              className={`lv-ctl ${locais.mic ? "" : "desligado"}`}
              onClick={() => (locais.mic ? void parar("mic", "mic") : void ligarMic())}
              aria-label={locais.mic ? "Desligar o microfone" : "Ligar o microfone"}
              title={esperandoOrganizador ? "Liga quando a reunião começar" : locais.mic ? "Desligar o microfone" : "Ligar o microfone"}
              disabled={encerrada || esperandoOrganizador}
            >
              {locais.mic ? <Microphone /> : <MicrophoneSlash />}
            </button>
          ) : null}
          {posso.cam ? (
            <button
              type="button"
              className={`lv-ctl ${locais.cam ? "" : "desligado"}`}
              onClick={() => (locais.cam ? void parar("cam", "cam") : void ligarCam())}
              aria-label={locais.cam ? "Desligar a câmera" : "Ligar a câmera"}
              title={esperandoOrganizador ? "Liga quando a reunião começar" : locais.cam ? "Desligar a câmera" : "Ligar a câmera"}
              disabled={encerrada || esperandoOrganizador}
            >
              {locais.cam ? <VideoCamera /> : <VideoCameraSlash />}
            </button>
          ) : null}
          {posso.cam ? (
            <div className="lv-ancora lv-ancora-efeitos" data-menu>
              <button
                type="button"
                className={`lv-ctl lv-ctl-efeitos ${menu === "efeitos" ? "ativo" : ""}`}
                onClick={() => setMenu(menu === "efeitos" ? null : "efeitos")}
                aria-label="Efeitos da câmera: fundo desfocado e filtros"
                aria-expanded={menu === "efeitos"}
                title="Efeitos: fundo desfocado e filtros"
                disabled={encerrada}
              >
                <Sparkle weight={temEfeito(efeitos) ? "fill" : "regular"} />
              </button>
              {menu === "efeitos" ? (
                <PainelDeEfeitos variante="sala" efeitos={efeitos} aoMudar={aoMudarEfeitos} estado={estadoDosEfeitos} aoFechar={() => setMenu(null)} />
              ) : null}
            </div>
          ) : null}
          {posso.tela ? (
            <button
              type="button"
              className={`lv-ctl ${locais.tela ? "ativo" : ""}`}
              onClick={() =>
                locais.tela ? void pararTela() : quemMostraTela && quemMostraTela.id !== eu.id ? setAssumirTela(quemMostraTela.nome) : void mostrarTela()
              }
              aria-label={locais.tela ? "Parar de apresentar" : "Apresentar a tela"}
              title={esperandoOrganizador ? "Liga quando a reunião começar" : locais.tela ? "Parar de apresentar" : "Apresentar a tela"}
              disabled={encerrada || esperandoOrganizador}
            >
              <MonitorArrowUp />
            </button>
          ) : null}
          {podeDestacar ? (
            <button
              type="button"
              className={`lv-ctl ${caneta ? "ativo" : ""}`}
              onClick={() => setCaneta((atual) => (atual ? null : { ferramenta: "laser", cor: "#ea4335" }))}
              aria-label="Destacar na tela (caneta e ponteiro)"
              title="Destacar na tela: ponteiro, caneta e marca-texto"
            >
              <PencilSimple />
            </button>
          ) : null}
          <div className="lv-ancora" data-menu>
            <button type="button" className={`lv-ctl ${menu === "reacoes" ? "ativo" : ""}`} onClick={() => setMenu(menu === "reacoes" ? null : "reacoes")} aria-label="Reagir" title="Reagir" disabled={encerrada}>
              <Smiley />
            </button>
            {menu === "reacoes" ? (
              <div className="lv-emojis">
                {REACOES.map((e) => (
                  <button key={e} type="button" onClick={() => s.reagir(e)}>
                    {e}
                  </button>
                ))}
              </div>
            ) : null}
          </div>
          {posso.mao ? (
            <button
              type="button"
              className={`lv-ctl ${maoLevantada ? "mao-on" : ""}`}
              onClick={() => void s.levantarMao(!maoLevantada).catch((e) => aviso({ texto: e instanceof Error ? e.message : "Não deu agora.", tom: "ruim" }))}
              aria-label={maoLevantada ? "Baixar a mão" : "Levantar a mão"}
              title={s.podeFalar ? "Você pode falar" : maoLevantada ? "Baixar a mão" : "Levantar a mão"}
              disabled={encerrada || (!livre && s.podeFalar)}
            >
              <HandWaving />
            </button>
          ) : null}
          <div className="lv-ancora" data-menu>
            <button type="button" className={`lv-ctl ${menu === "mais" || menu === "aparelhos" ? "ativo" : ""}`} onClick={() => setMenu(menu === "mais" ? null : "mais")} aria-label="Mais opções" title="Mais opções">
              <DotsThreeVertical weight="bold" />
            </button>
            {menu === "mais" ? (
              <div className="lv-menu" role="menu">
                {apresentador && !encerrada ? (
                  <>
                    <button type="button" role="menuitem" onClick={() => void (gravacao === "gravando" ? terminarGravacao(true) : comecarGravacao()).then(() => setMenu(null))} disabled={gravacao === "salvando"}>
                      <Record weight={gravacao === "gravando" ? "fill" : "regular"} className={gravacao === "gravando" ? "lv-vermelho" : ""} />
                      {gravacao === "gravando" ? "Parar a gravação" : gravacao === "salvando" ? "Salvando a gravação…" : "Gravar"}
                    </button>
                    <div className="lv-menu-grupo">
                      <small>O que a gravação mostra</small>
                      <label>
                        <input type="radio" name="modo-gravacao" checked={modoDaGravacao === "palco"} onChange={() => void trocarModoDaGravacao("palco")} />
                        Tela e câmera de quem apresenta
                      </label>
                      <label>
                        <input type="radio" name="modo-gravacao" checked={modoDaGravacao === "sala"} onChange={() => void trocarModoDaGravacao("sala")} />
                        A sala inteira, como você vê
                      </label>
                    </div>
                  </>
                ) : null}
                {posso.mic || posso.cam ? (
                  <button type="button" role="menuitem" onClick={() => setMenu("aparelhos")}>
                    <GearSix /> Microfone e câmera
                  </button>
                ) : null}
                {posso.cam ? (
                  <button type="button" role="menuitem" className="lv-menu-efeitos" onClick={() => setMenu("efeitos")}>
                    <Sparkle /> Efeitos: fundo e filtros
                  </button>
                ) : null}
                <button type="button" role="menuitem" onClick={() => void copiarLink()}>
                  {linkCopiado ? <Check /> : <Copy />} {linkCopiado ? "Link copiado" : "Copiar o link da sala"}
                </button>
                <button type="button" role="menuitem" onClick={telaCheia}>
                  <CornersOut /> Tela cheia
                </button>
              </div>
            ) : null}
            {menu === "aparelhos" ? (
              <Aparelhos
                escolhidos={aparelhos}
                aoEscolher={(tipo, id) => {
                  setAparelhos((a) => ({ ...a, [tipo]: id }));
                  if (tipo === "mic" && locaisRef.current.mic) void ligarMic(id);
                  if (tipo === "cam" && locaisRef.current.cam) void ligarCam(id);
                }}
                aoFechar={() => setMenu(null)}
              />
            ) : null}
          </div>
          <div className="lv-ancora" data-menu>
            <button
              type="button"
              className="lv-sair"
              onClick={() => (apresentador && !encerrada ? setMenu(menu === "sair" ? null : "sair") : void sair())}
              aria-label={reuniao ? "Sair da reunião" : "Sair da live"}
              title={reuniao ? "Sair da reunião" : "Sair da live"}
            >
              <PhoneDisconnect weight="fill" />
            </button>
            {menu === "sair" ? (
              <div className="lv-menu lv-menu-sair" role="menu">
                <button type="button" role="menuitem" onClick={() => void sair()}>
                  <SignOut /> Só sair
                </button>
                {aoVivo || reuniao ? (
                  <button
                    type="button"
                    role="menuitem"
                    className="lv-vermelho"
                    onClick={() => {
                      setMenu(null);
                      setConfirmarFim(true);
                    }}
                  >
                    <PhoneDisconnect weight="fill" /> Encerrar para todos
                  </button>
                ) : null}
              </div>
            ) : null}
          </div>
        </div>

        <div className="lv-barra-dir">
          <button type="button" className={`lv-icone${aba === "pessoas" ? " on" : ""}`} onClick={() => alternarAba("pessoas")} aria-label="Pessoas" title="Pessoas">
            <Users weight={aba === "pessoas" ? "fill" : "regular"} />
            {estado.maos.length && equipe ? <em className="alerta">{estado.maos.length}</em> : null}
          </button>
          <button type="button" className={`lv-icone${aba === "chat" ? " on" : ""}`} onClick={() => alternarAba("chat")} aria-label="Bate-papo" title="Bate-papo">
            <ChatCircleText weight={aba === "chat" ? "fill" : "regular"} />
            {naoLidas ? <em>{naoLidas > 9 ? "9+" : naoLidas}</em> : null}
          </button>
          {equipe ? (
            <button type="button" className={`lv-icone${aba === "aovivo" ? " on" : ""}`} onClick={() => alternarAba("aovivo")} aria-label="Ferramentas" title={reuniao ? "Links e recados" : "Ofertas, produtos e recados"}>
              <Lightning weight={aba === "aovivo" ? "fill" : "regular"} />
            </button>
          ) : null}
        </div>
      </footer>

      {janela.controles}
      {novidade ? (
        <button type="button" className="lv-mini-novo" onClick={janela.expandir}>
          {novidade}
        </button>
      ) : null}
      {janela.loja ? (
        <LojaPorTras
          endereco={janela.loja}
          visivel={janela.minimizada}
          espaco={janela.espacoDaLoja}
          nomeDaLoja={props.loja.nome}
          aoVoltar={janela.expandir}
          aoFechar={janela.fecharLoja}
        />
      ) : null}

      {assumirTela ? (
        <div className="lv-caixa-fundo" role="dialog" aria-modal="true" aria-labelledby="lv-assumir-titulo">
          <div className="lv-caixa">
            <h3 id="lv-assumir-titulo">{assumirTela} está apresentando</h3>
            <p>Se você apresentar agora, a apresentação de {assumirTela} para e a sua tela aparece para todos.</p>
            <div className="lv-acoes">
              <button type="button" className="lv-botao leve" onClick={() => setAssumirTela(null)}>
                Voltar
              </button>
              <button type="button" className="lv-botao" onClick={() => void mostrarTela()}>
                Apresentar no lugar
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {confirmarFim ? (
        <div className="lv-caixa-fundo" role="dialog" aria-modal="true" aria-labelledby="lv-fim-titulo">
          <div className="lv-caixa">
            <h3 id="lv-fim-titulo">{reuniao ? "Encerrar a reunião para todos?" : "Encerrar a live para todos?"}</h3>
            <p>
              {reuniao ? "Todo mundo sai da sala e o link deixa de funcionar." : "A sala fecha para quem está assistindo."}
              {gravacao === "gravando" ? (entrada.sala.gravacaoPara === "compradores" && !reuniao ? " A gravação é salva e vai para a área de membros." : " A gravação é salva no painel, para você baixar.") : ""}
            </p>
            <div className="lv-acoes">
              <button type="button" className="lv-botao leve" onClick={() => setConfirmarFim(false)} disabled={ocupado === "fim"}>
                Voltar
              </button>
              <button type="button" className="lv-botao perigo" onClick={() => void encerrar()} disabled={ocupado === "fim"}>
                {ocupado === "fim" ? (gravacao === "salvando" ? "Salvando a gravação…" : "Encerrando…") : "Encerrar para todos"}
              </button>
            </div>
          </div>
        </div>
      ) : null}
    </div>
  );
}

/* ------------------------------------------------------------- carinhas */

function Quadradinho({ c, grande }: { c: Carinha; grande?: boolean }) {
  return (
    <div className={`lv-quadro${c.falando ? " falando" : ""}${grande ? " grande" : ""}`}>
      {c.track ? <VideoDaTrilha track={c.track} espelho={c.eu} /> : <Avatar nome={c.nome} foto={c.foto} className="lv-quadro-av" />}
      {c.mao ? (
        <span className="lv-quadro-mao" title="Mão levantada">
          <HandWaving weight="fill" />
        </span>
      ) : null}
      {!c.mic ? (
        <span className="lv-quadro-mudo" title="Microfone desligado">
          <MicrophoneSlash weight="fill" />
        </span>
      ) : null}
      <span className="lv-quadro-nome">{c.nome}</span>
    </div>
  );
}

function MaisPessoas({ n, carinhas }: { n: number; carinhas: Carinha[] }) {
  const rostos = carinhas.slice(0, 2);
  return (
    <div className="lv-quadro lv-quadro-mais">
      <span className="lv-rostos">
        {rostos.map((c) => (
          <Avatar key={c.id} nome={c.nome} foto={c.foto} className="lv-rosto" />
        ))}
      </span>
      <b>
        Mais {n} {n === 1 ? "pessoa" : "pessoas"}
      </b>
    </div>
  );
}

/* Trocar o microfone e a câmera (como nas configurações do Meet). */
function Aparelhos({
  escolhidos,
  aoEscolher,
  aoFechar,
}: {
  escolhidos: { mic: string | null; cam: string | null };
  aoEscolher: (tipo: "mic" | "cam", id: string) => void;
  aoFechar: () => void;
}) {
  const [lista, setLista] = useState<MediaDeviceInfo[]>([]);
  useEffect(() => {
    void navigator.mediaDevices
      ?.enumerateDevices()
      .then(setLista)
      .catch(() => setLista([]));
  }, []);
  const mics = lista.filter((d) => d.kind === "audioinput" && d.deviceId);
  const cams = lista.filter((d) => d.kind === "videoinput" && d.deviceId);
  return (
    <div className="lv-menu lv-menu-aparelhos" role="dialog" aria-label="Microfone e câmera">
      <label>
        <small>Microfone</small>
        <select value={escolhidos.mic ?? mics[0]?.deviceId ?? ""} onChange={(e) => aoEscolher("mic", e.target.value)}>
          {mics.map((d, i) => (
            <option key={d.deviceId} value={d.deviceId}>
              {d.label || `Microfone ${i + 1}`}
            </option>
          ))}
        </select>
      </label>
      <label>
        <small>Câmera</small>
        <select value={escolhidos.cam ?? cams[0]?.deviceId ?? ""} onChange={(e) => aoEscolher("cam", e.target.value)}>
          {cams.map((d, i) => (
            <option key={d.deviceId} value={d.deviceId}>
              {d.label || `Câmera ${i + 1}`}
            </option>
          ))}
        </select>
      </label>
      <button type="button" className="lv-botao leve" onClick={aoFechar}>
        Pronto
      </button>
    </div>
  );
}

/* ------------------------------------------------------------- na tela */

function moeda(valor: number) {
  return valor.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}

function abrirDestino(destino: string | null) {
  if (!destino) return;
  window.open(destino, "_blank", "noopener");
}

/*
 * A tag do que está na tela aparece inteira e, depois de uns 3 segundos,
 * recolhe para um cartão compacto (como o card da loja), para não cobrir a
 * live. Tocar no cartão (fora do botão) mostra inteira de novo.
 */
const TEMPO_ATE_RECOLHER_MS = 3000;

function useRecolher(chave: string) {
  const [compacto, setCompacto] = useState(false);
  const [vez, setVez] = useState(0);
  useEffect(() => {
    setCompacto(false);
    const relogio = window.setTimeout(() => setCompacto(true), TEMPO_ATE_RECOLHER_MS);
    return () => window.clearTimeout(relogio);
  }, [chave, vez]);
  const abrir = (evento: React.MouseEvent) => {
    if ((evento.target as HTMLElement).closest("button")) return;
    setVez((n) => n + 1);
  };
  return { compacto, abrir };
}

function OfertaNaTela({ oferta, aoComprar }: { oferta: Oferta; aoComprar: (destino: string | null) => void }) {
  const [copiado, setCopiado] = useState(false);
  const { compacto, abrir } = useRecolher(oferta.id);
  const copiarCodigo = async () => {
    if (!oferta.codigo) return;
    const ok = await copiar(oferta.codigo);
    setCopiado(ok);
    setTimeout(() => setCopiado(false), 1800);
  };
  /* Produto com foto recolhe em cartão de loja; cupom e link, numa pílula. */
  const cartao = compacto && oferta.tipo === "produto" && Boolean(oferta.imagem);
  return (
    <div className={`lv-oferta${cartao ? " lv-cartao" : compacto ? " lv-pilula" : ""}`} key={oferta.id} onClick={abrir}>
      {oferta.imagem ? <img src={oferta.imagem} alt="" /> : <span className="lv-ic">{oferta.tipo === "cupom" ? <Ticket /> : <ArrowSquareOut />}</span>}
      <div className="lv-txt">
        <small>{oferta.tipo === "cupom" ? "Cupom da live" : oferta.tipo === "produto" ? "Oferta da live" : "Link"}</small>
        <b>{oferta.titulo}</b>
        {oferta.detalhe ? <span className="lv-det">{oferta.detalhe}</span> : null}
        {oferta.preco ? (
          <span className="lv-det lv-det-preco">
            {oferta.precoDe ? <s>{moeda(oferta.precoDe)}</s> : null} <b className="lv-preco-inline">{moeda(oferta.preco)}</b>
          </span>
        ) : null}
        {oferta.codigo ? (
          <button type="button" className="lv-cupom" onClick={() => void copiarCodigo()}>
            {copiado ? "Copiado" : oferta.codigo}
          </button>
        ) : null}
      </div>
      {oferta.destino ? (
        <button type="button" className="lv-botao-mini" onClick={() => aoComprar(oferta.destino)}>
          {oferta.tipo === "link" ? "Abrir" : oferta.tipo === "produto" ? "Comprar" : "Quero"}
        </button>
      ) : null}
    </div>
  );
}

function ProdutoEmFoco({ oferta, aoComprar }: { oferta: Oferta; aoComprar: (destino: string | null) => void }) {
  const { compacto, abrir } = useRecolher(oferta.id);
  return (
    <div className={`lv-foco${compacto ? " lv-cartao" : ""}`} key={oferta.id} onClick={abrir}>
      {oferta.imagem ? <img src={oferta.imagem} alt="" /> : <span className="lv-foco-sem" />}
      <div className="lv-txt">
        <small>Mostrando agora</small>
        <b>{oferta.titulo}</b>
        {oferta.preco ? <div className="lv-preco">{moeda(oferta.preco)}</div> : null}
      </div>
      <button type="button" className="lv-botao-mini" onClick={() => aoComprar(oferta.destino)}>
        Comprar
      </button>
    </div>
  );
}

export type { SalaAoVivo };
