/**
 * @fileoverview Retry budget management to prevent resource exhaustion
 * This module implements retry budgets to limit the number of retries within a time window.
 */

import { createLogger } from '../logging/logger.js';

const logger = createLogger('RetryBudget');

/**
 * Retry budget configuration
 * @interface
 * @public
 */
export interface RetryBudgetConfig {
  /** Maximum number of retries allowed per time window */
  maxRetries: number;
  /** Time window in milliseconds */
  timeWindow: number;
  /** Name for logging purposes */
  name?: string;
}

/**
 * Retry budget statistics
 * @interface
 * @public
 */
export interface RetryBudgetStats {
  /** Current number of retries consumed */
  consumed: number;
  /** Maximum retries allowed */
  maximum: number;
  /** Remaining retries available */
  remaining: number;
  /** Time until budget resets (ms) */
  resetInMs: number;
  /** Whether budget is exhausted */
  isExhausted: boolean;
  /** Percentage of budget consumed */
  consumedPercentage: number;
}

/**
 * Default retry budget configuration from environment
 * @private
 */
const DEFAULT_CONFIG: RetryBudgetConfig = {
  maxRetries: parseInt(process.env.RETRY_BUDGET_PER_MINUTE || '10', 10),
  timeWindow: 60000, // 1 minute
};

/**
 * Retry budget implementation
 * @class
 * @public
 */
export class RetryBudget {
  private readonly config: RetryBudgetConfig;
  private readonly name: string;
  private retryTimestamps: number[] = [];

  /**
   * Creates a new retry budget
   * @param config Configuration for the retry budget
   */
  constructor(config: Partial<RetryBudgetConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
    this.name = config.name || 'default';
    logger.debug('Retry budget created', { name: this.name, config: this.config });
  }

  /**
   * Check if a retry can be consumed from the budget
   * @returns True if a retry is available
   * @public
   */
  canRetry(): boolean {
    this.cleanup();
    const available = this.retryTimestamps.length < this.config.maxRetries;

    if (!available) {
      logger.warn('Retry budget exhausted', {
        name: this.name,
        consumed: this.retryTimestamps.length,
        max: this.config.maxRetries,
      });
    }

    return available;
  }

  /**
   * Consume a retry from the budget
   * @returns True if the retry was successfully consumed
   * @public
   */
  consumeRetry(): boolean {
    if (!this.canRetry()) {
      return false;
    }

    const now = Date.now();
    this.retryTimestamps.push(now);

    logger.debug('Retry consumed from budget', {
      name: this.name,
      consumed: this.retryTimestamps.length,
      max: this.config.maxRetries,
    });

    return true;
  }

  /**
   * Get current budget statistics
   * @returns The current budget statistics
   * @public
   */
  getStats(): RetryBudgetStats {
    this.cleanup();
    const consumed = this.retryTimestamps.length;
    const remaining = Math.max(0, this.config.maxRetries - consumed);

    // Calculate time until next reset
    let resetInMs = 0;
    if (this.retryTimestamps.length > 0) {
      const oldestTimestamp = this.retryTimestamps[0];
      if (oldestTimestamp !== undefined) {
        const timeSinceOldest = Date.now() - oldestTimestamp;
        resetInMs = Math.max(0, this.config.timeWindow - timeSinceOldest);
      }
    }

    const consumedPercentage = (consumed / this.config.maxRetries) * 100;

    return {
      consumed,
      maximum: this.config.maxRetries,
      remaining,
      resetInMs,
      isExhausted: remaining === 0,
      consumedPercentage: Math.round(consumedPercentage),
    };
  }

  /**
   * Reset the budget (clear all consumed retries)
   * @public
   */
  reset(): void {
    this.retryTimestamps = [];
    logger.info('Retry budget reset', { name: this.name });
  }

