/**
 * As regras da configuração fiscal pelo assistente que não falam com o banco:
 * nomes de gente para regime, ambiente e preferências, a sugestão do regime a
 * partir do cadastro público do CNPJ, a lista do que falta (em ordem, dizendo
 * se o assistente resolve ou se é a pessoa no painel), a troca dos campos da
 * conversa pelos campos da tela e a limpeza de qualquer segredo da saída.
 *
 * Fica separado para ser testado sem servidor (configuracao-fiscal.test.ts).
 * Nada aqui importa com "@/": só caminhos relativos com extensão.
 */

import { REGIMES, type Regime, type RegrasDeMercadoria, type RegrasDeServico } from "../../fiscal/regras.ts";
import { UFS } from "../../fiscal/tabelas.ts";
import { PRESENCAS, semFornecedor } from "./notas-fiscais-regras.ts";

/* ================================================================== */
/* Onde fica cada coisa no painel                                      */
/* ================================================================== */

export const CAMINHO_DA_CONFIGURACAO = "/dashboard/notas-fiscais/configuracao";

/** Os lugares da tela de configuração que só a pessoa mexe (segredos e arquivo). */
export const NO_PAINEL = {
  empresa: { onde: "Notas fiscais › Configuração › passo 1 \"Sua empresa\"", caminho: `${CAMINHO_DA_CONFIGURACAO}?passo=empresa` },
  certificado: {
    onde: "Notas fiscais › Configuração › passo 2 \"Certificado digital\" (escolher o arquivo .pfx ou .p12 e digitar a senha do certificado)",
    caminho: `${CAMINHO_DA_CONFIGURACAO}?passo=certificado`,
  },
  csc: {
    onde: "Notas fiscais › Configuração › passo 3 \"Notas e impostos\" › bloco \"Cupom fiscal\" (campos \"Identificação do CSC de teste\", \"CSC de teste\" e, para emitir de verdade, os dois \"de uso real\")",
    caminho: `${CAMINHO_DA_CONFIGURACAO}?passo=notas`,
  },
  prefeitura: {
    onde: "Notas fiscais › Configuração › passo 3 \"Notas e impostos\" › bloco \"Seus serviços\" (campos \"Usuário da prefeitura\" e \"Senha da prefeitura\"; o \"Número de autenticação\" fica em \"Mais opções da nota de serviço\")",
    caminho: `${CAMINHO_DA_CONFIGURACAO}?passo=notas`,
  },
  testar: { onde: "Notas fiscais › Configuração › passo 4 \"Testar e começar\"", caminho: `${CAMINHO_DA_CONFIGURACAO}?passo=testar` },
  pacotes: { onde: "Assinatura › Notas fiscais (pacotes de notas)", caminho: "/dashboard/assinatura#notas" },
} as const;

export const AVISO_DOS_SEGREDOS =
  "Certificado digital (arquivo e senha), CSC do cupom fiscal e usuário/senha da prefeitura NUNCA passam por esta conversa: não peça, não aceite e não repita esses dados. A pessoa preenche no painel, no lugar indicado. Por aqui só se sabe se já estão configurados.";

/* ================================================================== */
/* Nomes de gente                                                      */
/* ================================================================== */

export type Modelo = "nfe" | "nfce" | "nfse";
export const MODELOS: Modelo[] = ["nfe", "nfce", "nfse"];

export const NOME_DO_TIPO: Record<Modelo, string> = {
  nfe: "NF-e (nota de produto)",
  nfce: "NFC-e (cupom fiscal)",
  nfse: "NFS-e (nota de serviço)",
};

/** Regime: a opção da conversa e o valor que a tela grava. */
export const REGIME_POR_OPCAO: Record<string, Regime> = {
  simples_nacional: "simplesNacional",
  mei: "simplesNacionalMEI",
  simples_excesso_sublimite: "simplesNacionalExcessoSublimite",
  regime_normal: "regimeNormal",
};
export const OPCOES_DE_REGIME = Object.keys(REGIME_POR_OPCAO);

export function opcaoDoRegime(regime: string | null | undefined): string | null {
  return Object.entries(REGIME_POR_OPCAO).find(([, valor]) => valor === regime)?.[0] ?? null;
}

export function rotuloDoRegime(regime: string | null | undefined): string | null {
  return REGIMES.find((r) => r.valor === regime)?.rotulo ?? null;
}

/** Regime especial da nota de serviço (o mesmo seletor da tela "Sua empresa"). */
export const REGIME_ESPECIAL_POR_OPCAO: Record<string, string | null> = {
  me_epp: "microenterpriseAndSmallBusiness",
  mei: "individualMicroenterprise",
  microempresa_municipal: "municipalMicroenterprise",
  estimativa: "estimate",
  sociedade_de_profissionais: "societyOfProfessionals",
  cooperativa: "cooperative",
  nenhum: "noSpecialRegime",
  outro: "others",
  nao_informar: null,
};
const ROTULO_DO_REGIME_ESPECIAL: Record<string, string> = {
  microenterpriseAndSmallBusiness: "Microempresa ou empresa de pequeno porte (ME/EPP)",
  individualMicroenterprise: "Microempreendedor individual (MEI)",
  municipalMicroenterprise: "Microempresa municipal",
  estimate: "Estimativa",
  societyOfProfessionals: "Sociedade de profissionais",
  cooperative: "Cooperativa",
  noSpecialRegime: "Nenhum regime especial",
  others: "Outro",
};

/** Como o Simples recolhe na nota de serviço (o seletor da tela "Sua empresa"). */
export const REGIME_SIMPLES_NFSE_POR_OPCAO: Record<string, string | null> = {
  tudo_pelo_simples: "federalAndMunicipalBySimplesNacional",
  federais_pelo_simples_iss_na_prefeitura: "federalBySimplesAndIssqnByNfse",
  tudo_fora_do_simples: "federalAndMunicipalByNfse",
  nao_informar: null,
};
const ROTULO_DO_REGIME_SIMPLES_NFSE: Record<string, string> = {
  federalAndMunicipalBySimplesNacional: "Tudo pelo Simples, inclusive o ISS",
  federalBySimplesAndIssqnByNfse: "Impostos federais pelo Simples e ISS pago à prefeitura",
  federalAndMunicipalByNfse: "Tudo fora do Simples",
};

export const rotuloDoRegimeEspecial = (valor: string | null | undefined) => (valor ? ROTULO_DO_REGIME_ESPECIAL[valor] ?? valor : null);
export const rotuloDoRegimeSimplesNfse = (valor: string | null | undefined) => (valor ? ROTULO_DO_REGIME_SIMPLES_NFSE[valor] ?? valor : null);

