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

import { assinaturaDaEmpresa } from "@/lib/fiscal/servidor/assinatura-da-empresa";
import { createAdminClient } from "@/lib/supabase-server";

import {
  ACOES_POR_REGRA_POR_VOLTA,
  ACOES_POR_VOLTA,
  conferirAcao,
  conferirGatilho,
  empresasDaFila,
  novosAlvos,
  REGRAS_POR_VOLTA,
  type Acao,
  type Gatilho,
} from "../regras";
import { categoriaDaEmpresa, executarAcao } from "./acoes";
import { alvosDaRegra, chavesJaFeitas, pessoasDaEmpresa, type Empresa } from "./avaliar";

/**
 * A fila das regras: a cada 5 minutos, confere as regras ligadas de todas as
 * empresas e executa as ações do que disparou.
 *
 * Roda dentro do próprio servidor do painel (instrumentation), como a fila das
 * notas e a dos digitais. Não repete: antes de agir, grava a linha em
 * regras_execucoes, e a chave única (regra + registro + ocorrência) faz o
 * segundo servidor, se houver, desistir daquele registro.
 *
 * Cada regra roda isolada: o erro de uma fica em `ultimo_erro` dela e a volta
 * segue. Há teto por regra e por volta; o que sobra fica para a volta seguinte.
 * Só empresas com a assinatura em dia.
 *
 * O banco é um só para desenvolvimento e produção: fora de produção a fila só
 * liga com REGRAS_FILA_LOCAL e só mexe nas empresas de REGRAS_LIBERADAS.
 * REGRAS_DESLIGADAS=1 desliga em qualquer lugar.
 */

type LinhaDaRegra = {
  id: string;
  company_id: string;
  nome: string;
  gatilho: unknown;
  acoes: unknown;
  criada_em: string;
  vezes: number | null;
};

declare global {
  var __filaDasRegras: { iniciada: boolean; rodando: boolean; empresas: string[] | null } | undefined;
}

const INTERVALO_MS = 5 * 60 * 1000;
const REGRAS_POR_EMPRESA_POR_VOLTA = 20;

function textoDoErro(erro: unknown) {
  return (erro instanceof Error ? erro.message : String(erro)).slice(0, 480);
}

async function empresaDoDono(admin: SupabaseClient, dono: string): Promise<Empresa> {
  const tenantIds = new Set<string>([dono]);
  const { data } = await admin.from("companies").select("id").eq("owner_id", dono).limit(5);
  for (const c of (data ?? []) as Array<{ id: string }>) tenantIds.add(c.id);
  return { dono, tenantIds: Array.from(tenantIds) };
}

function regraValida(linha: LinhaDaRegra): { gatilho: Gatilho; acoes: Acao[] } | null {
  const gatilho = conferirGatilho(linha.gatilho);
  if (!gatilho.ok || !Array.isArray(linha.acoes)) return null;
  const acoes: Acao[] = [];
  for (const bruta of linha.acoes) {
    const acao = conferirAcao(bruta);
    if (!acao.ok) return null;
    acoes.push(acao.valor);
  }
  return acoes.length ? { gatilho: gatilho.valor, acoes } : null;
}

/** Uma regra: acha o que disparou, grava a vez e age. Devolve quantas vezes agiu. */
async function rodarRegra(
  admin: SupabaseClient,
  empresa: Empresa,
  linha: LinhaDaRegra,
  pessoas: Set<string>,
  categoria: () => Promise<string | null>,
  limite: number
): Promise<number> {
  const agora = new Date();
  const valida = regraValida(linha);
  if (!valida) {
    await admin
      .from("regras")
      .update({ ultima_verificacao: agora.toISOString(), ultimo_erro: "A regra está com um formato que não vale mais. Abra e salve de novo." })
      .eq("id", linha.id);
    return 0;
  }

  const alvos = await alvosDaRegra(admin, empresa, { gatilho: valida.gatilho, criada_em: linha.criada_em }, agora);
  const feitas = await chavesJaFeitas(admin, linha.id, alvos);
  const novos = novosAlvos(alvos, feitas, Math.min(limite, ACOES_POR_REGRA_POR_VOLTA));

  let vezes = 0;
  for (const alvo of novos) {
    /* Grava antes de agir: se outra volta chegou primeiro, a chave única barra. */
    const { data: reservada, error } = await admin
      .from("regras_execucoes")
      .upsert(
        {
          regra_id: linha.id,
          company_id: empresa.dono,
          alvo_tipo: alvo.tipo,
          alvo_id: alvo.id,
          alvo_titulo: alvo.titulo.slice(0, 300),
          chave_unica: alvo.chave.slice(0, 200),
          ok: null,
          detalhe: "em andamento",
        },
        { onConflict: "regra_id,chave_unica", ignoreDuplicates: true }
      )
      .select("id");
    if (error) throw new Error(`não foi possível registrar a execução: ${error.message}`);
    const execucao = ((reservada ?? []) as Array<{ id: string }>)[0];
    if (!execucao) continue;

    const resultados = [];
    for (const acao of valida.acoes) {
      resultados.push(
        await executarAcao({ admin, empresa, regra: { id: linha.id, nome: linha.nome }, pessoas, categoria }, acao, alvo)
      );
    }
    const ok = resultados.every((r) => r.ok);
    const detalhe = resultados.map((r) => r.texto).join("; ");
    await admin
      .from("regras_execucoes")
      .update({ ok, detalhe: (detalhe.charAt(0).toUpperCase() + detalhe.slice(1)).slice(0, 1000) })
      .eq("id", execucao.id);
    vezes += 1;
  }

  await admin
    .from("regras")
    .update({ ultima_verificacao: new Date().toISOString(), ultimo_erro: null, ...(vezes ? { vezes: Number(linha.vezes ?? 0) + vezes } : {}) })
    .eq("id", linha.id);
  return vezes;
}

