/**
 * A empresa emissora: o que fica gravado em `fiscal_emissores`, lido com
 * defesa, e como isso vira as configurações da empresa na Spedy.
 *
 * Duas armadilhas da Spedy moram aqui:
 *
 * - O PUT de configurações ZERA o que não vai no bloco enviado. Por isso cada
 *   bloco é montado em cima do que a Spedy devolveu no GET, e o CSC e a senha
 *   da prefeitura (que ela não devolve) saem do cofre a cada envio.
 * - A próxima numeração só vai quando alguém pediu para mudar. Reenviar o
 *   número guardado no cadastro voltaria o contador e a SEFAZ rejeitaria a
 *   próxima nota por número repetido.
 */

import type { AmbienteFiscal } from "./ambiente.ts";
import type { EmissorDaNota, EnderecoFiscal } from "./montar.ts";
import { regimeUsaCsosn } from "./regras.ts";
import type { ExigenciasDaCidade, Regime } from "./regras.ts";
import type { SpedyConfiguracoes, SpedyEmpresaEditavel, SpedyOpcoesDoProvedor } from "../spedy/api.ts";

export type AmbienteDaNota = "producao" | "homologacao" | "simulacao";

export type ConfigNfe = {
  ativo: boolean;
  ambiente: "homologacao" | "producao";
  serie: string;
  danfe: "default" | "simplified";
};

export type ConfigNfce = {
  ativo: boolean;
  ambiente: "homologacao" | "producao";
  serie: string;
  tokenIdHomologacao: string | null;
  tokenIdProducao: string | null;
  contingenciaOffline: boolean;
};

export type CidadeDaNfse = {
  codigo: string;
  nome: string;
  uf: string;
  provedor: string | null;
  opcoes: SpedyOpcoesDoProvedor | null;
};

export type ConfigNfse = {
  ativo: boolean;
  ambiente: AmbienteDaNota;
  serie: string | null;
  forma: "normal" | "annfs" | "website" | "alt";
  usuario: string | null;
  enviarInscricaoMunicipal: "automatic" | "yes" | "no";
  cidade: CidadeDaNfse | null;
};

export type SegredosDoEmissor = {
  cscHomologacao?: string;
  cscProducao?: string;
  nfseSenha?: string;
  nfseNumeroAutenticacao?: string;
};

export type Presenca = "presence" | "internet" | "telephone" | "delivery" | "othersNonPresenceOperation";

export type PreferenciasFiscais = {
  presencaPedido: Presenca;
  enviarEmail: boolean;
  emitirNaLoja: boolean;
  nfceNoPdv: "perguntar" | "sempre" | "nunca";
  consumidorFinal: boolean;
  /** Buscar na SEFAZ as notas que outras empresas emitem contra este CNPJ. */
  receberNotas: boolean;
  /** Data de corte da busca (a SEFAZ guarda cerca de 90 dias). */
  receberDesde: string | null;
};

export type CertificadoGuardado = {
  id: string | null;
  validade: string | null;
  titular: string | null;
  emissor: string | null;
  enviadoEm: string | null;
};

export type EmissorGuardado = {
  id: string;
  empresa_id: string;
  ambiente: AmbienteFiscal;
  status: "rascunho" | "ativo" | "suspenso";
  spedy_empresa_id: string | null;
  chave_cifrada: string | null;
  segredos_cifrados: string | null;
  cnpj: string;
  razao_social: string;
  nome_fantasia: string | null;
  inscricao_estadual: string | null;
  inscricao_municipal: string | null;
  regime: Regime;
  regime_especial: string | null;
  regime_simples_nfse: string | null;
  cnaes: unknown;
  email: string | null;
  telefone: string | null;
  endereco: unknown;
  certificado: unknown;
  certificado_validade: string | null;
  nfe: unknown;
  nfce: unknown;
  nfse: unknown;
  preferencias: unknown;
  ultimo_erro: string | null;
  sincronizado_em: string | null;
  created_at: string;
  updated_at: string;
};

const objeto = (valor: unknown): Record<string, unknown> =>
  valor && typeof valor === "object" && !Array.isArray(valor) ? (valor as Record<string, unknown>) : {};