export function rotuloDoAmbienteDaNota(ambiente: string | null | undefined): string {
  if (ambiente === "producao") return "produção — a nota vale de verdade, tem valor fiscal e gasta o pacote do mês";
  if (ambiente === "simulacao") return "teste (simulação da prefeitura) — sem valor fiscal";
  return "homologação (teste) — sem valor fiscal";
}

export function rotuloDoSistema(servidor: "sandbox" | "producao"): string {
  return servidor === "producao"
    ? "Ktalogu oficial: cada tipo de nota pode estar em teste ou valendo de verdade."
    : "Ambiente de testes do Ktalogu: toda nota sai em modo de teste, sem valor fiscal. A emissão de verdade só é ligada no Ktalogu oficial.";
}

/** Presença da venda: opção da conversa ↔ valor gravado (o mesmo de notas-fiscais-regras). */
export function opcaoDaPresenca(valor: string | null | undefined): string | null {
  return Object.entries(PRESENCAS).find(([, v]) => v === valor)?.[0] ?? null;
}

export const OPCOES_DO_CUPOM_NO_PDV = ["perguntar", "sempre", "nunca"] as const;

/* ================================================================== */
/* Regime sugerido pelo cadastro público                              */
/* ================================================================== */

export type SugestaoDeRegime = {
  regime_sugerido: string | null;
  regime_sugerido_nome: string | null;
  certeza: "alta" | "media" | "nenhuma";
  explicacao: string;
  pergunta_para_a_pessoa: string;
  opcoes: Array<{ opcao: string; nome: string }>;
};

const TODAS_AS_OPCOES = () => OPCOES_DE_REGIME.map((opcao) => ({ opcao, nome: rotuloDoRegime(REGIME_POR_OPCAO[opcao]) ?? opcao }));

/**
 * O que o cadastro público diz sobre o regime. MEI e Simples aparecem lá; fora
 * do Simples o cadastro não diz se é Lucro Presumido ou Real (para a nota os
 * dois são "regime normal", mas as alíquotas de PIS/COFINS mudam). Sempre é
 * sugestão: quem confirma é a pessoa, de preferência com o contador.
 */
export function sugerirRegime(cadastro: { simples: boolean | null; mei: boolean | null }): SugestaoDeRegime {
  const opcoes = TODAS_AS_OPCOES();
  if (cadastro.mei === true) {
    return {
      regime_sugerido: "mei",
      regime_sugerido_nome: rotuloDoRegime("simplesNacionalMEI"),
      certeza: "alta",
      explicacao: "O cadastro público mostra a empresa como MEI (microempreendedor individual).",
      pergunta_para_a_pessoa: "O cadastro da Receita mostra a empresa como MEI. Confirma que a empresa é MEI?",
      opcoes,
    };
  }
  if (cadastro.simples === true) {
    return {
      regime_sugerido: "simples_nacional",
      regime_sugerido_nome: rotuloDoRegime("simplesNacional"),
      certeza: "alta",
      explicacao:
        "O cadastro público mostra a empresa como optante pelo Simples Nacional. Se ela passou do sublimite de faturamento do estado, o certo é \"Simples Nacional — excesso de sublimite\": isso o cadastro não mostra, só o contador sabe.",
      pergunta_para_a_pessoa:
        "O cadastro da Receita mostra a empresa no Simples Nacional. Confirma? (Se o contador disse que a empresa passou do sublimite do estado, é o \"Simples — excesso de sublimite\".)",
      opcoes,
    };
  }
  if (cadastro.simples === false) {
    return {
      regime_sugerido: "regime_normal",
      regime_sugerido_nome: rotuloDoRegime("regimeNormal"),
      certeza: "media",
      explicacao:
        "O cadastro público mostra que a empresa NÃO está no Simples Nacional, então é regime normal (Lucro Presumido ou Lucro Real). O cadastro não diz qual dos dois: para a nota os dois são \"regime normal\", mas as alíquotas de PIS/COFINS mudam — pergunte à pessoa ou ao contador.",
      pergunta_para_a_pessoa:
        "O cadastro da Receita mostra que a empresa não está no Simples. Ela é Lucro Presumido ou Lucro Real? (Se não souber, o contador sabe.)",
      opcoes,
    };
  }
  return {
    regime_sugerido: null,
    regime_sugerido_nome: null,
    certeza: "nenhuma",
    explicacao: "O cadastro público não informou a opção pelo Simples. O regime precisa ser confirmado com a pessoa ou o contador.",
    pergunta_para_a_pessoa: "Qual é o regime tributário da empresa: Simples Nacional, MEI, Simples com excesso de sublimite, ou Lucro Presumido/Real?",
    opcoes,
  };
}

/* ================================================================== */
/* Segredos fora da saída                                              */
/* ================================================================== */

/* Nome de campo que denuncia segredo. Booleano ("csc_de_teste_configurado")
   passa: diz só se existe, nunca o valor. */
const NOME_DE_SEGREDO = /(cifrad|segredo|senha|password|token|csc|apikey|api_key|chave|credencia|autenticacao|certificado_arquivo|pfx)/i;
const PODE_PASSAR = new Set(["chave_de_acesso"]);
/* Valor que tem cara de chave de API, mesmo num campo de nome inocente. */
const VALOR_DE_CHAVE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b.*\b[0-9a-f]{8}-[0-9a-f]{4}/i;

/**
 * Tira da saída qualquer coisa com cara de segredo e o nome do fornecedor do
 * serviço de notas. É a última rede: as ferramentas já montam a saída sem
 * esses dados, mas um campo novo no painel não pode vazar por descuido.
 */
export function semSegredos<T>(valor: T): T {
  if (Array.isArray(valor)) return valor.map((item) => semSegredos(item)) as unknown as T;
  if (typeof valor === "string") return semFornecedor(VALOR_DE_CHAVE.test(valor) ? "[oculto]" : valor) as unknown as T;
  if (!valor || typeof valor !== "object") return valor;
  const saida: Record<string, unknown> = {};
  for (const [nome, item] of Object.entries(valor as Record<string, unknown>)) {
    if (NOME_DE_SEGREDO.test(nome) && !PODE_PASSAR.has(nome) && typeof item !== "boolean") continue;
    saida[nome] = semSegredos(item);
  }
  return saida as T;
}

/* ================================================================== */
/* Dados da empresa: conversa → tela                                   */
/* ================================================================== */

export type EmpresaFiscal = {
  cnpj: string;
  razaoSocial: string;
  nomeFantasia: string | null;
  inscricaoEstadual: string | null;
  inscricaoMunicipal: string | null;
  regime: string;
  regimeEspecial: string | null;
  regimeSimplesNfse: string | null;
  cnaes: Array<{ codigo: string; principal: boolean }>;
  email: string | null;
  telefone: string | null;
  endereco: { cep: string; logradouro: string; numero: string; complemento: string | null; bairro: string; cidade: string; uf: string; ibge: string };
};

