/**
 * Regras da empresa: "quando acontecer X, faça Y".
 *
 * Este arquivo é só a parte pura: o formato de uma regra, a conferência do que
 * chega (da tela ou do assistente), a frase que explica a regra em português e
 * a escolha de quais registros disparam a regra agora. Nada aqui vai ao banco:
 * quem consulta é `servidor/avaliar.ts`, quem age é `servidor/acoes.ts` e quem
 * roda de tempos em tempos é `servidor/fila.ts`.
 *
 * Sem importações com "@/": o teste roda no Node puro.
 *
 * Gatilhos são de estado e de tempo, conferidos a cada 5 minutos. Ações são só
 * as seguras: nada vai ao cliente, nada mexe em dinheiro.
 */

/* ------------------------------------------------------------------ formato */

export const TIPOS_DE_GATILHO = [
  "orcamento_sem_resposta",
  "pedido_parado_na_etapa",
  "pedido_entrou_na_etapa",
  "estoque_abaixo",
  "cliente_sem_comprar",
  "conta_a_pagar_vencendo",
  "conta_a_receber_vencida",
] as const;
export type TipoDeGatilho = (typeof TIPOS_DE_GATILHO)[number];

export const TIPOS_DE_ACAO = ["criar_compromisso", "avisar_pessoa", "adicionar_etiqueta_ao_cliente"] as const;
export type TipoDeAcao = (typeof TIPOS_DE_ACAO)[number];

export type Gatilho =
  | { tipo: "orcamento_sem_resposta"; parametros: { dias: number } }
  | { tipo: "pedido_parado_na_etapa"; parametros: { dias: number; etapa: string | null } }
  | { tipo: "pedido_entrou_na_etapa"; parametros: { etapa: string } }
  | { tipo: "estoque_abaixo"; parametros: { item_tipo: "insumo" | "produto"; item_id: string | null; quantidade: number | null } }
  | { tipo: "cliente_sem_comprar"; parametros: { dias: number } }
  | { tipo: "conta_a_pagar_vencendo"; parametros: { dias: number } }
  | { tipo: "conta_a_receber_vencida"; parametros: { dias: number } };

export type Acao =
  | {
      tipo: "criar_compromisso";
      parametros: { titulo: string | null; daqui_a_dias: number; lembrete_minutos: number | null; responsavel_id: string | null };
    }
  | { tipo: "avisar_pessoa"; parametros: { usuario_id: string | null; mensagem: string | null } }
  | { tipo: "adicionar_etiqueta_ao_cliente"; parametros: { etiqueta: string } };

export type RegraConferida = { nome: string; gatilho: Gatilho; acoes: Acao[] };

export const ROTULO_DO_GATILHO: Record<TipoDeGatilho, string> = {
  orcamento_sem_resposta: "Orçamento sem resposta",
  pedido_parado_na_etapa: "Pedido parado na mesma etapa",
  pedido_entrou_na_etapa: "Pedido entrou numa etapa",
  estoque_abaixo: "Estoque abaixo do limite",
  cliente_sem_comprar: "Cliente sem comprar",
  conta_a_pagar_vencendo: "Conta a pagar perto do vencimento",
  conta_a_receber_vencida: "Conta a receber vencida",
};

export const ROTULO_DA_ACAO: Record<TipoDeAcao, string> = {
  criar_compromisso: "Marcar um compromisso na agenda",
  avisar_pessoa: "Avisar uma pessoa da equipe",
  adicionar_etiqueta_ao_cliente: "Pôr uma etiqueta no cliente",
};

/* As etapas fixas do quadro de produção. As que a empresa cria têm id próprio
   (stage-…) e ficam em profiles.production_kanban_columns. */
export const ETAPAS_FIXAS: Record<string, string> = {
  "status:pending_production": "Aguardando produção",
  "status:in_production": "Em produção",
  "status:finished": "Finalizado",
  "status:delivered": "Pronto para entrega",
};

/* Gatilhos que apontam para um cliente cadastrado: só eles aceitam etiqueta. */
export const GATILHOS_COM_CLIENTE: ReadonlySet<TipoDeGatilho> = new Set([
  "orcamento_sem_resposta",
  "pedido_parado_na_etapa",
  "pedido_entrou_na_etapa",
  "cliente_sem_comprar",
]);

export const LEMBRETES_DA_REGRA = [5, 10, 15, 30, 60, 120, 240, 1440, 2880];

/* Até onde olhar para trás, além do prazo da regra. Sem isso, a primeira volta
   de uma regra nova ("orçamento 3 dias sem resposta") dispararia em cima de
   dois mil rascunhos de anos atrás. */
