import { randomUUID } from "node:crypto";

import { validateDocument } from "@/lib/document-utils";
import { ambienteFiscal, type AmbienteFiscal } from "@/lib/fiscal/ambiente";
import type { Presenca } from "@/lib/fiscal/emissor";
import type { Pendencia } from "@/lib/fiscal/montar";
import { ErroNaNotaAvulsa } from "@/lib/fiscal/nota-avulsa";
import { notaParaTela } from "@/lib/fiscal/servidor/apresentacao";
import { assinaturaDaEmpresa } from "@/lib/fiscal/servidor/assinatura-da-empresa";
import { emitirNotaAvulsa, lerNotaAvulsa, planejarNotaAvulsa, reenviarNotaAvulsa } from "@/lib/fiscal/servidor/avulsa";
import { saldoDoMes, servidorPronto, type Admin } from "@/lib/fiscal/servidor/base";
import { codigoIbge, ncmExiste } from "@/lib/fiscal/servidor/consultas";
import { acordarFila } from "@/lib/fiscal/servidor/fila";
import {
  aguardarResposta,
  arquivoDaNota,
  atualizarStatusAgora,
  cancelarNotaFiscal,
  carregarNota,
  cartaDeCorrecaoFiscal,
  emitirDoPedido,
  enviarNota,
  ErroFiscal,
  notaDaEmpresa,
  planejarEmissao,
  reenviarNotaDoPedido,
  type EscolhasDaEmissao,
  type NotaGuardada,
  type PlanoCompleto,
} from "@/lib/fiscal/servidor/notas";
import { salvarProdutosFiscais } from "@/lib/fiscal/servidor/produtos";
import { ErroDaSpedy } from "@/lib/spedy/cliente";
import { LIMITE_DAS_OBSERVACOES } from "@/lib/orders/observacoes";
import type { CompanyPermission } from "@/lib/company-context";

import { COMO_ENTREGAR_O_ARQUIVO, entregarArquivo } from "../arquivos";
import { contatosNovos, recusaDeContatos } from "../contatos";
import type { ContextoDoAssistente } from "../contexto";
import type { Campos } from "../esquema";
import { AVISO_DE_DADOS, contagem, dataBr, exigir, falha, inicioDoDia, limparBusca, pode, reais, recusar, somarDias, texto, um } from "./comum";
import {
  EM_ANDAMENTO,
  GRUPOS_DE_SITUACAO,
  NOME_DO_MODELO,
  OPCOES_DE_GRUPO,
  ORIGENS,
  PRESENCAS,
  alvoDaPendencia,
  cepFormatado,
  cestLido,
  comoResolver,
  documentoFormatado,
  inscricaoLida,
  ncmFormatado,
  ncmLido,
  segundosDeEspera,
  semFornecedor,
  soDigitos,
  textoComLimites,
} from "./notas-fiscais-regras";
import { rotuloDoAmbienteDaNota } from "./configuracao-fiscal-regras";
import { ErroDaFerramenta, type Ferramenta, type FerramentaDeEscrita, type FerramentaDeLeitura } from "./tipos";

/*
 * Notas fiscais pelo assistente: emitir a nota do pedido (ou sem pedido),
 * acompanhar, entender a recusa, corrigir o cadastro e reenviar, baixar o PDF,
 * cancelar e corrigir por carta.
 *
 * Tudo passa pelas mesmas funções do painel (lib/fiscal/servidor), com as
 * mesmas conferências da tela: assinatura da empresa em dia, emissão
 * disponível neste servidor, pacote de notas, pendências do cadastro. As
 * tabelas fiscais não têm política para o navegador: quem lê e grava é a chave
 * de serviço, sempre presa ao dono da empresa e depois da permissão conferida.
 */

/* ================================================================== */
/* Contexto fiscal                                                     */
/* ================================================================== */

export type Fiscal = { admin: Admin; empresaId: string; ambiente: AmbienteFiscal; usuarioId: string };

export async function abrirFiscal(ctx: ContextoDoAssistente, permissao: CompanyPermission, oQue: string): Promise<Fiscal> {
  exigir(ctx, permissao, oQue);
  const empresaId = ctx.donoDosDados;
  /* Igual ao painel: a assinatura da EMPRESA fecha o módulo inteiro. */
  const assinatura = await assinaturaDaEmpresa(ctx.admin, empresaId);
  if (!assinatura.ativa) recusar(assinatura.mensagem || "A assinatura do Ktalogu desta empresa não está ativa. Regularize em Assinatura para usar as notas fiscais.");
  return { admin: ctx.admin, empresaId, ambiente: ambienteFiscal(), usuarioId: ctx.user.id };
}

/**
 * Em que ambiente a nota sai, dito com todas as letras na prévia: homologação
 * (teste, sem valor fiscal) ou produção (vale de verdade). No ambiente de
 * testes do Ktalogu tudo é teste, mesmo que o tipo de nota diga produção.
 */
export function ambienteDaEmissao(f: Pick<Fiscal, "ambiente">, ambienteNota: string | null | undefined) {
  if (f.ambiente !== "producao") {
    return `${rotuloDoAmbienteDaNota(ambienteNota === "simulacao" ? "simulacao" : "homologacao")} (ambiente de testes do Ktalogu)`;
  }
  return rotuloDoAmbienteDaNota(ambienteNota);
}

export const SEM_CONFIGURACAO =
  "A empresa ainda não terminou a configuração para emitir notas fiscais. Chame ver_configuracao_fiscal para ver o que falta (e o que dá para fazer por aqui) e ofereça à pessoa configurar passo a passo.";

export function exigirEmissaoDisponivel() {
  const bloqueio = servidorPronto();
  if (bloqueio) recusar(bloqueio);
}

/** Erros das funções fiscais viram mensagens que o assistente pode repetir. */
export async function fiscal<T>(oQue: string, executar: () => Promise<T>): Promise<T> {
  try {
    return await executar();
  } catch (erro) {
    if (erro instanceof ErroDaFerramenta) throw erro;
    if (erro instanceof ErroFiscal) {
      if (erro.status >= 500) falha(oQue, erro);
      const pendencias = Array.isArray(erro.extra?.pendencias) ? (erro.extra.pendencias as Pendencia[]) : [];
      recusar(semFornecedor(`${erro.message}${pendencias.length ? ` ${textoDasPendencias(pendencias)}` : ""}`));
    }
    if (erro instanceof ErroNaNotaAvulsa) recusar(erro.message);
    if (erro instanceof ErroDaSpedy) {
      recusar(
        erro.transitorio
          ? "O serviço de notas não respondeu agora. Nada foi alterado de forma definitiva; tente de novo em alguns instantes."
          : semFornecedor(`A nota não foi aceita: ${erro.message}`)
      );
    }
    console.error(`[mcp] notas fiscais: não foi possível ${oQue}:`, erro instanceof Error ? erro.message : erro);
    throw new ErroDaFerramenta(`Não foi possível ${oQue} agora. Tente de novo em instantes.`);
  }
}

/* ================================================================== */
/* Apresentação                                                        */
/* ================================================================== */

function pendenciaParaLer(p: Pendencia) {
  return {
    bloqueia: p.nivel === "bloqueio",
    o_que_falta: p.titulo,
    ...(p.detalhe ? { detalhe: p.detalhe } : {}),
    ...(p.alvoNome ? { de_quem: p.alvoNome } : {}),
    ...alvoDaPendencia(p.codigo, p.alvoId),
    como_resolver: comoResolver(p.onde, p.codigo),
  };
}

function textoDasPendencias(pendencias: Pendencia[]) {
  const bloqueios = pendencias.filter((p) => p.nivel === "bloqueio");
  const lista = (bloqueios.length ? bloqueios : pendencias).slice(0, 12);
  return `Falta resolver: ${lista
    .map((p) => {
      const como = comoResolver(p.onde, p.codigo);
      const alvo = alvoDaPendencia(p.codigo, p.alvoId);
      const ids = alvo.produto_id ? ` [produto_id ${alvo.produto_id}]` : alvo.item_do_pedido_id ? ` [item_do_pedido_id ${alvo.item_do_pedido_id}]` : "";
      const onde = como.ferramenta && como.quem === "assistente" ? ` (resolva com ${como.ferramenta})` : como.onde_no_painel ? ` (no painel: ${como.onde_no_painel})` : "";
      return `${p.titulo}${p.alvoNome ? ` — ${p.alvoNome}` : ""}${p.detalhe && !p.alvoNome ? ` — ${p.detalhe}` : ""}${ids}${onde}`;
    })
    .join("; ")}.`;
}

function acoesDaNota(tela: ReturnType<typeof notaParaTela>) {
  const acoes: string[] = [];
  if (tela.emAndamento) acoes.push("consultar_situacao_da_nota");
  if (tela.temArquivos) acoes.push("baixar_nota_fiscal");
  if (tela.podeReenviar || tela.podeEmitirPrevia) acoes.push("reenviar_nota_fiscal");
  if (tela.podeCorrigir) acoes.push("carta_de_correcao");
  if (tela.podeCancelar) acoes.push("cancelar_nota_fiscal");
  return acoes;
}

export function notaParaLer(nota: NotaGuardada, extra?: { pedidoNumero?: number | null }) {
  const tela = notaParaTela(nota, extra);
  const recusada = ["rejeitada", "erro", "denegada"].includes(tela.status);
  const explicacao = tela.explicacao;
  return {
    id: tela.id,
    tipo: NOME_DO_MODELO[tela.modelo] ?? tela.modeloNome,
    modelo: tela.modelo,
    numero: tela.numero,
    serie: tela.serie,
    situacao: tela.rotulo,
    situacao_explicada: tela.descricaoDoStatus,
    valor: tela.valor,
    cliente: tela.destinatarioNome,
    documento_do_cliente: tela.destinatarioDocumento,
    email_do_cliente: tela.destinatarioEmail,
    pedido_id: tela.pedidoId,
    pedido_numero: tela.pedidoNumero,
    sem_pedido: tela.semPedido,
    ...(tela.semValorFiscal ? { nota_de_teste_sem_valor_fiscal: true } : {}),
    chave_de_acesso: tela.chave,
    criada_em: tela.criadaEm,
    autorizada_em: tela.autorizadaEm,
    cancelada_em: tela.canceladaEm,
    ...(tela.cancelamentoMotivo ? { motivo_do_cancelamento: tela.cancelamentoMotivo } : {}),
    ...(tela.mensagem ? { mensagem_da_autoridade: semFornecedor(tela.mensagem) } : {}),
    ...(explicacao && (recusada || tela.mensagem)
      ? {
          o_que_aconteceu: explicacao.titulo,
          o_que_fazer: explicacao.dica,
          como_resolver: comoResolver(explicacao.aguardar ? "aguardar" : explicacao.onde, tela.mensagemCodigo),
        }
      : {}),
    em_andamento: tela.emAndamento,
    tem_pdf_e_xml: tela.temArquivos,
    proximas_acoes: acoesDaNota(tela),
  };
}

async function numerosDosPedidos(admin: Admin, empresaId: string, notas: NotaGuardada[]) {
  const ids = [...new Set(notas.map((n) => n.pedido_id).filter((id): id is string => Boolean(id)))];
  const numeros = new Map<string, number | null>();
  if (!ids.length) return numeros;
  const { data } = await admin.from("orders").select("id,number").in("id", ids).eq("user_id", empresaId);
  for (const pedido of (data ?? []) as Array<{ id: string; number: number | null }>) numeros.set(pedido.id, pedido.number);
  return numeros;
}