export const soDigitos = (valor: unknown) => String(valor ?? "").replace(/\D/g, "");
const textoLimpo = (valor: unknown) => (typeof valor === "string" ? valor.replace(/\s+/g, " ").trim() : "");

/** Os campos que dá para apagar com `limpar` (os outros são obrigatórios na tela). */
export const CAMPOS_QUE_SE_APAGAM = [
  "nome_fantasia",
  "inscricao_estadual",
  "inscricao_municipal",
  "regime_especial",
  "regime_simples_nfse",
  "email",
  "telefone",
  "complemento",
] as const;

const ROTULO_DO_CAMPO: Record<string, string> = {
  cnpj: "CNPJ",
  razao_social: "Razão social",
  nome_fantasia: "Nome fantasia",
  inscricao_estadual: "Inscrição estadual",
  inscricao_municipal: "Inscrição municipal",
  regime: "Regime tributário",
  regime_especial: "Regime especial (nota de serviço)",
  regime_simples_nfse: "Como o Simples recolhe na nota de serviço",
  cnaes: "Atividades (CNAE)",
  email: "E-mail",
  telefone: "Telefone",
  cep: "CEP",
  logradouro: "Rua",
  numero: "Número",
  complemento: "Complemento",
  bairro: "Bairro",
  cidade: "Cidade",
  uf: "Estado",
};

export type Mudanca = { campo: string; nome: string; antes: unknown; depois: unknown };

export function vazioDeEmpresa(): EmpresaFiscal {
  return {
    cnpj: "",
    razaoSocial: "",
    nomeFantasia: null,
    inscricaoEstadual: null,
    inscricaoMunicipal: null,
    regime: "",
    regimeEspecial: null,
    regimeSimplesNfse: null,
    cnaes: [],
    email: null,
    telefone: null,
    endereco: { cep: "", logradouro: "", numero: "", complemento: null, bairro: "", cidade: "", uf: "", ibge: "" },
  };
}

/** Os dados como a pessoa lê (e como a conversa escreve). */
export function empresaParaLer(e: EmpresaFiscal) {
  return {
    cnpj: e.cnpj || null,
    razao_social: e.razaoSocial || null,
    nome_fantasia: e.nomeFantasia,
    inscricao_estadual: e.inscricaoEstadual,
    inscricao_municipal: e.inscricaoMunicipal,
    regime: opcaoDoRegime(e.regime),
    regime_nome: rotuloDoRegime(e.regime),
    regime_especial: rotuloDoRegimeEspecial(e.regimeEspecial),
    regime_simples_nfse: rotuloDoRegimeSimplesNfse(e.regimeSimplesNfse),
    cnaes: e.cnaes.map((c) => ({ codigo: c.codigo, principal: c.principal })),
    email: e.email,
    telefone: e.telefone,
    endereco: {
      cep: e.endereco.cep || null,
      logradouro: e.endereco.logradouro || null,
      numero: e.endereco.numero || null,
      complemento: e.endereco.complemento,
      bairro: e.endereco.bairro || null,
      cidade: e.endereco.cidade || null,
      uf: e.endereco.uf || null,
      codigo_ibge: e.endereco.ibge || null,
    },
  };
}

/**
 * Aplica o que a conversa mandou em cima dos dados atuais. Só muda o que veio;
 * `limpar` apaga os campos opcionais. Devolve os dados novos, a lista do que
 * mudou (antes e depois, para a prévia) e se o endereço mudou (aí o código
 * IBGE da cidade é procurado de novo).
 */
export function aplicarMudancasNaEmpresa(
  atual: EmpresaFiscal,
  args: Record<string, unknown>
): { dados: EmpresaFiscal; mudancas: Mudanca[]; enderecoMudou: boolean; erros: string[] } {
  const dados: EmpresaFiscal = { ...atual, cnaes: [...atual.cnaes], endereco: { ...atual.endereco } };
  const erros: string[] = [];
  const lido = (nome: string) => (args[nome] === undefined ? undefined : args[nome]);

  const texto = (nome: string, aplicar: (v: string) => void) => {
    const v = lido(nome);
    if (v === undefined) return;
    aplicar(textoLimpo(v));
  };

  const cnpj = lido("cnpj");
  if (cnpj !== undefined) {
    const d = soDigitos(cnpj);
    if (d.length !== 14) erros.push("O CNPJ precisa ter 14 números.");
    else dados.cnpj = d;
  }
  texto("razao_social", (v) => (dados.razaoSocial = v.slice(0, 120)));
  texto("nome_fantasia", (v) => (dados.nomeFantasia = v.slice(0, 120) || null));
  texto("inscricao_estadual", (v) => (dados.inscricaoEstadual = v.toUpperCase() === "ISENTO" ? "ISENTO" : soDigitos(v) || null));
  texto("inscricao_municipal", (v) => (dados.inscricaoMunicipal = v.replace(/[^\p{L}\p{N}./-]/gu, "") || null));

  const regime = lido("regime");
  if (regime !== undefined) {
    const valor = REGIME_POR_OPCAO[String(regime)];
    if (!valor) erros.push(`Regime desconhecido. Use: ${OPCOES_DE_REGIME.join(", ")}.`);
    else dados.regime = valor;
  }
  const especial = lido("regime_especial");
  if (especial !== undefined) {
    if (!(String(especial) in REGIME_ESPECIAL_POR_OPCAO)) erros.push("Regime especial desconhecido.");
    else dados.regimeEspecial = REGIME_ESPECIAL_POR_OPCAO[String(especial)];
  }
  const simplesNfse = lido("regime_simples_nfse");
  if (simplesNfse !== undefined) {
    if (!(String(simplesNfse) in REGIME_SIMPLES_NFSE_POR_OPCAO)) erros.push("Opção do Simples na nota de serviço desconhecida.");
    else dados.regimeSimplesNfse = REGIME_SIMPLES_NFSE_POR_OPCAO[String(simplesNfse)];
  }

  const cnaes = lido("cnaes");
  if (cnaes !== undefined) {
    const lista = (Array.isArray(cnaes) ? cnaes : [cnaes]).map(soDigitos).filter(Boolean);
    const errados = lista.filter((c) => c.length !== 7);
    if (errados.length) erros.push(`CNAE tem 7 números (ex.: 1813-0/01). Confira: ${errados.join(", ")}.`);
    const unicos = [...new Set(lista.filter((c) => c.length === 7))].slice(0, 30);
    const principal = soDigitos(lido("cnae_principal")) || unicos[0];
    dados.cnaes = unicos.map((codigo) => ({ codigo, principal: codigo === principal }));
    if (dados.cnaes.length && !dados.cnaes.some((c) => c.principal)) dados.cnaes[0].principal = true;
  } else if (lido("cnae_principal") !== undefined) {
    const principal = soDigitos(lido("cnae_principal"));
    if (principal.length !== 7) erros.push("O CNAE principal tem 7 números.");
    else {
      const existe = dados.cnaes.some((c) => c.codigo === principal);
      dados.cnaes = [...(existe ? [] : [{ codigo: principal, principal: true }]), ...dados.cnaes].map((c) => ({ codigo: c.codigo, principal: c.codigo === principal }));
    }
  }

  texto("email", (v) => {
    const email = v.toLowerCase();
    if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) erros.push("E-mail inválido.");
    else dados.email = email || null;
  });
  texto("telefone", (v) => (dados.telefone = soDigitos(v).slice(0, 13) || null));

  let enderecoMudou = false;
  texto("cep", (v) => {
    const cep = soDigitos(v);
    if (cep.length !== 8) erros.push("O CEP tem 8 números.");
    else if (cep !== dados.endereco.cep) {
      dados.endereco.cep = cep;
      enderecoMudou = true;
    }
  });
  texto("logradouro", (v) => (dados.endereco.logradouro = v.slice(0, 100)));
  texto("numero", (v) => (dados.endereco.numero = v.slice(0, 10) || "S/N"));
  texto("complemento", (v) => (dados.endereco.complemento = v.slice(0, 150) || null));
  texto("bairro", (v) => (dados.endereco.bairro = v.slice(0, 100)));
  texto("cidade", (v) => {
    if (v !== dados.endereco.cidade) enderecoMudou = true;
    dados.endereco.cidade = v.slice(0, 100);
  });
  texto("uf", (v) => {
    const uf = v.toUpperCase();
    if (!UFS.includes(uf)) erros.push("Estado (UF) inválido. Use a sigla, como SP.");
    else {
      if (uf !== dados.endereco.uf) enderecoMudou = true;
      dados.endereco.uf = uf;
    }
  });

  const limpar = Array.isArray(args.limpar) ? (args.limpar as unknown[]).map(String) : [];
  for (const campo of limpar) {
    if (!(CAMPOS_QUE_SE_APAGAM as readonly string[]).includes(campo)) {
      erros.push(`"${campo}" não pode ser apagado (é obrigatório ou não existe). Apagáveis: ${CAMPOS_QUE_SE_APAGAM.join(", ")}.`);
      continue;
    }
    if (campo === "nome_fantasia") dados.nomeFantasia = null;
    if (campo === "inscricao_estadual") dados.inscricaoEstadual = null;
    if (campo === "inscricao_municipal") dados.inscricaoMunicipal = null;
    if (campo === "regime_especial") dados.regimeEspecial = null;
    if (campo === "regime_simples_nfse") dados.regimeSimplesNfse = null;
    if (campo === "email") dados.email = null;
    if (campo === "telefone") dados.telefone = null;
    if (campo === "complemento") dados.endereco.complemento = null;
  }

  return { dados, mudancas: diferencasDaEmpresa(atual, dados), enderecoMudou, erros };
}

