import { createHash, createHmac, timingSafeEqual } from "node:crypto";

/**
 * O código de confirmação das escritas.
 *
 * Todo comando que grava tem dois tempos. No primeiro, o servidor mostra o que
 * vai fazer e devolve um código. No segundo, o assistente manda os MESMOS
 * dados com o código, e só aí a gravação acontece.
 *
 * O código é uma assinatura feita com um segredo do servidor, e ela amarra
 * três coisas sem guardar nada no banco:
 *
 * - os dados enviados: mudou um preço no pedido, o código não bate;
 * - a prévia que foi mostrada: o código carrega a marca dela. Se entre a
 *   prévia e a confirmação o preço do cadastro mudou, a prévia de agora tem
 *   outra marca, e quem confere percebe que gravaria algo diferente do que a
 *   pessoa viu;
 * - a passagem pela prévia: ninguém grava sem ela, porque só ela faz o código.
 *
 * O que o código não garante é que uma pessoa leu a prévia. Isso é do
 * assistente, que pede aprovação antes de cada comando, e por isso a prévia
 * manda mostrar o resumo a quem está usando.
 */

export const CONFIRMACAO_VALE_POR_MS = 10 * 60 * 1000;

/** JSON com as chaves em ordem: o mesmo dado vira sempre o mesmo texto. */
export function canonico(valor: unknown): string {
  if (valor === undefined || valor === null) return "null";
  if (typeof valor !== "object") return JSON.stringify(valor);
  if (Array.isArray(valor)) return `[${valor.map(canonico).join(",")}]`;
  const objeto = valor as Record<string, unknown>;
  const partes = Object.keys(objeto)
    .filter((chave) => objeto[chave] !== undefined)
    .sort()
    .map((chave) => `${JSON.stringify(chave)}:${canonico(objeto[chave])}`);
  return `{${partes.join(",")}}`;
}

/** A marca de uma prévia: muda se qualquer coisa mostrada à pessoa mudar. */
export function marcaDaPrevia(previa: unknown): string {
  return createHash("sha256").update(canonico(previa), "utf8").digest("base64url").slice(0, 16);
}

function assinar(segredo: string, partes: string[]) {
  return createHmac("sha256", segredo).update(partes.join("\n"), "utf8").digest("base64url").slice(0, 32);
}

type Alvo = { segredo: string; chaveId: string; ferramenta: string; argumentos: unknown };

/** O código tem três partes separadas por ponto: validade, marca da prévia e assinatura. */
export function emitirCodigo(alvo: Alvo & { previa: unknown; agora?: number; valePorMs?: number }) {
  const venceEmSegundos = Math.floor(((alvo.agora ?? Date.now()) + (alvo.valePorMs ?? CONFIRMACAO_VALE_POR_MS)) / 1000);
  const validade = venceEmSegundos.toString(36);
  const marca = marcaDaPrevia(alvo.previa);
  const codigo = `${validade}.${marca}.${assinar(alvo.segredo, [alvo.chaveId, alvo.ferramenta, validade, marca, canonico(alvo.argumentos)])}`;
  return { codigo, valeAte: new Date(venceEmSegundos * 1000).toISOString() };
}

export type CodigoConferido =
  | { veredito: "invalido" }
  | {
      veredito: "ok" | "vencido";
      /* A marca da prévia que este código confirma. */
      marca: string;
      /* Até quando o código vale, em milissegundos. */
      venceEm: number;
    };

/**
 * Confere a assinatura e o prazo. Código "ok" ainda precisa de uma conferência
 * de quem chama: a marca da prévia de agora tem de ser a mesma do código.
 */
export function conferirCodigo(alvo: Alvo & { codigo: string; agora?: number }): CodigoConferido {
  const [validade, marca, assinatura, sobra] = String(alvo.codigo || "").trim().split(".");
  if (!validade || !marca || !assinatura || sobra !== undefined) return { veredito: "invalido" };
  const venceEm = Number.parseInt(validade, 36) * 1000;
  if (!Number.isFinite(venceEm)) return { veredito: "invalido" };

  const esperada = Buffer.from(assinar(alvo.segredo, [alvo.chaveId, alvo.ferramenta, validade, marca, canonico(alvo.argumentos)]));
  const recebida = Buffer.from(assinatura);
  if (esperada.length !== recebida.length || !timingSafeEqual(esperada, recebida)) return { veredito: "invalido" };

  return { veredito: venceEm <= (alvo.agora ?? Date.now()) ? "vencido" : "ok", marca, venceEm };
}

/**
 * O caminho de uma escrita. Quem manda código vai para a conferência (mesmo
 * que o sensível só tenha aparecido no resultado da primeira chamada). Sem
 * código, a prévia sai quando a empresa escolheu "sempre", quando os dados já
 * denunciam o sensível, ou quando o preparo descobriu que o RESULTADO é
 * sensível (um cupom que fica grande, um pedido que vai para entregue). O
 * resto grava direto.
 */
export function caminhoDaEscrita(situacao: {
  sempre: boolean;
  sensivelPelosDados: boolean;
  temCodigo: boolean;
  sensivelPeloResultado?: boolean;
}): "conferir" | "previa" | "gravar" {
  if (situacao.temCodigo) return "conferir";
  if (situacao.sempre || situacao.sensivelPelosDados || situacao.sensivelPeloResultado === true) return "previa";
  return "gravar";
}
