import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { timingSafeEqual } from 'node:crypto';
import type { Request } from 'express';
import { IS_PUBLIC_KEY } from '../common/public.decorator';
import { CSRF_HEADER, computeCsrfToken } from '../common/csrf';

const SESSION_COOKIE = 'tms_session';
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);

/**
 * CSRF protection for cookie-authenticated state-changing requests.
 * Runs after SessionGuard. Public routes (login) are exempt — they are not
 * cookie-authenticated. Requests without a session cookie fail auth anyway.
 * The expected token is recomputed from the session cookie (session-bound
 * double-submit), so no server-side state is required.
 */
@Injectable()
export class CsrfGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const req = context
      .switchToHttp()
      .getRequest<Request & { cookies?: Record<string, string | undefined> }>();
    if (SAFE_METHODS.has(req.method)) return true;

    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;

    const sessionToken = req.cookies?.[SESSION_COOKIE];
    if (!sessionToken) return true; // no cookie auth in play; SessionGuard rejects

    const header = req.headers[CSRF_HEADER];
    const provided = Array.isArray(header) ? header[0] : header;
    if (!provided) throw new ForbiddenException('Missing CSRF token');

    const expected = computeCsrfToken(sessionToken);
    const a = Buffer.from(provided);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
      throw new ForbiddenException('Invalid CSRF token');
    }
    return true;
  }
}