export const JANELA_EM_DIAS: Record<TipoDeGatilho, number> = {
  orcamento_sem_resposta: 30,
  pedido_parado_na_etapa: 30,
  pedido_entrou_na_etapa: 7,
  estoque_abaixo: 0,
  cliente_sem_comprar: 365,
  conta_a_pagar_vencendo: 0,
  conta_a_receber_vencida: 30,
};

/* Enquanto o estoque continuar baixo, a regra lembra de novo a cada tantos dias. */
export const ESTOQUE_LEMBRA_A_CADA_DIAS = 7;

/* --------------------------------------------------------------- conferência */

const SO_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const SO_ETAPA = /^(status:(pending_production|in_production|finished|delivered)|stage-[A-Za-z0-9-]{1,80})$/;

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

function objeto(valor: unknown): Record<string, unknown> {
  return valor && typeof valor === "object" && !Array.isArray(valor) ? (valor as Record<string, unknown>) : {};
}

function inteiro(valor: unknown, min: number, max: number, nome: string): Conferido<number> {
  const numero = typeof valor === "string" ? Number(valor.replace(",", ".")) : Number(valor);
  if (valor === null || valor === undefined || valor === "" || !Number.isFinite(numero) || !Number.isInteger(numero)) {
    return { ok: false, erro: `Informe ${nome} (um número inteiro).` };
  }
  if (numero < min || numero > max) return { ok: false, erro: `${primeiraMaiuscula(nome)} precisa ficar entre ${min} e ${max}.` };
  return { ok: true, valor: numero };
}

function idOuNulo(valor: unknown, nome: string): Conferido<string | null> {
  if (valor === null || valor === undefined || valor === "") return { ok: true, valor: null };
  if (typeof valor !== "string" || !SO_UUID.test(valor)) return { ok: false, erro: `${primeiraMaiuscula(nome)} inválido.` };
  return { ok: true, valor: valor.toLowerCase() };
}

function textoOuNulo(valor: unknown, max: number): string | null {
  if (typeof valor !== "string") return null;
  const limpo = valor.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
  return limpo ? limpo.slice(0, max) : null;
}

function primeiraMaiuscula(texto: string) {
  return texto.charAt(0).toUpperCase() + texto.slice(1);
}

export function conferirGatilho(entrada: unknown): Conferido<Gatilho> {
  const bruto = objeto(entrada);
  const tipo = String(bruto.tipo ?? "");
  const p = objeto(bruto.parametros);
  switch (tipo) {
    case "orcamento_sem_resposta":
    case "cliente_sem_comprar": {
      const max = tipo === "cliente_sem_comprar" ? 730 : 180;
      const dias = inteiro(p.dias, 1, max, "o número de dias");
      if (!dias.ok) return dias;
      return { ok: true, valor: { tipo, parametros: { dias: dias.valor } } };
    }
    case "pedido_parado_na_etapa": {
      const dias = inteiro(p.dias, 1, 90, "o número de dias");
      if (!dias.ok) return dias;
      const etapa = p.etapa === null || p.etapa === undefined || p.etapa === "" ? null : String(p.etapa);
      if (etapa !== null && !SO_ETAPA.test(etapa)) return { ok: false, erro: "Etapa inválida. Escolha uma das etapas do quadro de produção." };
      return { ok: true, valor: { tipo, parametros: { dias: dias.valor, etapa } } };
    }
    case "pedido_entrou_na_etapa": {
      const etapa = String(p.etapa ?? "");
      if (!SO_ETAPA.test(etapa)) return { ok: false, erro: "Escolha a etapa do quadro de produção." };
      return { ok: true, valor: { tipo, parametros: { etapa } } };
    }
    case "estoque_abaixo": {
      const itemTipo = p.item_tipo === "produto" ? "produto" : p.item_tipo === "insumo" || p.item_tipo === undefined ? "insumo" : null;
      if (!itemTipo) return { ok: false, erro: "O estoque vigiado é de insumo ou de produto." };
      const item = idOuNulo(p.item_id, "o item");
      if (!item.ok) return item;
      let quantidade: number | null = null;
      if (p.quantidade !== null && p.quantidade !== undefined && p.quantidade !== "") {
        const numero = typeof p.quantidade === "string" ? Number(p.quantidade.replace(",", ".")) : Number(p.quantidade);
        if (!Number.isFinite(numero) || numero < 0 || numero > 1_000_000) return { ok: false, erro: "Quantidade inválida." };
        quantidade = Math.round(numero * 1000) / 1000;
      }
      /* Insumo sem quantidade usa o "avisar abaixo de" do próprio insumo. Produto não tem esse campo. */
      if (itemTipo === "produto" && quantidade === null) return { ok: false, erro: "Diga abaixo de quantas unidades do produto a regra dispara." };
      return { ok: true, valor: { tipo, parametros: { item_tipo: itemTipo, item_id: item.valor, quantidade } } };
    }
    case "conta_a_pagar_vencendo": {
      const dias = inteiro(p.dias, 0, 60, "quantos dias antes do vencimento");
      if (!dias.ok) return dias;
      return { ok: true, valor: { tipo, parametros: { dias: dias.valor } } };
    }
    case "conta_a_receber_vencida": {
      const dias = inteiro(p.dias, 0, 180, "quantos dias depois do vencimento");
      if (!dias.ok) return dias;
      return { ok: true, valor: { tipo, parametros: { dias: dias.valor } } };
    }
    default:
      return { ok: false, erro: `Gatilho desconhecido. Use um destes: ${TIPOS_DE_GATILHO.join(", ")}.` };
  }
}

