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

import {
  alvosDeClientesSemComprar,
  alvosDeContasAPagar,
  alvosDeContasAReceber,
  alvosDeEntradaNaEtapa,
  alvosDeEstoque,
  alvosDeOrcamentos,
  alvosDePedidosParados,
  etapasDoQuadro,
  hojeNoBrasil,
  JANELA_EM_DIAS,
  somarDiasNaData,
  type Alvo,
  type EventoDeEtapa,
  type Gatilho,
  type LinhaDeClienteSemComprar,
  type LinhaDeConta,
  type LinhaDeEstoque,
  type LinhaDeOrcamento,
  type LinhaDePedido,
  type NomesDaFrase,
} from "../regras";

/**
 * As consultas de cada gatilho: quais registros disparam a regra agora.
 *
 * Recebe o banco de quem pergunta. A fila passa a chave de serviço; o "testar"
 * da tela e do assistente passa a sessão da pessoa, e as travas das tabelas
 * valem. Toda consulta tem teto: uma empresa com cem mil orçamentos não pode
 * virar uma volta de minutos.
 */

export type Empresa = { dono: string; tenantIds: string[] };

/* Quantos candidatos cada consulta traz no máximo. A ação de cada volta é bem
   menor (ACOES_POR_REGRA_POR_VOLTA); o resto espera a volta seguinte. */
const TETO = 500;
const LOTE_IN = 100;

type Erro = { message?: string } | null;

export class ErroDaConsulta extends Error {}

function conferir<T>(resposta: { data: unknown; error: Erro }, oQue: string): T[] {
  if (resposta.error) throw new ErroDaConsulta(`Não foi possível ler ${oQue}: ${resposta.error.message ?? "erro do banco"}`);
  return (resposta.data as T[] | null) ?? [];
}

function emLotes<T>(lista: T[], tamanho = LOTE_IN): T[][] {
  const lotes: T[][] = [];
  for (let i = 0; i < lista.length; i += tamanho) lotes.push(lista.slice(i, i + tamanho));
  return lotes;
}

type Cliente = { id: string; name: string | null; company_name: string | null };

async function clientesPorId(db: SupabaseClient, empresa: Empresa, ids: Array<string | null>) {
  const unicos = Array.from(new Set(ids.filter((id): id is string => Boolean(id))));
  const mapa: Record<string, Cliente> = {};
  for (const lote of emLotes(unicos)) {
    const linhas = conferir<Cliente>(
      await db.from("clients").select("id,name,company_name").in("id", lote).in("user_id", empresa.tenantIds),
      "os clientes"
    );
    for (const c of linhas) mapa[c.id] = c;
  }
  return mapa;
}

/** As etapas que a empresa criou no quadro de produção. */
export async function etapasDaEmpresa(db: SupabaseClient, empresa: Empresa) {
  const { data } = await db.from("profiles").select("production_kanban_columns").eq("id", empresa.dono).maybeSingle();
  return etapasDoQuadro((data as { production_kanban_columns?: unknown } | null)?.production_kanban_columns);
}

/** Os nomes que a frase da regra usa: etapas, itens de estoque e pessoas. */
export async function nomesParaAFrase(db: SupabaseClient, admin: SupabaseClient, empresa: Empresa, gatilhos: Gatilho[], pessoasIds: string[]): Promise<NomesDaFrase> {
  const nomes: NomesDaFrase = { etapas: {}, itens: {}, pessoas: {} };
  if (gatilhos.some((g) => g.tipo === "pedido_entrou_na_etapa" || g.tipo === "pedido_parado_na_etapa")) {
    for (const e of await etapasDaEmpresa(db, empresa)) nomes.etapas![e.id] = e.nome;
  }
  const insumos = gatilhos.flatMap((g) => (g.tipo === "estoque_abaixo" && g.parametros.item_tipo === "insumo" && g.parametros.item_id ? [g.parametros.item_id] : []));
  const produtos = gatilhos.flatMap((g) => (g.tipo === "estoque_abaixo" && g.parametros.item_tipo === "produto" && g.parametros.item_id ? [g.parametros.item_id] : []));
  if (insumos.length) {
    const { data } = await db.from("materials").select("id,name").in("id", insumos.slice(0, 100)).in("user_id", empresa.tenantIds);
    for (const m of (data ?? []) as Array<{ id: string; name: string | null }>) nomes.itens![m.id] = m.name ?? "insumo";
  }
  if (produtos.length) {
    const { data } = await db.from("products").select("id,name").in("id", produtos.slice(0, 100)).in("user_id", empresa.tenantIds);
    for (const p of (data ?? []) as Array<{ id: string; name: string | null }>) nomes.itens![p.id] = p.name ?? "produto";
  }
  if (pessoasIds.length) {
    for (const p of await pessoasDaEmpresa(admin, empresa)) nomes.pessoas![p.id] = p.nome;
  }
  return nomes;
}

