import type { BackendError, User } from '~/types'
import type { Module } from 'vuex'
import type { RootState } from '~/store/index'
import type { RouteLocationRaw } from 'vue-router'
import type { WebviewWindow } from '@tauri-apps/api/webviewWindow'

import axios from 'axios'
import useLogger from '~/composables/useLogger'
import useFormData from '~/composables/useFormData'

import { clear as clearIDB } from 'idb-keyval'
import { useQueue } from '~/composables/audio/queue'
import { isTauri } from '~/composables/tauri'

export type Permission = 'settings' | 'library' | 'moderation'
export interface State {
  authenticated: boolean
  username: string
  fullUsername: string
  availablePermissions: Record<Permission, boolean>,
  profile: null | User
  oauth: OAuthTokens
  scopedTokens: ScopedTokens

  applicationSecret: string | undefined

  oauthWindow: WebviewWindow | undefined
}

interface ScopedTokens {
  listen: null | string
}

interface OAuthTokens {
  clientId: null | string
  clientSecret: null | string
  accessToken: null | string
  refreshToken: null | string
}

const NEEDED_SCOPES = 'read write'

const logger = useLogger()

function getDefaultScopedTokens (): ScopedTokens {
  return {
    listen: null
  }
}

function getDefaultOauth (): OAuthTokens {
  return {
    clientId: null,
    clientSecret: null,
    accessToken: null,
    refreshToken: null
  }
}

async function createOauthApp () {
  const payload = {
    name: `Funkwhale web client at ${location.hostname}`,
    website: location.origin,
    scopes: NEEDED_SCOPES,
    redirect_uris: `${location.origin}/auth/callback`
  }
  return (await axios.post('oauth/apps/', payload)).data
}