export function conferirAcao(entrada: unknown): Conferido<Acao> {
  const bruto = objeto(entrada);
  const tipo = String(bruto.tipo ?? "");
  const p = objeto(bruto.parametros);
  switch (tipo) {
    case "criar_compromisso": {
      let daqui = 0;
      if (p.daqui_a_dias !== undefined && p.daqui_a_dias !== null && p.daqui_a_dias !== "") {
        const dias = inteiro(p.daqui_a_dias, 0, 30, "daqui a quantos dias");
        if (!dias.ok) return dias;
        daqui = dias.valor;
      }
      let lembrete: number | null = null;
      if (p.lembrete_minutos !== undefined && p.lembrete_minutos !== null && p.lembrete_minutos !== "") {
        lembrete = Number(p.lembrete_minutos);
        if (!LEMBRETES_DA_REGRA.includes(lembrete)) return { ok: false, erro: "Lembrete inválido. Use 5, 10, 15, 30, 60, 120, 240, 1440 ou 2880 minutos." };
      }
      const responsavel = idOuNulo(p.responsavel_id, "o responsável");
      if (!responsavel.ok) return responsavel;
      return {
        ok: true,
        valor: { tipo, parametros: { titulo: textoOuNulo(p.titulo, 80), daqui_a_dias: daqui, lembrete_minutos: lembrete, responsavel_id: responsavel.valor } },
      };
    }
    case "avisar_pessoa": {
      const usuario = idOuNulo(p.usuario_id, "a pessoa");
      if (!usuario.ok) return usuario;
      return { ok: true, valor: { tipo, parametros: { usuario_id: usuario.valor, mensagem: textoOuNulo(p.mensagem, 200) } } };
    }
    case "adicionar_etiqueta_ao_cliente": {
      const etiqueta = textoOuNulo(p.etiqueta, 40);
      if (!etiqueta) return { ok: false, erro: "Diga qual etiqueta pôr no cliente." };
      return { ok: true, valor: { tipo, parametros: { etiqueta } } };
    }
    default:
      return { ok: false, erro: `Ação desconhecida. Use uma destas: ${TIPOS_DE_ACAO.join(", ")}.` };
  }
}

/** Confere uma regra inteira. O nome é opcional: sem ele, vira a frase do gatilho. */
export function conferirRegra(entrada: unknown, nomes: NomesDaFrase = {}): Conferido<RegraConferida> {
  const bruto = objeto(entrada);
  const gatilho = conferirGatilho(bruto.gatilho);
  if (!gatilho.ok) return gatilho;

  const listaBruta = Array.isArray(bruto.acoes) ? bruto.acoes : [];
  if (!listaBruta.length) return { ok: false, erro: "Escolha pelo menos uma ação." };
  if (listaBruta.length > 3) return { ok: false, erro: "Use no máximo 3 ações por regra." };
  const acoes: Acao[] = [];
  for (const item of listaBruta) {
    const acao = conferirAcao(item);
    if (!acao.ok) return acao;
    if (acoes.some((a) => a.tipo === acao.valor.tipo)) return { ok: false, erro: "Cada ação pode aparecer uma vez só na mesma regra." };
    if (acao.valor.tipo === "adicionar_etiqueta_ao_cliente" && !GATILHOS_COM_CLIENTE.has(gatilho.valor.tipo)) {
      return { ok: false, erro: "A etiqueta vai num cliente cadastrado, e este gatilho não aponta para um cliente." };
    }
    acoes.push(acao.valor);
  }

  const nome = textoOuNulo(bruto.nome, 120) ?? fraseDoGatilho(gatilho.valor, nomes).slice(0, 120);
  return { ok: true, valor: { nome, gatilho: gatilho.valor, acoes } };
}

/* -------------------------------------------------------------------- frase */

