/**
 * Os quadros de um painel: o formato guardado em `paineis.widgets` e a
 * conferência de tudo que chega da tela ou do assistente antes de gravar.
 *
 * Conferência rígida de propósito: um quadro com métrica que não existe, ou
 * agrupado de um jeito que a métrica não aceita, é recusado com a explicação,
 * para o assistente se corrigir na tentativa seguinte.
 */

import { DIMENSOES, ROTULO_DA_DIMENSAO, CANAIS, type Dimensao, type Canal } from "./agregar.ts";
import { metricaPorId, ROTULO_DO_TIPO, TIPOS_DE_WIDGET, IDS_DAS_METRICAS, type Filtros, type TipoDeWidget } from "./metricas.ts";
import { ehData, ehPeriodo, PERIODOS, diasEntre, DIAS_MAXIMOS_DO_PERIODO, type Periodo } from "./periodos.ts";

export type Widget = {
  id: string;
  titulo: string;
  metrica: string;
  tipo: TipoDeWidget;
  periodo: Periodo;
  de?: string | null;
  ate?: string | null;
  agrupar_por?: Dimensao | null;
  limite?: number | null;
  filtros?: Filtros | null;
};

export const MAXIMO_DE_WIDGETS = 24;
export const LIMITE_PADRAO = 10;
export const LIMITE_MAXIMO = 50;
const SO_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

export type Conferido<T> = { ok: true; valor: T } | { ok: false; erro: string };

function textoLimpo(valor: unknown, maximo: number) {
  if (typeof valor !== "string") return "";
  return valor.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, maximo);
}

export function novoIdDeWidget() {
  return `w_${Math.random().toString(36).slice(2, 10)}${Date.now().toString(36).slice(-4)}`;
}

/**
 * Confere um quadro e devolve a versão limpa. O que é opcional ganha o
 * padrão da métrica: o agrupamento sugerido para gráficos, nenhum para o
 * número, e "tempo" para a linha.
 */
export function conferirWidget(bruto: unknown, posicao = 0): Conferido<Widget> {
  const onde = `Quadro ${posicao + 1}`;
  if (!bruto || typeof bruto !== "object" || Array.isArray(bruto)) return { ok: false, erro: `${onde}: formato inválido.` };
  const w = bruto as Record<string, unknown>;

  const metrica = metricaPorId(w.metrica);
  if (!metrica) return { ok: false, erro: `${onde}: métrica "${String(w.metrica ?? "")}" não existe. Use uma destas: ${IDS_DAS_METRICAS.join(", ")}.` };

  const tipo = (w.tipo ?? metrica.tipoSugerido) as TipoDeWidget;
  if (!(TIPOS_DE_WIDGET as readonly string[]).includes(tipo)) return { ok: false, erro: `${onde}: tipo "${String(w.tipo)}" não existe. Use: ${TIPOS_DE_WIDGET.join(", ")}.` };
  if (!metrica.tipos.includes(tipo)) {
    return { ok: false, erro: `${onde}: "${metrica.rotulo}" não pode ser mostrada como ${ROTULO_DO_TIPO[tipo].toLowerCase()}. Tipos aceitos: ${metrica.tipos.join(", ")}.` };
  }

  const periodo = (w.periodo ?? "este_mes") as Periodo;
  if (!ehPeriodo(periodo)) return { ok: false, erro: `${onde}: período "${String(w.periodo)}" não existe. Use: ${PERIODOS.join(", ")}.` };
  let de: string | null = null;
  let ate: string | null = null;
  if (periodo === "personalizado") {
    if (!ehData(w.de) || !ehData(w.ate)) return { ok: false, erro: `${onde}: período personalizado precisa de "de" e "ate" no formato AAAA-MM-DD.` };
    if (w.ate < w.de) return { ok: false, erro: `${onde}: o fim do período é antes do início.` };
    if (diasEntre(w.de, w.ate) > DIAS_MAXIMOS_DO_PERIODO) return { ok: false, erro: `${onde}: período grande demais (no máximo dois anos).` };
    de = w.de;
    ate = w.ate;
  }

  let agruparPor: Dimensao | null = null;
  const pedido = w.agrupar_por === undefined || w.agrupar_por === null || w.agrupar_por === "" ? undefined : w.agrupar_por;
  if (pedido !== undefined) {
    if (!(DIMENSOES as readonly string[]).includes(String(pedido)) || !metrica.dimensoes.includes(pedido as Dimensao)) {
      return {
        ok: false,
        erro: `${onde}: "${metrica.rotulo}" não pode ser agrupada por "${String(pedido)}". Agrupamentos aceitos: ${metrica.dimensoes.join(", ") || "nenhum"}.`,
      };
    }
    agruparPor = pedido as Dimensao;
  }

  if (tipo === "numero") {
    agruparPor = null;
  } else if (tipo === "linha") {
    if (!metrica.dimensoes.includes("tempo")) return { ok: false, erro: `${onde}: "${metrica.rotulo}" não tem linha no tempo.` };
    if (agruparPor && agruparPor !== "tempo") return { ok: false, erro: `${onde}: o gráfico de linha é sempre ao longo do tempo; tire o agrupar_por ou use "tempo".` };
    agruparPor = "tempo";
  } else {
    /* Pizza não serve para o tempo: sem escolha, pega o primeiro agrupamento que não é tempo. */
    agruparPor =
      agruparPor ??
      (tipo === "pizza" && metrica.agruparPadrao === "tempo" ? metrica.dimensoes.find((d) => d !== "tempo") ?? null : metrica.agruparPadrao);
    if (!agruparPor) return { ok: false, erro: `${onde}: "${metrica.rotulo}" precisa de um agrupamento para virar ${ROTULO_DO_TIPO[tipo].toLowerCase()}.` };
    if (tipo === "pizza" && agruparPor === "tempo") return { ok: false, erro: `${onde}: pizza não serve para o tempo; use linha ou barras.` };
  }

  let limite: number | null = null;
  if (agruparPor && agruparPor !== "tempo") {
    const n = w.limite === undefined || w.limite === null || w.limite === "" ? LIMITE_PADRAO : Number(w.limite);
    if (!Number.isInteger(n) || n < 1 || n > LIMITE_MAXIMO) return { ok: false, erro: `${onde}: limite precisa ser um número inteiro de 1 a ${LIMITE_MAXIMO}.` };
    limite = n;
  }

  let filtros: Filtros | null = null;
  if (w.filtros !== undefined && w.filtros !== null) {
    if (typeof w.filtros !== "object" || Array.isArray(w.filtros)) return { ok: false, erro: `${onde}: filtros em formato inválido.` };
    const f = w.filtros as Record<string, unknown>;
    const limpos: Filtros = {};
    for (const nome of Object.keys(f)) {
      if (f[nome] === null || f[nome] === undefined || f[nome] === "") continue;
      if (!metrica.filtros.includes(nome as "canal" | "vendedor")) {
        return { ok: false, erro: `${onde}: "${metrica.rotulo}" não aceita o filtro "${nome}". Filtros aceitos: ${metrica.filtros.join(", ") || "nenhum"}.` };
      }
    }
    if (f.canal) {
      if (!(String(f.canal) in CANAIS)) return { ok: false, erro: `${onde}: canal "${String(f.canal)}" não existe. Use: ${Object.keys(CANAIS).join(", ")}.` };
      limpos.canal = String(f.canal) as Canal;
    }
    if (f.vendedor) {
      if (!SO_UUID.test(String(f.vendedor))) return { ok: false, erro: `${onde}: o filtro de vendedor precisa do id da pessoa.` };
      limpos.vendedor = String(f.vendedor);
    }
    filtros = Object.keys(limpos).length ? limpos : null;
  }

  const titulo = textoLimpo(w.titulo, 80) || tituloSugerido(metrica.rotulo, agruparPor);
  const id = typeof w.id === "string" && /^[\w-]{1,40}$/.test(w.id) ? w.id : novoIdDeWidget();

  return { ok: true, valor: { id, titulo, metrica: metrica.id, tipo, periodo, de, ate, agrupar_por: agruparPor, limite, filtros } };
}