export async function notasParaLer(f: Fiscal, notas: NotaGuardada[]) {
  const numeros = await numerosDosPedidos(f.admin, f.empresaId, notas);
  return notas.map((nota) => notaParaLer(nota, { pedidoNumero: nota.pedido_id ? numeros.get(nota.pedido_id) ?? null : null }));
}

function saldoParaLer(saldo: { entradas: number; usadas: number; reservadas: number; disponiveis: number }) {
  return { notas_do_pacote_no_mes: saldo.entradas, usadas: saldo.usadas, reservadas: saldo.reservadas, disponiveis: Math.max(0, saldo.disponiveis) };
}

const centavos = (valor: number) => Math.round(Number(valor) || 0) / 100;

/* ================================================================== */
/* Achar pedido e nota                                                 */
/* ================================================================== */

const CAMPOS_DO_PEDIDO: Campos = {
  pedido_id: { tipo: "id", descricao: "Id do pedido (de listar_pedidos)." },
  pedido_numero: { tipo: "inteiro", descricao: "Número do pedido, como aparece no painel. Ex.: 22.", min: 1 },
  cliente_id: { tipo: "id", descricao: "Sem pedido informado: usa o pedido mais recente deste cliente." },
  cliente_nome: { tipo: "texto", descricao: "Sem pedido informado: nome do cliente; usa o pedido mais recente dele.", max: 80 },
};

type PedidoAchado = { id: string; numero: number | null; cliente: string | null };

async function acharPedido(ctx: ContextoDoAssistente, args: Record<string, unknown>): Promise<PedidoAchado> {
  const selecao = "id,number,status,created_at,clients(id,name,company_name)";
  type Linha = { id: string; number: number | null; status: string | null; clients?: unknown };
  const achado = (linha: Linha): PedidoAchado => {
    const cliente = um(linha.clients as { name?: string | null; company_name?: string | null } | null);
    return { id: linha.id, numero: linha.number, cliente: cliente ? cliente.company_name || cliente.name || null : null };
  };

  if (args.pedido_id || args.pedido_numero) {
    let consulta = ctx.supabase.from("orders").select(selecao).in("user_id", ctx.tenantIds).order("created_at", { ascending: false }).limit(1);
    consulta = args.pedido_id ? consulta.eq("id", String(args.pedido_id)) : consulta.eq("number", Number(args.pedido_numero));
    const { data, error } = await consulta;
    if (error) falha("ler o pedido", error);
    const linha = ((data ?? []) as Linha[])[0];
    if (!linha) recusar("Pedido não encontrado. Use listar_pedidos para achar o número ou o id certo.");
    return achado(linha);
  }

  let clienteId = args.cliente_id ? String(args.cliente_id) : null;
  if (!clienteId) {
    const busca = limparBusca(args.cliente_nome);
    if (!busca) recusar("Informe o pedido (pedido_numero ou pedido_id) ou o cliente (cliente_id ou cliente_nome).");
    const { data, error } = await ctx.supabase
      .from("clients")
      .select("id,name,company_name")
      .in("user_id", ctx.tenantIds)
      .not("is_deleted", "is", true)
      .or(`name.ilike.%${busca}%,company_name.ilike.%${busca}%`)
      .limit(6);
    if (error) falha("procurar o cliente", error);
    const clientes = (data ?? []) as Array<{ id: string; name: string | null; company_name: string | null }>;
    if (!clientes.length) recusar(`Nenhum cliente encontrado com "${busca}". Use buscar_clientes.`);
    if (clientes.length > 1) {
      recusar(
        `Há mais de um cliente com "${busca}": ${clientes
          .slice(0, 5)
          .map((c) => `${c.company_name || c.name} (cliente_id ${c.id})`)
          .join("; ")}. Pergunte à pessoa qual é e chame de novo com o cliente_id.`
      );
    }
    clienteId = clientes[0].id;
  }

  const { data, error } = await ctx.supabase
    .from("orders")
    .select(selecao)
    .in("user_id", ctx.tenantIds)
    .eq("client_id", clienteId)
    .neq("status", "canceled")
    .order("created_at", { ascending: false })
    .limit(1);
  if (error) falha("ler os pedidos do cliente", error);
  const linha = ((data ?? []) as Linha[])[0];
  if (!linha) recusar("Este cliente não tem pedido (fora os cancelados). Para nota sem pedido, use emitir_nota_avulsa.");
  return achado(linha);
}

async function notaPeloId(f: Fiscal, notaId: unknown) {
  if (!notaId) recusar("Informe nota_id (de listar_notas_fiscais ou ver_nota_fiscal).");
  return fiscal("ler a nota", () => notaDaEmpresa(f.admin, f.empresaId, String(notaId)));
}

async function esperar(ms: number) {
  await new Promise((resolver) => setTimeout(resolver, ms));
}

/**
 * Espera a resposta da SEFAZ ou da prefeitura, no máximo `segundos`, para as
 * notas em andamento. A fila continua acompanhando depois, de qualquer jeito.
 */
export async function aguardarNotas(admin: Admin, ids: string[], segundos: number): Promise<NotaGuardada[]> {
  const limite = Date.now() + segundos * 1000;
  return Promise.all(
    ids.map(async (id) => {
      let nota = await carregarNota(admin, id);
      while (EM_ANDAMENTO.includes(nota.status) && Date.now() + 1000 < limite) {
        if (nota.spedy_nota_id && ["enviando", "processando"].includes(nota.status)) {
          nota = await aguardarResposta(admin, id, { ateMs: Math.max(1000, limite - Date.now()), intervaloMs: 1500 }).catch(() => nota);
          if (EM_ANDAMENTO.includes(nota.status) && Date.now() + 1500 < limite) await esperar(1500);
        } else {
          await esperar(1500);
        }
        nota = await carregarNota(admin, id);
      }
      return nota;
    })
  );
}

const CAMPO_DE_ESPERA: Campos = {
  aguardar_segundos: {
    tipo: "inteiro",
    descricao:
      "Quantos segundos esperar pela resposta da SEFAZ ou da prefeitura antes de devolver (0 a 25). Sem isto, devolve na hora com a nota em processamento; aí chame consultar_situacao_da_nota daqui a uns segundos.",
    min: 0,
    max: 25,
  },
};

export function proximoPasso(notas: ReturnType<typeof notaParaLer>[]) {
  if (notas.some((n) => n.em_andamento)) {
    return "A nota ainda está sendo processada pela SEFAZ ou pela prefeitura. Chame consultar_situacao_da_nota com o id daqui a uns 10 segundos. Não emita de novo.";
  }
  if (notas.some((n) => n.situacao === "Rejeitada" || n.situacao === "Não enviada" || n.situacao === "Denegada")) {
    return "A nota foi recusada. Leia o_que_aconteceu e o_que_fazer, explique à pessoa em palavras simples, corrija o que estiver ao seu alcance (como_resolver) e use reenviar_nota_fiscal.";
  }
  if (notas.some((n) => n.tem_pdf_e_xml)) return "Nota autorizada. Se a pessoa quiser o arquivo, use baixar_nota_fiscal.";
  return null;
}

/* ================================================================== */
/* Leitura                                                             */
/* ================================================================== */

export const listarNotasFiscais: FerramentaDeLeitura = {
  nome: "listar_notas_fiscais",
  titulo: "Listar notas fiscais",
  descricao:
    "Lista as notas fiscais emitidas pela empresa (NF-e, NFC-e e NFS-e), da mais nova para a mais antiga, com situação, valor, cliente e pedido. Filtra por grupo (autorizadas, em andamento, com problema, canceladas, prévias), tipo, período, cliente, pedido ou busca (nome, CPF/CNPJ, número ou chave da nota). Mostra também quantas notas do pacote do mês ainda restam. Use para responder 'quais notas saíram este mês', 'tem nota com problema?', 'a nota do pedido 22 saiu?'.",
  campos: {
    grupo: { tipo: "opcao", descricao: "problemas = rejeitadas ou não enviadas. O padrão é todas.", opcoes: OPCOES_DE_GRUPO },
    tipo: { tipo: "opcao", descricao: "nfe (produto), nfce (cupom), nfse (serviço). O padrão é todos.", opcoes: ["todos", "nfe", "nfce", "nfse"] },
    de: { tipo: "data", descricao: "Criadas a partir deste dia." },
    ate: { tipo: "data", descricao: "Criadas até este dia." },
    busca: { tipo: "texto", descricao: "Nome do cliente, CPF/CNPJ, número ou chave da nota.", max: 80 },
    pedido_id: { tipo: "id", descricao: "Só as notas deste pedido." },
    pedido_numero: { tipo: "inteiro", descricao: "Só as notas do pedido com este número.", min: 1 },
    cliente_id: { tipo: "id", descricao: "Só as notas deste cliente." },
    limite: { tipo: "inteiro", descricao: "Quantas devolver. O padrão é 20.", min: 1, max: 50 },
  },
  tipo: "leitura",
  permissoes: ["fiscal.ver"],
  async executar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.ver", "ver as notas fiscais");
    const limite = (args.limite as number | undefined) ?? 20;
    const grupo = String(args.grupo ?? "todas");

    let consulta = f.admin
      .from("fiscal_notas")
      .select("*", { count: "exact" })
      .eq("empresa_id", f.empresaId)
      .eq("ambiente", f.ambiente)
      .neq("status", "removida")
      .order("created_at", { ascending: false })
      .limit(limite);

    if (GRUPOS_DE_SITUACAO[grupo]) consulta = consulta.in("status", GRUPOS_DE_SITUACAO[grupo]);
    if (args.tipo && args.tipo !== "todos") consulta = consulta.eq("modelo", String(args.tipo));
    if (args.de) consulta = consulta.gte("created_at", inicioDoDia(String(args.de)));
    if (args.ate) consulta = consulta.lt("created_at", inicioDoDia(somarDias(String(args.ate), 1)));

    if (args.pedido_id || args.pedido_numero) {
      const pedido = await acharPedido(ctx, { pedido_id: args.pedido_id, pedido_numero: args.pedido_numero });
      consulta = consulta.eq("pedido_id", pedido.id);
    }
    if (args.cliente_id) {
      const clienteId = String(args.cliente_id);
      const { data: pedidos } = await ctx.supabase.from("orders").select("id").in("user_id", ctx.tenantIds).eq("client_id", clienteId).limit(500);
      const ids = ((pedidos ?? []) as Array<{ id: string }>).map((p) => p.id);
      consulta = consulta.or([`formulario->>clienteId.eq.${clienteId}`, ...(ids.length ? [`pedido_id.in.(${ids.join(",")})`] : [])].join(","));
    }
    const busca = limparBusca(args.busca);
    if (busca) {
      const digitos = soDigitos(busca);
      const filtros = [`destinatario->>nome.ilike.%${busca}%`];
      if (digitos) {
        if (digitos.length <= 9) filtros.push(`numero.eq.${Number(digitos)}`);
        filtros.push(`chave.ilike.%${digitos}%`, `destinatario->>documento.ilike.%${digitos}%`);
      }
      consulta = consulta.or(filtros.join(","));
    }

    const [{ data, error, count }, saldo] = await Promise.all([consulta, fiscal("ler o pacote de notas", () => saldoDoMes(f.admin, f.empresaId, f.ambiente))]);
    if (error) falha("listar as notas fiscais", error);
    const notas = await notasParaLer(f, (data ?? []) as NotaGuardada[]);

    return {
      dados: {
        _observacao: AVISO_DE_DADOS,
        notas,
        total_encontrado: count ?? notas.length,
        ...(count && count > notas.length ? { tem_mais: true } : {}),
        pacote_do_mes: saldoParaLer(saldo),
        ...(f.ambiente !== "producao" ? { aviso: "Este sistema está em modo de teste: as notas não têm valor fiscal." } : {}),
      },
      resumo: `Listou notas fiscais (${contagem(count ?? notas.length, "encontrada", "encontradas")})`,
    };
  },
};

