/**
 * Consultas públicas que ajudam a preencher o cadastro fiscal sem digitar:
 * CNPJ (BrasilAPI), código IBGE da cidade (ViaCEP e IBGE) e NCM (BrasilAPI).
 *
 * Tudo com tempo limite curto e memória de algumas horas: são dados que mudam
 * pouco, e a tela não pode travar porque um serviço de fora está lento.
 */

type Guardado<T> = { valor: T; em: number };

declare global {
  var __consultasFiscais: Map<string, Guardado<unknown>> | undefined;
}

function memoria() {
  globalThis.__consultasFiscais ??= new Map();
  return globalThis.__consultasFiscais;
}

async function lembrar<T>(chave: string, horas: number, buscar: () => Promise<T>): Promise<T> {
  const guardado = memoria().get(chave) as Guardado<T> | undefined;
  if (guardado && Date.now() - guardado.em < horas * 3600 * 1000) return guardado.valor;
  const valor = await buscar();
  if (valor !== null && valor !== undefined) {
    if (memoria().size > 2000) memoria().clear();
    memoria().set(chave, { valor, em: Date.now() });
  }
  return valor;
}

async function buscarJson(url: string, timeoutMs = 8000): Promise<unknown> {
  const controle = new AbortController();
  const relogio = setTimeout(() => controle.abort(), timeoutMs);
  try {
    /* Com identificação: serviço público costuma recusar pedido anônimo vindo de servidor. */
    const resposta = await fetch(url, {
      signal: controle.signal,
      headers: { Accept: "application/json", "User-Agent": "Ktalogu/1.0 (+https://ktalogu.com.br)" },
      cache: "no-store",
    });
    if (!resposta.ok) return null;
    return await resposta.json();
  } catch {
    return null;
  } finally {
    clearTimeout(relogio);
  }
}

const texto = (valor: unknown) => (typeof valor === "string" ? valor.trim() : typeof valor === "number" ? String(valor) : "");
const soDigitos = (valor: unknown) => texto(valor).replace(/\D/g, "");

export function semAcento(valor: string) {
  return valor
    .normalize("NFD")
    .replace(/\p{Diacritic}/gu, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, " ")
    .trim();
}

function titulo(valor: string) {
  const minusculas = ["de", "da", "do", "das", "dos", "e"];
  return valor
    .toLowerCase()
    .split(/\s+/)
    .filter(Boolean)
    .map((palavra, i) => (i > 0 && minusculas.includes(palavra) ? palavra : palavra.charAt(0).toUpperCase() + palavra.slice(1)))
    .join(" ");
}

/* ---------------- Cidades (IBGE) ---------------- */

export async function cidadesDoEstado(uf: string): Promise<Array<{ codigo: string; nome: string }>> {
  const sigla = texto(uf).toUpperCase();
  if (!/^[A-Z]{2}$/.test(sigla)) return [];
  return lembrar(`ibge:${sigla}`, 72, async () => {
    const dados = await buscarJson(`https://servicodados.ibge.gov.br/api/v1/localidades/estados/${sigla}/municipios`, 10000);
    if (!Array.isArray(dados)) return null as unknown as Array<{ codigo: string; nome: string }>;
    return dados.map((cidade) => ({ codigo: String((cidade as { id: number }).id), nome: String((cidade as { nome: string }).nome) }));
  }).then((lista) => lista ?? []);
}

export async function codigoIbge(input: { cep?: string | null; cidade?: string | null; uf?: string | null }) {
  const cep = soDigitos(input.cep);
  if (cep.length === 8) {
    const viaCep = await lembrar(`viacep:${cep}`, 72, async () => {
      const dados = (await buscarJson(`https://viacep.com.br/ws/${cep}/json/`)) as Record<string, unknown> | null;
      if (!dados || dados.erro) return null as unknown as { ibge: string; cidade: string; uf: string };
      return { ibge: soDigitos(dados.ibge), cidade: texto(dados.localidade), uf: texto(dados.uf).toUpperCase() };
    });
    if (viaCep?.ibge?.length === 7) return viaCep;
  }
  const uf = texto(input.uf).toUpperCase();
  const cidade = semAcento(texto(input.cidade));
  if (!uf || !cidade) return null;
  const cidades = await cidadesDoEstado(uf);
  const achada = cidades.find((c) => semAcento(c.nome) === cidade);
  return achada ? { ibge: achada.codigo, cidade: achada.nome, uf } : null;
}

/* ---------------- CNPJ ---------------- */

