/**
 * Os campos que cada comando do assistente aceita.
 *
 * Uma descrição só serve para duas coisas: virar o JSON Schema que o assistente
 * lê para saber o que mandar, e conferir o que ele mandou de fato. Escritas em
 * separado, as duas envelhecem tortas: o esquema promete um campo que a
 * conferência não aceita, e o assistente fica tentando até desistir.
 *
 * A conferência é rígida de propósito. Campo com nome errado é recusado com a
 * lista dos nomes certos, porque é assim que um assistente se corrige sozinho
 * na tentativa seguinte, em vez de seguir achando que mandou o cliente.
 */

type Base = { descricao: string; obrigatorio?: boolean };

export type Campo =
  | (Base & { tipo: "texto"; min?: number; max?: number })
  | (Base & { tipo: "inteiro" | "numero"; min?: number; max?: number })
  | (Base & { tipo: "booleano" })
  | (Base & { tipo: "opcao"; opcoes: readonly string[] })
  | (Base & { tipo: "data" })
  | (Base & { tipo: "hora" })
  | (Base & { tipo: "id" })
  | (Base & { tipo: "lista"; itens: Campos; minItens?: number; maxItens?: number })
  /* Um grupo de campos com nome (as condições de pagamento, a arte do item):
     { "formas": [...], "esquema": "..." }. Lido com as mesmas regras de fora. */
  | (Base & { tipo: "objeto"; campos: Campos })
  /* Lista de textos soltos (endereços, nomes): ["a", "b"]. `max` vale para cada texto. */
  | (Base & { tipo: "textos"; max?: number; minItens?: number; maxItens?: number })
  /* JSON livre (o documento de uma arte, as operações do editor): quem confere é
     a ferramenta, com a mesma limpeza que a tela usa. `max` = tamanho máximo em letras do JSON. */
  | (Base & { tipo: "livre"; max?: number });

export type Campos = Record<string, Campo>;

export type Lido = { ok: true; valor: Record<string, unknown> } | { ok: false; erro: string };

const SO_DATA = /^\d{4}-\d{2}-\d{2}$/;
const SO_HORA = /^([01]\d|2[0-3]):[0-5]\d$/;
const SO_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const TEXTO_MAXIMO = 4000;

function propriedade(campo: Campo): Record<string, unknown> {
  switch (campo.tipo) {
    case "texto":
      return {
        type: "string",
        description: campo.descricao,
        ...(campo.min ? { minLength: campo.min } : {}),
        maxLength: campo.max ?? TEXTO_MAXIMO,
      };
    case "inteiro":
    case "numero":
      return {
        type: campo.tipo === "inteiro" ? "integer" : "number",
        description: campo.descricao,
        ...(campo.min !== undefined ? { minimum: campo.min } : {}),
        ...(campo.max !== undefined ? { maximum: campo.max } : {}),
      };
    case "booleano":
      return { type: "boolean", description: campo.descricao };
    case "opcao":
      return { type: "string", description: campo.descricao, enum: [...campo.opcoes] };
    case "data":
      return { type: "string", description: `${campo.descricao} Formato AAAA-MM-DD.`, pattern: "^\\d{4}-\\d{2}-\\d{2}$" };
    case "hora":
      return { type: "string", description: `${campo.descricao} Formato HH:MM, 24 horas.`, pattern: "^([01]\\d|2[0-3]):[0-5]\\d$" };
    case "id":
      return { type: "string", description: campo.descricao, format: "uuid" };
    case "lista":
      return {
        type: "array",
        description: campo.descricao,
        items: paraJsonSchema(campo.itens),
        ...(campo.minItens ? { minItems: campo.minItens } : {}),
        ...(campo.maxItens ? { maxItems: campo.maxItens } : {}),
      };
    case "objeto":
      return { ...paraJsonSchema(campo.campos), description: campo.descricao };
    case "livre":
      return { type: ["object", "array"], description: campo.descricao };
    case "textos":
      return {
        type: "array",
        description: campo.descricao,
        items: { type: "string", maxLength: campo.max ?? TEXTO_MAXIMO },
        ...(campo.minItens ? { minItems: campo.minItens } : {}),
        ...(campo.maxItens ? { maxItems: campo.maxItens } : {}),
      };
  }
}

