/**
 * O catálogo fechado de métricas dos painéis.
 *
 * Quem monta um painel (a pessoa na tela, ou o assistente) só escolhe uma
 * métrica DESTA lista, um agrupamento que ela aceita e um período. Nenhum
 * texto vira consulta: cada métrica tem a sua leitura escrita aqui, sempre
 * presa às contas da empresa (`tenantIds`), e a conta é feita em
 * `agregar.ts`.
 *
 * As regras de "o que é venda" são as do relatório comercial
 * (lib/reports/service.ts): pedido cancelado ou rascunho não conta, e a data
 * da venda é a da criação do pedido.
 *
 * Este arquivo só importa por caminho relativo e tipos: o catálogo é usado
 * pelos testes (node --test) e pela tela.
 */

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

import {
  agregar,
  canalDaVenda,
  CANAIS,
  etapaDoPedido,
  lerColunasDoQuadro,
  pedidoConta,
  pedidoEmAberto,
  rotuloDoCliente,
  situacaoDoVencimento,
  SITUACAO_DO_PEDIDO,
  valorDoCanal,
  type Agregado,
  type Canal,
  type Dimensao,
  type Linha,
  type Metodo,
  type Valor,
} from "./agregar.ts";
import { diaNoBrasil, inicioDoDia, somarDias, type Intervalo } from "./periodos.ts";

export const TIPOS_DE_WIDGET = ["numero", "barras", "linha", "pizza", "tabela"] as const;
export type TipoDeWidget = (typeof TIPOS_DE_WIDGET)[number];

export const ROTULO_DO_TIPO: Record<TipoDeWidget, string> = {
  numero: "Número",
  barras: "Barras",
  linha: "Linha no tempo",
  pizza: "Pizza",
  tabela: "Tabela",
};

export type Formato = "dinheiro" | "inteiro" | "percentual";

export const FILTROS = ["canal", "vendedor"] as const;
export type NomeDoFiltro = (typeof FILTROS)[number];
export type Filtros = { canal?: Canal | null; vendedor?: string | null };

/** O que a métrica precisa para ser lida. */
export type Fonte = {
  /* O banco que lê: a chave de serviço, como no relatório (na tela e no
     assistente). Por isso TODA leitura daqui filtra por `tenantIds` (ou pelo
     dono), e a permissão é conferida antes de ler. */
  db: SupabaseClient;
  tenantIds: string[];
  companyId: string;
  donoId: string;
  hoje: string;
  pode: (permissao: string) => boolean;
  /* Leituras repetidas dentro do mesmo painel (vários quadros de venda no
     mesmo período) saem daqui em vez de irem de novo ao banco. */
  cache: Map<string, Promise<unknown>>;
};

export type Leitura = { linhas: Linha[]; cortado: boolean };

export type Coluna = { id: string; rotulo: string; formato: Formato };

export type DefinicaoDeMetrica = {
  id: string;
  rotulo: string;
  descricao: string;
  formato: Formato;
  metodo: Metodo;
  dimensoes: Dimensao[];
  agruparPadrao: Dimensao | null;
  /* false: é uma foto de agora (o que está em aberto); o período não muda nada. */
  usaPeriodo: boolean;
  filtros: NomeDoFiltro[];
  /* Além de "reports.read", que vale para qualquer painel. */
  permissoes: string[];
  tipos: TipoDeWidget[];
  tipoSugerido: TipoDeWidget;
  /* Colunas a mais da tabela (ex.: quantidade, entradas e saídas). */
  extras?: Coluna[];
  ler: (fonte: Fonte, intervalo: Intervalo) => Promise<Leitura>;
};

/* ------------------------------------------------------------ leitura base */

const TAMANHO_DA_PAGINA = 1000;
const TETO_DE_LINHAS = 20_000;
const PEDACO_DE_IDS = 150;

type Pagina = (de: number, ate: number) => PromiseLike<{ data: unknown; error: { message?: string } | null }>;

async function lerTudo<T>(pagina: Pagina, oQue: string, teto = TETO_DE_LINHAS): Promise<{ linhas: T[]; cortado: boolean }> {
  const linhas: T[] = [];
  for (let de = 0; de < teto; de += TAMANHO_DA_PAGINA) {
    const { data, error } = await pagina(de, de + TAMANHO_DA_PAGINA - 1);
    if (error) {
      console.error(`[paineis] não foi possível ler ${oQue}:`, error.message);
      throw new Error(`Não foi possível ler ${oQue} agora.`);
    }
    const lote = (data as T[] | null) ?? [];
    linhas.push(...lote);
    if (lote.length < TAMANHO_DA_PAGINA) return { linhas, cortado: false };
  }
  return { linhas, cortado: true };
}

