import { Injectable, Inject } from "@nestjs/common";
import type { OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config";
import { CacheService } from "@bff/infra/cache/cache.service.js";
import {
  SalesforceQueueMetricsService,
  type SalesforceRouteMetricsSnapshot,
} from "./salesforce-queue-metrics.service.js";
import {
  SalesforceQueueDegradationService,
  type SalesforceDegradationSnapshot,
} from "./salesforce-queue-degradation.service.js";
import { generateRequestId } from "@bff/core/logging/request-id.util.js";

export interface SalesforceQueueMetrics {
  totalRequests: number;
  completedRequests: number;
  failedRequests: number;
  queueSize: number;
  pendingRequests: number;
  averageWaitTime: number;
  averageExecutionTime: number;
  dailyApiUsage: number;
  lastRequestTime?: Date | undefined;
  lastErrorTime?: Date | undefined;
  lastRateLimitTime?: Date | undefined;
  dailyApiLimit?: number | undefined;
  dailyUsagePercent?: number | undefined;
  routeBreakdown?: Record<string, SalesforceRouteMetricsSnapshot> | undefined;
  degradation?: SalesforceDegradationSnapshot | undefined;
}

export interface SalesforceRequestOptions {
  priority?: number;
  timeout?: number;
  retryAttempts?: number;
  retryDelay?: number;
  isLongRunning?: boolean;
  label?: string;
}

type PQueueCtor = new (options: {
  concurrency?: number;
  interval?: number;
  intervalCap?: number;
  timeout?: number;
  throwOnTimeout?: boolean;
  carryoverConcurrencyCount?: boolean;
}) => PQueueInstance;

interface PQueueInstance {
  add<T>(fn: () => Promise<T>, options?: { priority?: number }): Promise<T>;
  clear(): void;
  onIdle(): Promise<void>;
  on(event: "add" | "next" | "idle" | "error", listener: (...args: unknown[]) => void): void;
  size: number;
  pending: number;
}

interface SalesforceDailyUsageCacheEntry {
  usage: number;
  resetTime: string;
}

/**
 * Salesforce Request Queue Service
 *
 * Manages concurrent requests to Salesforce API to prevent:
 * - Daily API limit exhaustion (100,000 + 1,000 per user)
 * - Concurrent request limit violations (25 long-running requests)
 * - Rate limit violations and 429 errors
 *
 * Delegates metrics collection to SalesforceQueueMetricsService
 * Delegates degradation logic to SalesforceQueueDegradationService
 */
@Injectable()
export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDestroy {
  private pQueueCtor: PQueueCtor | null = null;
  private standardQueue: PQueueInstance | null = null;
  private longRunningQueue: PQueueInstance | null = null;
  private dailyUsageResetTime: Date;
  private readonly dailyUsageCacheKey = "salesforce:queue:daily-usage";

  constructor(
    @Inject(Logger) private readonly logger: Logger,
    private readonly configService: ConfigService,
    private readonly cache: CacheService,
    private readonly metricsService: SalesforceQueueMetricsService,
    private readonly degradationService: SalesforceQueueDegradationService
  ) {
    this.dailyUsageResetTime = this.getNextDayReset();
  }

  private async loadPQueue(): Promise<PQueueCtor> {
    if (!this.pQueueCtor) {
      const module = await import("p-queue");
      this.pQueueCtor = module.default as PQueueCtor;
    }
    return this.pQueueCtor;
  }

  private async ensureQueuesInitialized(): Promise<{
    standardQueue: PQueueInstance;
    longRunningQueue: PQueueInstance;
  }> {
    if (!this.standardQueue || !this.longRunningQueue) {
      const PQueue = await this.loadPQueue();
      const concurrency = this.configService.get<number>("SF_QUEUE_CONCURRENCY", 15);
      const longRunningConcurrency = this.configService.get<number>(
        "SF_QUEUE_LONG_RUNNING_CONCURRENCY",
        22
      );
      const intervalCap = this.configService.get<number>("SF_QUEUE_INTERVAL_CAP", 600);
      const timeout = this.configService.get<number>("SF_QUEUE_TIMEOUT_MS", 30000);
      const longRunningTimeout = this.configService.get<number>(
        "SF_QUEUE_LONG_RUNNING_TIMEOUT_MS",
        600000
      );

      this.standardQueue = new PQueue({
        concurrency,
        interval: 60000,
        intervalCap,
        timeout,
        throwOnTimeout: true,
        carryoverConcurrencyCount: true,
      });

      this.longRunningQueue = new PQueue({
        concurrency: longRunningConcurrency,
        timeout: longRunningTimeout,
        throwOnTimeout: true,
        carryoverConcurrencyCount: true,
      });

      this.setupQueueListeners();
    }

    const standardQueue = this.standardQueue;
    const longRunningQueue = this.longRunningQueue;

    if (!standardQueue || !longRunningQueue) {
      throw new Error("Failed to initialize Salesforce queues");
    }

    return { standardQueue, longRunningQueue };
  }

  async onModuleInit() {
    await this.ensureQueuesInitialized();
    await this.restoreDailyUsageFromCache();

    const concurrency = this.configService.get<number>("SF_QUEUE_CONCURRENCY", 15);
    const longRunningConcurrency = this.configService.get<number>(
      "SF_QUEUE_LONG_RUNNING_CONCURRENCY",
      22
    );
    const intervalCap = this.configService.get<number>("SF_QUEUE_INTERVAL_CAP", 600);
    const timeout = this.configService.get<number>("SF_QUEUE_TIMEOUT_MS", 30000);
    const longRunningTimeout = this.configService.get<number>(
      "SF_QUEUE_LONG_RUNNING_TIMEOUT_MS",
      600000
    );

    this.logger.log("Salesforce Request Queue initialized", {
      standardConcurrency: concurrency,
      longRunningConcurrency,
      rateLimit: `${intervalCap} requests/minute (${(intervalCap / 60).toFixed(1)} RPS)`,
      standardTimeout: `${timeout / 1000} seconds`,
      longRunningTimeout: `${longRunningTimeout / 60000} minutes`,
    });
  }

  async onModuleDestroy() {
    const standardQueue = this.standardQueue;
    const longRunningQueue = this.longRunningQueue;

    if (!standardQueue || !longRunningQueue) {
      this.logger.debug("Salesforce Request Queue destroyed before initialization");
      return;
    }

    this.logger.log("Shutting down Salesforce Request Queue", {
      standardPending: standardQueue.pending,
      standardQueueSize: standardQueue.size,
      longRunningPending: longRunningQueue.pending,
      longRunningQueueSize: longRunningQueue.size,
    });

    try {
      await Promise.all([standardQueue.onIdle(), longRunningQueue.onIdle()]);
    } catch (error) {
      this.logger.warn("Some Salesforce requests may not have completed during shutdown", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  /**
   * Execute a Salesforce API request through the appropriate queue
   */
  async execute<T>(
    requestFn: () => Promise<T>,
    options: SalesforceRequestOptions = {}
  ): Promise<T> {
    const { standardQueue, longRunningQueue } = await this.ensureQueuesInitialized();
    this.checkDailyUsage();

    const startTime = Date.now();
    const requestId = this.generateRequestId();
    const isLongRunning = options.isLongRunning || false;
    const queue = isLongRunning ? longRunningQueue : standardQueue;
    const label = this.metricsService.normalizeLabel(options.label);

    try {
      const result = (await queue.add(
        async () => {
          this.metricsService.incrementTotalRequests();
          this.metricsService.incrementDailyApiUsage();
          void this.persistDailyUsage();
          this.metricsService.recordRouteStart(label);
          this.updateQueueMetrics();
          this.degradationService.maybeWarnOnUsage();

          this.logger.debug("Executing Salesforce request", {
            requestId,
            isLongRunning,
            queueSize: queue.size,
            pending: queue.pending,
            priority: options.priority || 0,
            dailyUsage: this.metricsService.getDailyApiUsage(),
            label,
          });

          const waitTime = Date.now() - startTime;
          this.metricsService.recordWaitTime(waitTime);

          const executionStart = Date.now();

          try {
            const response = await this.executeWithRetry(requestFn, options);

            const executionTime = Date.now() - executionStart;
            this.metricsService.recordExecutionTime(executionTime);
            this.metricsService.incrementCompletedRequests();
            this.metricsService.recordRouteSuccess(label);

            this.logger.debug("Salesforce request completed", {
              requestId,
              isLongRunning,
              waitTime,
              executionTime,
              totalTime: Date.now() - startTime,
              label,
            });

            return response;
          } catch (error) {
            const executionTime = Date.now() - executionStart;
            this.metricsService.recordExecutionTime(executionTime);
            this.metricsService.incrementFailedRequests();
            this.metricsService.recordRouteFailure(label);

            if (this.isRateLimitError(error)) {
              this.metricsService.recordRateLimitTime();
              this.degradationService.activateDegradeWindow("rate-limit");
              this.logger.warn("Salesforce rate limit encountered", {
                requestId,
                dailyUsage: this.metricsService.getDailyApiUsage(),
                label,
              });
            }

            this.logger.error(
              {
                requestId,
                isLongRunning,
                waitTime,
                executionTime,
                error: error instanceof Error ? error.message : String(error),
                label,
              },
              "Salesforce request failed"
            );

            throw error;
          }
        },
        {
          priority: options.priority ?? 0,
        }
      )) as T;

      return result;
    } finally {
      this.updateQueueMetrics();
    }
  }

  /**
   * Execute high-priority Salesforce request (jumps queue)
   */
  async executeHighPriority<T>(
    requestFn: () => Promise<T>,
    options: SalesforceRequestOptions = {}
  ): Promise<T> {
    return this.execute(requestFn, {
      ...options,
      priority: options.priority ?? 10,
    });
  }

  /**
   * Execute long-running Salesforce request (uses separate queue)
   */
  async executeLongRunning<T>(
    requestFn: () => Promise<T>,
    options: SalesforceRequestOptions = {}
  ): Promise<T> {
    return this.execute(requestFn, {
      ...options,
      isLongRunning: true,
      timeout: options.timeout ?? 600000,
    });
  }

  /**
   * Get current queue metrics
   */
  getMetrics(): SalesforceQueueMetrics {
    this.updateQueueMetrics();
    const dailyApiLimit = this.degradationService.getDailyApiLimit();
    const dailyUsagePercent =
      dailyApiLimit > 0 ? this.metricsService.getDailyApiUsage() / dailyApiLimit : 0;

    return {
      ...this.metricsService.getMetrics(),
      dailyApiLimit,
      dailyUsagePercent,
      routeBreakdown: this.metricsService.getRouteMetricsSnapshot(),
      degradation: this.degradationService.getDegradationState(),
    };
  }

  /**
   * Get queue health status
   */
  getHealthStatus(): {
    status: "healthy" | "degraded" | "unhealthy";
    queueSize: number;
    pendingRequests: number;
    errorRate: number;
    dailyUsagePercent: number;
    averageWaitTime: number;
  } {
    this.updateQueueMetrics();
    const metrics = this.metricsService.getMetrics();
    const errorRate = this.metricsService.getErrorRate();
    const dailyApiLimit = this.degradationService.getDailyApiLimit();
    const effectiveDailyLimit = dailyApiLimit > 0 ? dailyApiLimit : 150000;
    const dailyUsagePercent =
      effectiveDailyLimit > 0 ? metrics.dailyApiUsage / effectiveDailyLimit : 0;

    let status: "healthy" | "degraded" | "unhealthy" = "healthy";

    if (metrics.queueSize > 200 || errorRate > 0.1 || dailyUsagePercent > 0.9) {
      status = "unhealthy";
    } else if (metrics.queueSize > 80 || errorRate > 0.05 || dailyUsagePercent > 0.7) {
      status = "degraded";
    }

    return {
      status,
      queueSize: metrics.queueSize,
      pendingRequests: metrics.pendingRequests,
      errorRate,
      dailyUsagePercent,
      averageWaitTime: metrics.averageWaitTime,
    };
  }

  /**
   * Get daily API usage information
   */
  getDailyUsage(): {
    usage: number;
    resetTime: Date;
    hoursUntilReset: number;
    limit: number;
    usagePercent: number;
  } {
    const limit = this.degradationService.getDailyApiLimit();
    const usage = this.metricsService.getDailyApiUsage();
    const usagePercent = limit > 0 ? usage / limit : 0;
    return {
      usage,
      resetTime: this.dailyUsageResetTime,
      hoursUntilReset: Math.ceil(
        (this.dailyUsageResetTime.getTime() - Date.now()) / (1000 * 60 * 60)
      ),
      limit,
      usagePercent,
    };
  }

  /**
   * Clear the queues (emergency use only)
   */
  async clearQueues(): Promise<void> {
    const { standardQueue, longRunningQueue } = await this.ensureQueuesInitialized();

    this.logger.warn("Clearing Salesforce request queues", {
      standardQueueSize: standardQueue.size,
      standardPending: standardQueue.pending,
      longRunningQueueSize: longRunningQueue.size,
      longRunningPending: longRunningQueue.pending,
    });

    standardQueue.clear();
    longRunningQueue.clear();

    await Promise.all([standardQueue.onIdle(), longRunningQueue.onIdle()]);
  }

  /**
   * Check if queue is in degraded state
   */
  isDegraded(): boolean {
    return this.degradationService.isDegraded();
  }

  /**
   * Check if low priority requests should be throttled
   */
  shouldThrottleLowPriority(): boolean {
    return this.degradationService.shouldThrottleLowPriority();
  }

  /**
   * Get degradation state
   */
  getDegradationState(): SalesforceDegradationSnapshot {
    return this.degradationService.getDegradationState();
  }

  private async executeWithRetry<T>(
    requestFn: () => Promise<T>,
    options: SalesforceRequestOptions
  ): Promise<T> {
    const maxAttempts = options.retryAttempts || 3;
    const baseDelay = options.retryDelay || 1000;

    let lastError: Error | undefined;

    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        // eslint-disable-next-line no-await-in-loop -- Retry with backoff requires sequential attempts
        const result = await requestFn();
        return result;
      } catch (error) {
        lastError = error instanceof Error ? error : new Error(String(error));

        if (attempt === maxAttempts) {
          break;
        }

        let delay = baseDelay * Math.pow(2, attempt - 1);

        if (this.isRateLimitError(error)) {
          delay = Math.max(delay, 30000);
        }

        delay += Math.random() * 1000;

        this.logger.debug("Salesforce request failed, retrying", {
          attempt,
          maxAttempts,
          delay,
          isRateLimit: this.isRateLimitError(error),
          error: lastError.message,
        });

        // eslint-disable-next-line no-await-in-loop -- Exponential backoff delay between retries
        await new Promise(resolve => {
          setTimeout(resolve, delay);
        });
      }
    }

    throw lastError instanceof Error ? lastError : new Error(String(lastError));
  }

  private isRateLimitError(error: unknown): boolean {
    if (error instanceof Error) {
      const message = error.message.toLowerCase();
      return (
        message.includes("rate limit") ||
        message.includes("too many requests") ||
        message.includes("429") ||
        message.includes("request limit exceeded")
      );
    }
    return false;
  }

  private checkDailyUsage(): void {
    const now = new Date();

    if (now >= this.dailyUsageResetTime) {
      this.metricsService.resetDailyApiUsage();
      this.dailyUsageResetTime = this.getNextDayReset();
      this.degradationService.resetUsageWarnings();

      this.logger.log("Daily Salesforce API usage reset", {
        resetTime: this.dailyUsageResetTime,
        dailyApiLimit: this.degradationService.getDailyApiLimit(),
      });
      void this.persistDailyUsage();
    }
  }

  private getNextDayReset(): Date {
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    tomorrow.setHours(0, 0, 0, 0);
    return tomorrow;
  }

  private setupQueueListeners(): void {
    if (!this.standardQueue || !this.longRunningQueue) {
      return;
    }

    this.standardQueue.on("add", () => this.updateQueueMetrics());
    this.standardQueue.on("next", () => this.updateQueueMetrics());
    this.standardQueue.on("idle", () => {
      this.logger.debug("Salesforce standard queue is idle");
      this.updateQueueMetrics();
    });
    this.standardQueue.on("error", (error: unknown) => {
      this.logger.error(
        {
          error: error instanceof Error ? error.message : String(error),
        },
        "Salesforce standard queue error"
      );
    });

    this.longRunningQueue.on("add", () => this.updateQueueMetrics());
    this.longRunningQueue.on("next", () => this.updateQueueMetrics());
    this.longRunningQueue.on("idle", () => {
      this.logger.debug("Salesforce long-running queue is idle");
      this.updateQueueMetrics();
    });
    this.longRunningQueue.on("error", (error: unknown) => {
      this.logger.error(
        {
          error: error instanceof Error ? error.message : String(error),
        },
        "Salesforce long-running queue error"
      );
    });
  }

  private updateQueueMetrics(): void {
    if (!this.standardQueue || !this.longRunningQueue) {
      return;
    }

    this.degradationService.clearDegradeWindowIfElapsed();

    const queueSize = this.standardQueue.size + this.longRunningQueue.size;
    const pendingRequests = this.standardQueue.pending + this.longRunningQueue.pending;

    this.metricsService.updateQueueStats(queueSize, pendingRequests);

    this.degradationService.maybeTriggerQueuePressure(
      queueSize,
      pendingRequests,
      this.metricsService.getAverageWaitTime()
    );
  }

  private generateRequestId(): string {
    return generateRequestId("sf");
  }

  private async restoreDailyUsageFromCache(): Promise<void> {
    try {
      const snapshot = await this.cache.get<SalesforceDailyUsageCacheEntry | null>(
        this.dailyUsageCacheKey
      );

      if (!snapshot) {
        await this.persistDailyUsage();
        return;
      }

      const resetTime =
        typeof snapshot.resetTime === "string" ? new Date(snapshot.resetTime) : null;
      const resetTimestamp = resetTime?.getTime();

      if (
        !resetTime ||
        resetTimestamp === undefined ||
        Number.isNaN(resetTimestamp) ||
        Date.now() >= resetTimestamp
      ) {
        this.metricsService.resetDailyApiUsage();
        this.dailyUsageResetTime = this.getNextDayReset();
        this.degradationService.resetUsageWarnings();
        await this.persistDailyUsage();
        return;
      }

      this.metricsService.setDailyApiUsage(
        typeof snapshot.usage === "number" && snapshot.usage >= 0 ? snapshot.usage : 0
      );
      this.dailyUsageResetTime = resetTime;
      await this.persistDailyUsage();
    } catch (error) {
      this.logger.warn("Failed to restore Salesforce daily usage snapshot", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  private async persistDailyUsage(): Promise<void> {
    try {
      const ttlSeconds = this.calculateDailyUsageTtlSeconds();
      await this.cache.set(
        this.dailyUsageCacheKey,
        {
          usage: this.metricsService.getDailyApiUsage(),
          resetTime: this.dailyUsageResetTime.toISOString(),
        },
        ttlSeconds
      );
    } catch (error) {
      this.logger.warn("Failed to persist Salesforce daily usage snapshot", {
        error: error instanceof Error ? error.message : String(error),
      });
    }
  }

  private calculateDailyUsageTtlSeconds(): number {
    const millisUntilReset = this.dailyUsageResetTime.getTime() - Date.now();
    if (millisUntilReset <= 0) {
      return 60;
    }
    return Math.max(60, Math.ceil(millisUntilReset / 1000));
  }
}
