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

import { createAdminClient } from "@/lib/supabase-server";

import { conferirComprasPendentesDaLive } from "@/lib/live/servidor/compras";
import { agendarLembretesDasLives, encerrarLivesEsquecidas, montarLembreteDaLive } from "@/lib/live/servidor/lembretes";

import { esperaDaTentativa, lojasDaFila } from "../fila-regras.ts";
import { apagarObjeto, configuracaoDoR2 } from "../r2";
import { emailDaLojaConfigurado, enviarEmailDaLoja } from "./email-da-loja";
import { usosNosCursos } from "./cursos";
import {
  agendarLembrete,
  montarAvisoDeCortesia,
  montarAvisoDoCertificado,
  montarEntrega,
  montarLembreteDoCurso,
  type MontagemDaEntrega,
  type MotivoDaEntrega,
} from "./entrega";

/**
 * A fila dos produtos digitais: o que não pode depender de alguém estar com a
 * tela aberta.
 *
 * - o e-mail da entrega (`digital_outbox`), com novas tentativas em 1 min,
 *   5 min, 30 min, 2 h e 12 h;
 * - a entrega que falhou dentro do gatilho do banco (`digital_tasks`
 *   'liberar'): tenta de novo, e o pagamento nunca dependeu disso;
 * - o arquivo apagado que passou dos 30 dias na lixeira sai do depósito;
 * - as varreduras: a pré-venda que chegou na data (a cada minuto), os códigos
 *   que esperam (a cada 10 minutos) e o lembrete de quem parou de estudar há
 *   30 dias (a cada hora).
 *
 * Roda dentro do próprio servidor do painel (instrumentation), como a fila das
 * notas. A trava é do banco (`for update skip locked`, 10 minutos de validade).
 *
 * O banco é um só para o computador de desenvolvimento e a produção: fora de
 * produção a fila só liga com DIGITAIS_FILA_LOCAL e só mexe nas lojas de
 * DIGITAIS_LIBERADOS (as lojas de teste). Em produção cuida de todas, então
 * também das liberadas pelo banco (`digitais_ajustes`, migration 193).
 */

type Admin = SupabaseClient;

type Envio = {
  id: string;
  user_id: string;
  tipo: string;
  destino: string;
  dados: Record<string, unknown> | null;
  tentativas: number;
};

type Tarefa = {
  id: string;
  user_id: string | null;
  tipo: string;
  ref: string | null;
  dados: Record<string, unknown> | null;
  tentativas: number;
};

function depois(minutos: number) {
  return new Date(Date.now() + minutos * 60 * 1000).toISOString();
}

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

/* ---------------------------------------------------------------- envios */

async function concluirEnvio(admin: Admin, envio: Envio, resultado: { ok: true } | { ok: false; erro: string; definitivo?: boolean }) {
  const agora = new Date().toISOString();
  if (resultado.ok) {
    await admin.from("digital_outbox").update({ situacao: "enviado", enviado_em: agora, ultimo_erro: null }).eq("id", envio.id);
    return;
  }
  const espera = resultado.definitivo ? null : esperaDaTentativa(envio.tentativas);
  await admin
    .from("digital_outbox")
    .update(
      espera === null
        ? { situacao: resultado.definitivo ? "cancelado" : "falhou", ultimo_erro: resultado.erro.slice(0, 500) }
        : { situacao: "pendente", proxima_em: depois(espera), ultimo_erro: resultado.erro.slice(0, 500) }
    )
    .eq("id", envio.id);
}

/* O e-mail de cada tipo de envio que fala de um pedido. */
const MOTIVO_DO_ENVIO: Record<string, MotivoDaEntrega> = {
  entrega: "entrega",
  lembrete: "lembrete",
  codigo_pendente: "codigo",
  arquivo_final: "arquivo_final",
};

