import type { AmbienteFiscal } from "@/lib/fiscal/ambiente";
import { cifrar, cifrarObjeto } from "@/lib/fiscal/cofre";
import {
  blocosDeConfiguracao,
  empresaParaSpedy,
  lerCertificado,
  lerCnaes,
  lerEndereco,
  lerNfce,
  lerNfe,
  lerNfse,
  lerPreferencias,
  nfseNoPadraoNacional,
  passosDaConfiguracao,
  problemaNaIdentificacaoDoCsc,
  problemaNaSerieDoNacional,
  problemaNoCsc,
  type AjustesDeNumeracao,
  type CidadeDaNfse,
  type ConfigNfce,
  type ConfigNfe,
  type ConfigNfse,
  type EmissorGuardado,
  type PreferenciasFiscais,
  type SegredosDoEmissor,
} from "@/lib/fiscal/emissor";
import { documentoValido, problemaNaInscricaoEstadual, soDigitos, ufValida } from "@/lib/fiscal/montar";
import { REGIMES, type Regime } from "@/lib/fiscal/regras";
import { lerExemploDaCidade, type ExemploDaCidade } from "@/lib/fiscal/formato-da-cidade";
import {
  alterarConfiguracoes,
  alterarEmpresa,
  criarEmpresa,
  enviarCertificado,
  exemploDaCidade,
  listarCertificados,
  listarTodasAsEmpresas,
  obterConfiguracoes,
  obterEmpresa,
  procurarCidade,
  type SpedyConfiguracoes,
  type SpedyEmpresa,
  type SpedyEmpresaEditavel,
} from "@/lib/spedy/api";
import { ErroDaSpedy } from "@/lib/spedy/cliente";

import { notaParaTela, type NotaNaTela } from "./apresentacao";
import type { NotaGuardada } from "./notas";

import {
  carregarEmissor,
  regrasDaPlataforma,
  segredosDoEmissor,
  spedyDaGestao,
  spedyDaTitular,
  type Admin,
} from "./base";
import { ErroFiscal } from "./notas";

const texto = (valor: unknown) => (typeof valor === "string" ? valor.trim() : "");

/** O emissor sem nada que não deva sair do servidor. */
export function emissorParaTela(emissor: EmissorGuardado | null) {
  if (!emissor) return null;
  const segredos = segredosDoEmissor(emissor);
  return {
    id: emissor.id,
    ambiente: emissor.ambiente,
    status: emissor.status,
    cadastradoNaSpedy: Boolean(emissor.spedy_empresa_id && emissor.chave_cifrada),
    cnpj: emissor.cnpj,
    razaoSocial: emissor.razao_social,
    nomeFantasia: emissor.nome_fantasia,
    inscricaoEstadual: emissor.inscricao_estadual,
    inscricaoMunicipal: emissor.inscricao_municipal,
    regime: emissor.regime,
    regimeEspecial: emissor.regime_especial,
    regimeSimplesNfse: emissor.regime_simples_nfse,
    cnaes: lerCnaes(emissor.cnaes),
    email: emissor.email,
    telefone: emissor.telefone,
    endereco: lerEndereco(emissor.endereco),
    certificado: lerCertificado(emissor.certificado),
    nfe: lerNfe(emissor.nfe),
    nfce: {
      ...lerNfce(emissor.nfce),
      cscHomologacaoGuardado: Boolean(segredos.cscHomologacao),
      cscProducaoGuardado: Boolean(segredos.cscProducao),
      /* O CSC não volta para a tela; só o aviso de que o guardado está fora do tamanho. */
      problemaNoCscHomologacao: problemaNoCsc(segredos.cscHomologacao, "CSC de teste guardado"),
      problemaNoCscProducao: problemaNoCsc(segredos.cscProducao, "CSC de uso real guardado"),
    },
    nfse: {
      ...lerNfse(emissor.nfse),
      senhaGuardada: Boolean(segredos.nfseSenha),
      numeroAutenticacaoGuardado: Boolean(segredos.nfseNumeroAutenticacao),
    },
    preferencias: lerPreferencias(emissor.preferencias),
    ultimoErro: emissor.ultimo_erro,
    sincronizadoEm: emissor.sincronizado_em,
  };
}

export type EmissorNaTela = NonNullable<ReturnType<typeof emissorParaTela>>;

/* ================================================================== */
/* Painel da configuração                                              */
/* ================================================================== */