/** Busca por uma lista de ids, em pedaços (a lista vai no endereço do pedido ao banco). */
async function lerPorIds<T>(ids: string[], ler: (pedaco: string[]) => PromiseLike<{ data: unknown; error: { message?: string } | null }>, oQue: string): Promise<T[]> {
  const unicos = [...new Set(ids.filter(Boolean))];
  const saida: T[] = [];
  for (let i = 0; i < unicos.length; i += PEDACO_DE_IDS) {
    const { data, error } = await ler(unicos.slice(i, i + PEDACO_DE_IDS));
    if (error) {
      console.error(`[paineis] não foi possível ler ${oQue}:`, error.message);
      throw new Error(`Não foi possível ler ${oQue} agora.`);
    }
    saida.push(...((data as T[] | null) ?? []));
  }
  return saida;
}

function lembrar<T>(fonte: Fonte, chave: string, ler: () => Promise<T>): Promise<T> {
  const guardado = fonte.cache.get(chave);
  if (guardado) return guardado as Promise<T>;
  const promessa = ler();
  fonte.cache.set(chave, promessa);
  /* Uma leitura que falhou não fica guardada: o próximo quadro tenta de novo. */
  promessa.catch(() => fonte.cache.delete(chave));
  return promessa;
}

function numero(valor: unknown) {
  const n = Number(valor);
  return Number.isFinite(n) ? n : 0;
}

/* ----------------------------------------------------------- vendedores */

async function nomesDosVendedores(fonte: Fonte): Promise<Map<string, string>> {
  return lembrar(fonte, "vendedores", async () => {
    const nomes = new Map<string, string>();
    const { data: membros } = await fonte.db.from("company_users").select("user_id,email").eq("company_id", fonte.companyId);
    const ids = new Set<string>([fonte.donoId]);
    for (const membro of (membros ?? []) as Array<{ user_id: string | null; email: string | null }>) {
      if (!membro.user_id) continue;
      ids.add(membro.user_id);
      if (membro.email) nomes.set(membro.user_id, membro.email.split("@")[0]);
    }
    const { data: perfis } = await fonte.db.from("employee_profiles").select("user_id,full_name").in("user_id", [...ids]);
    for (const perfil of (perfis ?? []) as Array<{ user_id: string | null; full_name: string | null }>) {
      if (perfil.user_id && perfil.full_name?.trim()) nomes.set(perfil.user_id, perfil.full_name.trim());
    }
    if (!nomes.has(fonte.donoId)) nomes.set(fonte.donoId, "Dono da conta");
    return nomes;
  });
}

function valorDoVendedor(id: string | null, nomes: Map<string, string>): Valor {
  if (!id) return { chave: "sem_vendedor", rotulo: "Sem vendedor" };
  return { chave: id, rotulo: nomes.get(id) ?? "Vendedor" };
}

async function nomesDosClientes(fonte: Fonte, ids: string[]) {
  const clientes = await lerPorIds<{ id: string; name: string | null; company_name: string | null }>(
    ids,
    (pedaco) => fonte.db.from("clients").select("id,name,company_name").in("id", pedaco).in("user_id", fonte.tenantIds),
    "os clientes"
  );
  return new Map(clientes.map((cliente) => [cliente.id, rotuloDoCliente(cliente)]));
}

function valorDoClienteDe(id: string | null, nomes: Map<string, string>): Valor {
  if (!id) return { chave: "sem_cliente", rotulo: "Sem cliente" };
  return { chave: id, rotulo: nomes.get(id) ?? "Cliente" };
}

/* --------------------------------------------------------------- pedidos */

type PedidoDoBanco = {
  id: string;
  number: number | null;
  status: string | null;
  total_amount: number | null;
  paid_amount: number | null;
  financial_status: string | null;
  created_at: string | null;
  delivery_date: string | null;
  client_id: string | null;
  quote_id: string | null;
  primary_seller_user_id: string | null;
  internal_notes: string | null;
  production_stage_id: string | null;
};

type OrcamentoDoPedido = { id: string; title: string | null; internal_notes: string | null; primary_seller_user_id: string | null };

export type Venda = {
  id: string;
  dia: string | null;
  valor: number;
  status: string;
  dims: Partial<Record<Dimensao, Valor>>;
};

const CAMPOS_DO_PEDIDO =
  "id,number,status,total_amount,paid_amount,financial_status,created_at,delivery_date,client_id,quote_id,primary_seller_user_id,internal_notes,production_stage_id";