export const verNotaFiscal: FerramentaDeLeitura = {
  nome: "ver_nota_fiscal",
  titulo: "Ver uma nota fiscal",
  descricao:
    "Mostra uma nota fiscal por inteiro: situação, número, valor, cliente, pedido, a mensagem da SEFAZ ou da prefeitura e, se foi recusada, o que aconteceu em palavras simples, o que corrigir e com qual ferramenta (como_resolver), além do histórico. Informe o id da nota, ou o pedido (mostra as notas dele). Use sempre que a pessoa perguntar por que uma nota não saiu.",
  campos: {
    nota_id: { tipo: "id", descricao: "Id da nota (de listar_notas_fiscais)." },
    numero: { tipo: "inteiro", descricao: "Número da nota fiscal (o número impresso nela).", min: 1 },
    tipo: { tipo: "opcao", descricao: "Com numero: o tipo da nota, se houver notas de tipos diferentes com o mesmo número.", opcoes: ["nfe", "nfce", "nfse"] },
    pedido_id: { tipo: "id", descricao: "Mostra as notas deste pedido." },
    pedido_numero: { tipo: "inteiro", descricao: "Mostra as notas do pedido com este número.", min: 1 },
  },
  tipo: "leitura",
  permissoes: ["fiscal.ver"],
  async executar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.ver", "ver as notas fiscais");
    let notas: NotaGuardada[] = [];
    if (args.nota_id) {
      notas = [await notaPeloId(f, args.nota_id)];
    } else if (args.numero) {
      let consulta = f.admin
        .from("fiscal_notas")
        .select("*")
        .eq("empresa_id", f.empresaId)
        .eq("ambiente", f.ambiente)
        .eq("numero", Number(args.numero))
        .neq("status", "removida")
        .order("created_at", { ascending: false })
        .limit(5);
      if (args.tipo) consulta = consulta.eq("modelo", String(args.tipo));
      const { data, error } = await consulta;
      if (error) falha("ler a nota", error);
      notas = (data ?? []) as NotaGuardada[];
    } else if (args.pedido_id || args.pedido_numero) {
      const pedido = await acharPedido(ctx, args);
      const { data, error } = await f.admin
        .from("fiscal_notas")
        .select("*")
        .eq("empresa_id", f.empresaId)
        .eq("ambiente", f.ambiente)
        .eq("pedido_id", pedido.id)
        .neq("status", "removida")
        .order("created_at", { ascending: false })
        .limit(10);
      if (error) falha("ler as notas do pedido", error);
      notas = (data ?? []) as NotaGuardada[];
      if (!notas.length) {
        return {
          dados: { pedido_numero: pedido.numero, notas: [], proximo_passo: "Este pedido ainda não tem nota. Para emitir, comece por planejar_nota_do_pedido." },
          resumo: `Consultou as notas do pedido ${pedido.numero ?? ""} (nenhuma)`,
        };
      }
    } else {
      recusar("Informe nota_id, numero ou o pedido (pedido_numero ou pedido_id).");
    }
    if (!notas.length) recusar("Nota não encontrada. Use listar_notas_fiscais.");

    const lidas = await notasParaLer(f, notas);
    let historico: Array<Record<string, unknown>> = [];
    if (notas.length === 1) {
      const { data: eventos } = await f.admin
        .from("fiscal_eventos")
        .select("titulo,detalhe,created_at")
        .eq("nota_id", notas[0].id)
        .order("created_at", { ascending: false })
        .limit(15);
      historico = ((eventos ?? []) as Array<{ titulo: string; detalhe: string | null; created_at: string }>).map((e) => ({
        quando: e.created_at,
        o_que: semFornecedor(e.titulo),
        ...(e.detalhe ? { detalhe: semFornecedor(texto(e.detalhe, 300) ?? "") } : {}),
      }));
    }

    return {
      dados: {
        _observacao: AVISO_DE_DADOS,
        ...(lidas.length === 1 ? { nota: lidas[0], historico } : { notas: lidas }),
        proximo_passo: proximoPasso(lidas),
      },
      resumo: lidas.length === 1 ? `Consultou a nota fiscal ${lidas[0].numero ?? lidas[0].id}` : `Consultou ${contagem(lidas.length, "nota fiscal", "notas fiscais")}`,
    };
  },
};

/* ---------------- Escolhas da emissão ---------------- */

const CAMPOS_DAS_ESCOLHAS: Campos = {
  modelo_mercadoria: {
    tipo: "opcao",
    descricao: "Para os produtos: nfe (nota de produto, o padrão para empresa ou entrega) ou nfce (cupom fiscal, venda de balcão paga). Sem isto vale a preferência da empresa.",
    opcoes: ["nfe", "nfce"],
  },
  consumidor_final: { tipo: "booleano", descricao: "O cliente é consumidor final (usa o produto, não revende). Sem isto o sistema decide pelo cadastro." },
  presenca: {
    tipo: "opcao",
    descricao:
      "Como foi a venda. Sem isto o sistema decide. 'presencial' é o cliente retirando aqui: cliente de outro estado sem entrega sai como venda dentro do estado; enviada para outro estado, o ICMS sai pela alíquota entre estados.",
    opcoes: Object.keys(PRESENCAS),
  },
  documento: { tipo: "texto", descricao: "Só para cupom fiscal sem cliente cadastrado: CPF ou CNPJ a sair no cupom.", max: 20 },
};

function lerEscolhas(args: Record<string, unknown>): EscolhasDaEmissao {
  return {
    modeloDeMercadoria: args.modelo_mercadoria === "nfce" ? "nfce" : args.modelo_mercadoria === "nfe" ? "nfe" : null,
    consumidorFinal: typeof args.consumidor_final === "boolean" ? args.consumidor_final : null,
    presenca: args.presenca ? (PRESENCAS[String(args.presenca)] as Presenca) : null,
    observacao: typeof args.observacao === "string" ? args.observacao.slice(0, LIMITE_DAS_OBSERVACOES) : null,
    enviarEmail: typeof args.enviar_email === "boolean" ? args.enviar_email : null,
    documento: typeof args.documento === "string" ? soDigitos(args.documento) || null : null,
    somente: Array.isArray(args.somente) ? (args.somente as string[]).slice(0, 10) : null,
  };
}

function planoParaLer(plano: PlanoCompleto, f: Fiscal, somente?: string[] | null) {
  const filtro = (somente ?? []).filter(Boolean);
  const notas = plano.notas.map((nota) => {
    const escolhida = !filtro.length || filtro.includes(nota.chave) || (nota.modelo === "nfse" && filtro.includes("nfse"));
    return {
      chave: nota.chave,
      tipo: NOME_DO_MODELO[nota.modelo] ?? nota.modelo,
      titulo: nota.titulo,
      valor: nota.montada ? centavos(nota.montada.valor) : null,
      itens: nota.montada?.resumo.itens.slice(0, 30).map((i) => ({ descricao: i.descricao, quantidade: i.quantidade, unidade: i.unidade, valor: centavos(i.valor) })) ?? [],
      ...(nota.montada && nota.montada.resumo.desconto > 0 ? { desconto: centavos(nota.montada.resumo.desconto) } : {}),
      ...(nota.montada && nota.montada.resumo.frete > 0 ? { frete: centavos(nota.montada.resumo.frete) } : {}),
      ambiente: ambienteDaEmissao(f, nota.ambienteNota),
      ...(nota.ambienteNota !== "producao" ? { nota_de_teste_sem_valor_fiscal: true } : {}),
      gasta_nota_do_pacote: nota.contaCredito,
      ja_emitida: nota.jaEmitida,
      ...(nota.existente ? { nota_existente: { id: nota.existente.id, situacao: nota.existente.status, numero: nota.existente.numero } } : {}),
      ...(escolhida ? {} : { fora_desta_emissao: true }),
      pendencias: nota.pendencias.map(pendenciaParaLer),
    };
  });
  const bloqueios = [...plano.pendenciasGerais, ...plano.notas.filter((n) => !n.jaEmitida).flatMap((n) => n.pendencias)].filter((p) => p.nivel === "bloqueio");
  const aEmitir = plano.notas.filter((n) => !n.jaEmitida);
  const destinatario = plano.destinatario;
  return {
    pedido_id: plano.pedidoId,
    pedido_numero: plano.numero,
    cliente: destinatario
      ? {
          cliente_id: plano.clienteId,
          nome: destinatario.nome,
          tipo: destinatario.tipo === "pj" ? "empresa" : "pessoa",
          documento: destinatario.documento,
          email: destinatario.email,
          inscricao_estadual: destinatario.inscricaoEstadual,
          contribuinte_icms: destinatario.contribuinte,
          endereco: destinatario.endereco,
        }
      : null,
    notas,
    pendencias_gerais: plano.pendenciasGerais.map(pendenciaParaLer),
    pode_emitir: aEmitir.length > 0 && bloqueios.length === 0 && plano.emissor.existe,
    ...(plano.emissor.existe ? {} : { configuracao_fiscal: SEM_CONFIGURACAO }),
    notas_que_gastam_do_pacote: plano.notasQueGastam,
    pacote_do_mes: saldoParaLer(plano.saldo),
    valor_do_pedido: centavos(plano.totais.pedido),
    ...(f.ambiente !== "producao" ? { aviso: "Este sistema está em modo de teste: as notas não têm valor fiscal." } : {}),
  };
}

export const planejarNotaDoPedido: FerramentaDeLeitura = {
  nome: "planejar_nota_do_pedido",
  titulo: "Conferir a nota de um pedido",
  descricao:
    "Antes de emitir: mostra quais notas o pedido gera (produto, serviço ou cupom), com itens e valores, o que está faltando para emitir (pendências, cada uma com como_resolver dizendo se dá para resolver por aqui e com qual ferramenta), quantas notas do pacote vai gastar e quantas restam. Não emite nada. Aceita o número do pedido, o id, ou o cliente (usa o pedido mais recente dele). Comece sempre por aqui quando pedirem 'emita a nota do pedido X'.",
  campos: { ...CAMPOS_DO_PEDIDO, ...CAMPOS_DAS_ESCOLHAS },
  tipo: "leitura",
  permissoes: ["fiscal.ver"],
  async executar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.ver", "ver as notas fiscais");
    const pedido = await acharPedido(ctx, args);
    const plano = await fiscal("conferir a nota do pedido", () =>
      planejarEmissao(f.admin, { empresaId: f.empresaId, ambiente: f.ambiente, pedidoId: pedido.id, escolhas: lerEscolhas(args) })
    );
    const lido = planoParaLer(plano, f);
    const disponivel = servidorPronto() === null;
    return {
      dados: {
        _observacao: AVISO_DE_DADOS,
        ...lido,
        ...(disponivel ? {} : { pode_emitir: false, aviso_da_emissao: "A emissão de notas fiscais está indisponível no momento. Fale com o suporte do Ktalogu." }),
        proximo_passo: !disponivel
          ? null
          : lido.pode_emitir
            ? "Mostre à pessoa o que vai sair e, se ela quiser, use emitir_nota_do_pedido."
            : "Há pendências. Resolva as que como_resolver indica com uma ferramenta daqui (pergunte à pessoa os dados que faltam) e oriente a pessoa nas que são do painel; depois confira de novo.",
      },
      resumo: `Conferiu a nota do pedido ${pedido.numero ?? pedido.id}`,
    };
  },
};

