import {
  useCallback,
  useState,
  useMemo,
  useEffect,
} from 'react'
import { useHistory } from 'react-router'

import type { User } from 'oidc-client'

import isString from 'lodash/isString'
import isBoolean from 'lodash/isBoolean'
import includes from 'lodash/includes'

import { PAGES } from 'config'

import {
  addLanguageUrlParam,
  writeToken,
  removeToken,
  readToken,
  setCookie,
  removeCookie,
  TOKEN_KEY,
} from 'helpers'

import {
  useLocalStore,
  useSessionStore,
  useToggle,
  useEventListener,
} from 'hooks'

import { useLexicsStore } from 'features/LexicsStore'
import { queryParamStorage } from 'features/QueryParamsStorage'

import type { UserInfo, FailedResponse } from 'requests'
import {
  getUserInfo,
  checkDevice,
  getTokenVirtualUser,
} from 'requests'

import { userManager } from '../config'
import { needCheckNewDeviсe } from '../helpers'

export const useAuth = () => {
  const { changeLang, lang } = useLexicsStore()
  const history = useHistory()
  const {
    close: markUserLoaded,
    isOpen: loadingUser,
  } = useToggle(true)
  const [user, setUser] = useState<User>()
  const [isNewDeviceLogin, setIsNewDeviceLogin] = useState(false)
  const [userInfo, setUserInfo] = useState<UserInfo>()

  const login = useCallback(async () => {
    userManager.signinRedirect({ extraQueryParams: { lang } })
  }, [lang])

  const logout = useCallback((key?: string) => {
    setPage(history.location.pathname)
    userManager.clearStaleState()
    userManager.createSigninRequest().then(({ url }) => {
      const urlWithLang = addLanguageUrlParam(lang, url)
      userManager.signoutRedirect({ post_logout_redirect_uri: urlWithLang })
    })
    removeToken()
    if (key !== 'saveToken') {
      removeCookie('access_token')
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lang])

  const storeUser = useCallback((loadedUser: User) => {
    setUser(loadedUser)
    writeToken(loadedUser.access_token)
    setCookie({
      exdays: 1,
      name: 'access_token',
      value: loadedUser.access_token,
    })
  }, [])

  const checkUser = useCallback(async () => {
    const loadedUser = await userManager.getUser()

    if (!loadedUser) {
      if (!readToken()) {
        const token = await getTemporaryToken()
        token && await fetchUserInfo()
        return Promise.resolve()
      }
      return Promise.reject()
    }

    storeUser(loadedUser)
    markUserLoaded()
    return loadedUser
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    storeUser,
    markUserLoaded,
  ])

  const [page, setPage] = useLocalStore({
    clearOnUnmount: true,
    defaultValue: '',
    key: 'matchBackLocation',
    validator: isString,
  })

  const [search, setSearch] = useLocalStore({
    clearOnUnmount: true,
    defaultValue: '',
    key: 'searchBack',
    validator: isString,
  })

  const [isFromLanding, setIsFromLanding] = useSessionStore({
    clearOnUnmount: true,
    defaultValue: false,
    key: 'isFromLanding',
    validator: isBoolean,
  })

  const [landingUrlFrom, setLandingUrlFrom] = useSessionStore({
    clearOnUnmount: true,
    defaultValue: '',
    key: 'landingUrlFrom',
    validator: isString,
  })

  const getTemporaryToken = async () => {
    try {
      const { access_token } = await getTokenVirtualUser()

      writeToken(access_token)
      setCookie({
        exdays: 1,
        name: 'access_token',
        value: access_token,
      })
      return access_token
      // eslint-disable-next-line no-empty
    } catch {
      return ''
    }
  }

  const signinRedirectCallback = useCallback(() => {
    userManager.signinRedirectCallback()
      .then((loadedUser) => {
        storeUser(loadedUser)
        queryParamStorage.clear()
        if (page.includes(PAGES.useraccount)) {
          history.push(PAGES.home)
        } else {
          const route = `${page}${search}`
          history.push(route)
        }
        markUserLoaded()
        setPage('')
        setSearch('')
        // }
      }).catch(login)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    login,
    storeUser,
    markUserLoaded,
  ])

  useEventListener({
    callback: useCallback(async (e: StorageEvent) => {
      const loadedUser = await userManager.getUser()

      if (
        e.storageArea !== localStorage
          || e.key !== TOKEN_KEY
          || !e.newValue
          || !loadedUser
          || loadedUser.access_token === e.newValue
      ) return

      userManager.storeUser({
        ...loadedUser,
        access_token: e.newValue,
        toStorageString: loadedUser.toStorageString,
      })
    }, []),
    event: 'storage',
  })

  useEffect(() => {
    const isRedirectedBackFromAuthProvider = history.location.pathname === '/redirect'
    isRedirectedBackFromAuthProvider ? signinRedirectCallback() : checkUser()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    checkUser,
    signinRedirectCallback,
    login,
  ])

  const reChekNewDevice = useCallback(async () => {
    const loadedUser = await userManager.getUser()
    if (!loadedUser) return

    checkDevice(loadedUser.access_token).catch((er:FailedResponse) => {
      if (er.error) return
      if (isBoolean(er.ok) && !er.ok) {
        setIsNewDeviceLogin(true)
        setTimeout(logout, 10000)
      }
    })
  }, [logout])

  const checkNewDevice = useCallback(async () => {
    const loadedUser = await userManager.getUser()
    if (!loadedUser) return

    checkDevice(loadedUser.access_token).catch(() => {
      setTimeout(reChekNewDevice, 5000)
    })
  }, [reChekNewDevice])

  useEffect(() => {
    if (!needCheckNewDeviсe && !user) return undefined
    const startCheckDevice = setInterval(checkNewDevice, 20000)
    isNewDeviceLogin && clearInterval(startCheckDevice)
    return () => clearInterval(startCheckDevice)

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    checkNewDevice,
    isNewDeviceLogin,
    setIsNewDeviceLogin,
  ])

  useEffect(() => {
    // попытаемся обновить токен используя refresh_token
    const tryRenewToken = () => {
      const tokenLastUpdated = Number(localStorage.getItem('token_updated'))
      // предотвращаем одновременное обновление токена в разных окнах/вкладках
      const needRenewToken = Date.now() - tokenLastUpdated >= 2 * 1e3

      if (!needRenewToken) return

      localStorage.setItem('token_updated', String(Date.now()))

      userManager.signinSilent()
        .catch(logout)
    }
    // если запросы вернули 401 | 403
    window.addEventListener('FORBIDDEN_REQUEST', tryRenewToken)

    // и если токен истек
    userManager.events.addAccessTokenExpired(tryRenewToken)
    return () => {
      window.removeEventListener('FORBIDDEN_REQUEST', tryRenewToken)
      userManager.events.removeAccessTokenExpired(tryRenewToken)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [logout])

  useEffect(() => {
    // событие срабатывает после получения токена(первый
    // логин и обновление токена)
    userManager.events.addUserLoaded(storeUser)
    return () => userManager.events.removeUserLoaded(storeUser)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userManager, storeUser])

  const fetchUserInfo = useCallback(async () => {
    try {
      const userInfoFetched = await getUserInfo()

      setUserInfo(userInfoFetched)

      if (includes(window.location.pathname, PAGES.landing) && readToken()) {
        changeLang(navigator.language.substring(0, 2))
      } else {
        userInfoFetched.language.iso && changeLang(userInfoFetched.language.iso)
      }

      // eslint-disable-next-line no-empty
    } catch (error) {}
  }, [changeLang])

  useEffect(() => {
    readToken() && fetchUserInfo()
  }, [fetchUserInfo, user])

  const auth = useMemo(() => ({
    fetchUserInfo,
    isFromLanding,
    isNewDeviceLogin,
    landingUrlFrom,
    loadingUser,
    login,
    logout,
    page,
    setIsFromLanding,
    setLandingUrlFrom,
    setPage,
    setSearch,
    setUserInfo,
    user,
    userInfo,
  }), [
    fetchUserInfo,
    isNewDeviceLogin,
    logout,
    user,
    userInfo,
    landingUrlFrom,
    login,
    loadingUser,
    setSearch,
    setPage,
    setUserInfo,
    page,
    setIsFromLanding,
    setLandingUrlFrom,
    isFromLanding,
  ])

  return auth
}
