module.exports = {
  inputs: {
    code: {
      type: 'string',
      required: true,
    },
    nonce: {
      type: 'string',
      required: true,
    },
  },

  exits: {
    invalidCodeOrNonce: {},
    invalidUserinfoConfiguration: {},
    missingValues: {},
    emailAlreadyInUse: {},
    usernameAlreadyInUse: {},
  },

  async fn(inputs) {
    const client = sails.hooks.oidc.getClient();

    let tokenSet;
    try {
      tokenSet = await client.callback(
        sails.config.custom.oidcRedirectUri,
        {
          iss: sails.config.custom.oidcIssuer,
          code: inputs.code,
        },
        {
          nonce: inputs.nonce,
        },
      );
    } catch (error) {
      sails.log.warn(`Error while exchanging OIDC code: ${error}`);
      throw 'invalidCodeOrNonce';
    }

    let claims;
    if (sails.config.custom.oidcClaimsSource === 'id_token') {
      claims = tokenSet.claims();
    } else {
      try {
        claims = await client.userinfo(tokenSet);
      } catch (error) {
        let errorText;
        if (
          error instanceof SyntaxError &&
          error.message.includes('Unexpected token e in JSON at position 0')
        ) {
          errorText = 'response is signed';
        } else {
          errorText = error.toString();
        }

        sails.log.warn(`Error while fetching OIDC userinfo: ${errorText}`);
        throw 'invalidUserinfoConfiguration';
      }
    }

    if (
      !claims[sails.config.custom.oidcEmailAttribute] ||
      !claims[sails.config.custom.oidcNameAttribute]
    ) {
      throw 'missingValues';
    }

    let isAdmin = false;
    if (sails.config.custom.oidcAdminRoles.includes('*')) {
      isAdmin = true;
    } else {
      const roles = claims[sails.config.custom.oidcRolesAttribute];
      if (Array.isArray(roles)) {
        // Use a Set here to avoid quadratic time complexity
        const userRoles = new Set(claims[sails.config.custom.oidcRolesAttribute]);
        isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
      }
    }

    const values = {
      isAdmin,
      email: claims[sails.config.custom.oidcEmailAttribute],
      isSso: true,
      name: claims[sails.config.custom.oidcNameAttribute],
      subscribeToOwnCards: false,
    };
    if (!sails.config.custom.oidcIgnoreUsername) {
      values.username = claims[sails.config.custom.oidcUsernameAttribute];
    }

    let user;
    // This whole block technically needs to be executed in a transaction
    // with SERIALIZABLE isolation level (but Waterline does not support
    // that), so this will result in errors if for example users are deleted
    // concurrently with logging in via OIDC.
    let identityProviderUser = await IdentityProviderUser.findOne({
      issuer: sails.config.custom.oidcIssuer,
      sub: claims.sub,
    });

    if (identityProviderUser) {
      user = await sails.helpers.users.getOne(identityProviderUser.userId);
    } else {
      // If no IDP/User mapping exists, search for the user by email.
      user = await sails.helpers.users.getOne({
        email: values.email.toLowerCase(),
      });

      // Otherwise, create a new user.
      if (!user) {
        user = await sails.helpers.users.createOne
          .with({
            values,
            actorUser: User.OIDC,
          })
          .intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
      }

      identityProviderUser = await IdentityProviderUser.create({
        userId: user.id,
        issuer: sails.config.custom.oidcIssuer,
        sub: claims.sub,
      });
    }

    const updateFieldKeys = ['email', 'isSso', 'name'];
    if (!sails.config.custom.oidcIgnoreUsername) {
      updateFieldKeys.push('username');
    }
    if (!sails.config.custom.oidcIgnoreRoles) {
      updateFieldKeys.push('isAdmin');
    }

    const updateValues = {};
    // eslint-disable-next-line no-restricted-syntax
    for (const k of updateFieldKeys) {
      if (values[k] !== user[k]) updateValues[k] = values[k];
    }

    if (Object.keys(updateValues).length > 0) {
      user = await sails.helpers.users.updateOne
        .with({
          record: user,
          values: updateValues,
          actorUser: User.OIDC,
        })
        .intercept('emailAlreadyInUse', 'emailAlreadyInUse')
        .intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
    }

    return user;
  },
};