export const consultarSituacaoDaNota: FerramentaDeLeitura = {
  nome: "consultar_situacao_da_nota",
  titulo: "Atualizar a situação da nota",
  descricao:
    "Pergunta agora à SEFAZ ou à prefeitura como está a nota (ou várias) e devolve a situação atualizada, com a explicação se foi recusada. Use depois de emitir, reenviar ou cancelar, enquanto a nota estiver em processamento. Pode esperar alguns segundos pela resposta (aguardar_segundos).",
  campos: {
    nota_id: { tipo: "id", descricao: "Id da nota." },
    notas_ids: { tipo: "textos", descricao: "Vários ids de nota de uma vez (até 10).", maxItens: 10, max: 40 },
    ...CAMPO_DE_ESPERA,
  },
  tipo: "leitura",
  permissoes: ["fiscal.ver"],
  async executar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.ver", "ver as notas fiscais");
    const ids = [...new Set([...(args.nota_id ? [String(args.nota_id)] : []), ...((args.notas_ids as string[] | undefined) ?? [])])];
    if (!ids.length) recusar("Informe nota_id ou notas_ids.");
    if (ids.some((id) => !/^[0-9a-f-]{36}$/i.test(id))) recusar("Algum id de nota não é válido. Use os ids devolvidos pelas ferramentas de nota.");
    const atualizadas = await Promise.all(
      ids.map((id) => fiscal("consultar a nota", () => atualizarStatusAgora(f.admin, { empresaId: f.empresaId, notaId: id })))
    );
    const espera = segundosDeEspera(args.aguardar_segundos);
    const finais = espera ? await aguardarNotas(f.admin, atualizadas.map((n) => n.id), espera) : atualizadas;
    const lidas = await notasParaLer(f, finais);
    return {
      dados: { _observacao: AVISO_DE_DADOS, notas: lidas, proximo_passo: proximoPasso(lidas) },
      resumo: `Atualizou a situação de ${contagem(lidas.length, "nota fiscal", "notas fiscais")}`,
    };
  },
};

export const baixarNotaFiscal: FerramentaDeLeitura = {
  nome: "baixar_nota_fiscal",
  titulo: "Baixar o PDF ou XML da nota",
  descricao:
    "Gera o link para baixar o PDF (DANFE, cupom ou DANFSe) e/ou o XML de uma nota autorizada ou cancelada. O link vale 24 horas. Use quando pedirem 'me dê o PDF da nota' ou 'manda o XML para o contador'.",
  campos: {
    nota_id: { tipo: "id", descricao: "Id da nota.", obrigatorio: true },
    formato: { tipo: "opcao", descricao: "pdf, xml ou ambos. O padrão é pdf.", opcoes: ["pdf", "xml", "ambos"] },
  },
  tipo: "leitura",
  permissoes: ["fiscal.ver"],
  async executar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.ver", "baixar notas fiscais");
    const nota = await notaPeloId(f, args.nota_id);
    const tela = notaParaTela(nota);
    if (!tela.temArquivos) {
      recusar(
        tela.emAndamento
          ? "A nota ainda está sendo processada; o arquivo sai depois da autorização. Chame consultar_situacao_da_nota daqui a pouco."
          : `Esta nota está "${tela.rotulo}" e não tem PDF nem XML: o arquivo só existe para nota autorizada ou cancelada.`
      );
    }
    const formato = String(args.formato ?? "pdf");
    const tipos: Array<"pdf" | "xml"> = formato === "ambos" ? ["pdf", "xml"] : [formato === "xml" ? "xml" : "pdf"];
    const arquivos = [];
    for (const tipo of tipos) {
      const arquivo = await fiscal("baixar o arquivo da nota", () => arquivoDaNota(f.admin, { empresaId: f.empresaId, notaId: nota.id, tipo }));
      const entregue = await entregarArquivo(ctx, { conteudo: Buffer.from(arquivo.conteudo), nome: arquivo.nome, tipo: arquivo.tipoMime });
      arquivos.push({ formato: tipo, ...entregue });
    }
    return {
      dados: {
        nota: { id: nota.id, numero: nota.numero, tipo: NOME_DO_MODELO[nota.modelo], situacao: tela.rotulo, valor: tela.valor },
        arquivos,
        como_entregar: COMO_ENTREGAR_O_ARQUIVO,
      },
      resumo: `Gerou o ${tipos.join(" e ").toUpperCase()} da nota fiscal ${nota.numero ?? nota.id}`,
    };
  },
};

/* ================================================================== */
/* Emissão                                                             */
/* ================================================================== */

/* A observação da nota vai ao cliente (no DANFE e no e-mail): o assistente não
   escreve nela contato que a pessoa não escreveu (ver contatos.ts). */
function recusarContatosNaObservacao(observacao: unknown) {
  if (typeof observacao !== "string" || !observacao) return;
  const contatos = contatosNovos(null, observacao);
  if (contatos.length) recusar(recusaDeContatos(contatos, "no painel, na emissão da nota"));
}

type CargaDaEmissao = { pedidoId: string; pedidoNumero: number | null; escolhas: EscolhasDaEmissao; espera: number };

export const emitirNotaDoPedido: FerramentaDeEscrita<CargaDaEmissao> = {
  nome: "emitir_nota_do_pedido",
  titulo: "Emitir a nota fiscal do pedido",
  descricao:
    "Emite a(s) nota(s) fiscal(is) de um pedido: nota de produto (NF-e) ou cupom (NFC-e) para os produtos e nota de serviço (NFS-e) para os serviços, com os dados do cadastro. Aceita o número do pedido, o id ou o cliente (usa o pedido mais recente dele). Antes, use planejar_nota_do_pedido para ver pendências. A primeira chamada devolve a prévia e um código; com a aprovação da pessoa, chame de novo com os mesmos dados e o código em confirmacao. A emissão é processada pela SEFAZ ou pela prefeitura em segundos: use aguardar_segundos ou consultar_situacao_da_nota. Se for recusada, ver_nota_fiscal explica o que corrigir.",
  campos: {
    ...CAMPOS_DO_PEDIDO,
    ...CAMPOS_DAS_ESCOLHAS,
    observacao: { tipo: "texto", descricao: "Texto para as informações complementares da nota. Sem isto, vão as observações do pedido.", max: LIMITE_DAS_OBSERVACOES },
    enviar_email: { tipo: "booleano", descricao: "Mandar a nota por e-mail ao cliente quando autorizar. Sem isto vale a preferência da empresa." },
    somente: {
      tipo: "textos",
      descricao: "Só estas notas do pedido, pelas chaves que planejar_nota_do_pedido devolveu (ex.: nfe, nfce, nfse). Sem isto, todas as que faltam.",
      maxItens: 10,
      max: 80,
    },
    ...CAMPO_DE_ESPERA,
  },
  tipo: "escrita",
  sensivel: true,
  permissoes: ["fiscal.emitir"],
  async preparar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.emitir", "emitir notas fiscais");
    exigirEmissaoDisponivel();
    const pedido = await acharPedido(ctx, args);
    const escolhas = lerEscolhas(args);
    recusarContatosNaObservacao(escolhas.observacao);
    const plano = await fiscal("conferir a nota do pedido", () =>
      planejarEmissao(f.admin, { empresaId: f.empresaId, ambiente: f.ambiente, pedidoId: pedido.id, escolhas })
    );
    const filtro = (escolhas.somente ?? []).filter(Boolean);
    const alvo = plano.notas.filter((n) => !n.jaEmitida && (!filtro.length || filtro.includes(n.chave) || (n.modelo === "nfse" && filtro.includes("nfse"))));
    if (!alvo.length) {
      const jaSairam = plano.notas.filter((n) => n.jaEmitida);
      recusar(
        jaSairam.length
          ? `As notas deste pedido já foram emitidas (${jaSairam.map((n) => n.titulo).join(", ")}). Use ver_nota_fiscal com o pedido para ver a situação.`
          : "Não há nota para emitir neste pedido."
      );
    }
    const pendencias = [...plano.pendenciasGerais, ...alvo.flatMap((n) => n.pendencias)];
    if (!plano.emissor.existe) {
      recusar(SEM_CONFIGURACAO);
    }
    if (pendencias.some((p) => p.nivel === "bloqueio")) {
      recusar(`Ainda não dá para emitir a nota do pedido ${pedido.numero ?? ""}. ${textoDasPendencias(pendencias)} Use planejar_nota_do_pedido para ver os detalhes.`);
    }

    const avisos = pendencias.filter((p) => p.nivel === "aviso").map((p) => `${p.titulo}${p.detalhe ? `: ${p.detalhe}` : ""}`);
    if (alvo.some((n) => n.ambienteNota !== "producao")) avisos.push("Esta nota sai em modo de teste, sem valor fiscal.");
    const gasta = alvo.filter((n) => n.contaCredito).length;
    if (gasta) avisos.push(`Vai gastar ${contagem(gasta, "nota", "notas")} do pacote do mês (restam ${Math.max(0, plano.saldo.disponiveis)}).`);
    avisos.push("Nota fiscal autorizada só se desfaz por cancelamento, dentro do prazo da lei.");

    const destinatario = plano.destinatario;
    return {
      previa: {
        o_que_vai_acontecer: `Emitir ${contagem(alvo.length, "nota fiscal", "notas fiscais")} do pedido ${pedido.numero ?? ""}.`,
        pedido_numero: pedido.numero,
        cliente: destinatario ? { nome: destinatario.nome, documento: destinatario.documento, email: destinatario.email } : pedido.cliente,
        notas: alvo.map((n) => ({
          tipo: NOME_DO_MODELO[n.modelo] ?? n.modelo,
          titulo: n.titulo,
          valor: n.montada ? reais(centavos(n.montada.valor)) : null,
          itens: n.montada?.resumo.itens.length ?? 0,
          ambiente: ambienteDaEmissao(f, n.ambienteNota),
          ...(n.ambienteNota !== "producao" || f.ambiente !== "producao" ? { teste_sem_valor_fiscal: true } : {}),
        })),
        ambiente_de_emissao: [...new Set(alvo.map((n) => ambienteDaEmissao(f, n.ambienteNota)))].join(" / "),
        ...(escolhas.observacao !== null ? { observacao_na_nota: escolhas.observacao } : {}),
        ...(escolhas.enviarEmail !== null ? { enviar_por_email: escolhas.enviarEmail } : {}),
      },
      resumo: `Prévia da nota fiscal do pedido ${pedido.numero ?? pedido.id}`,
      avisos,
      carga: { pedidoId: pedido.id, pedidoNumero: pedido.numero, escolhas, espera: segundosDeEspera(args.aguardar_segundos) },
    };
  },
  async gravar(ctx, carga) {
    const f = await abrirFiscal(ctx, "fiscal.emitir", "emitir notas fiscais");
    exigirEmissaoDisponivel();
    const resultado = await fiscal("emitir a nota", () =>
      emitirDoPedido(f.admin, { empresaId: f.empresaId, ambiente: f.ambiente, pedidoId: carga.pedidoId, usuarioId: f.usuarioId, escolhas: carga.escolhas, previa: false })
    );
    acordarFila();
    if (resultado.bloqueado) recusar(`A nota não foi emitida. ${textoDasPendencias(resultado.pendencias)}`);

    const ids = resultado.notas.map((n) => n.id);
    const notas = carga.espera ? await aguardarNotas(f.admin, ids, carga.espera) : await Promise.all(ids.map((id) => carregarNota(f.admin, id)));
    const lidas = await notasParaLer(f, notas);
    return {
      dados: {
        _observacao: AVISO_DE_DADOS,
        enviada: true,
        pedido_numero: carga.pedidoNumero,
        notas: lidas,
        proximo_passo: proximoPasso(lidas),
      },
      resumo: `Emitiu ${contagem(lidas.length, "nota fiscal", "notas fiscais")} do pedido ${carga.pedidoNumero ?? carga.pedidoId}`,
    };
  },
};