const textoOuNulo = (valor: unknown) => {
  const t = typeof valor === "string" ? valor.trim() : typeof valor === "number" ? String(valor) : "";
  return t ? t : null;
};
const soDigitos = (valor: unknown) => (typeof valor === "string" || typeof valor === "number" ? String(valor).replace(/\D/g, "") : "");

export function lerNfe(bruto: unknown): ConfigNfe {
  const r = objeto(bruto);
  return {
    ativo: r.ativo === true,
    ambiente: r.ambiente === "producao" ? "producao" : "homologacao",
    serie: textoOuNulo(r.serie) ?? "1",
    danfe: r.danfe === "simplified" ? "simplified" : "default",
  };
}

export function lerNfce(bruto: unknown): ConfigNfce {
  const r = objeto(bruto);
  return {
    ativo: r.ativo === true,
    ambiente: r.ambiente === "producao" ? "producao" : "homologacao",
    serie: textoOuNulo(r.serie) ?? "1",
    tokenIdHomologacao: textoOuNulo(r.tokenIdHomologacao),
    tokenIdProducao: textoOuNulo(r.tokenIdProducao),
    contingenciaOffline: r.contingenciaOffline === true,
  };
}

/**
 * O CSC do cupom fiscal tem de 16 a 36 caracteres (regra nacional do QR Code da
 * NFC-e). Vale exatamente como a Secretaria da Fazenda mostrou: vários estados
 * geram no formato 9D76E5F6-2EAC-442A-..., com traços, e tirar os traços muda o
 * código e derruba o QR Code. Colado pela metade, a SEFAZ devolve uma resposta
 * que nem o serviço de notas reconhece ("Mensagem de retorno da Sefaz não
 * reconhecida"). Vazio não é problema aqui: quem exige o preenchimento é quem
 * chama.
 */
export function problemaNoCsc(csc: string | null | undefined, rotulo = "CSC"): string | null {
  const valor = String(csc ?? "").replace(/\s+/g, "");
  if (!valor) return null;
  /* Só barra o que nem aparece no site da Secretaria (acento, emoji, caractere invisível). */
  if (!/^[\x21-\x7E]+$/.test(valor)) {
    return `O ${rotulo} tem caracteres estranhos. Copie o código do site da Secretaria da Fazenda, sem acentos.`;
  }
  if (valor.length < 16 || valor.length > 36) {
    return `O ${rotulo} tem de 16 a 36 caracteres, e o informado tem ${valor.length}. Copie o código inteiro do site da Secretaria da Fazenda, com os traços se houver.`;
  }
  return null;
}

/** A identificação do CSC (idToken) é um número curto, de até 6 dígitos, como 000001. */
export function problemaNaIdentificacaoDoCsc(identificacao: string | null | undefined, rotulo = "identificação do CSC"): string | null {
  const valor = String(identificacao ?? "").trim();
  if (!valor) return null;
  if (!/^\d{1,6}$/.test(valor)) return `A ${rotulo} é um número de até 6 dígitos, como 000001.`;
  return null;
}

export function lerNfse(bruto: unknown): ConfigNfse {
  const r = objeto(bruto);
  const cidade = objeto(r.cidade);
  const ambiente = r.ambiente === "producao" || r.ambiente === "homologacao" ? r.ambiente : "simulacao";
  const forma = ["annfs", "website", "alt"].includes(String(r.forma)) ? (r.forma as ConfigNfse["forma"]) : "normal";
  const enviarIm = r.enviarInscricaoMunicipal === "yes" || r.enviarInscricaoMunicipal === "no" ? r.enviarInscricaoMunicipal : "automatic";
  return {
    ativo: r.ativo === true,
    ambiente,
    serie: textoOuNulo(r.serie),
    forma,
    usuario: textoOuNulo(r.usuario),
    enviarInscricaoMunicipal: enviarIm,
    cidade: textoOuNulo(cidade.codigo)
      ? {
          codigo: String(cidade.codigo),
          nome: textoOuNulo(cidade.nome) ?? "",
          uf: (textoOuNulo(cidade.uf) ?? "").toUpperCase(),
          provedor: textoOuNulo(cidade.provedor),
          opcoes: (Object.keys(objeto(cidade.opcoes)).length ? objeto(cidade.opcoes) : null) as SpedyOpcoesDoProvedor | null,
        }
      : null,
  };
}

