/**
 * Distributed-safe OAuth interstitial 2FA verification cookie.
 *
 * Replaces the previous in-memory `oauth-2fa-verification.ts` module
 * (PR #137) which kept verification state in a `Map` per Node process
 * — broken once the single-instance lock is lifted. This module is
 * fully stateless: each request brings its own HMAC-signed proof.
 *
 * Threat model
 * ------------
 * After the user posts a valid TOTP to `/api/auth/2fa-verify-oauth`,
 * the server issues an HttpOnly + Secure + SameSite=Strict cookie
 * containing { userId, expiresAt }, signed with NEXTAUTH_SECRET.
 * The jwt callback (triggered by `useSession().update()`) reads the
 * cookie, verifies the HMAC + the embedded userId matches the JWT,
 * and clears `requires2faOauth` on the re-issued JWT — then deletes
 * the cookie (one-shot).
 *
 * Why HMAC-signed instead of just trusting an HttpOnly cookie value:
 * an attacker who somehow controlled cookie writes could otherwise
 * forge `{ userId: <victim>, expiresAt: <future> }` and bypass the
 * interstitial. The HMAC binds the value to a server-held secret.
 *
 * Why bind to userId in the payload (not just session id):
 * NextAuth session cookies are HttpOnly already, but a single shared
 * "valid 2fa cookie" issued to user A would let user B (after a
 * cookie swap) skip the interstitial. Embedding userId in the
 * signed payload + checking it against token.sub forecloses that.
 *
 * Distributed safety
 * ------------------
 * No shared state. Worker A signs the cookie with NEXTAUTH_SECRET;
 * worker B verifies the same HMAC with the same secret. As long as
 * the secret is consistent across workers (which it must be for
 * NextAuth itself to function), this works on N workers trivially.
 *
 * TTL
 * ---
 * 5 minutes by default — only needs to cover the round-trip from
 * /api/auth/2fa-verify-oauth → useSession().update() → jwt callback.
 * In practice that's milliseconds. The TTL is the safety net against
 * a leaked cookie being used later.
 */
import crypto from 'crypto';
import { env } from '@/lib/env';

export const OAUTH_2FA_COOKIE_NAME = '__Host-uptime-sentinel.oauth-2fa-verified';

const DEFAULT_TTL_MS = 5 * 60 * 1000;
const SIG_ALGO = 'sha256';

interface VerificationPayload {
    userId: number;
    expiresAt: number; // epoch ms
}

function hmac(payload: string): string {
    return crypto.createHmac(SIG_ALGO, env.NEXTAUTH_SECRET).update(payload).digest('base64url');
}

function safeEqual(a: string, b: string): boolean {
    const ab = Buffer.from(a, 'utf8');
    const bb = Buffer.from(b, 'utf8');
    if (ab.length !== bb.length) return false;
    return crypto.timingSafeEqual(ab, bb);
}

/**
 * Build the cookie value (payload.signature) and the SameSite/Secure
 * options the caller should pass to `cookies().set(name, value, opts)`.
 */
export function buildOauth2faVerifiedCookie(userId: number, ttlMs: number = DEFAULT_TTL_MS): {
    name: string;
    value: string;
    options: {
        httpOnly: true;
        secure: true;
        sameSite: 'strict';
        path: '/';
        maxAge: number; // seconds
    };
} {
    const payload: VerificationPayload = {
        userId,
        expiresAt: Date.now() + ttlMs,
    };
    const payloadB64 = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
    const sig = hmac(payloadB64);
    return {
        name: OAUTH_2FA_COOKIE_NAME,
        value: `${payloadB64}.${sig}`,
        options: {
            httpOnly: true,
            secure: true,
            sameSite: 'strict',
            path: '/',
            maxAge: Math.floor(ttlMs / 1000),
        },
    };
}

/**
 * Verify a cookie value. Returns true iff:
 *   - the value parses as `<payloadB64>.<sig>`,
 *   - the signature is valid under NEXTAUTH_SECRET,
 *   - the embedded userId equals `expectedUserId`,
 *   - the embedded expiresAt has not elapsed.
 *
 * Constant-time signature compare to defeat timing oracles.
 */
export function verifyOauth2faVerifiedCookie(
    cookieValue: string | undefined,
    expectedUserId: number,
    now: number = Date.now(),
): boolean {
    if (!cookieValue) return false;
    const dot = cookieValue.lastIndexOf('.');
    if (dot <= 0) return false;
    const payloadB64 = cookieValue.slice(0, dot);
    const sig = cookieValue.slice(dot + 1);

    const expectedSig = hmac(payloadB64);
    if (!safeEqual(sig, expectedSig)) return false;

    let payload: VerificationPayload;
    try {
        const json = Buffer.from(payloadB64, 'base64url').toString('utf8');
        payload = JSON.parse(json) as VerificationPayload;
    } catch {
        return false;
    }

    if (typeof payload.userId !== 'number' || typeof payload.expiresAt !== 'number') {
        return false;
    }
    if (payload.userId !== expectedUserId) return false;
    if (payload.expiresAt <= now) return false;
    return true;
}
