import type { ApiError, ApiRequestOptions, ApiResponse, RequestInterceptor, ResponseInterceptor, ErrorInterceptor } from "./types";

export type { ApiError, ApiRequestOptions, ApiResponse } from "./types";

interface PendingRequest {
  promise: Promise<ApiResponse>;
  timestamp: number;
}

class ApiClient {
  private baseURL: string;
  private requestInterceptors: RequestInterceptor[] = [];
  private responseInterceptors: ResponseInterceptor[] = [];
  private errorInterceptors: ErrorInterceptor[] = [];
  private pendingRequests = new Map<string, PendingRequest>();
  private cache = new Map<string, { data: unknown; timestamp: number; ttl: number }>();
  private readonly DEDUPE_WINDOW = 100; // ms
  private readonly DEFAULT_RETRY_COUNT = 2;
  private readonly DEFAULT_RETRY_DELAY = 1000; // ms

  constructor(baseURL: string = "") {
    this.baseURL = baseURL;
  }

  addRequestInterceptor(interceptor: RequestInterceptor): void {
    this.requestInterceptors.push(interceptor);
  }

  addResponseInterceptor(interceptor: ResponseInterceptor): void {
    this.responseInterceptors.push(interceptor);
  }

  addErrorInterceptor(interceptor: ErrorInterceptor): void {
    this.errorInterceptors.push(interceptor);
  }

  private async applyRequestInterceptors(config: RequestInit): Promise<RequestInit> {
    // Always attach the CSRF header so the middleware accepts mutating requests.
    const headers = new Headers(config.headers);
    headers.set("X-Requested-With", "XMLHttpRequest");
    let result: RequestInit = { ...config, headers };

    for (const interceptor of this.requestInterceptors) {
      result = await interceptor(result);
    }
    return result;
  }

  private async applyResponseInterceptors(response: Response): Promise<Response> {
    let result = response;
    for (const interceptor of this.responseInterceptors) {
      result = await interceptor(result);
    }
    return result;
  }

  private async applyErrorInterceptors(error: ApiError): Promise<ApiError> {
    let result = error;
    for (const interceptor of this.errorInterceptors) {
      result = await interceptor(result);
    }
    return result;
  }

  private getCacheKey(url: string, options: RequestInit): string {
    const method = options.method || "GET";
    const body = options.body ? JSON.stringify(options.body) : "";
    return `${method}:${url}:${body}`;
  }

  private getCachedResponse<T>(cacheKey: string, ttl?: number): T | null {
    const cached = this.cache.get(cacheKey);
    if (!cached) return null;

    if (ttl && Date.now() - cached.timestamp > ttl) {
      this.cache.delete(cacheKey);
      return null;
    }

    return cached.data as T;
  }

  private setCachedResponse<T>(cacheKey: string, data: T, ttl?: number): void {
    if (ttl) {
      this.cache.set(cacheKey, {
        data,
        timestamp: Date.now(),
        ttl,
      });
    }
  }

  private async parseErrorResponse(response: Response): Promise<ApiError> {
    let errorData: { error?: { message?: string; code?: string; details?: unknown } } = {};
    
    try {
      const contentType = response.headers.get("content-type");
      if (contentType?.includes("application/json")) {
        errorData = await response.json();
      }
    } catch {
      // Ignore JSON parse errors
    }

    const message = errorData.error?.message || response.statusText || "An error occurred";
    const code = errorData.error?.code;
    const details = errorData.error?.details;

    return {
      message,
      code,
      details,
      status: response.status,
    };
  }

  private async handleResponse<T>(response: Response, skipErrorHandling: boolean): Promise<ApiResponse<T>> {
    const interceptedResponse = await this.applyResponseInterceptors(response);

    if (!interceptedResponse.ok && !skipErrorHandling) {
      const error = await this.parseErrorResponse(interceptedResponse);
      const interceptedError = await this.applyErrorInterceptors(error);
      throw interceptedError;
    }

    let data: T;
    const contentType = interceptedResponse.headers.get("content-type");
    
    if (contentType?.includes("application/json")) {
      try {
        data = await interceptedResponse.json();
      } catch (err) {
        throw {
          message: "Failed to parse JSON response",
          status: interceptedResponse.status,
        } as ApiError;
      }
    } else {
      data = (await interceptedResponse.text()) as unknown as T;
    }

    return {
      data,
      status: interceptedResponse.status,
      headers: interceptedResponse.headers,
    };
  }