export function lerPreferencias(bruto: unknown): PreferenciasFiscais {
  const r = objeto(bruto);
  const presencas: Presenca[] = ["presence", "internet", "telephone", "delivery", "othersNonPresenceOperation"];
  return {
    presencaPedido: presencas.includes(r.presencaPedido as Presenca) ? (r.presencaPedido as Presenca) : "presence",
    enviarEmail: r.enviarEmail !== false,
    emitirNaLoja: r.emitirNaLoja === true,
    nfceNoPdv: r.nfceNoPdv === "sempre" || r.nfceNoPdv === "nunca" ? r.nfceNoPdv : "perguntar",
    consumidorFinal: r.consumidorFinal !== false,
    receberNotas: r.receberNotas === true,
    receberDesde: typeof r.receberDesde === "string" && /^\d{4}-\d{2}-\d{2}$/.test(r.receberDesde) ? r.receberDesde : null,
  };
}

export function lerCertificado(bruto: unknown): CertificadoGuardado | null {
  const r = objeto(bruto);
  if (!Object.keys(r).length) return null;
  return {
    id: textoOuNulo(r.id),
    validade: textoOuNulo(r.validade),
    titular: textoOuNulo(r.titular),
    emissor: textoOuNulo(r.emissor),
    enviadoEm: textoOuNulo(r.enviadoEm),
  };
}

export function lerEndereco(bruto: unknown): EnderecoFiscal {
  const r = objeto(bruto);
  return {
    cep: textoOuNulo(r.cep),
    logradouro: textoOuNulo(r.logradouro),
    numero: textoOuNulo(r.numero),
    complemento: textoOuNulo(r.complemento),
    bairro: textoOuNulo(r.bairro),
    cidade: textoOuNulo(r.cidade),
    uf: textoOuNulo(r.uf)?.toUpperCase() ?? null,
    ibge: textoOuNulo(r.ibge),
  };
}

export function lerCnaes(bruto: unknown): Array<{ codigo: string; principal: boolean }> {
  if (!Array.isArray(bruto)) return [];
  const vistos = new Set<string>();
  const saida: Array<{ codigo: string; principal: boolean }> = [];
  for (const item of bruto) {
    const r = objeto(item);
    const codigo = soDigitos(r.codigo);
    if (codigo.length !== 7 || vistos.has(codigo)) continue;
    vistos.add(codigo);
    saida.push({ codigo, principal: r.principal === true });
  }
  if (saida.length && !saida.some((c) => c.principal)) saida[0].principal = true;
  return saida;
}

export function emissorParaNota(emissor: EmissorGuardado): EmissorDaNota {
  const endereco = lerEndereco(emissor.endereco);
  return {
    cnpj: soDigitos(emissor.cnpj),
    razaoSocial: emissor.razao_social,
    regime: emissor.regime,
    uf: endereco.uf ?? "",
    cidadeIbge: endereco.ibge,
    inscricaoEstadual: emissor.inscricao_estadual,
    inscricaoMunicipal: emissor.inscricao_municipal,
  };
}

/*
  O modo de teste da nota de serviço. A simulação (a Spedy faz o papel da
  prefeitura) só existe no ambiente de testes dela: no Ktalogu oficial a Spedy
  recusa a configuração inteira com "Ambiente de simulação está disponível
  apenas no Sandbox" e a empresa não passa do passo das notas. Lá, o teste da
  nota de serviço é a homologação da prefeitura.
*/
export function ambienteDaNfse(emissor: Pick<EmissorGuardado, "ambiente" | "nfse">): AmbienteDaNota {
  const ambiente = lerNfse(emissor.nfse).ambiente;
  return emissor.ambiente === "producao" && ambiente === "simulacao" ? "homologacao" : ambiente;
}