async function traduzirPedidos(fonte: Fonte, pedidos: PedidoDoBanco[]): Promise<Venda[]> {
  const [orcamentos, vendedores, clientes, colunas] = await Promise.all([
    lerPorIds<OrcamentoDoPedido>(
      pedidos.map((p) => p.quote_id).filter((id): id is string => Boolean(id)),
      (pedaco) => fonte.db.from("quotes").select("id,title,internal_notes,primary_seller_user_id").in("id", pedaco).in("user_id", fonte.tenantIds),
      "os orçamentos dos pedidos"
    ),
    nomesDosVendedores(fonte),
    nomesDosClientes(fonte, pedidos.map((p) => p.client_id).filter((id): id is string => Boolean(id))),
    colunasDoQuadro(fonte),
  ]);
  const porId = new Map(orcamentos.map((o) => [o.id, o]));

  return pedidos.map((pedido) => {
    const orcamento = pedido.quote_id ? porId.get(pedido.quote_id) : undefined;
    const canal = canalDaVenda({ tituloDoOrcamento: orcamento?.title, notasDoOrcamento: orcamento?.internal_notes, notasDoPedido: pedido.internal_notes });
    const vendedor = pedido.primary_seller_user_id || orcamento?.primary_seller_user_id || null;
    const status = String(pedido.status || "pending_production");
    return {
      id: pedido.id,
      dia: diaNoBrasil(pedido.created_at),
      valor: numero(pedido.total_amount),
      status,
      dims: {
        canal: valorDoCanal(canal),
        vendedor: valorDoVendedor(vendedor, vendedores),
        cliente: valorDoClienteDe(pedido.client_id, clientes),
        situacao: { chave: status, rotulo: SITUACAO_DO_PEDIDO[status] ?? status },
        etapa: etapaDoPedido(pedido, colunas),
      },
    };
  });
}

/** As vendas do período: pedidos criados nele, sem cancelados nem rascunhos. */
function lerVendas(fonte: Fonte, intervalo: Intervalo) {
  return lembrar(fonte, `vendas:${intervalo.de}:${intervalo.ate}`, async () => {
    const { linhas, cortado } = await lerTudo<PedidoDoBanco>(
      (de, ate) =>
        fonte.db
          .from("orders")
          .select(CAMPOS_DO_PEDIDO)
          .in("user_id", fonte.tenantIds)
          .gte("created_at", inicioDoDia(intervalo.de))
          .lt("created_at", inicioDoDia(somarDias(intervalo.ate, 1)))
          .order("created_at", { ascending: true })
          .order("id", { ascending: true })
          .range(de, ate),
      "os pedidos"
    );
    const validos = linhas.filter((p) => pedidoConta(p.status));
    return { vendas: await traduzirPedidos(fonte, validos), pedidos: validos, cortado };
  });
}

/** Pedidos que ainda andam (nem entregues nem cancelados), de qualquer data. */
function lerPedidosEmAberto(fonte: Fonte) {
  return lembrar(fonte, "pedidos-em-aberto", async () => {
    const { linhas, cortado } = await lerTudo<PedidoDoBanco>(
      (de, ate) =>
        fonte.db
          .from("orders")
          .select(CAMPOS_DO_PEDIDO)
          .in("user_id", fonte.tenantIds)
          .in("status", ["pending_production", "in_production", "finished"])
          .order("created_at", { ascending: true })
          .order("id", { ascending: true })
          .range(de, ate),
      "os pedidos em aberto",
      10_000
    );
    const abertos = linhas.filter((p) => pedidoEmAberto(p.status));
    return { vendas: await traduzirPedidos(fonte, abertos), pedidos: abertos, cortado };
  });
}

function colunasDoQuadro(fonte: Fonte) {
  return lembrar(fonte, "colunas", async () => {
    const { data } = await fonte.db.from("profiles").select("production_kanban_columns").eq("id", fonte.donoId).maybeSingle();
    return lerColunasDoQuadro((data as { production_kanban_columns?: unknown } | null)?.production_kanban_columns);
  });
}

function linhasDeVenda(vendas: Venda[], conta: (venda: Venda) => Pick<Linha, "num" | "den">): Linha[] {
  return vendas.map((venda) => ({ dia: venda.dia, dims: venda.dims, ...conta(venda) }));
}

/* ------------------------------------------------------------ orçamentos */

type OrcamentoDoBanco = {
  id: string;
  status: string | null;
  title: string | null;
  internal_notes: string | null;
  created_at: string | null;
  primary_seller_user_id: string | null;
};

const SITUACAO_DO_ORCAMENTO: Record<string, string> = {
  draft: "Rascunho",
  sent: "Enviado",
  approved: "Aprovado",
  rejected: "Recusado",
  production: "Em produção",
  delivered: "Entregue",
};

/**
 * Os orçamentos criados no período, sem as vendas rápidas do PDV: o PDV cria
 * um orçamento e o pedido no mesmo instante, e contá-los como orçamento
 * inflaria a taxa de conversão sem ninguém ter negociado nada.
 */