export function diferencasDaEmpresa(antes: EmpresaFiscal, depois: EmpresaFiscal): Mudanca[] {
  const a = empresaParaLer(antes);
  const d = empresaParaLer(depois);
  const planos: Record<string, unknown>[] = [
    { ...a, ...a.endereco, endereco: undefined, regime_nome: undefined },
    { ...d, ...d.endereco, endereco: undefined, regime_nome: undefined },
  ];
  const mudancas: Mudanca[] = [];
  for (const campo of Object.keys(ROTULO_DO_CAMPO)) {
    const x = planos[0][campo];
    const y = planos[1][campo];
    if (JSON.stringify(x ?? null) === JSON.stringify(y ?? null)) continue;
    mudancas.push({ campo, nome: ROTULO_DO_CAMPO[campo], antes: x ?? null, depois: y ?? null });
  }
  return mudancas;
}

/** O que falta nos dados para a tela aceitar (as mesmas conferências de lerDadosDaEmpresa). */
export function faltasDaEmpresa(e: EmpresaFiscal): string[] {
  const faltas: string[] = [];
  if (soDigitos(e.cnpj).length !== 14) faltas.push("CNPJ");
  if (textoLimpo(e.razaoSocial).length < 2) faltas.push("razão social");
  if (!REGIMES.some((r) => r.valor === e.regime)) faltas.push("regime tributário");
  if (soDigitos(e.endereco.cep).length !== 8) faltas.push("CEP");
  if (!e.endereco.logradouro) faltas.push("rua");
  if (!e.endereco.bairro) faltas.push("bairro");
  if (!e.endereco.cidade) faltas.push("cidade");
  if (!UFS.includes(e.endereco.uf)) faltas.push("estado (UF)");
  return faltas;
}

/** Preenche com o cadastro público só o que ainda está vazio (o que a pessoa já escreveu vale mais). */
export function completarComCadastro(
  atual: EmpresaFiscal,
  cadastro: {
    cnpj: string;
    razaoSocial: string;
    nomeFantasia: string;
    email: string;
    telefone: string;
    inscricaoEstadual: string | null;
    cnaes: Array<{ codigo: string; principal: boolean }>;
    endereco: { cep: string; logradouro: string; numero: string; complemento: string; bairro: string; cidade: string; uf: string; ibge: string };
  }
): EmpresaFiscal {
  const e: EmpresaFiscal = { ...atual, cnaes: [...atual.cnaes], endereco: { ...atual.endereco } };
  if (!e.cnpj) e.cnpj = soDigitos(cadastro.cnpj);
  if (!e.razaoSocial) e.razaoSocial = cadastro.razaoSocial;
  if (!e.nomeFantasia && cadastro.nomeFantasia) e.nomeFantasia = cadastro.nomeFantasia;
  if (!e.email && cadastro.email) e.email = cadastro.email;
  if (!e.telefone && cadastro.telefone) e.telefone = cadastro.telefone;
  if (!e.inscricaoEstadual && cadastro.inscricaoEstadual) e.inscricaoEstadual = cadastro.inscricaoEstadual;
  if (!e.cnaes.length && cadastro.cnaes.length) e.cnaes = cadastro.cnaes.map((c) => ({ codigo: c.codigo, principal: c.principal }));
  const vazio = !e.endereco.cep && !e.endereco.logradouro;
  if (vazio) {
    e.endereco = {
      cep: cadastro.endereco.cep,
      logradouro: cadastro.endereco.logradouro,
      numero: cadastro.endereco.numero || "S/N",
      complemento: cadastro.endereco.complemento || null,
      bairro: cadastro.endereco.bairro,
      cidade: cadastro.endereco.cidade,
      uf: cadastro.endereco.uf,
      ibge: cadastro.endereco.ibge,
    };
  }
  return e;
}

