/**
 * A nota fiscal sem pedido, na parte que não fala com o banco: ler o que a
 * tela "Emitir nota fiscal" mandou, guardar o formulário na nota e montar o
 * "pedido em memória" que segue o mesmo caminho da nota do pedido.
 */

import type { Presenca } from "./emissor.ts";
import { soDigitos, type ItemDaNota, type PedidoDaNota, type Pendencia } from "./montar.ts";

export const FORMAS_DA_NOTA_AVULSA = {
  pix: { metodo: "pix", pago: true, rotulo: "Pix" },
  dinheiro: { metodo: "cash", pago: true, rotulo: "Dinheiro" },
  credito: { metodo: "credit_card", pago: true, rotulo: "Cartão de crédito" },
  debito: { metodo: "debit_card", pago: true, rotulo: "Cartão de débito" },
  transferencia: { metodo: "transfer", pago: true, rotulo: "Transferência" },
  boleto: { metodo: "boleto", pago: false, rotulo: "Boleto a receber" },
  a_prazo: { metodo: null, pago: false, rotulo: "A prazo" },
} as const;

export type FormaDaNotaAvulsa = keyof typeof FORMAS_DA_NOTA_AVULSA;

export const LIMITE_DE_ITENS_DA_NOTA = 60;

export type ItemDaNotaAvulsa = {
  /** Posição da linha na tela (0, 1, 2...), para a pendência apontar a linha certa. */
  posicao: number;
  produtoId: string | null;
  descricao: string;
  ncm: string | null;
  unidade: string;
  quantidade: number;
  valorUnitario: number;
};

export type NotaAvulsa = {
  /** Gerado pela tela a cada nota: dois cliques não viram duas notas. */
  chave: string | null;
  modelo: "nfe" | "nfce" | "nfse";
  clienteId: string | null;
  /** CPF/CNPJ na nota, para cupom sem cliente cadastrado. */
  documento: string | null;
  itens: ItemDaNotaAvulsa[];
  desconto: number;
  frete: number;
  pagamento: FormaDaNotaAvulsa;
  vencimento: string | null;
  /** Tributação escolhida para os itens digitados (e para o serviço). Vazio = a padrão. */
  perfilId: string | null;
  consumidorFinal: boolean | null;
  presenca: Presenca | null;
  observacao: string | null;
  enviarEmail: boolean | null;
};

/** O produto do catálogo com o que a nota usa dele. */
export type ProdutoDaNotaAvulsa = {
  id: string;
  sku?: string | null;
  barcode?: string | null;
  fiscal_tipo?: string | null;
  ncm?: string | null;
  cest?: string | null;
  fiscal_origem?: number | null;
  fiscal_perfil_id?: string | null;
};

/** Formulário que não dá para ler (sem tipo de nota, itens demais). Vira resposta 400. */
export class ErroNaNotaAvulsa extends Error {
  constructor(mensagem: string) {
    super(mensagem);
    this.name = "ErroNaNotaAvulsa";
  }
}

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const PRESENCAS: Presenca[] = ["presence", "internet", "telephone", "delivery", "othersNonPresenceOperation"];

const objeto = (valor: unknown): Record<string, unknown> =>
  valor && typeof valor === "object" && !Array.isArray(valor) ? (valor as Record<string, unknown>) : {};
const texto = (valor: unknown) => (typeof valor === "string" ? valor.replace(/\s+/g, " ").trim() : "");
const numero = (valor: unknown) => {
  if (valor === null || valor === undefined || valor === "") return Number.NaN;
  const n = typeof valor === "number" ? valor : Number(String(valor).replace(",", "."));
  return Number.isFinite(n) ? n : Number.NaN;
};
const duasCasas = (valor: number) => Math.round(valor * 100) / 100;
const quatroCasas = (valor: number) => Math.round(valor * 10000) / 10000;
const uuidOuNulo = (valor: unknown) => (typeof valor === "string" && UUID.test(valor) ? valor : null);

/**
 * Lê o formulário. Erro de estrutura (sem tipo de nota, itens demais) para
 * tudo; linha incompleta vira pendência, para a tela conferir enquanto a
 * pessoa digita sem aparecer erro a cada tecla.
 */
