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

import { chaveDoCodigo, CODIGOS_POR_VEZ, lerListaDeCodigos, mascararCodigo, situacaoDoEstoque } from "../codigos.ts";
import { lerConfigDigital } from "../produto";
import { ErroDigital } from "./contexto";
import { atualizarOQueRecebe, lerProdutoDaLoja } from "./produtos";

/*
 * Os códigos e licenças de um produto: a lista que a loja colou, quantos
 * estão livres, quem levou cada um e quem está esperando. Entregar é do banco
 * (`entregar_codigos_digitais`): um por unidade comprada, sem repetir, na
 * ordem das compras.
 */

export type CodigoNaTela = {
  id: string;
  mascarado: string;
  situacao: "livre" | "entregue";
  entregueEm: string | null;
  cliente: string | null;
  pedido: number | null;
  criadoEm: string;
};

export type CodigosDoProduto = {
  entregavelId: string;
  livres: number;
  entregues: number;
  esperando: number;
  /* Vendidos na pré-venda: o código sai no dia do lançamento, e até lá fica guardado. */
  reservados: number;
  /* Os livres que ainda podem ser vendidos (sem os guardados para quem já comprou). */
  aVenda: number;
  avisarCom: number;
  situacao: "esgotado" | "acabando" | "ok";
  codigos: CodigoNaTela[];
};

/* O produto de código tem um item "Código de licença" (a unidade que leva o código). */
export async function garantirEntregavelDeCodigos(admin: SupabaseClient, lojaId: string, produtoId: string): Promise<string> {
  const { data } = await admin
    .from("digital_deliverables")
    .select("id,ativo")
    .eq("user_id", lojaId)
    .eq("product_id", produtoId)
    .eq("tipo", "codigos")
    .is("apagado_em", null)
    .order("criado_em", { ascending: true })
    .limit(1);
  const existente = ((data ?? []) as Array<{ id: string; ativo: boolean }>)[0];
  if (existente) {
    if (!existente.ativo) await admin.from("digital_deliverables").update({ ativo: true, atualizado_em: new Date().toISOString() }).eq("id", existente.id);
    return existente.id;
  }
  const { data: criado, error } = await admin
    .from("digital_deliverables")
    .insert({
      user_id: lojaId,
      product_id: produtoId,
      tipo: "codigos",
      titulo: "Código de licença",
      descricao: "Um código para cada unidade comprada.",
      posicao: 0,
    })
    .select("id")
    .single();
  if (error || !criado) throw new ErroDigital("Não foi possível preparar a lista de códigos.", 500);
  await atualizarOQueRecebe(admin, lojaId, produtoId).catch(() => undefined);
  return (criado as { id: string }).id;
}

async function contar(consulta: PromiseLike<{ count: number | null }>): Promise<number> {
  const { count } = await consulta;
  return count ?? 0;
}

export async function lerCodigos(admin: SupabaseClient, lojaId: string, produtoId: string): Promise<CodigosDoProduto> {
  const produto = await lerProdutoDaLoja(admin, lojaId, produtoId);
  const config = lerConfigDigital(produto.digital_config);
  if (config.tipo !== "codigo") throw new ErroDigital("Este produto não é de código ou licença.", 400);
  const entregavelId = await garantirEntregavelDeCodigos(admin, lojaId, produtoId);

  const [livres, entregues, esperando, reservados, lista] = await Promise.all([
    contar(
      admin
        .from("digital_codes")
        .select("id", { count: "exact", head: true })
        .eq("deliverable_id", entregavelId)
        .is("entitlement_id", null)
        .is("apagado_em", null)
    ),
    contar(
      admin
        .from("digital_codes")
        .select("id", { count: "exact", head: true })
        .eq("deliverable_id", entregavelId)
        .not("entitlement_id", "is", null)
    ),
    contar(
      admin
        .from("digital_entitlements")
        .select("id", { count: "exact", head: true })
        .eq("deliverable_id", entregavelId)
        .eq("status", "aguardando_codigo")
    ),
    contar(
      admin
        .from("digital_entitlements")
        .select("id", { count: "exact", head: true })
        .eq("deliverable_id", entregavelId)
        .eq("status", "agendado")
    ),
    admin
      .from("digital_codes")
      .select("id,codigo,entitlement_id,entregue_em,criado_em,digital_entitlements(order_id,clients(name),orders(number))")
      .eq("user_id", lojaId)
      .eq("deliverable_id", entregavelId)
      .is("apagado_em", null)
      .order("criado_em", { ascending: false })
      .limit(300),
  ]);
  if (lista.error) throw new ErroDigital("Não foi possível ler os códigos.", 500);

  type Linha = {
    id: string;
    codigo: string;
    entitlement_id: string | null;
    entregue_em: string | null;
    criado_em: string;
    digital_entitlements: { order_id: string | null; clients: { name: string | null } | null; orders: { number: number | null } | null } | null;
  };
  const aVenda = Math.max(0, livres - esperando - reservados);
  return {
    entregavelId,
    livres,
    entregues,
    esperando,
    reservados,
    aVenda,
    avisarCom: config.avisarCodigosCom,
    situacao: situacaoDoEstoque(aVenda, config.avisarCodigosCom),
    codigos: ((lista.data ?? []) as unknown as Linha[]).map((c) => ({
      id: c.id,
      mascarado: mascararCodigo(c.codigo),
      situacao: c.entitlement_id ? "entregue" : "livre",
      entregueEm: c.entregue_em,
      cliente: c.digital_entitlements?.clients?.name?.trim() || null,
      pedido: c.digital_entitlements?.orders?.number ?? null,
      criadoEm: c.criado_em,
    })),
  };
}

