const JWT = require('@arcblock/jwt');
const { CustomError } = require('@blocklet/error');
const { callFederated } = require('@abtnode/auth/lib/util/federated');
const { fromAppDid } = require('@arcblock/did-ext');

const { SECURITY_RULE_DEFAULT_ID, ROLES } = require('@abtnode/constant');
const { getApplicationInfo, messages } = require('@abtnode/auth/lib/auth');
const { extractUserAvatar, getAvatarByEmail, getUserAvatarUrl, getAvatarByUrl } = require('@abtnode/util/lib/user');
const getRequestIP = require('@abtnode/util/lib/get-request-ip');
const formatContext = require('@abtnode/util/lib/format-context');
const { checkInvitedUserOnly } = require('@abtnode/auth/lib/oauth');
const { getDeviceData } = require('@abtnode/util/lib/device');
const getOrigin = require('@abtnode/util/lib/get-origin');
const { parseWalletUA } = require('@arcblock/did-connect-js/lib/handlers/util');
const { USER_SESSION_STATUS } = require('@blocklet/constant');
const isUrl = require('is-url');
const uniqBy = require('lodash/uniqBy');
const { isSlackWebhookUrl } = require('@abtnode/util/lib/url-evaluation');
const { isAllowedURL } = require('@abtnode/util/lib/ssrf-protector');

const federatedUtil = require('./federated');
const initJwt = require('../libs/jwt');
const { createTokenFn, getDidConnectVersion } = require('.');

// 当无法从 gravatar 获取头像时，使用默认头像
const defaultAvatar =
  'data:image/png;base64, ';

/**
 * @description 获取默认的通行证（常见用于 oauth 账户账户首次登录，无任何可以使用的通行证的情况下，日志中记录的通行证信息）
 * @returns { name: string, role: string }
 */
const getDefaultPassport = () => {
  return { name: 'Guest', role: 'guest', scope: 'passport' };
};

/**
 * @description 从 request 参数中获取验证码
 * @param {object} options
 * @param {object} options.logger 日志对象
 * @param {object} options.req 请求对象
 * @returns {Promise<string>}
 */
const getVerifyCodeFromReq = async ({ logger, req }) => {
  const { code, magicToken } = req.body;
  const blocklet = await req.getBlocklet();
  const blockletInfo = await req.getBlockletInfo();
  const teamDid = blocklet.appPid;
  let finalCode = code;
  if (!code && magicToken) {
    const valid = await JWT.verify(magicToken, blockletInfo.wallet.publicKey);
    if (!valid) {
      logger.error('Email login: Invalid magic token', { teamDid, magicToken });
      throw new CustomError(401, 'Invalid magic link');
    }
    const decodeData = JWT.decode(magicToken, true);
    if (!decodeData?.data?.code) {
      logger.error('Email login: failed to parse magicToken data', { teamDid, magicToken, decodeData });
      throw new CustomError(400, 'Invalid magic token format');
    }
    finalCode = decodeData?.data?.code;
  }
  return finalCode;
};

/**
 * @description 根据 sub 获取用户钱包信息
 * @param {string} sub 用户信息 sub
 * @param {object} options
 * @param {object} options.req 请求对象
 * @returns {Promise<{ wallet: string }>} 用户钱包信息
 */
const getUserFromSub = async (sub, { req }) => {
  const blocklet = await req.getBlocklet();
  const blockletInfo = await req.getBlockletInfo();
  const { sourceAppPid = null } = req.body;

  let userWallet;
  if (sourceAppPid) {
    const masterSite = federatedUtil.getFederatedMaster(blocklet);
    const { permanentWallet } = blockletInfo;
    const result = await callFederated({
      action: 'getUser',
      site: masterSite,
      permanentWallet,
      data: {
        userSub: sub,
      },
    });
    userWallet = result?.wallet;
  } else {
    userWallet = fromAppDid(sub, blockletInfo.wallet.secretKey);
  }
  return {
    wallet: userWallet,
  };
};

const getAvatarBnByEmail = async (email, { req, node }) => {
  if (!email) {
    return defaultAvatar;
  }
  const blocklet = await req.getBlocklet();
  const nodeInfo = await req.getNodeInfo();
  const teamDid = blocklet.appPid;
  const { dataDir } = await getApplicationInfo({ node, nodeInfo, teamDid });
  let result = await getAvatarByEmail(email);
  if (!result) result = defaultAvatar;
  result = await extractUserAvatar(result, { dataDir });

  return result;
};

const getAvatarBnByUrl = async (url, { req, node }) => {
  if (!url) {
    return defaultAvatar;
  }
  const blocklet = await req.getBlocklet();
  const nodeInfo = await req.getNodeInfo();
  const teamDid = blocklet.appPid;
  const { dataDir } = await getApplicationInfo({ node, nodeInfo, teamDid });
  let result;
  try {
    result = await getAvatarByUrl(url);
  } catch {
    result = defaultAvatar;
  }
  result = await extractUserAvatar(result, { dataDir });
  return result;
};

const getUserNameByEmail = (email) => {
  // 目前通过 email 前缀来获取用户名称
  return email.split('@')[0];
};

