/**
 * Circuit Breaker Implementation
 *
 * Prevents cascading failures by cutting off failing services.
 * Implements the Circuit Breaker pattern with three states:
 * - CLOSED: Normal operation, requests flow through
 * - OPEN: Service is failing, requests are rejected immediately
 * - HALF_OPEN: Testing if service has recovered
 *
 * Features:
 * - Configurable failure/success thresholds
 * - Exponential backoff on retries
 * - Telemetry integration for monitoring
 * - Pre-configured instances for common services
 * - Retry-After headers for backpressure signaling (RFC 7231)
 *
 * @module circuit-breaker
 */

import { NextResponse } from "next/server";
import { logger } from "./logger";
import { sendTelemetryEvent } from "./services/telemetry.service";
import { sendCircuitBreakerAlert } from "./services/alerting.service";
import {
  calculateRetryAfterFromDuration,
  createCircuitBreakerSignal,
  logBackpressure,
  type BackpressureSignal,
} from "./backpressure";

/**
 * Circuit breaker states
 */
export type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";

/**
 * Configuration options for circuit breaker
 */
export interface CircuitBreakerConfig {
  /** Unique name for this circuit (used in logs/telemetry) */
  name: string;
  /** Number of consecutive failures before opening circuit */
  failureThreshold: number;
  /** Number of consecutive successes in half-open to close circuit */
  successThreshold: number;
  /** Milliseconds before attempting half-open after opening */
  resetTimeout: number;
  /** Maximum retry attempts per request (default: 3) */
  maxRetries?: number;
  /** Request timeout in milliseconds (default: 30000) */
  timeout?: number;
  /** Initial backoff delay in milliseconds (default: 1000) */
  initialBackoff?: number;
  /** Maximum backoff delay in milliseconds (default: 30000) */
  maxBackoff?: number;
  /** Backoff multiplier (default: 2) */
  backoffMultiplier?: number;
  /** Whether to jitter backoff times to prevent thundering herd (default: true) */
  jitter?: boolean;
}

/**
 * Current state of a circuit breaker
 */
export interface CircuitBreakerState {
  /** Current circuit state */
  state: CircuitState;
  /** Consecutive failure count */
  failures: number;
  /** Consecutive success count (in half-open state) */
  successes: number;
  /** Timestamp of last failure */
  lastFailure: Date | null;
  /** Timestamp of last attempt */
  lastAttempt: Date | null;
  /** Timestamp when circuit opened */
  openedAt: Date | null;
  /** Total requests processed */
  totalRequests: number;
  /** Total successful requests */
  successfulRequests: number;
  /** Total failed requests */
  failedRequests: number;
}

/**
 * Error thrown when circuit is open
 */
export class CircuitOpenError extends Error {
  /** Retry-After in seconds (RFC 7231 compliant) */
  public readonly retryAfterSeconds: number;
  /** Backpressure signal for HTTP response generation */
  public readonly backpressureSignal: BackpressureSignal;

  constructor(
    public readonly circuitName: string,
    public readonly remainingTimeout: number,
  ) {
    const retryAfterSeconds = calculateRetryAfterFromDuration(remainingTimeout);
    super(
      `Circuit breaker '${circuitName}' is OPEN. Retry after ${retryAfterSeconds}s`,
    );
    this.name = "CircuitOpenError";
    this.retryAfterSeconds = retryAfterSeconds;
    this.backpressureSignal = createCircuitBreakerSignal(
      remainingTimeout,
      circuitName,
    );
  }

  /**
   * Create a 503 NextResponse with Retry-After header
   */
  toResponse(): NextResponse {
    logBackpressure(this.backpressureSignal);

    return NextResponse.json(
      {
        error: this.backpressureSignal.message,
        retryAfter: this.retryAfterSeconds,
        service: this.circuitName,
        reason: "circuit_breaker_open",
      },
      {
        status: 503,
        headers: {
          "Retry-After": this.retryAfterSeconds.toString(),
          "X-Backpressure-Reason": "circuit_breaker_open",
          "X-Backpressure-Service": this.circuitName,
        },
      },
    );
  }
}

