import { randomBytes } from "crypto";

import { Injectable, Inject } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";

import { CacheService } from "@/infra/cache/cache.service.js";

/**
 * OTP data stored in Redis
 */
interface OtpData {
  /** The 6-digit code */
  code: string;
  /** Number of verification attempts */
  attempts: number;
  /** When the code was created */
  createdAt: string;
  /** Optional fingerprint binding (SHA256 hash of IP + User-Agent) */
  fingerprint?: string | undefined;
}

/**
 * OTP verification result
 */
export interface OtpVerifyResult {
  /** Whether the code was valid */
  valid: boolean;
  /** Reason for failure (if invalid) */
  reason?: "expired" | "invalid" | "max_attempts";
  /** Remaining attempts (if invalid but not expired) */
  attemptsRemaining?: number;
}

/**
 * OTP Service
 *
 * Manages one-time password generation and verification for email verification.
 * Uses Redis for storage with automatic TTL expiration.
 *
 * Security features:
 * - Cryptographically secure code generation
 * - Limited verification attempts (default: 3)
 * - Automatic expiration (default: 10 minutes)
 * - Code invalidation after successful verification
 */
@Injectable()
export class OtpService {
  private readonly OTP_PREFIX = "otp:";
  private readonly ttlSeconds: number;
  private readonly maxAttempts: number;

  constructor(
    private readonly cache: CacheService,
    private readonly config: ConfigService,
    @Inject(Logger) private readonly logger: Logger
  ) {
    this.ttlSeconds = this.config.get<number>("OTP_TTL_SECONDS", 600); // 10 minutes
    this.maxAttempts = this.config.get<number>("OTP_MAX_ATTEMPTS", 3);
  }

  /**
   * Generate a new OTP code and store it for the given email
   * If a code already exists for this email, it will be replaced
   *
   * @param email - Email address to generate code for (will be normalized)
   * @param fingerprint - Optional fingerprint to bind OTP to (e.g., SHA256 of IP + User-Agent)
   * @returns The generated 6-digit code
   */
  async generateAndStore(email: string, fingerprint?: string): Promise<string> {
    const normalizedEmail = this.normalizeEmail(email);
    const code = this.generateSecureCode();
    const key = this.buildKey(normalizedEmail);

    const otpData: OtpData = {
      code,
      attempts: 0,
      createdAt: new Date().toISOString(),
      fingerprint,
    };

    await this.cache.set(key, otpData, this.ttlSeconds);

    this.logger.log(
      { email: normalizedEmail, hasFingerprint: !!fingerprint },
      "OTP code generated"
    );

    return code;
  }

  /**
   * Verify an OTP code for the given email
   *
   * Uses atomic Redis operations to prevent race conditions where
   * concurrent verification requests could both pass.
   *
   * @param email - Email address to verify code for
   * @param code - The 6-digit code to verify
   * @param fingerprint - Optional fingerprint to check against stored value (logs warning if mismatch)
   * @returns Verification result with validity and reason if invalid
   */
  async verify(email: string, code: string, fingerprint?: string): Promise<OtpVerifyResult> {
    const normalizedEmail = this.normalizeEmail(email);
    const key = this.buildKey(normalizedEmail);

    // Log fingerprint mismatch before atomic verification (for audit purposes)
    // This is a soft check - we still proceed with verification
    if (fingerprint) {
      const otpData = await this.cache.get<OtpData>(key);
      if (otpData?.fingerprint && otpData.fingerprint !== fingerprint) {
        this.logger.warn(
          { email: normalizedEmail },
          "OTP verification from different context than request (fingerprint mismatch)"
        );
      }
    }

    // Use atomic verification to prevent race conditions
    const result = await this.cache.verifyOtpAtomic(key, code, this.maxAttempts);

    // Log the verification result
    if (result.valid) {
      this.logger.log({ email: normalizedEmail }, "OTP verification successful");
    } else {
      this.logger.warn(
        {
          email: normalizedEmail,
          reason: result.reason,
          attemptsRemaining: result.attemptsRemaining,
        },
        `OTP verification failed: ${result.reason}`
      );
    }

    return result;
  }

  /**
   * Delete any existing OTP for the given email
   */
  async invalidate(email: string): Promise<void> {
    const normalizedEmail = this.normalizeEmail(email);
    const key = this.buildKey(normalizedEmail);
    await this.cache.del(key);
  }

  /**
   * Generate a cryptographically secure 6-digit code
   */
  private generateSecureCode(): string {
    // Use 4 random bytes to generate a number, then mod by 1000000
    const buffer = randomBytes(4);
    const num = buffer.readUInt32BE(0) % 1000000;
    // Pad with leading zeros to ensure 6 digits
    return num.toString().padStart(6, "0");
  }

  /**
   * Build Redis key for OTP storage
   */
  private buildKey(email: string): string {
    return `${this.OTP_PREFIX}${email}`;
  }

  /**
   * Normalize email for consistent key generation
   */
  private normalizeEmail(email: string): string {
    return email.toLowerCase().trim();
  }
}
