const { z } = require('zod');
const fs = require('fs').promises;
const { nanoid } = require('nanoid');
const { logger } = require('@librechat/data-schemas');
const {
  agentCreateSchema,
  agentUpdateSchema,
  refreshListAvatars,
  mergeAgentOcrConversion,
  MAX_AVATAR_REFRESH_AGENTS,
  convertOcrToContextInPlace,
} = require('@librechat/api');
const {
  Tools,
  Constants,
  FileSources,
  ResourceType,
  AccessRoleIds,
  PrincipalType,
  EToolResources,
  PermissionBits,
  actionDelimiter,
  removeNullishValues,
  CacheKeys,
  Time,
} = require('librechat-data-provider');
const {
  getListAgentsByAccess,
  countPromotedAgents,
  revertAgentVersion,
  createAgent,
  updateAgent,
  deleteAgent,
  getAgent,
} = require('~/models/Agent');
const {
  findPubliclyAccessibleResources,
  findAccessibleResources,
  hasPublicPermission,
  grantPermission,
} = require('~/server/services/PermissionService');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { getCategoriesWithCounts, deleteFileByFilter } = require('~/models');
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
const { refreshS3Url } = require('~/server/services/Files/S3/crud');
const { filterFile } = require('~/server/services/Files/process');
const { updateAction, getActions } = require('~/models/Action');
const { getCachedTools } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');

const systemTools = {
  [Tools.execute_code]: true,
  [Tools.file_search]: true,
  [Tools.web_search]: true,
};

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

/**
 * Creates an Agent.
 * @route POST /Agents
 * @param {ServerRequest} req - The request object.
 * @param {AgentCreateParams} req.body - The request body.
 * @param {ServerResponse} res - The response object.
 * @returns {Promise<Agent>} 201 - success response - application/json
 */
const createAgentHandler = async (req, res) => {
  try {
    const validatedData = agentCreateSchema.parse(req.body);
    const { tools = [], ...agentData } = removeNullishValues(validatedData);

    if (agentData.model_parameters && typeof agentData.model_parameters === 'object') {
      agentData.model_parameters = removeNullishValues(agentData.model_parameters, true);
    }

    const { id: userId } = req.user;

    agentData.id = `agent_${nanoid()}`;
    agentData.author = userId;
    agentData.tools = [];

    const availableTools = (await getCachedTools()) ?? {};
    for (const tool of tools) {
      if (availableTools[tool]) {
        agentData.tools.push(tool);
      } else if (systemTools[tool]) {
        agentData.tools.push(tool);
      } else if (tool.includes(Constants.mcp_delimiter)) {
        agentData.tools.push(tool);
      }
    }

    const agent = await createAgent(agentData);

    // Automatically grant owner permissions to the creator
    try {
      await grantPermission({
        principalType: PrincipalType.USER,
        principalId: userId,
        resourceType: ResourceType.AGENT,
        resourceId: agent._id,
        accessRoleId: AccessRoleIds.AGENT_OWNER,
        grantedBy: userId,
      });
      logger.debug(
        `[createAgent] Granted owner permissions to user ${userId} for agent ${agent.id}`,
      );
    } catch (permissionError) {
      logger.error(
        `[createAgent] Failed to grant owner permissions for agent ${agent.id}:`,
        permissionError,
      );
    }

    res.status(201).json(agent);
  } catch (error) {
    if (error instanceof z.ZodError) {
      logger.error('[/Agents] Validation error', error.errors);
      return res.status(400).json({ error: 'Invalid request data', details: error.errors });
    }
    logger.error('[/Agents] Error creating agent', error);
    res.status(500).json({ error: error.message });
  }
};

/**
 * Retrieves an Agent by ID.
 * @route GET /Agents/:id
 * @param {object} req - Express Request
 * @param {object} req.params - Request params
 * @param {string} req.params.id - Agent identifier.
 * @param {object} req.user - Authenticated user information
 * @param {string} req.user.id - User ID
 * @returns {Promise<Agent>} 200 - success response - application/json
 * @returns {Error} 404 - Agent not found
 */
