import type { AmbienteFiscal } from "@/lib/fiscal/ambiente";
import { ambienteDaNota, lerPreferencias, modeloAtivo, type AmbienteDaNota, type Presenca } from "@/lib/fiscal/emissor";
import { soDigitos, type Pendencia } from "@/lib/fiscal/montar";
import {
  ErroNaNotaAvulsa,
  formularioDaNota,
  itensDaNotaAvulsa,
  lerNotaAvulsa as lerFormulario,
  pedidoDaNotaAvulsa,
  pendenciaNaLinha,
  type NotaAvulsa,
  type ProdutoDaNotaAvulsa,
} from "@/lib/fiscal/nota-avulsa";

import { carregarEmissor, saldoDoMes, type Admin, type SaldoDoMes } from "./base";
import {
  carregarNota,
  descartarPrevia,
  destinatarioDoCliente,
  emitirPlano,
  enviarNota,
  ErroFiscal,
  notaDaEmpresa,
  pendenciaDeSaldo,
  planejarCarregado,
  type NotaExistente,
  type NotaGuardada,
  type PedidoCarregado,
  type PlanoCompleto,
  type ResultadoDaEmissao,
} from "./notas";

/**
 * A nota fiscal sem pedido: o botão "Emitir nota fiscal" da tela Emissão de
 * notas. A pessoa escolhe o cliente da base (ou nenhum, no cupom), os itens do
 * catálogo ou digitados, e o pagamento. Isso vira um pedido em memória e segue
 * o mesmo caminho da nota do pedido: planejamento, conferências, pacote do mês
 * e envio. O que foi digitado fica guardado na nota (`formulario`), para
 * corrigir e reenviar ou emitir outra igual. A parte sem banco mora em
 * lib/fiscal/nota-avulsa.ts, com testes.
 */

export type { FormaDaNotaAvulsa, NotaAvulsa } from "@/lib/fiscal/nota-avulsa";

/** Lê o formulário da tela; o que não dá para ler vira resposta 400. */
export function lerNotaAvulsa(bruto: unknown) {
  try {
    return lerFormulario(bruto);
  } catch (erro) {
    if (erro instanceof ErroNaNotaAvulsa) throw new ErroFiscal(erro.message, 400);
    throw erro;
  }
}

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/* A nota que ainda pode ser remontada com o que está na tela. */
const ABERTAS = ["preparando", "rascunho", "rejeitada", "erro"];

const texto = (valor: unknown) => (typeof valor === "string" ? valor.replace(/\s+/g, " ").trim() : "");
const uuidOuNulo = (valor: unknown) => (typeof valor === "string" && UUID.test(valor) ? valor : null);

/* ================================================================== */
/* Carregar e planejar                                                 */
/* ================================================================== */

const CAMPOS_DO_CLIENTE =
  "id,name,company_name,type,document,email,phone,whatsapp,address_zip,address_street,address_number,address_complement,address_neighborhood,address_city,address_state,address_ibge,inscricao_estadual,contribuinte_icms,is_system_placeholder";