/**
 * Error thrown when request times out
 */
export class CircuitTimeoutError extends Error {
  constructor(
    public readonly circuitName: string,
    public readonly timeout: number,
  ) {
    super(
      `Circuit breaker '${circuitName}' request timed out after ${timeout}ms`,
    );
    this.name = "CircuitTimeoutError";
  }
}

/**
 * Circuit Breaker class
 *
 * Wraps service calls with failure detection and automatic recovery.
 *
 * @example
 * ```typescript
 * const circuit = new CircuitBreaker({
 *   name: 'my-service',
 *   failureThreshold: 5,
 *   successThreshold: 2,
 *   resetTimeout: 30000
 * });
 *
 * try {
 *   const result = await circuit.execute(() => myServiceCall());
 * } catch (error) {
 *   if (error instanceof CircuitOpenError) {
 *     // Circuit is open, service is unavailable
 *   }
 * }
 * ```
 */
export class CircuitBreaker {
  private readonly config: Required<CircuitBreakerConfig>;
  private internalState: CircuitBreakerState;
  private readonly scopedLogger;

  constructor(config: CircuitBreakerConfig) {
    this.config = {
      maxRetries: 3,
      timeout: 30000,
      initialBackoff: 1000,
      maxBackoff: 30000,
      backoffMultiplier: 2,
      jitter: true,
      ...config,
    };

    this.internalState = {
      state: "CLOSED",
      failures: 0,
      successes: 0,
      lastFailure: null,
      lastAttempt: null,
      openedAt: null,
      totalRequests: 0,
      successfulRequests: 0,
      failedRequests: 0,
    };

    this.scopedLogger = logger.scope(`CircuitBreaker:${this.config.name}`);
  }

  /**
   * Get current circuit state (read-only)
   */
  get state(): CircuitBreakerState {
    return { ...this.internalState };
  }

  /**
   * Get current circuit state name
   */
  get currentState(): CircuitState {
    return this.internalState.state;
  }

  /**
   * Check if circuit is closed (normal operation)
   */
  get isClosed(): boolean {
    return this.internalState.state === "CLOSED";
  }

  /**
   * Check if circuit is open (failing)
   */
  get isOpen(): boolean {
    return this.internalState.state === "OPEN";
  }

  /**
   * Check if circuit is half-open (testing)
   */
  get isHalfOpen(): boolean {
    return this.internalState.state === "HALF_OPEN";
  }

  /**
   * Get remaining time until circuit transitions to half-open (in milliseconds)
   * Returns 0 if circuit is not open
   */
  get remainingTimeout(): number {
    if (this.internalState.state !== "OPEN" || !this.internalState.openedAt) {
      return 0;
    }
    const elapsed = Date.now() - this.internalState.openedAt.getTime();
    return Math.max(0, this.config.resetTimeout - elapsed);
  }

  /**
   * Get Retry-After value in seconds for backpressure signaling
   * Returns the time until circuit transitions to half-open
   */
  get retryAfterSeconds(): number {
    return calculateRetryAfterFromDuration(this.remainingTimeout);
  }

  /**
   * Create a 503 response if circuit is open, null otherwise
   * Useful for API routes to quickly reject requests when service is unavailable
   */
  getOpenCircuitResponse(): NextResponse | null {
    if (this.internalState.state !== "OPEN") {
      return null;
    }

    const remaining = this.remainingTimeout;
    if (remaining <= 0) {
      // Circuit is about to transition to half-open
      return null;
    }

    const error = new CircuitOpenError(this.config.name, remaining);
    return error.toResponse();
  }