export type NomesDaFrase = {
  etapas?: Record<string, string>;
  itens?: Record<string, string>;
  pessoas?: Record<string, string>;
};

function dias(n: number) {
  return n === 1 ? "1 dia" : `${n} dias`;
}

function numeroBr(n: number) {
  return n.toLocaleString("pt-BR", { maximumFractionDigits: 3 });
}

export function nomeDaEtapa(etapa: string | null, nomes: NomesDaFrase = {}) {
  if (!etapa) return null;
  return nomes.etapas?.[etapa] ?? ETAPAS_FIXAS[etapa] ?? "uma etapa que não existe mais";
}

export function fraseDoGatilho(gatilho: Gatilho, nomes: NomesDaFrase = {}): string {
  switch (gatilho.tipo) {
    case "orcamento_sem_resposta":
      return `Quando um orçamento em aberto passar ${dias(gatilho.parametros.dias)} sem resposta`;
    case "pedido_parado_na_etapa": {
      const etapa = nomeDaEtapa(gatilho.parametros.etapa, nomes);
      return etapa
        ? `Quando um pedido ficar ${dias(gatilho.parametros.dias)} parado na etapa "${etapa}"`
        : `Quando um pedido ficar ${dias(gatilho.parametros.dias)} parado na mesma etapa da produção`;
    }
    case "pedido_entrou_na_etapa":
      return `Quando um pedido entrar na etapa "${nomeDaEtapa(gatilho.parametros.etapa, nomes)}"`;
    case "estoque_abaixo": {
      const { item_tipo, item_id, quantidade } = gatilho.parametros;
      const item = item_id ? `"${nomes.itens?.[item_id] ?? (item_tipo === "produto" ? "o produto escolhido" : "o insumo escolhido")}"` : null;
      const limite = quantidade === null ? "do mínimo cadastrado" : `de ${numeroBr(quantidade)}`;
      if (item) return `Quando o estoque de ${item} ficar abaixo ${limite}`;
      return item_tipo === "produto"
        ? `Quando o estoque de um produto ficar abaixo ${limite}`
        : `Quando o estoque de um insumo ficar abaixo ${limite}`;
    }
    case "cliente_sem_comprar":
      return `Quando um cliente passar ${dias(gatilho.parametros.dias)} sem comprar`;
    case "conta_a_pagar_vencendo":
      return gatilho.parametros.dias === 0
        ? "Quando uma conta a pagar vencer hoje"
        : `Quando faltarem ${dias(gatilho.parametros.dias)} para uma conta a pagar vencer`;
    case "conta_a_receber_vencida":
      return gatilho.parametros.dias === 0
        ? "Quando uma conta a receber vencer sem ser paga"
        : `Quando uma conta a receber estiver vencida há ${dias(gatilho.parametros.dias)}`;
  }
}

export function fraseDaAcao(acao: Acao, nomes: NomesDaFrase = {}): string {
  switch (acao.tipo) {
    case "criar_compromisso": {
      const quando = acao.parametros.daqui_a_dias === 0 ? "para o mesmo dia" : `para ${dias(acao.parametros.daqui_a_dias)} depois`;
      const quem = acao.parametros.responsavel_id ? ` com ${nomes.pessoas?.[acao.parametros.responsavel_id] ?? "a pessoa escolhida"}` : "";
      const titulo = acao.parametros.titulo ? ` "${acao.parametros.titulo}"` : "";
      return `marcar um compromisso${titulo} na agenda ${quando}${quem}`;
    }
    case "avisar_pessoa":
      return acao.parametros.usuario_id ? `avisar ${nomes.pessoas?.[acao.parametros.usuario_id] ?? "a pessoa escolhida"}` : "avisar o dono da empresa";
    case "adicionar_etiqueta_ao_cliente":
      return `pôr a etiqueta "${acao.parametros.etiqueta}" no cliente`;
  }
}

function juntar(partes: string[]) {
  if (partes.length <= 1) return partes[0] ?? "";
  return `${partes.slice(0, -1).join(", ")} e ${partes[partes.length - 1]}`;
}

/** "Quando um orçamento em aberto passar 3 dias sem resposta, marcar um compromisso na agenda para o mesmo dia." */
export function fraseDaRegra(gatilho: Gatilho, acoes: Acao[], nomes: NomesDaFrase = {}): string {
  return `${fraseDoGatilho(gatilho, nomes)}, ${juntar(acoes.map((a) => fraseDaAcao(a, nomes)))}.`;
}

/* ------------------------------------------------------------- datas e etapa */

const DIA_MS = 86_400_000;