function lerOrcamentos(fonte: Fonte, intervalo: Intervalo) {
  return lembrar(fonte, `orcamentos:${intervalo.de}:${intervalo.ate}`, async () => {
    const { linhas, cortado } = await lerTudo<OrcamentoDoBanco>(
      (de, ate) =>
        fonte.db
          .from("quotes")
          .select("id,status,title,internal_notes,created_at,primary_seller_user_id")
          .in("user_id", fonte.tenantIds)
          .gte("created_at", inicioDoDia(intervalo.de))
          .lt("created_at", inicioDoDia(somarDias(intervalo.ate, 1)))
          .order("created_at", { ascending: true })
          .order("id", { ascending: true })
          .range(de, ate),
      "os orçamentos"
    );
    const orcamentos = linhas.filter((o) => canalDaVenda({ tituloDoOrcamento: o.title, notasDoOrcamento: o.internal_notes }) !== "pdv");

    const [pedidos, vendedores] = await Promise.all([
      lerPorIds<{ quote_id: string | null; status: string | null }>(
        orcamentos.map((o) => o.id),
        (pedaco) => fonte.db.from("orders").select("quote_id,status").in("quote_id", pedaco).in("user_id", fonte.tenantIds),
        "os pedidos dos orçamentos"
      ),
      nomesDosVendedores(fonte),
    ]);
    const viraram = new Set(pedidos.filter((p) => p.quote_id && pedidoConta(p.status)).map((p) => String(p.quote_id)));

    const resultado = orcamentos.map((o) => {
      const status = String(o.status || "draft");
      return {
        dia: diaNoBrasil(o.created_at),
        virou: viraram.has(o.id),
        dims: {
          canal: valorDoCanal(canalDaVenda({ tituloDoOrcamento: o.title, notasDoOrcamento: o.internal_notes })),
          vendedor: valorDoVendedor(o.primary_seller_user_id, vendedores),
          situacao: { chave: status, rotulo: SITUACAO_DO_ORCAMENTO[status] ?? status },
        } as Partial<Record<Dimensao, Valor>>,
      };
    });
    return { orcamentos: resultado, cortado };
  });
}

/* ------------------------------------------------------------- definições */

const VENDAS: Dimensao[] = ["tempo", "vendedor", "canal", "cliente"];
const TODOS_OS_TIPOS: TipoDeWidget[] = [...TIPOS_DE_WIDGET];
const SEM_LINHA: TipoDeWidget[] = ["numero", "barras", "pizza", "tabela"];

const ORIGEM_DO_CAIXA: Record<string, string> = {
  PEDIDO: "Pedidos",
  PAGAR: "Contas pagas",
  RECEBER: "Contas recebidas",
  AJUSTE: "Ajustes e transferências",
};