  /**
   * Execute a function with circuit breaker protection
   *
   * @param fn - Async function to execute
   * @returns Promise resolving to function result
   * @throws CircuitOpenError if circuit is open
   * @throws CircuitTimeoutError if request times out
   * @throws Original error if all retries exhausted
   */
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    this.internalState.totalRequests++;
    this.internalState.lastAttempt = new Date();

    // Check if circuit should transition from OPEN to HALF_OPEN
    if (this.internalState.state === "OPEN") {
      const timeSinceOpen = this.internalState.openedAt
        ? Date.now() - this.internalState.openedAt.getTime()
        : 0;

      if (timeSinceOpen >= this.config.resetTimeout) {
        await this.transitionTo("HALF_OPEN");
      } else {
        const remaining = this.config.resetTimeout - timeSinceOpen;
        throw new CircuitOpenError(this.config.name, remaining);
      }
    }

    let lastError: Error | undefined;
    let attempt = 0;

    while (attempt <= this.config.maxRetries) {
      try {
        const result = await this.executeWithTimeout(fn);
        await this.recordSuccess();
        return result;
      } catch (error) {
        lastError = error instanceof Error ? error : new Error(String(error));
        attempt++;

        // Don't retry on circuit open errors
        if (error instanceof CircuitOpenError) {
          throw error;
        }

        await this.recordFailure(lastError);

        // Check if circuit opened during this attempt
        if (this.internalState.state === "OPEN") {
          throw new CircuitOpenError(
            this.config.name,
            this.config.resetTimeout,
          );
        }

        // If we have retries left, wait with exponential backoff
        if (attempt <= this.config.maxRetries) {
          const backoffTime = this.calculateBackoff(attempt);
          this.scopedLogger.debug(
            `Retrying after ${backoffTime}ms (attempt ${attempt}/${this.config.maxRetries})`,
          );
          await this.sleep(backoffTime);
        }
      }
    }

