import type { Oferta, PapelNaSala, TipoDeTrilha } from "./evento.ts";

/*
 * As regras da sala que não dependem do banco: em que momento a live está,
 * quem pode mandar o quê, e a leitura do que a loja deixou pronto para soltar.
 */

export type SalaParaRegras = {
  status: "agendada" | "ao_vivo" | "encerrada" | "cancelada";
  comeca_em: string;
  duracao_min: number;
  espera_min: number;
  participacao: "mao" | "so_apresentador" | "livre";
  cameras_alunos: boolean;
  /* Reunião: a sala abre a qualquer hora antes, como um link do Meet. */
  categoria?: "venda" | "reuniao";
  iniciada_em?: string | null;
};

export type Momento = "cedo" | "espera" | "ao_vivo" | "encerrada" | "cancelada";

/* Quanto tempo depois do horário a sala de espera continua aberta sem a loja entrar. */
const ESPERA_DEPOIS_DO_HORARIO_MS = 3 * 60 * 60 * 1000;

export function momentoDaSala(sala: SalaParaRegras, agora = Date.now()): Momento {
  if (sala.status === "cancelada") return "cancelada";
  if (sala.status === "encerrada") return "encerrada";
  if (sala.status === "ao_vivo") {
    /* Ficou "no ar" sem ninguém encerrar (todo mundo fechou a aba): acaba sozinha. */
    const inicio = sala.iniciada_em ? new Date(sala.iniciada_em).getTime() : NaN;
    if (Number.isFinite(inicio) && agora > inicio + Math.max(sala.duracao_min, 60) * 60 * 1000 + ESPERA_DEPOIS_DO_HORARIO_MS) return "encerrada";
    return "ao_vivo";
  }
  const comeca = new Date(sala.comeca_em).getTime();
  if (!Number.isFinite(comeca)) return "cedo";
  if (sala.categoria !== "reuniao" && agora < comeca - sala.espera_min * 60 * 1000) return "cedo";
  /* Passou muito do horário e ninguém entrou ao vivo: a sala fecha. */
  if (agora > comeca + sala.duracao_min * 60 * 1000 + ESPERA_DEPOIS_DO_HORARIO_MS) return "encerrada";
  return "espera";
}

export function ehDaEquipe(papel: PapelNaSala): boolean {
  return papel === "apresentador" || papel === "convidado";
}

/*
 * Quem pode publicar cada trilha. Quem apresenta: tudo. Quem participa: a
 * câmera (quando a live deixa) e o microfone só depois que a loja libera a
 * fala; tela, só a equipe. Na participação livre (reunião), todo mundo manda tudo.
 */
export function podePublicar(
  papel: PapelNaSala,
  tipo: TipoDeTrilha,
  sala: Pick<SalaParaRegras, "participacao" | "cameras_alunos">,
  podeFalar: boolean,
): boolean {
  if (ehDaEquipe(papel) || sala.participacao === "livre") return true;
  if (tipo === "tela" || tipo === "tela-audio") return false;
  if (tipo === "cam") return sala.cameras_alunos || (sala.participacao === "mao" && podeFalar);
  if (tipo === "mic") return sala.participacao === "mao" && podeFalar;
  return false;
}

/* O nome da trilha no SFU: único na sessão, para a tela saber que é uma trilha nova. */
export function nomeDaTrilha(tipo: TipoDeTrilha, agora = Date.now()): string {
  return `${tipo}-${agora.toString(36)}${Math.floor(Math.random() * 1296).toString(36).padStart(2, "0")}`;
}

export function nomeDeTrilhaValido(nome: unknown): nome is string {
  return typeof nome === "string" && /^(cam|mic|tela|tela-audio)-[a-z0-9]{4,20}$/.test(nome);
}

function texto(valor: unknown, teto: number): string | null {
  if (typeof valor !== "string") return null;
  const limpo = valor.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
  return limpo ? limpo.slice(0, teto) : null;
}

function enderecoSeguro(valor: unknown): string | null {
  const bruto = texto(valor, 1000);
  if (!bruto) return null;
  if (bruto.startsWith("/") && !bruto.startsWith("//")) return bruto;
  try {
    const url = new URL(bruto);
    return url.protocol === "https:" ? url.toString() : null;
  } catch {
    return null;
  }
}

function numero(valor: unknown): number | null {
  const n = typeof valor === "number" ? valor : typeof valor === "string" && valor.trim() ? Number(valor) : NaN;
  return Number.isFinite(n) && n >= 0 && n < 10_000_000 ? Math.round(n * 100) / 100 : null;
}