/** O esquema que o assistente recebe em `tools/list`. */
export function paraJsonSchema(campos: Campos): Record<string, unknown> {
  const properties: Record<string, unknown> = {};
  const required: string[] = [];
  for (const [nome, campo] of Object.entries(campos)) {
    properties[nome] = propriedade(campo);
    if (campo.obrigatorio) required.push(nome);
  }
  return {
    type: "object",
    properties,
    ...(required.length ? { required } : {}),
    additionalProperties: false,
  };
}

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

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

function numeroDe(valor: unknown): number | null {
  if (typeof valor === "number") return Number.isFinite(valor) ? valor : null;
  if (typeof valor !== "string") return null;
  /* O assistente às vezes manda "10,5" porque leu o preço num texto brasileiro. */
  const limpo = valor.trim().replace(/\s/g, "");
  if (!/^-?\d+([.,]\d+)?$/.test(limpo)) return null;
  const numero = Number(limpo.replace(",", "."));
  return Number.isFinite(numero) ? numero : null;
}

function dataReal(iso: string) {
  if (!SO_DATA.test(iso)) return false;
  const [ano, mes, dia] = iso.split("-").map(Number);
  const data = new Date(Date.UTC(ano, mes - 1, dia));
  return data.getUTCFullYear() === ano && data.getUTCMonth() === mes - 1 && data.getUTCDate() === dia;
}

function lerCampo(nome: string, campo: Campo, bruto: unknown): { ok: true; valor: unknown } | { ok: false; erro: string } {
  switch (campo.tipo) {
    case "texto": {
      if (typeof bruto !== "string" && typeof bruto !== "number") return { ok: false, erro: `"${nome}" precisa ser um texto.` };
      const texto = String(bruto).trim();
      const maximo = campo.max ?? TEXTO_MAXIMO;
      if (campo.min && texto.length < campo.min) return { ok: false, erro: `"${nome}" precisa ter ao menos ${campo.min} letras.` };
      if (texto.length > maximo) return { ok: false, erro: `"${nome}" passou do tamanho máximo (${maximo} letras).` };
      return { ok: true, valor: texto };
    }
    case "inteiro":
    case "numero": {
      const numero = numeroDe(bruto);
      if (numero === null) return { ok: false, erro: `"${nome}" precisa ser um número.` };
      if (campo.tipo === "inteiro" && !Number.isInteger(numero)) return { ok: false, erro: `"${nome}" precisa ser um número inteiro.` };
      if (campo.min !== undefined && numero < campo.min) return { ok: false, erro: `"${nome}" não pode ser menor que ${campo.min}.` };
      if (campo.max !== undefined && numero > campo.max) return { ok: false, erro: `"${nome}" não pode ser maior que ${campo.max}.` };
      return { ok: true, valor: numero };
    }
    case "booleano": {
      if (typeof bruto === "boolean") return { ok: true, valor: bruto };
      const texto = String(bruto).trim().toLowerCase();
      if (["true", "sim", "1"].includes(texto)) return { ok: true, valor: true };
      if (["false", "nao", "não", "0"].includes(texto)) return { ok: true, valor: false };
      return { ok: false, erro: `"${nome}" precisa ser verdadeiro ou falso.` };
    }
    case "opcao": {
      const texto = String(bruto).trim().toLowerCase();
      const achada = campo.opcoes.find((opcao) => opcao.toLowerCase() === texto);
      if (!achada) return { ok: false, erro: `"${nome}" precisa ser um destes: ${campo.opcoes.join(", ")}.` };
      return { ok: true, valor: achada };
    }
    case "data": {
      const texto = String(bruto).trim().slice(0, 10);
      if (!dataReal(texto)) return { ok: false, erro: `"${nome}" precisa ser uma data no formato AAAA-MM-DD.` };
      return { ok: true, valor: texto };
    }
    case "hora": {
      const texto = String(bruto).trim().slice(0, 5);
      if (!SO_HORA.test(texto)) return { ok: false, erro: `"${nome}" precisa ser um horário no formato HH:MM.` };
      return { ok: true, valor: texto };
    }
    case "id": {
      const texto = String(bruto).trim().toLowerCase();
      if (!SO_UUID.test(texto)) return { ok: false, erro: `"${nome}" não é um identificador válido. Use o id devolvido por uma busca.` };
      return { ok: true, valor: texto };
    }
    case "lista": {
      if (!Array.isArray(bruto)) return { ok: false, erro: `"${nome}" precisa ser uma lista.` };
      if (campo.minItens && bruto.length < campo.minItens) return { ok: false, erro: `"${nome}" precisa ter ao menos ${campo.minItens} item(ns).` };
      if (campo.maxItens && bruto.length > campo.maxItens) return { ok: false, erro: `"${nome}" aceita no máximo ${campo.maxItens} itens.` };
      const itens: Record<string, unknown>[] = [];
      for (let i = 0; i < bruto.length; i += 1) {
        const lido = lerArgumentos(campo.itens, bruto[i]);
        if (!lido.ok) return { ok: false, erro: `${nome}[${i + 1}]: ${lido.erro}` };
        itens.push(lido.valor);
      }
      return { ok: true, valor: itens };
    }
    case "objeto": {
      if (!ehObjeto(bruto)) return { ok: false, erro: `"${nome}" precisa ser um objeto JSON, com os campos: ${Object.keys(campo.campos).join(", ")}.` };
      const lido = lerArgumentos(campo.campos, bruto);
      if (!lido.ok) return { ok: false, erro: `${nome}: ${lido.erro}` };
      return { ok: true, valor: lido.valor };
    }
    case "livre": {
      /* Alguns programas mandam o JSON como texto: vale também. */
      let valor = bruto;
      if (typeof valor === "string") {
        try {
          valor = JSON.parse(valor);
        } catch {
          return { ok: false, erro: `"${nome}" precisa ser um objeto ou uma lista JSON (o texto mandado não é JSON válido).` };
        }
      }
      if (!valor || typeof valor !== "object") return { ok: false, erro: `"${nome}" precisa ser um objeto ou uma lista JSON.` };
      const tamanho = JSON.stringify(valor).length;
      const maximo = campo.max ?? 200_000;
      if (tamanho > maximo) return { ok: false, erro: `"${nome}" passou do tamanho máximo (${maximo} letras de JSON; veio ${tamanho}). Mande em partes.` };
      return { ok: true, valor };
    }
    case "textos": {
      if (!Array.isArray(bruto)) return { ok: false, erro: `"${nome}" precisa ser uma lista de textos.` };
      if (campo.minItens && bruto.length < campo.minItens) return { ok: false, erro: `"${nome}" precisa ter ao menos ${campo.minItens} item(ns).` };
      if (campo.maxItens && bruto.length > campo.maxItens) return { ok: false, erro: `"${nome}" aceita no máximo ${campo.maxItens} itens.` };
      const maximo = campo.max ?? TEXTO_MAXIMO;
      const textos: string[] = [];
      for (let i = 0; i < bruto.length; i += 1) {
        if (typeof bruto[i] !== "string" && typeof bruto[i] !== "number") return { ok: false, erro: `${nome}[${i + 1}] precisa ser um texto.` };
        const texto = String(bruto[i]).trim();
        if (!texto) continue;
        if (texto.length > maximo) return { ok: false, erro: `${nome}[${i + 1}] passou do tamanho máximo (${maximo} letras).` };
        textos.push(texto);
      }
      return { ok: true, valor: textos };
    }
  }
}