  private async fetchWithRetry<T>(
    url: string,
    options: ApiRequestOptions,
    retryCount: number = 0,
  ): Promise<ApiResponse<T>> {
    try {
      const config = await this.applyRequestInterceptors(options);
      const response = await fetch(url, config);
      return await this.handleResponse<T>(response, options.skipErrorHandling ?? false);
    } catch (error) {
      const maxRetries = options.retryCount ?? this.DEFAULT_RETRY_COUNT;
      const shouldRetry = !options.skipRetry && retryCount < maxRetries && this.isRetryableError(error);

      if (shouldRetry) {
        const delay = this.calculateRetryDelay(retryCount, options.retryDelay);
        await this.sleep(delay);
        return this.fetchWithRetry<T>(url, options, retryCount + 1);
      }

      // If it's already an ApiError, throw it
      if (error && typeof error === "object" && "status" in error) {
        throw error;
      }

      // Otherwise, wrap it
      throw {
        message: error instanceof Error ? error.message : "Network error",
        status: 0,
      } as ApiError;
    }
  }

  private isRetryableError(error: unknown): boolean {
    if (error && typeof error === "object" && "status" in error) {
      const status = (error as ApiError).status;
      // Retry on network errors (status 0) or 5xx errors
      return status === 0 || (status >= 500 && status < 600);
    }
    // Network errors (no status) are retryable
    return true;
  }

  private calculateRetryDelay(retryCount: number, baseDelay?: number): number {
    const delay = baseDelay ?? this.DEFAULT_RETRY_DELAY;
    // Exponential backoff: delay * 2^retryCount
    return delay * Math.pow(2, retryCount);
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  async request<T = unknown>(url: string, options: ApiRequestOptions = {}): Promise<ApiResponse<T>> {
    const fullUrl = url.startsWith("http") ? url : `${this.baseURL}${url}`;
    const cacheKey = this.getCacheKey(fullUrl, options);
    const method = options.method || "GET";

    // Check cache for GET requests
    if (method === "GET" && options.cache !== "no-store") {
      const cached = this.getCachedResponse<T>(cacheKey, options.cacheTTL);
      if (cached !== null) {
        return {
          data: cached,
          status: 200,
          headers: new Headers(),
        };
      }
    }

    // Request deduplication
    const pending = this.pendingRequests.get(cacheKey);
    if (pending && Date.now() - pending.timestamp < this.DEDUPE_WINDOW) {
      return pending.promise as Promise<ApiResponse<T>>;
    }

    // Make the request
    const promise = this.fetchWithRetry<T>(fullUrl, {
      ...options,
      cache: options.cache || "default",
    });

    // Store pending request for deduplication
    this.pendingRequests.set(cacheKey, {
      promise: promise as Promise<ApiResponse>,
      timestamp: Date.now(),
    });

    try {
      const response = await promise;

      // Cache successful GET responses
      if (method === "GET" && response.status === 200 && options.cache !== "no-store") {
        this.setCachedResponse(cacheKey, response.data, options.cacheTTL);
      }

      return response;
    } finally {
      // Clean up pending request after a delay
      setTimeout(() => {
        this.pendingRequests.delete(cacheKey);
      }, this.DEDUPE_WINDOW);
    }
  }

  async get<T = unknown>(url: string, options?: Omit<ApiRequestOptions, "method" | "body">): Promise<ApiResponse<T>> {
    return this.request<T>(url, { ...options, method: "GET" });
  }

  async post<T = unknown>(url: string, data?: unknown, options?: Omit<ApiRequestOptions, "method">): Promise<ApiResponse<T>> {
    const isFormData = data instanceof FormData;
    return this.request<T>(url, {
      ...options,
      method: "POST",
      body: isFormData ? data : JSON.stringify(data),
      headers: isFormData
        ? options?.headers
        : {
            ...options?.headers,
            "Content-Type": "application/json",
          },
    });
  }

  async put<T = unknown>(url: string, data?: unknown, options?: Omit<ApiRequestOptions, "method">): Promise<ApiResponse<T>> {
    const isFormData = data instanceof FormData;
    return this.request<T>(url, {
      ...options,
      method: "PUT",
      body: isFormData ? data : JSON.stringify(data),
      headers: isFormData
        ? options?.headers
        : {
            ...options?.headers,
            "Content-Type": "application/json",
          },
    });
  }

  async delete<T = unknown>(url: string, options?: Omit<ApiRequestOptions, "method" | "body">): Promise<ApiResponse<T>> {
    return this.request<T>(url, { ...options, method: "DELETE" });
  }

  clearCache(): void {
    this.cache.clear();
  }

  clearCacheForUrl(url: string): void {
    const fullUrl = url.startsWith("http") ? url : `${this.baseURL}${url}`;
    for (const [key] of this.cache) {
      if (key.includes(fullUrl)) {
        this.cache.delete(key);
      }
    }
  }
}

// Export singleton instance
export const apiClient = new ApiClient();

// Export class for testing or custom instances
export { ApiClient };

