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

/**
 * A chave de acesso dos assistentes e apps.
 *
 * É um segredo longo e aleatório, com o prefixo `ktl_` para a pessoa (e os
 * detectores de segredo vazado) reconhecerem de onde ele é. A chave inteira
 * aparece uma vez só, na hora em que nasce; no banco fica o SHA-256 dela. Como
 * o segredo tem 256 bits de sorteio, o hash simples basta: não há senha fraca
 * para proteger com um hash lento.
 *
 * Este arquivo é só regra, sem banco, para o teste ao lado rodar sozinho.
 */

export const ESCOPOS = ["leitura", "escrita"] as const;
export type Escopo = (typeof ESCOPOS)[number];

/** As validades que a tela oferece, em dias. `null` é "sem validade". */
export const VALIDADES_EM_DIAS = [30, 90, 365] as const;

export const MAXIMO_DE_CHAVES_ATIVAS = 10;
/* Criações por pessoa em 24 horas. Ninguém precisa de trinta chaves num dia. */
export const MAXIMO_DE_CHAVES_POR_DIA = 30;

const PREFIXO = "ktl_";
const FORMATO = /^ktl_[A-Za-z0-9_-]{43}$/;

/**
 * A marca do servidor.
 *
 * Dos 32 bytes de um segredo nosso, 24 são sorteio e os 8 do fim são uma marca
 * que só o servidor sabe fazer (uma conta com um segredo dele sobre os outros
 * 24). Ela não substitui a conferência no banco; serve para separar, sem ir ao
 * banco, o que saiu daqui do que alguém inventou.
 *
 * Isso importa porque o Claude e o ChatGPT falam com o conector a partir dos
 * mesmos poucos endereços, de todos os clientes deles. O freio de quem erra a
 * chave em série é por endereço: sem a marca, alguém despejando chave
 * inventada por um desses endereços frearia junto os clientes de verdade. Com
 * ela, o que tem marca passa pelo freio, e o freio pega só o que não tem.
 *
 * O formato visível não muda: prefixo e 43 caracteres. Chave antiga, sem
 * marca, continua valendo; só não tem o passe.
 */
const TAMANHO_DO_SORTEIO = 24;
const TAMANHO_DA_MARCA = 8;

function marcaDe(segredo: string, prefixo: string, sorteio: Buffer) {
  return createHmac("sha256", segredo).update(`ktalogu-marca:${prefixo}:`, "utf8").update(sorteio).digest().subarray(0, TAMANHO_DA_MARCA);
}

/** Fecha 24 bytes de sorteio com a marca do servidor e o prefixo. */
export function fecharComMarca(segredo: string, prefixo: string, sorteio: Buffer): string {
  if (sorteio.length !== TAMANHO_DO_SORTEIO) throw new Error("O sorteio tem de ter 24 bytes.");
  return `${prefixo}${Buffer.concat([sorteio, marcaDe(segredo, prefixo, sorteio)]).toString("base64url")}`;
}

/** Este segredo (com o prefixo) carrega a marca do servidor? */
export function temMarca(segredo: string, valor: string): boolean {
  const corte = valor.indexOf("_") + 1;
  if (corte <= 0) return false;
  const bytes = Buffer.from(valor.slice(corte), "base64url");
  if (bytes.length !== TAMANHO_DO_SORTEIO + TAMANHO_DA_MARCA) return false;
  const esperada = marcaDe(segredo, valor.slice(0, corte), bytes.subarray(0, TAMANHO_DO_SORTEIO));
  return timingSafeEqual(esperada, bytes.subarray(TAMANHO_DO_SORTEIO));
}

/** `segredo` é o do servidor: com ele, a chave nasce com a marca. Sem ele (nos testes), nasce só com sorteio. */
export function gerarChave(segredo?: string) {
  const chave = segredo ? fecharComMarca(segredo, PREFIXO, randomBytes(TAMANHO_DO_SORTEIO)) : `${PREFIXO}${randomBytes(32).toString("base64url")}`;
  return { chave, prefixo: chave.slice(0, 12), hash: hashDaChave(chave) };
}

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

/** Tem a forma de uma chave nossa? Evita ir ao banco por qualquer coisa. */
export function pareceChave(valor: string) {
  return FORMATO.test(valor);
}

/**
 * O que a chave pode fazer. "leitura" entra sempre: uma chave que só grava não
 * consegue nem achar o cliente para quem vai orçar.
 */
export function lerEscopos(bruto: unknown): Escopo[] | null {
  if (bruto === "leitura") return ["leitura"];
  if (bruto === "escrita") return ["leitura", "escrita"];
  if (!Array.isArray(bruto)) return null;
  const pedidos = new Set(bruto.map((item) => String(item)));
  for (const item of pedidos) {
    if (!(ESCOPOS as readonly string[]).includes(item)) return null;
  }
  return pedidos.has("escrita") ? ["leitura", "escrita"] : ["leitura"];
}

export function lerNome(bruto: unknown): { ok: true; nome: string } | { ok: false; erro: string } {
  const nome = String(bruto ?? "").replace(/\s+/g, " ").trim();
  if (!nome) return { ok: false, erro: "Dê um nome à chave, para reconhecer depois onde ela está sendo usada." };
  if (nome.length > 80) return { ok: false, erro: "Nome muito longo (máximo de 80 letras)." };
  return { ok: true, nome };
}

/** A validade escolhida vira a data em que a chave para de valer. */
export function lerValidade(
  bruto: unknown,
  agora: Date = new Date()
): { ok: true; expiraEm: string | null } | { ok: false; erro: string } {
  if (bruto === null || bruto === undefined || bruto === "" || bruto === "sem") return { ok: true, expiraEm: null };
  const dias = Number(bruto);
  if (!(VALIDADES_EM_DIAS as readonly number[]).includes(dias)) {
    return { ok: false, erro: "Validade inválida. Escolha 30 dias, 90 dias, 1 ano ou sem validade." };
  }
  return { ok: true, expiraEm: new Date(agora.getTime() + dias * 24 * 60 * 60 * 1000).toISOString() };
}

export type SituacaoDaChave = "ativa" | "revogada" | "vencida";

export function situacaoDaChave(
  linha: { revogada_em?: string | null; expira_em?: string | null },
  agora: number = Date.now()
): SituacaoDaChave {
  if (linha.revogada_em) return "revogada";
  if (linha.expira_em) {
    const fim = new Date(linha.expira_em).getTime();
    if (Number.isFinite(fim) && fim <= agora) return "vencida";
  }
  return "ativa";
}

export function temEscopo(escopos: readonly string[] | null | undefined, escopo: Escopo) {
  return Array.isArray(escopos) && escopos.includes(escopo);
}
