/**
 * this file holds functions to manage the http client
 */
import axios from "axios"
import log from "../lib/logger"
import { rememberLastDataUpdateRetrieval } from "@racletteCore/lib/dataHelper"

const DEFAULT_TIMEOUT = 10000

// Axios client (existing implementation)
const axiosClient = axios.create({
  headers: {
    "Accept-Version": "*",
  },
  validateStatus: (status) => status >= 200 && status < 300,
  withCredentials: true, // Required for CORS with credentials
})

// Fetch client configuration
interface FetchConfig {
  baseURL?: string
  headers?: Record<string, string>
  timeout?: number
}

interface FetchInterceptors {
  request: ((
    config: RequestInit & { url?: string },
  ) => RequestInit & { url?: string })[]
  response: ((response: Response) => Response | Promise<Response>)[]
}

interface FetchClientInterface {
  defaults: {
    baseURL: string
    headers: {
      common: Record<string, string>
    }
    timeout: number
  }
  interceptors: {
    request: {
      use: (
        interceptor: (
          config: RequestInit & { url?: string },
        ) => RequestInit & { url?: string },
      ) => void
    }
    response: {
      use: (
        interceptor: (response: Response) => Response | Promise<Response>,
      ) => void
    }
  }
  get(url: string, config?: RequestInit): Promise<Response>
  post(url: string, data?: any, config?: RequestInit): Promise<Response>
  put(url: string, data?: any, config?: RequestInit): Promise<Response>
  patch(url: string, data?: any, config?: RequestInit): Promise<Response>
  delete(url: string, data?: any, config?: RequestInit): Promise<Response>
}

export type { FetchClientInterface }

class FetchClient implements FetchClientInterface {
  private config: FetchConfig = {
    headers: {
      "Accept-Version": "*",
    },
    timeout: DEFAULT_TIMEOUT,
  }

  private interceptorHandlers: FetchInterceptors = {
    request: [],
    response: [],
  }

  defaults = {
    baseURL: "",
    headers: {
      common: {} as Record<string, string>,
    },
    timeout: DEFAULT_TIMEOUT,
  }

  private isStreamingRequest(
    options: RequestInit & { responseType?: string },
  ): boolean {
    return options.responseType === "stream"
  }

  private isStreamingResponse(response: Response): boolean {
    const contentType = response.headers.get("content-type") || ""

    return (
      contentType.includes("text/event-stream") ||
      contentType.includes("text/plain")
    )
  }

  private async request(
    url: string,
    options: RequestInit & { responseType?: string } = {},
  ): Promise<Response> {
    // Check if this is a streaming request
    const isStreaming = this.isStreamingRequest(options)

    // Prepare full URL
    const fullUrl = url.startsWith("http")
      ? url
      : `${this.defaults.baseURL}${url}`

    // Merge headers
    const headers = {
      ...this.config.headers,
      ...this.defaults.headers.common,
      ...options.headers,
    }

    // Remove responseType from options as it's not a standard fetch option
    const { responseType, ...fetchOptions } = options

    // Prepare request config
    let requestConfig = {
      ...fetchOptions,
      url: fullUrl,
      headers,
      credentials: "include",
    }

    // Apply request interceptors
    for (const interceptor of this.interceptorHandlers.request) {
      requestConfig = interceptor(requestConfig)
    }

    // Extract url back from config (in case interceptor modified it)
    const { url: finalUrl, ...finalFetchOptions } = requestConfig

    // Create AbortController for timeout (but use longer timeout for streaming)
    const controller = new AbortController()
    const timeoutDuration = isStreaming ? 300000 : this.defaults.timeout // 5 minutes for streaming
    const timeoutId = setTimeout(() => controller.abort(), timeoutDuration)

    try {
      let response = await fetch(finalUrl, {
        ...finalFetchOptions,
        signal: controller.signal,
      })

      clearTimeout(timeoutId)

      // For streaming responses, skip response interceptors and validation
      // to avoid consuming the stream
      if (isStreaming || this.isStreamingResponse(response)) {
        log.api(`streaming response ${response.url}`, response)
        return response
      }

      // Apply response interceptors for non-streaming responses
      for (const interceptor of this.interceptorHandlers.response) {
        response = await interceptor(response)
      }

      // Validate status (similar to axios validateStatus)
      if (response.status < 200 || response.status >= 300) {
        throw new Error(`HTTP Error: ${response.status} ${response.statusText}`)
      }

      return response
    } catch (error) {
      clearTimeout(timeoutId)
      if (error.name === "AbortError") {
        throw new Error(`Request timeout after ${timeoutDuration}ms`)
      }
      throw error
    }
  }

  async get(
    url: string,
    config?: RequestInit & { responseType?: string },
  ): Promise<Response> {
    return this.request(url, { ...config, method: "GET" })
  }

  async post(
    url: string,
    data?: any,
    config?: RequestInit & { responseType?: string },
  ): Promise<Response> {
    const body = data ? JSON.stringify(data) : undefined
    const headers = {
      "Content-Type": "application/json",
      ...config?.headers,
    }
    return this.request(url, { ...config, method: "POST", body, headers })
  }

  async put(
    url: string,
    data?: any,
    config?: RequestInit & { responseType?: string },
  ): Promise<Response> {
    const body = data ? JSON.stringify(data) : undefined
    const headers = {
      "Content-Type": "application/json",
      ...config?.headers,
    }
    return this.request(url, { ...config, method: "PUT", body, headers })
  }

  async patch(
    url: string,
    data?: any,
    config?: RequestInit & { responseType?: string },
  ): Promise<Response> {
    const body = data ? JSON.stringify(data) : undefined
    const headers = {
      "Content-Type": "application/json",
      ...config?.headers,
    }
    return this.request(url, { ...config, method: "PATCH", body, headers })
  }