export const METRICAS: DefinicaoDeMetrica[] = [
  {
    id: "faturamento",
    rotulo: "Faturamento",
    descricao: "Soma dos pedidos criados no período, sem cancelados. É o valor vendido, não o que já entrou no caixa.",
    formato: "dinheiro",
    metodo: "soma",
    dimensoes: VENDAS,
    agruparPadrao: "tempo",
    usaPeriodo: true,
    filtros: ["canal", "vendedor"],
    permissoes: [],
    tipos: TODOS_OS_TIPOS,
    tipoSugerido: "numero",
    async ler(fonte, intervalo) {
      const { vendas, cortado } = await lerVendas(fonte, intervalo);
      return { linhas: linhasDeVenda(vendas, (v) => ({ num: v.valor })), cortado };
    },
  },
  {
    id: "quantidade_de_pedidos",
    rotulo: "Pedidos",
    descricao: "Quantos pedidos foram criados no período, sem cancelados.",
    formato: "inteiro",
    metodo: "soma",
    dimensoes: [...VENDAS, "situacao"],
    agruparPadrao: "tempo",
    usaPeriodo: true,
    filtros: ["canal", "vendedor"],
    permissoes: [],
    tipos: TODOS_OS_TIPOS,
    tipoSugerido: "numero",
    async ler(fonte, intervalo) {
      const { vendas, cortado } = await lerVendas(fonte, intervalo);
      return { linhas: linhasDeVenda(vendas, () => ({ num: 1 })), cortado };
    },
  },
  {
    id: "ticket_medio",
    rotulo: "Ticket médio",
    descricao: "Valor médio de cada pedido do período (faturamento dividido pelo número de pedidos), sem cancelados.",
    formato: "dinheiro",
    metodo: "razao",
    dimensoes: VENDAS,
    agruparPadrao: "tempo",
    usaPeriodo: true,
    filtros: ["canal", "vendedor"],
    permissoes: [],
    tipos: ["numero", "barras", "linha", "tabela"],
    tipoSugerido: "numero",
    async ler(fonte, intervalo) {
      const { vendas, cortado } = await lerVendas(fonte, intervalo);
      return { linhas: linhasDeVenda(vendas, (v) => ({ num: v.valor, den: 1 })), cortado };
    },
  },
  {
    id: "vendas_por_vendedor",
    rotulo: "Vendas por vendedor",
    descricao: "Faturamento do período dividido pelo vendedor do pedido (ou do orçamento que o originou).",
    formato: "dinheiro",
    metodo: "soma",
    dimensoes: ["vendedor"],
    agruparPadrao: "vendedor",
    usaPeriodo: true,
    filtros: ["canal"],
    permissoes: [],
    tipos: ["barras", "pizza", "tabela", "numero"],
    tipoSugerido: "barras",
    extras: [{ id: "pedidos", rotulo: "Pedidos", formato: "inteiro" }],
    async ler(fonte, intervalo) {
      const { vendas, cortado } = await lerVendas(fonte, intervalo);
      return { linhas: vendas.map((v) => ({ dia: v.dia, dims: v.dims, num: v.valor, extra: { pedidos: 1 } })), cortado };
    },
  },
  {
    id: "vendas_por_canal",
    rotulo: "Vendas por canal",
    descricao: `Faturamento do período por canal de entrada: ${Object.values(CANAIS).join(", ")}. Vendas lançadas pela equipe (balcão, WhatsApp, telefone) aparecem juntas como "${CANAIS.painel}", porque o sistema não separa esses.`,
    formato: "dinheiro",
    metodo: "soma",
    dimensoes: ["canal"],
    agruparPadrao: "canal",
    usaPeriodo: true,
    filtros: ["vendedor"],
    permissoes: [],
    tipos: ["barras", "pizza", "tabela", "numero"],
    tipoSugerido: "pizza",
    extras: [{ id: "pedidos", rotulo: "Pedidos", formato: "inteiro" }],
    async ler(fonte, intervalo) {
      const { vendas, cortado } = await lerVendas(fonte, intervalo);
      return { linhas: vendas.map((v) => ({ dia: v.dia, dims: v.dims, num: v.valor, extra: { pedidos: 1 } })), cortado };
    },
  },
  {
    id: "produtos_mais_vendidos",
    rotulo: "Produtos mais vendidos",
    descricao: "Os produtos que mais faturaram nos pedidos do período (sem cancelados), com a quantidade vendida.",
    formato: "dinheiro",
    metodo: "soma",
    dimensoes: ["produto"],
    agruparPadrao: "produto",
    usaPeriodo: true,
    filtros: ["canal", "vendedor"],
    permissoes: [],
    tipos: ["barras", "pizza", "tabela", "numero"],
    tipoSugerido: "tabela",
    extras: [{ id: "quantidade", rotulo: "Quantidade", formato: "inteiro" }],
    async ler(fonte, intervalo) {
      const { vendas, cortado } = await lerVendas(fonte, intervalo);
      const porPedido = new Map(vendas.map((v) => [v.id, v]));
      const itens = await lerPorIds<{ order_id: string; product_id: string | null; title: string | null; description: string | null; quantity: number | null; total_price: number | null }>(
        vendas.map((v) => v.id),
        (pedaco) => fonte.db.from("order_items").select("order_id,product_id,title,description,quantity,total_price").in("order_id", pedaco),
        "os itens dos pedidos"
      );
      const produtos = await lerPorIds<{ id: string; name: string | null }>(
        itens.map((i) => i.product_id).filter((id): id is string => Boolean(id)),
        (pedaco) => fonte.db.from("products").select("id,name").in("id", pedaco).in("user_id", fonte.tenantIds),
        "os produtos"
      );
      const nomes = new Map(produtos.map((p) => [p.id, p.name?.trim() || "Produto"]));
      const linhas: Linha[] = itens.map((item) => {
        const venda = porPedido.get(item.order_id);
        const nomeSolto = (item.title || item.description || "Item avulso").trim().slice(0, 80);
        const produto: Valor = item.product_id
          ? { chave: item.product_id, rotulo: nomes.get(item.product_id) ?? nomeSolto }
          : { chave: `avulso:${nomeSolto.toLowerCase()}`, rotulo: nomeSolto };
        return { dia: venda?.dia ?? null, dims: { ...(venda?.dims ?? {}), produto }, num: numero(item.total_price), extra: { quantidade: numero(item.quantity) } };
      });
      return { linhas, cortado };
    },
  },
  {
    id: "orcamentos_criados",
    rotulo: "Orçamentos criados",
    descricao: "Quantos orçamentos foram criados no período. As vendas rápidas do PDV não contam como orçamento.",
    formato: "inteiro",
    metodo: "soma",
    dimensoes: ["tempo", "vendedor", "canal", "situacao"],
    agruparPadrao: "tempo",
    usaPeriodo: true,
    filtros: ["canal", "vendedor"],
    permissoes: [],
    tipos: TODOS_OS_TIPOS,
    tipoSugerido: "numero",
    async ler(fonte, intervalo) {
      const { orcamentos, cortado } = await lerOrcamentos(fonte, intervalo);
      return { linhas: orcamentos.map((o) => ({ dia: o.dia, dims: o.dims, num: 1 })), cortado };
    },
  },
  {
    id: "taxa_de_conversao",
    rotulo: "Conversão de orçamentos",
    descricao: "De cada 100 orçamentos criados no período, quantos viraram pedido (em %). Pedido cancelado não conta. Sem as vendas rápidas do PDV.",
    formato: "percentual",
    metodo: "percentual",
    dimensoes: ["tempo", "vendedor", "canal"],
    agruparPadrao: "canal",
    usaPeriodo: true,
    filtros: ["canal", "vendedor"],
    permissoes: [],
    tipos: ["numero", "barras", "linha", "tabela"],
    tipoSugerido: "numero",
    extras: [
      { id: "orcamentos", rotulo: "Orçamentos", formato: "inteiro" },
      { id: "viraram_pedido", rotulo: "Viraram pedido", formato: "inteiro" },
    ],
    async ler(fonte, intervalo) {
      const { orcamentos, cortado } = await lerOrcamentos(fonte, intervalo);
      return {
        linhas: orcamentos.map((o) => ({ dia: o.dia, dims: o.dims, num: o.virou ? 1 : 0, den: 1, extra: { orcamentos: 1, viraram_pedido: o.virou ? 1 : 0 } })),
        cortado,
      };
    },
  },
  {
    id: "clientes_novos",
    rotulo: "Clientes novos",
    descricao: "Quantos clientes foram cadastrados no período (pela equipe, pela loja ou pelo PDV).",
    formato: "inteiro",
    metodo: "soma",
    dimensoes: ["tempo", "origem"],
    agruparPadrao: "tempo",
    usaPeriodo: true,
    filtros: [],
    permissoes: [],
    tipos: TODOS_OS_TIPOS,
    tipoSugerido: "numero",
    async ler(fonte, intervalo) {
      const { linhas, cortado } = await lerTudo<{ created_at: string | null; origin: string | null; is_deleted: boolean | null; is_system_placeholder: boolean | null }>(
        (de, ate) =>
          fonte.db
            .from("clients")
            .select("created_at,origin,is_deleted,is_system_placeholder")
            .in("user_id", fonte.tenantIds)
            .gte("created_at", inicioDoDia(intervalo.de))
            .lt("created_at", inicioDoDia(somarDias(intervalo.ate, 1)))
            .order("created_at", { ascending: true })
            .range(de, ate),
        "os clientes"
      );
      return {
        linhas: linhas
          .filter((c) => c.is_deleted !== true && c.is_system_placeholder !== true)
          .map((c) => {
            const origem = (c.origin || "").trim();
            return { dia: diaNoBrasil(c.created_at), dims: { origem: origem ? { chave: origem.toLowerCase(), rotulo: rotuloDaOrigemDoCliente(origem) } : { chave: "painel", rotulo: "Cadastro no painel" } }, num: 1 };
          }),
        cortado,
      };
    },
  },
  {
    id: "pedidos_por_etapa",
    rotulo: "Pedidos por etapa da produção",
    descricao: "Foto de agora: quantos pedidos ainda não entregues estão em cada etapa do quadro da produção. O período não muda este número.",
    formato: "inteiro",
    metodo: "soma",
    dimensoes: ["etapa", "situacao", "vendedor", "cliente"],
    agruparPadrao: "etapa",
    usaPeriodo: false,
    filtros: ["canal", "vendedor"],
    permissoes: [],
    tipos: SEM_LINHA,
    tipoSugerido: "barras",
    extras: [{ id: "valor_dos_pedidos", rotulo: "Valor", formato: "dinheiro" }],
    async ler(fonte) {
      const { vendas, cortado } = await lerPedidosEmAberto(fonte);
      return { linhas: vendas.map((v) => ({ dia: v.dia, dims: v.dims, num: 1, extra: { valor_dos_pedidos: v.valor } })), cortado };
    },
  },
  {
    id: "pedidos_atrasados",
    rotulo: "Pedidos atrasados",
    descricao: "Foto de agora: pedidos com a data de entrega já passada e que ainda não foram entregues (nem cancelados). O período não muda este número.",
    formato: "inteiro",
    metodo: "soma",
    dimensoes: ["etapa", "situacao", "vendedor", "cliente"],
    agruparPadrao: "etapa",
    usaPeriodo: false,
    filtros: ["canal", "vendedor"],
    permissoes: [],
    tipos: SEM_LINHA,
    tipoSugerido: "numero",
    extras: [{ id: "valor_dos_pedidos", rotulo: "Valor", formato: "dinheiro" }],
    async ler(fonte) {
      const { vendas, pedidos, cortado } = await lerPedidosEmAberto(fonte);
      const atrasados = new Set(pedidos.filter((p) => p.delivery_date && p.delivery_date < fonte.hoje).map((p) => p.id));
      return {
        linhas: vendas.filter((v) => atrasados.has(v.id)).map((v) => ({ dia: v.dia, dims: v.dims, num: 1, extra: { valor_dos_pedidos: v.valor } })),
        cortado,
      };
    },
  },
  {
    id: "contas_a_receber_em_aberto",
    rotulo: "Contas a receber em aberto",
    descricao: "Foto de agora: o que os clientes ainda devem. Soma o saldo dos pedidos não quitados (sem cancelados) e das contas a receber lançadas no financeiro. O período não muda este número.",
    formato: "dinheiro",
    metodo: "soma",
    dimensoes: ["origem", "situacao", "cliente"],
    agruparPadrao: "origem",
    usaPeriodo: false,
    filtros: [],
    permissoes: ["financial.receivables.read", "financial.amounts.see"],
    tipos: SEM_LINHA,
    tipoSugerido: "numero",
    async ler(fonte) {
      const [pedidos, contas] = await Promise.all([
        lerTudo<{ client_id: string | null; total_amount: number | null; paid_amount: number | null; created_at: string | null }>(
          (de, ate) =>
            fonte.db
              .from("orders")
              .select("client_id,total_amount,paid_amount,created_at")
              .in("user_id", fonte.tenantIds)
              .neq("status", "canceled")
              .neq("financial_status", "paid")
              .order("created_at", { ascending: false })
              .order("id", { ascending: true })
              .range(de, ate),
          "os pedidos em aberto",
          10_000
        ),
        lerTudo<{ due_date: string | null; total_amount: number | null; paid_amount: number | null; client_name: string | null; client_company: string | null; status: string | null }>(
          (de, ate) =>
            fonte.db
              .from("accounts_receivable")
              .select("due_date,total_amount,paid_amount,client_name,client_company,status")
              .in("company_id", fonte.tenantIds)
              .not("status", "in", "(PAID,CANCELED)")
              .order("due_date", { ascending: true })
              .order("id", { ascending: true })
              .range(de, ate),
          "as contas a receber",
          10_000
        ),
      ]);
      const abertos = pedidos.linhas
        .map((p) => ({ ...p, saldo: numero(p.total_amount) - numero(p.paid_amount) }))
        .filter((p) => p.saldo > 0.004);
      const clientes = await nomesDosClientes(fonte, abertos.map((p) => p.client_id).filter((id): id is string => Boolean(id)));
      const linhas: Linha[] = [
        ...abertos.map((p) => ({
          dia: diaNoBrasil(p.created_at),
          dims: {
            origem: { chave: "pedidos", rotulo: "Pedidos não quitados" },
            situacao: { chave: "pedido", rotulo: "Pedido sem parcela lançada" },
            cliente: valorDoClienteDe(p.client_id, clientes),
          },
          num: p.saldo,
        })),
        ...contas.linhas
          .map((c) => ({ ...c, saldo: numero(c.total_amount) - numero(c.paid_amount) }))
          .filter((c) => c.saldo > 0.004)
          .map((c) => {
            const nome = (c.client_company || c.client_name || "").trim();
            return {
              dia: c.due_date,
              dims: {
                origem: { chave: "contas", rotulo: "Contas a receber do financeiro" },
                situacao: situacaoDoVencimento(c.due_date, fonte.hoje),
                cliente: nome ? { chave: `nome:${nome.toLowerCase()}`, rotulo: nome } : { chave: "sem_cliente", rotulo: "Sem cliente" },
              },
              num: c.saldo,
            };
          }),
      ];
      return { linhas, cortado: pedidos.cortado || contas.cortado };
    },
  },
  {
    id: "contas_a_pagar_em_aberto",
    rotulo: "Contas a pagar em aberto",
    descricao: "Foto de agora: as contas a pagar pendentes e vencidas, de qualquer vencimento. O período não muda este número.",
    formato: "dinheiro",
    metodo: "soma",
    dimensoes: ["situacao", "fornecedor"],
    agruparPadrao: "situacao",
    usaPeriodo: false,
    filtros: [],
    permissoes: ["financial.payables.read", "financial.amounts.see"],
    tipos: SEM_LINHA,
    tipoSugerido: "numero",
    async ler(fonte) {
      const { linhas, cortado } = await lerTudo<{ amount: number | null; due_date: string | null; supplier_name: string | null; supplier_id: string | null }>(
        (de, ate) =>
          fonte.db
            .from("accounts_payable")
            .select("amount,due_date,supplier_name,supplier_id")
            .in("company_id", fonte.tenantIds)
            .in("status", ["PENDING", "OVERDUE"])
            .is("reversed_at", null)
            .order("due_date", { ascending: true })
            .order("id", { ascending: true })
            .range(de, ate),
        "as contas a pagar",
        10_000
      );
      return {
        linhas: linhas.map((c) => {
          const nome = (c.supplier_name || "").trim();
          return {
            dia: c.due_date,
            dims: {
              situacao: situacaoDoVencimento(c.due_date, fonte.hoje),
              fornecedor: nome ? { chave: c.supplier_id || `nome:${nome.toLowerCase()}`, rotulo: nome } : { chave: "sem_fornecedor", rotulo: "Sem fornecedor" },
            },
            num: numero(c.amount),
          };
        }),
        cortado,
      };
    },
  },
  {
    id: "fluxo_de_caixa",
    rotulo: "Fluxo de caixa",
    descricao: "O que entrou menos o que saiu do caixa no período (lançamentos do fluxo de caixa). Na tabela e no tempo, mostra também as entradas e as saídas.",
    formato: "dinheiro",
    metodo: "soma",
    dimensoes: ["tempo", "tipo", "origem"],
    agruparPadrao: "tempo",
    usaPeriodo: true,
    filtros: [],
    permissoes: ["financial.cashflow.read", "financial.amounts.see"],
    tipos: ["numero", "barras", "linha", "tabela"],
    tipoSugerido: "linha",
    extras: [
      { id: "entradas", rotulo: "Entradas", formato: "dinheiro" },
      { id: "saidas", rotulo: "Saídas", formato: "dinheiro" },
    ],
    async ler(fonte, intervalo) {
      const { linhas, cortado } = await lerTudo<{ date: string | null; type: string | null; origin: string | null; amount: number | null }>(
        (de, ate) =>
          fonte.db
            .from("cash_flow")
            .select("date,type,origin,amount")
            .in("company_id", fonte.tenantIds)
            .gte("date", intervalo.de)
            .lte("date", intervalo.ate)
            .order("date", { ascending: true })
            .order("id", { ascending: true })
            .range(de, ate),
        "o fluxo de caixa"
      );
      return {
        linhas: linhas.map((l) => {
          const entrada = String(l.type).toUpperCase() === "IN";
          const valor = Math.abs(numero(l.amount));
          const origem = String(l.origin || "").toUpperCase();
          return {
            dia: l.date,
            dims: {
              tipo: entrada ? { chave: "entradas", rotulo: "Entradas" } : { chave: "saidas", rotulo: "Saídas" },
              origem: { chave: origem || "outros", rotulo: ORIGEM_DO_CAIXA[origem] ?? "Outros" },
            },
            num: entrada ? valor : -valor,
            extra: entrada ? { entradas: valor, saidas: 0 } : { entradas: 0, saidas: valor },
          };
        }),
        cortado,
      };
    },
  },
];