export type EmpresaConsultada = {
  cnpj: string;
  razaoSocial: string;
  nomeFantasia: string;
  email: string;
  telefone: string;
  simples: boolean | null;
  mei: boolean | null;
  inscricaoEstadual: string | null;
  cnaes: Array<{ codigo: string; descricao: string; principal: boolean }>;
  endereco: { cep: string; logradouro: string; numero: string; complemento: string; bairro: string; cidade: string; uf: string; ibge: string };
};

/**
 * A inscrição estadual ativa no estado da empresa. A Receita não publica esse
 * número; a consulta pública do CNPJ.ws junta os cadastros estaduais. É só uma
 * ajuda para preencher: sem resposta, a pessoa digita.
 */
async function inscricaoEstadualDoCnpj(cnpj: string, uf: string): Promise<string | null> {
  const dados = (await buscarJson(`https://publica.cnpj.ws/cnpj/${cnpj}`, 6000)) as Record<string, unknown> | null;
  const estabelecimento = dados && typeof dados.estabelecimento === "object" ? (dados.estabelecimento as Record<string, unknown>) : null;
  const lista = Array.isArray(estabelecimento?.inscricoes_estaduais) ? (estabelecimento?.inscricoes_estaduais as Array<Record<string, unknown>>) : [];
  const siglaDe = (item: Record<string, unknown>) =>
    texto(item.estado && typeof item.estado === "object" ? (item.estado as Record<string, unknown>).sigla : "").toUpperCase();
  const ativa = lista.find((item) => item.ativo === true && siglaDe(item) === uf) ?? null;
  const numero = soDigitos(ativa?.inscricao_estadual);
  return numero || null;
}

/** A resposta do CNPJ.ws no formato da BrasilAPI, para o resto da leitura ser um só. */
function doCnpjWs(bruto: unknown): Record<string, unknown> | null {
  const dados = bruto && typeof bruto === "object" ? (bruto as Record<string, unknown>) : null;
  const e = dados && typeof dados.estabelecimento === "object" && dados.estabelecimento ? (dados.estabelecimento as Record<string, unknown>) : null;
  if (!dados || !e) return null;
  const objeto = (valor: unknown) => (valor && typeof valor === "object" ? (valor as Record<string, unknown>) : {});
  const simples = dados.simples && typeof dados.simples === "object" ? (dados.simples as Record<string, unknown>) : null;
  const secundarias = Array.isArray(e.atividades_secundarias) ? (e.atividades_secundarias as Array<Record<string, unknown>>) : [];
  return {
    razao_social: dados.razao_social,
    nome_fantasia: e.nome_fantasia,
    descricao_tipo_de_logradouro: e.tipo_logradouro,
    logradouro: e.logradouro,
    numero: e.numero,
    complemento: e.complemento,
    bairro: e.bairro,
    cep: e.cep,
    uf: objeto(e.estado).sigla,
    municipio: objeto(e.cidade).nome,
    codigo_municipio_ibge: objeto(e.cidade).ibge_id,
    email: e.email,
    ddd_telefone_1: `${texto(e.ddd1)}${texto(e.telefone1)}`,
    cnae_fiscal: objeto(e.atividade_principal).id,
    cnae_fiscal_descricao: objeto(e.atividade_principal).descricao,
    cnaes_secundarios: secundarias.map((a) => ({ codigo: a.id, descricao: a.descricao })),
    opcao_pelo_simples: simples ? texto(simples.simples).toLowerCase() === "sim" : null,
    opcao_pelo_mei: simples ? texto(simples.mei).toLowerCase() === "sim" : null,
  };
}

/**
 * O cadastro da Receita, por três portas. Em produção a primeira passou a
 * recusar o servidor (serviço público bloqueia faixa de IP de datacenter sem
 * avisar) e a tela dizia "não encontramos este CNPJ" para um CNPJ que existe.
 * As três leem a mesma base aberta da Receita; a segunda tem o mesmo formato
 * da primeira, e a terceira é traduzida para ele.
 */
async function cadastroNaReceita(cnpj: string): Promise<Record<string, unknown> | null> {
  const portas: Array<{ nome: string; abrir: () => Promise<Record<string, unknown> | null> }> = [
    { nome: "brasilapi", abrir: async () => (await buscarJson(`https://brasilapi.com.br/api/cnpj/v1/${cnpj}`, 7000)) as Record<string, unknown> | null },
    { nome: "minhareceita", abrir: async () => (await buscarJson(`https://minhareceita.org/${cnpj}`, 7000)) as Record<string, unknown> | null },
    { nome: "cnpj.ws", abrir: async () => doCnpjWs(await buscarJson(`https://publica.cnpj.ws/cnpj/${cnpj}`, 7000)) },
  ];
  for (const porta of portas) {
    const dados = await porta.abrir().catch(() => null);
    if (dados && texto(dados.razao_social)) return dados;
    /* Fica no log do servidor: é assim que se descobre qual serviço passou a recusar a VPS. */
    console.warn(`[fiscal] consulta de CNPJ: ${porta.nome} não respondeu, tentando a próxima`);
  }
  return null;
}

