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

import { fecharComMarca, hashDaChave, temMarca } from "../chave.ts";

/**
 * Os segredos do login por botão: quanto valem e como nascem.
 *
 * São quatro, e nenhum fica guardado inteiro. No banco mora só o resumo
 * (SHA-256) de cada um, como já acontece com as chaves de acesso:
 *
 * - o código: o que volta para o programa quando a pessoa clica em "Permitir".
 *   Vale três minutos e uma troca só;
 * - o acesso: o que o programa manda em cada pedido. Tem o mesmo formato da
 *   chave de acesso (`ktl_`), e por isso passa pela mesma conferência. Vale
 *   uma hora;
 * - a renovação: o que o programa usa para pedir um acesso novo sem chamar a
 *   pessoa de novo. A cada uso ela é trocada por outra;
 * - o segredo do programa, para os programas que se registram pedindo um.
 *
 * Os três primeiros carregam a marca do servidor (ver `chave.ts`): o que
 * alguém inventa é recusado sem ir ao banco.
 *
 * Só regra, sem banco: o segredo do servidor chega por parâmetro, para o teste
 * ao lado rodar sozinho.
 */

export const CODIGO_VALE_POR_MS = 3 * 60 * 1000;
export const ACESSO_VALE_POR_MS = 60 * 60 * 1000;
/* A conexão que ninguém usa por três meses se desfaz sozinha. Cada renovação
   empurra esse prazo para a frente. */
export const CONEXAO_PARADA_VALE_POR_MS = 90 * 24 * 60 * 60 * 1000;
/* Uma renovação que acabou de ser trocada ainda é aceita por dois minutos: o
   programa pode ter pedido, a resposta ter se perdido na rede (ou no meio de
   uma atualização do servidor), e ele tentar de novo com a antiga; ou ter duas
   janelas renovando juntas. Nesse prazo a resposta é a mesma da primeira
   troca. Depois dele, renovação antiga reaparecendo é sinal de cópia, e a
   conexão inteira é desfeita.

   O prazo é curto de propósito. Quem tem uma cópia recebe o mesmo par que o
   dono enquanto estiver dentro da folga, e é a renovação fora dela que
   denuncia a cópia; uma folga longa esconderia justamente isso. */
export const FOLGA_DA_RENOVACAO_MS = 2 * 60 * 1000;
/* Acesso que acabou de nascer não é trocado de novo: quem pede renovação no
   primeiro minuto recebe o mesmo par. Há programa que renova antes de cada
   comando, e sem isto cada comando viraria uma gravação no banco. */
export const REPOUSO_DA_RENOVACAO_MS = 60 * 1000;

const PREFIXO_DO_CODIGO = "ktc_";
const PREFIXO_DO_ACESSO = "ktl_";
const PREFIXO_DA_RENOVACAO = "ktr_";

export function resumoDaFicha(valor: string) {
  return createHash("sha256").update(valor, "utf8").digest("hex");
}

export function novoCodigo(segredoDoServidor: string) {
  const valor = fecharComMarca(segredoDoServidor, PREFIXO_DO_CODIGO, randomBytes(24));
  return { valor, hash: resumoDaFicha(valor) };
}

/** A primeira renovação de uma conexão, sorteada. As seguintes saem dela, em corrente. */
export function primeiraRenovacao(segredoDoServidor: string) {
  const valor = fecharComMarca(segredoDoServidor, PREFIXO_DA_RENOVACAO, randomBytes(24));
  return { valor, hash: resumoDaFicha(valor) };
}

function derivar(segredoDoServidor: string, papel: string, renovacao: string) {
  return createHmac("sha256", segredoDoServidor).update(`${papel}:${renovacao}`, "utf8").digest().subarray(0, 24);
}

/**
 * O acesso que acompanha uma renovação.
 *
 * Não é sorteado: sai da própria renovação, misturada com o segredo do
 * servidor. É isso que deixa o servidor repetir uma resposta. No banco só mora
 * o resumo, e um acesso sorteado não poderia ser devolvido de novo a quem não
 * recebeu a resposta; saindo de uma conta, quem mostra a renovação recebe
 * sempre o mesmo acesso.
 *
 * Quem tem o acesso não chega à renovação (a conta só anda num sentido), e
 * quem tem a renovação não chega ao acesso sem o segredo do servidor: precisa
 * pedir, e o servidor confere se ela ainda vale.
 */
export function acessoDaRenovacao(segredoDoServidor: string, renovacao: string) {
  const valor = fecharComMarca(segredoDoServidor, PREFIXO_DO_ACESSO, derivar(segredoDoServidor, "ktalogu-oauth-acesso", renovacao));
  return { valor, hash: hashDaChave(valor), prefixo: valor.slice(0, 12) };
}

/** A renovação que sucede esta, pelo mesmo motivo: o pedido repetido recebe a mesma sucessora. */
export function renovacaoSeguinte(segredoDoServidor: string, renovacao: string) {
  const valor = fecharComMarca(segredoDoServidor, PREFIXO_DA_RENOVACAO, derivar(segredoDoServidor, "ktalogu-oauth-renovacao", renovacao));
  return { valor, hash: resumoDaFicha(valor) };
}

export function novoSegredoDoPrograma() {
  const valor = `kts_${randomBytes(32).toString("base64url")}`;
  return { valor, hash: resumoDaFicha(valor) };
}

const FORMA = /^kt[clr]_[A-Za-z0-9_-]{43}$/;

/** Tem a forma de um código e a marca do servidor? Só então vale a pena ir ao banco. */
export const codigoComMarca = (segredoDoServidor: string, valor: unknown): valor is string =>
  typeof valor === "string" && FORMA.test(valor) && valor.startsWith(PREFIXO_DO_CODIGO) && temMarca(segredoDoServidor, valor);

export const pareceRenovacao = (valor: unknown): valor is string => typeof valor === "string" && FORMA.test(valor) && valor.startsWith(PREFIXO_DA_RENOVACAO);

/** Compara dois resumos sem deixar o tempo da comparação contar a diferença. */
export function mesmoResumo(a: string | null | undefined, b: string | null | undefined) {
  if (!a || !b) return false;
  const x = Buffer.from(a);
  const y = Buffer.from(b);
  return x.length === y.length && timingSafeEqual(x, y);
}