export async function painelDaConfiguracao(admin: Admin, empresaId: string, ambiente: AmbienteFiscal) {
  const [emissor, perfisR, semNcmR, testesR, perfilDaLojaR] = await Promise.all([
    carregarEmissor(admin, empresaId, ambiente),
    admin.from("fiscal_perfis").select("id,nome,tipo,padrao,ativo,regras,updated_at").eq("empresa_id", empresaId).order("created_at"),
    admin.from("products").select("id,ncm,fiscal_tipo").eq("user_id", empresaId).neq("product_kind", "digital").eq("active", true).limit(5000),
    admin
      .from("fiscal_notas")
      .select("id", { count: "exact", head: true })
      .eq("empresa_id", empresaId)
      .eq("ambiente", ambiente)
      .in("status", ["autorizada", "cancelada"]),
    admin
      .from("profiles")
      .select(
        "store_name,cnpj,company_email,company_phone,phone,address_zip,address_street,address_number,address_complement,address_neighborhood,address_city,address_state"
      )
      .eq("id", empresaId)
      .maybeSingle(),
  ]);

  const perfis = (perfisR.data ?? []) as Array<{ id: string; nome: string; tipo: string; padrao: boolean; ativo: boolean; regras: unknown }>;
  const produtosSemNcm = ((semNcmR.data ?? []) as Array<{ ncm: string | null; fiscal_tipo: string | null }>).filter(
    (produto) => produto.fiscal_tipo !== "servico" && soDigitos(produto.ncm).length !== 8
  ).length;
  const ativos = perfis.filter((p) => p.ativo);
  const loja = (perfilDaLojaR.data ?? {}) as Record<string, unknown>;

  /* O resultado do último teste de cada tipo de nota. Sem isto, quem recarrega
     a página perde o motivo da recusa e fica sem saber o que corrigir. */
  const { data: ultimosR } = await admin
    .from("fiscal_notas")
    .select("*")
    .eq("empresa_id", empresaId)
    .eq("ambiente", ambiente)
    .eq("origem", "teste")
    .order("created_at", { ascending: false })
    .limit(12);
  const ultimosTestes: Record<string, NotaNaTela> = {};
  for (const nota of (ultimosR ?? []) as NotaGuardada[]) {
    if (ultimosTestes[nota.modelo]) continue;
    ultimosTestes[nota.modelo] = notaParaTela(nota);
  }

  return {
    emissor: emissorParaTela(emissor),
    ultimosTestes,
    passos: passosDaConfiguracao({
      emissor,
      perfisDeMercadoria: ativos.filter((p) => p.tipo === "mercadoria").length,
      perfisDeServico: ativos.filter((p) => p.tipo === "servico").length,
      produtosSemNcm,
      notasDeTesteAutorizadas: testesR.count ?? 0,
    }),
    perfis,
    produtosSemNcm,
    sugestaoDaLoja: {
      razaoSocial: texto(loja.store_name),
      cnpj: soDigitos(loja.cnpj),
      email: texto(loja.company_email),
      telefone: soDigitos(loja.company_phone || loja.phone),
      endereco: {
        cep: soDigitos(loja.address_zip),
        logradouro: texto(loja.address_street),
        numero: texto(loja.address_number),
        complemento: texto(loja.address_complement),
        bairro: texto(loja.address_neighborhood),
        cidade: texto(loja.address_city),
        uf: texto(loja.address_state).toUpperCase(),
      },
    },
  };
}

/* ================================================================== */
/* Empresa                                                             */
/* ================================================================== */

export type DadosDaEmpresa = {
  cnpj: string;
  razaoSocial: string;
  nomeFantasia: string | null;
  inscricaoEstadual: string | null;
  inscricaoMunicipal: string | null;
  regime: Regime;
  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;
  };
};

const REGIMES_ESPECIAIS = [
  "municipalMicroenterprise",
  "estimate",
  "societyOfProfessionals",
  "cooperative",
  "individualMicroenterprise",
  "microenterpriseAndSmallBusiness",
  "noSpecialRegime",
  "others",
];
const REGIMES_SIMPLES_NFSE = ["federalAndMunicipalBySimplesNacional", "federalBySimplesAndIssqnByNfse", "federalAndMunicipalByNfse"];