/** O CNPJ que aparece no titular do certificado ("EMPRESA LTDA:12345678000199"). */
export function cnpjDoTitular(titular: string | null | undefined): string | null {
  const achado = String(titular ?? "").match(/\d{14}/);
  return achado ? achado[0] : null;
}

/* ================================================================== */
/* O que falta, em ordem                                               */
/* ================================================================== */

export type EstadoDosTipos = Record<
  Modelo,
  {
    ativo: boolean;
    ambiente: string;
    /** O último teste deste tipo saiu autorizado. */
    testeAprovado: boolean;
    /** NFC-e: identificação + CSC do ambiente atual guardados. */
    cscDoAmbiente?: boolean;
    /** NFS-e: a prefeitura pede login e ele não está guardado. */
    faltaLoginDaPrefeitura?: boolean;
  }
>;

export type EstadoParaPassos = {
  servidor: "sandbox" | "producao";
  emissaoDisponivel: boolean;
  suspensa: boolean;
  temEmissor: boolean;
  empresaCadastrada: boolean;
  faltasNaEmpresa: string[];
  ultimoErro: string | null;
  inscricaoEstadual: boolean;
  inscricaoMunicipal: boolean;
  certificado: { enviado: boolean; dias: number | null };
  tipos: EstadoDosTipos;
  perfis: { mercadoria: { existe: boolean; faltas: string[] }; servico: { existe: boolean; faltas: string[] } };
  produtosSemNcm: number;
  algumTesteAprovado: boolean;
  saldoDisponivel: number | null;
};

export type Passo = {
  ordem: number;
  passo: string;
  feito: boolean;
  quem_faz: "assistente" | "pessoa_no_painel" | "suporte" | "ninguem";
  ferramenta?: string;
  onde_no_painel?: string;
  caminho_no_painel?: string;
  o_que_fazer: string;
};

export type Pergunta = { pergunta: string; por_que: string; opcoes?: Array<{ opcao: string; nome: string }>; usar_em: string };

/**
 * A lista do que falta para emitir, em ordem, dizendo para cada passo quem
 * faz: o assistente (com qual ferramenta) ou a pessoa no painel (onde). E as
 * decisões que só a pessoa toma, em forma de pergunta com opções.
 */