type CargaDoReenvio = { notaId: string; espera: number };

export const reenviarNotaFiscal: FerramentaDeEscrita<CargaDoReenvio> = {
  nome: "reenviar_nota_fiscal",
  titulo: "Reenviar uma nota recusada",
  descricao:
    "Reenvia uma nota fiscal que foi recusada ou não chegou a ser enviada, depois de corrigir o cadastro (cliente, produto). A nota é montada de novo com os dados de agora. Também emite uma prévia que ficou guardada. Confira antes com ver_nota_fiscal que a causa da recusa foi corrigida. Devolve prévia e código; confirme com a aprovação da pessoa.",
  campos: { nota_id: { tipo: "id", descricao: "Id da nota recusada.", obrigatorio: true }, ...CAMPO_DE_ESPERA },
  tipo: "escrita",
  sensivel: true,
  permissoes: ["fiscal.emitir"],
  async preparar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.emitir", "emitir notas fiscais");
    exigirEmissaoDisponivel();
    const nota = await notaPeloId(f, args.nota_id);
    const tela = notaParaTela(nota);
    if (!["rejeitada", "erro", "rascunho"].includes(nota.status)) {
      recusar(
        tela.emAndamento
          ? "Esta nota ainda está em processamento. Não reenvie; use consultar_situacao_da_nota."
          : `Esta nota está "${tela.rotulo}" e não precisa ser reenviada.`
      );
    }
    const explicacao = tela.explicacao;
    if (explicacao?.aguardar) recusar(`${explicacao.titulo}: ${explicacao.dica} Use consultar_situacao_da_nota.`);

    /* Confere o que ainda falta antes de mostrar a prévia: reenviar com a mesma pendência só gasta uma tentativa. */
    let pendencias: Pendencia[] = [];
    if (nota.origem === "avulsa" && nota.formulario) {
      const formulario = nota.formulario;
      const plano = await fiscal("conferir a nota", async () => {
        const { dados, problemas } = lerNotaAvulsa({ ...formulario, modelo: nota.modelo, chave: nota.id });
        return planejarNotaAvulsa(f.admin, { empresaId: f.empresaId, tenantIds: ctx.tenantIds, ambiente: f.ambiente, dados, problemas });
      });
      pendencias = [...plano.pendenciasGerais, ...plano.notas.flatMap((n) => n.pendencias)];
    } else if (nota.pedido_id && nota.origem !== "teste") {
      const chave = nota.modelo === "nfse" ? `nfse:${nota.grupo}` : nota.modelo;
      const plano = await fiscal("conferir a nota", () =>
        planejarEmissao(f.admin, {
          empresaId: f.empresaId,
          ambiente: f.ambiente,
          pedidoId: nota.pedido_id as string,
          escolhas: { modeloDeMercadoria: nota.modelo === "nfse" ? null : nota.modelo, somente: [chave] },
        })
      );
      pendencias = [...plano.pendenciasGerais, ...plano.notas.filter((n) => n.chave === chave).flatMap((n) => n.pendencias)];
    }
    if (pendencias.some((p) => p.nivel === "bloqueio")) recusar(`Ainda não dá para reenviar. ${textoDasPendencias(pendencias)}`);

    const numeros = await numerosDosPedidos(f.admin, f.empresaId, [nota]);
    const avisos = [];
    if (nota.status !== "rascunho" && explicacao) avisos.push(`Motivo da recusa anterior: ${explicacao.titulo}. Confira se já foi corrigido.`);
    if (tela.semValorFiscal) avisos.push("Esta nota sai em modo de teste, sem valor fiscal.");
    return {
      previa: {
        o_que_vai_acontecer: nota.status === "rascunho" ? "Emitir de verdade a nota que estava em prévia." : "Reenviar a nota com os dados atuais do cadastro.",
        nota: { id: nota.id, tipo: NOME_DO_MODELO[nota.modelo], valor: reais(tela.valor), cliente: tela.destinatarioNome, situacao_atual: tela.rotulo },
        ambiente_de_emissao: ambienteDaEmissao(f, nota.ambiente_nota),
        pedido_numero: nota.pedido_id ? numeros.get(nota.pedido_id) ?? null : null,
        ...(tela.mensagem ? { recusa_anterior: semFornecedor(tela.mensagem) } : {}),
      },
      resumo: `Prévia do reenvio da nota fiscal ${nota.numero ?? nota.id}`,
      avisos,
      carga: { notaId: nota.id, espera: segundosDeEspera(args.aguardar_segundos) },
    };
  },
  async gravar(ctx, carga) {
    const f = await abrirFiscal(ctx, "fiscal.emitir", "emitir notas fiscais");
    exigirEmissaoDisponivel();
    /* Mesmo caminho do botão "Corrigi, reenviar" (api/fiscal/notas/[id]/reenviar). */
    const reenviada = await fiscal("reenviar a nota", async () => {
      const atual = await notaDaEmpresa(f.admin, f.empresaId, carga.notaId);
      return atual.origem === "teste"
        ? enviarNota(f.admin, atual.id, { emitir: true, ator: f.usuarioId })
        : atual.origem === "avulsa"
          ? reenviarNotaAvulsa(f.admin, { empresaId: f.empresaId, tenantIds: ctx.tenantIds, ambiente: f.ambiente, notaId: atual.id, usuarioId: f.usuarioId })
          : reenviarNotaDoPedido(f.admin, { empresaId: f.empresaId, ambiente: f.ambiente, notaId: atual.id, usuarioId: f.usuarioId });
    });
    acordarFila();
    const [nota] = carga.espera ? await aguardarNotas(f.admin, [reenviada.id], carga.espera) : [await carregarNota(f.admin, reenviada.id)];
    const lidas = await notasParaLer(f, [nota]);
    return {
      dados: { _observacao: AVISO_DE_DADOS, reenviada: true, nota: lidas[0], proximo_passo: proximoPasso(lidas) },
      resumo: `Reenviou a nota fiscal ${nota.numero ?? nota.id}`,
    };
  },
};

/* ---------------- Nota sem pedido ---------------- */

const FORMAS_DE_PAGAMENTO = ["pix", "dinheiro", "credito", "debito", "transferencia", "boleto", "a_prazo"] as const;

type CargaDaAvulsa = { formulario: Record<string, unknown>; espera: number };

function formularioDaAvulsa(args: Record<string, unknown>): Record<string, unknown> {
  const itens = ((args.itens as Array<Record<string, unknown>> | undefined) ?? []).map((item) => ({
    produtoId: item.produto_id ?? null,
    descricao: item.descricao,
    ncm: item.ncm ?? null,
    unidade: item.unidade ?? "UN",
    quantidade: item.quantidade,
    valorUnitario: item.valor_unitario,
  }));
  return {
    modelo: args.tipo,
    clienteId: args.cliente_id ?? null,
    documento: args.documento ?? null,
    itens,
    desconto: args.desconto ?? 0,
    frete: args.frete ?? 0,
    pagamento: args.pagamento ?? "pix",
    vencimento: args.vencimento ?? null,
    perfilId: null,
    consumidorFinal: typeof args.consumidor_final === "boolean" ? args.consumidor_final : null,
    presenca: args.presenca ? PRESENCAS[String(args.presenca)] : null,
    observacao: args.observacao ?? null,
    enviarEmail: typeof args.enviar_email === "boolean" ? args.enviar_email : null,
  };
}

