/**
 * Campos próprios: os campos a mais que a empresa cria para cliente,
 * orçamento e pedido ("Aniversário", "Quem indicou", "Nome do evento").
 *
 * A definição mora na tabela `campos_proprios` (migration 187). O valor mora
 * na própria linha (clients/quotes/orders), na coluna jsonb `campos_proprios`,
 * guardado pela `chave` do campo: { "aniversario": "1990-05-12" }.
 *
 * Aqui ficam só as regras puras, sem React e sem banco: o formulário do
 * painel, a tela de ajustes, as ferramentas do assistente e (depois) as
 * regras automáticas leem e conferem os valores pelo mesmo caminho. O teste
 * ao lado confere cada tipo.
 *
 * Como cada valor é guardado:
 *   texto, texto_longo, opcoes -> texto
 *   numero, moeda              -> número (moeda com 2 casas)
 *   data                       -> "AAAA-MM-DD"
 *   sim_nao                    -> true / false
 * Valor vazio não é guardado: a chave sai do objeto.
 */

export const ENTIDADES = ["cliente", "orcamento", "pedido"] as const;
export type EntidadeDoCampo = (typeof ENTIDADES)[number];

export const TIPOS = ["texto", "texto_longo", "numero", "moeda", "data", "sim_nao", "opcoes"] as const;
export type TipoDoCampo = (typeof TIPOS)[number];

export const ROTULO_DA_ENTIDADE: Record<EntidadeDoCampo, string> = {
  cliente: "Cliente",
  orcamento: "Orçamento",
  pedido: "Pedido",
};

export const ROTULO_DO_TIPO: Record<TipoDoCampo, string> = {
  texto: "Texto curto",
  texto_longo: "Texto longo",
  numero: "Número",
  moeda: "Valor em reais",
  data: "Data",
  sim_nao: "Sim ou não",
  opcoes: "Lista de opções",
};

/** A tabela onde cada entidade guarda os valores. */
export const TABELA_DA_ENTIDADE: Record<EntidadeDoCampo, "clients" | "quotes" | "orders"> = {
  cliente: "clients",
  orcamento: "quotes",
  pedido: "orders",
};

export const LIMITES = {
  rotulo: 80,
  ajuda: 200,
  chave: 50,
  opcoes: 50,
  opcao: 80,
  texto: 500,
  textoLongo: 4000,
  camposPorEntidade: 40,
} as const;

export type ValorDeCampo = string | number | boolean;
export type ValoresDosCampos = Record<string, ValorDeCampo>;

export type DefinicaoDeCampo = {
  id: string;
  entidade: EntidadeDoCampo;
  chave: string;
  rotulo: string;
  tipo: TipoDoCampo;
  opcoes: string[];
  obrigatorio: boolean;
  posicao: number;
  ativo: boolean;
  ajuda: string | null;
};

/** As colunas que as telas e o assistente leem de `campos_proprios`. */
export const COLUNAS_DA_DEFINICAO = "id,entidade,chave,rotulo,tipo,opcoes,obrigatorio,posicao,ativo,ajuda";

export function ehEntidade(valor: unknown): valor is EntidadeDoCampo {
  return typeof valor === "string" && (ENTIDADES as readonly string[]).includes(valor);
}

export function ehTipo(valor: unknown): valor is TipoDoCampo {
  return typeof valor === "string" && (TIPOS as readonly string[]).includes(valor);
}

function ehObjeto(valor: unknown): valor is Record<string, unknown> {
  return Boolean(valor) && typeof valor === "object" && !Array.isArray(valor);
}

/* ---------------------------------------------------------------- definições */

/** Lista de opções limpa: sem repetida, sem vazia, sem espaço sobrando. */
export function limparOpcoes(bruto: unknown): string[] {
  const lista = Array.isArray(bruto) ? bruto : typeof bruto === "string" ? bruto.split(/\n|;/) : [];
  const vistas = new Set<string>();
  const opcoes: string[] = [];
  for (const item of lista) {
    if (typeof item !== "string" && typeof item !== "number") continue;
    const opcao = String(item).replace(/\s+/g, " ").trim().slice(0, LIMITES.opcao);
    if (!opcao) continue;
    const comparavel = semAcento(opcao).toLowerCase();
    if (vistas.has(comparavel)) continue;
    vistas.add(comparavel);
    opcoes.push(opcao);
    if (opcoes.length >= LIMITES.opcoes) break;
  }
  return opcoes;
}