/** O dia de hoje no Brasil, AAAA-MM-DD. O servidor roda em UTC. */
export function hojeNoBrasil(agora: Date = new Date()) {
  return new Intl.DateTimeFormat("en-CA", { timeZone: "America/Sao_Paulo", year: "numeric", month: "2-digit", day: "2-digit" }).format(agora);
}

export function somarDiasNaData(iso: string, n: number) {
  const [ano, mes, dia] = iso.split("-").map(Number);
  return new Date(Date.UTC(ano, mes - 1, dia + n)).toISOString().slice(0, 10);
}

/** Dias inteiros entre um instante e agora (para baixo). */
export function diasDesde(instante: string | null | undefined, agora: Date): number | null {
  if (!instante) return null;
  const t = Date.parse(instante);
  if (!Number.isFinite(t)) return null;
  return Math.floor((agora.getTime() - t) / DIA_MS);
}

export function dataBrasileira(iso: string | null | undefined) {
  const [ano, mes, dia] = String(iso ?? "").slice(0, 10).split("-");
  return ano && mes && dia ? `${dia}/${mes}/${ano}` : String(iso ?? "");
}

export const ETAPA_ARQUIVADA = "__kanban_archived__";
const STATUS_DO_QUADRO = new Set(["pending_production", "in_production", "finished", "delivered"]);

/**
 * Em que etapa do quadro o pedido está, do jeito que a tela de produção mostra:
 * a etapa criada pela empresa quando `production_stage_id` é uma delas; senão,
 * a coluna fixa do status. `null` = fora do quadro (cancelado ou arquivado).
 */
export function etapaDoPedido(
  pedido: { status: string | null; production_stage_id: string | null },
  etapasDaEmpresa: ReadonlySet<string>
): string | null {
  const status = String(pedido.status ?? "");
  if (!STATUS_DO_QUADRO.has(status)) return null;
  const etapa = pedido.production_stage_id ?? null;
  if (etapa === ETAPA_ARQUIVADA) return null;
  if (etapa && etapasDaEmpresa.has(etapa)) return etapa;
  return `status:${status}`;
}

/** A etapa de um registro do histórico de produção (order_production_events). */
export function etapaDoEvento(
  evento: { status: string | null; stage_id: string | null },
  etapasDaEmpresa: ReadonlySet<string>
): string | null {
  return etapaDoPedido({ status: evento.status, production_stage_id: evento.stage_id }, etapasDaEmpresa);
}

/** As etapas que a empresa criou no quadro, em ordem. */
export function etapasDoQuadro(colunas: unknown): Array<{ id: string; nome: string; status: string }> {
  if (!Array.isArray(colunas)) return [];
  return colunas
    .map((item, indice) => {
      const c = objeto(item);
      const id = typeof c.id === "string" ? c.id : "";
      if (!/^stage-[A-Za-z0-9-]{1,80}$/.test(id)) return null;
      const nome = typeof c.label === "string" && c.label.trim() ? c.label.trim() : `Etapa ${indice + 1}`;
      const status = typeof c.status === "string" && STATUS_DO_QUADRO.has(c.status) ? c.status : "in_production";
      const posicao = Number.isFinite(Number(c.position)) ? Number(c.position) : indice;
      return { id, nome, status, posicao };
    })
    .filter((x): x is { id: string; nome: string; status: string; posicao: number } => Boolean(x))
    .sort((a, b) => a.posicao - b.posicao)
    .map(({ id, nome, status }) => ({ id, nome, status }));
}

/** Todas as etapas para escolher: as fixas e as da empresa, cada uma com o nome que a tela mostra. */
export function opcoesDeEtapa(colunas: unknown): Array<{ id: string; nome: string }> {
  const daEmpresa = etapasDoQuadro(colunas);
  const lista: Array<{ id: string; nome: string }> = [];
  for (const [id, nome] of Object.entries(ETAPAS_FIXAS)) {
    lista.push({ id, nome });
    const status = id.slice("status:".length);
    for (const e of daEmpresa.filter((x) => x.status === status)) lista.push({ id: e.id, nome: e.nome });
  }
  return lista;
}

/* -------------------------------------------------- quem dispara a regra agora */

export type TipoDeAlvo = "orcamento" | "pedido" | "insumo" | "produto" | "cliente" | "conta_a_pagar" | "conta_a_receber";

export type Alvo = {
  tipo: TipoDeAlvo;
  id: string;
  /* Uma por ocorrência: a regra não age duas vezes na mesma. */
  chave: string;
  titulo: string;
  detalhe: string;
  clienteId: string | null;
  pedidoId: string | null;
  orcamentoId: string | null;
  /* A tela onde a pessoa resolve (o aviso no celular abre aqui). */
  rota: string;
};