export function lerDadosDaEmpresa(bruto: unknown): DadosDaEmpresa {
  const r = bruto && typeof bruto === "object" ? (bruto as Record<string, unknown>) : {};
  const e = r.endereco && typeof r.endereco === "object" ? (r.endereco as Record<string, unknown>) : {};
  const dados: DadosDaEmpresa = {
    cnpj: soDigitos(r.cnpj),
    razaoSocial: texto(r.razaoSocial),
    nomeFantasia: texto(r.nomeFantasia) || null,
    inscricaoEstadual: texto(r.inscricaoEstadual) ? (texto(r.inscricaoEstadual).toUpperCase() === "ISENTO" ? "ISENTO" : soDigitos(r.inscricaoEstadual)) : null,
    inscricaoMunicipal: texto(r.inscricaoMunicipal) || null,
    regime: texto(r.regime) as Regime,
    regimeEspecial: REGIMES_ESPECIAIS.includes(texto(r.regimeEspecial)) ? texto(r.regimeEspecial) : null,
    regimeSimplesNfse: REGIMES_SIMPLES_NFSE.includes(texto(r.regimeSimplesNfse)) ? texto(r.regimeSimplesNfse) : null,
    cnaes: lerCnaes(r.cnaes),
    email: texto(r.email) || null,
    telefone: soDigitos(r.telefone) || null,
    endereco: {
      cep: soDigitos(e.cep),
      logradouro: texto(e.logradouro),
      numero: texto(e.numero) || "S/N",
      complemento: texto(e.complemento) || null,
      bairro: texto(e.bairro),
      cidade: texto(e.cidade),
      uf: texto(e.uf).toUpperCase(),
      ibge: soDigitos(e.ibge),
    },
  };
  const faltas: string[] = [];
  if (documentoValido(dados.cnpj) !== "cnpj") faltas.push("CNPJ válido");
  if (dados.razaoSocial.length < 2) faltas.push("razão social");
  if (!REGIMES.some((regime) => regime.valor === dados.regime)) faltas.push("regime tributário");
  if (dados.endereco.cep.length !== 8) faltas.push("CEP");
  if (!dados.endereco.logradouro) faltas.push("rua");
  if (!dados.endereco.bairro) faltas.push("bairro");
  if (!dados.endereco.cidade) faltas.push("cidade");
  if (!ufValida(dados.endereco.uf)) faltas.push("estado");
  if (dados.endereco.ibge.length !== 7) faltas.push("cidade (confira o CEP)");
  if (dados.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(dados.email)) faltas.push("e-mail válido");
  if (faltas.length) throw new ErroFiscal(`Confira os dados da empresa: ${faltas.join(", ")}.`, 400);
  const problemaDaIe = dados.inscricaoEstadual === "ISENTO" ? null : problemaNaInscricaoEstadual(dados.inscricaoEstadual, dados.endereco.uf);
  if (problemaDaIe) throw new ErroFiscal(problemaDaIe, 400);
  return dados;
}

export async function salvarEmpresa(
  admin: Admin,
  input: { empresaId: string; ambiente: AmbienteFiscal; dados: DadosDaEmpresa }
) {
  const { dados } = input;
  const existente = await carregarEmissor(admin, input.empresaId, input.ambiente);

  if (existente?.spedy_empresa_id && soDigitos(existente.cnpj) !== dados.cnpj) {
    throw new ErroFiscal("O CNPJ não pode ser trocado depois que a empresa começou a emitir notas. Fale com o suporte do Ktalogu.", 409);
  }

  const linha = {
    empresa_id: input.empresaId,
    ambiente: input.ambiente,
    cnpj: dados.cnpj,
    razao_social: dados.razaoSocial,
    nome_fantasia: dados.nomeFantasia,
    inscricao_estadual: dados.inscricaoEstadual,
    inscricao_municipal: dados.inscricaoMunicipal,
    regime: dados.regime,
    regime_especial: dados.regimeEspecial,
    regime_simples_nfse: dados.regimeSimplesNfse,
    cnaes: dados.cnaes,
    email: dados.email,
    telefone: dados.telefone,
    endereco: dados.endereco,
    updated_at: new Date().toISOString(),
  };

  let emissor: EmissorGuardado;
  if (existente) {
    const { data, error } = await admin.from("fiscal_emissores").update(linha).eq("id", existente.id).select("*").single();
    if (error) throw new ErroFiscal(`Falha ao salvar a empresa: ${error.message}`, 500);
    emissor = data as EmissorGuardado;
  } else {
    const regras = await regrasDaPlataforma(admin);
    const { count } = await admin
      .from("fiscal_emissores")
      .select("id", { count: "exact", head: true })
      .eq("ambiente", input.ambiente)
      .not("spedy_empresa_id", "is", null);
    if ((count ?? 0) >= regras.limite_emissores) {
      throw new ErroFiscal("A emissão de notas está com as vagas esgotadas no momento. Fale com o suporte do Ktalogu.", 409);
    }
    const { data, error } = await admin.from("fiscal_emissores").insert(linha).select("*").single();
    if (error) throw new ErroFiscal(`Falha ao salvar a empresa: ${error.message}`, 500);
    emissor = data as EmissorGuardado;
  }

  return sincronizarEmpresa(admin, emissor);
}