/** Uma linha do banco vira uma definição confiável, ou null se não serve. */
export function normalizarDefinicao(bruto: unknown): DefinicaoDeCampo | null {
  if (!ehObjeto(bruto)) return null;
  const { id, entidade, chave, rotulo, tipo } = bruto;
  if (typeof id !== "string" || !ehEntidade(entidade) || !ehTipo(tipo)) return null;
  if (typeof chave !== "string" || !chaveValida(chave)) return null;
  const nome = typeof rotulo === "string" ? rotulo.trim() : "";
  if (!nome) return null;
  const posicao = Number(bruto.posicao);
  const ajuda = typeof bruto.ajuda === "string" && bruto.ajuda.trim() ? bruto.ajuda.trim() : null;
  return {
    id,
    entidade,
    chave,
    rotulo: nome,
    tipo,
    opcoes: tipo === "opcoes" ? limparOpcoes(bruto.opcoes) : [],
    obrigatorio: bruto.obrigatorio === true,
    posicao: Number.isFinite(posicao) ? posicao : 0,
    ativo: bruto.ativo !== false,
    ajuda,
  };
}

/** As definições na ordem da tela: pela posição, e pelo nome no empate. */
export function normalizarDefinicoes(linhas: unknown, entidade?: EntidadeDoCampo): DefinicaoDeCampo[] {
  if (!Array.isArray(linhas)) return [];
  return linhas
    .map(normalizarDefinicao)
    .filter((def): def is DefinicaoDeCampo => def !== null && (!entidade || def.entidade === entidade))
    .sort((a, b) => a.posicao - b.posicao || a.rotulo.localeCompare(b.rotulo, "pt-BR"));
}

export function semAcento(texto: string) {
  return texto.normalize("NFD").replace(/[̀-ͯ]/g, "");
}

export function chaveValida(chave: string) {
  return /^[a-z][a-z0-9_]{0,49}$/.test(chave);
}

/** "Quem indicou?" vira "quem_indicou". Começa sempre por letra. */
export function chaveDoRotulo(rotulo: string): string {
  let chave = semAcento(String(rotulo ?? ""))
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");
  if (!chave) chave = "campo";
  if (!/^[a-z]/.test(chave)) chave = `campo_${chave}`;
  return chave.slice(0, LIMITES.chave).replace(/_+$/, "");
}

/** A chave do rótulo, com _2, _3… se já existir uma igual na mesma entidade. */
export function chaveUnica(rotulo: string, existentes: Iterable<string>): string {
  const usadas = new Set(existentes);
  const base = chaveDoRotulo(rotulo);
  if (!usadas.has(base)) return base;
  for (let n = 2; n < 1000; n += 1) {
    const sufixo = `_${n}`;
    const candidata = `${base.slice(0, LIMITES.chave - sufixo.length)}${sufixo}`;
    if (!usadas.has(candidata)) return candidata;
  }
  return `${base.slice(0, 40)}_${Date.now().toString(36).slice(-8)}`;
}

export type DefinicaoNova = {
  rotulo: string;
  tipo: TipoDoCampo;
  opcoes: string[];
  obrigatorio: boolean;
  ajuda: string | null;
};