export async function consultarCnpj(cnpjBruto: string): Promise<EmpresaConsultada | null> {
  const cnpj = soDigitos(cnpjBruto);
  if (cnpj.length !== 14) return null;
  return lembrar(`cnpj:${cnpj}`, 24, async () => {
    const dados = await cadastroNaReceita(cnpj);
    if (!dados) return null as unknown as EmpresaConsultada;
    const tipoDeRua = texto(dados.descricao_tipo_de_logradouro);
    const rua = texto(dados.logradouro);
    const logradouro = titulo(tipoDeRua && !rua.toUpperCase().startsWith(tipoDeRua.toUpperCase()) ? `${tipoDeRua} ${rua}` : rua);
    const uf = texto(dados.uf).toUpperCase();
    const cidade = titulo(texto(dados.municipio));
    const cep = soDigitos(dados.cep);
    const ibgeDaApi = soDigitos(dados.codigo_municipio_ibge);
    const ibge = ibgeDaApi.length === 7 ? ibgeDaApi : (await codigoIbge({ cep, cidade, uf }))?.ibge ?? "";
    const principal = soDigitos(dados.cnae_fiscal);
    const secundarios = Array.isArray(dados.cnaes_secundarios) ? (dados.cnaes_secundarios as Array<Record<string, unknown>>) : [];
    const cnaes = [
      ...(principal.length === 7 ? [{ codigo: principal, descricao: texto(dados.cnae_fiscal_descricao), principal: true }] : []),
      ...secundarios
        .map((c) => ({ codigo: soDigitos(c.codigo), descricao: texto(c.descricao), principal: false }))
        .filter((c) => c.codigo.length === 7 && c.codigo !== principal),
    ];
    const inscricaoEstadual = await inscricaoEstadualDoCnpj(cnpj, uf).catch(() => null);
    return {
      cnpj,
      inscricaoEstadual,
      razaoSocial: texto(dados.razao_social),
      nomeFantasia: titulo(texto(dados.nome_fantasia)),
      email: texto(dados.email).toLowerCase(),
      telefone: soDigitos(dados.ddd_telefone_1),
      simples: typeof dados.opcao_pelo_simples === "boolean" ? dados.opcao_pelo_simples : null,
      mei: typeof dados.opcao_pelo_mei === "boolean" ? dados.opcao_pelo_mei : null,
      cnaes,
      endereco: {
        cep,
        logradouro,
        numero: texto(dados.numero) || "S/N",
        complemento: titulo(texto(dados.complemento)),
        bairro: titulo(texto(dados.bairro)),
        cidade,
        uf,
        ibge,
      },
    };
  });
}

/* ---------------- NCM ---------------- */

export type NcmEncontrado = { codigo: string; descricao: string };

function formatarNcm(codigo: string) {
  const d = soDigitos(codigo);
  return d.length === 8 ? `${d.slice(0, 4)}.${d.slice(4, 6)}.${d.slice(6)}` : d;
}

export async function procurarNcm(termo: string): Promise<NcmEncontrado[]> {
  const limpo = texto(termo);
  if (limpo.length < 2) return [];
  const digitos = soDigitos(limpo);
  const porCodigo = digitos.length >= 4 && digitos.length === limpo.replace(/[.\s]/g, "").length;
  return lembrar(`ncm:${semAcento(limpo)}`, 72, async () => {
    const url = porCodigo && digitos.length === 8
      ? `https://brasilapi.com.br/api/ncm/v1/${digitos}`
      : `https://brasilapi.com.br/api/ncm/v1?search=${encodeURIComponent(porCodigo ? digitos : limpo)}`;
    const dados = await buscarJson(url, 10000);
    const lista = Array.isArray(dados) ? dados : dados && typeof dados === "object" ? [dados] : [];
    return (lista as Array<Record<string, unknown>>)
      .map((item) => ({ codigo: soDigitos(item.codigo), descricao: texto(item.descricao).replace(/^[-–\s]+/, "") }))
      .filter((item) => item.codigo.length === 8)
      .slice(0, 40)
      .map((item) => ({ codigo: item.codigo, descricao: `${formatarNcm(item.codigo)} · ${item.descricao}` }));
  }).then((lista) => lista ?? []);
}

export async function ncmExiste(codigo: string): Promise<boolean | null> {
  const digitos = soDigitos(codigo);
  if (digitos.length !== 8) return false;
  const encontrados = await procurarNcm(digitos);
  if (!encontrados.length) return null;
  return encontrados.some((item) => item.codigo === digitos);
}
