import { ambienteFiscal, pendenciasDoServidor, type AmbienteFiscal } from "@/lib/fiscal/ambiente";
import { lerPreferencias } from "@/lib/fiscal/emissor";
import { createAdminClient } from "@/lib/supabase-server";

import { garantirAvisoDaSpedy } from "./avisos-spedy";
import { agendarTarefa, registrarEvento, type Admin, type TipoDeTarefa } from "./base";
import { conferirCompra } from "./compras";
import { baixarArquivos, consultarNota, emitirDoPedido, enviarNota, type EscolhasDaEmissao } from "./notas";
import { sincronizarNotasRecebidas } from "./recebidas";

/**
 * A fila das notas: o que não pode depender de alguém estar com a tela aberta.
 *
 * Roda dentro do próprio servidor (instrumentation), a cada poucos segundos,
 * pegando só as tarefas do ambiente deste servidor — o banco é um só para o
 * `next dev` e a produção. A trava é do banco (`for update skip locked`, com
 * 5 minutos de validade), então dois processos nunca pegam a mesma tarefa.
 */

type Tarefa = {
  id: number;
  ambiente: AmbienteFiscal;
  tipo: TipoDeTarefa;
  empresa_id: string | null;
  nota_id: string | null;
  compra_id: string | null;
  pedido_id: string | null;
  dados: Record<string, unknown> | null;
  tentativas: number;
  max_tentativas: number;
};

async function executar(admin: Admin, tarefa: Tarefa) {
  const dados = tarefa.dados ?? {};
  switch (tarefa.tipo) {
    case "emitir":
      if (tarefa.nota_id) await enviarNota(admin, tarefa.nota_id, { emitir: true, ator: null });
      return;
    case "consultar":
      if (tarefa.nota_id) await consultarNota(admin, tarefa.nota_id, Number(dados.rodada ?? 0) || 0);
      return;
    case "baixar_arquivos":
      if (tarefa.nota_id) await baixarArquivos(admin, tarefa.nota_id);
      return;
    case "conferir_compra":
      if (tarefa.compra_id) await conferirCompra(admin, { compraId: tarefa.compra_id });
      return;
    case "emitir_pedido": {
      if (!tarefa.pedido_id || !tarefa.empresa_id) return;
      const resultado = await emitirDoPedido(admin, {
        empresaId: tarefa.empresa_id,
        ambiente: tarefa.ambiente,
        pedidoId: tarefa.pedido_id,
        usuarioId: typeof dados.usuarioId === "string" ? dados.usuarioId : null,
        escolhas: (dados.escolhas ?? {}) as EscolhasDaEmissao,
      });
      if (resultado.bloqueado) {
        const bloqueios = resultado.pendencias.filter((p) => p.nivel === "bloqueio");
        /* Sem bloqueio e sem nada a emitir: a nota já saiu (o balcão emite na hora e esta tarefa é só a reserva). */
        if (!bloqueios.length) return;
        throw new PendenciaDeEmissao(bloqueios.map((p) => p.titulo).join(" · ") || "Nota não emitida: há pendências.");
      }
      return;
    }
    default:
      return;
  }
}

/** Pendência de cadastro não se resolve tentando de novo: conclui com o motivo. */
class PendenciaDeEmissao extends Error {}

function esperaDaTentativa(tentativas: number) {
  return Math.min(3600, 30 * 2 ** Math.max(0, tentativas - 1));
}