const loginUserSession = async (
  { did, pk, profile, passport, sourceAppPid, connectedAccount, provider },
  { req, node, options, loggedUser }
) => {
  if (!did || !pk) {
    throw new CustomError(400, 'Missing required parameters');
  }
  const blocklet = await req.getBlocklet();
  const blockletInfo = await req.getBlockletInfo();
  const { locale = 'en' } = req.query;
  const teamDid = blocklet.appPid;
  const lastLoginIp = getRequestIP(req);
  const passportForLog = passport || getDefaultPassport();

  if (!loggedUser) {
    // eslint-disable-next-line no-param-reassign
    loggedUser = await node.loginUser({
      teamDid,
      user: {
        ...profile,
        did,
        pk,
        locale,
        lastLoginIp,
        sourceAppPid,
        passport,
        connectedAccount,
      },
    });
  }

  await node.createAuditLog(
    {
      action: 'login',
      args: {
        teamDid,
        userDid: did,
        passport: passportForLog,
        provider,
        sourceAppPid,
      },
      context: formatContext(Object.assign(req, { user: loggedUser })),
      result: loggedUser,
    },
    node
  );

  const ua = req.get('user-agent');
  const visitorId = req.get('x-blocklet-visitor-id');
  const deviceData = getDeviceData({ req });
  const parsedUA = parseWalletUA(ua);
  const walletOS = parsedUA?.os || 'web';
  const userSessionDoc = await node.upsertUserSession({
    teamDid,
    userDid: did,
    visitorId,
    appPid: teamDid,
    passportId: passport?.id,
    status: USER_SESSION_STATUS.ONLINE,
    ua: null,
    lastLoginIp,
    extra: {
      walletOS,
      device: deviceData,
    },
    locale,
    origin: await getOrigin({ req }),
  });

  if (federatedUtil.shouldSyncFederated(blocklet, sourceAppPid)) {
    const syncUserData = {
      ...profile,
      did,
      pk,
      connectedAccount: [connectedAccount],
      inviter: loggedUser.inviter,
    };
    if (syncUserData.avatar) {
      syncUserData.avatar = getUserAvatarUrl(blockletInfo.appUrl, syncUserData.avatar);
    }
    const masterSite = federatedUtil.getFederatedMaster(blocklet);

    await node.syncFederated({
      did: teamDid,
      data: {
        users: [
          {
            ...syncUserData,
            action: 'connectAccount',
            sourceAppPid: sourceAppPid || masterSite?.appPid,
          },
        ],
      },
    });
    await node.syncUserSession({
      teamDid,
      userDid: userSessionDoc.userDid,
      visitorId: userSessionDoc.visitorId,
      passportId: passport?.id,
      targetAppPid: sourceAppPid,
      ua,
      lastLoginIp,
      extra: {
        walletOS,
        device: deviceData,
      },
    });
  }
  const role = passport?.scope === 'passport' ? passport.role : ROLES.GUEST;
  const org = await req.getUserOrg(role);
  const { createSessionToken } = initJwt(node, Object.assign({}, options, { org }));
  const createToken = createTokenFn(createSessionToken);
  const sessionConfig = blocklet.settings?.session || {};
  const { sessionToken, refreshToken } = createToken(
    did,
    {
      secret: blockletInfo.secret,
      passport,
      role,
      fullName: loggedUser.fullName,
      provider,
      walletOS,
      emailVerified: !!loggedUser?.emailVerified,
      phoneVerified: !!loggedUser?.phoneVerified,
    },
    { ...sessionConfig, didConnectVersion: getDidConnectVersion(req) }
  );

  return {
    sessionToken,
    refreshToken,
    visitorId: userSessionDoc.visitorId,
  };
};

const checkNeedInvite = async ({ req, node, teamDid, locale }) => {
  const { accessPolicyConfig } = await req.getSecurityConfig({ id: SECURITY_RULE_DEFAULT_ID });
  const isInvitedUserOnly = await checkInvitedUserOnly(accessPolicyConfig, node, teamDid);
  if (isInvitedUserOnly) {
    throw new CustomError(403, messages.notInvited[locale]);
  }
};

// eslint-disable-next-line require-await
const validateWebhooks = async ({ webhooks, user }) => {
  if (!webhooks || !Array.isArray(webhooks) || webhooks.length === 0) {
    return { value: webhooks, error: null };
  }

  const existWebhooks = user.extra?.webhooks || [];

  // 找出用户输入中不存在于数据库的 webhook（新增或修改的）
  const webhooksToValidate = webhooks.filter((webhook) => {
    return !existWebhooks.some((existing) => existing.url === webhook.url);
  });

  // 删除操作不进行校验
  if (!webhooksToValidate.length) {
    return { value: webhooks, error: null };
  }

  // 对需要校验的 webhook 进行校验
  for (const webhook of webhooksToValidate) {
    const { url, type } = webhook;
    // 1. 校验 URL 是否合法
    if (!isUrl(url)) {
      return { value: null, error: 'Invalid webhook URL' };
    }

    // 2. 校验类型是否正确
    if (!['slack', 'api'].includes(type) || (type === 'slack' && !isSlackWebhookUrl(url))) {
      return { value: null, error: 'Invalid webhook type' };
    }
    // 3. 校验 URL 是否存在 SSRF 攻击
    // eslint-disable-next-line no-await-in-loop
    if (!(await isAllowedURL(url))) {
      return { value: null, error: 'Invalid webhook URL: internal' };
    }
  }

  // 去重：webhooks 包含全量的数据，去重时会把历史数据存在的重复数据清理掉
  const uniqueWebhooks = uniqBy(webhooks, 'url').map((webhook) => {
    return {
      ...webhook,
      consecutiveFailures: webhook.enabled ? webhook.consecutiveFailures : 0,
    };
  });

  return { value: uniqueWebhooks, error: null };
};

module.exports = {
  getDefaultPassport,
  getVerifyCodeFromReq,
  getUserFromSub,
  getAvatarBnByEmail,
  getAvatarBnByUrl,
  getUserNameByEmail,
  loginUserSession,
  checkNeedInvite,
  validateWebhooks,
};