    // All retries exhausted
    throw lastError;
  }

  /**
   * Execute function with timeout wrapper
   */
  private async executeWithTimeout<T>(fn: () => Promise<T>): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new CircuitTimeoutError(this.config.name, this.config.timeout));
      }, this.config.timeout);

      fn()
        .then((result) => {
          clearTimeout(timeoutId);
          resolve(result);
        })
        .catch((error) => {
          clearTimeout(timeoutId);
          reject(error);
        });
    });
  }

  /**
   * Calculate exponential backoff with optional jitter
   */
  private calculateBackoff(attempt: number): number {
    const exponentialDelay =
      this.config.initialBackoff *
      Math.pow(this.config.backoffMultiplier, attempt - 1);
    const cappedDelay = Math.min(exponentialDelay, this.config.maxBackoff);

    if (this.config.jitter) {
      // Add random jitter between 0-25% of the delay
      const jitterAmount = cappedDelay * 0.25 * Math.random();
      return Math.floor(cappedDelay + jitterAmount);
    }

    return cappedDelay;
  }

  /**
   * Record a successful operation
   */
  private async recordSuccess(): Promise<void> {
    this.internalState.successfulRequests++;

    if (this.internalState.state === "HALF_OPEN") {
      this.internalState.successes++;

      if (this.internalState.successes >= this.config.successThreshold) {
        await this.transitionTo("CLOSED");
      }
    } else if (this.internalState.state === "CLOSED") {
      // Reset failure count on success in closed state
      this.internalState.failures = 0;
    }
  }

  /**
   * Record a failed operation
   */
  private async recordFailure(error: Error): Promise<void> {
    this.internalState.failedRequests++;
    this.internalState.failures++;
    this.internalState.lastFailure = new Date();

    this.scopedLogger.warn(`Operation failed`, {
      error: error.message,
      failures: this.internalState.failures,
      threshold: this.config.failureThreshold,
      state: this.internalState.state,
    });

    if (this.internalState.state === "HALF_OPEN") {
      // Any failure in half-open state reopens the circuit
      await this.transitionTo("OPEN");
    } else if (
      this.internalState.state === "CLOSED" &&
      this.internalState.failures >= this.config.failureThreshold
    ) {
      await this.transitionTo("OPEN");
    }
  }

  /**
   * Transition circuit to a new state
   */
  private async transitionTo(newState: CircuitState): Promise<void> {
    const previousState = this.internalState.state;

    if (previousState === newState) {
      return;
    }

    this.internalState.state = newState;

    // Reset counters based on new state
    if (newState === "OPEN") {
      this.internalState.openedAt = new Date();
      this.internalState.successes = 0;
    } else if (newState === "HALF_OPEN") {
      this.internalState.successes = 0;
      this.internalState.failures = 0;
    } else if (newState === "CLOSED") {
      this.internalState.failures = 0;
      this.internalState.successes = 0;
      this.internalState.openedAt = null;
    }

    this.scopedLogger.info(
      `State transition: ${previousState} -> ${newState}`,
      {
        totalRequests: this.internalState.totalRequests,
        successRate: this.getSuccessRate(),
      },
    );

    // Send telemetry event
    await this.sendStateChangeTelemetry(previousState, newState);

    // Send alert when circuit opens (service is failing)
    if (newState === "OPEN") {
      void sendCircuitBreakerAlert(
        this.config.name,
        previousState,
        newState,
        this.internalState.failures,
        this.config.failureThreshold,
        this.config.resetTimeout,
      ).catch((err) => {
        this.scopedLogger.error("Failed to send circuit breaker alert", err);
      });
    }
  }

  /**
   * Send telemetry event for state changes
   */
  private async sendStateChangeTelemetry(
    previousState: CircuitState,
    newState: CircuitState,
  ): Promise<void> {
    try {
      await sendTelemetryEvent({
        type: "performance",
        name: `circuit_breaker_state_change`,
        tags: {
          circuit_name: this.config.name,
          previous_state: previousState,
          new_state: newState,
        },
        extra: {
          totalRequests: this.internalState.totalRequests,
          successfulRequests: this.internalState.successfulRequests,
          failedRequests: this.internalState.failedRequests,
          successRate: this.getSuccessRate(),
          failureThreshold: this.config.failureThreshold,
          successThreshold: this.config.successThreshold,
          resetTimeout: this.config.resetTimeout,
        },
        timestamp: new Date().toISOString(),
      });
    } catch (error) {
      // Log but don't throw - telemetry failures shouldn't break the circuit breaker
      this.scopedLogger.error("Failed to send telemetry", error);
    }
  }

  /**
   * Get success rate as a percentage
   */
  private getSuccessRate(): number {
    if (this.internalState.totalRequests === 0) {
      return 100;
    }
    return (
      (this.internalState.successfulRequests /
        this.internalState.totalRequests) *
      100
    );
  }

  /**
   * Manually reset the circuit to closed state
   * Use with caution - mainly for testing or manual intervention
   */
  reset(): void {
    this.scopedLogger.info("Manual reset triggered");
    this.internalState = {
      state: "CLOSED",
      failures: 0,
      successes: 0,
      lastFailure: null,
      lastAttempt: null,
      openedAt: null,
      totalRequests: 0,
      successfulRequests: 0,
      failedRequests: 0,
    };
  }

  /**
   * Manually force the circuit open
   * Useful for maintenance windows or known outages
   */
  forceOpen(): void {
    this.scopedLogger.warn("Circuit manually forced open");
    this.internalState.state = "OPEN";
    this.internalState.openedAt = new Date();
  }

  /**
   * Get circuit health statistics
   */
  getStats(): {
    name: string;
    state: CircuitState;
    totalRequests: number;
    successRate: number;
    failureCount: number;
    lastFailure: string | null;
    openedAt: string | null;
    config: {
      failureThreshold: number;
      successThreshold: number;
      resetTimeout: number;
      maxRetries: number;
      timeout: number;
    };
  } {
    return {
      name: this.config.name,
      state: this.internalState.state,
      totalRequests: this.internalState.totalRequests,
      successRate: this.getSuccessRate(),
      failureCount: this.internalState.failures,
      lastFailure: this.internalState.lastFailure?.toISOString() ?? null,
      openedAt: this.internalState.openedAt?.toISOString() ?? null,
      config: {
        failureThreshold: this.config.failureThreshold,
        successThreshold: this.config.successThreshold,
        resetTimeout: this.config.resetTimeout,
        maxRetries: this.config.maxRetries,
        timeout: this.config.timeout,
      },
    };
  }

  /**
   * Sleep utility for backoff
   */
  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

