import type {
  ClientConfig,
  FollowAPIErrorResponse,
  FollowAPIResponse,
  RequestContentType,
  RequestOptions,
} from "../types"
import {
  FollowAPIError,
  FollowAuthError,
  FollowTimeoutError,
  FollowValidationError,
} from "../types/errors"
import { InterceptorManager } from "./interceptors"

/**
 * Core HTTP client for Follow API using native fetch
 */
export class HttpClient {
  private config: Required<ClientConfig>
  private fetchInstance: typeof fetch
  private interceptors: InterceptorManager

  constructor(config: ClientConfig) {
    this.config = {
      timeout: 30000,
      headers: {},
      credentials: "include",
      fetch: globalThis.fetch,
      ...config,
    }
    this.fetchInstance = this.config.fetch
    this.interceptors = new InterceptorManager()
  }

  /**
   * Build URL with query parameters
   */
  private buildURL(
    path: string,
    query?: Record<string, string | number | boolean>,
  ): string {
    const url = new URL(path, this.config.baseURL)

    if (query) {
      Object.entries(query).forEach(([key, value]) => {
        if (value !== undefined && value !== null) {
          url.searchParams.append(key, String(value))
        }
      })
    }

    return url.toString()
  }

  /**
   * Process request body based on content type
   */
  private processRequestBody(
    body: unknown,
    requestType: RequestContentType = "json",
  ): { processedBody: BodyInit | undefined, headers: Record<string, string> } {
    if (!body) {
      return { processedBody: undefined, headers: {} }
    }

    switch (requestType) {
      case "json": {
        return {
          processedBody: JSON.stringify(body),
          headers: { "Content-Type": "application/json" },
        }
      }

      case "formData": {
        if (body instanceof FormData) {
          return { processedBody: body, headers: {} }
        }
        if (typeof body === "object" && body !== null) {
          const formData = new FormData()
          Object.entries(body).forEach(([key, value]) => {
            if (value instanceof File || value instanceof Blob) {
              formData.append(key, value)
            } else if (value !== undefined && value !== null) {
              formData.append(key, String(value))
            }
          })
          return { processedBody: formData, headers: {} }
        }
        throw new Error("Invalid body type for formData request")
      }

      case "text": {
        return {
          processedBody: typeof body === "string" ? body : String(body),
          headers: { "Content-Type": "text/plain" },
        }
      }

      case "blob": {
        if (body instanceof Blob) {
          return { processedBody: body, headers: {} }
        }
        throw new Error("Body must be a Blob for blob request type")
      }

      case "arrayBuffer": {
        if (body instanceof ArrayBuffer) {
          return {
            processedBody: body,
            headers: { "Content-Type": "application/octet-stream" },
          }
        }
        throw new Error(
          "Body must be an ArrayBuffer for arrayBuffer request type",
        )
      }

      default: {
        throw new Error(`Unsupported request type: ${requestType}`)
      }
    }
  }

  /**
   * Handle response parsing and error handling
   */
  private async handleResponse<T>(
    response: Response,
    originalPath: string,
    finalUrl: string,
    requestOptions: RequestOptions,
  ): Promise<T> {
    const contentType = response.headers.get("content-type") || ""

    if (!response.ok) {
      let errorData: FollowAPIErrorResponse | null = null

      // Try to parse error response
      if (contentType.includes("application/json")) {
        try {
          errorData = await response.json()
        } catch {
          // Fall back to status text if JSON parsing fails
          errorData = { code: response.status, message: response.statusText }
        }
      } else {
        errorData = { code: response.status, message: response.statusText }
      }

      // Extract pathname from final URL for error context
      const finalPathname = new URL(finalUrl).pathname

      // Create detailed error context
      const requestContext = {
        originalPath,
        finalPathname,
        method: requestOptions.method || "GET",
        query: requestOptions.query,
        body: requestOptions.body,
        headers: requestOptions.headers,
      }

      const contextStr = `${requestContext.method} ${finalPathname} (original: ${originalPath})`
      const argsStr = JSON.stringify(
        {
          query: requestContext.query,
          body: requestContext.body,
          headers: requestContext.headers,
        },
        null,
        2,
      )

      // Handle specific error types
      if (response.status === 401) {
        throw new FollowAuthError(
          `${errorData?.message || "Authentication required"}\nRequest: ${contextStr}\nArgs: ${argsStr}`,
          errorData,
        )
      }

      if (response.status === 400 && errorData?.data) {
        throw new FollowValidationError(
          `${errorData.message || "Validation error"}\nRequest: ${contextStr}\nArgs: ${argsStr}`,
          Array.isArray(errorData.data) ? errorData.data : [errorData.data],
        )
      }

      throw new FollowAPIError(
        `${errorData?.message || response.statusText}\nRequest: ${contextStr}\nArgs: ${argsStr}`,
        response.status,
        errorData?.code?.toString(),
        errorData?.data,
      )
    }

    // Handle successful responses based on content type
    return this.parseResponseByContentType<T>(
      response,
      contentType,
    )
  }