export const emitirNotaAvulsaPeloAssistente: FerramentaDeEscrita<CargaDaAvulsa> = {
  nome: "emitir_nota_avulsa",
  titulo: "Emitir nota fiscal sem pedido",
  descricao:
    "Emite uma nota fiscal que não vem de um pedido do sistema (venda feita fora, serviço avulso): escolha o tipo (nfe, nfce ou nfse), o cliente (cliente_id de buscar_clientes, ou só o CPF/CNPJ no cupom), os itens com quantidade e valor, e a forma de pagamento. Produto do catálogo (produto_id) leva o NCM do cadastro; item digitado precisa do NCM na nota de produto. Devolve prévia e código; confirme com a aprovação da pessoa. Para nota de um pedido, use emitir_nota_do_pedido.",
  campos: {
    tipo: { tipo: "opcao", descricao: "nfe (nota de produto), nfce (cupom fiscal) ou nfse (nota de serviço).", opcoes: ["nfe", "nfce", "nfse"], obrigatorio: true },
    cliente_id: { tipo: "id", descricao: "Cliente da nota (de buscar_clientes). Obrigatório na NF-e." },
    documento: { tipo: "texto", descricao: "Só no cupom sem cliente cadastrado: CPF ou CNPJ a sair no cupom.", max: 20 },
    itens: {
      tipo: "lista",
      descricao: "O que foi vendido (até 60 linhas).",
      minItens: 1,
      maxItens: 60,
      itens: {
        produto_id: { tipo: "id", descricao: "Produto do catálogo (de buscar_produtos), se houver." },
        descricao: { tipo: "texto", descricao: "O que foi vendido, como sai na nota.", obrigatorio: true, min: 2, max: 120 },
        ncm: { tipo: "texto", descricao: "NCM com 8 números, para item digitado em nota de produto.", max: 12 },
        unidade: { tipo: "texto", descricao: "Unidade (UN, M2, CX...). O padrão é UN.", max: 6 },
        quantidade: { tipo: "numero", descricao: "Quantidade.", obrigatorio: true, min: 0.0001, max: 1_000_000 },
        valor_unitario: { tipo: "numero", descricao: "Valor de cada unidade, em reais.", obrigatorio: true, min: 0.01, max: 100_000_000 },
      },
    },
    desconto: { tipo: "numero", descricao: "Desconto total, em reais.", min: 0 },
    frete: { tipo: "numero", descricao: "Frete, em reais (só na NF-e).", min: 0 },
    pagamento: { tipo: "opcao", descricao: "Como foi ou será pago. O padrão é pix.", opcoes: FORMAS_DE_PAGAMENTO },
    vencimento: { tipo: "data", descricao: "Para boleto ou a prazo: quando vence." },
    consumidor_final: { tipo: "booleano", descricao: "O cliente é consumidor final. Sem isto o sistema decide." },
    presenca: {
      tipo: "opcao",
      descricao: "Como foi a venda. Sem isto o sistema decide. 'presencial' é o cliente retirando aqui: cliente de outro estado sai como venda dentro do estado.",
      opcoes: Object.keys(PRESENCAS),
    },
    observacao: { tipo: "texto", descricao: "Informações complementares da nota.", max: 500 },
    enviar_email: { tipo: "booleano", descricao: "Mandar a nota por e-mail ao cliente quando autorizar." },
    ...CAMPO_DE_ESPERA,
  },
  tipo: "escrita",
  sensivel: true,
  permissoes: ["fiscal.emitir"],
  async preparar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.emitir", "emitir notas fiscais");
    exigirEmissaoDisponivel();
    const formulario = formularioDaAvulsa(args);
    recusarContatosNaObservacao(formulario.observacao);
    const plano = await fiscal("conferir a nota", async () => {
      const { dados, problemas } = lerNotaAvulsa({ ...formulario, chave: null });
      return planejarNotaAvulsa(f.admin, { empresaId: f.empresaId, tenantIds: ctx.tenantIds, ambiente: f.ambiente, dados, problemas });
    });
    if (!plano.emissor.existe) recusar(SEM_CONFIGURACAO);
    const pendencias = [...plano.pendenciasGerais, ...plano.notas.flatMap((n) => n.pendencias)];
    if (pendencias.some((p) => p.nivel === "bloqueio")) recusar(`Ainda não dá para emitir esta nota. ${textoDasPendencias(pendencias)}`);
    const nota = plano.notas[0];
    if (!nota?.montada) recusar("Não há nota para emitir com estes dados.");

    const avisos = pendencias.filter((p) => p.nivel === "aviso").map((p) => `${p.titulo}${p.detalhe ? `: ${p.detalhe}` : ""}`);
    if (nota.ambienteNota !== "producao") avisos.push("Esta nota sai em modo de teste, sem valor fiscal.");
    if (nota.contaCredito) avisos.push(`Vai gastar 1 nota do pacote do mês (restam ${Math.max(0, plano.saldo.disponiveis)}).`);
    avisos.push("Nota fiscal autorizada só se desfaz por cancelamento, dentro do prazo da lei.");
    const destinatario = plano.destinatario;
    return {
      previa: {
        o_que_vai_acontecer: `Emitir uma ${NOME_DO_MODELO[nota.modelo]} sem pedido.`,
        cliente: destinatario ? { nome: destinatario.nome, documento: destinatario.documento } : formulario.documento ? { documento: formulario.documento } : null,
        itens: nota.montada.resumo.itens.map((i) => ({ descricao: i.descricao, quantidade: i.quantidade, unidade: i.unidade, valor: reais(centavos(i.valor)) })),
        ...(nota.montada.resumo.desconto > 0 ? { desconto: reais(centavos(nota.montada.resumo.desconto)) } : {}),
        ...(nota.montada.resumo.frete > 0 ? { frete: reais(centavos(nota.montada.resumo.frete)) } : {}),
        valor_da_nota: reais(centavos(nota.montada.valor)),
        ambiente_de_emissao: ambienteDaEmissao(f, nota.ambienteNota),
        pagamento: formulario.pagamento,
        ...(typeof formulario.observacao === "string" && formulario.observacao ? { observacao_na_nota: formulario.observacao } : {}),
        ...(typeof formulario.enviarEmail === "boolean" ? { enviar_por_email: formulario.enviarEmail } : {}),
      },
      resumo: `Prévia de nota fiscal sem pedido (${NOME_DO_MODELO[nota.modelo]})`,
      avisos,
      carga: { formulario, espera: segundosDeEspera(args.aguardar_segundos) },
    };
  },
  async gravar(ctx, carga) {
    const f = await abrirFiscal(ctx, "fiscal.emitir", "emitir notas fiscais");
    exigirEmissaoDisponivel();
    /* A chave nova a cada confirmação: o código só vale uma vez, e a chave impede duas notas no mesmo envio. */
    const resultado = await fiscal("emitir a nota", async () => {
      const { dados, problemas } = lerNotaAvulsa({ ...carga.formulario, chave: randomUUID() });
      return emitirNotaAvulsa(f.admin, { empresaId: f.empresaId, tenantIds: ctx.tenantIds, ambiente: f.ambiente, usuarioId: f.usuarioId, dados, problemas, previa: false });
    });
    acordarFila();
    if (resultado.bloqueado) recusar(`A nota não foi emitida. ${textoDasPendencias(resultado.pendencias)}`);
    const ids = resultado.notas.map((n) => n.id);
    const notas = carga.espera ? await aguardarNotas(f.admin, ids, carga.espera) : await Promise.all(ids.map((id) => carregarNota(f.admin, id)));
    const lidas = await notasParaLer(f, notas);
    return {
      dados: { _observacao: AVISO_DE_DADOS, enviada: true, notas: lidas, proximo_passo: proximoPasso(lidas) },
      resumo: `Emitiu nota fiscal sem pedido (${contagem(lidas.length, "nota", "notas")})`,
    };
  },
};

/* ================================================================== */
/* Depois da autorização                                               */
/* ================================================================== */

type CargaDoCancelamento = { notaId: string; motivo: string; espera: number };

export const cancelarNotaFiscalPeloAssistente: FerramentaDeEscrita<CargaDoCancelamento> = {
  nome: "cancelar_nota_fiscal",
  titulo: "Cancelar uma nota fiscal",
  descricao:
    "Cancela uma nota fiscal autorizada, com o motivo escrito (de 15 a 255 letras, exigência da lei). A SEFAZ e as prefeituras só aceitam o cancelamento dentro do prazo legal. Não tem volta. Devolve prévia e código; confirme com a aprovação da pessoa.",
  campos: {
    nota_id: { tipo: "id", descricao: "Id da nota autorizada.", obrigatorio: true },
    motivo: { tipo: "texto", descricao: "Por que a nota está sendo cancelada (15 a 255 letras). Ex.: Venda desfeita a pedido do cliente.", obrigatorio: true, min: 15, max: 255 },
    ...CAMPO_DE_ESPERA,
  },
  tipo: "escrita",
  sensivel: true,
  sobrescreve: true,
  permissoes: ["fiscal.cancelar"],
  async preparar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.cancelar", "cancelar notas fiscais");
    exigirEmissaoDisponivel();
    const motivo = textoComLimites(args.motivo, 15, 255);
    if (!motivo.ok) recusar(`O motivo do cancelamento ${motivo.erro}.`);
    const nota = await notaPeloId(f, args.nota_id);
    const tela = notaParaTela(nota);
    if (nota.status === "contingencia") recusar("Nota em contingência só pode ser cancelada depois que a SEFAZ confirmar a autorização. Tente mais tarde.");
    if (nota.status !== "autorizada") recusar(`Só nota autorizada pode ser cancelada. Esta está "${tela.rotulo}".`);
    const numeros = await numerosDosPedidos(f.admin, f.empresaId, [nota]);
    return {
      previa: {
        o_que_vai_acontecer: "Cancelar a nota fiscal. Não tem volta.",
        nota: {
          id: nota.id,
          tipo: NOME_DO_MODELO[nota.modelo],
          numero: nota.numero,
          valor: reais(tela.valor),
          cliente: tela.destinatarioNome,
          autorizada_em: tela.autorizadaEm ? dataBr(tela.autorizadaEm) : null,
          pedido_numero: nota.pedido_id ? numeros.get(nota.pedido_id) ?? null : null,
        },
        motivo: motivo.texto,
        ambiente_da_nota: ambienteDaEmissao(f, nota.ambiente_nota),
      },
      resumo: `Prévia do cancelamento da nota fiscal ${nota.numero ?? nota.id}`,
      avisos: [
        "O cancelamento só é aceito dentro do prazo da lei. Se o prazo passou, a recusa vem com o motivo.",
        ...(tela.semValorFiscal ? ["É uma nota de teste, sem valor fiscal."] : []),
      ],
      carga: { notaId: nota.id, motivo: motivo.texto, espera: segundosDeEspera(args.aguardar_segundos) },
    };
  },
  async gravar(ctx, carga) {
    const f = await abrirFiscal(ctx, "fiscal.cancelar", "cancelar notas fiscais");
    exigirEmissaoDisponivel();
    const nota = await fiscal("cancelar a nota", () =>
      cancelarNotaFiscal(f.admin, { empresaId: f.empresaId, notaId: carga.notaId, motivo: carga.motivo, usuarioId: f.usuarioId })
    );
    acordarFila();
    const [final] = carga.espera ? await aguardarNotas(f.admin, [nota.id], carga.espera) : [nota];
    const lidas = await notasParaLer(f, [final]);
    return {
      dados: {
        _observacao: AVISO_DE_DADOS,
        nota: lidas[0],
        proximo_passo: lidas[0].em_andamento ? "O cancelamento foi enviado e aguarda confirmação. Chame consultar_situacao_da_nota daqui a uns segundos." : null,
      },
      resumo: `Pediu o cancelamento da nota fiscal ${final.numero ?? final.id}`,
    };
  },
};

type CargaDaCorrecao = { notaId: string; texto: string };

export const cartaDeCorrecao: FerramentaDeEscrita<CargaDaCorrecao> = {
  nome: "carta_de_correcao",
  titulo: "Carta de correção da NF-e",
  descricao:
    "Envia uma carta de correção para uma NF-e (nota de produto) autorizada, corrigindo um erro de texto: endereço, dados de transporte, descrição, observação. A lei NÃO deixa corrigir por carta valores, impostos, quantidade, CPF/CNPJ, quem vende ou quem compra, nem a data — nesses casos o caminho é cancelar e emitir de novo. Cada carta substitui a anterior: escreva a correção completa (15 a 1000 letras). Devolve prévia e código; confirme com a aprovação da pessoa.",
  campos: {
    nota_id: { tipo: "id", descricao: "Id da NF-e autorizada.", obrigatorio: true },
    correcao: { tipo: "texto", descricao: "O texto da correção, completo (15 a 1000 letras). Ex.: Onde se lê Rua A, 10, leia-se Rua B, 100.", obrigatorio: true, min: 15, max: 1000 },
  },
  tipo: "escrita",
  sensivel: true,
  permissoes: ["fiscal.cancelar"],
  async preparar(ctx, args) {
    const f = await abrirFiscal(ctx, "fiscal.cancelar", "corrigir notas fiscais");
    exigirEmissaoDisponivel();
    const correcao = textoComLimites(args.correcao, 15, 1000);
    if (!correcao.ok) recusar(`A correção ${correcao.erro}.`);
    const nota = await notaPeloId(f, args.nota_id);
    if (nota.modelo !== "nfe") recusar("Carta de correção só existe para NF-e (nota de produto). Cupom e nota de serviço se corrigem cancelando e emitindo de novo.");
    if (nota.status !== "autorizada") recusar(`Só NF-e autorizada recebe carta de correção. Esta está "${notaParaTela(nota).rotulo}".`);
    return {
      previa: {
        o_que_vai_acontecer: "Enviar uma carta de correção para a NF-e. Ela substitui qualquer carta anterior desta nota.",
        nota: { id: nota.id, numero: nota.numero, valor: reais(Number(nota.valor) || 0), cliente: texto((nota.destinatario ?? {}).nome) },
        correcao: correcao.texto,
        ambiente_da_nota: ambienteDaEmissao(f, nota.ambiente_nota),
      },
      resumo: `Prévia da carta de correção da NF-e ${nota.numero ?? nota.id}`,
      avisos: ["A carta não corrige valores, impostos, quantidade, CPF/CNPJ, as partes da venda nem a data. Se for isso, ela será recusada ou não terá efeito."],
      carga: { notaId: nota.id, texto: correcao.texto },
    };
  },
  async gravar(ctx, carga) {
    const f = await abrirFiscal(ctx, "fiscal.cancelar", "corrigir notas fiscais");
    exigirEmissaoDisponivel();
    await fiscal("enviar a carta de correção", () => cartaDeCorrecaoFiscal(f.admin, { empresaId: f.empresaId, notaId: carga.notaId, texto: carga.texto, usuarioId: f.usuarioId }));
    acordarFila();
    return {
      dados: { enviada: true, nota_id: carga.notaId, proximo_passo: "A carta foi enviada. O PDF da carta aparece junto da nota em alguns instantes." },
      resumo: "Enviou carta de correção de NF-e",
    };
  },
};