async function sincronizarEmpresa(admin: Admin, emissor: EmissorGuardado): Promise<EmissorGuardado> {
  const titular = spedyDaTitular(emissor.ambiente);
  const dados = empresaParaSpedy(emissor);
  try {
    if (!emissor.spedy_empresa_id || !emissor.chave_cifrada) {
      let criada: SpedyEmpresa;
      try {
        const resposta = await criarEmpresa(titular, dados);
        /* A documentação mostra a criação dentro de `result`; as consultas chegam sem. Vale os dois. */
        criada = ((resposta as { result?: SpedyEmpresa } | null)?.result ?? resposta) as SpedyEmpresa;
      } catch (erro) {
        if (erro instanceof ErroDaSpedy && erro.status === 400) {
          const vinculada = await vincularEmpresaExistente(admin, emissor, titular, dados);
          if (vinculada) return vinculada;
        }
        throw erro;
      }
      const chave = criada?.apiCredentials?.apiKey;
      if (!criada?.id || !chave) throw new ErroFiscal("O cadastro da empresa não foi concluído. Fale com o suporte do Ktalogu.", 502);
      /* A chave aparece uma vez só: grava antes de qualquer outra coisa. */
      const { data, error } = await admin
        .from("fiscal_emissores")
        .update({
          spedy_empresa_id: criada.id,
          chave_cifrada: cifrar(chave),
          ultimo_erro: null,
          sincronizado_em: new Date().toISOString(),
        })
        .eq("id", emissor.id)
        .select("*")
        .single();
      if (error) {
        console.error("[fiscal] empresa criada na Spedy mas a chave não foi gravada", emissor.id, criada.id, error.message);
        throw new ErroFiscal("O cadastro da empresa não foi concluído. Fale com o suporte do Ktalogu.", 500);
      }
      return data as EmissorGuardado;
    }

    await alterarEmpresa(titular, emissor.spedy_empresa_id, dados);
    const { data, error } = await admin
      .from("fiscal_emissores")
      .update({ ultimo_erro: null, sincronizado_em: new Date().toISOString() })
      .eq("id", emissor.id)
      .select("*")
      .single();
    if (error) throw new ErroFiscal(`Falha ao salvar a empresa: ${error.message}`, 500);
    return data as EmissorGuardado;
  } catch (erro) {
    const mensagem = erro instanceof Error ? erro.message : "Falha na comunicação com o serviço de notas.";
    await admin.from("fiscal_emissores").update({ ultimo_erro: mensagem }).eq("id", emissor.id);
    if (erro instanceof ErroFiscal) throw erro;
    throw new ErroFiscal(`Os dados da empresa não foram aceitos: ${mensagem}`, erro instanceof ErroDaSpedy && erro.transitorio ? 503 : 422);
  }
}

/**
 * O CNPJ já existe na conta emissora. A chave de uma empresa só aparece na
 * criação, com uma exceção: a empresa titular, a primeira da conta, cuja chave
 * é a que este servidor usa. A consulta da empresa mostra o começo da chave, e
 * é isso que confirma que é ela. Outra empresa com o mesmo CNPJ fica para o
 * suporte, e a mesma titular nunca é vinculada a duas contas do Ktalogu.
 */
async function vincularEmpresaExistente(
  admin: Admin,
  emissor: EmissorGuardado,
  titular: { chave: string; ambiente: AmbienteFiscal },
  dados: SpedyEmpresaEditavel
): Promise<EmissorGuardado | null> {
  const todas = await listarTodasAsEmpresas(titular).catch(() => [] as SpedyEmpresa[]);
  const mesma = todas.find((empresa) => soDigitos(empresa.federalTaxNumber) === soDigitos(emissor.cnpj));
  if (!mesma?.id) return null;

  const detalhe = await obterEmpresa(titular, mesma.id);
  const inicioDaChave = String(detalhe?.apiCredentials?.apiKey ?? "").split("*")[0].replace(/-+$/, "");
  if (inicioDaChave.length < 4 || !titular.chave.startsWith(inicioDaChave)) {
    throw new ErroFiscal("Este CNPJ já está com a emissão de notas ligada em outro cadastro. Fale com o suporte do Ktalogu para liberar.", 409);
  }

  const { data: outro } = await admin
    .from("fiscal_emissores")
    .select("id")
    .eq("ambiente", emissor.ambiente)
    .eq("spedy_empresa_id", mesma.id)
    .neq("id", emissor.id)
    .limit(1)
    .maybeSingle();
  if (outro) {
    throw new ErroFiscal("Este CNPJ já está com a emissão de notas ligada em outra conta do Ktalogu. Fale com o suporte para liberar.", 409);
  }

  const { data, error } = await admin
    .from("fiscal_emissores")
    .update({
      spedy_empresa_id: mesma.id,
      chave_cifrada: cifrar(titular.chave),
      ultimo_erro: null,
      sincronizado_em: new Date().toISOString(),
    })
    .eq("id", emissor.id)
    .select("*")
    .single();
  if (error) throw new ErroFiscal(`Falha ao salvar a empresa: ${error.message}`, 500);
  await alterarEmpresa(titular, mesma.id, dados);
  return trazerCertificadoJaEnviado(admin, data as EmissorGuardado);
}