/** A nota digitada vira o mesmo "pedido carregado" que a nota do pedido usa. */
export async function carregarNotaAvulsa(
  admin: Admin,
  input: { empresaId: string; tenantIds: string[]; dados: NotaAvulsa }
): Promise<PedidoCarregado> {
  const { dados } = input;
  const produtoIds = [...new Set(dados.itens.map((item) => item.produtoId).filter((id): id is string => Boolean(id)))];

  const [clienteR, produtosR, perfisR] = await Promise.all([
    dados.clienteId
      ? admin.from("clients").select(CAMPOS_DO_CLIENTE).eq("id", dados.clienteId).in("user_id", input.tenantIds).maybeSingle()
      : Promise.resolve({ data: null, error: null }),
    produtoIds.length
      ? admin
          .from("products")
          .select("id,name,sku,barcode,fiscal_tipo,ncm,cest,fiscal_origem,fiscal_perfil_id")
          .in("id", produtoIds)
          .in("user_id", input.tenantIds)
      : Promise.resolve({ data: [], error: null }),
    admin.from("fiscal_perfis").select("id,nome,tipo,padrao,ativo,regras").eq("empresa_id", input.empresaId),
  ]);
  if (clienteR.error) throw new ErroFiscal(`Falha ao ler o cliente: ${clienteR.error.message}`, 500);
  if (dados.clienteId && !clienteR.data) throw new ErroFiscal("Cliente não encontrado. Busque de novo.", 404);
  if (produtosR.error) throw new ErroFiscal(`Falha ao ler os produtos: ${produtosR.error.message}`, 500);
  if (perfisR.error) throw new ErroFiscal(`Falha ao ler a tributação: ${perfisR.error.message}`, 500);

  const linhasDosProdutos = (produtosR.data ?? []) as Array<Record<string, unknown>>;
  const produtos = new Map<string, ProdutoDaNotaAvulsa>(linhasDosProdutos.map((p) => [String(p.id), p as ProdutoDaNotaAvulsa]));
  const pedido = pedidoDaNotaAvulsa(dados, itensDaNotaAvulsa(dados, produtos));

  const linhaDoCliente = (clienteR.data ?? null) as Parameters<typeof destinatarioDoCliente>[0];
  const placeholder = Boolean(linhaDoCliente?.is_system_placeholder);

  return {
    pedido,
    destinatario: linhaDoCliente && !placeholder ? destinatarioDoCliente(linhaDoCliente) : null,
    clienteId: linhaDoCliente && !placeholder ? String(linhaDoCliente.id) : null,
    clienteEhConsumidorAvulso: placeholder,
    status: "avulsa",
    origem: "avulsa",
    servicosDoCatalogo: [],
    perfis: ((perfisR.data ?? []) as Array<Record<string, unknown>>).map((p) => ({
      id: String(p.id),
      nome: texto(p.nome),
      tipo: p.tipo === "servico" ? "servico" : "mercadoria",
      padrao: p.padrao === true,
      ativo: p.ativo !== false,
      regras: p.regras,
    })),
    produtos: linhasDosProdutos.map((p) => ({
      id: String(p.id),
      nome: texto(p.name),
      ncm: texto(p.ncm) || null,
      fiscalTipo: texto(p.fiscal_tipo) || null,
      perfilId: texto(p.fiscal_perfil_id) || null,
    })),
  };
}

export type PlanoDaNotaAvulsa = PlanoCompleto & {
  /** O id da nota nova: a chave da tela, a não ser que já exista nota com ela. */
  idDaNovaNota: string | null;
  /** Prévia ou tentativa de outro tipo de nota feita nesta mesma tela: sai de cena ao emitir. */
  notaAnterior: string | null;
};

/* Estas o plano do pedido já avisa com texto de pedido; a nota avulsa troca pelas dela. */
const TROCADAS = new Set(["empresa.sem_modelo_produto", "empresa.sem_nfse", "creditos.sem_saldo", "pedido.cancelado"]);

export async function planejarNotaAvulsa(
  admin: Admin,
  input: { empresaId: string; tenantIds: string[]; ambiente: AmbienteFiscal; dados: NotaAvulsa; problemas: Pendencia[] }
): Promise<PlanoDaNotaAvulsa> {
  const { dados } = input;
  const [carregado, emissor, saldo, anteriorR] = await Promise.all([
    carregarNotaAvulsa(admin, input),
    carregarEmissor(admin, input.empresaId, input.ambiente),
    saldoDoMes(admin, input.empresaId, input.ambiente),
    dados.chave
      ? admin
          .from("fiscal_notas")
          .select("id,modelo,grupo,status,numero,serie,mensagem,mensagem_codigo,ambiente")
          .eq("id", dados.chave)
          .eq("empresa_id", input.empresaId)
          .maybeSingle()
      : Promise.resolve({ data: null, error: null }),
  ]);
  if (anteriorR.error) throw new ErroFiscal(`Falha ao ler a nota: ${anteriorR.error.message}`, 500);

  const anterior = anteriorR.data as (NotaExistente & { ambiente: string }) | null;
  const naTela = anterior && anterior.status !== "removida" && anterior.ambiente === input.ambiente ? anterior : null;
  const aberta = naTela && ABERTAS.includes(naTela.status) ? naTela : null;
  const reaproveitada = aberta && aberta.modelo === dados.modelo ? aberta : null;

  const plano = planejarCarregado({
    ambiente: input.ambiente,
    carregado,
    emissor,
    saldo,
    /* A prévia (ou a tentativa recusada) desta mesma nota é remontada com o que está na tela. */
    existentes: reaproveitada ? [reaproveitada] : [],
    escolhas: {
      modeloDeMercadoria: dados.modelo === "nfse" ? null : dados.modelo,
      consumidorFinal: dados.consumidorFinal,
      presenca: dados.presenca ?? (dados.modelo === "nfce" ? "presence" : null),
      observacao: dados.observacao,
      enviarEmail: dados.enviarEmail,
      documento: dados.documento,
    },
  });

  const naLinha = pendenciaNaLinha(dados);
  const notas = plano.notas
    .filter((nota) => nota.modelo === dados.modelo)
    .map((nota) => ({ ...nota, pendencias: nota.pendencias.map(naLinha) }));

  const pendenciasGerais: Pendencia[] = [
    ...input.problemas,
    ...plano.pendenciasGerais.filter((p) => !TROCADAS.has(p.codigo) && !(p.codigo === "pedido.valores" && !dados.itens.length)),
  ];
  if (!dados.itens.length && !input.problemas.length) {
    pendenciasGerais.push({ codigo: "avulsa.sem_itens", nivel: "bloqueio", onde: "pedido", titulo: "Adicione pelo menos um item" });
  }
  if (emissor && !pendenciasGerais.some((p) => p.codigo === "empresa.inativa") && !plano.emissor.modelos[dados.modelo]) {
    pendenciasGerais.push({
      codigo: "avulsa.modelo_desligado",
      nivel: "bloqueio",
      onde: "empresa",
      titulo: "Este tipo de nota está desligado",
      detalhe: "Ligue em Notas fiscais → Configuração, no passo Notas e impostos.",
    });
  }
  if (naTela && !aberta) {
    pendenciasGerais.push({
      codigo: "avulsa.ja_enviada",
      nivel: "bloqueio",
      onde: "pedido",
      titulo: "Esta nota já foi enviada",
      detalhe: "Para emitir outra, comece uma nota nova.",
    });
  }

  const notasQueGastam = notas.filter((nota) => !nota.jaEmitida && nota.contaCredito).length;
  if (emissor && notasQueGastam > saldo.disponiveis) pendenciasGerais.push(pendenciaDeSaldo(saldo, notasQueGastam));

  return {
    ...plano,
    notas,
    pendenciasGerais,
    notasQueGastam,
    idDaNovaNota: anterior ? null : dados.chave,
    notaAnterior: aberta && !reaproveitada ? aberta.id : null,
  };
}

