/**
 * Cliente HTTP isolado da BrasilAPI (CNPJ e CEP).
 *
 * Só consulta documentos e CEPs explicitamente pedidos pelo usuário
 * (cadastro/import). Timeout curto; Zod na borda; nunca lança para o React —
 * retorna Result.
 */
import { z } from "zod";

const DEFAULT_BASE = "https://brasilapi.com.br/api";
const DEFAULT_TIMEOUT_MS = 8_000;

/**
 * O `User-Agent` PADRÃO DO NODE É RECUSADO PELA BRASILAPI — por isso este é explícito.
 *
 * Quando o código não define o cabeçalho, o `fetch` do Node 22 (o do `.nvmrc`)
 * manda `User-Agent: node` sozinho. A borda que serve a BrasilAPI recusa esse
 * VALOR. Medido contra o mesmo CNPJ, da mesma máquina, na mesma rodada:
 *
 *   403/429  `User-Agent: node`  (era exatamente o que daqui saía)
 *   429      sem `User-Agent`, ou com ele vazio
 *   200      `User-Agent: self-hosted-crm/1.0`, e também `curl/8.7.1`
 *
 * O status da recusa variou entre 403 e 429 de uma medição para outra; o 200 do
 * valor abaixo, não. Não era bloqueio temporário nem limite de uso do IP de
 * alguém: deixava a consulta de CNPJ quebrada em TODA instalação — o lookup do
 * cadastro, o enriquecimento de `lib/crm-b2b/enrich.ts` e a importação em lote,
 * que usam este mesmo cliente.
 *
 * O valor é NEUTRO de propósito, e não reusa o `APP_USER_AGENT` de
 * `lib/nuvemshop/config.ts`. Lá o nome do produto está na allowlist de marca
 * porque identifica uma aplicação REGISTRADA na Nuvemshop; aqui não há registro
 * nenhum. Mandar o nome da marca entregaria o revendedor a um terceiro,
 * variaria por instalação (deixando o tráfego justamente inidentificável) e
 * pediria linha nova numa allowlist que, por doutrina, só encolhe.
 */
const USER_AGENT = "self-hosted-crm/1.0";

const cnaeSchema = z
  .object({
    code: z.union([z.string(), z.number()]).transform(String).optional(),
    text: z.string().optional(),
  })
  .passthrough();

export const brasilApiCnpjSchema = z
  .object({
    cnpj: z.string().optional(),
    razao_social: z.string().nullable().optional(),
    nome_fantasia: z.string().nullable().optional(),
    descricao_situacao_cadastral: z.string().nullable().optional(),
    natureza_juridica: z.string().nullable().optional(),
    porte: z.string().nullable().optional(),
    capital_social: z.union([z.number(), z.string()]).nullable().optional(),
    data_inicio_atividade: z.string().nullable().optional(),
    cnae_fiscal: z.union([z.number(), z.string()]).nullable().optional(),
    cnae_fiscal_descricao: z.string().nullable().optional(),
    cnaes_secundarios: z.array(cnaeSchema).nullable().optional(),
    logradouro: z.string().nullable().optional(),
    numero: z.string().nullable().optional(),
    complemento: z.string().nullable().optional(),
    bairro: z.string().nullable().optional(),
    municipio: z.string().nullable().optional(),
    uf: z.string().nullable().optional(),
    cep: z.string().nullable().optional(),
    email: z.string().nullable().optional(),
    ddd_telefone_1: z.string().nullable().optional(),
  })
  .passthrough();

export type BrasilApiCnpj = z.infer<typeof brasilApiCnpjSchema>;

export type BrasilApiResult =
  | { ok: true; data: BrasilApiCnpj; raw: unknown }
  | {
      ok: false;
      code: "invalid_cnpj" | "not_found" | "timeout" | "upstream_error" | "incomplete";
      message: string;
      status?: number;
    };

export interface BrasilApiClientOpts {
  baseUrl?: string;
  timeoutMs?: number;
  fetchFn?: typeof fetch;
}

