/**
 * O lado do servidor dos painéis: calcular os quadros e guardar os painéis.
 *
 * Usado pelas rotas /api/paineis e pelas ferramentas do assistente. A tabela
 * `paineis` é sempre lida e gravada com a sessão da pessoa (as travas do banco
 * valem: ver exige reports.read, mexer exige company.settings). Os números
 * vêm da `Fonte` que cada lado monta.
 */

import type { SupabaseClient } from "@supabase/supabase-js";

import { ROTULO_DA_DIMENSAO, variacao, type Grupo } from "./agregar.ts";
import { calcularMetrica, metricaPorId, MetricaSemPermissao, type Coluna, type Fonte, type Formato } from "./metricas.ts";
import { resolverPeriodo } from "./periodos.ts";
import { conferirDescricao, conferirNome, conferirWidgets, lerWidgetsGravados, type Widget } from "./widgets.ts";

export type ResultadoDoWidget = {
  widget_id: string;
  formato: Formato;
  periodo: { de: string; ate: string; rotulo: string } | null;
  valor: number | null;
  anterior: { valor: number; variacao: number | null; rotulo: string } | null;
  grupos: Grupo[] | null;
  colunas: Coluna[];
  rotulo_do_grupo: string | null;
  aviso: string | null;
  erro: string | null;
};

export async function calcularWidget(fonte: Fonte, widget: Widget): Promise<ResultadoDoWidget> {
  const metrica = metricaPorId(widget.metrica);
  const base: ResultadoDoWidget = {
    widget_id: widget.id,
    formato: metrica?.formato ?? "inteiro",
    periodo: null,
    valor: null,
    anterior: null,
    grupos: null,
    colunas: [],
    rotulo_do_grupo: null,
    aviso: null,
    erro: null,
  };
  if (!metrica) return { ...base, erro: "Esta métrica não existe mais." };

  try {
    const periodo = resolverPeriodo(widget.periodo, fonte.hoje, { de: widget.de, ate: widget.ate });
    const pedido = { intervalo: periodo, agruparPor: widget.agrupar_por ?? null, limite: widget.limite, filtros: widget.filtros ?? undefined };
    const comparar = widget.tipo === "numero" && metrica.usaPeriodo;

    const [atual, anterior] = await Promise.all([
      calcularMetrica(fonte, metrica, pedido),
      comparar ? calcularMetrica(fonte, metrica, { ...pedido, intervalo: periodo.anterior, agruparPor: null }) : Promise.resolve(null),
    ]);

    return {
      ...base,
      periodo: metrica.usaPeriodo ? { de: periodo.de, ate: periodo.ate, rotulo: periodo.rotulo } : null,
      valor: atual.valor,
      anterior: anterior ? { valor: anterior.valor, variacao: variacao(atual.valor, anterior.valor), rotulo: periodo.rotuloDoAnterior } : null,
      grupos: atual.grupos,
      colunas: metrica.extras ?? [],
      rotulo_do_grupo: widget.agrupar_por ? ROTULO_DA_DIMENSAO[widget.agrupar_por] : null,
      aviso:
        atual.cortado || anterior?.cortado
          ? "Há registros demais para somar de uma vez: o número pode estar abaixo do real. Escolha um período menor."
          : metrica.usaPeriodo
            ? null
            : "Foto de agora: o período não muda este número.",
    };
  } catch (erro) {
    if (erro instanceof MetricaSemPermissao) return { ...base, erro: erro.message };
    console.error("[paineis] quadro", widget.id, widget.metrica, erro instanceof Error ? erro.message : erro);
    /* O detalhe (do banco) fica no log; a tela recebe só o que dá para entender. */
    return { ...base, erro: "Não foi possível calcular este quadro agora. Tente de novo em instantes." };
  }
}

/** Calcula os quadros três de cada vez, para não abrir conexões demais. */
export async function calcularWidgets(fonte: Fonte, widgets: Widget[]): Promise<Record<string, ResultadoDoWidget>> {
  const saida: Record<string, ResultadoDoWidget> = {};
  const fila = [...widgets];
  const trabalhar = async () => {
    for (let w = fila.shift(); w; w = fila.shift()) saida[w.id] = await calcularWidget(fonte, w);
  };
  await Promise.all([trabalhar(), trabalhar(), trabalhar()]);
  return saida;
}

/* ------------------------------------------------------------- guardar */

export type PainelGravado = {
  id: string;
  company_id: string;
  nome: string;
  descricao: string | null;
  widgets: Widget[];
  posicao: number;
  criado_por: string | null;
  criado_em: string;
  atualizado_em: string;
};

export class ErroDoPainel extends Error {
  status: number;
  constructor(status: number, mensagem: string) {
    super(mensagem);
    this.status = status;
  }
}

const CAMPOS = "id,company_id,nome,descricao,widgets,posicao,criado_por,criado_em,atualizado_em";