export function montarProximosPassos(e: EstadoParaPassos): { passos: Passo[]; perguntas: Pergunta[]; pronta: boolean } {
  const passos: Omit<Passo, "ordem">[] = [];
  const perguntas: Pergunta[] = [];
  const ligados = MODELOS.filter((m) => e.tipos[m].ativo);
  const precisaMercadoria = e.tipos.nfe.ativo || e.tipos.nfce.ativo;

  if (!e.emissaoDisponivel) {
    passos.push({
      passo: "Emissão disponível no Ktalogu",
      feito: false,
      quem_faz: "suporte",
      o_que_fazer: "A emissão de notas está indisponível no momento. Os dados podem ser preenchidos, mas nada é enviado; se demorar, a pessoa fala com o suporte do Ktalogu.",
    });
  }
  if (e.suspensa) {
    passos.push({ passo: "Emissão suspensa", feito: false, quem_faz: "suporte", o_que_fazer: "A emissão desta empresa está suspensa. A pessoa fala com o suporte do Ktalogu." });
  }

  /* 1. Empresa */
  const empresaOk = e.empresaCadastrada && !e.faltasNaEmpresa.length;
  passos.push({
    passo: "Dados da empresa (CNPJ, razão social, regime, endereço, inscrições)",
    feito: empresaOk,
    quem_faz: "assistente",
    ferramenta: "alterar_dados_fiscais_da_empresa",
    o_que_fazer: empresaOk
      ? "Pronto."
      : `${e.temEmissor ? "Complete e salve" : "Cadastre"} os dados da empresa: use consultar_cnpj_para_nota para trazer o cadastro público e sugerir o regime, confirme com a pessoa e grave com alterar_dados_fiscais_da_empresa (preencher_pelo_cnpj ajuda).${
          e.faltasNaEmpresa.length ? ` Falta: ${e.faltasNaEmpresa.join(", ")}.` : ""
        }${e.ultimoErro ? ` Última recusa: ${semFornecedor(e.ultimoErro)}` : ""}`,
  });
  if (!empresaOk && !e.temEmissor) {
    perguntas.push({
      pergunta: "Qual é o regime tributário da empresa?",
      por_que: "Define os códigos de imposto de toda nota. O cadastro público ajuda (consultar_cnpj_para_nota), mas quem confirma é a pessoa ou o contador.",
      opcoes: TODAS_AS_OPCOES(),
      usar_em: "alterar_dados_fiscais_da_empresa.regime",
    });
  }

  /* 2. Certificado */
  const dias = e.certificado.dias;
  const certOk = e.certificado.enviado && dias !== null && dias >= 0;
  passos.push({
    passo: "Certificado digital A1",
    feito: certOk,
    quem_faz: "pessoa_no_painel",
    onde_no_painel: NO_PAINEL.certificado.onde,
    caminho_no_painel: NO_PAINEL.certificado.caminho,
    o_que_fazer: !e.empresaCadastrada
      ? "Depois de salvar os dados da empresa, a pessoa envia o arquivo do certificado A1 (.pfx ou .p12) e digita a senha dele no painel. O arquivo e a senha nunca passam pela conversa."
      : !e.certificado.enviado
        ? "A pessoa envia o arquivo do certificado A1 (.pfx ou .p12) e digita a senha dele no painel. O arquivo e a senha nunca passam pela conversa."
        : dias !== null && dias < 0
          ? "O certificado venceu. A pessoa envia o certificado novo no painel."
          : dias !== null && dias <= 30
            ? `Pronto, mas vence em ${dias} ${dias === 1 ? "dia" : "dias"}: avise a pessoa para renovar e enviar o novo no painel.`
            : "Pronto.",
  });

  /* 3. Tipos de nota */
  passos.push({
    passo: "Escolher os tipos de nota (NF-e, NFC-e, NFS-e)",
    feito: ligados.length > 0,
    quem_faz: "assistente",
    ferramenta: "configurar_tipo_de_nota",
    o_que_fazer: ligados.length
      ? `Ligados: ${ligados.map((m) => NOME_DO_TIPO[m]).join(", ")}.`
      : "Pergunte quais notas a empresa emite e ligue cada uma com configurar_tipo_de_nota (começa em teste).",
  });
  if (!ligados.length) {
    perguntas.push({
      pergunta: "Quais notas a empresa vai emitir?",
      por_que: "Produto vendido a empresa/cliente identificado = NF-e; venda de balcão/PDV = NFC-e (cupom); serviço (arte, impressão sob encomenda) = NFS-e.",
      opcoes: MODELOS.map((m) => ({ opcao: m, nome: NOME_DO_TIPO[m] })),
      usar_em: "configurar_tipo_de_nota.tipo (uma chamada por tipo)",
    });
  }
  if (precisaMercadoria && !e.inscricaoEstadual) {
    passos.push({
      passo: "Inscrição estadual",
      feito: false,
      quem_faz: "assistente",
      ferramenta: "alterar_dados_fiscais_da_empresa",
      o_que_fazer: "A nota de produto e o cupom fiscal precisam da inscrição estadual. Pergunte o número à pessoa (o cadastro público às vezes traz) e grave em inscricao_estadual.",
    });
  }
  if (e.tipos.nfse.ativo && !e.inscricaoMunicipal) {
    passos.push({
      passo: "Inscrição municipal",
      feito: false,
      quem_faz: "assistente",
      ferramenta: "alterar_dados_fiscais_da_empresa",
      o_que_fazer: "A nota de serviço precisa da inscrição municipal. Pergunte o número à pessoa e grave em inscricao_municipal.",
    });
  }
  if (e.tipos.nfce.ativo && e.tipos.nfce.cscDoAmbiente === false) {
    passos.push({
      passo: "CSC do cupom fiscal",
      feito: false,
      quem_faz: "pessoa_no_painel",
      onde_no_painel: NO_PAINEL.csc.onde,
      caminho_no_painel: NO_PAINEL.csc.caminho,
      o_que_fazer: "O cupom fiscal precisa do CSC (código de segurança gerado no site da SEFAZ do estado) e da identificação dele. É segredo: a pessoa preenche no painel, não pela conversa.",
    });
  }
  if (e.tipos.nfse.ativo && e.tipos.nfse.faltaLoginDaPrefeitura) {
    passos.push({
      passo: "Acesso ao sistema da prefeitura",
      feito: false,
      quem_faz: "pessoa_no_painel",
      onde_no_painel: NO_PAINEL.prefeitura.onde,
      caminho_no_painel: NO_PAINEL.prefeitura.caminho,
      o_que_fazer: "A prefeitura exige usuário e senha (ou código de acesso) do sistema de notas dela. É segredo: a pessoa preenche no painel, não pela conversa.",
    });
  }

  /* 4. Tributação */
  const faltaMerc = precisaMercadoria && (!e.perfis.mercadoria.existe || e.perfis.mercadoria.faltas.length > 0);
  const faltaServ = e.tipos.nfse.ativo && (!e.perfis.servico.existe || e.perfis.servico.faltas.length > 0);
  const tribOk = ligados.length > 0 && !faltaMerc && !faltaServ;
  passos.push({
    passo: "Tributação (CFOP, CSOSN/CST, PIS/COFINS, ISS)",
    feito: tribOk,
    quem_faz: "assistente",
    ferramenta: "salvar_tributacao_fiscal",
    o_que_fazer: tribOk
      ? "Pronto."
      : [
          !ligados.length ? "Depende dos tipos de nota ligados." : null,
          faltaMerc
            ? e.perfis.mercadoria.existe
              ? `A tributação de produtos está incompleta: ${e.perfis.mercadoria.faltas.join(", ")}.`
              : "Crie a tributação de produtos (modelo producao-simples/revenda-simples no Simples ou MEI; producao-normal/revenda-normal no regime normal)."
            : null,
          faltaServ
            ? e.perfis.servico.existe
              ? `A tributação de serviços está incompleta: ${e.perfis.servico.faltas.join(", ")}.`
              : "Crie a tributação de serviços (código do serviço da LC 116 e alíquota do ISS)."
            : null,
          "Os modelos prontos são sugestões: peça à pessoa para conferir com o contador.",
        ]
          .filter(Boolean)
          .join(" "),
  });
  if (faltaMerc && !e.perfis.mercadoria.existe) {
    perguntas.push({
      pergunta: "Os produtos são fabricados/personalizados pela empresa ou comprados prontos para revender?",
      por_que: "Muda o CFOP da nota (5101 produção própria / 5102 revenda).",
      opcoes: [
        { opcao: "producao", nome: "Fabrico ou personalizo" },
        { opcao: "revenda", nome: "Compro pronto e revendo" },
      ],
      usar_em: "salvar_tributacao_fiscal.modelo",
    });
  }
  if (faltaServ) {
    perguntas.push({
      pergunta: "Qual é a alíquota do ISS da empresa (e o código do serviço, se a pessoa souber)?",
      por_que: "A nota de serviço não sai sem a alíquota do ISS; no Simples é a da faixa da empresa. O contador sabe.",
      usar_em: "salvar_tributacao_fiscal.aliquota_iss",
    });
  }

  /* 5. Produtos */
  if (precisaMercadoria) {
    passos.push({
      passo: "NCM dos produtos",
      feito: e.produtosSemNcm === 0,
      quem_faz: "assistente",
      ferramenta: "corrigir_ncm_dos_produtos",
      o_que_fazer: e.produtosSemNcm === 0 ? "Pronto." : `${e.produtosSemNcm} produto(s) ativo(s) sem NCM. Liste com buscar_produtos e corrija com corrigir_ncm_dos_produtos (nunca chute o NCM).`,
    });
  }

  /* 6. Teste */
  const emTeste = ligados.filter((m) => e.tipos[m].ambiente !== "producao");
  const semTeste = emTeste.filter((m) => !e.tipos[m].testeAprovado);
  passos.push({
    passo: "Nota de teste em homologação",
    feito: e.algumTesteAprovado && semTeste.length === 0,
    quem_faz: "assistente",
    ferramenta: "emitir_nota_de_teste",
    o_que_fazer:
      !ligados.length
        ? "Depois de ligar um tipo de nota."
        : semTeste.length
          ? `Emita uma nota de teste de R$ 1,00, sem valor fiscal, de: ${semTeste.map((m) => NOME_DO_TIPO[m]).join(", ")}.`
          : "Pronto.",
  });

  /* 7. Produção */
  if (e.servidor === "sandbox") {
    passos.push({
      passo: "Emitir de verdade",
      feito: false,
      quem_faz: "ninguem",
      o_que_fazer: "Aqui é o ambiente de testes do Ktalogu: as notas ficam sempre em modo de teste. A emissão de verdade é ligada no Ktalogu oficial.",
    });
  } else {
    const valendo = ligados.filter((m) => e.tipos[m].ambiente === "producao");
    passos.push({
      passo: "Emitir de verdade (produção)",
      feito: ligados.length > 0 && valendo.length === ligados.length,
      quem_faz: "assistente",
      ferramenta: "configurar_tipo_de_nota",
      o_que_fazer: !ligados.length
        ? "Depois de ligar e testar um tipo de nota."
        : valendo.length === ligados.length
          ? "Todos os tipos ligados já emitem de verdade."
          : `Com o teste aprovado e a autorização da pessoa, passe para produção com configurar_tipo_de_nota (ambiente producao): ${ligados
              .filter((m) => e.tipos[m].ambiente !== "producao")
              .map((m) => NOME_DO_TIPO[m])
              .join(", ")}.`,
    });
    if (valendo.length && e.saldoDisponivel !== null && e.saldoDisponivel <= 0) {
      passos.push({
        passo: "Pacote de notas do mês",
        feito: false,
        quem_faz: "pessoa_no_painel",
        onde_no_painel: NO_PAINEL.pacotes.onde,
        caminho_no_painel: NO_PAINEL.pacotes.caminho,
        o_que_fazer: "As notas do pacote do mês acabaram: a pessoa compra um pacote no painel.",
      });
    }
  }

  const pronta = Boolean(e.emissaoDisponivel && !e.suspensa && empresaOk && certOk && tribOk && ligados.length);
  return { passos: passos.map((p, i) => ({ ordem: i + 1, ...p })), perguntas, pronta };
}

