/**
 * CSRF Service
 *
 * Handles CSRF token generation and validation with session binding.
 * Tokens are cryptographically tied to the user session via HMAC.
 *
 * Token format: {random_part}:{hmac(random_part + session_id, secret)}
 */

import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import * as crypto from "crypto";

@Injectable()
export class CsrfService implements OnModuleInit {
  private readonly logger = new Logger(CsrfService.name);
  private csrfSecret = "";

  onModuleInit(): void {
    const secret = process.env.CSRF_SECRET;

    if (process.env.NODE_ENV === "production" && !secret) {
      throw new Error(
        "CSRF_SECRET environment variable is required in production. " +
          "Generate with: node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\""
      );
    }

    // Use provided secret or generate a random one for development
    if (secret) {
      this.csrfSecret = secret;
      this.logger.log("CSRF service initialized with configured secret");
    } else {
      this.csrfSecret = crypto.randomBytes(32).toString("hex");
      this.logger.warn(
        "CSRF service initialized with random secret (development mode). " +
          "Set CSRF_SECRET for persistent tokens across restarts."
      );
    }
  }

  /**
   * Generate a CSRF token bound to a session identifier
   * @param sessionId - User session identifier (e.g., user ID or session token)
   * @returns Token in format: {random}:{hmac}
   */
  generateToken(sessionId: string): string {
    // Generate cryptographically secure random part (32 bytes = 64 hex chars)
    const randomPart = crypto.randomBytes(32).toString("hex");

    // Create HMAC binding the random part to the session
    const hmac = this.createHmac(randomPart, sessionId);

    return `${randomPart}:${hmac}`;
  }

  /**
   * Validate a CSRF token against a session identifier
   * @param token - The full CSRF token (random:hmac format)
   * @param sessionId - User session identifier to validate against
   * @returns true if token is valid and bound to the session
   */
  validateToken(token: string, sessionId: string): boolean {
    if (!token || !sessionId) {
      return false;
    }

    // Parse token parts
    const colonIndex = token.indexOf(":");
    if (colonIndex === -1) {
      this.logger.debug("Invalid token format: missing colon separator");
      return false;
    }

    const randomPart = token.substring(0, colonIndex);
    const providedHmac = token.substring(colonIndex + 1);

    if (!randomPart || !providedHmac) {
      this.logger.debug("Invalid token format: empty random part or HMAC");
      return false;
    }

    // Verify the random part is valid hex (64 characters for 32 bytes)
    if (!/^[0-9a-fA-F]{64}$/.test(randomPart)) {
      this.logger.debug("Invalid token format: random part is not valid hex");
      return false;
    }

    // Compute expected HMAC
    const expectedHmac = this.createHmac(randomPart, sessionId);

    // Use timing-safe comparison to prevent timing attacks
    try {
      return crypto.timingSafeEqual(
        Buffer.from(providedHmac, "hex"),
        Buffer.from(expectedHmac, "hex")
      );
    } catch {
      // Buffer creation fails if providedHmac is not valid hex
      this.logger.debug("Invalid token format: HMAC is not valid hex");
      return false;
    }
  }

  /**
   * Create HMAC for token binding
   * @param randomPart - The random part of the token
   * @param sessionId - The session identifier
   * @returns Hex-encoded HMAC
   */
  private createHmac(randomPart: string, sessionId: string): string {
    return crypto
      .createHmac("sha256", this.csrfSecret)
      .update(`${randomPart}:${sessionId}`)
      .digest("hex");
  }
}