/* ================================================================== */
/* Corrigir o cadastro                                                 */
/* ================================================================== */

type MudancaDeNcm =
  | { alvo: "produto"; id: string; nome: string; antes: Record<string, unknown>; mudanca: { id: string; ncm?: string | null; cest?: string | null; origem?: number | null } }
  | { alvo: "item"; id: string; pedidoId: string; nome: string; antes: Record<string, unknown>; ncm: string | null };

export const corrigirNcmDosProdutos: FerramentaDeEscrita<MudancaDeNcm[]> = {
  nome: "corrigir_ncm_dos_produtos",
  titulo: "Corrigir NCM, CEST e origem",
  descricao:
    "Corrige os dados fiscais de produtos do catálogo (NCM, CEST, origem da mercadoria) ou o NCM de um item digitado à mão num pedido (sem produto do catálogo), em lote. Use quando o plano da nota ou a recusa apontar NCM faltando ou inválido: o plano devolve produto_id ou item_do_pedido_id. O NCM é conferido na tabela oficial. Nunca chute o NCM: se a pessoa não souber, ela confirma com o contador.",
  campos: {
    itens: {
      tipo: "lista",
      descricao: "Um por produto ou item.",
      minItens: 1,
      maxItens: 50,
      itens: {
        produto_id: { tipo: "id", descricao: "Produto do catálogo." },
        item_do_pedido_id: { tipo: "id", descricao: "Item digitado no pedido (sem produto). Exige pedido_id ou pedido_numero." },
        pedido_id: { tipo: "id", descricao: "O pedido do item." },
        pedido_numero: { tipo: "inteiro", descricao: "O número do pedido do item.", min: 1 },
        ncm: { tipo: "texto", descricao: "NCM com 8 números (ex.: 4911.10.90). Vazio não muda.", max: 12 },
        cest: { tipo: "texto", descricao: "Só produto do catálogo: CEST com 7 números, quando houver.", max: 12 },
        origem: { tipo: "inteiro", descricao: "Só produto do catálogo: origem da mercadoria (0 = nacional; 1 a 8 = importada ou com conteúdo importado).", min: 0, max: 8 },
      },
    },
  },
  tipo: "escrita",
  sobrescreve: true,
  permissoes: ["fiscal.configurar", "fiscal.emitir"],
  async preparar(ctx, args) {
    const f = await abrirFiscal(ctx, pode(ctx, "fiscal.configurar") ? "fiscal.configurar" : "fiscal.emitir", "corrigir dados fiscais");
    const linhas = (args.itens as Array<Record<string, unknown>>) ?? [];
    const mudancas: MudancaDeNcm[] = [];
    const avisos: string[] = [];
    const conferidos = new Map<string, boolean | null>();
    const conferirNcm = async (ncm: string, quem: string) => {
      if (!conferidos.has(ncm)) conferidos.set(ncm, await ncmExiste(ncm).catch(() => null));
      const existe = conferidos.get(ncm);
      if (existe === false) recusar(`O NCM ${ncmFormatado(ncm)} (${quem}) não existe na tabela oficial. Confira o número com o contador.`);
      if (existe === null) avisos.push(`Não deu para conferir o NCM ${ncmFormatado(ncm)} na tabela oficial agora; confira se está certo.`);
    };

    for (const [indice, linha] of linhas.entries()) {
      const posicao = `linha ${indice + 1}`;
      const ncm = linha.ncm === undefined ? undefined : ncmLido(linha.ncm);
      if (ncm === null) recusar(`${posicao}: o NCM tem 8 números (ex.: 4911.10.90).`);

      if (linha.produto_id) {
        exigir(ctx, "fiscal.configurar", "mudar os dados fiscais dos produtos");
        const cest = linha.cest === undefined ? undefined : cestLido(linha.cest);
        if (cest === null) recusar(`${posicao}: o CEST tem 7 números.`);
        const { data: produto, error } = await f.admin
          .from("products")
          .select("id,name,ncm,cest,fiscal_origem")
          .eq("id", String(linha.produto_id))
          .eq("user_id", f.empresaId)
          .maybeSingle();
        if (error) falha("ler o produto", error);
        if (!produto) recusar(`${posicao}: produto não encontrado nesta empresa. Use buscar_produtos.`);
        const p = produto as { id: string; name: string | null; ncm: string | null; cest: string | null; fiscal_origem: number | null };
        const mudanca: { id: string; ncm?: string | null; cest?: string | null; origem?: number | null } = { id: p.id };
        if (ncm) {
          await conferirNcm(ncm, p.name ?? "produto");
          mudanca.ncm = ncm;
        }
        if (cest !== undefined && cest !== "") mudanca.cest = cest;
        if (linha.origem !== undefined) mudanca.origem = Number(linha.origem);
        if (Object.keys(mudanca).length === 1) recusar(`${posicao}: informe o NCM, o CEST ou a origem a corrigir.`);
        mudancas.push({
          alvo: "produto",
          id: p.id,
          nome: p.name ?? "Produto",
          antes: { ncm: ncmFormatado(p.ncm), cest: p.cest, origem: p.fiscal_origem },
          mudanca,
        });
        continue;
      }

      if (linha.item_do_pedido_id) {
        exigir(ctx, "fiscal.emitir", "informar o NCM dos itens do pedido");
        if (linha.cest !== undefined || linha.origem !== undefined) avisos.push(`${posicao}: item digitado no pedido aceita só o NCM; CEST e origem foram ignorados.`);
        if (!ncm) recusar(`${posicao}: informe o NCM do item.`);
        if (!linha.pedido_id && !linha.pedido_numero) recusar(`${posicao}: informe pedido_id ou pedido_numero do item.`);
        const pedido = await acharPedido(ctx, { pedido_id: linha.pedido_id, pedido_numero: linha.pedido_numero });
        const { data: item, error } = await f.admin
          .from("order_items")
          .select("id,title,fiscal_ncm,order_id,orders!inner(user_id)")
          .eq("id", String(linha.item_do_pedido_id))
          .eq("order_id", pedido.id)
          .eq("orders.user_id", f.empresaId)
          .maybeSingle();
        if (error) falha("ler o item do pedido", error);
        if (!item) recusar(`${posicao}: este item não é do pedido ${pedido.numero ?? ""}.`);
        const i = item as { id: string; title: string | null; fiscal_ncm: string | null };
        await conferirNcm(ncm as string, i.title ?? "item");
        mudancas.push({ alvo: "item", id: i.id, pedidoId: pedido.id, nome: `${i.title ?? "Item"} (pedido ${pedido.numero ?? ""})`, antes: { ncm: ncmFormatado(i.fiscal_ncm) }, ncm: ncm as string });
        continue;
      }
      recusar(`${posicao}: informe produto_id ou item_do_pedido_id.`);
    }

    return {
      previa: {
        o_que_vai_acontecer: `Corrigir os dados fiscais de ${contagem(mudancas.length, "cadastro", "cadastros")}.`,
        mudancas: mudancas.map((m) => ({
          de: m.nome,
          tipo: m.alvo === "produto" ? "produto do catálogo" : "item digitado no pedido",
          antes: m.antes,
          depois:
            m.alvo === "produto"
              ? {
                  ...(m.mudanca.ncm !== undefined ? { ncm: ncmFormatado(m.mudanca.ncm) } : {}),
                  ...(m.mudanca.cest !== undefined ? { cest: m.mudanca.cest } : {}),
                  ...(m.mudanca.origem !== undefined ? { origem: `${m.mudanca.origem} (${ORIGENS[m.mudanca.origem as number]})` } : {}),
                }
              : { ncm: ncmFormatado(m.ncm) },
        })),
      },
      resumo: `Prévia da correção fiscal de ${contagem(mudancas.length, "cadastro", "cadastros")}`,
      avisos,
      carga: mudancas,
    };
  },
  async gravar(ctx, carga) {
    const f = await abrirFiscal(ctx, pode(ctx, "fiscal.configurar") ? "fiscal.configurar" : "fiscal.emitir", "corrigir dados fiscais");
    const produtos = carga.flatMap((m) => (m.alvo === "produto" ? [m.mudanca] : []));
    let salvos = 0;
    if (produtos.length) {
      /* A permissão pode ter saído entre a prévia e a confirmação. */
      exigir(ctx, "fiscal.configurar", "mudar os dados fiscais dos produtos");
      const resultado = await fiscal("salvar os produtos", () => salvarProdutosFiscais(f.admin, f.empresaId, produtos));
      salvos += resultado.salvos;
    }
    for (const m of carga) {
      if (m.alvo !== "item") continue;
      /* Igual ao PATCH de api/fiscal/pedidos/[id]: vale só para a nota deste pedido. */
      const { data, error } = await f.admin.from("order_items").update({ fiscal_ncm: m.ncm }).eq("id", m.id).eq("order_id", m.pedidoId).select("id");
      if (error) falha("guardar o NCM do item", error);
      salvos += data?.length ?? 0;
    }
    return {
      dados: {
        corrigidos: salvos,
        proximo_passo: "Se havia uma nota recusada por isso, use reenviar_nota_fiscal; se ainda não foi emitida, confira de novo com planejar_nota_do_pedido.",
      },
      resumo: `Corrigiu os dados fiscais de ${contagem(salvos, "cadastro", "cadastros")}`,
    };
  },
};

type CargaDoCliente = { clienteId: string; nome: string; mudancas: Record<string, unknown> };

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