/*
  A série da nota de serviço no padrão nacional. Sem série a nota nem é lida:
  o ambiente nacional recusa o arquivo ("invalid child element 'nDPS' ...
  expected: 'serie'"). E a série não é livre: de 1 a 49999 é de quem emite por
  sistema próprio, que é o caso do Ktalogu; 70000 em diante é do portal do
  governo, e a nota que a empresa emitiu por lá não serve de modelo.
*/
export const SERIE_PADRAO_DO_NACIONAL = "1";

export function nfseNoPadraoNacional(nfse: Pick<ConfigNfse, "forma" | "cidade">) {
  return nfse.forma === "annfs" || nfse.cidade?.opcoes?.useNationalLayout === true;
}

export function problemaNaSerieDoNacional(serie: string | null | undefined): string | null {
  const valor = String(serie ?? "").trim();
  if (!valor) return null;
  const numero = /^\d{1,5}$/.test(valor) ? Number(valor) : 0;
  if (numero >= 1 && numero <= 49999) return null;
  return "A série da nota de serviço vai de 1 a 49999. As séries de 70000 em diante são do portal do governo e não servem aqui. Deixe 1.";
}

/** O ambiente da nota de cada modelo, pelo que está configurado. */
export function ambienteDaNota(emissor: EmissorGuardado, modelo: "nfe" | "nfce" | "nfse"): AmbienteDaNota {
  if (modelo === "nfe") return lerNfe(emissor.nfe).ambiente;
  if (modelo === "nfce") return lerNfce(emissor.nfce).ambiente;
  return ambienteDaNfse(emissor);
}

export function modeloAtivo(emissor: EmissorGuardado, modelo: "nfe" | "nfce" | "nfse") {
  if (modelo === "nfe") return lerNfe(emissor.nfe).ativo;
  if (modelo === "nfce") return lerNfce(emissor.nfce).ativo;
  return lerNfse(emissor.nfse).ativo;
}

export function ambienteParaSpedy(ambiente: AmbienteDaNota): "production" | "development" | "simulation" {
  if (ambiente === "producao") return "production";
  if (ambiente === "simulacao") return "simulation";
  return "development";
}

export function exigenciasDaCidade(opcoes: SpedyOpcoesDoProvedor | null | undefined): ExigenciasDaCidade {
  if (!opcoes) return { codigoLc116: true };
  /* No padrão nacional o código da prefeitura é um complemento que só algumas
     cadastram. A Spedy marca todas como se exigissem, mas a que não tem recusa
     qualquer código mandado ("não existe ou não está administrado pelo
     município", Parauapebas, 01/10/2026): o certo é a nota ir sem ele. */
  const nacional = opcoes.useNationalLayout === true;
  return {
    codigoLc116: opcoes.federalServiceCode !== false,
    codigoMunicipal: opcoes.cityServiceCode === true && !nacional,
    codigoMunicipalOpcional: opcoes.cityServiceCode === true && nacional,
    cnae: opcoes.cnaeCode === true,
    /* No padrão nacional o NBS vai em toda nota, mesmo quando a Spedy não marca a cidade. */
    nbs: opcoes.nbsCode === true || opcoes.useNationalLayout === true,
    codigoTributacaoNacional: opcoes.nationalTaxationCode === true,
    anexoSimples: opcoes.useSimplesNacionalAnnex === true,
  };
}

