/**
 * CSRF Guard
 *
 * Implements CSRF protection using double-submit cookie pattern with session binding.
 * Validates that:
 * 1. CSRF token in cookie matches token in header
 * 2. Token HMAC is valid for the current user session
 *
 * Usage:
 * - Apply to controllers handling state-changing operations
 * - Use @SkipCsrf() decorator to exempt specific endpoints
 * - Safe methods (GET, HEAD, OPTIONS) are automatically exempted
 */

import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Logger,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Request } from "express";
import { CsrfService } from "../services/csrf.service";
import type { AuthenticatedUser } from "../types/user.types";

export const SKIP_CSRF_KEY = "skipCsrf";

interface RequestWithUser extends Request {
  user?: AuthenticatedUser;
}

@Injectable()
export class CsrfGuard implements CanActivate {
  private readonly logger = new Logger(CsrfGuard.name);

  constructor(
    private reflector: Reflector,
    private csrfService: CsrfService
  ) {}

  canActivate(context: ExecutionContext): boolean {
    // Check if endpoint is marked to skip CSRF
    const skipCsrf = this.reflector.getAllAndOverride<boolean>(SKIP_CSRF_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (skipCsrf) {
      return true;
    }

    const request = context.switchToHttp().getRequest<RequestWithUser>();

    // Exempt safe HTTP methods (GET, HEAD, OPTIONS)
    if (["GET", "HEAD", "OPTIONS"].includes(request.method)) {
      return true;
    }

    const authHeader = request.headers.authorization;
    if (typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
      return true;
    }

    // Get CSRF token from cookie and header
    const cookies = request.cookies as Record<string, string> | undefined;
    const cookieToken = cookies?.["csrf-token"];
    const headerToken = request.headers["x-csrf-token"] as string | undefined;

    // Validate tokens exist and match
    if (!cookieToken || !headerToken) {
      this.logger.warn({
        event: "CSRF_TOKEN_MISSING",
        method: request.method,
        path: request.path,
        hasCookie: !!cookieToken,
        hasHeader: !!headerToken,
        securityEvent: true,
        timestamp: new Date().toISOString(),
      });

      throw new ForbiddenException("CSRF token missing");
    }

    if (cookieToken !== headerToken) {
      this.logger.warn({
        event: "CSRF_TOKEN_MISMATCH",
        method: request.method,
        path: request.path,
        securityEvent: true,
        timestamp: new Date().toISOString(),
      });

      throw new ForbiddenException("CSRF token mismatch");
    }

    // Validate session binding via HMAC when user context is available.
    // CsrfGuard is a global guard (APP_GUARD) that runs before per-controller
    // AuthGuard, so request.user may not be populated yet. In that case, the
    // double-submit cookie match above is sufficient CSRF protection.
    const userId = request.user?.id;
    if (userId) {
      if (!this.csrfService.validateToken(cookieToken, userId)) {
        this.logger.warn({
          event: "CSRF_SESSION_BINDING_INVALID",
          method: request.method,
          path: request.path,
          securityEvent: true,
          timestamp: new Date().toISOString(),
        });

        throw new ForbiddenException("CSRF token not bound to session");
      }
    }
    // Note: when userId is absent, the double-submit cookie check above is
    // sufficient CSRF protection. AuthGuard populates request.user afterward.

    return true;
  }
}