export function lerNotaAvulsa(bruto: unknown): { dados: NotaAvulsa; problemas: Pendencia[] } {
  const r = objeto(bruto);
  const modelo = r.modelo === "nfce" ? "nfce" : r.modelo === "nfse" ? "nfse" : r.modelo === "nfe" ? "nfe" : null;
  if (!modelo) throw new ErroNaNotaAvulsa("Escolha o tipo de nota.");

  const listaDeItens = Array.isArray(r.itens) ? r.itens : [];
  if (listaDeItens.length > LIMITE_DE_ITENS_DA_NOTA) {
    throw new ErroNaNotaAvulsa(`Uma nota aceita até ${LIMITE_DE_ITENS_DA_NOTA} itens.`);
  }

  const problemas: Pendencia[] = [];
  const itens: ItemDaNotaAvulsa[] = [];
  listaDeItens.forEach((brutoDoItem, posicao) => {
    const i = objeto(brutoDoItem);
    const descricao = texto(i.descricao).slice(0, 120);
    const quantidade = numero(i.quantidade);
    const valorUnitario = numero(i.valorUnitario);
    const problema = (campo: string, titulo: string) =>
      problemas.push({
        codigo: `avulsa.item.${campo}:${posicao}`,
        nivel: "bloqueio",
        onde: "pedido",
        titulo: `Linha ${posicao + 1}: ${titulo}`,
        alvoId: String(posicao),
      });

    if (descricao.length < 2) problema("descricao", "escreva o que foi vendido");
    if (!(quantidade > 0) || quantidade > 1_000_000) {
      problema("quantidade", "informe a quantidade");
      return;
    }
    const valorValido = valorUnitario > 0 && valorUnitario <= 100_000_000;
    if (!valorValido) problema("valor", "informe o valor");

    itens.push({
      posicao,
      produtoId: uuidOuNulo(i.produtoId),
      descricao: descricao.length >= 2 ? descricao : `Item ${posicao + 1}`,
      ncm: soDigitos(i.ncm).slice(0, 8) || null,
      unidade: texto(i.unidade).toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 6) || "UN",
      quantidade: quatroCasas(quantidade),
      valorUnitario: valorValido ? quatroCasas(valorUnitario) : 0,
    });
  });

  const desconto = numero(r.desconto);
  const frete = numero(r.frete);
  const pagamento = Object.hasOwn(FORMAS_DA_NOTA_AVULSA, String(r.pagamento)) ? (r.pagamento as FormaDaNotaAvulsa) : "pix";
  const vencimento = typeof r.vencimento === "string" && /^\d{4}-\d{2}-\d{2}$/.test(r.vencimento) ? r.vencimento : null;
  const clienteId = uuidOuNulo(r.clienteId);

  return {
    dados: {
      chave: uuidOuNulo(r.chave),
      modelo,
      clienteId,
      documento: clienteId ? null : soDigitos(r.documento).slice(0, 14) || null,
      itens,
      desconto: desconto > 0 ? duasCasas(desconto) : 0,
      frete: modelo === "nfe" && frete > 0 ? duasCasas(frete) : 0,
      pagamento,
      vencimento: FORMAS_DA_NOTA_AVULSA[pagamento].pago ? null : vencimento,
      perfilId: uuidOuNulo(r.perfilId),
      consumidorFinal: typeof r.consumidorFinal === "boolean" ? r.consumidorFinal : null,
      presenca: PRESENCAS.includes(r.presenca as Presenca) ? (r.presenca as Presenca) : null,
      observacao: texto(r.observacao).slice(0, 500) || null,
      enviarEmail: typeof r.enviarEmail === "boolean" ? r.enviarEmail : null,
    },
    problemas,
  };
}

/** O que fica guardado na nota: o formulário sem a chave e sem as posições. */
export function formularioDaNota(dados: NotaAvulsa): Record<string, unknown> {
  return {
    versao: 1,
    modelo: dados.modelo,
    clienteId: dados.clienteId,
    documento: dados.documento,
    itens: dados.itens.map((item) => ({
      produtoId: item.produtoId,
      descricao: item.descricao,
      ncm: item.ncm,
      unidade: item.unidade,
      quantidade: item.quantidade,
      valorUnitario: item.valorUnitario,
    })),
    desconto: dados.desconto,
    frete: dados.frete,
    pagamento: dados.pagamento,
    vencimento: dados.vencimento,
    perfilId: dados.perfilId,
    consumidorFinal: dados.consumidorFinal,
    presenca: dados.presenca,
    observacao: dados.observacao,
    enviarEmail: dados.enviarEmail,
  };
}