export const corrigirDadosFiscaisDoCliente: FerramentaDeEscrita<CargaDoCliente> = {
  nome: "corrigir_dados_fiscais_do_cliente",
  titulo: "Corrigir dados fiscais do cliente",
  descricao:
    "Corrige no cadastro do cliente o que a nota fiscal usa: CPF/CNPJ, razão social, tipo (pessoa ou empresa), inscrição estadual, se é contribuinte de ICMS, e-mail e endereço completo (CEP, rua, número, bairro, cidade, estado). O código IBGE da cidade é achado sozinho pelo CEP ou pela cidade. Mande só o que muda. Use quando o plano da nota ou a recusa apontarem problema no cliente. Pergunte à pessoa os dados; nunca invente.",
  campos: {
    cliente_id: { tipo: "id", descricao: "Id do cliente (do plano da nota ou de buscar_clientes).", obrigatorio: true },
    tipo: { tipo: "opcao", descricao: "pessoa (CPF) ou empresa (CNPJ).", opcoes: ["pessoa", "empresa"] },
    nome: { tipo: "texto", descricao: "Nome da pessoa ou do contato.", min: 2, max: 120 },
    razao_social: { tipo: "texto", descricao: "Nome da empresa (razão social), quando for empresa.", min: 2, max: 120 },
    documento: { tipo: "texto", descricao: "CPF ou CNPJ, com ou sem pontuação.", max: 20 },
    contribuinte_icms: {
      tipo: "opcao",
      descricao: "Só empresa: contribuinte (tem inscrição estadual ativa), isento (tem inscrição mas é isento) ou nao_contribuinte.",
      opcoes: ["contribuinte", "isento", "nao_contribuinte"],
    },
    inscricao_estadual: { tipo: "texto", descricao: "Só empresa contribuinte: a inscrição estadual (só números).", max: 20 },
    email: { tipo: "texto", descricao: "E-mail para onde a nota vai.", max: 120 },
    cep: { tipo: "texto", descricao: "CEP com 8 números.", max: 10 },
    rua: { tipo: "texto", descricao: "Rua, avenida.", max: 150 },
    numero: { tipo: "texto", descricao: "Número do endereço (ou S/N).", max: 20 },
    complemento: { tipo: "texto", descricao: "Complemento.", max: 80 },
    bairro: { tipo: "texto", descricao: "Bairro.", max: 80 },
    cidade: { tipo: "texto", descricao: "Cidade.", max: 80 },
    estado: { tipo: "texto", descricao: "Sigla do estado com duas letras. Ex.: SP.", min: 2, max: 2 },
  },
  tipo: "escrita",
  sobrescreve: true,
  /* E-mail e documento decidem para quem a nota sai: passam pela aprovação. */
  sensivel: (args) => args.email !== undefined || args.documento !== undefined || args.tipo !== undefined,
  permissoes: ["clients.list.write"],
  async preparar(ctx, args) {
    exigir(ctx, "clients.list.write", "alterar clientes");
    const { data, error } = await ctx.supabase
      .from("clients")
      .select(COLUNAS_FISCAIS_DO_CLIENTE)
      .eq("id", String(args.cliente_id))
      .in("user_id", ctx.tenantIds)
      .maybeSingle();
    if (error) falha("ler o cliente", error);
    if (!data) recusar("Cliente não encontrado. Use buscar_clientes.");
    const atual = data as Record<string, string | boolean | null>;
    if (atual.is_system_placeholder) recusar("Este é o cliente genérico de balcão e não pode ser alterado. Cadastre ou escolha o cliente certo.");

    const m: Record<string, unknown> = {};
    const tipo = args.tipo === "empresa" ? "pj" : args.tipo === "pessoa" ? "pf" : null;
    const digitos = args.documento !== undefined ? soDigitos(args.documento) : null;
    const tipoFinal = tipo ?? (digitos?.length === 14 ? "pj" : digitos?.length === 11 ? "pf" : (atual.type as string) === "pj" ? "pj" : "pf");
    if (tipo || (digitos && tipoFinal !== atual.type)) m.type = tipoFinal;
    if (digitos !== null) {
      if (!validateDocument(digitos, tipoFinal)) recusar(`O ${tipoFinal === "pj" ? "CNPJ" : "CPF"} informado não é válido. Confira os números.`);
      m.document = documentoFormatado(digitos);
      const { data: repetidos } = await ctx.supabase
        .from("clients")
        .select("id,name,company_name")
        .in("user_id", ctx.tenantIds)
        .in("document", [digitos, documentoFormatado(digitos)])
        .neq("id", String(atual.id))
        .not("is_deleted", "is", true)
        .limit(1);
      const outro = (repetidos ?? [])[0] as { id: string; name: string | null; company_name: string | null } | undefined;
      if (outro) recusar(`Outro cliente já tem este documento: ${outro.company_name || outro.name} (cliente_id ${outro.id}). Confira se a nota não deveria ser desse cadastro.`);
    }
    if (args.nome) m.name = String(args.nome);
    if (args.razao_social) m.company_name = String(args.razao_social);

    const email = args.email !== undefined ? String(args.email).trim().toLowerCase() : null;
    if (email !== null) {
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) recusar("O e-mail informado não é válido.");
      m.email = email;
    }

    /* Mesma regra da tela do cliente: a inscrição só vale para empresa contribuinte. */
    const contribuinte = args.contribuinte_icms ? String(args.contribuinte_icms) : tipoFinal === "pj" ? (atual.contribuinte_icms as string | null) : null;
    const inscricao = args.inscricao_estadual !== undefined ? inscricaoLida(args.inscricao_estadual) : ((atual.inscricao_estadual as string | null) ?? "");
    if (tipoFinal === "pf") {
      if (args.contribuinte_icms || args.inscricao_estadual) recusar("Inscrição estadual e contribuinte de ICMS são só de empresa. Para pessoa, deixe sem.");
      if (atual.contribuinte_icms || atual.inscricao_estadual) {
        m.contribuinte_icms = null;
        m.inscricao_estadual = null;
      }
    } else if (args.contribuinte_icms || args.inscricao_estadual !== undefined) {
      if (contribuinte === "contribuinte" && (!inscricao || inscricao === "ISENTO")) recusar("Cliente contribuinte de ICMS precisa da inscrição estadual (só números).");
      if (inscricao && inscricao !== "ISENTO" && (inscricao.length < 2 || inscricao.length > 14)) recusar("A inscrição estadual tem de 2 a 14 números.");
      m.contribuinte_icms = contribuinte || null;
      m.inscricao_estadual = contribuinte === "contribuinte" && inscricao ? inscricao : null;
    }

    /* Endereço: o CEP completa cidade e estado; o código IBGE sai do CEP ou da cidade. */
    const mexeuNoEndereco = ["cep", "rua", "numero", "complemento", "bairro", "cidade", "estado"].some((c) => args[c] !== undefined);
    const avisos: string[] = [];
    if (mexeuNoEndereco) {
      if (args.cep !== undefined) {
        const cep = cepFormatado(args.cep);
        if (!cep) recusar("O CEP tem 8 números.");
        m.address_zip = cep;
      }
      if (args.rua !== undefined) m.address_street = String(args.rua);
      if (args.numero !== undefined) m.address_number = String(args.numero);
      if (args.complemento !== undefined) m.address_complement = String(args.complemento);
      if (args.bairro !== undefined) m.address_neighborhood = String(args.bairro);
      if (args.cidade !== undefined) m.address_city = String(args.cidade);
      if (args.estado !== undefined) {
        const uf = String(args.estado).toUpperCase();
        if (!/^[A-Z]{2}$/.test(uf)) recusar("O estado é a sigla com duas letras. Ex.: SP.");
        m.address_state = uf;
      }
      const cep = (m.address_zip ?? atual.address_zip) as string | null;
      const cidade = (m.address_city ?? atual.address_city) as string | null;
      const uf = (m.address_state ?? atual.address_state) as string | null;
      const achado = await codigoIbge({ cep, cidade, uf }).catch(() => null);
      if (achado?.ibge?.length === 7) {
        m.address_ibge = achado.ibge;
        if (args.cidade === undefined && achado.cidade && achado.cidade !== atual.address_city) m.address_city = achado.cidade;
        if (args.estado === undefined && achado.uf && achado.uf !== atual.address_state) m.address_state = achado.uf;
        if (args.cidade !== undefined && args.cep !== undefined && achado.cidade && achado.cidade.toLowerCase() !== String(args.cidade).toLowerCase()) {
          avisos.push(`O CEP é de ${achado.cidade}/${achado.uf}, mas a cidade informada foi ${args.cidade}. Confira.`);
        }
      } else {
        avisos.push("Não achei o código IBGE da cidade pelo CEP nem pelo nome. A nota precisa dele: confira o CEP, a cidade e o estado.");
      }
    }

    const mudancas = Object.fromEntries(Object.entries(m).filter(([coluna, valor]) => (atual[coluna] ?? null) !== (valor ?? null)));
    if (!Object.keys(mudancas).length) recusar("Nada a mudar: o cadastro já está com estes dados.");

    const ROTULO: Record<string, string> = {
      type: "tipo",
      name: "nome",
      company_name: "razão social",
      document: "CPF/CNPJ",
      email: "e-mail",
      contribuinte_icms: "contribuinte de ICMS",
      inscricao_estadual: "inscrição estadual",
      address_zip: "CEP",
      address_street: "rua",
      address_number: "número",
      address_complement: "complemento",
      address_neighborhood: "bairro",
      address_city: "cidade",
      address_state: "estado",
      address_ibge: "código IBGE da cidade",
    };
    const nome = String(atual.company_name || atual.name || "Cliente");
    return {
      previa: {
        o_que_vai_acontecer: `Atualizar o cadastro de ${nome}.`,
        mudancas: Object.entries(mudancas).map(([coluna, valor]) => ({
          campo: ROTULO[coluna] ?? coluna,
          antes: coluna === "type" ? (atual.type === "pj" ? "empresa" : "pessoa") : atual[coluna] ?? null,
          depois: coluna === "type" ? (valor === "pj" ? "empresa" : "pessoa") : valor,
        })),
      },
      resumo: `Prévia da correção do cadastro de ${nome}`,
      avisos,
      carga: { clienteId: String(atual.id), nome, mudancas },
    };
  },
  async gravar(ctx, carga) {
    const { data, error } = await ctx.supabase
      .from("clients")
      .update({ ...carga.mudancas, updated_at: new Date().toISOString() })
      .eq("id", carga.clienteId)
      .in("user_id", ctx.tenantIds)
      .select("id");
    if (error) falha("salvar o cliente", error);
    if (!data?.length) recusar("O cliente não foi alterado: ele não foi encontrado ou esta pessoa não pode alterá-lo.");
    return {
      dados: {
        atualizado: true,
        cliente_id: carga.clienteId,
        campos: Object.keys(carga.mudancas).length,
        proximo_passo: "Se havia uma nota recusada por isso, use reenviar_nota_fiscal; se ainda não foi emitida, confira de novo com planejar_nota_do_pedido.",
      },
      resumo: `Corrigiu os dados fiscais de ${carga.nome}`,
    };
  },
};

export const FERRAMENTAS_DE_NOTAS_FISCAIS: Ferramenta[] = [
  listarNotasFiscais,
  verNotaFiscal,
  planejarNotaDoPedido,
  consultarSituacaoDaNota,
  baixarNotaFiscal,
  emitirNotaDoPedido as FerramentaDeEscrita,
  reenviarNotaFiscal as FerramentaDeEscrita,
  emitirNotaAvulsaPeloAssistente as FerramentaDeEscrita,
  corrigirNcmDosProdutos as FerramentaDeEscrita,
  corrigirDadosFiscaisDoCliente as FerramentaDeEscrita,
  cartaDeCorrecao as FerramentaDeEscrita,
  cancelarNotaFiscalPeloAssistente as FerramentaDeEscrita,
];
