import PropTypes from 'prop-types';
import isUndefined from 'lodash/isUndefined';
import { createContext, use } from 'react';
import Toast from '@arcblock/ux/lib/Toast';
import { useCreation, useLatest, useMemoizedFn, useReactive } from 'ahooks';
import noop from 'lodash/noop';
import omit from 'lodash/omit';
import { joinURL } from 'ufo';
import { translate } from '@arcblock/ux/lib/Locale/util';
import useConfirm from '@arcblock/ux/lib/Dialog/use-confirm';
import { getFederatedEnabled, getMaster, getBlockletData } from '@arcblock/ux/lib/Util/federated';
import { LOGIN_PROVIDER_NAME, BLOCKLET_SERVICE_PATH_PREFIX } from '@arcblock/ux/lib/Util/constant';

// eslint-disable-next-line import/no-unresolved
import { getApiErrorMessage, createAxios, openPopup, runPopup, sleep } from '../utils';
import { PassportSwitcher, parseResponse } from '../components/PassportSwitcher';

import useMobile from '../Session/hooks/use-mobile';
import BindConflictAlert from './bind-conflict-alert';

const OAuthContext = createContext({});
const { Provider, Consumer: OAuthConsumer } = OAuthContext;

const prefix = `${BLOCKLET_SERVICE_PATH_PREFIX}/api/oauth`;

const translations = {
  zh: {
    bindOAuth: '绑定第三方登录',
    bind: '绑定',
    noneOAuth: '未配置第三方登录方式',
    bindOAuthSucceed: '绑定 {provider} 成功',
    unbindOAuthSucceed: '解绑 {provider} 成功',
    loginOAuthFailed: '登录 {provider} 失败',
    bindOAuthFailed: '绑定 {provider} 失败',
    unbindOAuthFailed: '绑定 {provider} 失败',
    loginRequired: '授权请求被拒绝',
    cancelAuth: '取消授权',
    noPassports: '没有可更换的通行证',
    confirm: '我知道了',
    ALREADY_EXIST: '当前账户已存在',
    ALREADY_BIND: '当前账户绑定过 {provider}',
  },
  en: {
    bindOAuth: 'Bind third party login',
    bind: 'Bind ',
    noneOAuth: 'Third party login is not configured',
    bindOAuthSucceed: 'Bind {provider} succeed',
    unbindOAuthSucceed: 'Unbind {provider} succeed',
    loginOAuthFailed: 'Login {provider} failed',
    bindOAuthFailed: 'Bind {provider} failed',
    unbindOAuthFailed: 'Unbind {provider} failed',
    loginRequired: 'You have declined the authentication request',
    cancelAuth: 'Cancel authentication',
    noPassports: 'No passports to switch',
    confirm: 'I know',
    ALREADY_EXIST: 'Your account already exists',
    ALREADY_BIND: 'Your account is already bind to {provider}',
  },
};

