/**
 * HTTP Request Manager
 * Handles all HTTP operations, rate limiting, and retries
 */

// Use native fetch in Node.js 18+
import type { HTTPMethod, RequestOptions, ClientStats, WordPressClientConfig } from "@/types/client.js";
import { WordPressAPIError, RateLimitError } from "@/types/client.js";
import { config } from "@/config/Config.js";
import { BaseManager } from "./BaseManager.js";
import { AuthenticationManager } from "./AuthenticationManager.js";
import { LoggerFactory, startTimer } from "@/utils/logger.js";

const log = LoggerFactory.client("REQUEST");
import { getUserAgent } from "@/utils/version.js";
import { assertNoMojibake } from "@/utils/mojibake.js";

export class RequestManager extends BaseManager {
  private stats: ClientStats;
  private lastRequestTime: number = 0;
  private requestInterval: number;
  private authManager: AuthenticationManager;

  constructor(clientConfig: WordPressClientConfig, authManager: AuthenticationManager) {
    super(clientConfig);

    this.authManager = authManager;
    this.requestInterval = 60000 / config().security.rateLimit;
    this.stats = {
      totalRequests: 0,
      successfulRequests: 0,
      failedRequests: 0,
      averageResponseTime: 0,
      rateLimitHits: 0,
      authFailures: 0,
      errors: 0,
    };
  }

  /**
   * Make HTTP request with retry logic and rate limiting
   */
  async request<T>(method: HTTPMethod, endpoint: string, data?: unknown, options: RequestOptions = {}): Promise<T> {
    const timer = startTimer();

    try {
      await this.enforceRateLimit();

      const response = await this.makeRequestWithRetry(method, endpoint, data, options);

      this.stats.successfulRequests++;
      this.updateAverageResponseTime(timer.end());

      return response as T;
    } catch (_error) {
      this.stats.failedRequests++;
      this.handleError(_error, `${method} ${endpoint}`);
    } finally {
      this.stats.totalRequests++;
    }
  }

  /**
   * Make request with retry logic
   */
  private async makeRequestWithRetry<T>(
    method: HTTPMethod,
    endpoint: string,
    data?: unknown,
    options: RequestOptions = {},
  ): Promise<T> {
    let lastError: unknown;
    const maxRetries = options.retries ?? this.config.maxRetries ?? 3;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await this.makeRequest<T>(method, endpoint, data, options);
      } catch (error: unknown) {
        lastError = error;

        // Type guard for error-like objects
        const isErrorLike = (err: unknown): err is { statusCode?: number; message?: string } => {
          return typeof err === "object" && err !== null;
        };

        // Don't retry on authentication errors or client errors
        if ((isErrorLike(error) && error.statusCode && error.statusCode < 500) || attempt === maxRetries) {
          throw error;
        }

        const errorMessage = isErrorLike(error) && error.message ? error.message : String(error);
        log.debug(`Request failed (attempt ${attempt}/${maxRetries})`, { error: errorMessage });

        // Exponential backoff
        const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }

    throw lastError;
  }

  /**
   * Make single HTTP request
   */
  private async makeRequest<T>(
    method: HTTPMethod,
    endpoint: string,
    data?: unknown,
    options: RequestOptions = {},
  ): Promise<T> {
    const url = this.buildUrl(endpoint);
    const timeout = options.timeout ?? this.config.timeout ?? 30000;

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      // Get authentication headers
      const authHeaders = await this.authManager.getAuthHeaders();

      const fetchOptions: RequestInit = {
        method,
        headers: {
          "Content-Type": "application/json",
          "User-Agent": getUserAgent(),
          ...authHeaders, // Add auth headers before custom headers
          ...options.headers,
        },
        signal: controller.signal,
      };

      if (data && method !== "GET") {
        if (
          data instanceof FormData ||
          (typeof data === "object" &&
            "append" in data &&
            typeof (data as Record<string, unknown>).append === "function")
        ) {
          // For FormData, don't set Content-Type (let fetch set it with boundary)
          const headers = fetchOptions.headers as Record<string, string>;
          if ("Content-Type" in headers) {
            delete headers["Content-Type"];
          }
          fetchOptions.body = data as FormData;
        } else if (Buffer.isBuffer(data)) {
          // For Buffer data, keep Content-Type from headers
          fetchOptions.body = data;
        } else if (typeof data === "string") {
          fetchOptions.body = data;
        } else {
          assertNoMojibake(data, "request body");
          fetchOptions.body = JSON.stringify(data);
        }
      }

      log.debug(`API Request: ${method} ${url}`);

      const response = await fetch(url, fetchOptions);

      if (!response.ok) {
        await this.handleErrorResponse(response);
      }

      const buffer = await response.arrayBuffer();
      const body = new TextDecoder("utf-8", { fatal: false }).decode(buffer);
      const responseData = JSON.parse(body);
      return responseData as T;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  /**
   * Handle HTTP error responses
   */
  private async handleErrorResponse(response: Response): Promise<never> {
    let errorData: Record<string, unknown> = {};

    try {
      const jsonData = await response.json();
      if (typeof jsonData === "object" && jsonData !== null) {
        errorData = jsonData as Record<string, unknown>;
      }
    } catch {
      // Ignore JSON parsing errors
    }

    const message =
      (typeof errorData.message === "string" ? errorData.message : undefined) ||
      `HTTP ${response.status}: ${response.statusText}`;
    const code = (typeof errorData.code === "string" ? errorData.code : undefined) || "http_error";

    if (response.status === 429) {
      this.stats.rateLimitHits++;
      throw new RateLimitError(message, Date.now() + 60000); // Retry after 1 minute
    }

    if (response.status === 401 || response.status === 403) {
      this.stats.authFailures++;
    }

    throw new WordPressAPIError(message, response.status, code, errorData);
  }

  /**
   * Build full URL from endpoint
   */
  private buildUrl(endpoint: string): string {
    const baseUrl = this.config.baseUrl.replace(/\/$/, "");
    const apiBase = "/wp-json/wp/v2";
    const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;

    return `${baseUrl}${apiBase}${cleanEndpoint}`;
  }

  /**
   * Enforce rate limiting
   */
  private async enforceRateLimit(): Promise<void> {
    const now = Date.now();
    const timeSinceLastRequest = now - this.lastRequestTime;

    if (timeSinceLastRequest < this.requestInterval) {
      const delay = this.requestInterval - timeSinceLastRequest;
      await new Promise((resolve) => setTimeout(resolve, delay));
    }

    this.lastRequestTime = Date.now();
  }

  /**
   * Update average response time
   */
  private updateAverageResponseTime(responseTime: number): void {
    const totalRequests = this.stats.successfulRequests;
    const currentAverage = this.stats.averageResponseTime;

    this.stats.averageResponseTime = (currentAverage * (totalRequests - 1) + responseTime) / totalRequests;
  }

  /**
   * Get request statistics
   */
  getStats(): ClientStats {
    return { ...this.stats };
  }
}
