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

export class RealTimeHandler {
  constructor(context) {
    this.context = context
    this.socket = null
    this.connected = false
    this.reconnect = false
    this.anonymous = false
    this.reconnectTimeout = null
    this.attempts = 0
    this.events = {}
    this.page = null
    this.pageParameters = {}
    this.subscribedToPage = true
    this.lastToken = null
    this.authenticationSuccess = true
    this.registerCoreEvents()
  }

  /**
   * Creates a new connection with to the web socket so that real time updates can be
   * received.
   */
  connect(reconnect = true, anonymous = false) {
    this.reconnect = reconnect
    this.anonymous = anonymous

    const jwtToken = this.context.store.getters['auth/token']
    const token = anonymous ? jwtToken || 'anonymous' : jwtToken

    // If the user is already connected to the web socket, we don't have to do
    // anything.
    if (this.connected) {
      return
    }

    // Stop connecting if we have already tried more than 10 times, if we do not have
    // an authentication token or if the server has already responded with a failed
    // authentication error and the token has not changed.
    if (
      this.attempts > 10 ||
      token === null ||
      (!this.authenticationSuccess && token === this.lastToken)
    ) {
      this.context.store.dispatch('notification/setFailedConnecting', true)
      return
    }

    this.lastToken = token

    // The web socket url is the same as the PUBLIC_BACKEND_URL apart from the
    // protocol.
    const rawUrl = this.context.app.$env.PUBLIC_BACKEND_URL
    const url = new URL(rawUrl)
    url.protocol = isSecureURL(rawUrl) ? 'wss:' : 'ws:'
    url.pathname = '/ws/core/'

    this.socket = new WebSocket(`${url}?jwt_token=${token}`)
    this.socket.onopen = () => {
      this.context.store.dispatch('notification/setConnecting', false)
      this.connected = true
      this.attempts = 0

      // If the client needs to be subscribed to a page we can do that directly
      // after connecting.
      if (!this.subscribedToPage) {
        this.subscribeToPage()
      }
    }

    /**
     * The received messages are always JSON so we need to the parse it, extract the
     * type and call the correct event.
     */
    this.socket.onmessage = (message) => {
      let data = {}

      try {
        data = JSON.parse(message.data)
      } catch {
        return
      }

      if (
        Object.prototype.hasOwnProperty.call(data, 'type') &&
        Object.prototype.hasOwnProperty.call(this.events, data.type)
      ) {
        this.events[data.type](this.context, data)
      }
    }

    /**
     * When the connection closes we want to reconnect immediately because we don't
     * want to miss any important real time updates. After the first attempt we want to
     * delay retry with 5 seconds.
     */
    this.socket.onclose = () => {
      this.connected = false
      // By default the user not subscribed to a page a.k.a `null`, so if the current
      // page is already null we can mark it as subscribed.
      this.subscribedToPage = this.page === null
      this.delayedReconnect()
    }
  }

  /**
   * If reconnecting is enabled then a timeout is created that will try to connect
   * to the backend one more time.
   */
  delayedReconnect() {
    if (!this.reconnect) {
      return
    }

    this.attempts++
    this.context.store.dispatch('notification/setConnecting', true)

    this.reconnectTimeout = setTimeout(
      () => {
        this.connect(true, this.anonymous)
      },
      // After the first try, we want to try again every 5 seconds.
      this.attempts > 1 ? 5000 : 0
    )
  }

  /**
   * Subscribes the client to a given page. After subscribing the client will
   * receive updated related to that page. This is for example used when a user
   * opens a table page.
   */
  subscribe(page, parameters) {
    this.page = page
    this.pageParameters = parameters
    this.subscribedToPage = false

    // If the client is already connected we can directly subscribe to the page.
    if (this.connected) {
      this.subscribeToPage()
    }
  }

  /**
   * Sends a request to the real time server that updates for a certain page +
   * parameters must be received.
   */
  subscribeToPage() {
    this.socket.send(
      JSON.stringify({
        page: this.page === null ? '' : this.page,
        ...this.pageParameters,
      })
    )
    this.subscribedToPage = true
  }

  /**
   * Disconnects the socket and resets all the variables. The can be used when
   * navigating to another page that doesn't require updates.
   */
  disconnect() {
    if (this.connected) {
      this.socket.close()
    }

    this.context.store.dispatch('notification/setConnecting', false)
    this.context.store.dispatch('notification/setFailedConnecting', false)
    clearTimeout(this.reconnectTimeout)
    this.reconnect = false
    this.attempts = 0
    this.connected = false
  }

