/* eslint-disable arrow-parens */
const get = require('lodash/get');
const joinUrl = require('url-join');
const {
  messages,
  getVCFromClaims,
  checkWalletVersion,
  getUser,
  getPassportStatusEndpoint,
  validatePassportStatus,
} = require('@abtnode/auth/lib/auth');
const {
  ROLES,
  NFT_TYPE_GENERAL_PASSPORT,
  NFT_TYPE_GENERAL_PASSPORT_OLD,
  NFT_TYPE_NODE_PASSPORT,
} = require('@abtnode/constant');
const {
  createPassportVC,
  createPassport,
  validatePassport,
  isUserPassportRevoked,
  upsertToPassports,
  getRoleFromLocalPassport,
  getRoleFromExternalPassport,
  createUserPassport,
} = require('@abtnode/auth/lib/passport');
const logger = require('@abtnode/logger')(require('../../package.json').name);

const vcTypes = [NFT_TYPE_GENERAL_PASSPORT, NFT_TYPE_GENERAL_PASSPORT_OLD, NFT_TYPE_NODE_PASSPORT];

/**
 * @returns {Array} config
 * @returns {Boolean} config[0] is invited user only
 * @returns {String} config[1] default role
 * @returns {Boolean} config[2] issue passport
 */
const isInvitedUserOnly = async (invitedUserOnly, node, teamDid) => {
  if (invitedUserOnly === 'not-first') {
    const count = await node.getUsersCount({ teamDid });
    return count > 0 ? [true] : [false, ROLES.ADMIN, true];
  }

  if (invitedUserOnly === true || invitedUserOnly === 'yes') {
    return [true];
  }

  return [false, ROLES.GUEST];
};

module.exports = function createRoutes(node, authenticator, login, opts) {
  return {
    action: 'login',
    claims: {
      profile: async ({ extraParams, context }) => {
        const { locale } = extraParams;

        const config = await context.request.getServiceConfig();
        const profileFields = get(config, 'profileFields');

        return {
          fields: profileFields || ['fullName', 'avatar'],
          description: messages.description[locale],
        };
      },

      verifiableCredential: async ({ context, extraParams: { locale } }) => {
        const { request, abtwallet } = context;
        const { wallet, did: teamDid } = await request.getBlockletInfo();

        checkWalletVersion({ abtwallet, locale });

        const blocklet = await request.getBlocklet();
        const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
        const trustedIssuers = [wallet.address, ...trustedPassports].filter(Boolean);

        const config = (await request.getServiceConfig()) || {};
        const [invitedUserOnly] = await isInvitedUserOnly(config.invitedUserOnly, node, teamDid);

        return {
          description: messages.requestPassport[locale],
          item: vcTypes,
          trustedIssuers,
          optional: !invitedUserOnly,
        };
      },
    },

    // eslint-disable-next-line consistent-return
    onAuth: async ({ claims, challenge, userDid, userPk, token, storage, extraParams, req, baseUrl }) => {
      const { locale } = extraParams;
      const blocklet = await req.getBlocklet();
      const { wallet, name, did: teamDid } = await req.getBlockletInfo();
      const teamAppDid = wallet.address;

      // check user approved
      const user = await getUser(node, teamDid, userDid);
      if (user && !user.approved) {
        throw new Error(messages.notAllowed[locale]);
      }

      // Get passport
      const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
      const trustedIssuers = [teamAppDid, ...trustedPassports].filter(Boolean);
      let { vc } = await getVCFromClaims({
        claims,
        challenge,
        trustedIssuers,
        vcTypes,
        locale,
      });

      const config = (await req.getServiceConfig()) || {};
      const [invitedUserOnly, defaultRole, issuePassport] = await isInvitedUserOnly(
        config.invitedUserOnly,
        node,
        teamDid
      );

      if (invitedUserOnly && !vc) {
        throw new Error(messages.missingCredentialClaim[locale]);
      }

      // issue passport for the first login user in a invite-only team
      if (issuePassport) {
        logger.info('issue passport to user at the login workflow', { role: defaultRole });
        vc = createPassportVC({
          issuerName: name,
          issuerWallet: wallet,
          ownerDid: userDid,
          passport: await createPassport({
            name: defaultRole,
            node,
            teamDid,
            locale,
            endpoint: baseUrl,
          }),
          endpoint: getPassportStatusEndpoint({
            baseUrl: joinUrl(baseUrl, opts.prefix),
            userDid,
            teamDid,
          }),
        });
      }

      // Get user passport from vc
      let passport = vc ? createUserPassport(vc) : null;
      if (user && passport && isUserPassportRevoked(user, passport)) {
        throw new Error(messages.passportRevoked[locale](name));
      }

      // Get role
      let role = ROLES.GUEST;
      if (vc) {
        await validatePassport(get(vc, 'credentialSubject.passport'));
        const issuerId = get(vc, 'issuer.id');
        if (issuerId === teamAppDid) {
          role = getRoleFromLocalPassport(get(vc, 'credentialSubject.passport'));
        } else {
          // map external passport to local role
          const { mappings = [] } = (blocklet.trustedPassports || []).find((x) => x.issuerDid === issuerId) || {};
          role = await getRoleFromExternalPassport({
            passport: get(vc, 'credentialSubject.passport'),
            node,
            teamDid,
            locale,
            mappings,
          });

          // check status of external passport if passport has an endpoint
          const endpoint = get(vc, 'credentialStatus.id');
          if (endpoint) {
            await validatePassportStatus({ vcId: vc.id, endpoint, locale });
          }
        }
      }

      // Recreate passport with correct role
      passport = vc ? createUserPassport(vc, { role }) : null;

      // Update profile
      try {
        const profile = claims.find((x) => x.type === 'profile');
        if (user) {
          // Update user
          await node.updateUser({
            teamDid,
            user: {
              ...profile,
              did: userDid,
              pk: userPk,
              locale,
              passports: upsertToPassports(user.passports || [], passport),
              lastLoginAt: new Date().toISOString(),
            },
          });
        } else {
          // Create user
          await node.addUser({
            teamDid,
            user: {
              ...profile,
              did: userDid,
              pk: userPk,
              approved: true,
              locale,
              passports: [passport].filter(Boolean),
              firstLoginAt: new Date().toISOString(),
              lastLoginAt: new Date().toISOString(),
            },
          });
        }

        // Generate new session token that client can save to localStorage
        const loginToken = await login(userDid, { passport, role });
        await storage.update(token, { did: userDid, loginToken });
        logger.info('login.success', { userDid, role });

        // issue passport for the first login user in a invite-only team
        if (issuePassport) {
          return {
            disposition: 'attachment',
            type: 'VerifiableCredential',
            data: vc,
          };
        }
      } catch (err) {
        logger.error('login.error', { error: err, userDid });
        throw new Error(err.message);
      }
    },
  };
};