/* ================================================================== */
/* Emitir, reenviar e abrir de novo                                    */
/* ================================================================== */

export async function emitirNotaAvulsa(
  admin: Admin,
  input: {
    empresaId: string;
    tenantIds: string[];
    ambiente: AmbienteFiscal;
    usuarioId: string | null;
    dados: NotaAvulsa;
    problemas: Pendencia[];
    previa: boolean;
  }
): Promise<ResultadoDaEmissao & { plano: PlanoDaNotaAvulsa }> {
  const plano = await planejarNotaAvulsa(admin, input);
  const resultado = await emitirPlano(admin, {
    empresaId: input.empresaId,
    ambiente: input.ambiente,
    usuarioId: input.usuarioId,
    previa: input.previa,
    plano,
    pedidoId: null,
    idDaNovaNota: plano.idDaNovaNota,
    formulario: formularioDaNota(input.dados),
  });
  if (!resultado.bloqueado && plano.notaAnterior && !resultado.notas.some((nota) => nota.id === plano.notaAnterior)) {
    await descartarPrevia(admin, { empresaId: input.empresaId, notaId: plano.notaAnterior, usuarioId: input.usuarioId }).catch(() => undefined);
  }
  return { ...resultado, plano };
}

/**
 * "Corrigi, reenviar" (ou "Emitir esta nota", na prévia) de uma nota sem
 * pedido: remonta com o cadastro de agora a partir do formulário guardado.
 */
export async function reenviarNotaAvulsa(
  admin: Admin,
  input: { empresaId: string; tenantIds: string[]; ambiente: AmbienteFiscal; notaId: string; usuarioId: string | null }
): Promise<NotaGuardada> {
  const nota = await notaDaEmpresa(admin, input.empresaId, input.notaId);
  if (!["rejeitada", "erro", "rascunho"].includes(nota.status)) throw new ErroFiscal("Esta nota não precisa ser reenviada.", 409);
  if (!nota.formulario) return enviarNota(admin, nota.id, { emitir: true, ator: input.usuarioId });

  const { dados, problemas } = lerNotaAvulsa({ ...nota.formulario, modelo: nota.modelo, chave: nota.id });
  const resultado = await emitirNotaAvulsa(admin, { ...input, dados, problemas, previa: false });
  if (resultado.bloqueado) {
    throw new ErroFiscal("Ainda há o que resolver para reenviar. Use Editar a nota.", 422, { pendencias: resultado.pendencias });
  }
  return carregarNota(admin, resultado.notas[0]?.id ?? nota.id);
}

/* ================================================================== */
/* O que a tela precisa para começar, e as buscas                      */
/* ================================================================== */

export type ClienteDaEmissao = {
  id: string;
  nome: string;
  outroNome: string | null;
  tipo: "pf" | "pj";
  documento: string | null;
  email: string | null;
  telefone: string | null;
  cidade: string | null;
  uf: string | null;
};