/* Envia o que foi montado e fecha o envio. */
async function enviarMontagem(admin: Admin, envio: Envio, montagem: MontagemDaEntrega): Promise<boolean> {
  if (!montagem.ok) {
    await concluirEnvio(admin, envio, { ok: false, erro: montagem.motivo, definitivo: montagem.definitivo });
    return false;
  }
  const { email } = montagem;
  const enviado = await enviarEmailDaLoja({
    para: email.para,
    assunto: email.assunto,
    html: email.html,
    texto: email.texto,
    nomeDaLoja: email.nomeDaLoja,
    responderPara: email.responderPara,
  });
  await concluirEnvio(admin, envio, enviado.ok ? { ok: true } : { ok: false, erro: enviado.erro });
  return enviado.ok;
}

async function mandarEnvio(admin: Admin, envio: Envio) {
  const dados = envio.dados ?? {};
  const texto = (chave: string) => (typeof dados[chave] === "string" ? (dados[chave] as string) : "");

  /* O aluno parou de estudar: "continue de onde parou". */
  if (envio.tipo === "lembrete_do_curso") {
    const clienteId = texto("client_id");
    const produtoId = texto("product_id");
    await enviarMontagem(
      admin,
      envio,
      clienteId && produtoId
        ? await montarLembreteDoCurso(admin, { lojaId: envio.user_id, clienteId, produtoId, destino: envio.destino })
        : { ok: false, motivo: "lembrete sem cliente ou conteúdo", definitivo: true }
    );
    return;
  }
  /* A live: 1 dia antes e 15 minutos antes. */
  if (envio.tipo === "lembrete_da_live") {
    const salaId = texto("sala_id");
    const quando = texto("quando") === "15" ? "15" : "dia";
    await enviarMontagem(
      admin,
      envio,
      salaId
        ? await montarLembreteDaLive(admin, {
            lojaId: envio.user_id,
            salaId,
            quando,
            orderId: texto("order_id") || null,
            catalogOrderId: texto("catalog_order_id") || null,
            clienteId: texto("client_id") || null,
            destino: envio.destino,
          })
        : { ok: false, motivo: "lembrete sem live", definitivo: true }
    );
    return;
  }
  /* O certificado ficou pronto. */
  if (envio.tipo === "certificado") {
    const certificadoId = texto("certificado_id");
    await enviarMontagem(
      admin,
      envio,
      certificadoId
        ? await montarAvisoDoCertificado(admin, { lojaId: envio.user_id, certificadoId, destino: envio.destino })
        : { ok: false, motivo: "aviso sem certificado", definitivo: true }
    );
    return;
  }
  const motivoDoEnvio = MOTIVO_DO_ENVIO[envio.tipo];
  if (!motivoDoEnvio) {
    await concluirEnvio(admin, envio, { ok: false, erro: `tipo ${envio.tipo} não é enviado por esta fila`, definitivo: true });
    return;
  }
  /* Acesso de cortesia (sem pedido): o aviso leva a Minhas compras. */
  if (envio.dados?.cortesia === true) {
    const clienteId = typeof envio.dados.client_id === "string" ? envio.dados.client_id : "";
    const produtoId = typeof envio.dados.product_id === "string" ? envio.dados.product_id : "";
    const aviso = clienteId && produtoId
      ? await montarAvisoDeCortesia(admin, { lojaId: envio.user_id, clienteId, produtoId, destino: envio.destino })
      : ({ ok: false, motivo: "aviso sem cliente ou produto", definitivo: true } as const);
    if (!aviso.ok) {
      await concluirEnvio(admin, envio, { ok: false, erro: aviso.motivo, definitivo: aviso.definitivo });
      return;
    }
    const enviado = await enviarEmailDaLoja({
      para: aviso.email.para,
      assunto: aviso.email.assunto,
      html: aviso.email.html,
      texto: aviso.email.texto,
      nomeDaLoja: aviso.email.nomeDaLoja,
      responderPara: aviso.email.responderPara,
    });
    await concluirEnvio(admin, envio, enviado.ok ? { ok: true } : { ok: false, erro: enviado.erro });
    return;
  }
  const orderId = typeof envio.dados?.order_id === "string" ? envio.dados.order_id : "";
  if (!orderId) {
    await concluirEnvio(admin, envio, { ok: false, erro: "envio sem pedido", definitivo: true });
    return;
  }
  const catalogOrderId = typeof envio.dados?.catalog_order_id === "string" ? envio.dados.catalog_order_id : null;
  /* A pré-venda que chegou na data volta como entrega, marcada. */
  const motivo: MotivoDaEntrega = envio.tipo === "entrega" && envio.dados?.pre_venda === true ? "pre_venda" : motivoDoEnvio;
  const montagem = await montarEntrega(admin, {
    lojaId: envio.user_id,
    orderId,
    destino: envio.destino,
    catalogOrderId,
    motivo,
  });
  const enviado = await enviarMontagem(admin, envio, montagem);
  /* "Lembrar quem não abriu": uma vez, 24 h depois da entrega. */
  if (enviado && montagem.ok && envio.tipo === "entrega" && montagem.email.lembrarEm24h) {
    await agendarLembrete(admin, { lojaId: envio.user_id, orderId, destino: montagem.email.para, catalogOrderId }).catch(() => undefined);
  }
}

