/*
 * KtaloguLive no painel: o que a loja configura na live (prévia 21), conferido
 * e cortado no tamanho antes de ir para o banco. Arquivo sem servidor: os
 * testes rodam direto.
 */

export type AcessoDaLive = "paga" | "cadastro" | "aberta";

export type ItemParaSoltar = {
  id: string;
  tipo: "cupom" | "produto" | "link";
  titulo: string;
  detalhe: string | null;
  codigo: string | null;
  imagem: string | null;
  preco: number | null;
  precoDe: number | null;
  destino: string | null;
};

export type CategoriaDaLive = "venda" | "reuniao";

export type DadosDaLive = {
  /* "venda": live da loja virtual (tem página de produto). "reuniao": conversa por link, fora da loja. */
  categoria: CategoriaDaLive;
  titulo: string;
  comecaEm: string;
  duracaoMin: number;
  esperaMin: number;
  acesso: AcessoDaLive;
  preco: number;
  venderDurante: boolean;
  limiteVagas: number | null;
  participacao: "mao" | "so_apresentador" | "livre";
  camerasAlunos: boolean;
  chat: "aberto" | "perguntas" | "desligado";
  gravar: boolean;
  gravacaoPara: "compradores" | "ninguem";
  /* "palco": a tela compartilhada com a câmera de quem apresenta. "sala": a sala inteira, como no Meet. */
  gravacaoModo: "palco" | "sala";
  vaga: "gratis" | "pacote";
  vagaCompraId: string | null;
  itens: ItemParaSoltar[];
  vitrine: boolean;
  produtosVitrine: string[];
  apresentadorNome: string | null;
  apresentadorBio: string | null;
  apresentadorFoto: string | null;
  aprender: string[];
  capa: string | null;
  descricao: string | null;
};

export class ErroNosDados extends Error {}

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

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

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

function inteiro(valor: unknown, padrao: number, minimo: number, maximo: number): number {
  const n = typeof valor === "number" ? valor : typeof valor === "string" && valor.trim() ? Number(valor) : NaN;
  if (!Number.isFinite(n)) return padrao;
  return Math.min(maximo, Math.max(minimo, Math.round(n)));
}

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

function umDe<T extends string>(valor: unknown, opcoes: readonly T[], padrao: T): T {
  return opcoes.includes(valor as T) ? (valor as T) : padrao;
}

/*
 * Dia e hora de Brasília → ISO. O Brasil não tem mais horário de verão
 * (desde 2019): é sempre -03:00.
 */
export function horarioDeBrasilia(dia: unknown, hora: unknown): string | null {
  if (typeof dia !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(dia)) return null;
  if (typeof hora !== "string" || !/^\d{2}:\d{2}$/.test(hora)) return null;
  const t = new Date(`${dia}T${hora}:00-03:00`).getTime();
  return Number.isFinite(t) ? new Date(t).toISOString() : null;
}

/* O contrário: o ISO guardado → dia e hora de Brasília para os campos. */
export function paraOsCampos(iso: string): { dia: string; hora: string } {
  const d = new Date(new Date(iso).getTime() - 3 * 3600 * 1000);
  const dia = d.toISOString().slice(0, 10);
  const hora = d.toISOString().slice(11, 16);
  return { dia, hora };
}

export function novoIdDeItem(): string {
  return `i${Date.now().toString(36)}${Math.floor(Math.random() * 46656).toString(36)}`;
}

export function lerItem(bruto: unknown): ItemParaSoltar | null {
  if (!bruto || typeof bruto !== "object" || Array.isArray(bruto)) return null;
  const o = bruto as Record<string, unknown>;
  const tipo = umDe(o.tipo, ["cupom", "produto", "link"] as const, "cupom");
  const titulo = texto(o.titulo, 90);
  if (!titulo) return null;
  const id = typeof o.id === "string" && /^[a-z0-9-]{4,40}$/i.test(o.id) ? o.id : novoIdDeItem();
  const destinoBruto = texto(o.destino, 1000);
  const destino = destinoBruto && destinoBruto.startsWith("/") && !destinoBruto.startsWith("//") ? destinoBruto : https(destinoBruto);
  return {
    id,
    tipo,
    titulo,
    detalhe: texto(o.detalhe, 140),
    codigo: tipo === "cupom" ? texto(o.codigo, 40)?.toUpperCase().replace(/\s+/g, "") ?? null : null,
    imagem: https(o.imagem),
    preco: dinheiro(o.preco),
    precoDe: dinheiro(o.precoDe),
    destino,
  };
}