/** Acrescenta os códigos colados (ou importados) e já entrega a quem estava esperando. */
export async function adicionarCodigos(
  admin: SupabaseClient,
  params: { lojaId: string; produtoId: string; texto: unknown }
): Promise<{ adicionados: number; repetidos: number; grandesDemais: number; entregues: number }> {
  const produto = await lerProdutoDaLoja(admin, params.lojaId, params.produtoId);
  if (lerConfigDigital(produto.digital_config).tipo !== "codigo") throw new ErroDigital("Este produto não é de código ou licença.", 400);
  const texto = typeof params.texto === "string" ? params.texto : "";
  const lidos = lerListaDeCodigos(texto);
  if (!lidos.codigos.length) {
    throw new ErroDigital(
      lidos.grandesDemais ? "Os códigos passaram de 500 letras. Confira a lista." : "Cole os códigos, um por linha.",
      400
    );
  }
  if (lidos.codigos.length > CODIGOS_POR_VEZ) throw new ErroDigital(`Mande até ${CODIGOS_POR_VEZ} códigos de cada vez.`, 400);
  const entregavelId = await garantirEntregavelDeCodigos(admin, params.lojaId, params.produtoId);

  /* Os que já estão no produto (o banco também confere: isto é para contar). */
  const existentes = new Set<string>();
  for (let pagina = 0; pagina < 40; pagina += 1) {
    const { data } = await admin
      .from("digital_codes")
      .select("codigo")
      .eq("deliverable_id", entregavelId)
      .is("apagado_em", null)
      .range(pagina * 1000, pagina * 1000 + 999);
    const linhas = (data ?? []) as Array<{ codigo: string }>;
    for (const linha of linhas) existentes.add(chaveDoCodigo(linha.codigo));
    if (linhas.length < 1000) break;
  }
  const novos = lidos.codigos.filter((c) => !existentes.has(chaveDoCodigo(c)));
  const repetidos = lidos.repetidosNoTexto + (lidos.codigos.length - novos.length);

  for (let i = 0; i < novos.length; i += 500) {
    const { error } = await admin.from("digital_codes").insert(
      novos.slice(i, i + 500).map((codigo) => ({
        user_id: params.lojaId,
        product_id: params.produtoId,
        deliverable_id: entregavelId,
        codigo: codigo.trim(),
      }))
    );
    if (error) {
      if (error.code === "23505") throw new ErroDigital("Alguns desses códigos acabaram de entrar por outra tela. Recarregue e tente de novo.", 409);
      throw new ErroDigital("Não foi possível salvar os códigos.", 500);
    }
  }

  const { data: entregues } = await admin.rpc("entregar_codigos_digitais", { p_deliverable_id: entregavelId, p_pedidos_sem_aviso: [] });
  return { adicionados: novos.length, repetidos, grandesDemais: lidos.grandesDemais, entregues: Number(entregues ?? 0) };
}