/* ---------------------------------------------------------------- varreduras */

declare global {
  var __varredurasDigitais: { agendados: number; codigos: number; lembretes: number; lives?: number; comprasDaLive?: number } | undefined;
}

const UM_MINUTO = 60_000;

/* O que roda por relógio: cada coisa no seu ritmo, sem travar a fila se falhar. */
async function varrer(admin: Admin, lojas: string[] | null) {
  const marcas = (globalThis.__varredurasDigitais ??= { agendados: 0, codigos: 0, lembretes: 0 });
  const agora = Date.now();
  if (agora - marcas.agendados >= UM_MINUTO) {
    marcas.agendados = agora;
    const { error } = await admin.rpc("liberar_agendados_digitais", { p_limite: 200, p_lojas: lojas });
    if (error) console.error("[digitais] pré-venda: a varredura não respondeu", error.message);
  }
  if (agora - marcas.codigos >= 10 * UM_MINUTO) {
    marcas.codigos = agora;
    const { error } = await admin.rpc("entregar_codigos_pendentes_digitais", { p_lojas: lojas });
    if (error) console.error("[digitais] códigos: a varredura não respondeu", error.message);
  }
  /* As lives: os lembretes por e-mail e a live esquecida no ar. */
  if (agora - (marcas.lives ?? 0) >= UM_MINUTO) {
    marcas.lives = agora;
    await agendarLembretesDasLives(admin, lojas).catch((erro) => console.error("[live] lembretes", textoDoErro(erro)));
    await encerrarLivesEsquecidas(admin, lojas).catch((erro) => console.error("[live] lives esquecidas", textoDoErro(erro)));
  }
  /* Pacotes KtaloguLive pagos que o aviso do Mercado Pago não trouxe (só em produção: a compra é da Ktalogu, não da loja). */
  if (!lojas && agora - (marcas.comprasDaLive ?? 0) >= 10 * UM_MINUTO) {
    marcas.comprasDaLive = agora;
    await conferirComprasPendentesDaLive(admin).catch((erro) => console.error("[live] compras pendentes", textoDoErro(erro)));
  }
  if (agora - marcas.lembretes >= 60 * UM_MINUTO) {
    marcas.lembretes = agora;
    const { error } = await admin.rpc("lembrar_alunos_parados", { p_lojas: lojas, p_limite: 100 });
    if (error) console.error("[digitais] lembretes: a varredura não respondeu", error.message);
  }
}

/* ---------------------------------------------------------------- tarefas */