/**
 * Quem pode receber aviso ou compromisso: o dono e as pessoas ativas da equipe.
 * Lido com a chave de serviço (a lista da equipe é do dono), mas só desta empresa.
 */
export async function pessoasDaEmpresa(admin: SupabaseClient, empresa: Empresa): Promise<Array<{ id: string; nome: string; dono: boolean }>> {
  const lista: Array<{ id: string; nome: string; dono: boolean }> = [];
  const { data: dono } = await admin.auth.admin.getUserById(empresa.dono);
  lista.push({ id: empresa.dono, nome: dono?.user?.email ? `${dono.user.email} (dono)` : "Dono da empresa", dono: true });

  const { data: empresas } = await admin.from("companies").select("id").eq("owner_id", empresa.dono).limit(5);
  const ids = Array.from(new Set([empresa.dono, ...((empresas ?? []) as Array<{ id: string }>).map((c) => c.id)]));
  const { data: equipe } = await admin
    .from("company_users")
    .select("user_id,email,active,is_active")
    .in("company_id", ids)
    .limit(200);
  for (const m of (equipe ?? []) as Array<{ user_id: string | null; email: string | null; active: boolean | null; is_active: boolean | null }>) {
    if (!m.user_id || m.user_id === empresa.dono || m.active === false || m.is_active === false) continue;
    if (lista.some((p) => p.id === m.user_id)) continue;
    lista.push({ id: m.user_id, nome: m.email || "Pessoa da equipe", dono: false });
  }
  return lista;
}

/* ------------------------------------------------------------------ gatilhos */