/* Confere tudo o que veio da tela. Erro de preenchimento vira ErroNosDados com a frase para a pessoa. */
export function lerDadosDaLive(bruto: unknown, agora = Date.now()): DadosDaLive {
  const o = bruto && typeof bruto === "object" && !Array.isArray(bruto) ? (bruto as Record<string, unknown>) : {};
  const categoria = umDe(o.categoria, ["venda", "reuniao"] as const, "venda");
  const reuniao = categoria === "reuniao";
  const titulo = texto(o.titulo, 160);
  if (!titulo || titulo.length < 2) throw new ErroNosDados(reuniao ? "Dê um nome para a reunião." : "Dê um nome para a live.");
  /* "Começar agora" (reunião instantânea): a hora é a de agora. */
  const comecaEm = (o.agora === true ? new Date(agora).toISOString() : null) ?? horarioDeBrasilia(o.dia, o.hora) ?? (typeof o.comecaEm === "string" && Number.isFinite(new Date(o.comecaEm).getTime()) ? new Date(o.comecaEm).toISOString() : null);
  if (!comecaEm) throw new ErroNosDados("Escolha o dia e a hora da live.");
  if (new Date(comecaEm).getTime() < agora - 365 * 24 * 3600 * 1000) throw new ErroNosDados("Confira o dia da live.");
  /* Reunião: quem tem o link entra com o nome, como no Meet. */
  const acesso = reuniao ? "aberta" : umDe(o.acesso, ["paga", "cadastro", "aberta"] as const, "paga");
  const preco = acesso === "paga" ? dinheiro(o.preco) ?? 0 : 0;
  if (acesso === "paga" && preco < 1) throw new ErroNosDados("Live paga precisa de preço (a partir de R$ 1,00).");
  const limite = o.limiteVagas === null || o.limiteVagas === "" || o.limiteVagas === undefined ? null : inteiro(o.limiteVagas, 0, 0, 100000);
  const vaga = umDe(o.vaga, ["gratis", "pacote"] as const, "gratis");
  const vagaCompraId = vaga === "pacote" && typeof o.vagaCompraId === "string" && UUID.test(o.vagaCompraId) ? o.vagaCompraId : null;
  if (vaga === "pacote" && !vagaCompraId) throw new ErroNosDados("Escolha o pacote KtaloguLive que esta live vai usar.");
  const aprender = (Array.isArray(o.aprender) ? o.aprender : [])
    .map((linha) => texto(linha, 140))
    .filter((linha): linha is string => Boolean(linha))
    .slice(0, 8);
  return {
    categoria,
    titulo,
    comecaEm,
    duracaoMin: inteiro(o.duracaoMin, 60, 10, 720),
    esperaMin: inteiro(o.esperaMin, 15, 0, 120),
    acesso,
    preco,
    venderDurante: o.venderDurante !== false,
    limiteVagas: limite && limite > 0 ? limite : null,
    participacao: umDe(o.participacao, ["mao", "so_apresentador", "livre"] as const, reuniao ? "livre" : "mao"),
    camerasAlunos: o.camerasAlunos !== false,
    chat: umDe(o.chat, ["aberto", "perguntas", "desligado"] as const, "aberto"),
    gravar: o.gravar !== false,
    /* A reunião não tem área de membros: a gravação fica com a loja, para baixar. */
    gravacaoPara: reuniao ? "ninguem" : umDe(o.gravacaoPara, ["compradores", "ninguem"] as const, "compradores"),
    gravacaoModo: umDe(o.gravacaoModo, ["palco", "sala"] as const, reuniao ? "sala" : "palco"),
    vaga,
    vagaCompraId,
    itens: (Array.isArray(o.itens) ? o.itens : []).map(lerItem).filter((i): i is ItemParaSoltar => Boolean(i)).slice(0, 30),
    vitrine: !reuniao && o.vitrine === true,
    produtosVitrine: reuniao ? [] : Array.from(
      new Set((Array.isArray(o.produtosVitrine) ? o.produtosVitrine : []).filter((id): id is string => typeof id === "string" && UUID.test(id))),
    ).slice(0, 40),
    apresentadorNome: texto(o.apresentadorNome, 80),
    apresentadorBio: texto(o.apresentadorBio, 200),
    apresentadorFoto: https(o.apresentadorFoto),
    aprender,
    capa: https(o.capa),
    descricao: texto(o.descricao, 4000),
  };
}