/** Confere o que a pessoa (ou o assistente) mandou para criar ou alterar um campo. */
export function conferirDefinicao(bruto: {
  rotulo?: unknown;
  tipo?: unknown;
  opcoes?: unknown;
  obrigatorio?: unknown;
  ajuda?: unknown;
}): { ok: true; definicao: DefinicaoNova } | { ok: false; erro: string } {
  const rotulo = typeof bruto.rotulo === "string" ? bruto.rotulo.replace(/\s+/g, " ").trim() : "";
  if (!rotulo) return { ok: false, erro: "Dê um nome ao campo." };
  if (rotulo.length > LIMITES.rotulo) return { ok: false, erro: `O nome do campo pode ter até ${LIMITES.rotulo} letras.` };
  if (!ehTipo(bruto.tipo)) return { ok: false, erro: "Escolha o tipo do campo." };
  const opcoes = bruto.tipo === "opcoes" ? limparOpcoes(bruto.opcoes) : [];
  if (bruto.tipo === "opcoes" && opcoes.length < 2) return { ok: false, erro: "Uma lista de opções precisa de pelo menos 2 opções." };
  const ajuda = typeof bruto.ajuda === "string" ? bruto.ajuda.trim() : "";
  if (ajuda.length > LIMITES.ajuda) return { ok: false, erro: `A explicação pode ter até ${LIMITES.ajuda} letras.` };
  return {
    ok: true,
    definicao: { rotulo, tipo: bruto.tipo, opcoes, obrigatorio: bruto.obrigatorio === true, ajuda: ajuda || null },
  };
}

/* -------------------------------------------------------------------- valores */

/** O objeto de valores que veio do banco, sem nada estranho dentro. */
export function valoresDaLinha(bruto: unknown): ValoresDosCampos {
  if (!ehObjeto(bruto)) return {};
  const valores: ValoresDosCampos = {};
  for (const [chave, valor] of Object.entries(bruto)) {
    if (!chaveValida(chave)) continue;
    if (typeof valor === "string" || typeof valor === "boolean" || (typeof valor === "number" && Number.isFinite(valor))) {
      valores[chave] = valor;
    }
  }
  return valores;
}

export function valorVazio(valor: unknown) {
  return valor === undefined || valor === null || (typeof valor === "string" && valor.trim() === "");
}

/**
 * Lê um número escrito por gente: 10, "10,5", "1.234,56", "R$ 12,50".
 * Mais rígido que o campo decimal: o que não é número claro volta null.
 */
export function numeroEscrito(bruto: unknown): number | null {
  if (typeof bruto === "number") return Number.isFinite(bruto) ? bruto : null;
  if (typeof bruto !== "string") return null;
  const t = bruto.replace(/R\$/gi, "").replace(/\s/g, "");
  if (!t) return null;
  let normal: string;
  if (/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(t)) normal = t.replace(/\./g, "").replace(",", ".");
  else if (/^-?\d*([.,]\d+)?$/.test(t) && /\d/.test(t)) normal = t.replace(",", ".");
  else return null;
  const n = Number(normal);
  return Number.isFinite(n) ? n : null;
}

function dataReal(ano: number, mes: number, dia: number) {
  const data = new Date(Date.UTC(ano, mes - 1, dia));
  return data.getUTCFullYear() === ano && data.getUTCMonth() === mes - 1 && data.getUTCDate() === dia;
}

/** "2026-10-14", "14/10/2026" ou "14-10-2026" viram "2026-10-14". */
export function dataEscrita(bruto: unknown): string | null {
  if (typeof bruto !== "string") return null;
  const t = bruto.trim();
  let m = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/.exec(t);
  if (m) {
    const [ano, mes, dia] = [Number(m[1]), Number(m[2]), Number(m[3])];
    return dataReal(ano, mes, dia) ? `${m[1]}-${m[2]}-${m[3]}` : null;
  }
  m = /^(\d{1,2})[/.-](\d{1,2})[/.-](\d{4})$/.exec(t);
  if (m) {
    const [dia, mes, ano] = [Number(m[1]), Number(m[2]), Number(m[3])];
    if (!dataReal(ano, mes, dia)) return null;
    return `${m[3]}-${String(mes).padStart(2, "0")}-${String(dia).padStart(2, "0")}`;
  }
  return null;
}

const SIM = new Set(["true", "sim", "s", "1", "yes", "verdadeiro"]);
const NAO = new Set(["false", "nao", "n", "0", "no", "falso"]);

export type ValorLido = { ok: true; valor: ValorDeCampo | null } | { ok: false; erro: string };