export function tituloSugerido(rotulo: string, agruparPor: Dimensao | null) {
  /* "Vendas por canal" já diz como divide: não vira "Vendas por canal por canal de venda". */
  if (!agruparPor || agruparPor === "tempo" || / por /i.test(rotulo)) return rotulo;
  const dim = ROTULO_DA_DIMENSAO[agruparPor].toLowerCase();
  return rotulo.toLowerCase().includes(dim.replace(/^por /, "")) ? rotulo : `${rotulo} ${dim}`;
}

/** Confere a lista inteira; ids repetidos ganham um novo. */
export function conferirWidgets(bruto: unknown): Conferido<Widget[]> {
  if (!Array.isArray(bruto)) return { ok: false, erro: "Os quadros precisam vir numa lista." };
  if (bruto.length > MAXIMO_DE_WIDGETS) return { ok: false, erro: `Um painel tem no máximo ${MAXIMO_DE_WIDGETS} quadros.` };
  const saida: Widget[] = [];
  const vistos = new Set<string>();
  for (let i = 0; i < bruto.length; i++) {
    const conferido = conferirWidget(bruto[i], i);
    if (!conferido.ok) return conferido;
    const widget = conferido.valor;
    if (vistos.has(widget.id)) widget.id = novoIdDeWidget();
    vistos.add(widget.id);
    saida.push(widget);
  }
  return { ok: true, valor: saida };
}

/** Lê o que está gravado no banco, deixando de fora o que não confere mais (métrica retirada, por exemplo). */
export function lerWidgetsGravados(bruto: unknown): Widget[] {
  if (!Array.isArray(bruto)) return [];
  const saida: Widget[] = [];
  /* Gravado por fora da conferência (ou antes do teto), o painel ainda abre só com o máximo de quadros. */
  bruto.slice(0, MAXIMO_DE_WIDGETS).forEach((item, i) => {
    const conferido = conferirWidget(item, i);
    if (conferido.ok) saida.push(conferido.valor);
  });
  return saida;
}

export function conferirNome(valor: unknown): Conferido<string> {
  const nome = textoLimpo(valor, 80);
  if (!nome) return { ok: false, erro: "Dê um nome ao painel." };
  return { ok: true, valor: nome };
}

export function conferirDescricao(valor: unknown): string | null {
  return textoLimpo(valor, 400) || null;
}

/** Move um quadro de lugar (para cima: -1, para baixo: +1). */
export function moverWidget(widgets: Widget[], id: string, direcao: -1 | 1): Widget[] {
  const i = widgets.findIndex((w) => w.id === id);
  const j = i + direcao;
  if (i < 0 || j < 0 || j >= widgets.length) return widgets;
  const copia = [...widgets];
  [copia[i], copia[j]] = [copia[j], copia[i]];
  return copia;
}
