import { useEffect, useRef } from 'react';
import get from 'lodash/get';
import Cookie from 'js-cookie';
import omitBy from 'lodash/omitBy';
import isNil from 'lodash/isNil';
import omit from 'lodash/omit';
import cloneDeep from 'lodash/cloneDeep';
import { useCreation, useMemoizedFn, useReactive } from 'ahooks';
import useInterval from '@arcblock/react-hooks/lib/useInterval';
import useBrowser from '@arcblock/react-hooks/lib/useBrowser';
import { getVisitorId, isUrl, stringifyQuery } from '@arcblock/ux/lib/Util';
import { WsClient } from '@arcblock/ws';

import {
  decodeConnectUrl,
  parseTokenFromConnectUrl,
  updateConnectedInfo,
  parseNextWorkflow,
  createAxios,
  sleep,
  getConnectedInfo,
  getExtraHeaders,
} from '../../utils';

import translations from '../assets/locale';
import useSecurity from './security';
import usePageShow from './page-show';
import { RELAY_SOCKET_PREFIX } from '../../constant';

function getSocketHost(baseUrl) {
  const targetUrl = baseUrl || window.location.href;
  return new URL(targetUrl).host;
}

const fns = {};
function createTokenFn({ action, prefix, checkFn, extraParams, baseUrl }) {
  const key = `${prefix}/token?${stringifyQuery(extraParams)}`;
  if (!fns[key]) {
    fns[key] = async function createToken() {
      const startTime = +new Date();
      const res = await checkFn(key, { headers: getExtraHeaders(baseUrl) });
      const endTime = +new Date();
      if (endTime - startTime < 500) {
        await sleep(500 - (endTime - startTime));
      }
      if (res?.data?.token) {
        return res.data;
      }

      if (res?.data?.error) {
        throw new Error(res.data.error);
      }

      const errorUrl = isUrl(prefix) ? prefix : `${baseUrl}${prefix}`;
      throw new Error(`Error generating ${action} QR Code from: ${errorUrl}`);
    };
  }
  return fns[key];
}

// 从 url params 中获取已存在的 session (token & connect url)
const parseExistingSession = () => {
  try {
    const url = new URL(window.location.href);
    const connectUrlParam = url.searchParams.get('__connect_url__');
    if (!connectUrlParam) {
      return null;
    }
    const connectUrl = decodeConnectUrl(connectUrlParam);
    const token = parseTokenFromConnectUrl(connectUrl);
    return {
      token,
      url: connectUrl,
    };
  } catch (e) {
    return {
      error: e,
    };
  }
};

// FIXME: @zhanghan 当 connect 组件使用不同的 baseUrl 时，不应该直接从 window.blocklet 去取值
const getAppId = () =>
  get(globalThis, 'blocklet.appPid') || get(globalThis, 'blocklet.appId') || get(globalThis, 'env.appId') || '';
const getAppPrefix = () =>
  (get(globalThis, 'env.apiPrefix') || '/').replace(/\/$/, '').replace(RELAY_SOCKET_PREFIX, '');