/** Os dados da empresa no formato da Spedy. */
export function empresaParaSpedy(emissor: EmissorGuardado): SpedyEmpresaEditavel {
  const endereco = lerEndereco(emissor.endereco);
  const cnaes = lerCnaes(emissor.cnaes);
  const dados: SpedyEmpresaEditavel = {
    name: (emissor.nome_fantasia || emissor.razao_social).slice(0, 80),
    legalName: emissor.razao_social.slice(0, 80),
    federalTaxNumber: soDigitos(emissor.cnpj),
    stateTaxNumber: emissor.inscricao_estadual ? emissor.inscricao_estadual.trim().toUpperCase() === "ISENTO" ? "ISENTO" : soDigitos(emissor.inscricao_estadual) : null,
    cityTaxNumber: emissor.inscricao_municipal ? soDigitos(emissor.inscricao_municipal) || emissor.inscricao_municipal.trim() : null,
    email: emissor.email ? emissor.email.slice(0, 80) : null,
    phone: soDigitos(emissor.telefone) || null,
    address: {
      street: (endereco.logradouro ?? "").slice(0, 100),
      number: (endereco.numero ?? "S/N").slice(0, 10),
      district: (endereco.bairro ?? "").slice(0, 100),
      postalCode: soDigitos(endereco.cep),
      additionalInformation: endereco.complemento ? endereco.complemento.slice(0, 150) : null,
      city: { code: soDigitos(endereco.ibge), name: endereco.cidade ?? "", state: (endereco.uf ?? "").toLowerCase() },
      country: { name: "Brasil", code: 1058 },
    },
    taxRegime: emissor.regime,
  };
  if (emissor.regime_especial) dados.specialTaxRegime = emissor.regime_especial;
  if (emissor.regime_simples_nfse) dados.simplesNacionalTaxRegime = emissor.regime_simples_nfse;
  if (cnaes.length) dados.economicActivities = cnaes.map((c) => ({ code: c.codigo, type: c.principal ? "main" : "secondary" }));
  return dados;
}

export type AjustesDeNumeracao = {
  nfe?: number | null;
  nfce?: number | null;
  nfse?: number | null;
  nfseLote?: number | null;
};

/**
 * Os blocos do PUT de configurações, montados em cima do que a Spedy tem.
 * Só vão os blocos dos modelos ligados; bloco que não vai fica intocado lá.
 */
export function blocosDeConfiguracao(
  emissor: EmissorGuardado,
  segredos: SegredosDoEmissor,
  atuais: SpedyConfiguracoes | null,
  numeracao: AjustesDeNumeracao = {}
): SpedyConfiguracoes {
  const blocos: SpedyConfiguracoes = {};
  const nfe = lerNfe(emissor.nfe);
  const nfce = lerNfce(emissor.nfce);
  const nfse = lerNfse(emissor.nfse);

  if (nfe.ativo) {
    const bloco: Record<string, unknown> = {
      ...objeto(atuais?.productInvoice),
      series: nfe.serie,
      environmentType: ambienteParaSpedy(nfe.ambiente),
      danfePrintLayout: nfe.danfe,
    };
    if (numeracao.nfe && numeracao.nfe > 0) bloco.nextNumber = Math.floor(numeracao.nfe);
    /* Empresa que nunca emitiu (ou criada fora do Ktalogu) vem sem numeração: a nota sairia com número 0. */
    if (!(Number(bloco.nextNumber) > 0)) bloco.nextNumber = 1;
    blocos.productInvoice = bloco;
  }

  if (nfce.ativo) {
    const producao = nfce.ambiente === "producao";
    const bloco: Record<string, unknown> = {
      ...objeto(atuais?.consumerInvoice),
      series: nfce.serie,
      environmentType: ambienteParaSpedy(nfce.ambiente),
      tokenId: (producao ? nfce.tokenIdProducao : nfce.tokenIdHomologacao) ?? null,
      csc: (producao ? segredos.cscProducao : segredos.cscHomologacao) ?? null,
      allowOfflineContingency: nfce.contingenciaOffline,
    };
    if (numeracao.nfce && numeracao.nfce > 0) bloco.nextNumber = Math.floor(numeracao.nfce);
    if (!(Number(bloco.nextNumber) > 0)) bloco.nextNumber = 1;
    blocos.consumerInvoice = bloco;
  }

  /* Reforma tributária: a Spedy só põe o grupo de IBS/CBS na nota quando a empresa está com
     esses campos ligados. Sem isso a SEFAZ recusa quem já é obrigado ("Grupo IBS/CBS não
     informado"). Ligamos para o regime normal, que é de quem o grupo já é exigido; o bloco vai
     montado em cima do que a Spedy tem, senão o PUT apagaria o responsável técnico. */
  if (!regimeUsaCsosn(emissor.regime) && objeto(atuais?.general).taxReformFieldsEnabled !== true) {
    blocos.general = { ...objeto(atuais?.general), taxReformFieldsEnabled: true };
  }

  if (nfse.ativo) {
    const bloco: Record<string, unknown> = {
      ...objeto(atuais?.serviceInvoice),
      environmentType: ambienteParaSpedy(ambienteDaNfse(emissor)),
      issueType: nfse.forma,
      sendCityTaxNumber: nfse.enviarInscricaoMunicipal,
    };
    const serie = nfse.serie ?? (nfseNoPadraoNacional(nfse) && !String(bloco.series ?? "").trim() ? SERIE_PADRAO_DO_NACIONAL : null);
    if (serie) bloco.series = serie;
    if (nfse.usuario) bloco.userName = nfse.usuario;
    if (segredos.nfseSenha) bloco.password = segredos.nfseSenha;
    if (segredos.nfseNumeroAutenticacao) bloco.authNumber = segredos.nfseNumeroAutenticacao;
    if (numeracao.nfse && numeracao.nfse > 0) bloco.nextNumber = Math.floor(numeracao.nfse);
    if (numeracao.nfseLote && numeracao.nfseLote > 0) bloco.nextBatchNumber = Math.floor(numeracao.nfseLote);
    if (!(Number(bloco.nextNumber) > 0)) bloco.nextNumber = 1;
    if (!(Number(bloco.nextBatchNumber) > 0)) bloco.nextBatchNumber = 1;
    blocos.serviceInvoice = bloco;
  }

  return blocos;
}