  /**
   * Parse response based on content type
   */
  private async parseResponseByContentType<T>(
    response: Response,
    contentType: string,
  ): Promise<T> {
    // Handle event stream
    if (contentType.includes("text/event-stream")) {
      return response as unknown as T
    }

    // Handle JSON responses
    if (contentType.includes("application/json")) {
      const jsonResponse = await response.json()

      // Handle Follow API response format
      if (typeof jsonResponse === "object" && "code" in jsonResponse) {
        const apiResponse = jsonResponse as FollowAPIResponse<T>

        return apiResponse as unknown as T
      }

      return jsonResponse
    }

    // Handle blob responses
    if (
      contentType.includes("application/octet-stream") ||
      contentType.includes("image/") ||
      contentType.includes("video/") ||
      contentType.includes("audio/")
    ) {
      const blobResponse = await response.blob()
      return blobResponse as unknown as T
    }

    // Handle text responses
    if (contentType.includes("text/")) {
      const textResponse = await response.text()
      return textResponse as T
    }

    // Default to arrayBuffer for unknown binary types
    if (contentType.includes("application/")) {
      const arrayBufferResponse = await response.arrayBuffer()
      return arrayBufferResponse as unknown as T
    }

    // Fallback to text
    const textResponse = await response.text()
    return textResponse as T
  }

  /**
   * Make an HTTP request
   */
  async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
    let currentUrl = this.buildURL(path, options.query)
    let currentOptions = options
    let response: Response | null = null

    try {
      // Process request interceptors
      const interceptedRequest = await this.interceptors.processRequest(
        currentUrl,
        currentOptions,
      )
      currentUrl = interceptedRequest.url
      currentOptions = interceptedRequest.options

      const timeout = currentOptions.timeout || this.config.timeout

      // Create abort controller for timeout
      const controller = new AbortController()
      const timeoutId = setTimeout(() => controller.abort(), timeout)

      // Combine signals if user provided one
      let { signal } = controller
      if (currentOptions.signal) {
        const combinedController = new AbortController()
        const cleanup = () => {
          clearTimeout(timeoutId)
          combinedController.abort()
        }

        currentOptions.signal.addEventListener("abort", cleanup)
        controller.signal.addEventListener("abort", cleanup)

        signal = combinedController.signal
      }

      // Process request body based on content type
      const { processedBody, headers: bodyHeaders } = this.processRequestBody(
        currentOptions.body,
        currentOptions.requestType,
      )

      const defaultHeaders: Record<string, string> = processedBody ?
          {} :
          { "Content-Type": "application/json" }

      response = await this.fetchInstance(currentUrl, {
        method: currentOptions.method || "GET",
        headers: {
          ...defaultHeaders,
          ...this.config.headers,
          ...bodyHeaders,
          ...currentOptions.headers,
        },
        credentials: this.config.credentials,
        body: processedBody,
        signal,
      })

      clearTimeout(timeoutId)

      // Process response interceptors
      const interceptedResponse = await this.interceptors.processResponse(
        response,
        currentUrl,
        currentOptions,
      )

      return this.handleResponse<T>(
        interceptedResponse,
        path,
        currentUrl,
        currentOptions,
      )
    } catch (error) {
      const processedError = await this.interceptors.processError(
        error instanceof Error ? error : new Error("Unknown error"),
        response,
        currentUrl,
        currentOptions,
      )

      if (processedError) {
        throw processedError
      }

      // Handle specific error types
      if (error instanceof DOMException && error.name === "AbortError") {
        throw new FollowTimeoutError("Request timeout")
      }

      // Re-throw our custom errors
      if (error instanceof FollowAPIError) {
        throw error
      }

      throw error
    }
  }

  /**
   * Convenience methods for different HTTP verbs
   */
  async get<T>(
    path: string,
    options?: Omit<RequestOptions, "method">,
  ): Promise<T> {
    return this.request<T>(path, { ...options, method: "GET" })
  }

  async post<T>(
    path: string,
    body?: unknown,
    options?: Omit<RequestOptions, "method" | "body">,
  ): Promise<T> {
    return this.request<T>(path, { ...options, method: "POST", body })
  }

  async put<T>(
    path: string,
    body?: unknown,
    options?: Omit<RequestOptions, "method" | "body">,
  ): Promise<T> {
    return this.request<T>(path, { ...options, method: "PUT", body })
  }

  async patch<T>(
    path: string,
    body?: unknown,
    options?: Omit<RequestOptions, "method" | "body">,
  ): Promise<T> {
    return this.request<T>(path, { ...options, method: "PATCH", body })
  }

  /**
   * Convenience method for form data uploads
   */
  async postForm<T>(
    path: string,
    formData: FormData | Record<string, unknown>,
    options?: Omit<RequestOptions, "method" | "body" | "requestType">,
  ): Promise<T> {
    return this.request<T>(path, {
      ...options,
      method: "POST",
      body: formData,
      requestType: "formData",
    })
  }

  /**
   * Convenience method for event stream responses
   */
  async getStream(
    path: string,
    options?: Omit<RequestOptions, "method">,
  ): Promise<Response> {
    return this.request<Response>(path, { ...options, method: "GET" })
  }

  async delete<T>(
    path: string,
    options?: Omit<RequestOptions, "method">,
  ): Promise<T> {
    return this.request<T>(path, { ...options, method: "DELETE" })
  }

  /**
   * Update client configuration
   */
  setConfig(config: Partial<ClientConfig>): void {
    this.config = { ...this.config, ...config }
    if (config.fetch) {
      this.fetchInstance = config.fetch
    }
  }

  /**
   * Set additional headers
   */
  setHeaders(headers: Record<string, string>): void {
    this.config.headers = { ...this.config.headers, ...headers }
  }

  /**
   * Set custom fetch instance
   */
  setFetch(fetchInstance: typeof fetch): void {
    this.fetchInstance = fetchInstance
    this.config.fetch = fetchInstance
  }

  /**
   * Get current configuration (readonly)
   */
  getConfig(): Readonly<Required<ClientConfig>> {
    return { ...this.config }
  }

  /**
   * Get interceptor manager for advanced usage
   */
  getInterceptors(): InterceptorManager {
    return this.interceptors
  }
}