function nomeDeCliente(c: { name?: string | null; company_name?: string | null } | null | undefined) {
  return (c?.company_name || c?.name || "").trim() || null;
}

export type LinhaDeOrcamento = {
  id: string;
  number: number | null;
  title: string | null;
  status: string | null;
  client_id: string | null;
  total: number | null;
  updated_at: string | null;
  created_at: string | null;
};

export function alvosDeOrcamentos(
  linhas: LinhaDeOrcamento[],
  diasDaRegra: number,
  agora: Date,
  clientes: Record<string, { name?: string | null; company_name?: string | null }> = {}
): Alvo[] {
  const janela = diasDaRegra + JANELA_EM_DIAS.orcamento_sem_resposta;
  const alvos: Alvo[] = [];
  for (const q of linhas) {
    if (q.status !== "draft" && q.status !== "sent") continue;
    const desde = q.updated_at || q.created_at;
    const parado = diasDesde(desde, agora);
    if (parado === null || parado < diasDaRegra || parado > janela) continue;
    const cliente = q.client_id ? nomeDeCliente(clientes[q.client_id]) : null;
    const numero = q.number ? `#${q.number}` : "";
    alvos.push({
      tipo: "orcamento",
      id: q.id,
      /* A última mexida entra na chave: se o orçamento for mexido e parar de novo, a regra volta a valer. */
      chave: `orcamento:${q.id}:${String(desde).slice(0, 19)}`,
      titulo: `Orçamento ${numero}${cliente ? ` de ${cliente}` : ""} sem resposta há ${dias(parado)}`.replace(/\s+/g, " "),
      detalhe: [q.title ? `Orçamento: ${q.title}.` : "", `Última mexida em ${dataBrasileira(desde)}.`].filter(Boolean).join(" "),
      clienteId: q.client_id,
      pedidoId: null,
      orcamentoId: q.id,
      rota: `/dashboard/quotes/${q.id}`,
    });
  }
  return alvos;
}

export type LinhaDePedido = {
  id: string;
  number: number | null;
  status: string | null;
  production_stage_id: string | null;
  client_id: string | null;
  production_updated_at: string | null;
  updated_at: string | null;
  created_at: string | null;
};

/**
 * Pedidos parados: o tempo conta desde que o pedido entrou na etapa em que
 * está (o histórico de produção). Sem histórico, vale a última mexida na
 * produção. Sem etapa escolhida, olha só quem ainda está sendo feito
 * (aguardando e em produção, com as etapas da empresa dentro delas).
 */
export function alvosDePedidosParados(
  pedidos: LinhaDePedido[],
  entradaNaEtapa: Record<string, string>,
  parametros: { dias: number; etapa: string | null },
  etapasDaEmpresa: ReadonlySet<string>,
  agora: Date,
  clientes: Record<string, { name?: string | null; company_name?: string | null }> = {},
  nomes: NomesDaFrase = {}
): Alvo[] {
  const janela = parametros.dias + JANELA_EM_DIAS.pedido_parado_na_etapa;
  const alvos: Alvo[] = [];
  for (const p of pedidos) {
    const etapa = etapaDoPedido(p, etapasDaEmpresa);
    if (!etapa) continue;
    if (parametros.etapa) {
      if (etapa !== parametros.etapa) continue;
    } else if (p.status !== "pending_production" && p.status !== "in_production") {
      continue;
    }
    const desde = entradaNaEtapa[p.id] || p.production_updated_at || p.updated_at || p.created_at;
    const parado = diasDesde(desde, agora);
    if (parado === null || parado < parametros.dias || parado > janela) continue;
    const cliente = p.client_id ? nomeDeCliente(clientes[p.client_id]) : null;
    alvos.push({
      tipo: "pedido",
      id: p.id,
      chave: `parado:${p.id}:${etapa}:${String(desde).slice(0, 19)}`,
      titulo: `Pedido ${p.number ? `#${p.number}` : ""}${cliente ? ` de ${cliente}` : ""} parado há ${dias(parado)} em "${nomeDaEtapa(etapa, nomes)}"`.replace(/\s+/g, " "),
      detalhe: `Na etapa desde ${dataBrasileira(desde)}.`,
      clienteId: p.client_id,
      pedidoId: p.id,
      orcamentoId: null,
      rota: `/dashboard/orders/${p.id}`,
    });
  }
  return alvos;
}

export type EventoDeEtapa = { id: string; order_id: string; status: string | null; stage_id: string | null; started_at: string };

