import { useAuthStore } from '@/stores/auth-store'
import { toQueryParams, type Page, type PageRequest } from './pagination'

export abstract class ApiError {
  readonly message: string

  constructor(message: string) {
    this.message = message
  }
}

export class NetworkError extends ApiError {}

export class StatusError extends ApiError {
  readonly status: number

  constructor(status: number, message: string) {
    super(message)
    this.status = status
  }
}

export abstract class ApiClient {
  private baseUrl: string = import.meta.env.VITE_API_BASE_URL

  protected async getJson<T>(path: string): Promise<T> {
    const r = await this.doRequest('GET', path)
    return await r.json()
  }

  protected async getJsonPage<T>(
    path: string,
    paginationOptions: PageRequest | undefined = undefined,
  ): Promise<Page<T>> {
    let p = path
    if (paginationOptions !== undefined) {
      p += '?' + toQueryParams(paginationOptions)
    }
    return this.getJson(p)
  }

  protected async getText(path: string): Promise<string> {
    const r = await this.doRequest('GET', path)
    return await r.text()
  }

  protected async getFile(path: string): Promise<void> {
    const r = await this.doRequest('GET', path)
    const blob = await r.blob()
    const objURL = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = objURL
    a.download = this.extractFileName(r) ?? 'file.dat'
    document.body.appendChild(a)
    a.click() // Trigger the download.
    document.body.removeChild(a)
    URL.revokeObjectURL(objURL)
  }

  private extractFileName(response: Response): string | null {
    const contentDisposition = response.headers.get('Content-Disposition')
    if (!contentDisposition) return null
    const chunks = contentDisposition.split(';').map((c) => c.trim())
    const filenameChunk = chunks.find((c) => c.startsWith('filename='))
    if (!filenameChunk) return null
    return filenameChunk.split('=')[1].trim()
  }

  protected async postJson<T>(path: string, body: object | undefined = undefined): Promise<T> {
    const r = await this.doRequest('POST', path, body)
    return await r.json()
  }

  protected async postFormData<T>(path: string, body: FormData): Promise<T> {
    try {
      const authStore = useAuthStore()
      const response = await fetch(this.baseUrl + path, {
        headers: {
          Authorization: 'Bearer ' + authStore.state?.token,
        },
        body: body,
        method: 'POST',
      })
      if (!response.ok) {
        const message = await response.text()
        throw new StatusError(response.status, message)
      }
      return await response.json()
    } catch (error) {
      if (error instanceof ApiError) throw error
      let message = 'Request to ' + path + ' failed.'
      if (
        typeof error === 'object' &&
        error !== null &&
        'message' in error &&
        typeof error.message === 'string'
      ) {
        message = error.message
      }
      console.error(error)
      throw new NetworkError(message)
    }
  }

  protected async postText(path: string, body: object | undefined = undefined): Promise<string> {
    const r = await this.doRequest('POST', path, body)
    return await r.text()
  }

  protected async postNoResponse(
    path: string,
    body: object | undefined = undefined,
  ): Promise<void> {
    await this.doRequest('POST', path, body)
  }

  protected async delete(path: string): Promise<void> {
    await this.doRequest('DELETE', path)
  }

  protected async putJson<T>(path: string, body: object | undefined = undefined): Promise<T> {
    const r = await this.doRequest('PUT', path, body)
    return await r.json()
  }

  protected async putFormData<T>(path: string, body: FormData): Promise<T> {
    try {
      const authStore = useAuthStore()
      const response = await fetch(this.baseUrl + path, {
        headers: {
          Authorization: 'Bearer ' + authStore.state?.token,
        },
        body: body,
        method: 'PUT',
      })
      if (!response.ok) {
        const message = await response.text()
        throw new StatusError(response.status, message)
      }
      return await response.json()
    } catch (error) {
      if (error instanceof ApiError) throw error
      let message = 'Request to ' + path + ' failed.'
      if (
        typeof error === 'object' &&
        error !== null &&
        'message' in error &&
        typeof error.message === 'string'
      ) {
        message = error.message
      }
      console.error(error)
      throw new NetworkError(message)
    }
  }

  async getApiStatus(): Promise<boolean> {
    try {
      await this.doRequest('GET', '/status')
      return true
    } catch {
      return false
    }
  }

  /**
   * Does a generic request, returning the response if successful, or throwing an ApiError if
   * any sort of error or non-OK status is returned.
   * @param method The HTTP method to use.
   * @param path The API path to request.
   * @param body The request body.
   * @returns A promise that resolves to an OK response.
   */
  private async doRequest(
    method: string,
    path: string,
    body: object | undefined = undefined,
  ): Promise<Response> {
    const settings: RequestInit = { method }
    const headers: HeadersInit = {}
    if (body !== undefined && typeof body === 'object') {
      headers['Content-Type'] = 'application/json'
      settings.body = JSON.stringify(body)
    }
    const authStore = useAuthStore()
    if (authStore.state) {
      headers['Authorization'] = 'Bearer ' + authStore.state.token
    }
    settings.headers = headers
    try {
      const response = await fetch(this.baseUrl + path, settings)
      if (!response.ok) {
        const message = await response.text()
        throw new StatusError(response.status, message)
      }
      return response
    } catch (error) {
      if (error instanceof ApiError) throw error
      let message = 'Request to ' + path + ' failed.'
      if (
        typeof error === 'object' &&
        error !== null &&
        'message' in error &&
        typeof error.message === 'string'
      ) {
        message = error.message
      }
      console.error(error)
      throw new NetworkError(message)
    }
  }
}
