import { useState } from 'react';
import { useCreation, useInterval, useMemoizedFn } from 'ahooks';
import { joinURL, withQuery } from 'ufo';
import Cookie from 'js-cookie';
import { getCookieOptions, setVisitorId } from '@arcblock/ux/lib/Util';
import jwtDecode from 'jwt-decode';
import Toast from '@arcblock/ux/lib/Toast';
import pRetry from 'p-retry';
import noop from 'lodash/noop';

import { ReactGA } from '@arcblock/ux/lib/withTracker';
import { GA_LAST_SOURCE_PROVIDER } from '@arcblock/ux/lib/withTracker/constant';
import createService from '../../Service';
import {
  createAxios,
  getBrowserLang,
  decrypt as _decrypt,
  sleep,
  logger,
  debug,
  debugTmp,
  debugTmpInterval,
} from '../../utils';
import { CHECK_INTERVAL_TIME, LANG_COOKIE_NAME } from '../../constant';

class ErrorToken extends Error {}

const MAX_RETRY_COUNT = 10;

const decrypt = (v) => _decrypt(v, localStorage.getItem('__encKey'), localStorage.getItem('__decKey'));

export default function useSessionToken({
  state,
  pageState,
  serviceHost,
  sessionTokenStorage,
  csrfTokenStorage,
  refreshTokenStorage,
  lazyRefreshToken,
  apiOptions = {},
  onRefresh = noop,
}) {
  const [retryCount, setRetryCount] = useState(0);
  const [intervalTime, setIntervalTime] = useState(CHECK_INTERVAL_TIME);
  const { getToken: getSessionToken, setToken: setSessionToken, removeToken: removeSessionToken } = sessionTokenStorage;
  const { setToken: setCsrfToken } = csrfTokenStorage;
  const { getToken: getRefreshToken, setToken: setRefreshToken, removeToken: removeRefreshToken } = refreshTokenStorage;
  const request = useCreation(() => {
    return createAxios({
      baseURL: serviceHost,
      timeout: 10 * 1000,
      secure: true,
    });
  }, [serviceHost]);
  const removeToken = () => {
    debugTmp('[removeToken] useSessionToken -> removeToken');
    removeSessionToken();
    removeRefreshToken();
  };

  const service = useCreation(() => {
    return createService(
      {
        sessionTokenStorage,
        refreshTokenStorage,
        serviceHost,
        authServicePrefix: pageState.prefix,
        onRefreshTokenError() {
          debugTmp('[removeToken] useSessionToken -> createService: onRefreshTokenError');
          removeToken();
          // HACK: 在使用 useReactive 后，即使重复的给子字段赋值为 null，也会引起组件的重渲染，所以对于定时循环来说，一定要避免这一点，防止无效的赋值引起页面重渲染
          if (state.user) {
            state.user = null;
          }
        },
      },
      apiOptions
    );
  }, [serviceHost, pageState.prefix]);

  const clearSession = () => {
    const cookieOptions = getCookieOptions({ returnDomain: false });
    Cookie.remove('connected_did', cookieOptions);
    Cookie.remove('connected_pk', cookieOptions);
    Cookie.remove('connected_app', cookieOptions);
    Cookie.remove('connected_wallet_os', cookieOptions);
    debugTmpInterval('[removeToken] clearSession-interval');
    removeToken();
  };

  const checkCookieLocale = useMemoizedFn(() => {
    const latestLocale = Cookie.get(LANG_COOKIE_NAME) || getBrowserLang();
    if (latestLocale !== pageState.currentLocale) {
      pageState.currentLocale = latestLocale;
    }
  });

  const handleGetUnreadCount = useMemoizedFn(async () => {
    const requestFn = () => service.get(joinURL(pageState.notificationPrefix, '/unread-count'));
    try {
      const { data } = await requestFn();
      return data.unReadCount;
    } catch (err) {
      console.error('getUnreadCount error', err);
      return 0;
    }
  });

  const _refresh = useMemoizedFn(
    async ({ showProgress = false, requestFn = null, onlyRefreshToken = false, type = 'refreshToken' }) => {
      try {
        if (state.loading) {
          console.warn('SessionProvider.refresh is currently in progress, call it will be noop');
          return { loading: true };
        }

        if (showProgress) {
          state.loading = true;
        }
        const { data, status } = await requestFn();

        const userId = data?.user?.did;
        if (userId) {
          ReactGA.set({
            user_id: userId,
          });
        }
        if (data?.user?.sourceProvider) {
          localStorage.setItem(GA_LAST_SOURCE_PROVIDER, data.user.sourceProvider);
        }

        if (status === 400) {
          // NOTE: 如果通过多种方式传递了 token，服务端会报 400 的错误，需要移除现有的 token
          debugTmp('[removeToken] useSessionToken -> _refresh: status 400');
          removeToken();
          state.user = null;
          state.error = '';
        }

        if (data.error) {
          // Some thing went wrong
          state.error = data.error;
          state.open = false;
          state.unReadCount = 0;
          throw new Error(data.error);
        } else if (data.user) {
          // 当获取到 user 后再去请求 unreadCount
          try {
            const count = await handleGetUnreadCount();
            state.unReadCount = count || 0;
          } catch (err) {
            logger.error('getUnreadCount error', err);
            state.unReadCount = 0;
          }
          // We have valid user
          // 用于 refreshSession 的逻辑
          if (data.nextToken) {
            setSessionToken(data.nextToken);
            onRefresh({
              type,
              sessionToken: data.nextToken,
              refreshToken: data.nextRefreshToken,
              user: data.user,
            });
            if (data.nextRefreshToken) {
              setRefreshToken(data.nextRefreshToken);
            }
          }
          if (!onlyRefreshToken) {
            state.open = false;
            state.user = data.user;
            state.provider = data.provider;
            state.walletOS = data.walletOS;
          }
          if (!getSessionToken()) {
            // eslint-disable-next-line quotes
            Toast.error(`Can't write session token, please use https url to access this page`);
            // NOTICE: 出现这种情况暂不移除 token，可以通过刷新页面看是否能解决
            // removeToken();
          }
        } else {
          // We may have an invalid token
          if (showProgress) {
            state.loading = pageState.autoConnect;
          }
          state.open = pageState.autoConnect;
          state.user = null;
          state.provider = '';
          state.walletOS = '';
          state.unReadCount = 0;
          throw new ErrorToken('Invalid token');
        }

        return { data, status };
      } catch (err) {
        logger.error('SessionProvider.refresh error', err);
        state.open = false;
        state.error = err.message;
        if (err?.response?.status === 400) {
          throw new ErrorToken('Invalid token');
        } else {
          throw err;
        }
      } finally {
        if (showProgress) {
          state.loading = false;
        }
      }
    }
  );

  // eslint-disable-next-line require-await
  const handleRefreshToken = useMemoizedFn(async (showProgress = false, onlyRefreshToken = false) => {
    debug('handleRefreshToken', { showProgress, onlyRefreshToken });
    const requestFn = () =>
      request.post(withQuery(joinURL(pageState.prefix, '/refreshSession')), null, {
        headers: {
          authorization: `Bearer ${encodeURIComponent(getRefreshToken())}`,
        },
      });

    setIntervalTime(undefined);
    // 此时 sessionToken 已经没用了，直接移除
    // HACK: 不使用手动删除 sessionToken 的行为，以免造成应用中短时出现没有 sessionToken 的情况 2023-07-18
    // removeSessionToken();
    try {
      await _refresh({ showProgress, requestFn, onlyRefreshToken, type: 'refreshToken' });
      setRetryCount(0);
      setIntervalTime(CHECK_INTERVAL_TIME);
    } catch (err) {
      if (err instanceof ErrorToken) {
        console.warn('refresh token failed, remove all tokens');
        setRetryCount(0);
        debugTmp('[removeToken] useSessionToken -> handleRefreshToken: refresh token failed, remove all tokens');
        removeToken();
        setIntervalTime(CHECK_INTERVAL_TIME);
      } else {
        // HACK: 此时不能 removeToken，因为可能是网络问题导致的，而不是 token 问题
        logger.error('refresh token failed, unexpected error:', err);
        setRetryCount(retryCount + 1);
        await sleep(CHECK_INTERVAL_TIME);
        if (retryCount >= MAX_RETRY_COUNT) {
          // @fix https://github.com/ArcBlock/blocklet-server/issues/9754
          // @fix https://github.com/ArcBlock/blocklet-server/issues/9422
          setRetryCount(0);
          setIntervalTime(undefined);
        } else {
          setIntervalTime(CHECK_INTERVAL_TIME);
        }
      }
    }
  });
  const checkToken = useMemoizedFn((force = false) => {
    if (lazyRefreshToken) {
      return;
    }
    const sessionToken = getSessionToken();
    let needRefresh = force;
    const now = new Date().getTime();
    if (sessionToken) {
      try {
        const { exp, iat } = jwtDecode(sessionToken);
        // NOTICE: 当 sessionToken 剩余有效期为设定有效期的一半时，自动更新一次 token
        if (exp * 1000 - now < now - iat * 1000) {
          needRefresh = true;
        }
      } catch {
        needRefresh = true;
      }
    } else {
      needRefresh = true;
    }

    if (needRefresh) {
      const refreshToken = getRefreshToken();
      // 如果 sessionToken 不存在但是 refreshToken 存在, 需要自动刷新
      if (refreshToken) {
        handleRefreshToken(false, true);
      } else {
        debugTmpInterval('[clearSession-interval] useSessionToken -> checkToken: empty refreshToken');
        clearSession();
        // HACK: 在使用 useReactive 后，即使重复的给子字段赋值为 null，也会引起组件的重渲染，所以对于定时循环来说，一定要避免这一点，防止无效的赋值引起页面重渲染
        if (state.user) {
          state.user = null;
        }
      }
    }
  });

  // ek 参数用于获取 nextToken, 参考: https://github.com/ArcBlock/blocklet-server/issues/5944
  const handleRefreshUser = useMemoizedFn(async ({ showProgress = false, forceRefreshToken = false } = {}) => {
    const requestFn = () =>
      pRetry(() => service.get(joinURL(pageState.prefix, '/session'), { secure: true }), {
        retries: 2,
      });
    debug('handleRefreshUser', { showProgress, forceRefreshToken });
    try {
      if (forceRefreshToken) {
        // 相当于强制触发一次 refreshToken 的刷新
        throw new ErrorToken('need force refresh token');
      }
      await _refresh({ showProgress, requestFn, type: 'refreshUser' });
    } catch (err) {
      if (err instanceof ErrorToken) {
        debug('handleRefreshUser failed, try to refresh token', { err });
        await handleRefreshToken();
      }
    }
  });

  const syncSessionSate = useMemoizedFn(async () => {
    const sessionToken = getSessionToken();
    const refreshToken = getRefreshToken();
    if (state.user) return;

    if (sessionToken || refreshToken) {
      await handleRefreshUser();
    } else {
      debug('[clearSession] syncSessionSate: empty sessionToken and refreshToken');
      clearSession();
      if (state.user) {
        state.user = null;
      }
    }
  });

  const renewToken = useMemoizedFn(() => {
    if (!state.initialized) {
      return;
    }

    // 没有 session 时，页面获得焦点不应该去刷新 session
    if (!state.user) {
      return;
    }

    try {
      checkToken();
    } catch {
      // do nothing
    }
  });

  const handleLoginResult = useMemoizedFn((result) => {
    const loginResult = Array.isArray(result) ? result[0] : result;
    debug('handleLoginResult', { loginResult, result });
    const { loginToken, csrfToken, sessionToken, refreshToken, visitorId, encrypted = true } = loginResult;
    const token = loginToken || sessionToken;
    let decryptSessionToken;
    let decryptRefreshToken;
    let decryptVisitorId;
    if (token) {
      decryptSessionToken = encrypted ? decrypt(token) : token;
      debug('handleLoginResult: setSessionToken', { decryptSessionToken });
      setSessionToken(decryptSessionToken);

      if (csrfToken) {
        const decryptCsrfToken = encrypted ? decrypt(csrfToken) : csrfToken;
        setCsrfToken(decryptCsrfToken);
      }

      if (refreshToken) {
        decryptRefreshToken = encrypted ? decrypt(refreshToken) : refreshToken;
        debug('handleLoginResult: setRefreshToken', { decryptRefreshToken });
        setRefreshToken(decryptRefreshToken);
      }
    }
    if (visitorId) {
      decryptVisitorId = encrypted ? decrypt(visitorId) : visitorId;
      debug('handleLoginResult: setVisitorId', { decryptVisitorId });
      setVisitorId(decryptVisitorId);
    }
  });

  useInterval(
    () => {
      checkCookieLocale();
      checkToken();
    },
    intervalTime,
    { immediate: true }
  );

  return {
    renewToken,
    handleRefreshUser,
    handleRefreshToken,
    syncSessionSate,
    handleLoginResult,
    decrypt,
    removeToken,
    clearSession,
    getSessionToken,
    getRefreshToken,
    setRefreshToken,
    setSessionToken,

    service,
  };
}