function rotuloDaOrigemDoCliente(origem: string) {
  const o = origem.toLowerCase();
  if (o.includes("catalog") || o.includes("loja") || o.includes("store")) return "Loja online";
  if (o.includes("pdv")) return "Balcão (PDV)";
  if (o.includes("chat")) return "Chat";
  if (o.includes("portal")) return "Portal do cliente";
  if (o.includes("import")) return "Importação";
  if (o.includes("form")) return "Formulário público";
  return origem.slice(0, 40);
}

export type MetricaId = string;

export function metricaPorId(id: unknown): DefinicaoDeMetrica | null {
  return METRICAS.find((m) => m.id === id) ?? null;
}

export const IDS_DAS_METRICAS = METRICAS.map((m) => m.id);

/** O catálogo como o assistente e a tela leem: sem as funções. */
export function catalogoPublico() {
  return METRICAS.map((m) => ({
    id: m.id,
    rotulo: m.rotulo,
    descricao: m.descricao,
    formato: m.formato,
    agrupar_por: m.dimensoes,
    agrupar_padrao: m.agruparPadrao,
    usa_periodo: m.usaPeriodo,
    filtros: m.filtros,
    tipos: m.tipos,
    tipo_sugerido: m.tipoSugerido,
    precisa_de_permissao_do_financeiro: m.permissoes.length > 0,
  }));
}