/** Lê o valor de um campo pelo tipo dele. Vazio volta `null` (sem erro). */
export function lerValor(def: Pick<DefinicaoDeCampo, "rotulo" | "tipo" | "opcoes">, bruto: unknown): ValorLido {
  if (valorVazio(bruto)) return { ok: true, valor: null };
  const nome = `"${def.rotulo}"`;
  switch (def.tipo) {
    case "texto":
    case "texto_longo": {
      if (typeof bruto !== "string" && typeof bruto !== "number") return { ok: false, erro: `${nome} precisa ser um texto.` };
      const limite = def.tipo === "texto" ? LIMITES.texto : LIMITES.textoLongo;
      const texto = def.tipo === "texto" ? String(bruto).replace(/\s+/g, " ").trim() : String(bruto).trim();
      if (texto.length > limite) return { ok: false, erro: `${nome} pode ter até ${limite} letras.` };
      return { ok: true, valor: texto };
    }
    case "numero":
    case "moeda": {
      const numero = numeroEscrito(bruto);
      if (numero === null) return { ok: false, erro: `${nome} precisa ser um número.` };
      if (Math.abs(numero) > 1e12) return { ok: false, erro: `${nome} é grande demais.` };
      return { ok: true, valor: def.tipo === "moeda" ? Math.round(numero * 100) / 100 : numero };
    }
    case "data": {
      const data = dataEscrita(bruto);
      if (!data) return { ok: false, erro: `${nome} precisa ser uma data válida (DD/MM/AAAA).` };
      return { ok: true, valor: data };
    }
    case "sim_nao": {
      if (typeof bruto === "boolean") return { ok: true, valor: bruto };
      const texto = semAcento(String(bruto)).trim().toLowerCase();
      if (SIM.has(texto)) return { ok: true, valor: true };
      if (NAO.has(texto)) return { ok: true, valor: false };
      return { ok: false, erro: `${nome} precisa ser sim ou não.` };
    }
    case "opcoes": {
      const procurada = semAcento(String(bruto)).replace(/\s+/g, " ").trim().toLowerCase();
      const achada = def.opcoes.find((opcao) => semAcento(opcao).toLowerCase() === procurada);
      if (!achada) return { ok: false, erro: `${nome} precisa ser uma destas opções: ${def.opcoes.join(", ")}.` };
      return { ok: true, valor: achada };
    }
  }
}

export type ResultadoDosValores = {
  ok: boolean;
  /** O objeto pronto para gravar: os valores antigos mantidos, os novos por cima. */
  valores: ValoresDosCampos;
  /** chave -> mensagem, para mostrar ao lado do campo. */
  erros: Record<string, string>;
};

/**
 * Confere os valores novos contra as definições e junta com os que já estavam
 * na linha.
 *
 * - Só os campos ativos são conferidos e podem ser mudados.
 * - O valor de um campo desativado (ou apagado) que já estava gravado fica
 *   onde está: desativar não apaga o que a equipe preencheu.
 * - Chave sem campo é recusada (`erros`), para o assistente se corrigir.
 * - Valor vazio tira a chave do objeto.
 * - `exigirObrigatorios`: campo obrigatório sem valor (nem novo, nem antigo) é erro.
 */
export function conferirValores(
  definicoes: DefinicaoDeCampo[],
  novos: Record<string, unknown>,
  { atuais = {}, exigirObrigatorios = false }: { atuais?: ValoresDosCampos; exigirObrigatorios?: boolean } = {}
): ResultadoDosValores {
  const ativos = new Map(definicoes.filter((def) => def.ativo).map((def) => [def.chave, def]));
  const valores: ValoresDosCampos = { ...valoresDaLinha(atuais) };
  const erros: Record<string, string> = {};

  for (const [chave, bruto] of Object.entries(novos ?? {})) {
    const def = ativos.get(chave);
    if (!def) {
      erros[chave] = `Não existe campo ativo com a chave "${chave}".`;
      continue;
    }
    const lido = lerValor(def, bruto);
    if (!lido.ok) {
      erros[chave] = lido.erro;
      continue;
    }
    if (lido.valor === null) delete valores[chave];
    else valores[chave] = lido.valor;
  }

  if (exigirObrigatorios) {
    for (const def of ativos.values()) {
      if (def.obrigatorio && valorVazio(valores[def.chave]) && !erros[def.chave]) {
        erros[def.chave] = `Preencha "${def.rotulo}".`;
      }
    }
  }

  return { ok: Object.keys(erros).length === 0, valores, erros };
}