/** O certificado que já estava lá (enviado antes, por outro caminho) é aproveitado sem pedir de novo. */
async function trazerCertificadoJaEnviado(admin: Admin, emissor: EmissorGuardado): Promise<EmissorGuardado> {
  if (lerCertificado(emissor.certificado)?.id || !emissor.spedy_empresa_id) return emissor;
  try {
    const lista = await listarCertificados(spedyDaGestao(emissor), emissor.spedy_empresa_id);
    const cnpj = soDigitos(emissor.cnpj);
    const valido = (Array.isArray(lista) ? lista : [])
      .filter((c) => c.isActive !== false && c.expirationAt && new Date(c.expirationAt).getTime() > Date.now())
      .filter((c) => !c.subject || soDigitos(c.subject).includes(cnpj))
      .sort((a, b) => String(b.expirationAt).localeCompare(String(a.expirationAt)))[0];
    if (!valido) return emissor;
    const guardado = {
      id: valido.id,
      validade: valido.expirationAt,
      titular: valido.subject ? valido.subject.split(":")[0] : null,
      emissor: valido.issuer ?? null,
      enviadoEm: new Date().toISOString(),
    };
    const { data, error } = await admin
      .from("fiscal_emissores")
      .update({ certificado: guardado, certificado_validade: valido.expirationAt, updated_at: new Date().toISOString() })
      .eq("id", emissor.id)
      .select("*")
      .single();
    if (error || !data) return emissor;
    return recalcularStatus(admin, data as EmissorGuardado);
  } catch (erro) {
    console.error("[fiscal] não foi possível ler o certificado já enviado", emissor.id, erro instanceof Error ? erro.message : erro);
    return emissor;
  }
}

/* ================================================================== */
/* Certificado A1                                                      */
/* ================================================================== */

export async function enviarCertificadoA1(
  admin: Admin,
  input: { empresaId: string; ambiente: AmbienteFiscal; arquivo: File; senha: string }
) {
  const emissor = await carregarEmissor(admin, input.empresaId, input.ambiente);
  if (!emissor?.spedy_empresa_id || !emissor.chave_cifrada) {
    throw new ErroFiscal("Salve os dados da empresa antes de enviar o certificado.", 409);
  }
  const nome = input.arquivo.name || "certificado.pfx";
  if (!/\.(pfx|p12)$/i.test(nome)) throw new ErroFiscal("O certificado A1 é um arquivo .pfx ou .p12.", 400);
  if (input.arquivo.size <= 0 || input.arquivo.size > 1024 * 1024) throw new ErroFiscal("Arquivo de certificado inválido.", 400);
  if (!input.senha) throw new ErroFiscal("Digite a senha do certificado.", 400);

  try {
    const certificado = await enviarCertificado(spedyDaGestao(emissor), emissor.spedy_empresa_id, input.arquivo, nome, input.senha);
    const guardado = {
      id: certificado.id,
      validade: certificado.expirationAt,
      titular: certificado.subject ?? null,
      emissor: certificado.issuer ?? null,
      enviadoEm: new Date().toISOString(),
    };
    const { data, error } = await admin
      .from("fiscal_emissores")
      .update({ certificado: guardado, certificado_validade: certificado.expirationAt, ultimo_erro: null, updated_at: new Date().toISOString() })
      .eq("id", emissor.id)
      .select("*")
      .single();
    if (error) throw new ErroFiscal(`Falha ao guardar o certificado: ${error.message}`, 500);
    await recalcularStatus(admin, data as EmissorGuardado);
    return lerCertificado(guardado);
  } catch (erro) {
    if (erro instanceof ErroFiscal) throw erro;
    if (erro instanceof ErroDaSpedy && erro.status === 400) {
      throw new ErroFiscal(`Certificado recusado: ${erro.message}. Confira a senha e se o arquivo é o A1 deste CNPJ.`, 422);
    }
    throw new ErroFiscal(erro instanceof Error ? erro.message : "Não foi possível enviar o certificado agora.", 503);
  }
}

/* ================================================================== */
/* Modelos de nota e configurações                                     */
/* ================================================================== */

/* O exemplo muda pouco (é uma nota que passou na cidade): um dia na memória
   basta, e falha lembrada por pouco tempo para não martelar a Spedy. */
const exemplosDasCidades = new Map<string, { quando: number; validade: number; exemplo: ExemploDaCidade | null }>();

/**
 * Os códigos de uma nota de serviço que a prefeitura aceitou: o formato do
 * código do serviço no município, a atividade e o item da lista. A tela usa
 * para mostrar o formato certo e avisar quando o digitado não tem a cara dele.
 * Sem exemplo (ou sem rede), volta null e a tela só não mostra a dica.
 */
