export class HttpParams {
  private readonly searchParams: URLSearchParams

  constructor(searchParams?: URLSearchParams) {
    this.searchParams = searchParams ? new URLSearchParams(searchParams) : new URLSearchParams()
  }

  append(key: string, value: string | number | boolean): HttpParams {
    const next = new URLSearchParams(this.searchParams)
    next.append(key, String(value))
    return new HttpParams(next)
  }

  set(key: string, value: string | number | boolean): HttpParams {
    const next = new URLSearchParams(this.searchParams)
    next.set(key, String(value))
    return new HttpParams(next)
  }

  toString(): string {
    return this.searchParams.toString()
  }
}

type QueryParams = HttpParams | Record<string, string | number | boolean | Array<string | number | boolean>>

function withQuery(url: string, params?: QueryParams): string {
  if (!params) return url

  const target = new URL(url)
  if (params instanceof HttpParams) {
    const query = params.toString()
    if (query) {
      const source = new URLSearchParams(query)
      source.forEach((value, key) => target.searchParams.append(key, value))
    }
    return target.toString()
  }

  Object.entries(params).forEach(([key, value]) => {
    if (Array.isArray(value)) {
      value.forEach((item) => target.searchParams.append(key, String(item)))
      return
    }
    target.searchParams.set(key, String(value))
  })
  return target.toString()
}

async function parseResponse<T>(response: Response, responseType: 'json' | 'blob' = 'json'): Promise<T> {
  if (!response.ok) {
    const text = await response.text()
    throw new Error(text || `HTTP ${response.status}`)
  }

  if (response.status === 204) {
    return undefined as T
  }

  if (responseType === 'blob') {
    return (await response.blob()) as T
  }

  return (await response.json()) as T
}

function withAuth(headers?: Record<string, string>): Record<string, string> {
  const token = typeof window === 'undefined' ? null : window.localStorage.getItem('token')
  return {
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
    ...(headers ?? {}),
  }
}

export class HttpClient {
  async get<T>(
    url: string,
    options?: { params?: QueryParams; headers?: Record<string, string>; responseType?: 'json' | 'blob' },
  ): Promise<T> {
    const response = await fetch(withQuery(url, options?.params), {
      method: 'GET',
      headers: withAuth(options?.headers),
    })
    return parseResponse<T>(response, options?.responseType)
  }

  async post<T>(url: string, body?: unknown, options?: { params?: QueryParams; headers?: Record<string, string> }): Promise<T> {
    const response = await fetch(withQuery(url, options?.params), {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...withAuth(options?.headers),
      },
      body: body === undefined ? undefined : JSON.stringify(body),
    })
    return parseResponse<T>(response)
  }

  async put<T>(url: string, body?: unknown, options?: { params?: QueryParams; headers?: Record<string, string> }): Promise<T> {
    const response = await fetch(withQuery(url, options?.params), {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        ...withAuth(options?.headers),
      },
      body: body === undefined ? undefined : JSON.stringify(body),
    })
    return parseResponse<T>(response)
  }

  async delete<T>(url: string, options?: { params?: QueryParams; headers?: Record<string, string> }): Promise<T> {
    const response = await fetch(withQuery(url, options?.params), {
      method: 'DELETE',
      headers: withAuth(options?.headers),
    })
    return parseResponse<T>(response)
  }
}

export const httpClient = new HttpClient()