export type PassoDaConfiguracao = {
  chave: "empresa" | "certificado" | "modelos" | "tributacao" | "produtos" | "teste" | "producao";
  feito: boolean;
  detalhe: string | null;
};

export function diasParaVencer(validade: string | null | undefined, agora = new Date()) {
  if (!validade) return null;
  const fim = new Date(validade).getTime();
  if (!Number.isFinite(fim)) return null;
  return Math.floor((fim - agora.getTime()) / (24 * 60 * 60 * 1000));
}

/** O que falta, em ordem, para a empresa emitir. */
export function passosDaConfiguracao(input: {
  emissor: EmissorGuardado | null;
  perfisDeMercadoria: number;
  perfisDeServico: number;
  produtosSemNcm: number;
  notasDeTesteAutorizadas: number;
}): PassoDaConfiguracao[] {
  const { emissor } = input;
  const nfe = lerNfe(emissor?.nfe);
  const nfce = lerNfce(emissor?.nfce);
  const nfse = lerNfse(emissor?.nfse);
  const algumModelo = nfe.ativo || nfce.ativo || nfse.ativo;
  const precisaMercadoria = nfe.ativo || nfce.ativo;
  const certificado = lerCertificado(emissor?.certificado);
  const dias = diasParaVencer(certificado?.validade ?? emissor?.certificado_validade ?? null);
  const producao =
    (!nfe.ativo || nfe.ambiente === "producao") &&
    (!nfce.ativo || nfce.ambiente === "producao") &&
    (!nfse.ativo || nfse.ambiente === "producao");

  return [
    {
      chave: "empresa",
      feito: Boolean(emissor?.spedy_empresa_id && emissor.chave_cifrada),
      detalhe: emissor?.ultimo_erro ?? null,
    },
    {
      chave: "certificado",
      feito: Boolean(certificado?.id) && dias !== null && dias >= 0,
      detalhe: dias === null ? null : dias < 0 ? "Certificado vencido" : `Vence em ${dias} ${dias === 1 ? "dia" : "dias"}`,
    },
    { chave: "modelos", feito: algumModelo, detalhe: null },
    {
      chave: "tributacao",
      feito: (!precisaMercadoria || input.perfisDeMercadoria > 0) && (!nfse.ativo || input.perfisDeServico > 0) && algumModelo,
      detalhe: null,
    },
    {
      chave: "produtos",
      feito: algumModelo && (!precisaMercadoria || input.produtosSemNcm === 0),
      detalhe: precisaMercadoria && input.produtosSemNcm > 0 ? `${input.produtosSemNcm} sem NCM` : null,
    },
    { chave: "teste", feito: input.notasDeTesteAutorizadas > 0, detalhe: null },
    { chave: "producao", feito: algumModelo && producao, detalhe: null },
  ];
}
