const { logger } = require('@librechat/data-schemas');
const { encrypt, decrypt } = require('@librechat/api');
const { ErrorTypes } = require('librechat-data-provider');
const { updateUser } = require('~/models');
const { Key } = require('~/db/models');

/**
 * Updates the plugins for a user based on the action specified (install/uninstall).
 * @async
 * @param {Object} user - The user whose plugins are to be updated.
 * @param {string} pluginKey - The key of the plugin to install or uninstall.
 * @param {'install' | 'uninstall'} action - The action to perform, 'install' or 'uninstall'.
 * @returns {Promise<Object>} The result of the update operation.
 * @throws Logs the error internally if the update operation fails.
 * @description This function updates the plugin array of a user document based on the specified action.
 *              It adds a plugin key to the plugins array for an 'install' action, and removes it for an 'uninstall' action.
 */
const updateUserPluginsService = async (user, pluginKey, action) => {
  try {
    const userPlugins = user.plugins || [];
    if (action === 'install') {
      return await updateUser(user._id, { plugins: [...userPlugins, pluginKey] });
    } else if (action === 'uninstall') {
      return await updateUser(user._id, {
        plugins: userPlugins.filter((plugin) => plugin !== pluginKey),
      });
    }
  } catch (err) {
    logger.error('[updateUserPluginsService]', err);
    return err;
  }
};

/**
 * Retrieves and decrypts the key value for a given user identified by userId and identifier name.
 * @param {Object} params - The parameters object.
 * @param {string} params.userId - The unique identifier for the user.
 * @param {string} params.name - The name associated with the key.
 * @returns {Promise<string>} The decrypted key value.
 * @throws {Error} Throws an error if the key is not found or if there is a problem during key retrieval.
 * @description This function searches for a user's key in the database using their userId and name.
 *              If found, it decrypts the value of the key and returns it. If no key is found, it throws
 *              an error indicating that there is no user key available.
 */
const getUserKey = async ({ userId, name }) => {
  const keyValue = await Key.findOne({ userId, name }).lean();
  if (!keyValue) {
    throw new Error(
      JSON.stringify({
        type: ErrorTypes.NO_USER_KEY,
      }),
    );
  }
  return await decrypt(keyValue.value);
};

/**
 * Retrieves, decrypts, and parses the key values for a given user identified by userId and name.
 * @param {Object} params - The parameters object.
 * @param {string} params.userId - The unique identifier for the user.
 * @param {string} params.name - The name associated with the key.
 * @returns {Promise<Record<string,string>>} The decrypted and parsed key values.
 * @throws {Error} Throws an error if the key is invalid or if there is a problem during key value parsing.
 * @description This function retrieves a user's encrypted key using their userId and name, decrypts it,
 *              and then attempts to parse the decrypted string into a JSON object. If the parsing fails,
 *              it throws an error indicating that the user key is invalid.
 */
const getUserKeyValues = async ({ userId, name }) => {
  let userValues = await getUserKey({ userId, name });
  try {
    userValues = JSON.parse(userValues);
  } catch (e) {
    logger.error('[getUserKeyValues]', e);
    throw new Error(
      JSON.stringify({
        type: ErrorTypes.INVALID_USER_KEY,
      }),
    );
  }
  return userValues;
};

/**
 * Retrieves the expiry information of a user's key identified by userId and name.
 * @async
 * @param {Object} params - The parameters object.
 * @param {string} params.userId - The unique identifier for the user.
 * @param {string} params.name - The name associated with the key.
 * @returns {Promise<{expiresAt: Date | null}>} The expiry date of the key or null if the key doesn't exist.
 * @description This function fetches a user's key from the database using their userId and name and
 *              returns its expiry date. If the key is not found, it returns null for the expiry date.
 */
const getUserKeyExpiry = async ({ userId, name }) => {
  const keyValue = await Key.findOne({ userId, name }).lean();
  if (!keyValue) {
    return { expiresAt: null };
  }
  return { expiresAt: keyValue.expiresAt || 'never' };
};

/**
 * Updates or inserts a new key for a given user identified by userId and name, with a specified value and expiry date.
 * @async
 * @param {Object} params - The parameters object.
 * @param {string} params.userId - The unique identifier for the user.
 * @param {string} params.name - The name associated with the key.
 * @param {string} params.value - The value to be encrypted and stored as the key's value.
 * @param {Date} params.expiresAt - The expiry date for the key [optional]
 * @returns {Promise<Object>} The updated or newly inserted key document.
 * @description This function either updates an existing user key or inserts a new one into the database,
 *              after encrypting the provided value. It sets the provided expiry date for the key (or unsets for no expiry).
 */
const updateUserKey = async ({ userId, name, value, expiresAt = null }) => {
  const encryptedValue = await encrypt(value);
  let updateObject = {
    userId,
    name,
    value: encryptedValue,
  };
  const updateQuery = { $set: updateObject };
  // add expiresAt to the update object if it's not null
  if (expiresAt) {
    updateObject.expiresAt = new Date(expiresAt);
  } else {
    // make sure to remove if already present
    updateQuery.$unset = { expiresAt };
  }
  return await Key.findOneAndUpdate({ userId, name }, updateQuery, {
    upsert: true,
    new: true,
  }).lean();
};

/**
 * Deletes a key or all keys for a given user identified by userId, optionally based on a specified name.
 * @async
 * @param {Object} params - The parameters object.
 * @param {string} params.userId - The unique identifier for the user.
 * @param {string} [params.name] - The name associated with the key to delete. If not provided and all is true, deletes all keys.
 * @param {boolean} [params.all=false] - Whether to delete all keys for the user.
 * @returns {Promise<Object>} The result of the deletion operation.
 * @description This function deletes a specific key or all keys for a user from the database.
 *              If a name is provided and all is false, it deletes only the key with that name.
 *              If all is true, it ignores the name and deletes all keys for the user.
 */
const deleteUserKey = async ({ userId, name, all = false }) => {
  if (all) {
    return await Key.deleteMany({ userId });
  }

  await Key.findOneAndDelete({ userId, name }).lean();
};

/**
 * Checks if a user key has expired based on the provided expiration date and endpoint.
 * If the key has expired, it throws an Error with details including the type of error, the expiration date, and the endpoint.
 *
 * @param {string} expiresAt - The expiration date of the user key in a format that can be parsed by the Date constructor.
 * @param {string} endpoint - The endpoint associated with the user key to be checked.
 * @throws {Error} Throws an error if the user key has expired. The error message is a stringified JSON object
 * containing the type of error (`ErrorTypes.EXPIRED_USER_KEY`), the expiration date in the local string format, and the endpoint.
 */
const checkUserKeyExpiry = (expiresAt, endpoint) => {
  const expiresAtDate = new Date(expiresAt);
  if (expiresAtDate < new Date()) {
    const errorMessage = JSON.stringify({
      type: ErrorTypes.EXPIRED_USER_KEY,
      expiredAt: expiresAtDate.toLocaleString(),
      endpoint,
    });
    throw new Error(errorMessage);
  }
};

module.exports = {
  getUserKey,
  updateUserKey,
  deleteUserKey,
  getUserKeyValues,
  getUserKeyExpiry,
  checkUserKeyExpiry,
  updateUserPluginsService,
};