/* ---------------------------------------------------------------- calcular */

export type Pedido = {
  intervalo: Intervalo;
  agruparPor: Dimensao | null;
  limite?: number | null;
  filtros?: Filtros;
};

export class MetricaSemPermissao extends Error {}

export function podeVerMetrica(metrica: DefinicaoDeMetrica, pode: (permissao: string) => boolean) {
  return metrica.permissoes.every((permissao) => pode(permissao));
}

function aplicarFiltros(linhas: Linha[], filtros: Filtros | undefined, permitidos: NomeDoFiltro[]) {
  if (!filtros) return linhas;
  let saida = linhas;
  if (filtros.canal && permitidos.includes("canal")) saida = saida.filter((l) => l.dims.canal?.chave === filtros.canal);
  if (filtros.vendedor && permitidos.includes("vendedor")) saida = saida.filter((l) => l.dims.vendedor?.chave === filtros.vendedor);
  return saida;
}

/** Lê e calcula uma métrica para a empresa da fonte, num intervalo. */
export async function calcularMetrica(fonte: Fonte, metrica: DefinicaoDeMetrica, pedido: Pedido): Promise<Agregado & { cortado: boolean }> {
  if (!podeVerMetrica(metrica, fonte.pode)) {
    throw new MetricaSemPermissao(`"${metrica.rotulo}" é do financeiro: esta pessoa não tem permissão para ver esses valores.`);
  }
  if (pedido.agruparPor && !metrica.dimensoes.includes(pedido.agruparPor)) {
    throw new Error(`"${metrica.rotulo}" não pode ser agrupada assim.`);
  }
  const { linhas, cortado } = await metrica.ler(fonte, pedido.intervalo);
  const filtradas = aplicarFiltros(linhas, pedido.filtros, metrica.filtros);
  const agregado = agregar(filtradas, metrica.metodo, pedido.agruparPor, {
    intervalo: metrica.usaPeriodo ? pedido.intervalo : undefined,
    limite: pedido.agruparPor === "tempo" ? null : pedido.limite ?? null,
  });
  return { ...agregado, cortado };
}

export function criarFonte(dados: Omit<Fonte, "cache">): Fonte {
  return { ...dados, cache: new Map() };
}
