import { showAlert } from '@/alerts'
import { useAuthStore } from '@/stores/auth'

export abstract class APIError {
  message: string
  constructor(message: string) {
    this.message = message
  }
}

export class BadRequestError extends APIError {}

export class InternalServerError extends APIError {}

export class NetworkError extends APIError {}

export class AuthenticationError extends APIError {}

/**
 * Represents a generic API response that that will be completed in the future
 * using a given promise, which resolves either to a specified response type,
 * or an API Error type.
 */
export class APIResponse<T> {
  result: Promise<T | APIError>
  constructor(result: Promise<T | APIError>) {
    this.result = result
  }

  async handleErrorsWithAlert(): Promise<T | null> {
    const value = await this.result
    if (value instanceof APIError) {
      if (value instanceof BadRequestError || value instanceof AuthenticationError) {
        await showAlert(value.message, 'warning')
      } else {
        await showAlert(value.message, 'error')
      }
      return null
    }
    return value
  }

  async handleErrorsWithAlertNoBody(): Promise<boolean> {
    const value = await this.result
    if (value instanceof APIError) {
      if (value instanceof BadRequestError || value instanceof AuthenticationError) {
        await showAlert(value.message, 'warning')
      } else {
        await showAlert(value.message, 'error')
      }
      return false
    }
    return true
  }

  async getOrThrow(): Promise<T> {
    const value = await this.result
    if (value instanceof APIError) throw value
    return value
  }
}

export type AuthStoreType = ReturnType<typeof useAuthStore>

export abstract class APIClient {
  readonly baseUrl: string
  authStore: AuthStoreType | null
  constructor(baseUrl: string, authStore: AuthStoreType | null) {
    this.baseUrl = baseUrl
    this.authStore = authStore
  }

  protected get<T>(url: string): APIResponse<T> {
    const promise = fetch(this.baseUrl + url, { headers: this.getAuthHeaders() })
    return new APIResponse(this.handleAPIResponse(promise))
  }

  protected post<T, B>(url: string, body: B): APIResponse<T> {
    const promise = fetch(this.baseUrl + url, {
      headers: this.getAuthHeaders(),
      method: 'POST',
      body: JSON.stringify(body),
    })
    return new APIResponse(this.handleAPIResponse(promise))
  }

  protected postWithNoExpectedResponse<B>(url: string, body: B): APIResponse<void> {
    const promise = fetch(this.baseUrl + url, {
      headers: this.getAuthHeaders(),
      method: 'POST',
      body: JSON.stringify(body),
    })
    return new APIResponse(this.handleAPIResponseWithNoBody(promise))
  }

  protected put<T, B>(url: string, body: B): APIResponse<T> {
    const promise = fetch(this.baseUrl + url, {
      headers: this.getAuthHeaders(),
      method: 'PUT',
      body: JSON.stringify(body),
    })
    return new APIResponse(this.handleAPIResponse(promise))
  }

  protected delete(url: string): APIResponse<void> {
    const promise = fetch(this.baseUrl + url, {
      headers: this.getAuthHeaders(),
      method: 'DELETE',
    })
    return new APIResponse(this.handleAPIResponseWithNoBody(promise))
  }

  protected getAuthHeaders(): HeadersInit {
    if (this.authStore !== null && this.authStore.state) {
      const headers: HeadersInit = {
        Authorization: 'Basic ' + this.authStore.getBasicAuth(),
      }
      if (this.authStore.state.adminAsUser !== null) {
        headers['X-Admin-As-User'] = '' + this.authStore.state.adminAsUser.id
      }
      return headers
    }
    return {}
  }

  protected async handleAPIResponse<T>(promise: Promise<Response>): Promise<T | APIError> {
    try {
      const response = await promise
      if (response.ok) return (await response.json()) as T
      return this.transformErrorResponse(response)
    } catch (error) {
      return new NetworkError(
        '' + error + " (We couldn't connect to the remote server; it might be down!)",
      )
    }
  }

  protected async handleAPIResponseWithNoBody(
    promise: Promise<Response>,
  ): Promise<void | APIError> {
    try {
      const response = await promise
      if (response.ok) return undefined
      return this.transformErrorResponse(response)
    } catch (error) {
      return new NetworkError(
        '' + error + " (We couldn't connect to the remote server; it might be down!)",
      )
    }
  }

  private async transformErrorResponse(r: Response): Promise<APIError> {
    if (r.status === 401) return new AuthenticationError(await r.text())
    if (r.status === 400) return new BadRequestError(await r.text())
    return new InternalServerError(await r.text())
  }
}