export async function processarFila(ambiente: AmbienteFiscal = ambienteFiscal(), limite = 10) {
  const admin = createAdminClient();
  const { data, error } = await admin.rpc("fiscal_pegar_tarefas", { p_ambiente: ambiente, p_limite: limite });
  if (error) {
    console.error("[fiscal] falha ao pegar tarefas", error.message);
    return 0;
  }
  const tarefas = (data ?? []) as Tarefa[];
  for (const tarefa of tarefas) {
    try {
      await executar(admin, tarefa);
      await admin.rpc("fiscal_concluir_tarefa", { p_id: tarefa.id, p_erro: null, p_reagendar_em: null });
    } catch (erro) {
      const mensagem = erro instanceof Error ? erro.message : String(erro);
      const desistir = erro instanceof PendenciaDeEmissao || tarefa.tentativas >= tarefa.max_tentativas;
      const reagendar = desistir ? null : new Date(Date.now() + esperaDaTentativa(tarefa.tentativas) * 1000).toISOString();
      await admin.rpc("fiscal_concluir_tarefa", { p_id: tarefa.id, p_erro: mensagem.slice(0, 1000), p_reagendar_em: reagendar });
      if (!(erro instanceof PendenciaDeEmissao)) console.error(`[fiscal] tarefa ${tarefa.tipo} #${tarefa.id} falhou:`, mensagem);
    }
  }
  await rotinas(admin, ambiente);
  return tarefas.length;
}

async function pegarRotina(admin: Admin, nome: string, ambiente: AmbienteFiscal, intervalo: string) {
  const { data, error } = await admin.rpc("fiscal_pegar_rotina", { p_nome: nome, p_ambiente: ambiente, p_intervalo: intervalo });
  return !error && data === true;
}

async function rotinas(admin: Admin, ambiente: AmbienteFiscal) {
  if (await pegarRotina(admin, "conferir_compras", ambiente, "10 minutes")) {
    const doisMinutos = new Date(Date.now() - 2 * 60 * 1000).toISOString();
    const tresDias = new Date(Date.now() - 3 * 24 * 3600 * 1000).toISOString();
    const { data } = await admin
      .from("fiscal_compras")
      .select("id")
      .eq("ambiente", ambiente)
      .eq("status", "pendente")
      .lt("created_at", doisMinutos)
      .gt("created_at", tresDias)
      .limit(30);
    for (const compra of data ?? []) {
      await agendarTarefa(admin, { ambiente, tipo: "conferir_compra", compraId: String(compra.id) });
    }
  }

  if (await pegarRotina(admin, "notas_paradas", ambiente, "15 minutes")) {
    const dezMinutos = new Date(Date.now() - 10 * 60 * 1000).toISOString();
    const tresDias = new Date(Date.now() - 3 * 24 * 3600 * 1000).toISOString();
    const { data } = await admin
      .from("fiscal_notas")
      .select("id,empresa_id")
      .eq("ambiente", ambiente)
      .in("status", ["enviando", "processando", "cancelando", "contingencia"])
      .lt("updated_at", dezMinutos)
      .gt("created_at", tresDias)
      .limit(50);
    for (const nota of data ?? []) {
      await agendarTarefa(admin, { ambiente, tipo: "consultar", empresaId: String(nota.empresa_id), notaId: String(nota.id), dados: { rodada: 3 } });
    }

    /* Reserva presa: a nota reservou uma nota do pacote e nunca chegou a sair
       (o servidor caiu entre reservar e enviar). A empresa ficava com uma nota
       a menos até alguém descartar a prévia à mão. Solta a reserva e deixa a
       nota como erro, para a tela oferecer o reenvio. */
    const { data: presas } = await admin
      .from("fiscal_notas")
      .select("id,empresa_id")
      .eq("ambiente", ambiente)
      .eq("status", "preparando")
      .eq("credito", "reservado")
      .lt("updated_at", dezMinutos)
      .limit(50);
    for (const nota of presas ?? []) {
      await admin.rpc("fiscal_liberar_credito", { p_nota: nota.id });
      await admin
        .from("fiscal_notas")
        .update({
          status: "erro",
          mensagem: "O envio foi interrompido antes de sair. Reenvie a nota.",
          mensagem_codigo: "ENVIO_INTERROMPIDO",
          updated_at: new Date().toISOString(),
        })
        .eq("id", nota.id)
        .eq("status", "preparando");
      await registrarEvento(admin, {
        notaId: String(nota.id),
        empresaId: String(nota.empresa_id),
        tipo: "reserva_devolvida",
        titulo: "Envio interrompido: a nota do pacote voltou para o saldo",
      });
    }
  }

  /* O aviso da Spedy (webhook) aponta para este servidor e fica ligado: a Spedy
     desliga o aviso depois de 5 falhas seguidas. Sem https não há aviso, e a
     consulta periódica das notas cobre sozinha. */
  if (await pegarRotina(admin, "aviso_da_spedy", ambiente, "6 hours")) {
    const base = String(process.env.NEXT_PUBLIC_ADMIN_BASE_URL || process.env.ADMIN_BASE_URL || process.env.NEXT_PUBLIC_ADMIN_URL || "").trim();
    if (base.startsWith("https://")) {
      try {
        const resultado = await garantirAvisoDaSpedy(ambiente, base);
        if (resultado.situacao !== "ok") console.info(`[fiscal] aviso da Spedy ${resultado.situacao}: ${resultado.url}`);
      } catch (erro) {
        console.error("[fiscal] não foi possível conferir o aviso da Spedy", erro instanceof Error ? erro.message : erro);
      }
    }
  }

  /* Notas recebidas: a Spedy consulta a distribuição da SEFAZ por conta dela, e
     aqui só trazemos o que já chegou — de hora em hora, para o sino avisar sem
     ninguém precisar abrir a tela. Pedir busca a cada rodada faria a SEFAZ
     bloquear o CNPJ por consumo indevido. */
  if (await pegarRotina(admin, "notas_recebidas", ambiente, "1 hour")) {
    const { data } = await admin
      .from("fiscal_emissores")
      .select("empresa_id,preferencias")
      .eq("ambiente", ambiente)
      .eq("status", "ativo")
      .limit(200);
    for (const emissor of data ?? []) {
      if (lerPreferencias(emissor.preferencias).receberNotas !== true) continue;
      try {
        await sincronizarNotasRecebidas(admin, { empresaId: String(emissor.empresa_id), ambiente, pedirBusca: false });
      } catch (erro) {
        console.error("[fiscal] notas recebidas não vieram", erro instanceof Error ? erro.message : erro);
      }
    }
  }

  if (await pegarRotina(admin, "arquivos_faltando", ambiente, "30 minutes")) {
    const cincoMinutos = new Date(Date.now() - 5 * 60 * 1000).toISOString();
    const { data } = await admin
      .from("fiscal_notas")
      .select("id,empresa_id")
      .eq("ambiente", ambiente)
      .eq("status", "autorizada")
      .is("pdf_path", null)
      .lt("updated_at", cincoMinutos)
      .limit(50);
    for (const nota of data ?? []) {
      await agendarTarefa(admin, { ambiente, tipo: "baixar_arquivos", empresaId: String(nota.empresa_id), notaId: String(nota.id) });
    }
  }
}