export async function alvosDaRegra(
  db: SupabaseClient,
  empresa: Empresa,
  regra: { gatilho: Gatilho; criada_em: string },
  agora: Date = new Date(),
  nomes: NomesDaFrase = {}
): Promise<Alvo[]> {
  const g = regra.gatilho;
  const tenants = empresa.tenantIds;
  const hoje = hojeNoBrasil(agora);
  const diaMs = 86_400_000;

  switch (g.tipo) {
    case "orcamento_sem_resposta": {
      const ate = new Date(agora.getTime() - g.parametros.dias * diaMs).toISOString();
      const de = new Date(agora.getTime() - (g.parametros.dias + JANELA_EM_DIAS.orcamento_sem_resposta + 1) * diaMs).toISOString();
      const linhas = conferir<LinhaDeOrcamento>(
        await db
          .from("quotes")
          .select("id,number,title,status,client_id,total,updated_at,created_at")
          .in("user_id", tenants)
          .in("status", ["draft", "sent"])
          .lte("updated_at", ate)
          .gte("updated_at", de)
          .order("updated_at", { ascending: false })
          .limit(TETO),
        "os orçamentos"
      );
      const clientes = await clientesPorId(db, empresa, linhas.map((l) => l.client_id));
      return alvosDeOrcamentos(linhas, g.parametros.dias, agora, clientes);
    }

    case "pedido_parado_na_etapa": {
      const etapas = await etapasDaEmpresa(db, empresa);
      const ids = new Set(etapas.map((e) => e.id));
      const nomesComEtapas: NomesDaFrase = { ...nomes, etapas: { ...Object.fromEntries(etapas.map((e) => [e.id, e.nome])), ...(nomes.etapas ?? {}) } };
      const status = g.parametros.etapa
        ? g.parametros.etapa.startsWith("status:")
          ? [g.parametros.etapa.slice("status:".length)]
          : [etapas.find((e) => e.id === g.parametros.etapa)?.status ?? "in_production"]
        : ["pending_production", "in_production"];
      const pedidos = conferir<LinhaDePedido>(
        await db
          .from("orders")
          .select("id,number,status,production_stage_id,client_id,production_updated_at,updated_at,created_at")
          .in("user_id", tenants)
          .in("status", status)
          .order("created_at", { ascending: false })
          .limit(TETO),
        "os pedidos"
      );
      /* Quando cada pedido entrou na etapa em que está: o registro aberto do histórico. */
      const entrada: Record<string, string> = {};
      for (const lote of emLotes(pedidos.map((p) => p.id))) {
        const eventos = conferir<{ order_id: string; started_at: string }>(
          await db
            .from("order_production_events")
            .select("order_id,started_at")
            .in("order_id", lote)
            .eq("kind", "etapa")
            .is("ended_at", null)
            .order("started_at", { ascending: true }),
          "o histórico da produção"
        );
        for (const e of eventos) entrada[e.order_id] = e.started_at;
      }
      const clientes = await clientesPorId(db, empresa, pedidos.map((p) => p.client_id));
      return alvosDePedidosParados(pedidos, entrada, g.parametros, ids, agora, clientes, nomesComEtapas);
    }

    case "pedido_entrou_na_etapa": {
      const etapas = await etapasDaEmpresa(db, empresa);
      const ids = new Set(etapas.map((e) => e.id));
      const nomesComEtapas: NomesDaFrase = { ...nomes, etapas: { ...Object.fromEntries(etapas.map((e) => [e.id, e.nome])), ...(nomes.etapas ?? {}) } };
      /* Nunca antes da regra existir: a regra nova não dispara pelo passado. */
      const semana = new Date(agora.getTime() - JANELA_EM_DIAS.pedido_entrou_na_etapa * diaMs).toISOString();
      const desde = regra.criada_em > semana ? regra.criada_em : semana;
      const eventos = conferir<EventoDeEtapa>(
        await db
          .from("order_production_events")
          .select("id,order_id,status,stage_id,started_at")
          .in("user_id", tenants)
          .eq("kind", "etapa")
          .gte("started_at", desde)
          .order("started_at", { ascending: true })
          .limit(TETO),
        "o histórico da produção"
      );
      const pedidos: Record<string, { number: number | null; client_id: string | null }> = {};
      for (const lote of emLotes(Array.from(new Set(eventos.map((e) => e.order_id))))) {
        const linhas = conferir<{ id: string; number: number | null; client_id: string | null; status: string | null }>(
          await db.from("orders").select("id,number,client_id,status").in("id", lote).in("user_id", tenants),
          "os pedidos"
        );
        for (const p of linhas) if (p.status !== "canceled") pedidos[p.id] = p;
      }
      const clientes = await clientesPorId(db, empresa, Object.values(pedidos).map((p) => p.client_id));
      return alvosDeEntradaNaEtapa(eventos, g.parametros.etapa, desde, ids, pedidos, clientes, nomesComEtapas);
    }

    case "estoque_abaixo": {
      const { item_tipo, item_id, quantidade } = g.parametros;
      let linhas: LinhaDeEstoque[];
      if (item_tipo === "insumo") {
        let consulta = db
          .from("materials")
          .select("id,name,unit,current_stock,min_stock_alert,is_active,track_stock")
          .in("user_id", tenants)
          .eq("track_stock", true)
          .order("name")
          .limit(TETO);
        if (item_id) consulta = consulta.eq("id", item_id);
        const brutas = conferir<{ id: string; name: string | null; unit: string | null; current_stock: number | null; min_stock_alert: number | null; is_active: boolean | null }>(
          await consulta,
          "os insumos"
        );
        linhas = brutas
          .filter((m) => m.is_active !== false)
          .map((m) => ({ id: m.id, nome: m.name ?? "Insumo", unidade: m.unit, atual: m.current_stock, minimo: m.min_stock_alert }));
      } else {
        let consulta = db
          .from("products")
          .select("id,name,unit_type,stock,active,track_inventory")
          .in("user_id", tenants)
          .eq("track_inventory", true)
          .order("name")
          .limit(TETO);
        if (item_id) consulta = consulta.eq("id", item_id);
        const brutas = conferir<{ id: string; name: string | null; unit_type: string | null; stock: number | null; active: boolean | null }>(await consulta, "os produtos");
        linhas = brutas
          .filter((p) => p.active !== false)
          .map((p) => ({ id: p.id, nome: p.name ?? "Produto", unidade: null, atual: p.stock, minimo: null }));
      }
      return alvosDeEstoque(linhas, item_tipo, quantidade, agora);
    }

    case "cliente_sem_comprar": {
      const { data, error } = await db.rpc("regras_clientes_sem_comprar", {
        p_tenants: tenants,
        p_dias: g.parametros.dias,
        p_janela: JANELA_EM_DIAS.cliente_sem_comprar,
        p_limite: TETO,
      });
      const linhas = conferir<LinhaDeClienteSemComprar>({ data, error }, "os clientes sem comprar");
      return alvosDeClientesSemComprar(linhas, agora);
    }

    case "conta_a_pagar_vencendo": {
      const linhas = conferir<{ id: string; description: string | null; due_date: string; status: string | null; amount: number | null; supplier_name: string | null }>(
        await db
          .from("accounts_payable")
          .select("id,description,due_date,status,amount,supplier_name")
          .in("company_id", tenants)
          .eq("status", "PENDING")
          .gte("due_date", hoje)
          .lte("due_date", somarDiasNaData(hoje, g.parametros.dias))
          .order("due_date")
          .limit(TETO),
        "as contas a pagar"
      );
      const contas: LinhaDeConta[] = linhas.map((c) => ({ id: c.id, description: c.description, due_date: c.due_date, status: c.status, valor: c.amount, pessoa: c.supplier_name }));
      return alvosDeContasAPagar(contas, g.parametros.dias, hoje);
    }

    case "conta_a_receber_vencida": {
      const ate = somarDiasNaData(hoje, -g.parametros.dias);
      const de = somarDiasNaData(ate, -JANELA_EM_DIAS.conta_a_receber_vencida);
      const linhas = conferir<{ id: string; description: string | null; due_date: string; status: string | null; total_amount: number | null; paid_amount: number | null; client_name: string | null; client_company: string | null }>(
        await db
          .from("accounts_receivable")
          .select("id,description,due_date,status,total_amount,paid_amount,client_name,client_company")
          .in("company_id", tenants)
          .in("status", ["PENDING", "OVERDUE", "PARTIALLY_PAID"])
          .lte("due_date", ate)
          .gte("due_date", de)
          .order("due_date", { ascending: false })
          .limit(TETO),
        "as contas a receber"
      );
      const contas: LinhaDeConta[] = linhas.map((c) => ({
        id: c.id,
        description: c.description,
        due_date: c.due_date,
        status: c.status,
        valor: Math.max(0, Number(c.total_amount ?? 0) - Number(c.paid_amount ?? 0)),
        pessoa: c.client_company || c.client_name,
      }));
      return alvosDeContasAReceber(contas, g.parametros.dias, hoje);
    }
  }
}

/** As chaves que a regra já tratou, entre as destes alvos. */
export async function chavesJaFeitas(db: SupabaseClient, regraId: string, alvos: Alvo[]): Promise<Set<string>> {
  const feitas = new Set<string>();
  const chaves = Array.from(new Set(alvos.map((a) => a.chave)));
  for (const lote of emLotes(chaves, 50)) {
    const linhas = conferir<{ chave_unica: string }>(
      await db.from("regras_execucoes").select("chave_unica").eq("regra_id", regraId).in("chave_unica", lote),
      "o que a regra já fez"
    );
    for (const l of linhas) feitas.add(l.chave_unica);
  }
  return feitas;
}