export async function exemploDoServicoNaCidade(input: { ambiente: AmbienteFiscal; ibge: string }): Promise<ExemploDaCidade | null> {
  const ibge = soDigitos(input.ibge);
  if (ibge.length !== 7) throw new ErroFiscal("Código IBGE da cidade inválido.", 400);
  const chave = `${input.ambiente}:${ibge}`;
  const lembrado = exemplosDasCidades.get(chave);
  if (lembrado && Date.now() - lembrado.quando < lembrado.validade) return lembrado.exemplo;
  let exemplo: ExemploDaCidade | null = null;
  let validade = 24 * 60 * 60 * 1000;
  try {
    const resposta = await exemploDaCidade(spedyDaTitular(input.ambiente), ibge);
    exemplo = lerExemploDaCidade(resposta?.examples?.[0]?.payload);
  } catch {
    validade = 5 * 60 * 1000;
  }
  exemplosDasCidades.set(chave, { quando: Date.now(), validade, exemplo });
  if (exemplosDasCidades.size > 500) exemplosDasCidades.clear();
  return exemplo;
}

export async function consultarCidadeDaNfse(admin: Admin, input: { empresaId: string; ambiente: AmbienteFiscal; ibge: string }): Promise<CidadeDaNfse | null> {
  const ibge = soDigitos(input.ibge);
  if (ibge.length !== 7) throw new ErroFiscal("Código IBGE da cidade inválido.", 400);
  /* Consulta geral, que qualquer chave válida faz: vai a da titular, que existe
     antes mesmo de a empresa estar cadastrada. */
  const cidade = await procurarCidade(spedyDaTitular(input.ambiente), ibge);
  if (!cidade) return null;
  return {
    codigo: String(cidade.code),
    nome: cidade.name,
    uf: String(cidade.state || "").toUpperCase(),
    provedor: cidade.provider?.name ?? null,
    opcoes: cidade.provider?.options ?? null,
  };
}

export type DadosDosModelos = {
  nfe: ConfigNfe;
  nfce: ConfigNfce;
  nfse: Omit<ConfigNfse, "cidade">;
  segredos: SegredosDoEmissor;
  numeracao: AjustesDeNumeracao;
};

export async function salvarModelos(admin: Admin, input: { empresaId: string; ambiente: AmbienteFiscal; dados: DadosDosModelos }) {
  const emissor = await carregarEmissor(admin, input.empresaId, input.ambiente);
  if (!emissor?.spedy_empresa_id || !emissor.chave_cifrada) {
    throw new ErroFiscal("Salve os dados da empresa antes de escolher as notas.", 409);
  }
  const { nfe, nfce, nfse } = input.dados;

  if (input.ambiente === "sandbox" && (nfe.ambiente === "producao" || nfce.ambiente === "producao" || nfse.ambiente === "producao")) {
    throw new ErroFiscal("Aqui é o ambiente de testes do Ktalogu: as notas só saem em modo de teste.", 400);
  }
  if ((nfe.ativo || nfce.ativo) && !soDigitos(emissor.inscricao_estadual)) {
    throw new ErroFiscal("A nota de produto e o cupom fiscal precisam do número da inscrição estadual da empresa. Preencha em Sua empresa.", 400);
  }

  const segredosAtuais = segredosDoEmissor(emissor);
  const novos: SegredosDoEmissor = { ...segredosAtuais };
  for (const chave of ["cscHomologacao", "cscProducao", "nfseSenha", "nfseNumeroAutenticacao"] as const) {
    const bruto = texto(input.dados.segredos[chave]);
    /* CSC não tem espaço: o que vem colado com espaço no meio é sujeira da cópia. */
    const valor = chave.startsWith("csc") ? bruto.replace(/\s+/g, "") : bruto;
    if (!valor) continue;
    if (chave.startsWith("csc")) {
      const problema = problemaNoCsc(valor, chave === "cscProducao" ? "CSC de uso real" : "CSC de teste");
      if (problema) throw new ErroFiscal(problema, 400);
    }
    novos[chave] = valor;
  }

  if (nfce.ativo) {
    const producao = nfce.ambiente === "producao";
    const tokenId = producao ? nfce.tokenIdProducao : nfce.tokenIdHomologacao;
    const csc = producao ? novos.cscProducao : novos.cscHomologacao;
    if (!tokenId || !csc) {
      throw new ErroFiscal(
        `O cupom fiscal precisa do código CSC ${producao ? "de uso real" : "de teste"} e do número de identificação dele. Os dois são gerados no site da SEFAZ do seu estado.`,
        400
      );
    }
    const problema =
      problemaNaIdentificacaoDoCsc(tokenId, `identificação do CSC ${producao ? "de uso real" : "de teste"}`) ??
      problemaNoCsc(csc, `CSC ${producao ? "de uso real" : "de teste"} guardado`);
    if (problema) throw new ErroFiscal(problema, 400);
  }

  let cidade: CidadeDaNfse | null = lerNfse(emissor.nfse).cidade;
  if (nfse.ativo) {
    if (!emissor.inscricao_municipal) throw new ErroFiscal("A NFS-e precisa da inscrição municipal da empresa.", 400);
    const ibge = lerEndereco(emissor.endereco).ibge ?? "";
    cidade = await consultarCidadeDaNfse(admin, { empresaId: input.empresaId, ambiente: input.ambiente, ibge });
    if (!cidade) {
      throw new ErroFiscal("A nota de serviço da sua cidade ainda não pode ser emitida pelo Ktalogu. Desligue a nota de serviço por enquanto.", 422);
    }
    if (nfseNoPadraoNacional({ forma: nfse.forma, cidade })) {
      const problema = problemaNaSerieDoNacional(nfse.serie);
      if (problema) throw new ErroFiscal(problema, 400);
    }
    const autenticacao = cidade.opcoes?.authentication;
    if (autenticacao === "userName" && !nfse.usuario) {
      throw new ErroFiscal("A prefeitura da sua cidade exige o usuário de acesso ao sistema de notas.", 400);
    }
    if ((autenticacao === "userName" || autenticacao === "token") && !novos.nfseSenha) {
      throw new ErroFiscal(
        autenticacao === "token" ? "A prefeitura da sua cidade exige o token de acesso." : "A prefeitura da sua cidade exige a senha de acesso.",
        400
      );
    }
  }

  const { data, error } = await admin
    .from("fiscal_emissores")
    .update({
      nfe,
      nfce,
      nfse: { ...nfse, cidade },
      segredos_cifrados: cifrarObjeto(novos),
      updated_at: new Date().toISOString(),
    })
    .eq("id", emissor.id)
    .select("*")
    .single();
  if (error) throw new ErroFiscal(`Falha ao salvar as notas: ${error.message}`, 500);
  const atualizado = data as EmissorGuardado;

  try {
    const ctx = spedyDaGestao(atualizado);
    const atuais = await obterConfiguracoes(ctx, atualizado.spedy_empresa_id as string);
    const blocos = blocosDeConfiguracao(atualizado, novos, atuais, input.dados.numeracao);
    if (Object.keys(blocos).length) {
      await alterarConfiguracoes(ctx, atualizado.spedy_empresa_id as string, blocos);
    }
    await admin
      .from("fiscal_emissores")
      .update({ ultimo_erro: null, sincronizado_em: new Date().toISOString() })
      .eq("id", atualizado.id);
  } catch (erro) {
    const mensagem = erro instanceof Error ? erro.message : "Falha na comunicação com o serviço de notas.";
    await admin.from("fiscal_emissores").update({ ultimo_erro: mensagem }).eq("id", atualizado.id);
    throw new ErroFiscal(`As configurações não foram aceitas: ${mensagem}`, erro instanceof ErroDaSpedy && erro.transitorio ? 503 : 422);
  }

  return recalcularStatus(admin, atualizado);
}