/**
 * Confere e normaliza o que o assistente mandou.
 *
 * O resultado traz só os campos preenchidos, na ordem em que foram declarados
 * (é o que deixa a prévia e a confirmação de uma escrita compararem a mesma
 * coisa, mesmo que o assistente reordene o JSON entre uma e outra).
 */
export function lerArgumentos(campos: Campos, bruto: unknown): Lido {
  const entrada = bruto === undefined || bruto === null ? {} : bruto;
  if (!ehObjeto(entrada)) return { ok: false, erro: "Os argumentos precisam ser um objeto JSON." };

  const aceitos = Object.keys(campos);
  const estranho = Object.keys(entrada).find((nome) => !aceitos.includes(nome));
  if (estranho) {
    return {
      ok: false,
      erro: `Campo desconhecido: "${estranho}". ${aceitos.length ? `Campos aceitos: ${aceitos.join(", ")}.` : "Este comando não recebe campos."}`,
    };
  }

  const valor: Record<string, unknown> = {};
  for (const [nome, campo] of Object.entries(campos)) {
    const recebido = entrada[nome];
    if (vazio(recebido)) {
      if (campo.obrigatorio) return { ok: false, erro: `Falta o campo "${nome}": ${campo.descricao}` };
      continue;
    }
    const lido = lerCampo(nome, campo, recebido);
    if (!lido.ok) return lido;
    valor[nome] = lido.valor;
  }
  return { ok: true, valor };
}
