const cookies = require('cookie');
const jwksRsa = require('jwks-rsa');
const { logger } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const {
  isEnabled,
  findOpenIDUser,
  getOpenIdEmail,
  getOpenIdIssuer,
  normalizeOpenIdIssuer,
  getHttpsProxyAgent,
  math,
} = require('@librechat/api');
const { updateUser, findUser } = require('~/models');

const getOpenIdJwtAudience = () => {
  const parsedAudience = (process.env.OPENID_AUDIENCE ?? '')
    .split(',')
    .map((value) => value.trim())
    .filter(Boolean);
  const audiences = [process.env.OPENID_CLIENT_ID, ...parsedAudience].filter(Boolean);
  const uniqueAudiences = [...new Set(audiences)];

  return uniqueAudiences.length > 1 ? uniqueAudiences : uniqueAudiences[0];
};

const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const issuerMatchesTemplate = (expectedIssuer, actualIssuer) => {
  if (!expectedIssuer.includes('{tenantid}')) {
    return false;
  }

  const escapedTemplate = expectedIssuer.split('{tenantid}').map(escapeRegExp).join('[^/]+');
  return new RegExp(`^${escapedTemplate}$`).test(actualIssuer);
};

const isOpenIdIssuerAllowed = (payload, openIdConfig) => {
  const actualIssuer = normalizeOpenIdIssuer(payload?.iss);
  const expectedIssuer = normalizeOpenIdIssuer(openIdConfig.serverMetadata().issuer);

  if (!actualIssuer || !expectedIssuer) {
    return false;
  }

  return actualIssuer === expectedIssuer || issuerMatchesTemplate(expectedIssuer, actualIssuer);
};

/**
 * @function openIdJwtLogin
 * @param {import('openid-client').Configuration} openIdConfig - Configuration object for the JWT strategy.
 * @returns {JwtStrategy}
 * @description This function creates a JWT strategy for OpenID authentication.
 * It uses the jwks-rsa library to retrieve the signing key from a JWKS endpoint.
 * The strategy extracts the JWT from the Authorization header as a Bearer token.
 * The JWT is then verified using the signing key, and the user is retrieved from the database.
 *
 * Includes email fallback mechanism:
 * 1. Primary lookup: Search user by openidId (sub claim)
 * 2. Fallback lookup: If not found, search by email claim
 * 3. User migration: If found by email without openidId, migrate the user by adding openidId
 * 4. Provider validation: Ensures users registered with other providers cannot use OpenID
 *
 * This enables seamless migration for existing users when SharePoint integration is enabled.
 */
const openIdJwtLogin = (openIdConfig) => {
  let jwksRsaOptions = {
    cache: process.env.OPENID_JWKS_URL_CACHE_ENABLED
      ? isEnabled(process.env.OPENID_JWKS_URL_CACHE_ENABLED)
      : true,
    cacheMaxAge: math(process.env.OPENID_JWKS_URL_CACHE_TIME, 60000),
    jwksUri: openIdConfig.serverMetadata().jwks_uri,
  };

  const requestAgent = getHttpsProxyAgent(jwksRsaOptions.jwksUri);
  if (requestAgent) {
    jwksRsaOptions.requestAgent = requestAgent;
  }

  return new JwtStrategy(
    {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKeyProvider: jwksRsa.passportJwtSecret(jwksRsaOptions),
      audience: getOpenIdJwtAudience(),
      passReqToCallback: true,
    },
    /**
     * @param {import('@librechat/api').ServerRequest} req
     * @param {import('openid-client').IDToken} payload
     * @param {import('passport-jwt').VerifyCallback} done
     */
    async (req, payload, done) => {
      try {
        if (!isOpenIdIssuerAllowed(payload, openIdConfig)) {
          done(null, false, { message: 'Invalid issuer' });
          return;
        }

        const authHeader = req.headers.authorization;
        const rawToken = authHeader?.replace('Bearer ', '');
        const openidIssuer = getOpenIdIssuer(payload, openIdConfig);

        const { user, error, migration } = await findOpenIDUser({
          findUser,
          email: payload ? getOpenIdEmail(payload) : undefined,
          openidId: payload?.sub,
          openidIssuer,
          idOnTheSource: payload?.oid,
          strategyName: 'openIdJwtLogin',
        });

        if (error) {
          done(null, false, { message: error });
          return;
        }

        if (user) {
          user.id = user._id.toString();

          const updateData = {};
          if (migration) {
            updateData.provider = 'openid';
            updateData.openidId = payload?.sub;
            if (openidIssuer) {
              updateData.openidIssuer = openidIssuer;
            }
          }
          if (!user.role) {
            user.role = SystemRoles.USER;
            updateData.role = user.role;
          }

          if (Object.keys(updateData).length > 0) {
            await updateUser(user.id, updateData);
          }

          /** Read tokens from session (server-side) to avoid large cookie issues */
          const sessionTokens = req.session?.openidTokens;
          let accessToken = sessionTokens?.accessToken;
          let idToken = sessionTokens?.idToken;
          let refreshToken = sessionTokens?.refreshToken;

          /** Fallback to cookies for backward compatibility */
          if (!accessToken || !refreshToken || !idToken) {
            const cookieHeader = req.headers.cookie;
            const parsedCookies = cookieHeader ? cookies.parse(cookieHeader) : {};
            accessToken = accessToken || parsedCookies.openid_access_token;
            idToken = idToken || parsedCookies.openid_id_token;
            refreshToken = refreshToken || parsedCookies.refreshToken;
          }

          user.federatedTokens = {
            access_token: accessToken || rawToken,
            id_token: idToken,
            refresh_token: refreshToken,
            expires_at: payload.exp,
          };

          done(null, user);
        } else {
          logger.warn(
            '[openIdJwtLogin] openId JwtStrategy => no user found with the sub claims: ' +
              payload?.sub +
              (payload?.email ? ' or email: ' + payload.email : ''),
          );
          done(null, false);
        }
      } catch (err) {
        done(err, false);
      }
    },
  );
};

module.exports = openIdJwtLogin;