/* Um item pronto para soltar (como a loja guardou), conferido e cortado no tamanho. */
export function lerOferta(bruto: unknown): Oferta | null {
  if (!bruto || typeof bruto !== "object" || Array.isArray(bruto)) return null;
  const o = bruto as Record<string, unknown>;
  const tipo = o.tipo === "cupom" || o.tipo === "produto" || o.tipo === "link" ? o.tipo : null;
  const id = typeof o.id === "string" && /^[a-z0-9-]{4,40}$/i.test(o.id) ? o.id : null;
  const titulo = texto(o.titulo, 90);
  if (!tipo || !id || !titulo) return null;
  const ate = typeof o.ate === "string" && Number.isFinite(new Date(o.ate).getTime()) ? new Date(o.ate).toISOString() : null;
  return {
    id,
    tipo,
    titulo,
    detalhe: texto(o.detalhe, 140),
    codigo: tipo === "cupom" ? texto(o.codigo, 40)?.toUpperCase().replace(/\s+/g, "") ?? null : null,
    imagem: enderecoSeguro(o.imagem),
    preco: numero(o.preco),
    precoDe: numero(o.precoDe),
    destino: enderecoSeguro(o.destino),
    ate,
  };
}

export function lerOfertas(bruto: unknown, teto = 30): Oferta[] {
  return (Array.isArray(bruto) ? bruto : []).map(lerOferta).filter((o): o is Oferta => Boolean(o)).slice(0, teto);
}

/* O painel guarda o produto como "/p/<id>": na loja o caminho ganha o slug. */
export function noEnderecoDaLoja(oferta: Oferta, slug: string): Oferta {
  if (oferta.destino && oferta.destino.startsWith("/p/") && slug) return { ...oferta, destino: `/${slug}${oferta.destino}` };
  return oferta;
}

/* O texto do bate-papo: uma linha limpa, até 500 letras. */
export function textoDaMensagem(valor: unknown): string | null {
  if (typeof valor !== "string") return null;
  const limpo = valor.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "").replace(/\n{3,}/g, "\n\n").trim();
  return limpo ? limpo.slice(0, 500) : null;
}

/* O nome que aparece na sala (quem entra sem cadastro digita). */
export function nomeNaSala(valor: unknown): string | null {
  const limpo = texto(valor, 60);
  if (!limpo || limpo.length < 2) return null;
  return limpo;
}

/* Iniciais para a carinha sem câmera. */
export function iniciais(nome: string): string {
  const partes = nome.trim().split(/\s+/).filter(Boolean);
  if (!partes.length) return "?";
  const primeira = partes[0][0] ?? "";
  const ultima = partes.length > 1 ? (partes[partes.length - 1][0] ?? "") : "";
  return (primeira + ultima).toUpperCase();
}

/* -------------------------------------------------------------- capacidade */

/*
 * Quantas pessoas do público cabem na sala, sem banco (os testes rodam direto).
 *
 * A cortesia da empresa (live_cortesias) vale antes de tudo e em qualquer
 * momento — sala agendada, sala de espera, reunião antes do anfitrião, ao vivo:
 *   - pessoas vazio  → sem limite (Infinity);
 *   - pessoas N      → pelo menos N (um pacote ou um limite maior não diminui).
 * Sem cortesia: o que a sala guardou ao ir ao ar → o pacote escolhido → o
 * grátis das regras (30 se não houver regra).
 *
 * O limite guardado na sala pode ser velho (reservado antes da cortesia); por
 * isso a cortesia vem primeiro.
 */
export type FontesDaCapacidade = {
  cortesia: { pessoas: number | null } | null;
  limiteDaSala: number | null;
  pessoasDoPacote: number | null;
  gratisPessoas: number | null;
};

const PESSOAS_DO_GRATIS_PADRAO = 30;

function positivo(valor: number | null | undefined): number {
  const n = Number(valor);
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
}

export function capacidadeResolvida(f: FontesDaCapacidade): number {
  const semCortesia = positivo(f.limiteDaSala) || positivo(f.pessoasDoPacote) || positivo(f.gratisPessoas) || PESSOAS_DO_GRATIS_PADRAO;
  if (!f.cortesia) return semCortesia;
  if (f.cortesia.pessoas === null || f.cortesia.pessoas === undefined) return Infinity;
  return Math.max(positivo(f.cortesia.pessoas), positivo(f.limiteDaSala), positivo(f.pessoasDoPacote)) || semCortesia;
}