async function concluirTarefa(admin: Admin, tarefa: Tarefa, resultado: { ok: true } | { ok: false; erro: string; definitivo?: boolean } | { depois: string }) {
  const agora = new Date().toISOString();
  if ("depois" in resultado) {
    await admin.from("digital_tasks").update({ situacao: "pendente", proxima_em: resultado.depois, tentativas: Math.max(0, tarefa.tentativas - 1), atualizado_em: agora }).eq("id", tarefa.id);
    return;
  }
  if (resultado.ok) {
    await admin.from("digital_tasks").update({ situacao: "feito", erro: null, atualizado_em: agora }).eq("id", tarefa.id);
    return;
  }
  const espera = resultado.definitivo ? null : esperaDaTentativa(tarefa.tentativas);
  await admin
    .from("digital_tasks")
    .update(
      espera === null
        ? { situacao: "falhou", erro: resultado.erro.slice(0, 500), atualizado_em: agora }
        : { situacao: "pendente", proxima_em: depois(espera), erro: resultado.erro.slice(0, 500), atualizado_em: agora }
    )
    .eq("id", tarefa.id);
}

/* Motivos em que não há nada a fazer agora: o próprio gatilho age quando o
   pedido mudar de novo (ficar pago, ser reaberto...). */
const SEM_NADA_A_FAZER = new Set(["sem_digital", "nao_pago", "cancelado", "estornado", "pedido", "sem_cliente"]);

async function fazerTarefa(admin: Admin, tarefa: Tarefa) {
  if (tarefa.tipo === "liberar") {
    const pedido = String(tarefa.ref || "");
    if (!/^[0-9a-f-]{36}$/i.test(pedido)) {
      await concluirTarefa(admin, tarefa, { ok: false, erro: "referência inválida", definitivo: true });
      return;
    }
    const { data, error } = await admin.rpc("liberar_entregas_digitais", { p_order_id: pedido, p_forcar: false, p_quem: null });
    if (error) {
      await concluirTarefa(admin, tarefa, { ok: false, erro: error.message });
      return;
    }
    const resultado = (data ?? {}) as { ok?: boolean; motivo?: string };
    if (resultado.ok || SEM_NADA_A_FAZER.has(String(resultado.motivo || ""))) {
      await concluirTarefa(admin, tarefa, { ok: true });
      return;
    }
    await concluirTarefa(admin, tarefa, { ok: false, erro: String(resultado.motivo || "não liberou") });
    return;
  }

  if (tarefa.tipo === "apagar_objeto") {
    const arquivoId = String(tarefa.ref || "");
    const { data } = await admin
      .from("digital_files")
      .select("id,user_id,storage_key,status,remover_do_deposito_em,meta")
      .eq("id", arquivoId)
      .maybeSingle();
    const arquivo = data as {
      id: string;
      user_id: string;
      storage_key: string;
      status: string;
      remover_do_deposito_em: string | null;
      meta: Record<string, unknown> | null;
    } | null;
    /* Sumiu ou voltou a valer: não há o que apagar. */
    if (!arquivo || arquivo.status !== "apagado" || (tarefa.user_id && arquivo.user_id !== tarefa.user_id)) {
      await concluirTarefa(admin, tarefa, { ok: true });
      return;
    }
    if (arquivo.remover_do_deposito_em && new Date(arquivo.remover_do_deposito_em).getTime() > Date.now()) {
      await concluirTarefa(admin, tarefa, { depois: arquivo.remover_do_deposito_em });
      return;
    }
    /* Alguém ainda baixa? Fica (a tela de apagar já confere, isto é a reserva). */
    const { data: entregaveis } = await admin.from("digital_deliverables").select("id").eq("file_id", arquivo.id);
    const ids = ((entregaveis ?? []) as Array<{ id: string }>).map((e) => e.id);
    if (ids.length) {
      const { count } = await admin
        .from("digital_entitlements")
        .select("id", { count: "exact", head: true })
        .in("deliverable_id", ids)
        .in("status", ["ativo", "agendado", "aguardando_arquivo", "pausado"]);
      if ((count ?? 0) > 0) {
        await concluirTarefa(admin, tarefa, { ok: false, erro: "o arquivo ainda tem acesso aberto", definitivo: true });
        return;
      }
    }
    /* Voltou para uma aula de curso? Também fica. */
    const nosCursos = await usosNosCursos(admin, arquivo.user_id, arquivo.id);
    if (nosCursos.produtos.length) {
      await concluirTarefa(admin, tarefa, { ok: false, erro: "o arquivo está num conteúdo em vídeo", definitivo: true });
      return;
    }
    const cfg = configuracaoDoR2();
    if (!cfg) {
      await concluirTarefa(admin, tarefa, { depois: depois(60) });
      return;
    }
    try {
      await apagarObjeto(cfg, arquivo.storage_key);
    } catch (erro) {
      await concluirTarefa(admin, tarefa, { ok: false, erro: textoDoErro(erro) });
      return;
    }
    await admin
      .from("digital_files")
      .update({ meta: { ...(arquivo.meta ?? {}), removido_do_deposito_em: new Date().toISOString() }, atualizado_em: new Date().toISOString() })
      .eq("id", arquivo.id);
    await concluirTarefa(admin, tarefa, { ok: true });
    return;
  }

  await concluirTarefa(admin, tarefa, { ok: false, erro: `tipo ${tarefa.tipo} não é desta fila`, definitivo: true });
}