/** As linhas da nota no formato que o planejamento usa. Produto de catálogo leva NCM, CEST e tributação dele. */
export function itensDaNotaAvulsa(dados: NotaAvulsa, produtos: Map<string, ProdutoDaNotaAvulsa>): ItemDaNota[] {
  const tipo: "mercadoria" | "servico" = dados.modelo === "nfse" ? "servico" : "mercadoria";
  return dados.itens.map((item) => {
    const produto = item.produtoId ? produtos.get(item.produtoId) : undefined;
    const origem = produto?.fiscal_origem;
    const perfilDoProduto = produto && produto.fiscal_tipo !== "servico" ? texto(produto.fiscal_perfil_id) : "";
    return {
      id: `avulsa-${item.posicao + 1}`,
      titulo: item.descricao,
      produtoId: produto ? String(produto.id) : null,
      sku: texto(produto?.sku) || null,
      codigoDeBarras: texto(produto?.barcode) || null,
      unitType: null,
      quantidade: item.quantidade,
      larguraM: null,
      alturaM: null,
      comprimentoM: null,
      valorTotal: duasCasas(item.quantidade * item.valorUnitario),
      opcoes: [],
      fiscalTipo: tipo,
      ncm: tipo === "mercadoria" ? item.ncm ?? (soDigitos(produto?.ncm) || null) : null,
      cest: tipo === "mercadoria" ? soDigitos(produto?.cest) || null : null,
      origem: tipo === "mercadoria" && origem !== null && origem !== undefined ? Number(origem) : null,
      perfilId: tipo === "mercadoria" ? perfilDoProduto || dados.perfilId : dados.perfilId,
      unidadeFixa: tipo === "mercadoria" ? item.unidade : "UN",
    };
  });
}

/** O "pedido em memória": total, pagamento à vista ou em aberto, e o vencimento informado. */
export function pedidoDaNotaAvulsa(dados: NotaAvulsa, itens: ItemDaNota[], agora = new Date()): PedidoDaNota {
  const subtotal = duasCasas(itens.reduce((acc, item) => acc + item.valorTotal, 0));
  const total = duasCasas(subtotal - dados.desconto + dados.frete);
  const forma = FORMAS_DA_NOTA_AVULSA[dados.pagamento];
  return {
    id: "avulsa",
    numero: null,
    total,
    criadoEm: agora.toISOString(),
    entregaEm: null,
    itens,
    freteDoOrcamento: dados.frete,
    descontoDoOrcamento: dados.desconto,
    temEntrega: dados.frete > 0,
    pagamentos: forma.pago && forma.metodo && total > 0 ? [{ metodo: forma.metodo, valor: total }] : [],
    condicoes: null,
    formaEmAberto: forma.pago ? null : forma.metodo,
    duplicatas: !forma.pago && dados.vencimento && total > 0 ? [{ vencimento: dados.vencimento, valor: total }] : null,
  };
}

/** A falta de NCM aponta a linha da tela, que é onde se corrige na nota sem pedido. */
export function pendenciaNaLinha(dados: NotaAvulsa) {
  return (pendencia: Pendencia): Pendencia => {
    if (!pendencia.codigo.startsWith("produto.ncm:")) return pendencia;
    const chave = pendencia.codigo.slice("produto.ncm:".length);
    const item = chave.startsWith("avulsa-")
      ? dados.itens.find((linha) => linha.posicao === Number(chave.slice("avulsa-".length)) - 1)
      : dados.itens.find((linha) => linha.produtoId === chave);
    if (!item) return pendencia;
    return {
      ...pendencia,
      onde: "pedido",
      titulo: `Linha ${item.posicao + 1}: ${item.ncm ? "o NCM tem 8 números" : "falta o NCM"}`,
      detalhe: "É o código fiscal do produto. Busque pela descrição no campo NCM da linha.",
      alvoId: String(item.posicao),
    };
  };
}