  /**
   * Registers a new event with the event registry.
   */
  registerEvent(type, callback) {
    this.events[type] = callback
  }

  /**
   * Registers all the core event handlers, which is for the groups and applications.
   */
  registerCoreEvents() {
    // When the authentication is successful we want to store the web socket id in
    // auth store. Every AJAX request will include the web socket id as header, this
    // way the backend knows that this client does not has to receive the event
    // because we already know about the change.
    this.registerEvent('authentication', ({ store }, data) => {
      store.dispatch('auth/setWebSocketId', data.web_socket_id)

      // Store if the authentication was successful in order to prevent retries that
      // will fail.
      this.authenticationSuccess = data.success
    })

    this.registerEvent('user_data_updated', ({ store }, data) => {
      store.dispatch('auth/forceUpdateUserData', data.user_data)
    })

    this.registerEvent('user_updated', ({ store }, data) => {
      store.dispatch('group/forceUpdateGroupUserAttributes', {
        userId: data.user.id,
        values: {
          name: data.user.first_name,
        },
      })
    })

    this.registerEvent('user_deleted', ({ store }, data) => {
      store.dispatch('group/forceUpdateGroupUserAttributes', {
        userId: data.user.id,
        values: {
          to_be_deleted: true,
        },
      })
    })

    this.registerEvent('user_restored', ({ store }, data) => {
      store.dispatch('group/forceUpdateGroupUserAttributes', {
        userId: data.user.id,
        values: {
          to_be_deleted: false,
        },
      })
    })

    this.registerEvent('user_permanently_deleted', ({ store }, data) => {
      store.dispatch('group/forceDeleteUser', {
        userId: data.user_id,
      })
    })

    this.registerEvent('group_created', ({ store }, data) => {
      store.dispatch('group/forceCreate', data.group)
    })

    this.registerEvent('group_restored', ({ store }, data) => {
      store.dispatch('group/forceCreate', data.group)
      store.dispatch('application/forceCreateAll', data.applications)
    })

    this.registerEvent('group_updated', ({ store }, data) => {
      const group = store.getters['group/get'](data.group_id)
      if (group !== undefined) {
        store.dispatch('group/forceUpdate', { group, values: data.group })
      }
    })

    this.registerEvent('group_deleted', ({ store }, data) => {
      const group = store.getters['group/get'](data.group_id)
      if (group !== undefined) {
        store.dispatch('group/forceDelete', group)
      }
    })

    this.registerEvent('groups_reordered', ({ store }, data) => {
      store.dispatch('group/forceOrder', data.group_ids)
    })

    this.registerEvent('group_user_added', ({ store }, data) => {
      store.dispatch('group/forceAddGroupUser', {
        groupId: data.group_id,
        values: data.group_user,
      })
    })

    this.registerEvent('group_user_updated', ({ store }, data) => {
      store.dispatch('group/forceUpdateGroupUser', {
        id: data.id,
        groupId: data.group_id,
        values: data.group_user,
      })
    })

    this.registerEvent('group_user_deleted', ({ store }, data) => {
      store.dispatch('group/forceDeleteGroupUser', {
        id: data.id,
        groupId: data.group_id,
        values: data.group_user,
      })
    })

    this.registerEvent('application_created', ({ store }, data) => {
      store.dispatch('application/forceCreate', data.application)
    })

    this.registerEvent('application_updated', ({ store }, data) => {
      const application = store.getters['application/get'](data.application_id)
      if (application !== undefined) {
        store.dispatch('application/forceUpdate', {
          application,
          data: data.application,
        })
      }
    })

    this.registerEvent('application_deleted', ({ store }, data) => {
      const application = store.getters['application/get'](data.application_id)
      if (application !== undefined) {
        store.dispatch('application/forceDelete', application)
      }
    })

    this.registerEvent('applications_reordered', ({ store }, data) => {
      const group = store.getters['group/get'](data.group_id)
      if (group !== undefined) {
        store.commit('application/ORDER_ITEMS', { group, order: data.order })
      }
    })
  }
}

export default function (context, inject) {
  inject('realtime', new RealTimeHandler(context))
}