const store: Module<State, RootState> = {
  namespaced: true,
  state: {
    authenticated: false,
    username: '',
    fullUsername: '',
    availablePermissions: {
      settings: false,
      library: false,
      moderation: false
    },
    profile: null,
    oauth: getDefaultOauth(),
    scopedTokens: getDefaultScopedTokens(),

    applicationSecret: undefined,

    oauthWindow: undefined
  },
  getters: {
    header: state => {
      if (state.oauth.accessToken) {
        return 'Bearer ' + state.oauth.accessToken
      }
    }
  },
  mutations: {
    reset (state) {
      state.authenticated = false
      state.profile = null
      state.username = ''
      state.fullUsername = ''
      state.scopedTokens = getDefaultScopedTokens()
      state.oauth = getDefaultOauth()
      state.availablePermissions = {
        settings: false,
        library: false,
        moderation: false
      }

      state.applicationSecret = undefined
    },
    profile: (state, value) => {
      state.profile = value
    },
    authenticated: (state, value) => {
      state.authenticated = value
      if (value === false) {
        state.username = ''
        state.fullUsername = ''
        state.profile = null
        state.scopedTokens = getDefaultScopedTokens()
        state.availablePermissions = {
          settings: false,
          library: false,
          moderation: false
        }
      }
    },
    username: (state, value) => {
      state.username = value
    },
    fullUsername: (state, value) => {
      state.fullUsername = value
    },
    avatar: (state, value) => {
      if (state.profile) {
        state.profile.avatar = value
      }
    },
    scopedTokens: (state, value) => {
      state.scopedTokens = { ...value }
    },
    permission: (state, { key, status }: { key: Permission, status: boolean }) => {
      state.availablePermissions[key] = status
    },
    profilePartialUpdate: (state, payload: User) => {
      if (!state.profile) {
        state.profile = {} as User
      }

      for (const [key, value] of Object.entries(payload)) {
        state.profile[key as keyof User] = value as never
      }
    },
    oauthApp: (state, payload) => {
      state.oauth.clientId = payload.client_id
      state.oauth.clientSecret = payload.client_secret
    },
    oauthToken: (state, payload) => {
      state.oauth.accessToken = payload.access_token
      state.oauth.refreshToken = payload.refresh_token
    }
  },
  actions: {
    // Send a request to the login URL and save the returned JWT
    async login ({ dispatch }, { credentials }) {
      const form = useFormData(credentials)
      await axios.post('users/login', form)

      logger.info('Successfully logged in as', credentials.username)
      await dispatch('fetchUser')
    },
    async logout ({ commit }) {
      try {
        await axios.post('users/logout')
      } catch (error) {
        logger.error('Error while logging out, probably logged in via oauth', error)
      }

      const modules = [
        'auth',
        'favorites',
        'player',
        'playlists',
        'queue',
        'radios'
      ]

      for (const module of modules) {
        commit(`${module}/reset`, null, { root: true })
      }

      // Clear session storage
      sessionStorage.clear()

      // Clear track queue
      await useQueue().clear()

      // Clear all indexedDB data
      await clearIDB()

      logger.info('Log out, goodbye!')
    },

    async fetchNotifications ({ dispatch, state }) {
      return Promise.all([
        dispatch('ui/fetchUnreadNotifications', null, { root: true }),
        state.availablePermissions.library && dispatch('ui/fetchPendingReviewEdits', null, { root: true }),
        state.availablePermissions.moderation && dispatch('ui/fetchPendingReviewReports', null, { root: true }),
        state.availablePermissions.moderation && dispatch('ui/fetchPendingReviewRequests', null, { root: true })
      ])
    },
    async fetchUser ({ dispatch }) {
      try {
        const response = await axios.get('users/me/')
        logger.info('Successfully fetched user profile')

        dispatch('updateUser', response.data)

        await Promise.all([
          dispatch('fetchNotifications'),
          dispatch('favorites/fetch', null, { root: true }),
          dispatch('playlists/fetchOwn', null, { root: true }),
          dispatch('libraries/fetchFollows', null, { root: true }),
          dispatch('users/fetchFollows', null, { root: true }),
          dispatch('channels/fetchSubscriptions', null, { root: true }),
          dispatch('moderation/fetchContentFilters', null, { root: true })
        ])
      } catch (error) {
        if ((error as BackendError).response?.status === 401) {
          logger.info('User is not authenticated')
          return
        }

        logger.error('Error while fetching user profile', error)
      }
    },
    updateUser ({ commit }, data) {
      commit('authenticated', true)
      commit('profile', data)
      commit('username', data.username)
      commit('fullUsername', data.full_username)

      if (data.tokens) {
        commit('scopedTokens', data.tokens)
      }

      for (const [permission, hasPermission] of Object.entries(data.permissions)) {
        // this makes it easier to check for permissions in templates
        commit('permission', { key: permission, status: hasPermission })
      }
    },
    async tryFinishOAuthFlow ({ state }) {
      if (isTauri()) {
        return state.oauthWindow?.close?.().catch(() => {
          // Ignore the error in case of window being already closed
        })
      }
    },
    async oauthLogin ({ state, rootState, commit, dispatch }, next: RouteLocationRaw) {
      const app = await createOauthApp()
      commit('oauthApp', app)
      const redirectUrl = encodeURIComponent(`${location.origin}/auth/callback`)
      const params = `response_type=code&scope=${encodeURIComponent(NEEDED_SCOPES)}&redirect_uri=${redirectUrl}&state=${next}&client_id=${state.oauth.clientId}`
      const authorizeUrl = `${rootState.instance.instanceUrl}authorize?${params}`

      if (isTauri()) {
        const { WebviewWindow, getCurrent } = await import('@tauri-apps/api/webviewWindow')

        state.oauthWindow = new WebviewWindow('oauth', {
          title: `Login to ${rootState.instance.settings.instance.name}`,
          parent: 'main',
          url: authorizeUrl
        })

        const token = await new Promise((resolve, reject) => {
          if (!state.oauthWindow) return

          const stop = getCurrent().once<string>('oauthToken', async (event) => {
            resolve(event.payload)
          })

          state.oauthWindow.once('tauri://error', async (error) => {
            (await stop)()
            reject(error)
          })

          state.oauthWindow.once('tauri://destroyed', async () => {
            (await stop)()
            reject(new Error('Aborted by user'))
          })
        }).finally(() => dispatch('tryFinishOAuthFlow'))

        commit('oauthToken', token)
        await dispatch('fetchUser')
      } else {
        logger.log('Redirecting user...', authorizeUrl)
        location.href = authorizeUrl
      }
    },
    async handleOauthCallback ({ state, commit, dispatch }, authorizationCode) {
      logger.log('Fetching token...')
      const payload = {
        client_id: state.oauth.clientId,
        client_secret: state.oauth.clientSecret,
        grant_type: 'authorization_code',
        code: authorizationCode,
        redirect_uri: `${location.origin}/auth/callback`
      }
      const response = await axios.post(
        'oauth/token/',
        useFormData(payload),
        { headers: { 'Content-Type': 'multipart/form-data' } }
      )

      if (isTauri()) {
        const { getCurrent } = await import('@tauri-apps/api/webviewWindow')
        const currentWindow = getCurrent()

        // If the current window is the oauth window, pass the event to the main window
        if (currentWindow.label === 'oauth') {
          await currentWindow.emitTo('main', 'oauthToken', response.data)
          return
        }
      }

      commit('oauthToken', response.data)
      await dispatch('fetchUser')
    },
    async refreshOauthToken ({ state, commit }) {
      const payload = {
        client_id: state.oauth.clientId,
        client_secret: state.oauth.clientSecret,
        grant_type: 'refresh_token',
        refresh_token: state.oauth.refreshToken
      }
      const response = await axios.post(
        'oauth/token/',
        useFormData(payload),
        { headers: { 'Content-Type': 'multipart/form-data' } }
      )
      commit('oauthToken', response.data)
    }
  }
}

export default store