const getAgentHandler = async (req, res, expandProperties = false) => {
  try {
    const id = req.params.id;
    const author = req.user.id;

    // Permissions are validated by middleware before calling this function
    // Simply load the agent by ID
    const agent = await getAgent({ id });

    if (!agent) {
      return res.status(404).json({ error: 'Agent not found' });
    }

    agent.version = agent.versions ? agent.versions.length : 0;

    if (agent.avatar && agent.avatar?.source === FileSources.s3) {
      try {
        agent.avatar = {
          ...agent.avatar,
          filepath: await refreshS3Url(agent.avatar),
        };
      } catch (e) {
        logger.warn('[/Agents/:id] Failed to refresh S3 URL', e);
      }
    }

    agent.author = agent.author.toString();

    // @deprecated - isCollaborative replaced by ACL permissions
    agent.isCollaborative = !!agent.isCollaborative;

    // Check if agent is public
    const isPublic = await hasPublicPermission({
      resourceType: ResourceType.AGENT,
      resourceId: agent._id,
      requiredPermissions: PermissionBits.VIEW,
    });
    agent.isPublic = isPublic;

    if (agent.author !== author) {
      delete agent.author;
    }

    if (!expandProperties) {
      // VIEW permission: Basic agent info only
      return res.status(200).json({
        _id: agent._id,
        id: agent.id,
        name: agent.name,
        description: agent.description,
        avatar: agent.avatar,
        author: agent.author,
        provider: agent.provider,
        model: agent.model,
        projectIds: agent.projectIds,
        // @deprecated - isCollaborative replaced by ACL permissions
        isCollaborative: agent.isCollaborative,
        isPublic: agent.isPublic,
        version: agent.version,
        // Safe metadata
        createdAt: agent.createdAt,
        updatedAt: agent.updatedAt,
      });
    }

    // EDIT permission: Full agent details including sensitive configuration
    return res.status(200).json(agent);
  } catch (error) {
    logger.error('[/Agents/:id] Error retrieving agent', error);
    res.status(500).json({ error: error.message });
  }
};

/**
 * Updates an Agent.
 * @route PATCH /Agents/:id
 * @param {object} req - Express Request
 * @param {object} req.params - Request params
 * @param {string} req.params.id - Agent identifier.
 * @param {AgentUpdateParams} req.body - The Agent update parameters.
 * @returns {Promise<Agent>} 200 - success response - application/json
 */
const updateAgentHandler = async (req, res) => {
  try {
    const id = req.params.id;
    const validatedData = agentUpdateSchema.parse(req.body);
    // Preserve explicit null for avatar to allow resetting the avatar
    const { avatar: avatarField, _id, ...rest } = validatedData;
    const updateData = removeNullishValues(rest);

    if (updateData.model_parameters && typeof updateData.model_parameters === 'object') {
      updateData.model_parameters = removeNullishValues(updateData.model_parameters, true);
    }

    if (avatarField === null) {
      updateData.avatar = avatarField;
    }

    // Convert OCR to context in incoming updateData
    convertOcrToContextInPlace(updateData);

    const existingAgent = await getAgent({ id });

    if (!existingAgent) {
      return res.status(404).json({ error: 'Agent not found' });
    }

    // Convert legacy OCR tool resource to context format in existing agent
    const ocrConversion = mergeAgentOcrConversion(existingAgent, updateData);
    if (ocrConversion.tool_resources) {
      updateData.tool_resources = ocrConversion.tool_resources;
    }
    if (ocrConversion.tools) {
      updateData.tools = ocrConversion.tools;
    }

    let updatedAgent =
      Object.keys(updateData).length > 0
        ? await updateAgent({ id }, updateData, {
            updatingUserId: req.user.id,
          })
        : existingAgent;

    // Add version count to the response
    updatedAgent.version = updatedAgent.versions ? updatedAgent.versions.length : 0;

    if (updatedAgent.author) {
      updatedAgent.author = updatedAgent.author.toString();
    }

    if (updatedAgent.author !== req.user.id) {
      delete updatedAgent.author;
    }

    return res.json(updatedAgent);
  } catch (error) {
    if (error instanceof z.ZodError) {
      logger.error('[/Agents/:id] Validation error', error.errors);
      return res.status(400).json({ error: 'Invalid request data', details: error.errors });
    }

    logger.error('[/Agents/:id] Error updating Agent', error);

    if (error.statusCode === 409) {
      return res.status(409).json({
        error: error.message,
        details: error.details,
      });
    }

    res.status(500).json({ error: error.message });
  }
};

/**
 * Duplicates an Agent based on the provided ID.
 * @route POST /Agents/:id/duplicate
 * @param {object} req - Express Request
 * @param {object} req.params - Request params
 * @param {string} req.params.id - Agent identifier.
 * @returns {Promise<Agent>} 201 - success response - application/json
 */