export function alvosDeEntradaNaEtapa(
  eventos: EventoDeEtapa[],
  etapaDaRegra: string,
  desdeQuando: string,
  etapasDaEmpresa: ReadonlySet<string>,
  pedidos: Record<string, { number: number | null; client_id: string | null }>,
  clientes: Record<string, { name?: string | null; company_name?: string | null }> = {},
  nomes: NomesDaFrase = {}
): Alvo[] {
  const limite = Date.parse(desdeQuando);
  const alvos: Alvo[] = [];
  for (const e of eventos) {
    if (!(Date.parse(e.started_at) >= limite)) continue;
    if (etapaDoEvento(e, etapasDaEmpresa) !== etapaDaRegra) continue;
    const pedido = pedidos[e.order_id];
    if (!pedido) continue;
    const cliente = pedido.client_id ? nomeDeCliente(clientes[pedido.client_id]) : null;
    alvos.push({
      tipo: "pedido",
      id: e.order_id,
      chave: `entrou:${e.id}`,
      titulo: `Pedido ${pedido.number ? `#${pedido.number}` : ""}${cliente ? ` de ${cliente}` : ""} entrou em "${nomeDaEtapa(etapaDaRegra, nomes)}"`.replace(/\s+/g, " "),
      detalhe: `Entrou na etapa em ${dataBrasileira(e.started_at)}.`,
      clienteId: pedido.client_id,
      pedidoId: e.order_id,
      orcamentoId: null,
      rota: `/dashboard/orders/${e.order_id}`,
    });
  }
  return alvos;
}

export type LinhaDeEstoque = { id: string; nome: string; unidade: string | null; atual: number | null; minimo: number | null };

/** A semana do ano em dias corridos desde 1970: a chave muda a cada 7 dias. */
function semana(agora: Date) {
  return Math.floor(agora.getTime() / DIA_MS / ESTOQUE_LEMBRA_A_CADA_DIAS);
}

export function alvosDeEstoque(
  linhas: LinhaDeEstoque[],
  itemTipo: "insumo" | "produto",
  quantidade: number | null,
  agora: Date
): Alvo[] {
  const alvos: Alvo[] = [];
  for (const l of linhas) {
    const limite = quantidade ?? (l.minimo && l.minimo > 0 ? Number(l.minimo) : null);
    if (limite === null) continue;
    const atual = Number(l.atual ?? 0);
    if (!Number.isFinite(atual) || atual >= limite) continue;
    const unidade = l.unidade ? ` ${l.unidade}` : "";
    alvos.push({
      tipo: itemTipo,
      id: l.id,
      chave: `estoque:${l.id}:${semana(agora)}`,
      titulo: `Estoque baixo: ${l.nome} (${numeroBr(atual)}${unidade}, abaixo de ${numeroBr(limite)}${unidade})`,
      detalhe: `Hora de comprar ${l.nome}. Tem ${numeroBr(atual)}${unidade}; o limite é ${numeroBr(limite)}${unidade}.`,
      clienteId: null,
      pedidoId: null,
      orcamentoId: null,
      rota: itemTipo === "insumo" ? "/dashboard/products/estoque" : `/dashboard/products/${l.id}`,
    });
  }
  return alvos;
}

export type LinhaDeClienteSemComprar = { cliente_id: string; nome: string | null; empresa: string | null; ultima_compra: string };

export function alvosDeClientesSemComprar(linhas: LinhaDeClienteSemComprar[], agora: Date): Alvo[] {
  return linhas.map((c) => {
    const nome = nomeDeCliente({ name: c.nome, company_name: c.empresa }) ?? "Cliente";
    const parado = diasDesde(c.ultima_compra, agora) ?? 0;
    return {
      tipo: "cliente" as const,
      id: c.cliente_id,
      /* A última compra entra na chave: se ele comprar e sumir de novo, a regra volta a valer. */
      chave: `sem_comprar:${c.cliente_id}:${String(c.ultima_compra).slice(0, 10)}`,
      titulo: `${nome} está há ${dias(parado)} sem comprar`,
      detalhe: `Última compra em ${dataBrasileira(c.ultima_compra)}.`,
      clienteId: c.cliente_id,
      pedidoId: null,
      orcamentoId: null,
      rota: `/dashboard/clients/${c.cliente_id}`,
    };
  });
}

export type LinhaDeConta = {
  id: string;
  description: string | null;
  due_date: string;
  status: string | null;
  valor: number | null;
  pessoa: string | null;
};

function reaisBr(valor: number | null) {
  return Number(valor ?? 0).toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
}