export type ProdutoDaEmissao = {
  id: string;
  nome: string;
  sku: string | null;
  preco: number;
  unidade: string;
  ncm: string | null;
  tipo: "mercadoria" | "servico";
  foto: string | null;
  ativo: boolean;
};

export type ContextoDaEmissao = {
  emissor: {
    existe: boolean;
    status: string | null;
    modelos: { nfe: boolean; nfce: boolean; nfse: boolean };
    ambientes: { nfe: AmbienteDaNota; nfce: AmbienteDaNota; nfse: AmbienteDaNota } | null;
  };
  saldo: SaldoDoMes;
  preferencias: { consumidorFinal: boolean; presenca: Presenca; enviarEmail: boolean };
  perfis: Array<{ id: string; nome: string; tipo: "mercadoria" | "servico"; padrao: boolean }>;
};

export async function contextoDaEmissao(admin: Admin, input: { empresaId: string; ambiente: AmbienteFiscal }): Promise<ContextoDaEmissao> {
  const [emissor, saldo, perfisR] = await Promise.all([
    carregarEmissor(admin, input.empresaId, input.ambiente),
    saldoDoMes(admin, input.empresaId, input.ambiente),
    admin.from("fiscal_perfis").select("id,nome,tipo,padrao,ativo").eq("empresa_id", input.empresaId).order("nome"),
  ]);
  const preferencias = lerPreferencias(emissor?.preferencias);
  return {
    emissor: {
      existe: Boolean(emissor),
      status: emissor?.status ?? null,
      modelos: {
        nfe: Boolean(emissor && modeloAtivo(emissor, "nfe")),
        nfce: Boolean(emissor && modeloAtivo(emissor, "nfce")),
        nfse: Boolean(emissor && modeloAtivo(emissor, "nfse")),
      },
      ambientes: emissor
        ? { nfe: ambienteDaNota(emissor, "nfe"), nfce: ambienteDaNota(emissor, "nfce"), nfse: ambienteDaNota(emissor, "nfse") }
        : null,
    },
    saldo,
    preferencias: { consumidorFinal: preferencias.consumidorFinal, presenca: preferencias.presencaPedido, enviarEmail: preferencias.enviarEmail },
    perfis: ((perfisR.data ?? []) as Array<Record<string, unknown>>)
      .filter((p) => p.ativo !== false)
      .map((p) => ({ id: String(p.id), nome: texto(p.nome), tipo: p.tipo === "servico" ? "servico" : "mercadoria", padrao: p.padrao === true })),
  };
}

const CAMPOS_DO_CLIENTE_NA_BUSCA = "id,name,company_name,type,document,email,phone,whatsapp,address_city,address_state";

function clienteParaTela(linha: Record<string, unknown>): ClienteDaEmissao {
  const documento = soDigitos(linha.document);
  const pj = linha.type === "pj" || documento.length === 14;
  const nome = texto(linha.name);
  const razao = texto(linha.company_name);
  return {
    id: String(linha.id),
    nome: (pj ? razao || nome : nome || razao) || "Cliente sem nome",
    outroNome: pj ? (nome && nome !== razao ? nome : null) : razao && razao !== nome ? razao : null,
    tipo: pj ? "pj" : "pf",
    documento: documento || null,
    email: texto(linha.email) || null,
    telefone: soDigitos(linha.phone || linha.whatsapp) || null,
    cidade: texto(linha.address_city) || null,
    uf: texto(linha.address_state).toUpperCase() || null,
  };
}