/** Contrato do cliente — o que o handler de lookup injeta no teste. */
export interface BrasilApiClient {
  lookupCnpj(normalizedCnpj: string): Promise<BrasilApiResult>;
}

/**
 * O CEP do contato pessoa física (#3494).
 *
 * `/cep/v1` e não `/cep/v2`: a v2 acrescenta coordenadas e, quando o serviço de
 * geocodificação não responde, devolve erro para um CEP que existe. O endereço
 * de entrega/cobrança não usa coordenada, então a rota mais simples é também a
 * que não inventa falha.
 */
export const brasilApiCepSchema = z
  .object({
    cep: z.string().nullable().optional(),
    state: z.string().nullable().optional(),
    city: z.string().nullable().optional(),
    neighborhood: z.string().nullable().optional(),
    street: z.string().nullable().optional(),
    service: z.string().nullable().optional(),
  })
  .passthrough();

export type BrasilApiCep = z.infer<typeof brasilApiCepSchema>;

export type BrasilApiCepResult =
  | { ok: true; data: BrasilApiCep; raw: unknown }
  | {
      ok: false;
      code: "invalid_cep" | "not_found" | "timeout" | "upstream_error" | "incomplete";
      message: string;
      status?: number;
    };

/**
 * Contrato do CEP, separado do de CNPJ.
 *
 * Interface PRÓPRIA em vez de um método a mais em `BrasilApiClient`: os dublês
 * de CNPJ (`lib/crm-b2b/*.test.ts`) já montam o objeto inteiro, e exigir um
 * método novo neles seria mudar teste alheio para acomodar esta fatia. Quem
 * consulta CEP declara a interface que precisa.
 */
export interface BrasilApiCepClient {
  lookupCep(normalizedCep: string): Promise<BrasilApiCepResult>;
}