/* A sala no banco, a partir do que a loja preencheu. */
export function salaParaOBanco(d: DadosDaLive) {
  return {
    titulo: d.titulo,
    comeca_em: d.comecaEm,
    duracao_min: d.duracaoMin,
    espera_min: d.esperaMin,
    acesso: d.acesso,
    vender_durante: d.venderDurante,
    limite_vagas: d.limiteVagas,
    participacao: d.participacao,
    cameras_alunos: d.camerasAlunos,
    chat: d.chat,
    gravar: d.gravar,
    gravacao_para: d.gravacaoPara,
    gravacao_modo: d.gravacaoModo,
    vaga: d.vaga,
    vaga_compra_id: d.vagaCompraId,
    itens: d.itens,
    vitrine: d.vitrine,
    produtos_vitrine: d.produtosVitrine,
    apresentador_nome: d.apresentadorNome,
    apresentador_bio: d.apresentadorBio,
    apresentador_foto: d.apresentadorFoto,
    aprender: d.aprender,
  };
}

/* O que a tela de Lives mostra como situação. */
export function situacaoDaLive(
  sala: { status: string; comeca_em: string; duracao_min: number; iniciada_em?: string | null },
  agora = Date.now(),
): "agendada" | "hoje" | "ao_vivo" | "encerrada" | "cancelada" | "passou" {
  if (sala.status === "ao_vivo") {
    /* Ninguém encerrou (todos fecharam a aba): a loja também dá por encerrada (a mesma regra da sala). */
    const inicio = sala.iniciada_em ? new Date(sala.iniciada_em).getTime() : NaN;
    if (Number.isFinite(inicio) && agora > inicio + (Math.max(sala.duracao_min, 60) + 180) * 60000) return "encerrada";
    return "ao_vivo";
  }
  if (sala.status === "encerrada") return "encerrada";
  if (sala.status === "cancelada") return "cancelada";
  const comeca = new Date(sala.comeca_em).getTime();
  if (agora > comeca + (sala.duracao_min + 180) * 60000) return "passou";
  const hoje = new Date(agora - 3 * 3600000).toISOString().slice(0, 10);
  const dia = new Date(comeca - 3 * 3600000).toISOString().slice(0, 10);
  return hoje === dia ? "hoje" : "agendada";
}

/* ------------------------------------------------- pessoas por transmissão */

/*
 * Quantas pessoas cabem nas transmissões grátis, como aparece na tela. A
 * cortesia sem limite (live_cortesias.pessoas vazio, migration 203) chega como
 * pessoas_sem_limite = true; o número que vem junto (100000) é só para o
 * código antigo não quebrar.
 */
export const PESSOAS_SEM_LIMITE = 100000;

export function semLimiteDePessoas(saldo: { gratis_pessoas?: number | null; pessoas_sem_limite?: boolean } | null | undefined): boolean {
  if (!saldo) return false;
  return saldo.pessoas_sem_limite === true || Number(saldo.gratis_pessoas) >= PESSOAS_SEM_LIMITE;
}

/* "sem limite de pessoas" ou "até 30 pessoas". */
export function pessoasDoGratis(
  saldo: { gratis_pessoas?: number | null; pessoas_sem_limite?: boolean } | null | undefined,
  padrao = 30,
): string {
  if (semLimiteDePessoas(saldo)) return "sem limite de pessoas";
  return `até ${Number(saldo?.gratis_pessoas) || padrao} pessoas`;
}
