/**
 * Brute Force Protection Service
 *
 * Implements exponential backoff for failed login attempts
 */

import { Injectable, Logger } from '@nestjs/common';
import { bruteForceConfig } from '../../infrastructure/security/security.config';

interface LoginAttempt {
  count: number;
  firstAttempt: Date;
  lastAttempt: Date;
  blockedUntil?: Date;
}

@Injectable()
export class BruteForceProtectionService {
  private readonly logger = new Logger(BruteForceProtectionService.name);
  private readonly attempts = new Map<string, LoginAttempt>();
  private readonly cleanupInterval = 60 * 60 * 1000; // 1 hour

  constructor() {
    // Periodically clean up old attempts
    setInterval(() => this.cleanup(), this.cleanupInterval);
  }

  /**
   * Record a failed login attempt
   */
  recordFailedAttempt(identifier: string): void {
    const now = new Date();
    const existing = this.attempts.get(identifier);

    if (existing) {
      existing.count++;
      existing.lastAttempt = now;

      // Calculate block time with exponential backoff
      if (existing.count > bruteForceConfig.freeRetries) {
        const waitTime = this.calculateWaitTime(existing.count - bruteForceConfig.freeRetries);
        existing.blockedUntil = new Date(now.getTime() + waitTime);

        this.logger.warn(
          `Brute force detected for ${identifier}. Blocked until ${existing.blockedUntil.toISOString()}`
        );
      }

      this.attempts.set(identifier, existing);
    } else {
      this.attempts.set(identifier, {
        count: 1,
        firstAttempt: now,
        lastAttempt: now,
      });
    }
  }

  /**
   * Record a successful login (clears attempts)
   */
  recordSuccessfulAttempt(identifier: string): void {
    this.attempts.delete(identifier);
    this.logger.log(`Cleared failed attempts for ${identifier}`);
  }

  /**
   * Check if identifier is currently blocked
   */
  isBlocked(identifier: string): boolean {
    const attempt = this.attempts.get(identifier);

    if (!attempt || !attempt.blockedUntil) {
      return false;
    }

    const now = new Date();
    if (now < attempt.blockedUntil) {
      return true;
    }

    // Block expired, reset
    this.attempts.delete(identifier);
    return false;
  }

  /**
   * Get remaining block time in seconds
   */
  getRemainingBlockTime(identifier: string): number {
    const attempt = this.attempts.get(identifier);

    if (!attempt || !attempt.blockedUntil) {
      return 0;
    }

    const now = new Date();
    const remaining = Math.max(
      0,
      Math.floor((attempt.blockedUntil.getTime() - now.getTime()) / 1000)
    );

    return remaining;
  }

  /**
   * Get failed attempt count
   */
  getAttemptCount(identifier: string): number {
    return this.attempts.get(identifier)?.count || 0;
  }

  /**
   * Calculate wait time with exponential backoff
   */
  private calculateWaitTime(failedAttempts: number): number {
    const waitTime = bruteForceConfig.minWait * Math.pow(2, failedAttempts - 1);
    return Math.min(waitTime, bruteForceConfig.maxWait);
  }

  /**
   * Clean up old attempts
   */
  private cleanup(): void {
    const now = new Date();
    const lifetimeMs = bruteForceConfig.lifetime * 1000;
    let cleaned = 0;

    for (const [identifier, attempt] of this.attempts.entries()) {
      const age = now.getTime() - attempt.firstAttempt.getTime();
      if (age > lifetimeMs) {
        this.attempts.delete(identifier);
        cleaned++;
      }
    }

    if (cleaned > 0) {
      this.logger.log(`Cleaned up ${cleaned} old brute force attempts`);
    }
  }

  /**
   * Get statistics
   */
  getStats(): {
    totalAttempts: number;
    currentlyBlocked: number;
    averageAttempts: number;
  } {
    let totalAttempts = 0;
    let currentlyBlocked = 0;

    for (const [identifier, attempt] of this.attempts.entries()) {
      totalAttempts += attempt.count;
      if (this.isBlocked(identifier)) {
        currentlyBlocked++;
      }
    }

    return {
      totalAttempts,
      currentlyBlocked,
      averageAttempts: this.attempts.size > 0 ? Math.round(totalAttempts / this.attempts.size) : 0,
    };
  }

  /**
   * Manually block an identifier (admin action)
   */
  manualBlock(identifier: string, durationMs: number): void {
    const now = new Date();
    const existing = this.attempts.get(identifier);

    if (existing) {
      existing.blockedUntil = new Date(now.getTime() + durationMs);
      existing.count = 999; // High count to indicate manual block
      this.attempts.set(identifier, existing);
    } else {
      this.attempts.set(identifier, {
        count: 999,
        firstAttempt: now,
        lastAttempt: now,
        blockedUntil: new Date(now.getTime() + durationMs),
      });
    }

    this.logger.warn(`Manually blocked ${identifier} for ${durationMs / 1000} seconds`);
  }

  /**
   * Manually unblock an identifier (admin action)
   */
  manualUnblock(identifier: string): void {
    this.attempts.delete(identifier);
    this.logger.log(`Manually unblocked ${identifier}`);
  }
}