const duplicateAgentHandler = async (req, res) => {
  const { id } = req.params;
  const { id: userId } = req.user;
  const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];

  try {
    const agent = await getAgent({ id });
    if (!agent) {
      return res.status(404).json({
        error: 'Agent not found',
        status: 'error',
      });
    }

    const {
      id: _id,
      _id: __id,
      author: _author,
      createdAt: _createdAt,
      updatedAt: _updatedAt,
      tool_resources: _tool_resources = {},
      versions: _versions,
      __v: _v,
      ...cloneData
    } = agent;
    cloneData.name = `${agent.name} (${new Date().toLocaleString('en-US', {
      dateStyle: 'short',
      timeStyle: 'short',
      hour12: false,
    })})`;

    if (_tool_resources?.[EToolResources.context]) {
      cloneData.tool_resources = {
        [EToolResources.context]: _tool_resources[EToolResources.context],
      };
    }

    if (_tool_resources?.[EToolResources.ocr]) {
      cloneData.tool_resources = {
        /** Legacy conversion from `ocr` to `context` */
        [EToolResources.context]: {
          ...(_tool_resources[EToolResources.context] ?? {}),
          ..._tool_resources[EToolResources.ocr],
        },
      };
    }

    const newAgentId = `agent_${nanoid()}`;
    const newAgentData = Object.assign(cloneData, {
      id: newAgentId,
      author: userId,
    });

    const newActionsList = [];
    const originalActions = (await getActions({ agent_id: id }, true)) ?? [];
    const promises = [];

    /**
     * Duplicates an action and returns the new action ID.
     * @param {Action} action
     * @returns {Promise<string>}
     */
    const duplicateAction = async (action) => {
      const newActionId = nanoid();
      const [domain] = action.action_id.split(actionDelimiter);
      const fullActionId = `${domain}${actionDelimiter}${newActionId}`;

      // Sanitize sensitive metadata before persisting
      const filteredMetadata = { ...(action.metadata || {}) };
      for (const field of sensitiveFields) {
        delete filteredMetadata[field];
      }

      const newAction = await updateAction(
        { action_id: newActionId },
        {
          metadata: filteredMetadata,
          agent_id: newAgentId,
          user: userId,
        },
      );

      newActionsList.push(newAction);
      return fullActionId;
    };

    for (const action of originalActions) {
      promises.push(
        duplicateAction(action).catch((error) => {
          logger.error('[/agents/:id/duplicate] Error duplicating Action:', error);
        }),
      );
    }

    const agentActions = await Promise.all(promises);
    newAgentData.actions = agentActions;
    const newAgent = await createAgent(newAgentData);

    // Automatically grant owner permissions to the duplicator
    try {
      await grantPermission({
        principalType: PrincipalType.USER,
        principalId: userId,
        resourceType: ResourceType.AGENT,
        resourceId: newAgent._id,
        accessRoleId: AccessRoleIds.AGENT_OWNER,
        grantedBy: userId,
      });
      logger.debug(
        `[duplicateAgent] Granted owner permissions to user ${userId} for duplicated agent ${newAgent.id}`,
      );
    } catch (permissionError) {
      logger.error(
        `[duplicateAgent] Failed to grant owner permissions for duplicated agent ${newAgent.id}:`,
        permissionError,
      );
    }

    return res.status(201).json({
      agent: newAgent,
      actions: newActionsList,
    });
  } catch (error) {
    logger.error('[/Agents/:id/duplicate] Error duplicating Agent:', error);

    res.status(500).json({ error: error.message });
  }
};

/**
 * Deletes an Agent based on the provided ID.
 * @route DELETE /Agents/:id
 * @param {object} req - Express Request
 * @param {object} req.params - Request params
 * @param {string} req.params.id - Agent identifier.
 * @returns {Promise<Agent>} 200 - success response - application/json
 */
const deleteAgentHandler = async (req, res) => {
  try {
    const id = req.params.id;
    const agent = await getAgent({ id });
    if (!agent) {
      return res.status(404).json({ error: 'Agent not found' });
    }
    await deleteAgent({ id });
    return res.json({ message: 'Agent deleted' });
  } catch (error) {
    logger.error('[/Agents/:id] Error deleting Agent', error);
    res.status(500).json({ error: error.message });
  }
};

/**
 * Lists agents using ACL-aware permissions (ownership + explicit shares).
 * @route GET /Agents
 * @param {object} req - Express Request
 * @param {object} req.query - Request query
 * @param {string} [req.query.user] - The user ID of the agent's author.
 * @returns {Promise<AgentListResponse>} 200 - success response - application/json
 */