/* ---------------------------------------------------------------- rodada */

export async function processarFilaDigital(limite = 10): Promise<number> {
  const lojas = lojasDaFila(process.env);
  if (lojas === false) return 0;
  const admin = createAdminClient();
  let feitos = 0;

  await varrer(admin, lojas).catch((erro) => console.error("[digitais] varreduras", textoDoErro(erro)));

  if (emailDaLojaConfigurado()) {
    const { data, error } = await admin.rpc("pegar_envios_digitais", { p_limite: limite, p_lojas: lojas });
    if (error) {
      console.error("[digitais] a fila de e-mails não respondeu", error.message);
    } else {
      for (const envio of (data ?? []) as Envio[]) {
        try {
          await mandarEnvio(admin, envio);
        } catch (erro) {
          await concluirEnvio(admin, envio, { ok: false, erro: textoDoErro(erro) }).catch(() => undefined);
        }
        feitos += 1;
      }
    }
  }

  const { data: tarefas, error: erroDasTarefas } = await admin.rpc("pegar_tarefas_digitais", { p_limite: limite, p_lojas: lojas });
  if (erroDasTarefas) {
    console.error("[digitais] a fila de trabalhos não respondeu", erroDasTarefas.message);
  } else {
    for (const tarefa of (tarefas ?? []) as Tarefa[]) {
      try {
        await fazerTarefa(admin, tarefa);
      } catch (erro) {
        await concluirTarefa(admin, tarefa, { ok: false, erro: textoDoErro(erro) }).catch(() => undefined);
      }
      feitos += 1;
    }
  }
  return feitos;
}

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

const INTERVALO_MS = 15_000;

async function rodada() {
  const estado = globalThis.__filaDigital;
  if (!estado) return;
  if (estado.rodando) {
    estado.pendente = true;
    return;
  }
  estado.rodando = true;
  try {
    do {
      estado.pendente = false;
      const feitos = await processarFilaDigital();
      if (feitos >= 10) estado.pendente = true;
    } while (estado.pendente);
  } catch (erro) {
    console.error("[digitais] rodada da fila falhou", textoDoErro(erro));
  } finally {
    estado.rodando = false;
  }
}

export function iniciarFilaDigital() {
  if (process.env.NODE_ENV === "test") return;
  if (globalThis.__filaDigital?.iniciada) return;
  const lojas = lojasDaFila(process.env);
  if (lojas === false) {
    if (process.env.NODE_ENV !== "production") console.info("[digitais] fila desligada neste computador (DIGITAIS_FILA_LOCAL)");
    return;
  }
  if (!emailDaLojaConfigurado()) console.info("[digitais] fila sem e-mail: falta RESEND_API_KEY / EMAIL_REMETENTE (os trabalhos seguem)");
  globalThis.__filaDigital = { iniciada: true, rodando: false, pendente: false };
  setInterval(() => {
    void rodada();
  }, INTERVALO_MS);
  setTimeout(() => void rodada(), 5000);
}

/** Chamado logo depois de pôr algo na fila que alguém está esperando ver. */
export function acordarFilaDigital() {
  if (!globalThis.__filaDigital?.iniciada) return;
  setTimeout(() => void rodada(), 50);
}