  async delete(
    url: string,
    data?: any,
    config?: RequestInit & { responseType?: string },
  ): Promise<Response> {
    const isFormData =
      typeof FormData !== "undefined" && data instanceof FormData
    const body = data
      ? isFormData
        ? data
        : JSON.stringify(data)
      : undefined
    const headers = isFormData
      ? { ...config?.headers }
      : {
          "Content-Type": "application/json",
          ...config?.headers,
        }
    return this.request(url, { ...config, method: "DELETE", body, headers })
  }

  // Interceptor management (similar to axios)
  public interceptors = {
    request: {
      use: (
        interceptor: (
          config: RequestInit & { url?: string },
        ) => RequestInit & { url?: string },
      ) => {
        this.interceptorHandlers.request.push(interceptor)
      },
    },
    response: {
      use: (
        interceptor: (response: Response) => Response | Promise<Response>,
      ) => {
        this.interceptorHandlers.response.push(interceptor)
      },
    },
  }
}

const fetchClient = new FetchClient()

const configureHttpInterceptors = ($store) => {
  // Configure axios interceptors
  axiosClient.interceptors.request.use((request) => {
    request.headers["current-project"] = $store.getState(["project", "_id"])
    log.api(`request ${request.method}:${request.url}`, request)
    return request
  })

  axiosClient.interceptors.response.use((response) => {
    log.api(`response ${response.request.responseURL}`, response)
    const projectId = $store.getState(["project", "_id"])
    if (response?.status === 200 && response?.data?.appRequestCode) {
      rememberLastDataUpdateRetrieval(projectId)
      log.api("Last Update Timestamp set for " + projectId, "")
    }
    return response
  })

  // Configure fetch interceptors
  fetchClient.interceptors.request.use((config) => {
    config.headers = {
      ...config.headers,
      "current-project": $store.getState(["project", "_id"]),
    }
    log.api(`request ${config.url}`, config)
    return config
  })

  fetchClient.interceptors.response.use(async (response) => {
    log.api(`response ${response.url}`, response)
    const projectId = $store.getState(["project", "_id"])

    if (response.status === 200) {
      // Clone response to read body without consuming it
      const clonedResponse = response.clone()
      try {
        const data = await clonedResponse.json()
        if (data?.appRequestCode) {
          rememberLastDataUpdateRetrieval(projectId)
          log.api("Last Update Timestamp set for " + projectId, "")
        }
      } catch (e) {
        // Ignore JSON parsing errors for non-JSON responses
      }
    }

    return response
  })
}

const configureClient = (
  baseURL: string,
  appSessionId: string,
  authToken?: string | null,
) => {
  // Configure axios
  axiosClient.defaults.baseURL = baseURL
  axiosClient.defaults.headers.common["app-session-id"] = appSessionId
  axiosClient.defaults.headers.common["Accept-Version"] = "*"
  axiosClient.defaults.headers.common["HTTP-CLIENT-USED"] = "axios"

  // Configure fetch
  fetchClient.defaults.baseURL = baseURL
  fetchClient.defaults.headers.common["app-session-id"] = appSessionId
  fetchClient.defaults.headers.common["Accept-Version"] = "*"
  fetchClient.defaults.headers.common["HTTP-CLIENT-USED"] = "fetch"

  if (authToken) {
    setHttpToken(authToken)
  }
}

/**
 * @param {object} client - axios instance
 */
const apiHealthcheck = async (store) => {
  try {
    const healthcheckResponse = await axiosClient.get("/health")

    if (healthcheckResponse.data?.stats) {
      store.dispatch({
        type: "backend/socket/status",
        payload: { status: 1, health: 1 },
      })
    }
  } catch (err) {
    store.dispatch({
      type: "backend/socket/status",
      payload: { status: 0 },
    })
    console.error("Can't connect to API")
    console.error(err)
  }
}

/**
 * Set JWT token in http client so we don't have to care about it
 * @param {string} token - the JWT token
 */
const setHttpToken = (token) => {
  // Set for axios
  axiosClient.defaults.headers.common.Authorization = `Bearer ${token}`
  axiosClient.defaults.headers.Authorization = `Bearer ${token}`

  // Set for fetch
  fetchClient.defaults.headers.common.Authorization = `Bearer ${token}`
}

/**
 * Unset a previously set token
 */
const unsetHttpToken = () => {
  // Unset for axios
  axiosClient.defaults.headers.common.Authorization = null
  axiosClient.defaults.headers.Authorization = null

  // Unset for fetch
  delete fetchClient.defaults.headers.common.Authorization
}

const WORK_SESSION_HEADER = "x-work-session-id"

/**
 * Set work session ID for unauthenticated shared sessions.
 * When set, all API requests will include this header so backend can allow access via authenticatedOrWorksession.
 * @param sessionId - Work session UUID or null to clear
 */
const setWorkSessionId = (sessionId: string | null) => {
  if (sessionId) {
    axiosClient.defaults.headers.common[WORK_SESSION_HEADER] = sessionId
    fetchClient.defaults.headers.common[WORK_SESSION_HEADER] = sessionId
  } else {
    delete axiosClient.defaults.headers.common[WORK_SESSION_HEADER]
    delete fetchClient.defaults.headers.common[WORK_SESSION_HEADER]
  }
}

// Main export object with both clients
const httpClient: {
  axios: typeof axiosClient
  fetch: FetchClientInterface
} = {
  axios: axiosClient,
  fetch: fetchClient,
}

export default httpClient
export {
  httpClient,
  configureClient,
  apiHealthcheck,
  setHttpToken,
  unsetHttpToken,
  setWorkSessionId,
  configureHttpInterceptors,
  DEFAULT_TIMEOUT,
}