function lerPainel(linha: Record<string, unknown>): PainelGravado {
  return {
    id: String(linha.id),
    company_id: String(linha.company_id),
    nome: String(linha.nome ?? ""),
    descricao: (linha.descricao as string | null) ?? null,
    widgets: lerWidgetsGravados(linha.widgets),
    posicao: Number(linha.posicao ?? 0),
    criado_por: (linha.criado_por as string | null) ?? null,
    criado_em: String(linha.criado_em ?? ""),
    atualizado_em: String(linha.atualizado_em ?? ""),
  };
}

function erroDoBanco(oQue: string, erro: { message?: string; code?: string }): never {
  console.error(`[paineis] não foi possível ${oQue}:`, erro.message);
  /* 42501: a trava do banco recusou (sem permissão). */
  if (erro.code === "42501") throw new ErroDoPainel(403, "Sem permissão para mexer nos painéis.");
  throw new ErroDoPainel(500, `Não foi possível ${oQue} agora. Tente de novo em instantes.`);
}

export async function listarPaineis(db: SupabaseClient, empresaId: string): Promise<PainelGravado[]> {
  const { data, error } = await db.from("paineis").select(CAMPOS).eq("company_id", empresaId).order("posicao").order("criado_em").limit(200);
  if (error) erroDoBanco("listar os painéis", error);
  return ((data ?? []) as Record<string, unknown>[]).map(lerPainel);
}

export async function buscarPainel(db: SupabaseClient, empresaId: string, id: string): Promise<PainelGravado> {
  if (!/^[0-9a-f-]{36}$/i.test(id)) throw new ErroDoPainel(404, "Painel não encontrado.");
  const { data, error } = await db.from("paineis").select(CAMPOS).eq("company_id", empresaId).eq("id", id).maybeSingle();
  if (error) erroDoBanco("abrir o painel", error);
  if (!data) throw new ErroDoPainel(404, "Painel não encontrado.");
  return lerPainel(data as Record<string, unknown>);
}

export type DadosDoPainel = { nome?: unknown; descricao?: unknown; widgets?: unknown; posicao?: unknown };

export async function criarPainel(db: SupabaseClient, empresaId: string, usuarioId: string, dados: DadosDoPainel): Promise<PainelGravado> {
  const nome = conferirNome(dados.nome);
  if (!nome.ok) throw new ErroDoPainel(400, nome.erro);
  const widgets = conferirWidgets(dados.widgets ?? []);
  if (!widgets.ok) throw new ErroDoPainel(400, widgets.erro);

  const { count } = await db.from("paineis").select("id", { count: "exact", head: true }).eq("company_id", empresaId);
  if ((count ?? 0) >= 100) throw new ErroDoPainel(400, "A empresa já tem 100 painéis. Exclua algum antes de criar outro.");

  const { data, error } = await db
    .from("paineis")
    .insert({
      company_id: empresaId,
      nome: nome.valor,
      descricao: conferirDescricao(dados.descricao),
      widgets: widgets.valor,
      posicao: count ?? 0,
      criado_por: usuarioId,
    })
    .select(CAMPOS)
    .single();
  if (error) erroDoBanco("criar o painel", error);
  return lerPainel(data as Record<string, unknown>);
}

export async function alterarPainel(db: SupabaseClient, empresaId: string, id: string, dados: DadosDoPainel): Promise<PainelGravado> {
  await buscarPainel(db, empresaId, id);
  const mudancas: Record<string, unknown> = { atualizado_em: new Date().toISOString() };
  if (dados.nome !== undefined) {
    const nome = conferirNome(dados.nome);
    if (!nome.ok) throw new ErroDoPainel(400, nome.erro);
    mudancas.nome = nome.valor;
  }
  if (dados.descricao !== undefined) mudancas.descricao = conferirDescricao(dados.descricao);
  if (dados.widgets !== undefined) {
    const widgets = conferirWidgets(dados.widgets);
    if (!widgets.ok) throw new ErroDoPainel(400, widgets.erro);
    mudancas.widgets = widgets.valor;
  }
  if (dados.posicao !== undefined) {
    const posicao = Number(dados.posicao);
    if (!Number.isInteger(posicao) || posicao < 0 || posicao > 10_000) throw new ErroDoPainel(400, "Posição inválida.");
    mudancas.posicao = posicao;
  }
  const { data, error } = await db.from("paineis").update(mudancas).eq("company_id", empresaId).eq("id", id).select(CAMPOS).maybeSingle();
  if (error) erroDoBanco("alterar o painel", error);
  if (!data) throw new ErroDoPainel(403, "Sem permissão para alterar este painel.");
  return lerPainel(data as Record<string, unknown>);
}

export async function excluirPainel(db: SupabaseClient, empresaId: string, id: string): Promise<void> {
  await buscarPainel(db, empresaId, id);
  const { data, error } = await db.from("paineis").delete().eq("company_id", empresaId).eq("id", id).select("id");
  if (error) erroDoBanco("excluir o painel", error);
  if (!data?.length) throw new ErroDoPainel(403, "Sem permissão para excluir este painel.");
}