/* ------------------------------------------------------------------- leitura */

export function dataBrDoCampo(iso: string) {
  const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(iso);
  return m ? `${m[3]}/${m[2]}/${m[1]}` : iso;
}

/** O valor como a pessoa lê: 14/10/2026, R$ 12,50, Sim. Vazio vira "". */
export function formatarValor(def: Pick<DefinicaoDeCampo, "tipo">, valor: unknown): string {
  if (valorVazio(valor)) return "";
  switch (def.tipo) {
    case "data":
      return typeof valor === "string" ? dataBrDoCampo(valor) : String(valor);
    case "moeda": {
      const n = typeof valor === "number" ? valor : numeroEscrito(valor);
      return n === null ? String(valor) : n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
    }
    case "numero": {
      const n = typeof valor === "number" ? valor : numeroEscrito(valor);
      return n === null ? String(valor) : n.toLocaleString("pt-BR", { maximumFractionDigits: 6 });
    }
    case "sim_nao":
      if (valor === true) return "Sim";
      if (valor === false) return "Não";
      return String(valor);
    default:
      return String(valor);
  }
}

export type CampoParaLer = { chave: string; campo: string; tipo: TipoDoCampo; valor: ValorDeCampo | null; texto: string };

/**
 * Os valores de uma linha, na ordem dos campos, para mostrar ou mandar ao
 * assistente. Campo desativado só aparece se tiver valor gravado.
 */
export function camposParaLer(definicoes: DefinicaoDeCampo[], bruto: unknown, { incluirVazios = false } = {}): CampoParaLer[] {
  const valores = valoresDaLinha(bruto);
  const lista: CampoParaLer[] = [];
  for (const def of definicoes) {
    const valor = valores[def.chave];
    const vazio = valorVazio(valor);
    if (vazio && (!incluirVazios || !def.ativo)) continue;
    lista.push({ chave: def.chave, campo: def.rotulo, tipo: def.tipo, valor: vazio ? null : valor, texto: formatarValor(def, valor) });
  }
  return lista;
}

/**
 * Na aprovação do orçamento: os valores que passam para o pedido. Só passa o
 * que tem um campo de pedido ativo com a mesma chave e o valor serve no tipo
 * dele; o que o pedido já tiver não é trocado.
 */
export function valoresQuePassamParaOPedido(
  definicoesDoPedido: DefinicaoDeCampo[],
  valoresDoOrcamento: unknown,
  valoresDoPedido: unknown = {}
): ValoresDosCampos {
  const doOrcamento = valoresDaLinha(valoresDoOrcamento);
  const resultado: ValoresDosCampos = { ...valoresDaLinha(valoresDoPedido) };
  for (const def of definicoesDoPedido) {
    if (!def.ativo) continue;
    if (!valorVazio(resultado[def.chave])) continue;
    const bruto = doOrcamento[def.chave];
    if (valorVazio(bruto)) continue;
    const lido = lerValor(def, bruto);
    if (lido.ok && lido.valor !== null) resultado[def.chave] = lido.valor;
  }
  return resultado;
}

/* --------------------------------------------------------------------- busca */

export type FiltroDoCampo = {
  /** Igual ao valor (texto sem diferença de maiúscula e acento). */
  igual?: unknown;
  /** O texto contém este trecho. */
  contem?: string;
  /** Número/valor/data a partir de (inclusive). */
  de?: unknown;
  /** Número/valor/data até (inclusive). */
  ate?: unknown;
  /** Só para data: o mês (1 a 12), de qualquer ano. É o "aniversariantes do mês". */
  mes?: number;
  /** Só para data, junto com `mes`: o dia do mês. */
  dia?: number;
  /** true = só quem tem o campo preenchido; false = só quem não tem. */
  preenchido?: boolean;
};