declare global {
  var __filaFiscal: { iniciada: boolean; rodando: boolean; pendente: boolean } | undefined;
}

const INTERVALO_MS = 10_000;

async function rodada() {
  const estado = globalThis.__filaFiscal;
  if (!estado) return;
  if (estado.rodando) {
    estado.pendente = true;
    return;
  }
  estado.rodando = true;
  try {
    do {
      estado.pendente = false;
      const feitas = await processarFila();
      if (feitas >= 10) estado.pendente = true;
    } while (estado.pendente);
  } catch (erro) {
    console.error("[fiscal] rodada da fila falhou", erro instanceof Error ? erro.message : erro);
  } finally {
    estado.rodando = false;
  }
}

export function iniciarFilaFiscal() {
  if (process.env.NODE_ENV === "test") return;
  if (globalThis.__filaFiscal?.iniciada) return;
  const faltando = pendenciasDoServidor();
  if (faltando.length) {
    console.info(`[fiscal] fila das notas desligada: falta ${faltando.join(", ")}`);
    return;
  }
  globalThis.__filaFiscal = { iniciada: true, rodando: false, pendente: false };
  setInterval(() => {
    void rodada();
  }, INTERVALO_MS);
  setTimeout(() => void rodada(), 3000);
}

/** Chamado logo depois de agendar algo que o usuário está esperando ver. */
export function acordarFila() {
  if (!globalThis.__filaFiscal?.iniciada) return;
  setTimeout(() => void rodada(), 50);
}
