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
   *
   * @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);

    const otpData = await this.cache.get<OtpData>(key);

    // Code not found or expired
    if (!otpData) {
      this.logger.warn(
        { email: normalizedEmail },
        "OTP verification failed: code expired or not found"
      );
      return { valid: false, reason: "expired" };
    }

    // Check fingerprint mismatch (soft check - warn only, don't block)
    // Users may legitimately switch devices/networks between requesting and verifying
    if (otpData.fingerprint && fingerprint && otpData.fingerprint !== fingerprint) {
      this.logger.warn(
        { email: normalizedEmail },
        "OTP verification from different context than request (fingerprint mismatch)"
      );
    }

    // Max attempts reached
    if (otpData.attempts >= this.maxAttempts) {
      this.logger.warn({ email: normalizedEmail }, "OTP verification failed: max attempts reached");
      await this.cache.del(key);
      return { valid: false, reason: "max_attempts" };
    }

    // Invalid code
    if (otpData.code !== code) {
      const newAttempts = otpData.attempts + 1;
      const attemptsRemaining = this.maxAttempts - newAttempts;

      // Update attempts count (keeping existing TTL would be ideal, but we reset it for simplicity)
      const updatedData: OtpData = {
        ...otpData,
        attempts: newAttempts,
      };

      // Calculate remaining TTL
      const createdAt = new Date(otpData.createdAt).getTime();
      const elapsed = (Date.now() - createdAt) / 1000;
      const remainingTtl = Math.max(1, Math.floor(this.ttlSeconds - elapsed));

      await this.cache.set(key, updatedData, remainingTtl);

      this.logger.warn(
        { email: normalizedEmail, attemptsRemaining },
        "OTP verification failed: invalid code"
      );

      return { valid: false, reason: "invalid", attemptsRemaining };
    }

    // Valid code - delete it (one-time use)
    await this.cache.del(key);

    this.logger.log({ email: normalizedEmail }, "OTP verification successful");

    return { valid: true };
  }

  /**
   * Check if an OTP exists for the given email (without consuming attempts)
   */
  async exists(email: string): Promise<boolean> {
    const normalizedEmail = this.normalizeEmail(email);
    const key = this.buildKey(normalizedEmail);
    const otpData = await this.cache.get<OtpData>(key);
    return otpData !== null;
  }

  /**
   * 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();
  }
}