  /**
   * Clean up expired retry timestamps
   * @private
   */
  private cleanup(): void {
    const now = Date.now();
    const cutoff = now - this.config.timeWindow;
    const before = this.retryTimestamps.length;
    this.retryTimestamps = this.retryTimestamps.filter((timestamp) => timestamp > cutoff);
    const after = this.retryTimestamps.length;

    if (before !== after) {
      logger.debug('Cleaned up expired retries', {
        name: this.name,
        removed: before - after,
        remaining: after,
      });
    }
  }
}

/**
 * Retry budget manager for managing multiple budgets
 * @class
 * @public
 */
export class RetryBudgetManager {
  private static instance: RetryBudgetManager;
  private budgets: Map<string, RetryBudget> = new Map();
  private globalBudget: RetryBudget;

  /**
   * Private constructor for singleton pattern
   * @private
   */
  private constructor() {
    this.globalBudget = new RetryBudget({ name: 'global' });
  }

  /**
   * Get the singleton instance
   * @returns The retry budget manager instance
   * @public
   */
  static getInstance(): RetryBudgetManager {
    if (!RetryBudgetManager.instance) {
      RetryBudgetManager.instance = new RetryBudgetManager();
    }
    return RetryBudgetManager.instance;
  }

  /**
   * Get or create a budget for an endpoint
   * @param endpoint The endpoint name
   * @param config Optional configuration overrides
   * @returns The retry budget instance
   * @public
   */
  getBudget(endpoint: string, config?: Partial<RetryBudgetConfig>): RetryBudget {
    let budget = this.budgets.get(endpoint);
    if (!budget) {
      budget = new RetryBudget({ ...config, name: endpoint });
      this.budgets.set(endpoint, budget);
    }
    return budget;
  }

  /**
   * Get the global retry budget
   * @returns The global retry budget instance
   * @public
   */
  getGlobalBudget(): RetryBudget {
    return this.globalBudget;
  }

  /**
   * Check if both endpoint and global budgets allow a retry
   * @param endpoint The endpoint to check
   * @returns True if both budgets allow a retry
   * @public
   */
  canRetry(endpoint: string): boolean {
    const endpointBudget = this.getBudget(endpoint);
    const globalAllows = this.globalBudget.canRetry();
    const endpointAllows = endpointBudget.canRetry();

    if (!globalAllows) {
      logger.warn('Global retry budget exhausted');
    }
    if (!endpointAllows) {
      logger.warn('Endpoint retry budget exhausted', { endpoint });
    }

    return globalAllows && endpointAllows;
  }

  /**
   * Consume a retry from both endpoint and global budgets
   * @param endpoint The endpoint to consume from
   * @returns True if the retry was successfully consumed from both budgets
   * @public
   */
  consumeRetry(endpoint: string): boolean {
    const endpointBudget = this.getBudget(endpoint);

    // Check both budgets first
    if (!this.globalBudget.canRetry() || !endpointBudget.canRetry()) {
      return false;
    }

    // Consume from both budgets
    const globalConsumed = this.globalBudget.consumeRetry();
    const endpointConsumed = endpointBudget.consumeRetry();

    return globalConsumed && endpointConsumed;
  }

  /**
   * Get statistics for all budgets
   * @returns Map of budget name to statistics
   * @public
   */
  getAllStats(): Map<string, RetryBudgetStats> {
    const stats = new Map<string, RetryBudgetStats>();
    stats.set('global', this.globalBudget.getStats());

    for (const [endpoint, budget] of this.budgets) {
      stats.set(endpoint, budget.getStats());
    }

    return stats;
  }

  /**
   * Reset all budgets
   * @public
   */
  resetAll(): void {
    this.globalBudget.reset();
    for (const budget of this.budgets.values()) {
      budget.reset();
    }
    logger.info('All retry budgets reset');
  }

  /**
   * Clear all budgets (for testing)
   * @public
   */
  clear(): void {
    this.budgets.clear();
    this.globalBudget.reset();
  }
}
