import axios from 'axios'

import { lowerCaseFirst } from '@baserow/modules/core/utils/string'

export class ResponseErrorMessage {
  constructor(title, message) {
    this.title = title
    this.message = message
  }
}

class ErrorHandler {
  constructor(store, response, code = null, detail = null) {
    this.isHandled = false
    this.store = store
    this.response = response
    this.setError(code, detail)

    // A temporary global errorMap containing error messages for certain errors codes.
    // This must later be replaced by a more dynamic way.
    this.errorMap = {
      ERROR_USER_NOT_IN_GROUP: new ResponseErrorMessage(
        'Action not allowed.',
        "The action couldn't be completed because you aren't a " +
          'member of the related group.'
      ),
      // @TODO move these errors to the module.
      ERROR_TABLE_DOES_NOT_EXIST: new ResponseErrorMessage(
        "Table doesn't exist.",
        "The action couldn't be completed because the related table doesn't exist" +
          ' anymore.'
      ),
      ERROR_ROW_DOES_NOT_EXIST: new ResponseErrorMessage(
        "Row doesn't exist.",
        "The action couldn't be completed because the related row doesn't exist" +
          ' anymore.'
      ),
    }

    // A temporary notFoundMap containing the error messages for when the
    // response contains a 404 error based on the provided context name. Note
    // that an entry is not found a default message will be generated.
    this.notFoundMap = {}
  }

  /**
   * Changes the error code and details.
   */
  setError(code, detail) {
    this.code = code
    this.detail = detail
  }

  /**
   * Returns true if there is a readable error.
   * @return {boolean}
   */
  hasError() {
    return this.response !== undefined && this.response.code !== null
  }

  /**
   * Returns true is the response status code is equal to not found (404).
   * @return {boolean}
   */
  isNotFound() {
    return this.response !== undefined && this.response.status === 404
  }

  /**
   * Return true if there is a network error.
   * @return {boolean}
   */
  hasNetworkError() {
    return this.response === undefined
  }

  /**
   * Finds a message in the global errors or in the provided specific error map.
   */
  getErrorMessage(specificErrorMap = null) {
    if (
      specificErrorMap !== null &&
      Object.prototype.hasOwnProperty.call(specificErrorMap, this.code)
    ) {
      return specificErrorMap[this.code]
    }

    if (Object.prototype.hasOwnProperty.call(this.errorMap, this.code)) {
      return this.errorMap[this.code]
    }

    return new ResponseErrorMessage(
      'Action not completed.',
      "The action couldn't be completed because an unknown error has" +
        ' occured.'
    )
  }

  /**
   * Finds a not found message for a given context.
   */
  getNotFoundMessage(name) {
    if (!Object.prototype.hasOwnProperty.call(this.notFoundMap, name)) {
      return new ResponseErrorMessage(
        `${lowerCaseFirst(name)} not found.`,
        `The selected ${name.toLowerCase()} wasn't found, maybe it has already been deleted.`
      )
    }
    return this.notFoundMap[name]
  }

  /**
   * Returns a standard network error message. For example if the API server
   * could not be reached.
   */
  getNetworkErrorMessage() {
    return new ResponseErrorMessage(
      'Network error',
      'Could not connect to the API server.'
    )
  }

  /**
   * If there is an error or the requested detail is not found an error
   * message related to the problem is returned.
   */
  getMessage(name = null, specificErrorMap = null) {
    if (this.hasNetworkError()) {
      return this.getNetworkErrorMessage()
    }
    if (this.hasError()) {
      return this.getErrorMessage(specificErrorMap)
    }
    if (this.isNotFound()) {
      return this.getNotFoundMessage(name)
    }
    return null
  }

  /**
   * If there is an error or the requested detail is not found we will try to
   * get find an existing message of one is not provided and notify the user
   * about what went wrong. After that the error is marked as handled.
   */
  notifyIf(name = null, message = null) {
    if (
      !(this.hasError() || this.hasNetworkError() || this.isNotFound()) ||
      this.isHandled
    ) {
      return
    }

    if (message === null) {
      message = this.getMessage(name)
    }

    this.store.dispatch(
      'notification/error',
      {
        title: message.title,
        message: message.message,
      },
      { root: true }
    )

    this.handled()
  }

  /**
   * Will mark the error as handled so that the same error message isn't shown
   * twice.
   */
  handled() {
    this.isHandled = true
  }
}

export default function ({ store, app }, inject) {
  const url =
    (process.client
      ? app.$env.PUBLIC_BACKEND_URL
      : app.$env.PRIVATE_BACKEND_URL) + '/api/v0'
  const client = axios.create({
    baseURL: url,
    withCredentials: false,
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
  })

  // Create a request interceptor to add the authorization token to every
  // request if the user is authenticated.
  client.interceptors.request.use((config) => {
    if (store.getters['auth/isAuthenticated']) {
      const token = store.getters['auth/token']
      config.headers.Authorization = `JWT ${token}`
    }
    return config
  })

  // Create a response interceptor to add more detail tot the error message
  // and to create a notification when there is a network error.
  client.interceptors.response.use(
    (response) => {
      return response
    },
    (error) => {
      error.handler = new ErrorHandler(store, error.response)

      // Add the error message in the response to the error object.
      if (
        error.response &&
        'error' in error.response.data &&
        'detail' in error.response.data
      ) {
        error.handler.setError(
          error.response.data.error,
          error.response.data.detail
        )
      }

      return Promise.reject(error)
    }
  )

  inject('client', client)
}