/* Um termo seguro para o filtro "ou" do banco: sem vírgula, parêntese nem curinga. */
function termoDeBusca(termo: string) {
  return termo.replace(/[%,()*\\"'.:]/g, " ").replace(/\s+/g, " ").trim().slice(0, 60);
}

export async function buscarClientesDaEmissao(
  admin: Admin,
  input: { tenantIds: string[]; termo: string; id?: string | null }
): Promise<ClienteDaEmissao[]> {
  let consulta = admin
    .from("clients")
    .select(CAMPOS_DO_CLIENTE_NA_BUSCA)
    .in("user_id", input.tenantIds)
    .not("is_deleted", "is", true)
    .not("is_system_placeholder", "is", true);

  const id = uuidOuNulo(input.id);
  if (id) {
    const { data, error } = await consulta.eq("id", id).limit(1);
    if (error) throw new ErroFiscal(`Falha ao buscar o cliente: ${error.message}`, 500);
    return ((data ?? []) as Array<Record<string, unknown>>).map(clienteParaTela);
  }

  const limpo = termoDeBusca(input.termo);
  const digitos = soDigitos(input.termo);
  const filtros: string[] = [];
  if (limpo.replace(/\d/g, "").trim().length >= 2 || (limpo.length >= 2 && digitos.length < 3)) {
    const padrao = `%${limpo.split(" ").join("%")}%`;
    filtros.push(`name.ilike.${padrao}`, `company_name.ilike.${padrao}`, `email.ilike.${padrao}`);
  }
  if (digitos.length >= 3) {
    /* CPF, CNPJ e telefone ficam com pontuação no cadastro: os dígitos casam com qualquer máscara. */
    const espalhado = `%${digitos.slice(0, 14).split("").join("%")}%`;
    filtros.push(`document.ilike.${espalhado}`, `phone.ilike.${espalhado}`, `whatsapp.ilike.${espalhado}`);
  }

  consulta = filtros.length ? consulta.or(filtros.join(",")).order("name") : consulta.order("updated_at", { ascending: false });
  const { data, error } = await consulta.limit(filtros.length ? 12 : 8);
  if (error) throw new ErroFiscal(`Falha ao buscar clientes: ${error.message}`, 500);
  return ((data ?? []) as Array<Record<string, unknown>>).map(clienteParaTela);
}

function unidadeDoProduto(unitType: unknown) {
  if (unitType === "square_meter") return "M2";
  if (unitType === "meter" || unitType === "linear_meter") return "M";
  if (unitType === "ream") return "RESMA";
  if (unitType === "box") return "CX";
  return "UN";
}

export async function buscarProdutosDaEmissao(admin: Admin, input: { tenantIds: string[]; termo: string }): Promise<ProdutoDaEmissao[]> {
  let consulta = admin
    .from("products")
    .select("id,name,sku,barcode,price,unit_type,ncm,fiscal_tipo,image_url,active")
    .in("user_id", input.tenantIds);

  const limpo = termoDeBusca(input.termo);
  if (limpo.length >= 2) {
    const padrao = `%${limpo.split(" ").join("%")}%`;
    consulta = consulta.or(`name.ilike.${padrao},sku.ilike.${padrao},barcode.ilike.${padrao}`);
  }
  const { data, error } = await consulta
    .order("active", { ascending: false, nullsFirst: false })
    .order("name")
    .limit(limpo.length >= 2 ? 15 : 10);
  if (error) throw new ErroFiscal(`Falha ao buscar produtos: ${error.message}`, 500);
  return ((data ?? []) as Array<Record<string, unknown>>).map((p) => ({
    id: String(p.id),
    nome: texto(p.name) || "Produto sem nome",
    sku: texto(p.sku) || null,
    preco: Number(p.price) > 0 ? Number(p.price) : 0,
    unidade: unidadeDoProduto(p.unit_type),
    ncm: soDigitos(p.ncm).length === 8 ? soDigitos(p.ncm) : null,
    tipo: p.fiscal_tipo === "servico" ? "servico" : "mercadoria",
    foto: texto(p.image_url) || null,
    ativo: p.active !== false,
  }));
}

/** A nota guardada de volta para a tela: para editar (a mesma nota) ou copiar (uma nova). */
export async function formularioDaNotaGuardada(
  admin: Admin,
  input: { empresaId: string; tenantIds: string[]; notaId: string }
): Promise<{ nota: NotaGuardada; dados: NotaAvulsa; cliente: ClienteDaEmissao | null; fotos: Record<string, string> }> {
  const nota = await notaDaEmpresa(admin, input.empresaId, input.notaId);
  if (nota.origem !== "avulsa" || !nota.formulario) throw new ErroFiscal("Esta nota não pode ser aberta na emissão.", 404);
  const { dados } = lerNotaAvulsa({ ...nota.formulario, modelo: nota.modelo, chave: nota.id });
  const produtoIds = [...new Set(dados.itens.map((item) => item.produtoId).filter((id): id is string => Boolean(id)))];
  const [clientes, produtosR] = await Promise.all([
    dados.clienteId ? buscarClientesDaEmissao(admin, { tenantIds: input.tenantIds, termo: "", id: dados.clienteId }) : Promise.resolve([]),
    produtoIds.length
      ? admin.from("products").select("id,image_url").in("id", produtoIds).in("user_id", input.tenantIds)
      : Promise.resolve({ data: [], error: null }),
  ]);
  const fotos: Record<string, string> = {};
  for (const produto of (produtosR.data ?? []) as Array<{ id: string; image_url: string | null }>) {
    if (produto.image_url) fotos[produto.id] = produto.image_url;
  }
  return { nota, dados, cliente: clientes[0] ?? null, fotos };
}