export function createBrasilApiClient(
  opts: BrasilApiClientOpts = {},
): BrasilApiClient & BrasilApiCepClient {
  const baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const fetchFn = opts.fetchFn ?? fetch;

  return {
    async lookupCnpj(normalizedCnpj: string): Promise<BrasilApiResult> {
      // 12 posições de dígito ou LETRA + 2 dígitos verificadores: o CNPJ
      // alfanumérico da Receita (#3495). Recusar aqui devolveria `invalid_cnpj`
      // para um documento legítimo que a rota de empresa já aceita.
      if (!/^[0-9A-Z]{14}$/.test(normalizedCnpj)) {
        return {
          ok: false,
          code: "invalid_cnpj",
          message: "CNPJ deve ter 14 posições (12 alfanuméricas e 2 dígitos verificadores).",
        };
      }

      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), timeoutMs);
      try {
        const res = await fetchFn(`${baseUrl}/cnpj/v1/${normalizedCnpj}`, {
          method: "GET",
          headers: { Accept: "application/json", "User-Agent": USER_AGENT },
          signal: ctrl.signal,
        });
        if (res.status === 404) {
          return {
            ok: false,
            code: "not_found",
            message: "CNPJ não encontrado na BrasilAPI.",
            status: 404,
          };
        }
        if (!res.ok) {
          return {
            ok: false,
            code: "upstream_error",
            message: `BrasilAPI respondeu ${res.status}.`,
            status: res.status,
          };
        }
        const raw: unknown = await res.json();
        const parsed = brasilApiCnpjSchema.safeParse(raw);
        if (!parsed.success) {
          return {
            ok: false,
            code: "incomplete",
            message: "Resposta da BrasilAPI incompleta ou inválida.",
          };
        }
        if (!parsed.data.razao_social && !parsed.data.nome_fantasia) {
          return {
            ok: false,
            code: "incomplete",
            message: "BrasilAPI sem razão social nem nome fantasia.",
          };
        }
        return { ok: true, data: parsed.data, raw };
      } catch (err) {
        const aborted = err instanceof Error && err.name === "AbortError";
        return {
          ok: false,
          code: aborted ? "timeout" : "upstream_error",
          message: aborted
            ? "Timeout ao consultar a BrasilAPI."
            : "Falha de rede ao consultar a BrasilAPI.",
        };
      } finally {
        clearTimeout(timer);
      }
    },

    /**
     * CEP → endereço (issue #3494). Mesmo `baseUrl`, mesmo `User-Agent`
     * explícito, mesmo `AbortController` e mesmas famílias de erro do CNPJ —
     * quem já depura uma chamada desta BrasilAPI reconhece a outra.
     *
     * A validação de forma é aqui de propósito: a rota que chama isto é pública
     * para a tela, e um CEP de 7 dígitos não pode virar uma requisição a um
     * serviço de terceiro nem um 500 por causa de `fetch` com URL estranha.
     */
    async lookupCep(normalizedCep: string): Promise<BrasilApiCepResult> {
      const digitos = normalizedCep.replace(/\D/g, "");
      if (digitos.length !== 8) {
        return {
          ok: false,
          code: "invalid_cep",
          message: "CEP deve ter 8 dígitos.",
        };
      }

      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), timeoutMs);
      try {
        const res = await fetchFn(`${baseUrl}/cep/v1/${digitos}`, {
          method: "GET",
          headers: { Accept: "application/json", "User-Agent": USER_AGENT },
          signal: ctrl.signal,
        });
        if (res.status === 404) {
          return {
            ok: false,
            code: "not_found",
            message: "CEP não encontrado na BrasilAPI.",
            status: 404,
          };
        }
        if (!res.ok) {
          return {
            ok: false,
            code: "upstream_error",
            message: `BrasilAPI respondeu ${res.status}.`,
            status: res.status,
          };
        }
        const raw: unknown = await res.json();
        const parsed = brasilApiCepSchema.safeParse(raw);
        if (!parsed.success) {
          return {
            ok: false,
            code: "incomplete",
            message: "Resposta da BrasilAPI incompleta ou inválida.",
          };
        }
        // CEP de município inteiro (o "CEP geral" de cidade pequena) vem só com
        // cidade/UF — isso é resposta VÁLIDA. Vazio de tudo é que não serve.
        if (!parsed.data.city && !parsed.data.state) {
          return {
            ok: false,
            code: "incomplete",
            message: "BrasilAPI sem cidade nem UF para este CEP.",
          };
        }
        return { ok: true, data: parsed.data, raw };
      } catch (err) {
        const aborted = err instanceof Error && err.name === "AbortError";
        return {
          ok: false,
          code: aborted ? "timeout" : "upstream_error",
          message: aborted
            ? "Timeout ao consultar a BrasilAPI."
            : "Falha de rede ao consultar a BrasilAPI.",
        };
      } finally {
        clearTimeout(timer);
      }
    },
  };
}

/** Mapeia payload BrasilAPI → colunas de `companies` (sem id/org). */
export function mapBrasilApiToCompanyFields(data: BrasilApiCnpj): Record<string, unknown> {
  const capital =
    data.capital_social === null || data.capital_social === undefined
      ? null
      : Number(data.capital_social);
  return {
    legal_name: data.razao_social ?? null,
    trade_name: data.nome_fantasia || data.razao_social || null,
    registration_status: data.descricao_situacao_cadastral ?? null,
    legal_nature: data.natureza_juridica ?? null,
    company_size: data.porte ?? null,
    share_capital: Number.isFinite(capital) ? capital : null,
    opened_at: data.data_inicio_atividade ?? null,
    main_cnae_code: data.cnae_fiscal != null ? String(data.cnae_fiscal) : null,
    main_cnae_description: data.cnae_fiscal_descricao ?? null,
    secondary_cnaes: data.cnaes_secundarios ?? [],
    street: data.logradouro ?? null,
    number: data.numero ?? null,
    complement: data.complemento ?? null,
    district: data.bairro ?? null,
    city: data.municipio ?? null,
    state: data.uf ?? null,
    zip_code: data.cep ?? null,
    email: data.email ?? null,
    phone: data.ddd_telefone_1 ?? null,
  };
}