/**
 * Antes da nota sair: se o emissor está sem numeração para o tipo de nota (empresa
 * que nunca emitiu, configurada antes desta regra ou criada fora do Ktalogu), ela
 * começa do 1; e se os campos da reforma tributária ainda não estão ligados para
 * quem já é obrigado a eles, liga. Com tudo em ordem, não mexe em nada.
 */
export async function garantirNumeracao(emissor: EmissorGuardado, modelo: "nfe" | "nfce" | "nfse") {
  if (!emissor.spedy_empresa_id || !emissor.chave_cifrada) return;
  const chave = modelo === "nfe" ? "productInvoice" : modelo === "nfce" ? "consumerInvoice" : "serviceInvoice";
  const ctx = spedyDaGestao(emissor);
  const atuais = await obterConfiguracoes(ctx, emissor.spedy_empresa_id);
  const atual = (atuais?.[chave] ?? {}) as Record<string, unknown>;
  const blocos = blocosDeConfiguracao(emissor, segredosDoEmissor(emissor), atuais);
  /* No padrão nacional a nota de serviço sem série volta recusada: quem configurou antes de a série ter padrão ganha a dela aqui. */
  const semSerie = modelo === "nfse" && Boolean(blocos.serviceInvoice?.series) && !String(atual.series ?? "").trim();
  const falta = !(Number(atual.nextNumber) > 0) || (modelo === "nfse" && !(Number(atual.nextBatchNumber) > 0)) || semSerie;
  const envio: SpedyConfiguracoes = {};
  if (falta && blocos[chave]) envio[chave] = blocos[chave];
  /* O bloco geral só volta de blocosDeConfiguracao quando falta ligar os campos da reforma. */
  if (blocos.general) envio.general = blocos.general;
  if (!Object.keys(envio).length) return;
  await alterarConfiguracoes(ctx, emissor.spedy_empresa_id, envio);
}