// =============================================================================
// Pre-configured Circuit Breaker Instances
// =============================================================================

/**
 * Circuit breaker for Stripe payment operations
 *
 * Configuration rationale:
 * - 3 failures before opening: Payments are critical, fail fast
 * - 2 successes to close: Quick recovery for payments
 * - 60s reset timeout: Longer wait for payment gateway issues
 * - 2 retries with 30s timeout: Payment APIs can be slower
 */
export const stripeCircuit = new CircuitBreaker({
  name: "stripe",
  failureThreshold: 3,
  successThreshold: 2,
  resetTimeout: 60000,
  maxRetries: 2,
  timeout: 30000,
  initialBackoff: 1000,
  maxBackoff: 10000,
});

/**
 * Circuit breaker for WhatsApp messaging operations
 *
 * Configuration rationale:
 * - 5 failures before opening: Messaging can have intermittent issues
 * - 3 successes to close: Ensure messaging is reliable
 * - 45s reset timeout: Balance between availability and stability
 * - 3 retries with 20s timeout: External API, allow reasonable time
 */
export const whatsappCircuit = new CircuitBreaker({
  name: "whatsapp",
  failureThreshold: 5,
  successThreshold: 3,
  resetTimeout: 45000,
  maxRetries: 3,
  timeout: 20000,
  initialBackoff: 1000,
  maxBackoff: 15000,
});

/**
 * Circuit breaker for Redis cache operations
 *
 * Configuration rationale:
 * - 10 failures before opening: Cache is non-critical, allow degraded operation
 * - 5 successes to close: Higher bar for cache reliability
 * - 15s reset timeout: Cache issues often resolve quickly
 * - 1 retry with 5s timeout: Cache should be fast, don't wait long
 */
export const redisCircuit = new CircuitBreaker({
  name: "redis",
  failureThreshold: 10,
  successThreshold: 5,
  resetTimeout: 15000,
  maxRetries: 1,
  timeout: 5000,
  initialBackoff: 200,
  maxBackoff: 2000,
});

/**
 * Circuit breaker for external email service operations
 *
 * Configuration rationale:
 * - 5 failures before opening: Email delivery can be flaky
 * - 3 successes to close: Ensure email service is stable
 * - 60s reset timeout: Email services may take time to recover
 * - 2 retries with 30s timeout: Email APIs can be slow
 */
export const emailCircuit = new CircuitBreaker({
  name: "email",
  failureThreshold: 5,
  successThreshold: 3,
  resetTimeout: 60000,
  maxRetries: 2,
  timeout: 30000,
  initialBackoff: 2000,
  maxBackoff: 20000,
});

/**
 * Get all circuit breaker instances for monitoring
 */
export function getAllCircuits(): CircuitBreaker[] {
  return [
    stripeCircuit,
    whatsappCircuit,
    redisCircuit,
    emailCircuit,
  ];
}

/**
 * Get health status of all circuits
 */
export function getCircuitsHealth(): ReturnType<CircuitBreaker["getStats"]>[] {
  return getAllCircuits().map((circuit) => circuit.getStats());
}

export default CircuitBreaker;
