/* ============================================================
 * Copyright (c) 2026 RAVAPI Soluções. www.ravapi.com
 * Token de sessão — JWT HS256 mínimo, sem dependência nova
 *
 * É o login local de hoje (docs/decisoes.md §18) — não é o login único da
 * RAVAPI, que ainda não existe. Formato JWT de propósito: se um dia o
 * login único emitir token no mesmo formato, o lado de cá (verificarSessao)
 * muda só a fonte do segredo/chave, não o formato.
 * ============================================================ */

import { createHmac, timingSafeEqual } from 'node:crypto';
import { SessaoInvalida } from './errors.ts';

export interface ClaimsSessao {
  sub: string;               // id do usuário
  tenant_id: string;
  papel: string;
  nome: string;
  email: string;
  seriesPermitidas: string[];
  exp: number;                // unix seconds
  iat: number;
}

function base64url(input: Buffer | string): string {
  return Buffer.from(input as any).toString('base64url');
}
function base64urlJSON(obj: unknown): string {
  return base64url(JSON.stringify(obj));
}

export function assinarSessao(
  claims: Omit<ClaimsSessao, 'exp' | 'iat'>,
  segredo: string,
  ttlSegundos = 12 * 3600,
): string {
  const agora = Math.floor(Date.now() / 1000);
  const header = { alg: 'HS256', typ: 'JWT' };
  const payload: ClaimsSessao = { ...claims, iat: agora, exp: agora + ttlSegundos };
  const dados = `${base64urlJSON(header)}.${base64urlJSON(payload)}`;
  const assinatura = base64url(createHmac('sha256', segredo).update(dados).digest());
  return `${dados}.${assinatura}`;
}

export function verificarSessao(token: string, segredo: string): ClaimsSessao {
  const partes = token.split('.');
  if (partes.length !== 3) throw new SessaoInvalida('formato');
  const [headerB64, payloadB64, assinatura] = partes;

  const esperada = base64url(createHmac('sha256', segredo).update(`${headerB64}.${payloadB64}`).digest());
  const bufRecebido = Buffer.from(assinatura);
  const bufEsperado = Buffer.from(esperada);
  if (bufRecebido.length !== bufEsperado.length || !timingSafeEqual(bufRecebido, bufEsperado)) {
    throw new SessaoInvalida('assinatura');
  }

  let payload: ClaimsSessao;
  try {
    payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8'));
  } catch {
    throw new SessaoInvalida('formato');
  }
  if (typeof payload.exp !== 'number' || payload.exp < Math.floor(Date.now() / 1000)) {
    throw new SessaoInvalida('expirada');
  }
  return payload;
}