/** Uma volta: as regras ligadas há mais tempo sem conferência vão primeiro. */
export async function voltaDasRegras(admin: SupabaseClient, empresasLiberadas: string[] | null): Promise<{ regras: number; acoes: number }> {
  let consulta = admin
    .from("regras")
    .select("id,company_id,nome,gatilho,acoes,criada_em,vezes")
    .eq("ativa", true)
    .order("ultima_verificacao", { ascending: true, nullsFirst: true })
    .limit(REGRAS_POR_VOLTA);
  if (empresasLiberadas) consulta = consulta.in("company_id", empresasLiberadas);
  const { data, error } = await consulta;
  if (error) throw new Error(`não foi possível ler as regras: ${error.message}`);
  const linhas = (data ?? []) as LinhaDaRegra[];

  /* Teto por empresa em cada volta: uma empresa com muitas regras não toma a
     volta inteira das outras. As que sobram continuam na frente da fila e
     entram na volta seguinte. */
  const porEmpresa = new Map<string, LinhaDaRegra[]>();
  for (const l of linhas) {
    const daEmpresa = porEmpresa.get(l.company_id) ?? [];
    if (daEmpresa.length < REGRAS_POR_EMPRESA_POR_VOLTA) porEmpresa.set(l.company_id, [...daEmpresa, l]);
  }

  let acoes = 0;
  let regras = 0;
  for (const [dono, daEmpresa] of porEmpresa) {
    if (acoes >= ACOES_POR_VOLTA) break;
    try {
      const assinatura = await assinaturaDaEmpresa(admin, dono);
      if (!assinatura.ativa) {
        /* Assinatura parada: as regras esperam, sem erro, e não ficam na frente da fila. */
        await admin.from("regras").update({ ultima_verificacao: new Date().toISOString() }).in("id", daEmpresa.map((r) => r.id));
        continue;
      }
      const empresa = await empresaDoDono(admin, dono);
      const pessoas = new Set((await pessoasDaEmpresa(admin, empresa)).map((p) => p.id));
      const categoria = categoriaDaEmpresa(admin, dono);
      for (const linha of daEmpresa) {
        if (acoes >= ACOES_POR_VOLTA) break;
        regras += 1;
        try {
          acoes += await rodarRegra(admin, empresa, linha, pessoas, categoria, ACOES_POR_VOLTA - acoes);
        } catch (erro) {
          console.error(`[regras] a regra ${linha.id} falhou`, textoDoErro(erro));
          await admin
            .from("regras")
            .update({ ultima_verificacao: new Date().toISOString(), ultimo_erro: "A última conferência falhou. A regra tenta de novo sozinha." })
            .eq("id", linha.id);
        }
      }
    } catch (erro) {
      console.error(`[regras] a empresa ${dono} falhou`, textoDoErro(erro));
      /* Sem isto as regras dela ficariam para sempre na frente da fila,
         falhando a cada volta e empurrando as outras empresas para trás. */
      const { error: erroAoMarcar } = await admin
        .from("regras")
        .update({ ultima_verificacao: new Date().toISOString() })
        .in("id", daEmpresa.map((r) => r.id));
      if (erroAoMarcar) console.error(`[regras] não foi possível marcar as regras da empresa ${dono}`, erroAoMarcar.message);
    }
  }
  return { regras, acoes };
}

async function rodada() {
  const estado = globalThis.__filaDasRegras;
  if (!estado || estado.rodando) return;
  estado.rodando = true;
  try {
    const { acoes } = await voltaDasRegras(createAdminClient(), estado.empresas);
    if (acoes) console.info(`[regras] ${acoes} ${acoes === 1 ? "ação" : "ações"} nesta volta`);
  } catch (erro) {
    console.error("[regras] a volta falhou", textoDoErro(erro));
  } finally {
    estado.rodando = false;
  }
}

export function iniciarFilaDasRegras() {
  if (process.env.NODE_ENV === "test") return;
  if (globalThis.__filaDasRegras?.iniciada) return;
  const empresas = empresasDaFila(process.env);
  if (empresas === false) {
    console.info("[regras] fila desligada neste servidor (REGRAS_DESLIGADAS, ou fora de produção sem REGRAS_FILA_LOCAL/REGRAS_LIBERADAS)");
    return;
  }
  if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
    console.info("[regras] fila desligada: falta SUPABASE_SERVICE_ROLE_KEY");
    return;
  }
  globalThis.__filaDasRegras = { iniciada: true, rodando: false, empresas };
  setInterval(() => void rodada(), INTERVALO_MS);
  setTimeout(() => void rodada(), 60_000);
}