const getListAgentsHandler = async (req, res) => {
  try {
    const userId = req.user.id;
    const { category, search, limit, cursor, promoted } = req.query;
    let requiredPermission = req.query.requiredPermission;
    if (typeof requiredPermission === 'string') {
      requiredPermission = parseInt(requiredPermission, 10);
      if (isNaN(requiredPermission)) {
        requiredPermission = PermissionBits.VIEW;
      }
    } else if (typeof requiredPermission !== 'number') {
      requiredPermission = PermissionBits.VIEW;
    }
    // Base filter
    const filter = {};

    // Handle category filter - only apply if category is defined
    if (category !== undefined && category.trim() !== '') {
      filter.category = category;
    }

    // Handle promoted filter - only from query param
    if (promoted === '1') {
      filter.is_promoted = true;
    } else if (promoted === '0') {
      filter.is_promoted = { $ne: true };
    }

    // Handle search filter (escape regex and cap length)
    if (search && search.trim() !== '') {
      const safeSearch = escapeRegex(search.trim().slice(0, MAX_SEARCH_LEN));
      const regex = new RegExp(safeSearch, 'i');
      filter.$or = [{ name: regex }, { description: regex }];
    }

    // Get agent IDs the user has VIEW access to via ACL
    const accessibleIds = await findAccessibleResources({
      userId,
      role: req.user.role,
      resourceType: ResourceType.AGENT,
      requiredPermissions: requiredPermission,
    });

    const publiclyAccessibleIds = await findPubliclyAccessibleResources({
      resourceType: ResourceType.AGENT,
      requiredPermissions: PermissionBits.VIEW,
    });

    /**
     * Refresh all S3 avatars for this user's accessible agent set (not only the current page)
     * This addresses page-size limits preventing refresh of agents beyond the first page
     */
    const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL);
    const refreshKey = `${userId}:agents_avatar_refresh`;
    const alreadyChecked = await cache.get(refreshKey);
    if (alreadyChecked) {
      logger.debug('[/Agents] S3 avatar refresh already checked, skipping');
    } else {
      try {
        const fullList = await getListAgentsByAccess({
          accessibleIds,
          otherParams: {},
          limit: MAX_AVATAR_REFRESH_AGENTS,
          after: null,
        });
        await refreshListAvatars({
          agents: fullList?.data ?? [],
          userId,
          refreshS3Url,
          updateAgent,
        });
        await cache.set(refreshKey, true, Time.THIRTY_MINUTES);
      } catch (err) {
        logger.error('[/Agents] Error refreshing avatars for full list: %o', err);
      }
    }

    // Use the new ACL-aware function
    const data = await getListAgentsByAccess({
      accessibleIds,
      otherParams: filter,
      limit,
      after: cursor,
    });

    const agents = data?.data ?? [];
    if (!agents.length) {
      return res.json(data);
    }

    const publicSet = new Set(publiclyAccessibleIds.map((oid) => oid.toString()));

    data.data = agents.map((agent) => {
      try {
        if (agent?._id && publicSet.has(agent._id.toString())) {
          agent.isPublic = true;
        }
      } catch (e) {
        // Silently ignore mapping errors
        void e;
      }
      return agent;
    });

    return res.json(data);
  } catch (error) {
    logger.error('[/Agents] Error listing Agents: %o', error);
    res.status(500).json({ error: error.message });
  }
};

/**
 * Uploads and updates an avatar for a specific agent.
 * @route POST /:agent_id/avatar
 * @param {object} req - Express Request
 * @param {object} req.params - Request params
 * @param {string} req.params.agent_id - The ID of the agent.
 * @param {Express.Multer.File} req.file - The avatar image file.
 * @param {object} req.body - Request body
 * @param {string} [req.body.avatar] - Optional avatar for the agent's avatar.
 * @returns {Promise<void>} 200 - success response - application/json
 */
