const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const openidClient = require('openid-client');
const { getRemoteAddress } = require('../../../utils/remoteAddress');

const Errors = {
  INVALID_TOKEN: {
    invalidToken: 'Access Token is invalid',
  },
  MISSING_VALUES: {
    missingValues:
      'Unable to retrieve required values. Verify the access token or UserInfo endpoint has email, username and name claims',
  },
};

const jwks = jwksClient({
  jwksUri: sails.config.custom.oidcJwksUri,
  requestHeaders: {}, // Optional
  timeout: 30000, // Defaults to 30s
});

const getJwtVerificationOptions = () => {
  const options = {};
  if (sails.config.custom.oidcIssuer) {
    options.issuer = sails.config.custom.oidcIssuer;
  }
  if (sails.config.custom.oidcAudience) {
    options.audience = sails.config.custom.oidcAudience;
  }
  return options;
};

const validateAndDecodeToken = async (accessToken, options) => {
  const keys = await jwks.getSigningKeys();
  let validToken = {};

  const isTokenValid = keys.some((signingKey) => {
    try {
      const key = signingKey.getPublicKey();
      validToken = jwt.verify(accessToken, key, options);
      return 'true';
    } catch (error) {
      sails.log.error(error);
    }
    return false;
  });

  if (!isTokenValid) {
    const tokenForLogging = jwt.decode(accessToken);
    const remoteAddress = getRemoteAddress(this.req);

    sails.log.warn(
      `invalid token: sub: "${tokenForLogging.sub}" issuer: "${tokenForLogging.iss}" audience: "${tokenForLogging.aud}" exp: ${tokenForLogging.exp} (IP: ${remoteAddress})`,
    );
    throw Errors.INVALID_TOKEN;
  }
  return validToken;
};

const getUserInfo = async (accessToken, options) => {
  if (sails.config.custom.oidcSkipUserInfo) {
    return {};
  }
  const issuer = await openidClient.Issuer.discover(options.issuer);
  const oidcClient = new issuer.Client({
    client_id: 'irrelevant',
  });
  const userInfo = await oidcClient.userinfo(accessToken);
  return userInfo;
};
const mergeUserData = (validToken, userInfo) => {
  const oidcUser = { ...validToken, ...userInfo };
  return oidcUser;
};
const getOrCreateUser = async (newUser) => {
  const user = await User.findOne({
    where: {
      username: newUser.username,
    },
  });
  if (user) {
    return user;
  }
  return User.create(newUser).fetch();
};
module.exports = {
  inputs: {
    token: {
      type: 'string',
      required: true,
    },
  },

  exits: {
    invalidToken: {
      responseType: 'unauthorized',
    },
    missingValues: {
      responseType: 'unauthorized',
    },
  },

  async fn(inputs) {
    const options = getJwtVerificationOptions();
    const validToken = await validateAndDecodeToken(inputs.token, options);
    const userInfo = await getUserInfo(inputs.token, options);
    const oidcUser = mergeUserData(validToken, userInfo);

    const now = new Date();
    let isAdmin = false;
    if (sails.config.custom.oidcAdminRoles.includes('*')) isAdmin = true;
    else if (Array.isArray(oidcUser[sails.config.custom.oidcRolesAttribute])) {
      const userRoles = new Set(oidcUser[sails.config.custom.oidcRolesAttribute]);
      isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
    }

    const newUser = {
      email: oidcUser.email,
      isAdmin,
      name: oidcUser.name,
      username: oidcUser.preferred_username,
      subscribeToOwnCards: false,
      createdAt: now,
      updatedAt: now,
      locked: true,
    };

    if (!newUser.email || !newUser.username || !newUser.name) {
      sails.log.error(Errors.MISSING_VALUES.missingValues);
      throw Errors.MISSING_VALUES;
    }

    const identityProviderUser = await IdentityProviderUser.findOne({
      where: {
        issuer: oidcUser.iss,
        sub: oidcUser.sub,
      },
    }).populate('userId');

    let user = identityProviderUser ? identityProviderUser.userId : {};
    if (!identityProviderUser) {
      user = await getOrCreateUser(newUser);
      await IdentityProviderUser.create({
        issuer: oidcUser.iss,
        sub: oidcUser.sub,
        userId: user.id,
      });
    }

    const controlledFields = ['email', 'password', 'isAdmin', 'name', 'username'];
    const updateFields = {};
    controlledFields.forEach((field) => {
      if (user[field] !== newUser[field]) {
        updateFields[field] = newUser[field];
      }
    });

    if (Object.keys(updateFields).length > 0) {
      updateFields.updatedAt = now;
      await User.updateOne({ id: user.id }).set(updateFields);
    }

    const plankaToken = sails.helpers.utils.createToken(user.id);

    const remoteAddress = getRemoteAddress(this.req);
    await Session.create({
      accessToken: plankaToken,
      remoteAddress,
      userId: user.id,
      userAgent: this.req.headers['user-agent'],
    });

    return {
      item: plankaToken,
    };
  },
};