/* ================================================================== */
/* Tributação: conversa → tela                                         */
/* ================================================================== */

type CampoDeRegra =
  | { chave: string; tipo: "inteiro" | "numero"; descricao: string; min?: number; max?: number }
  | { chave: string; tipo: "texto"; descricao: string; max: number }
  | { chave: string; tipo: "opcao"; descricao: string; valores: Record<string, string> }
  | { chave: string; tipo: "uf"; descricao: string };

export const TIPOS_DE_ISS: Record<string, string> = {
  tributado_no_municipio: "taxationInMunicipality",
  tributado_fora_do_municipio: "taxationOutsideMunicipality",
  isento: "exemption",
  imune: "immune",
  nao_incidencia: "nonIncidence",
  exportacao: "exportation",
  suspenso_por_decisao_judicial: "suspendedByCourt",
  suspenso_por_processo_administrativo: "suspendedByAdministrativeProcedure",
};

export const LOCAIS_DE_INCIDENCIA: Record<string, string> = {
  municipio_da_empresa: "companyMunicipality",
  municipio_do_cliente: "customerMunicipality",
  local_da_prestacao: "serviceProvisionMunicipality",
};

export const REGRAS_DE_MERCADORIA: Record<string, CampoDeRegra> = {
  natureza_da_operacao: { chave: "natureza", tipo: "texto", descricao: "Natureza da operação. Ex.: Venda de mercadoria.", max: 60 },
  cfop_dentro_do_estado: { chave: "cfopInterno", tipo: "inteiro", descricao: "CFOP para cliente do mesmo estado (4 números, ex.: 5101 produção própria, 5102 revenda).", min: 1000, max: 7999 },
  cfop_outro_estado: { chave: "cfopInterestadual", tipo: "inteiro", descricao: "CFOP para outro estado (ex.: 6101, 6102).", min: 1000, max: 7999 },
  cfop_outro_estado_consumidor_final: { chave: "cfopInterestadualConsumidor", tipo: "inteiro", descricao: "CFOP para consumidor final não contribuinte de outro estado (ex.: 6107, 6108).", min: 1000, max: 7999 },
  origem: { chave: "origem", tipo: "inteiro", descricao: "Origem da mercadoria (0 nacional ... 8).", min: 0, max: 8 },
  csosn: { chave: "csosn", tipo: "inteiro", descricao: "CSOSN (Simples e MEI). Ex.: 102 sem crédito, 101 com crédito.", min: 100, max: 999 },
  cst_icms: { chave: "cstIcms", tipo: "inteiro", descricao: "CST do ICMS (regime normal). Ex.: 0 tributada integralmente.", min: 0, max: 99 },
  aliquota_icms: { chave: "aliquotaIcms", tipo: "numero", descricao: "Alíquota do ICMS em % (regime normal).", min: 0, max: 100 },
  reducao_base_icms: { chave: "reducaoBaseIcms", tipo: "numero", descricao: "Redução da base do ICMS em % (CST 20).", min: 0, max: 100 },
  aliquota_credito_simples: { chave: "aliquotaCreditoSimples", tipo: "numero", descricao: "Alíquota do crédito de ICMS do Simples em % (CSOSN 101).", min: 0, max: 100 },
  cst_pis: { chave: "pisCst", tipo: "inteiro", descricao: "CST do PIS (ex.: 49 no Simples, 1 no regime normal).", min: 1, max: 99 },
  aliquota_pis: { chave: "pisAliquota", tipo: "numero", descricao: "Alíquota do PIS em % (ex.: 0,65 no Lucro Presumido).", min: 0, max: 100 },
  cst_cofins: { chave: "cofinsCst", tipo: "inteiro", descricao: "CST do COFINS.", min: 1, max: 99 },
  aliquota_cofins: { chave: "cofinsAliquota", tipo: "numero", descricao: "Alíquota do COFINS em % (ex.: 3 no Lucro Presumido).", min: 0, max: 100 },
  cst_ipi: { chave: "ipiCst", tipo: "inteiro", descricao: "CST do IPI (50 a 99). Vazio = sem IPI na nota.", min: 50, max: 99 },
  aliquota_ipi: { chave: "ipiAliquota", tipo: "numero", descricao: "Alíquota do IPI em %.", min: 0, max: 100 },
  enquadramento_ipi: { chave: "ipiEnquadramento", tipo: "texto", descricao: "Código de enquadramento do IPI (3 números).", max: 3 },
  cst_ibs_cbs: { chave: "ibsCbsCst", tipo: "inteiro", descricao: "CST do IBS/CBS (reforma tributária).", min: 0, max: 999 },
  classificacao_ibs_cbs: { chave: "ibsCbsClassificacao", tipo: "inteiro", descricao: "Classificação tributária do IBS/CBS (a partir de 1).", min: 1, max: 999999 },
  informacoes_adicionais: { chave: "informacoesAdicionais", tipo: "texto", descricao: "Texto fixo das informações complementares.", max: 2000 },
  aliquota_interna_por_uf: { chave: "aliquotaInternaPorUf", tipo: "uf", descricao: "DIFAL: alíquota interna do ICMS do estado de destino, por UF." },
  fcp_por_uf: { chave: "fcpPorUf", tipo: "uf", descricao: "DIFAL: adicional do Fundo de Combate à Pobreza do estado de destino, por UF." },
};