/** Tira um código que ainda não foi entregue (o entregue fica: o comprador já viu). */
export async function apagarCodigo(admin: SupabaseClient, params: { lojaId: string; produtoId: string; codigoId: string }) {
  if (!/^[0-9a-f-]{36}$/i.test(params.codigoId)) throw new ErroDigital("Código não encontrado.", 404);
  const { data } = await admin
    .from("digital_codes")
    .select("id,entitlement_id,deliverable_id")
    .eq("id", params.codigoId)
    .eq("user_id", params.lojaId)
    .eq("product_id", params.produtoId)
    .is("apagado_em", null)
    .maybeSingle();
  const linha = data as { id: string; entitlement_id: string | null; deliverable_id: string } | null;
  if (!linha) throw new ErroDigital("Código não encontrado.", 404);
  if (linha.entitlement_id) throw new ErroDigital("Este código já foi entregue a um comprador e fica guardado.", 409);
  /* Os livres que sobram precisam cobrir quem já comprou e espera o seu (na pré-venda ou sem código ainda). */
  const [livres, guardados] = await Promise.all([
    contar(
      admin
        .from("digital_codes")
        .select("id", { count: "exact", head: true })
        .eq("deliverable_id", linha.deliverable_id)
        .is("entitlement_id", null)
        .is("apagado_em", null)
    ),
    contar(
      admin
        .from("digital_entitlements")
        .select("id", { count: "exact", head: true })
        .eq("deliverable_id", linha.deliverable_id)
        .in("status", ["agendado", "aguardando_codigo"])
    ),
  ]);
  if (livres - 1 < guardados) {
    throw new ErroDigital("Este código está guardado para quem já comprou na pré-venda. Adicione outro antes de tirar este.", 409);
  }
  const { error } = await admin
    .from("digital_codes")
    .update({ apagado_em: new Date().toISOString() })
    .eq("id", linha.id)
    .is("entitlement_id", null);
  if (error) throw new ErroDigital("Não foi possível tirar o código.", 500);
}

/* aVenda: os livres sem os guardados para quem já comprou (esperando ou na pré-venda). */
export type EstoqueDeCodigos = { produtoId: string; livres: number; total: number; esperando: number; reservados: number; aVenda: number; avisarCom: number };

/** Os números dos produtos de código da loja (lista de produtos e Visão geral). */
export async function estoqueDeCodigos(
  admin: SupabaseClient,
  lojaId: string,
  produtos: Array<{ id: string; digital_config: unknown }>
): Promise<Map<string, EstoqueDeCodigos>> {
  const deCodigo = produtos.filter((p) => lerConfigDigital(p.digital_config).tipo === "codigo").slice(0, 50);
  const mapa = new Map<string, EstoqueDeCodigos>();
  await Promise.all(
    deCodigo.map(async (produto) => {
      const [livres, total, esperando, reservados] = await Promise.all([
        contar(
          admin
            .from("digital_codes")
            .select("id", { count: "exact", head: true })
            .eq("user_id", lojaId)
            .eq("product_id", produto.id)
            .is("entitlement_id", null)
            .is("apagado_em", null)
        ),
        contar(
          admin
            .from("digital_codes")
            .select("id", { count: "exact", head: true })
            .eq("user_id", lojaId)
            .eq("product_id", produto.id)
            .or("apagado_em.is.null,entitlement_id.not.is.null")
        ),
        contar(
          admin
            .from("digital_entitlements")
            .select("id", { count: "exact", head: true })
            .eq("user_id", lojaId)
            .eq("product_id", produto.id)
            .eq("status", "aguardando_codigo")
        ),
        /* Pré-venda: só a unidade do código conta (o produto pode levar um arquivo junto). */
        contar(
          admin
            .from("digital_entitlements")
            .select("id,digital_deliverables!digital_entitlements_deliverable_id_fkey!inner(tipo)", { count: "exact", head: true })
            .eq("user_id", lojaId)
            .eq("product_id", produto.id)
            .eq("status", "agendado")
            .eq("digital_deliverables.tipo", "codigos")
        ),
      ]);
      mapa.set(produto.id, {
        produtoId: produto.id,
        livres,
        total,
        esperando,
        reservados,
        aVenda: Math.max(0, livres - esperando - reservados),
        avisarCom: lerConfigDigital(produto.digital_config).avisarCodigosCom,
      });
    })
  );
  return mapa;
}