const comparavel = (texto: unknown) => semAcento(String(texto ?? "")).toLowerCase().replace(/\s+/g, " ").trim();

/** O filtro conferido para o tipo do campo, ou o motivo de não servir. */
export function conferirFiltro(def: DefinicaoDeCampo, filtro: FiltroDoCampo): { ok: true; filtro: FiltroDoCampo } | { ok: false; erro: string } {
  const limpo: FiltroDoCampo = {};
  if (filtro.preenchido !== undefined) limpo.preenchido = filtro.preenchido;
  if (!valorVazio(filtro.igual)) {
    const lido = lerValor(def, filtro.igual);
    if (!lido.ok) return lido;
    limpo.igual = lido.valor;
  }
  if (!valorVazio(filtro.contem)) {
    if (!["texto", "texto_longo", "opcoes"].includes(def.tipo)) return { ok: false, erro: `"contém" só vale para campo de texto; "${def.rotulo}" é ${ROTULO_DO_TIPO[def.tipo].toLowerCase()}.` };
    limpo.contem = String(filtro.contem);
  }
  for (const ponta of ["de", "ate"] as const) {
    if (valorVazio(filtro[ponta])) continue;
    if (!["numero", "moeda", "data"].includes(def.tipo)) return { ok: false, erro: `Faixa (de/até) só vale para número, valor ou data; "${def.rotulo}" é ${ROTULO_DO_TIPO[def.tipo].toLowerCase()}.` };
    const lido = lerValor(def, filtro[ponta]);
    if (!lido.ok) return lido;
    limpo[ponta] = lido.valor;
  }
  if (filtro.mes !== undefined || filtro.dia !== undefined) {
    if (def.tipo !== "data") return { ok: false, erro: `Mês e dia só valem para campo de data; "${def.rotulo}" é ${ROTULO_DO_TIPO[def.tipo].toLowerCase()}.` };
    if (filtro.mes !== undefined && !(Number.isInteger(filtro.mes) && filtro.mes >= 1 && filtro.mes <= 12)) return { ok: false, erro: "O mês vai de 1 a 12." };
    if (filtro.dia !== undefined && !(Number.isInteger(filtro.dia) && filtro.dia >= 1 && filtro.dia <= 31)) return { ok: false, erro: "O dia vai de 1 a 31." };
    if (filtro.mes !== undefined) limpo.mes = filtro.mes;
    if (filtro.dia !== undefined) limpo.dia = filtro.dia;
  }
  return { ok: true, filtro: limpo };
}

/** O valor gravado combina com o filtro (já conferido)? */
export function valorCombina(def: DefinicaoDeCampo, bruto: unknown, filtro: FiltroDoCampo): boolean {
  const vazio = valorVazio(bruto);
  if (filtro.preenchido === true && vazio) return false;
  if (filtro.preenchido === false) return vazio;
  const temCondicao = ["igual", "contem", "de", "ate", "mes", "dia"].some((k) => filtro[k as keyof FiltroDoCampo] !== undefined);
  if (!temCondicao) return !vazio;
  if (vazio) return false;

  const lido = lerValor(def, bruto);
  if (!lido.ok || lido.valor === null) return false;
  const valor = lido.valor;

  if (filtro.igual !== undefined) {
    if (typeof valor === "string") {
      if (comparavel(valor) !== comparavel(filtro.igual)) return false;
    } else if (valor !== filtro.igual) return false;
  }
  if (filtro.contem !== undefined && !comparavel(valor).includes(comparavel(filtro.contem))) return false;
  if (filtro.de !== undefined && filtro.de !== null && (valor as number | string) < (filtro.de as number | string)) return false;
  if (filtro.ate !== undefined && filtro.ate !== null && (valor as number | string) > (filtro.ate as number | string)) return false;
  if (filtro.mes !== undefined || filtro.dia !== undefined) {
    const m = /^\d{4}-(\d{2})-(\d{2})$/.exec(String(valor));
    if (!m) return false;
    if (filtro.mes !== undefined && Number(m[1]) !== filtro.mes) return false;
    if (filtro.dia !== undefined && Number(m[2]) !== filtro.dia) return false;
  }
  return true;
}