const getRelayChannel = (token) => `relay:${getAppId()}:${token}`;
const getRelayProtocol = () => (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
const isSameOrigin = (baseUrl) => !baseUrl || new URL(baseUrl).origin === window.location.origin;
const isSessionEnded = (status) => ['succeed', 'error', 'timeout', 'busy'].includes(status);

export default function useToken({
  action,
  checkFn,
  checkInterval,
  checkTimeout,
  extraParams,
  locale,
  prefix,
  tokenKey,
  encKey,
  onError,
  onSuccess,
  baseUrl,
  autoConnect = true,
  forceConnected = true,
  saveConnect = true, // FIXME: @zhanghan 将来需要设置为默认 false，仅在需要的时候进行保存 login, switch/role/profile
  useSocket = true,
  provider = 'wallet',
}) {
  /**
   * @type {import('../../types').TokenState}
   */
  const state = useReactive({
    checking: false,
    loading: false,
    token: '',
    url: '',
    store: null,
    status: 'created',
    error: '',
    mfaCode: 0,
    appInfo: null,
    memberAppInfo: null,
    connectedDid: '',
    saveConnect: false,
    inExistingSession: false,
    nextWorkflow: '',
    baseUrl,
    prefix: `${prefix}/${action}`,
    extraParams: omitBy(extraParams, isNil),
    checkFn,
    results: {},
    action,
    provider,
    reset() {
      this.error = '';
      this.status = 'created';
    },
  });

  // 使用 ref 存储 checkCount，避免计数时触发不必要的渲染
  const checkCountRef = useRef(0);
  const statusExtraParams = useRef(null);

  const translation = translations[locale] || translations.en;
  const existingSession = useCreation(() => parseExistingSession(), []);
  const maxCheckCount = Math.ceil(checkTimeout / checkInterval);

  const currentState = useReactive({
    onSuccessCalled: false,
    cancelWhenScannedCounter: 0,
    isSocketAvailable: false,
  });

  const { encryptKey, decrypt } = useSecurity();

  // scanned 状态下 cancel (Back 按钮) 操作的计数器, 每次 cancel 操作时 +1
  // - 外部可以传入 autoConnect 表示是否启用自动连接
  // - 计数器默认为 0, 表示还未进行过 cancel 操作
  // - 如果计数器 > 0, 说明用户进行过 cancel 操作
  //   (临时禁用, 重新打开 did-connect 窗口时恢复)
  const browser = useBrowser();
  const visitorId = getVisitorId();
  const socket = useRef(null);
  const subscription = useRef(null);

  const params = {
    ...state.extraParams,
    locale,
    [tokenKey]: state.token || existingSession?.token,
    provider: state.provider,
  };

  const unsubscribe = async () => {
    // HACK: 如果 url 中已经存在 session，则不允许注销当前的 session（需要利用当前 session 作为 sourceToken 去创建一个新的 session）
    if (existingSession?.token && existingSession?.token === state.token) {
      return;
    }
    try {
      if (params[tokenKey] && state.status === 'created') {
        await state.checkFn(`${state.prefix}/timeout?${stringifyQuery(params)}`, {
          headers: getExtraHeaders(state.baseUrl),
        });
      }
    } catch (err) {
      // Do nothing
    }

    try {
      if (state.token && subscription.current) {
        await socket.current.unsubscribe(getRelayChannel(state.token));
        subscription.current = null;
      }
    } catch (err) {
      // Do nothing
    }
  };

  // HACK: 为了保证 createToken 函数永远都是最新的，需要使用 useMemoizedFn 来包裹
  const createToken = useMemoizedFn((...args) => {
    const innerParams = {
      [encKey]: encryptKey,
      ...state.extraParams,
      locale,
      forceConnected,
      // - autoConnect 请求参数用于控制服务端是否给钱包应用发送自动连接通知
      // - 使用 connect 组件时明确传入了 autoConnect = false, 则 autoConnect 请求参数 为 false
      // - 如果 cancelWhenScannedCounter > 0, 说明用户进行过 cancel 操作, 则临时禁用自动连接, autoConnect 请求参数 为 false
      //   (避免 "无限自动连接问题")
      // - 如果上次连接设备 (connected_wallet_os) 不是 ios/android, 则禁用自动连接
      autoConnect:
        autoConnect &&
        // 如果是 wallet webview 环境, 不发送通知, 避免 wallet 连续弹出 auth 窗口 2 次 (#341)
        !(browser.wallet || browser.arcSphere) &&
        !currentState.cancelWhenScannedCounter &&
        ['ios', 'android'].includes(Cookie.get('connected_wallet_os')),
      provider: state.provider,
    };

    if (visitorId) {
      // NOTICE: 这个 visitorId 必须保留，才能在三方的 did connect 流程中正确传递
      innerParams.visitorId = visitorId;
    }

    const mergeInData = omit(statusExtraParams.current || {}, ['sourceAppPid', 'verbose']);
    Object.keys(mergeInData).forEach((x) => {
      if (!innerParams[x] || (x === encKey && mergeInData[x])) {
        innerParams[x] = mergeInData[x];
      }
    });
    if (existingSession?.token && !innerParams.sourceToken) {
      innerParams.sourceToken = existingSession?.token;
    }
    const fn = createTokenFn({
      action,
      prefix: state.prefix,
      baseUrl: state.baseUrl,
      checkFn: state.checkFn,
      extraParams: innerParams,
    });
    return fn(...args);
  });

  // 切换账户或者 token 过期之后需要重新生成
  const generate = useMemoizedFn(async (cleanup = true) => {
    if (cleanup) {
      unsubscribe();
    }

    try {
      state.loading = true;
      state.token = '';
      state.url = '';
      state.store = null;
      state.extraParams = omitBy(extraParams, isNil);
      // HACK: 更改了 createToken 中依赖的参数，需要强制等待 createToken 函数更新后，再调用 createToken 函数
      await sleep();

      const data = await createToken();
      const extra = data.extra || {};

      state.loading = false;
      state.token = data.token;
      state.url = data.url;
      state.status = 'created';
      state.error = '';
      checkCountRef.current = 0;
      state.appInfo = data.appInfo;
      state.memberAppInfo = data.memberAppInfo;
      state.connectedDid = extra.connectedDid;
      state.saveConnect = extra.saveConnect;
    } catch (err) {
      state.loading = false;
      state.status = 'error';
      state.error = `${translation.generateError}: ${err.message}`;
    }
  });

  // 每次 cancel 操作时计数器 +1 => 重新生成 token
  const cancelWhenScanned = useMemoizedFn(() => {
    currentState.cancelWhenScannedCounter++;
  });
  useEffect(() => {
    // FIXME: @zhanghan 在有 nw 的情况下，需要考虑如何进行 regenrate
    // 计数器 > 0, 说明人为触发了 cancel, 重新生成 token，但是只在没有 nextWorkflow 的情况下重新生成
    if (currentState.cancelWhenScannedCounter > 0) {
      if (extraParams.nw) {
        onError(new Error(translation.retryForbidden));
      } else {
        generate();
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [currentState.cancelWhenScannedCounter]);

  useEffect(() => {
    const isSafari = /^((?!chrome|android).)*safari/i.test(globalThis.navigator.userAgent);
    const disableSocket = isSafari || Object.hasOwn(globalThis?.blocklet || {}, 'DID_CONNECT_DISABLE_SOCKET');
    // FIXME: @zhanghan websocket 默认支持跨域，这里为何要检查域名是同源
    if (!disableSocket && useSocket && isSameOrigin(state.baseUrl) && getAppId()) {
      const needReconnect =
        !socket.current || socket.current.isConnected() === false || socket.current.baseUrl !== state.baseUrl;
      if (needReconnect) {
        socket.current = new WsClient(
          `${getRelayProtocol()}//${getSocketHost(state.baseUrl)}${getAppPrefix()}${RELAY_SOCKET_PREFIX}/relay`,
          {
            longpollerTimeout: 5000, // connection timeout
            heartbeatIntervalMs: 30 * 1000,
          }
        );
        socket.current.baseUrl = state.baseUrl;
        socket.current.onOpen(() => {
          currentState.isSocketAvailable = true;
        });
        socket.current.connect();
      }
    } else if (currentState.isSocketAvailable) {
      currentState.isSocketAvailable = false;
    }

    return () => {
      if (socket.current) {
        socket.current.disconnect();
        socket.current = null;
      }
    };
  }, [state.baseUrl]); // eslint-disable-line react-hooks/exhaustive-deps

  const getCheckInterval = () => {
    if (!state.token) {
      return null;
    }
    if (state.loading) {
      return null;
    }
    if (isSessionEnded(state.status)) {
      return null;
    }

    return checkInterval;
  };

  // 任何导致 Connect 组件 unmounted 的操作 (比如 dialog 关闭) => 调用 timeout api 清除 token
  // 关闭时如果 token 状态处于 created，那么应该通知后端给删掉
  // 说明: 此处需要借助 useRef 在 useEffect return function 中访问 state
  const closeSessionRef = useRef(null);
  useEffect(() => {
    closeSessionRef.current = { state, params };
  });
  useEffect(() => {
    return () => {
      // eslint-disable-next-line no-shadow
      const { state, params } = closeSessionRef.current;
      if (state.status === 'created') {
        if (params[tokenKey]) {
          state.checkFn(`${state.prefix}/timeout?${stringifyQuery(params)}`, {
            headers: getExtraHeaders(state.baseUrl),
          });
        }
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Check auth token status
  const checkStatus = useMemoizedFn(async (force = false) => {
    if ((state.checking || document.hidden) && !force) {
      return null;
    }

    // NOTICE: 仅当状态为 created 时才去计算次数统计
    if (state.status === 'created') {
      checkCountRef.current++;

      // 超时判断：超过最大检查次数则标记为超时
      if (state.status !== 'timeout' && checkCountRef.current > maxCheckCount) {
        state.status = 'timeout';
        unsubscribe();
        return null;
      }
    }

    if (currentState.isSocketAvailable && !force) {
      return null;
    }

    try {
      state.checking = true;
      const res = await state.checkFn(`${state.prefix}/status?${stringifyQuery(params)}`, {
        headers: getExtraHeaders(state.baseUrl),
      });
      const { status, error: newError, mfaCode = 0 } = res.data;

      state.store = res.data;
      state.mfaCode = mfaCode;
      state.checking = false;
      if (state.status === 'scanned' && status === 'created') {
        // HACK: 不把 scanned 状态切换回 created
      } else {
        state.status = status;
      }
      if (status === 'error' && newError) {
        const err = new Error(newError);
        err.response = res;
        throw err;
      }
      return res.data;
    } catch (err) {
      const { response } = err;
      if (response?.status) {
        if (params[tokenKey]) {
          state.checkFn(`${state.prefix}/timeout?${stringifyQuery(params)}`, {
            headers: getExtraHeaders(state.baseUrl),
          });
        }
        const _msg = response?.data?.error ? response.data.error : err.message;
        const _err = new Error(_msg);
        _err.code = response.status;
        state.status = 'error';
        state.checking = false;
        state.error = _msg;
        onError(_err, state?.store, decrypt);
      } else {
        state.status = 'error';
        state.checking = false;
        state.error = translation.generateError;
      }
    }
    return null;
  });

  const applyExistingToken = async () => {
    try {
      if (existingSession.error) {
        throw existingSession.error;
      }
      state.loading = true;
      state.token = existingSession.token;
      state.url = existingSession.url;
      state.inExistingSession = true;
      let data = await checkStatus();

      // 1. 如果 existingSession 当前状态已经是 scanned，则刷新页面后，应该根据 existingSession.token 生成一个新的 session，继续完成后续的操作
      // 2. HACK: 如果当前 existingSession 已经产生过新的 session，则目前已经无法通过 existingSession 本身来监听变化了，需要再创建一个新的 session 才能完成监听状态
      if (['scanned'].includes(data?.status) || data?.sourceToken === existingSession?.token) {
        await generate(false);
        data = await checkStatus();
      }
      statusExtraParams.current = data?.extraParams || null;
      // existingSession.token 合法的初始状态必须是 created
      if (data?.status && !['created'].includes(data?.status)) {
        throw new Error(`${translation.invalidSessionStatus} [${data.status}]`);
      }
    } catch (e) {
      state.status = 'error';
      state.error = e.message;
      onError(e, state?.store, decrypt);
    } finally {
      state.loading = false;
    }
  };

  // eslint-disable-next-line react-hooks/exhaustive-deps
  useEffect(() => {
    if (!state.token && !state.store && !state.loading && !state.error) {
      // connect to existing session if any
      if (existingSession) {
        applyExistingToken();
        // Create our first token if we do not have one
      } else {
        generate(false);
        return;
      }
    }

    // Trigger on success if we completed the process
    if (state.status === 'succeed' && !currentState.onSuccessCalled) {
      // save connected_did to cookie if only on same origin
      const connectedInfo = getConnectedInfo({
        ...state.store,
        appInfo: state.appInfo,
        memberAppInfo: state.memberAppInfo,
      });
      if (isSameOrigin(state.baseUrl) && saveConnect && state.saveConnect && state.store.did) {
        updateConnectedInfo(connectedInfo, true);
      }

      if (state.store) {
        const { nextWorkflow } = state.store;
        // Switch to nextWorkflow if we have
        if (nextWorkflow) {
          try {
            const parsed = parseNextWorkflow(nextWorkflow, tokenKey);
            state.nextWorkflow = parsed?.nextWorkflow || nextWorkflow;
            Object.assign(state, parsed);
            state.store = null;
            state.error = '';
            state.url = '';
            state.appInfo = null;
            state.memberAppInfo = null;
            checkCountRef.current = 0;
            state.status = 'scanned';
            state.results = {
              ...state.results,
              [state.token]: state.store,
            };
            state.checkFn = createAxios({ baseURL: parsed.baseUrl, timeout: 8000 }).get;
            checkStatus();
          } catch (err) {
            console.error(`Invalid nextWorkflow: ${nextWorkflow}`, err);
            state.status = 'error';
            state.error = `Invalid nextWorkflow: ${nextWorkflow}: ${err.message}`;
          }
        } else {
          if (state.nextWorkflow) {
            state.nextWorkflow = '';
            state.url = '';
          }

          // trigger success callback when last workflow complete
          if (typeof onSuccess === 'function') {
            const results = Object.values({ ...state.results, [state.token]: state.store });
            currentState.onSuccessCalled = true;
            const result = results.length > 1 ? results : results[0];
            const clonedResult = cloneDeep(result);
            onSuccess(clonedResult, decrypt, {
              sourceAppPid: extraParams?.sourceAppPid,
              ...connectedInfo,
            });
          }
        }
      }
      return;
    }

    // FIXME: @zhanghan 待调查最近 ws 通道不会将 status 设置为 status，而直接进行到下一 nextWorkflow status 为 scanned 的情况
    // 此处仅用于处理上述情况
    if (state.store?.nextWorkflow) {
      const { nextWorkflow } = state.store;
      try {
        const parsed = parseNextWorkflow(nextWorkflow, tokenKey);
        state.nextWorkflow = parsed?.nextWorkflow || nextWorkflow;
        Object.assign(state, parsed);
        state.store = null;
        state.error = '';
        state.url = '';
        state.appInfo = null;
        state.memberAppInfo = null;
        checkCountRef.current = 0;
        state.status = 'scanned';
        state.results = {
          ...state.results,
          [state.token]: state.store,
        };
        state.checkFn = createAxios({ baseURL: parsed.baseUrl, timeout: 8000 }).get;
        checkStatus();
      } catch (err) {
        console.error(`Invalid nextWorkflow: ${nextWorkflow}`, err);
        state.status = 'error';
        state.error = `Invalid nextWorkflow: ${nextWorkflow}: ${err.message}`;
      }
    }
  });
  const subscriptionUpdatedAt = useRef(0);

  // Try to use websocket
  useEffect(() => {
    if (state.token && currentState.isSocketAvailable && socket.current) {
      let needSubscription = false;
      if (subscription.current) {
        if (subscription.current.token !== state.token) {
          socket.current.unsubscribe(getRelayChannel(subscription.current.token));
          needSubscription = true;
        }
      } else {
        needSubscription = true;
      }

      if (needSubscription) {
        subscription.current = socket.current.subscribe(getRelayChannel(state.token));
        // 每次重新监听时，需要重置 subscriptionUpdatedAt
        subscriptionUpdatedAt.current = 0;
        subscription.current.token = state.token;
        subscription.current.on('updated', ({ response }) => {
          const updatedAt = +new Date(response.updatedAt);
          if (updatedAt <= subscriptionUpdatedAt.current) {
            console.warn('Ignore outdated message', response);
            return;
          }

          subscriptionUpdatedAt.current = updatedAt;
          let { status, error } = response;

          if (status === 'forbidden') {
            error = translation.forbidden;
            status = 'error';
          }
          state.status = status;
          state.mfaCode = response.mfaCode || 0;
          state.store = response;
          state.error = error;
          // ws 通道发现的错误，也需要 onError 回调
          if (error) {
            onError(new Error(error), state?.store, decrypt);
          }
        });
      }
    }
  }, [state.token, currentState.isSocketAvailable, socket.current]); // eslint-disable-line react-hooks/exhaustive-deps

  useInterval(checkStatus, getCheckInterval());

  // Restore session status when page lost focus and get focus again
  usePageShow(() => {
    // HACK: 由于 safari 的特性，在页面不可见时，无法正常发起请求，所以需要在页面可见时强制发送一次请求来更新状态
    if (state.token && isSessionEnded(state.status) === false) {
      state.checking = false;
      // HACK: 不能在页面可见时立刻发起请求，仍然可能会被 safari 拦截，需要等一段时间
      setTimeout(() => {
        checkStatus(true);
      }, 100);
    }
  });

  return { state, generate, cancelWhenScanned };
}