export async function salvarPreferencias(admin: Admin, input: { empresaId: string; ambiente: AmbienteFiscal; preferencias: PreferenciasFiscais }) {
  const emissor = await carregarEmissor(admin, input.empresaId, input.ambiente);
  if (!emissor) throw new ErroFiscal("Configure a empresa primeiro.", 409);
  const preferencias = lerPreferencias(input.preferencias);
  const { error } = await admin
    .from("fiscal_emissores")
    .update({ preferencias, updated_at: new Date().toISOString() })
    .eq("id", emissor.id);
  if (error) throw new ErroFiscal(`Falha ao salvar: ${error.message}`, 500);
  return preferencias;
}

/**
 * "ativo" quando dá para emitir: empresa na Spedy, certificado válido, um modelo
 * ligado e a tributação de cada tipo que esse modelo usa. "suspenso" é decisão
 * do owner e não muda sozinho.
 */
export async function recalcularStatus(admin: Admin, emissor: EmissorGuardado): Promise<EmissorGuardado> {
  if (emissor.status === "suspenso") return emissor;
  const { data: perfis } = await admin.from("fiscal_perfis").select("tipo,ativo").eq("empresa_id", emissor.empresa_id);
  const lista = (perfis ?? []) as Array<{ tipo: string; ativo: boolean }>;
  const passos = passosDaConfiguracao({
    emissor,
    perfisDeMercadoria: lista.filter((p) => p.ativo && p.tipo === "mercadoria").length,
    perfisDeServico: lista.filter((p) => p.ativo && p.tipo === "servico").length,
    produtosSemNcm: 0,
    notasDeTesteAutorizadas: 0,
  });
  const pronto = ["empresa", "certificado", "modelos", "tributacao"].every((chave) => passos.find((p) => p.chave === chave)?.feito);
  const status = pronto ? "ativo" : "rascunho";
  if (status === emissor.status) return emissor;
  const { data } = await admin.from("fiscal_emissores").update({ status }).eq("id", emissor.id).select("*").single();
  return (data as EmissorGuardado) ?? emissor;
}

/* ================================================================== */
/* Perfis de tributação                                                */
/* ================================================================== */

export async function salvarPerfil(
  admin: Admin,
  input: { empresaId: string; id?: string | null; nome: string; tipo: "mercadoria" | "servico"; padrao: boolean; regras: Record<string, unknown> }
) {
  const nome = texto(input.nome).slice(0, 80);
  if (nome.length < 2) throw new ErroFiscal("Dê um nome para esta tributação.", 400);
  const tipo = input.tipo === "servico" ? "servico" : "mercadoria";

  const { count } = await admin
    .from("fiscal_perfis")
    .select("id", { count: "exact", head: true })
    .eq("empresa_id", input.empresaId)
    .eq("tipo", tipo)
    .eq("ativo", true);
  const primeiroDoTipo = (count ?? 0) === 0 || ((count ?? 0) === 1 && Boolean(input.id));
  const padrao = input.padrao || primeiroDoTipo;

  if (padrao) {
    let limpar = admin.from("fiscal_perfis").update({ padrao: false }).eq("empresa_id", input.empresaId).eq("tipo", tipo);
    if (input.id) limpar = limpar.neq("id", input.id);
    await limpar;
  }

  const linha = { empresa_id: input.empresaId, nome, tipo, padrao, regras: input.regras, ativo: true, updated_at: new Date().toISOString() };
  if (input.id) {
    const { data, error } = await admin
      .from("fiscal_perfis")
      .update(linha)
      .eq("id", input.id)
      .eq("empresa_id", input.empresaId)
      .select("*")
      .maybeSingle();
    if (error) throw new ErroFiscal(`Falha ao salvar: ${error.message}`, 500);
    if (!data) throw new ErroFiscal("Tributação não encontrada.", 404);
    return data;
  }
  const { data, error } = await admin.from("fiscal_perfis").insert(linha).select("*").single();
  if (error) throw new ErroFiscal(`Falha ao salvar: ${error.message}`, 500);
  return data;
}

export async function excluirPerfil(admin: Admin, input: { empresaId: string; id: string }) {
  const { data: perfil } = await admin
    .from("fiscal_perfis")
    .select("id,tipo,padrao")
    .eq("id", input.id)
    .eq("empresa_id", input.empresaId)
    .maybeSingle();
  if (!perfil) throw new ErroFiscal("Tributação não encontrada.", 404);
  const { error } = await admin.from("fiscal_perfis").delete().eq("id", input.id).eq("empresa_id", input.empresaId);
  if (error) throw new ErroFiscal(`Falha ao excluir: ${error.message}`, 500);
  if (perfil.padrao) {
    const { data: outro } = await admin
      .from("fiscal_perfis")
      .select("id")
      .eq("empresa_id", input.empresaId)
      .eq("tipo", perfil.tipo)
      .eq("ativo", true)
      .order("created_at")
      .limit(1)
      .maybeSingle();
    if (outro) await admin.from("fiscal_perfis").update({ padrao: true }).eq("id", outro.id);
  }
}