export const REGRAS_DE_SERVICO: Record<string, CampoDeRegra> = {
  codigo_do_servico_lc116: { chave: "codigoLc116", tipo: "texto", descricao: "Item da lista de serviços (LC 116). Ex.: 13.05 gráfico sob encomenda, 23.01 comunicação visual.", max: 10 },
  codigo_do_servico_no_municipio: { chave: "codigoMunicipal", tipo: "texto", descricao: "Código do serviço na prefeitura (quando ela exige).", max: 30 },
  cnae: { chave: "cnae", tipo: "texto", descricao: "CNAE do serviço (7 números), quando a prefeitura exige.", max: 12 },
  nbs: { chave: "nbs", tipo: "texto", descricao: "Código NBS do serviço.", max: 20 },
  codigo_tributacao_nacional: { chave: "codigoTributacaoNacional", tipo: "texto", descricao: "Código de tributação nacional (padrão nacional).", max: 20 },
  aliquota_iss: { chave: "aliquotaIss", tipo: "numero", descricao: "Alíquota do ISS em %.", min: 0, max: 100 },
  iss_retido: { chave: "issRetido", tipo: "opcao", descricao: "Quando o ISS fica retido pelo cliente.", valores: { nunca: "nunca", sempre: "sempre", cliente_pj: "cliente_pj" } },
  tipo_de_tributacao_iss: { chave: "tipoTributacao", tipo: "opcao", descricao: "Como o ISS é tributado.", valores: TIPOS_DE_ISS },
  local_de_incidencia: { chave: "localIncidencia", tipo: "opcao", descricao: "Onde o ISS é devido.", valores: LOCAIS_DE_INCIDENCIA },
  cst_pis_cofins: { chave: "cstPisCofins", tipo: "texto", descricao: "CST de PIS/COFINS do serviço.", max: 3 },
  anexo_do_simples: { chave: "anexoSimples", tipo: "opcao", descricao: "Anexo do Simples Nacional (quando a prefeitura exige).", valores: { I: "I", II: "II", III: "III", IV: "IV", V: "V" } },
  cst_ibs_cbs: { chave: "ibsCbsCst", tipo: "inteiro", descricao: "CST do IBS/CBS (reforma tributária).", min: 0, max: 999 },
  classificacao_ibs_cbs: { chave: "ibsCbsClassificacao", tipo: "inteiro", descricao: "Classificação tributária do IBS/CBS (a partir de 1).", min: 1, max: 999999 },
  indicador_de_operacao_ibs_cbs: { chave: "ibsCbsIndicadorOperacao", tipo: "texto", descricao: "Onde o IBS/CBS é devido (cIndOp, 6 números). Vazio: o emissor decide.", max: 6 },
  informacoes_adicionais: { chave: "informacoesAdicionais", tipo: "texto", descricao: "Texto fixo das informações complementares.", max: 2000 },
};

/**
 * Aplica os campos da conversa nas regras (de mercadoria ou de serviço). O
 * resultado passa depois pelo mesmo leitor da tela (lerRegrasDe...), que é
 * quem tem a palavra final sobre o formato.
 */
export function aplicarRegras(
  tipo: "mercadoria" | "servico",
  base: Record<string, unknown>,
  args: Record<string, unknown>
): { regras: Record<string, unknown>; erros: string[]; mudou: string[] } {
  const mapa = tipo === "mercadoria" ? REGRAS_DE_MERCADORIA : REGRAS_DE_SERVICO;
  const outro = tipo === "mercadoria" ? REGRAS_DE_SERVICO : REGRAS_DE_MERCADORIA;
  const regras: Record<string, unknown> = { ...base };
  const erros: string[] = [];
  const mudou: string[] = [];

  for (const [nome, valor] of Object.entries(args)) {
    if (valor === undefined) continue;
    const campo = mapa[nome];
    if (!campo) {
      if (outro[nome]) erros.push(`"${nome}" é da tributação de ${tipo === "mercadoria" ? "serviço" : "produto"}, não desta.`);
      continue;
    }
    if (campo.tipo === "inteiro" || campo.tipo === "numero") {
      const n = Number(String(valor).replace(",", "."));
      if (!Number.isFinite(n)) {
        erros.push(`"${nome}" precisa ser um número.`);
        continue;
      }
      regras[campo.chave] = campo.tipo === "inteiro" ? Math.trunc(n) : n;
    } else if (campo.tipo === "texto") {
      regras[campo.chave] = textoLimpo(valor).slice(0, campo.max) || null;
    } else if (campo.tipo === "opcao") {
      const v = campo.valores[String(valor)];
      if (!v) {
        erros.push(`"${nome}" aceita: ${Object.keys(campo.valores).join(", ")}.`);
        continue;
      }
      regras[campo.chave] = v;
    } else {
      const lista = Array.isArray(valor) ? (valor as Array<Record<string, unknown>>) : [];
      const porUf: Record<string, number> = { ...((regras[campo.chave] as Record<string, number> | undefined) ?? {}) };
      for (const item of lista) {
        const uf = String(item?.uf ?? "").toUpperCase().trim();
        const aliquota = Number(String(item?.aliquota ?? "").replace(",", "."));
        if (!UFS.includes(uf)) {
          erros.push(`UF inválida em ${nome}: "${item?.uf}".`);
          continue;
        }
        if (!Number.isFinite(aliquota) || aliquota < 0 || aliquota > 100) {
          erros.push(`Alíquota inválida para ${uf} em ${nome}.`);
          continue;
        }
        if (aliquota === 0) delete porUf[uf];
        else porUf[uf] = aliquota;
      }
      regras[campo.chave] = porUf;
    }
    mudou.push(nome);
  }

  const limpar = Array.isArray(args.limpar) ? (args.limpar as unknown[]).map(String) : [];
  for (const nome of limpar) {
    const campo = mapa[nome];
    if (!campo) {
      erros.push(`"${nome}" não é um campo desta tributação.`);
      continue;
    }
    regras[campo.chave] = campo.tipo === "uf" ? {} : null;
    mudou.push(nome);
  }
  return { regras, erros, mudou };
}

/** As regras gravadas, com os mesmos nomes que a conversa usa para alterar. */
export function regrasParaLer(tipo: "mercadoria" | "servico", regras: RegrasDeMercadoria | RegrasDeServico) {
  const mapa = tipo === "mercadoria" ? REGRAS_DE_MERCADORIA : REGRAS_DE_SERVICO;
  const r = regras as unknown as Record<string, unknown>;
  const saida: Record<string, unknown> = {};
  for (const [nome, campo] of Object.entries(mapa)) {
    const valor = r[campo.chave];
    if (campo.tipo === "opcao") {
      saida[nome] = Object.entries(campo.valores).find(([, v]) => v === valor)?.[0] ?? valor ?? null;
    } else if (campo.tipo === "uf") {
      const porUf = (valor ?? {}) as Record<string, number>;
      saida[nome] = Object.entries(porUf).map(([uf, aliquota]) => ({ uf, aliquota }));
    } else {
      saida[nome] = valor ?? null;
    }
  }
  return saida;
}
