import { API_PREFIX, PUBLIC_API_PREFIX } from '@/config'
import { refreshToken } from './auth'
import { triggerLogout } from '@/utils/auth'

const CSRF_COOKIE_NAME = 'csrf_token'
const CSRF_HEADER_NAME = 'X-CSRF-Token'
const DEFAULT_REQUEST_TIMEOUT = 30000

let refreshInFlight = null

function getCsrfToken() {
  const cookies = document.cookie.split(';')
  const csrfCookie = cookies.find(c => c.trim().startsWith(CSRF_COOKIE_NAME + '='))
  if (csrfCookie) {
    return csrfCookie.split('=')[1]
  }
  return ''
}

function getAccessToken() {
  try {
    return localStorage.getItem('access_token') || ''
  } catch (_) {
    return ''
  }
}

function buildAuthHeaders(headers = {}) {
  const nextHeaders = {
    'Content-Type': 'application/json',
    [CSRF_HEADER_NAME]: getCsrfToken(),
    ...headers,
  }
  const accessToken = getAccessToken()
  if (accessToken && !nextHeaders.Authorization) {
    nextHeaders.Authorization = `Bearer ${accessToken}`
  }
  return nextHeaders
}

async function ensureFreshToken() {
  if (refreshInFlight) return refreshInFlight
  refreshInFlight = refreshToken()
  try {
    return await refreshInFlight
  } finally {
    refreshInFlight = null
  }
}

async function request(url, options = {}, isPublicAPI = false, _isRetry = false) {
  const { timeout = DEFAULT_REQUEST_TIMEOUT, signal: externalSignal, ...fetchOptions } = options
  const controller = new AbortController()
  let timeoutId
  let didTimeout = false
  const abortFromExternalSignal = () => controller.abort(externalSignal.reason)

  if (externalSignal?.aborted) {
    abortFromExternalSignal()
  } else if (externalSignal) {
    externalSignal.addEventListener('abort', abortFromExternalSignal, { once: true })
  }

  if (timeout > 0) {
    timeoutId = window.setTimeout(() => {
      didTimeout = true
      controller.abort()
    }, timeout)
  }

  const headers = buildAuthHeaders(options.headers)
  const baseUrl = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
  const fullUrl = url.startsWith('http') ? url : `${baseUrl}${url}`

  try {
    const response = await fetch(fullUrl, {
      ...fetchOptions,
      headers,
      credentials: 'include',
      signal: controller.signal,
    })

    if (response.status === 401 && !_isRetry && !isPublicAPI) {
      try {
        await ensureFreshToken()
        return await request(url, options, isPublicAPI, true)
      } catch (refreshErr) {
        triggerLogout()
        throw { code: 401, message: 'Login expired, please sign in again' }
      }
    }

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}))
      throw { code: errorData.code || response.status, message: errorData.message || 'Request failed' }
    }

    return response.json()
  } catch (error) {
    if (didTimeout) {
      throw { code: 'REQUEST_TIMEOUT', message: 'Request timed out, please try again later' }
    }
    if (error?.name === 'AbortError') {
      throw { code: 'REQUEST_ABORTED', message: 'Request was aborted' }
    }
    throw error
  } finally {
    if (timeoutId) window.clearTimeout(timeoutId)
    if (externalSignal) {
      externalSignal.removeEventListener('abort', abortFromExternalSignal)
    }
  }
}

export function get(url, params = {}, isPublicAPI = false) {
  let queryString = ''
  if (typeof params === 'string') {
    queryString = params
  } else if (params && typeof params === 'object') {
    const entries = []
    for (const [k, v] of Object.entries(params)) {
      if (v === undefined || v === null) continue
      entries.push([k, String(v)])
    }
    queryString = new URLSearchParams(entries).toString()
  }
  const fullUrl = queryString ? `${url}?${queryString}` : url
  return request(fullUrl, { method: 'GET' }, isPublicAPI)
}

export async function downloadFile(url, isPublicAPI = false) {
  const baseUrl = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
  const fullUrl = url.startsWith('http') ? url : `${baseUrl}${url}`
  const response = await fetch(fullUrl, {
    method: 'GET',
    credentials: 'include',
    headers: buildAuthHeaders(),
  })

  if (!response.ok) {
    let msg = `Download failed (${response.status})`
    try {
      const j = await response.json()
      if (j?.message) msg = j.message
    } catch (_) {
      /* ignore */
    }
    throw { code: response.status, message: msg }
  }

  const disp = response.headers.get('content-disposition') || ''
  let filename = ''
  const rfc5987 = disp.match(/filename\*\s*=\s*(?:UTF-8'')?([^;]+)/i)
  if (rfc5987) {
    filename = decodeURIComponent(rfc5987[1].replace(/^["']|["']$/g, '').trim())
  } else {
    const fnMatch = disp.match(/filename\s*=\s*["']?([^"';]+)["']?/i)
    if (fnMatch) filename = fnMatch[1].trim()
  }

  const blob = await response.blob()
  return { blob, filename }
}

export function post(url, body = {}, isPublicAPI = false) {
  return request(url, {
    method: 'POST',
    body: JSON.stringify(body),
  }, isPublicAPI)
}

export function put(url, body = {}, isPublicAPI = false) {
  return request(url, {
    method: 'PUT',
    body: JSON.stringify(body),
  }, isPublicAPI)
}

export function del(url, isPublicAPI = false) {
  return request(url, { method: 'DELETE' }, isPublicAPI)
}

export function patch(url, body = {}, isPublicAPI = false) {
  return request(url, {
    method: 'PATCH',
    body: JSON.stringify(body),
  }, isPublicAPI)
}