export function alvosDeContasAPagar(linhas: LinhaDeConta[], diasAntes: number, hoje: string): Alvo[] {
  const ate = somarDiasNaData(hoje, diasAntes);
  return linhas
    .filter((c) => String(c.status).toUpperCase() === "PENDING" && c.due_date >= hoje && c.due_date <= ate)
    .map((c) => ({
      tipo: "conta_a_pagar" as const,
      id: c.id,
      chave: `pagar:${c.id}:${c.due_date}`,
      titulo: `Conta a pagar vence ${c.due_date === hoje ? "hoje" : `em ${dataBrasileira(c.due_date)}`}: ${c.description || "sem descrição"} (${reaisBr(c.valor)})`,
      detalhe: [c.pessoa ? `Fornecedor: ${c.pessoa}.` : "", `Vencimento em ${dataBrasileira(c.due_date)}.`].filter(Boolean).join(" "),
      clienteId: null,
      pedidoId: null,
      orcamentoId: null,
      rota: "/dashboard/financial/payables",
    }));
}

export function alvosDeContasAReceber(linhas: LinhaDeConta[], diasDepois: number, hoje: string): Alvo[] {
  const ate = somarDiasNaData(hoje, -diasDepois);
  const de = somarDiasNaData(ate, -JANELA_EM_DIAS.conta_a_receber_vencida);
  const abertas = new Set(["PENDING", "OVERDUE", "PARTIALLY_PAID"]);
  return linhas
    .filter((c) => abertas.has(String(c.status).toUpperCase()) && c.due_date < hoje && c.due_date <= ate && c.due_date >= de)
    .map((c) => ({
      tipo: "conta_a_receber" as const,
      id: c.id,
      chave: `receber:${c.id}:${c.due_date}`,
      titulo: `Conta a receber vencida em ${dataBrasileira(c.due_date)}: ${c.description || "sem descrição"} (${reaisBr(c.valor)})`,
      detalhe: [c.pessoa ? `Cliente: ${c.pessoa}.` : "", "Ainda não foi paga."].filter(Boolean).join(" "),
      clienteId: null,
      pedidoId: null,
      orcamentoId: null,
      rota: "/dashboard/financial/receivables",
    }));
}

/* ------------------------------------------------------------------- limites */

/* Por regra, em cada volta: o resto fica para a volta seguinte (5 minutos). */
export const ACOES_POR_REGRA_POR_VOLTA = 20;
/* No total da volta, somando todas as empresas. */
export const ACOES_POR_VOLTA = 200;
/* Regras conferidas em cada volta: as que estão há mais tempo sem conferência vão primeiro. */
export const REGRAS_POR_VOLTA = 100;
/* Regras por empresa. */
export const REGRAS_POR_EMPRESA = 30;

/** Os alvos que ainda não foram tratados, na ordem, até o limite. */
export function novosAlvos(alvos: Alvo[], jaFeitos: ReadonlySet<string>, limite: number): Alvo[] {
  const vistos = new Set<string>();
  const saida: Alvo[] = [];
  for (const a of alvos) {
    if (saida.length >= limite) break;
    if (jaFeitos.has(a.chave) || vistos.has(a.chave)) continue;
    vistos.add(a.chave);
    saida.push(a);
  }
  return saida;
}

/* --------------------------------------------------------- onde a fila roda */

/**
 * De que empresas a fila cuida. `null` = todas (produção); lista = só essas;
 * `false` = desligada.
 *
 * O banco é um só para o computador de desenvolvimento e a produção: fora de
 * produção a fila só liga com REGRAS_FILA_LOCAL e só mexe nas empresas de
 * REGRAS_LIBERADAS. REGRAS_DESLIGADAS desliga em qualquer lugar.
 */
export function empresasDaFila(env: Record<string, string | undefined>): string[] | null | false {
  if (String(env.REGRAS_DESLIGADAS || "").trim()) return false;
  if (env.NODE_ENV === "production") return null;
  if (!String(env.REGRAS_FILA_LOCAL || "").trim()) return false;
  const lista = String(env.REGRAS_LIBERADAS || "")
    .split(/[\s,;]+/)
    .map((id) => id.trim().toLowerCase())
    .filter((id) => SO_UUID.test(id));
  return lista.length ? Array.from(new Set(lista)) : false;
}

/** O dia do compromisso: hoje no Brasil mais N dias, nunca antes do dia do servidor. */
export function diaDoCompromisso(daquiADias: number, agora: Date = new Date()) {
  const noBrasil = somarDiasNaData(hojeNoBrasil(agora), daquiADias);
  /* A conferência da agenda recusa dia anterior ao do relógio do servidor
     (UTC). Entre 21h e meia-noite no Brasil o servidor já está no dia seguinte. */
  const doServidor = `${agora.getFullYear()}-${String(agora.getMonth() + 1).padStart(2, "0")}-${String(agora.getDate()).padStart(2, "0")}`;
  return noBrasil < doServidor ? doServidor : noBrasil;
}