function OAuthProvider({
  children,
  locale = 'en',
  onBindOAuth = noop,
  onUnbindOAuth = noop,
  onSwitchPassport = noop,
  session = null,
}) {
  const isMobile = useMobile();
  const { confirmApi, confirmHolder } = useConfirm({
    maxWidth: false,
    PaperProps: {
      style: {
        minWidth: isMobile ? '100%' : 600,
      },
    },
  });
  const state = useReactive({
    baseUrl: '/',
    bindFnSession: undefined,
    bindAuthLoading: false,
    unbindAuthLoading: false,
  });
  const switchState = useReactive({
    open: false,
    currentUser: null,
    passports: [],
    selectedPassport: undefined,
    reset() {
      switchState.open = false;
      switchState.currentUser = null;
      switchState.passports = [];
      switchState.selectedPassport = undefined;
    },
  });
  const oauthState = useReactive({
    user: null,
    // loginLoading: false,
    checking: false,
    loading: false,
    error: '',
    status: '', // scanned, succeed, error
    reset({ status = '' } = {}) {
      oauthState.loading = !!status;
      oauthState.checking = false;
      oauthState.error = '';
      oauthState.status = status;
    },
  });

  const t = useMemoizedFn((key, data = {}) => {
    return translate(translations, key, locale, 'en', data);
  });

  const rawApi = useCreation(() => {
    return createAxios({
      // NOTICE: 请求必须向当前站点发起
      baseURL: prefix,
    });
  }, [state.baseUrl, prefix]);

  const api = useLatest(rawApi);

  const getBlocklet = useMemoizedFn(async () => {
    if (state.baseUrl === '/') {
      return window.blocklet;
    }
    try {
      const url = new URL(state.baseUrl);
      if (url.host === window.location.host) {
        return window.blocklet;
      }
      // eslint-disable-next-line no-empty
    } catch {}

    const blocklet = await getBlockletData(state.baseUrl);
    return blocklet;
  });

  const getOAuthConfigs = useMemoizedFn(async ({ sourceAppPid } = {}) => {
    const blocklet = await getBlocklet();
    const federatedEnabled = getFederatedEnabled(blocklet);
    const master = getMaster(blocklet);
    let result = {};
    if (federatedEnabled && master?.appPid && sourceAppPid === master?.appPid) {
      const masterBlocklet = await getBlockletData(master.appUrl);
      result = masterBlocklet?.settings?.authentication || {};
    } else {
      result = blocklet?.settings?.authentication || {};
    }
    return Object.keys(result).reduce((acc, cur) => {
      if (result[cur]?.type === 'oauth') {
        acc[cur] = result[cur];
      }
      return acc;
    }, {});
  });
  const getOAuthConfigList = useMemoizedFn(async ({ sourceAppPid } = {}) => {
    const oauthConfigs = await getOAuthConfigs({ sourceAppPid });
    const oauthConfigList = Object.entries(oauthConfigs)
      .map(([key, value]) => {
        return { ...value, provider: key };
      })
      .filter((item) => item.enabled)
      .sort((a, b) => {
        if (a?.order !== undefined && b?.order !== undefined) {
          return a.order - b.order;
        }
        if (a?.order !== undefined) {
          return -1;
        }
        return 1;
      });
    return oauthConfigList;
  });

  const getCode = useMemoizedFn(async ({ provider } = {}) => {
    const popup = openPopup(joinURL(state.baseUrl, BLOCKLET_SERVICE_PATH_PREFIX, '/oauth/login', provider), {
      name: 'oauth-login:popup',
      offsetX: 650,
      height: 700,
      width: 500,
    });
    try {
      const data = await runPopup({
        popup,
        closeTimeout: 1500,
      });
      if (data?.response) {
        return data.response?.code;
      }

      return null;
    } catch (err) {
      if (err?.message === 'Popup closed') {
        throw new Error(t('cancelAuth'));
      }
      throw err;
    }
  });

  const bindOAuth = async ({ session: userSession, oauthItem } = {}) => {
    state.bindFnSession = userSession;
    state.bindAuthLoading = true;

    try {
      const code = await getCode(oauthItem);
      const sourceAppPid = state.bindFnSession?.user?.sourceAppPid;
      await api.current.post('/bind', {
        locale,
        provider: oauthItem.provider,
        code,
        sourceAppPid,
      });

      Toast.success(t('bindOAuthSucceed', { provider: LOGIN_PROVIDER_NAME[oauthItem.provider] }));
      onBindOAuth(oauthItem);
    } catch (err) {
      // 对已存在的错误类型特殊处理
      const statusCode = err.response?.status;
      if (statusCode === 409) {
        const { data = {} } = err.response || {};
        const { code } = data || {};
        if (['ALREADY_EXIST', 'ALREADY_BIND'].includes(code)) {
          confirmApi.open({
            title: t(code, { provider: oauthItem.provider }),
            showCancelButton: false,
            confirmButtonText: t('confirm'),
            content: <BindConflictAlert data={data} />,
            onConfirm(close) {
              close();
            },
          });
        } else {
          Toast.error(
            getApiErrorMessage(err, t('bindOAuthFailed', { provider: LOGIN_PROVIDER_NAME[oauthItem.provider] }))
          );
        }
      } else {
        Toast.error(
          getApiErrorMessage(err, t('bindOAuthFailed', { provider: LOGIN_PROVIDER_NAME[oauthItem.provider] }))
        );
      }
    } finally {
      state.bindAuthLoading = false;
    }
  };

  const unbindOAuth = async ({ session: userSession, connectedAccount }) => {
    state.unbindAuthLoading = true;
    try {
      const sourceAppPid = userSession?.user?.sourceAppPid;
      await api.current.post('/unbind', {
        locale,
        connectedAccount: omit(connectedAccount, ['showProvider']),
        sourceAppPid,
      });

      Toast.success(
        t('unbindOAuthSucceed', {
          provider: LOGIN_PROVIDER_NAME[connectedAccount.showProvider || connectedAccount.provider],
        })
      );
      onUnbindOAuth(connectedAccount);
    } catch (err) {
      Toast.error(
        getApiErrorMessage(
          err,
          t('unbindOAuthFailed', {
            provider: LOGIN_PROVIDER_NAME[connectedAccount.showProvider || connectedAccount.provider],
          })
        )
      );
    } finally {
      state.unbindAuthLoading = false;
    }
  };

  const switchOAuthPassport = async (user = {}) => {
    try {
      const { data: passports } = await api.current.get('/passports');
      if (passports.length > 0) {
        switchState.open = true;
        switchState.currentUser = user;
        switchState.passports = passports || [];
      } else {
        Toast.error(t('noPassports'));
      }
    } catch (err) {
      Toast.error(err.message || t('getPassportFailed'));
    }
  };

  const handleSwitchPassport = useMemoizedFn((...args) => {
    switchState.reset();
    onSwitchPassport(...args);
  });

  const setBaseUrl = (value) => {
    state.baseUrl = value || '/';
  };

  const loginOAuth = useMemoizedFn(async ({ provider } = {}, { action = 'login', ...extraParams } = {}) => {
    // 打开 oauth 弹窗
    // 弹窗中回调 code 或者 token 给我
    // 通过 code 或 token 去执行登录操作
    const backupBaseUrl = state.baseUrl;
    if (extraParams?.sourceAppPid === window.blocklet?.appPid) {
      state.baseUrl = window.blocklet?.appUrl || '/';
    } else if (extraParams?.sourceAppPid) {
      const blocklet = await getBlocklet();
      const federatedEnabled = getFederatedEnabled(blocklet);
      const master = getMaster(blocklet);
      if (federatedEnabled && master?.appPid && extraParams?.sourceAppPid === master?.appPid) {
        state.baseUrl = master.appUrl;
      }
    }

    const componentId = window?.blocklet?.componentId;

    try {
      const code = await getCode({ provider });
      // 获取 code 必须设置为主站的 appUrl，设置完后就恢复其原有值
      state.baseUrl = backupBaseUrl;
      await sleep(100);
      if (code) {
        const payload = {
          ...extraParams,
          action,
          locale,
          code,
          provider,
          componentId,
        };
        if (isUndefined(payload.inviter) && window.localStorage.getItem('inviter')) {
          payload.inviter = window.localStorage.getItem('inviter');
        }

        // @FIXME: 暂时兼容一下 connect-to-did-space 的情况 @zhanghan
        // FIXME: @zhanghan 增强安全性，防止 code 泄漏后就可以直接登录。之后可以尝试将 service 接入 CSRF-token
        const loginWithAction = ['connect-to-did-space', 'connect-to-did-domain'].includes(payload.action)
          ? 'login'
          : payload.action;
        const { data: loginData } = await api.current.post('/login', { ...payload, action: loginWithAction });

        const loginResult = parseResponse(loginData);
        loginResult.provider = provider;

        return loginResult;
      }
      state.baseUrl = backupBaseUrl;
      return null;
    } catch (err) {
      const errMsg = getApiErrorMessage(err, t('loginOAuthFailed', { provider: LOGIN_PROVIDER_NAME[provider] }));
      state.baseUrl = backupBaseUrl;
      throw new Error(errMsg);
    }
  });
  const logoutOAuth = useMemoizedFn(() => {});

  return (
    <Provider
      value={{
        locale,
        getOAuthConfigs,
        getOAuthConfigList,
        bindOAuth,
        unbindOAuth,
        loginOAuth,
        logoutOAuth,
        switchOAuthPassport,
        baseUrl: state.baseUrl,
        setBaseUrl,
        oauthState,
        unbindAuthLoading: state.unbindAuthLoading,
        bindAuthLoading: state.bindAuthLoading,
        t,
      }}>
      {children}
      {confirmHolder}
      <PassportSwitcher
        api={api.current}
        locale={locale}
        switchState={switchState}
        onSwitchPassport={handleSwitchPassport}
        session={session}
      />
    </Provider>
  );
}

function useOAuth() {
  const context = use(OAuthContext);

  return omit(context, ['locale', 'getToken']);
}

OAuthProvider.propTypes = {
  children: PropTypes.node.isRequired,
  locale: PropTypes.string,
  onBindOAuth: PropTypes.func,
  onUnbindOAuth: PropTypes.func,
  onSwitchPassport: PropTypes.func,
  session: PropTypes.object,
};

export { OAuthContext, OAuthConsumer, OAuthProvider, useOAuth };