const uploadAgentAvatarHandler = async (req, res) => {
  try {
    const appConfig = req.config;
    if (!req.file) {
      return res.status(400).json({ message: 'No file uploaded' });
    }
    filterFile({ req, file: req.file, image: true, isAvatar: true });
    const { agent_id } = req.params;
    if (!agent_id) {
      return res.status(400).json({ message: 'Agent ID is required' });
    }

    const existingAgent = await getAgent({ id: agent_id });

    if (!existingAgent) {
      return res.status(404).json({ error: 'Agent not found' });
    }

    const buffer = await fs.readFile(req.file.path);
    const fileStrategy = getFileStrategy(appConfig, { isAvatar: true });
    const resizedBuffer = await resizeAvatar({
      userId: req.user.id,
      input: buffer,
    });

    const { processAvatar } = getStrategyFunctions(fileStrategy);
    const avatarUrl = await processAvatar({
      buffer: resizedBuffer,
      userId: req.user.id,
      manual: 'false',
      agentId: agent_id,
    });

    const image = {
      filepath: avatarUrl,
      source: fileStrategy,
    };

    let _avatar = existingAgent.avatar;

    if (_avatar && _avatar.source) {
      const { deleteFile } = getStrategyFunctions(_avatar.source);
      try {
        await deleteFile(req, { filepath: _avatar.filepath });
        await deleteFileByFilter({ user: req.user.id, filepath: _avatar.filepath });
      } catch (error) {
        logger.error('[/:agent_id/avatar] Error deleting old avatar', error);
      }
    }

    const data = {
      avatar: {
        filepath: image.filepath,
        source: image.source,
      },
    };

    const updatedAgent = await updateAgent({ id: agent_id }, data, {
      updatingUserId: req.user.id,
    });
    res.status(201).json(updatedAgent);
  } catch (error) {
    const message = 'An error occurred while updating the Agent Avatar';
    logger.error(
      `[/:agent_id/avatar] ${message} (${req.params?.agent_id ?? 'unknown agent'})`,
      error,
    );
    res.status(500).json({ message });
  } finally {
    try {
      await fs.unlink(req.file.path);
      logger.debug('[/:agent_id/avatar] Temp. image upload file deleted');
    } catch {
      logger.debug('[/:agent_id/avatar] Temp. image upload file already deleted');
    }
  }
};

/**
 * Reverts an agent to a previous version from its version history.
 * @route PATCH /agents/:id/revert
 * @param {object} req - Express Request object
 * @param {object} req.params - Request parameters
 * @param {string} req.params.id - The ID of the agent to revert
 * @param {object} req.body - Request body
 * @param {number} req.body.version_index - The index of the version to revert to
 * @param {object} req.user - Authenticated user information
 * @param {string} req.user.id - User ID
 * @param {string} req.user.role - User role
 * @param {ServerResponse} res - Express Response object
 * @returns {Promise<Agent>} 200 - The updated agent after reverting to the specified version
 * @throws {Error} 400 - If version_index is missing
 * @throws {Error} 403 - If user doesn't have permission to modify the agent
 * @throws {Error} 404 - If agent not found
 * @throws {Error} 500 - If there's an internal server error during the reversion process
 */
const revertAgentVersionHandler = async (req, res) => {
  try {
    const { id } = req.params;
    const { version_index } = req.body;

    if (version_index === undefined) {
      return res.status(400).json({ error: 'version_index is required' });
    }

    const existingAgent = await getAgent({ id });

    if (!existingAgent) {
      return res.status(404).json({ error: 'Agent not found' });
    }

    // Permissions are enforced via route middleware (ACL EDIT)

    const updatedAgent = await revertAgentVersion({ id }, version_index);

    if (updatedAgent.author) {
      updatedAgent.author = updatedAgent.author.toString();
    }

    if (updatedAgent.author !== req.user.id) {
      delete updatedAgent.author;
    }

    return res.json(updatedAgent);
  } catch (error) {
    logger.error('[/agents/:id/revert] Error reverting Agent version', error);
    res.status(500).json({ error: error.message });
  }
};
/**
 * Get all agent categories with counts
 *
 * @param {Object} _req - Express request object (unused)
 * @param {Object} res - Express response object
 */
const getAgentCategories = async (_req, res) => {
  try {
    const categories = await getCategoriesWithCounts();
    const promotedCount = await countPromotedAgents();
    const formattedCategories = categories.map((category) => ({
      value: category.value,
      label: category.label,
      count: category.agentCount,
      description: category.description,
    }));

    if (promotedCount > 0) {
      formattedCategories.unshift({
        value: 'promoted',
        label: 'Promoted',
        count: promotedCount,
        description: 'Our recommended agents',
      });
    }

    formattedCategories.push({
      value: 'all',
      label: 'All',
      description: 'All available agents',
    });

    res.status(200).json(formattedCategories);
  } catch (error) {
    logger.error('[/Agents/Marketplace] Error fetching agent categories:', error);
    res.status(500).json({
      error: 'Failed to fetch agent categories',
      userMessage: 'Unable to load categories. Please refresh the page.',
      suggestion: 'Try refreshing the page or check your network connection',
    });
  }
};
module.exports = {
  createAgent: createAgentHandler,
  getAgent: getAgentHandler,
  updateAgent: updateAgentHandler,
  duplicateAgent: duplicateAgentHandler,
  deleteAgent: deleteAgentHandler,
  getListAgents: getListAgentsHandler,
  uploadAgentAvatar: uploadAgentAvatarHandler,
  revertAgentVersion: revertAgentVersionHandler,
  getAgentCategories,
};
