import express from 'express';
import { CompositeKey, EntityDeleteOptions, LogError, Metadata } from '@memberjunction/core';
import { EntityCRUDHandler } from './EntityCRUDHandler.js';
import { ViewOperationsHandler } from './ViewOperationsHandler.js';
/**
 * RESTEndpointHandler provides REST API functionality for MemberJunction entities
 * This class handles request routing and processing for a /rest endpoint that exposes
 * entity operations via REST instead of GraphQL
 */
export class RESTEndpointHandler {
    constructor(options = {}) {
        this.router = express.Router();
        this.options = options;
        this.setupRoutes();
    }
    /**
     * Helper to safely extract a string from Express route params
     * Express 5.x types params as string | string[] | undefined
     */
    getStringParam(param) {
        if (Array.isArray(param)) {
            return param[0] || '';
        }
        return param || '';
    }
    /**
     * Determines if an entity is allowed based on include/exclude lists
     * with support for wildcards and schema-level filtering
     * @param entityName The name of the entity to check
     * @returns True if the entity is allowed, false otherwise
     */
    isEntityAllowed(entityName) {
        const name = entityName.toLowerCase();
        const md = new Metadata();
        const entity = md.Entities.find(e => e.Name.toLowerCase() === name);
        // If entity not found in metadata, don't allow it
        if (!entity) {
            return false;
        }
        const schemaName = entity.SchemaName.toLowerCase();
        // 1. Check schema exclusions first (these take highest precedence)
        if (this.options.excludeSchemas && this.options.excludeSchemas.length > 0) {
            if (this.options.excludeSchemas.some(schema => schema.toLowerCase() === schemaName)) {
                return false;
            }
        }
        // 2. Check entity exclusions next (these override entity inclusions)
        if (this.options.excludeEntities && this.options.excludeEntities.length > 0) {
            // Check for direct match
            if (this.options.excludeEntities.includes(name)) {
                return false;
            }
            // Check for wildcard matches
            for (const pattern of this.options.excludeEntities) {
                if (pattern.includes('*')) {
                    const regex = new RegExp('^' + pattern.toLowerCase().replace(/\*/g, '.*') + '$');
                    if (regex.test(name)) {
                        return false;
                    }
                }
            }
        }
        // 3. Check schema inclusions (if specified, only entities from these schemas are allowed)
        if (this.options.includeSchemas && this.options.includeSchemas.length > 0) {
            if (!this.options.includeSchemas.some(schema => schema.toLowerCase() === schemaName)) {
                return false;
            }
        }
        // 4. Check entity inclusions
        if (this.options.includeEntities && this.options.includeEntities.length > 0) {
            // Check for direct match
            if (this.options.includeEntities.includes(name)) {
                return true;
            }
            // Check for wildcard matches
            for (const pattern of this.options.includeEntities) {
                if (pattern.includes('*')) {
                    const regex = new RegExp('^' + pattern.toLowerCase().replace(/\*/g, '.*') + '$');
                    if (regex.test(name)) {
                        return true;
                    }
                }
            }
            // If include list is specified but no matches found, entity is not allowed
            return false;
        }
        // By default, allow all entities
        return true;
    }
    /**
     * Set up all the API routes for the REST endpoints
     */
    setupRoutes() {
        // Middleware to verify MJ user was set by upstream auth
        this.router.use(this.extractMJUser.bind(this));
        // Middleware to check entity allowlist/blocklist
        this.router.use('/entities/:entityName', this.checkEntityAccess.bind(this));
        this.router.use('/views/:entityName', this.checkEntityAccess.bind(this));
        this.router.use('/metadata/entities/:entityName', this.checkEntityAccess.bind(this));
        this.router.use('/users/:userId/favorites/:entityName', this.checkEntityAccess.bind(this));
        // Entity collection operations
        this.router.get('/entities/:entityName', this.getEntityList.bind(this));
        this.router.post('/entities/:entityName', this.createEntity.bind(this));
        // Individual entity operations
        this.router.get('/entities/:entityName/:id', this.getEntity.bind(this));
        this.router.put('/entities/:entityName/:id', this.updateEntity.bind(this));
        this.router.delete('/entities/:entityName/:id', this.deleteEntity.bind(this));
        // Record changes and dependencies
        this.router.get('/entities/:entityName/:id/changes', this.getRecordChanges.bind(this));
        this.router.get('/entities/:entityName/:id/dependencies', this.getRecordDependencies.bind(this));
        this.router.get('/entities/:entityName/:id/name', this.getEntityRecordName.bind(this));
        // View operations
        this.router.post('/views/:entityName', this.runView.bind(this));
        this.router.post('/views/batch', this.runViews.bind(this));
        this.router.get('/views/entity', this.getViewEntity.bind(this));
        // Metadata endpoints
        this.router.get('/metadata/entities', this.getEntityMetadata.bind(this));
        this.router.get('/metadata/entities/:entityName', this.getEntityFieldMetadata.bind(this));
        // Views metadata
        this.router.get('/views/:entityName/metadata', this.getViewsMetadata.bind(this));
        // User operations
        this.router.get('/users/current', this.getCurrentUser.bind(this));
        this.router.get('/users/:userId/favorites/:entityName/:id', this.getRecordFavoriteStatus.bind(this));
        this.router.put('/users/:userId/favorites/:entityName/:id', this.setRecordFavoriteStatus.bind(this));
        this.router.delete('/users/:userId/favorites/:entityName/:id', this.removeRecordFavoriteStatus.bind(this));
        // Transaction operations
        this.router.post('/transactions', this.executeTransaction.bind(this));
        // Reports and queries
        this.router.get('/reports/:reportId', this.runReport.bind(this));
        this.router.post('/queries/run', this.runQuery.bind(this));
        // Error handling
        this.router.use(this.errorHandler.bind(this));
    }
    /**
     * Middleware to check entity access based on include/exclude lists
     */
    checkEntityAccess(req, res, next) {
        const entityName = this.getStringParam(req.params.entityName);
        if (!entityName) {
            next();
            return;
        }
        if (!this.isEntityAllowed(entityName)) {
            res.status(403).json({
                error: `Access to entity '${entityName}' is not allowed through the REST API`,
                details: 'This entity is either not included in the allowlist or is explicitly excluded in the REST API configuration'
            });
            return;
        }
        next();
    }
    /**
     * Guard middleware: verifies that the unified auth middleware (or a custom
     * per-route authMiddleware) has already set req['mjUser'] to a UserInfo.
     * Does NOT overwrite the value — just returns 401 if it is missing.
     */
    extractMJUser(req, res, next) {
        if (!req['mjUser']) {
            res.status(401).json({ error: 'Authentication required' });
            return;
        }
        next();
    }
    /**
     * Error handling middleware
     */
    errorHandler(err, req, res, next) {
        LogError(err);
        if (err.name === 'UnauthorizedError') {
            res.status(401).json({ error: 'Invalid token' });
            return;
        }
        res.status(500).json({ error: err.message || 'Internal server error' });
    }
    /**
     * Get the current user
     */
    async getCurrentUser(req, res) {
        try {
            const user = req['mjUser'];
            // Return user info without sensitive data
            res.json({
                ID: user.ID,
                Name: user.Name,
                Email: user.Email,
                FirstName: user.FirstName,
                LastName: user.LastName,
                UserRoles: user.UserRoles.map(role => ({
                    RoleID: role.RoleID,
                    Role: role.Role
                }))
            });
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Lists entities with optional filtering
     */
    async getEntityList(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const { filter, orderBy, fields, maxRows, startRow } = req.query;
            const user = req['mjUser'];
            // Convert the request to a RunViewParams object
            const params = {
                EntityName: entityName,
                ExtraFilter: filter,
                OrderBy: orderBy,
                Fields: fields ? fields.split(',') : undefined,
                MaxRows: maxRows ? parseInt(maxRows) : undefined,
                StartRow: startRow ? parseInt(startRow) : undefined
            };
            const result = await ViewOperationsHandler.listEntities(params, user);
            res.json(result);
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get a single entity by ID
     */
    async getEntity(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const { include } = req.query; // Optional related entities to include
            const user = req['mjUser'];
            const relatedEntities = include ? include.split(',') : null;
            const result = await EntityCRUDHandler.getEntity(entityName, id, relatedEntities, user);
            if (result.success) {
                res.json(result.entity);
            }
            else {
                res.status(result.error.includes('not found') ? 404 : 400).json({ error: result.error });
            }
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Create a new entity
     */
    async createEntity(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const entityData = req.body;
            const user = req['mjUser'];
            const result = await EntityCRUDHandler.createEntity(entityName, entityData, user);
            if (result.success) {
                res.status(201).json(result.entity);
            }
            else {
                res.status(400).json({
                    error: result.error,
                    details: result.details
                });
            }
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Update an existing entity
     */
    async updateEntity(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const updateData = req.body;
            const user = req['mjUser'];
            const result = await EntityCRUDHandler.updateEntity(entityName, id, updateData, user);
            if (result.success) {
                res.json(result.entity);
            }
            else {
                res.status(result.error.includes('not found') ? 404 : 400).json({
                    error: result.error,
                    details: result.details
                });
            }
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Delete an entity
     */
    async deleteEntity(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const options = req.query.options ? JSON.parse(req.query.options) : {};
            const user = req['mjUser'];
            // Convert options to EntityDeleteOptions
            const deleteOptions = new EntityDeleteOptions();
            if (options.SkipEntityAIActions !== undefined)
                deleteOptions.SkipEntityAIActions = !!options.SkipEntityAIActions;
            if (options.SkipEntityActions !== undefined)
                deleteOptions.SkipEntityActions = !!options.SkipEntityActions;
            if (options.ReplayOnly !== undefined)
                deleteOptions.ReplayOnly = !!options.ReplayOnly;
            const result = await EntityCRUDHandler.deleteEntity(entityName, id, deleteOptions, user);
            if (result.success) {
                res.status(204).send();
            }
            else {
                res.status(result.error.includes('not found') ? 404 : 400).json({ error: result.error });
            }
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get record changes for an entity
     */
    async getRecordChanges(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const user = req['mjUser'];
            // Get the entity object
            const md = new Metadata();
            const entity = await md.GetEntityObject(entityName, user);
            // Create a composite key
            const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
            // Use a direct approach for getting record changes
            // Note: This is a simplification. The actual implementation may need to be adjusted
            // based on how the MemberJunction core handles record changes
            const changes = []; // This would be populated with actual record changes
            res.json(changes);
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get record dependencies for an entity
     */
    async getRecordDependencies(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const user = req['mjUser'];
            // Get the entity object
            const md = new Metadata();
            const entity = await md.GetEntityObject(entityName, user);
            // Create a composite key
            const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
            // Use a direct approach for getting record dependencies
            // Note: This is a simplification. The actual implementation may need to be adjusted
            // based on how the MemberJunction core handles record dependencies
            const dependencies = []; // This would be populated with actual record dependencies
            res.json(dependencies);
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get entity record name
     */
    async getEntityRecordName(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const user = req['mjUser'];
            // Get the entity object
            const md = new Metadata();
            const entity = await md.GetEntityObject(entityName, user);
            // Create a composite key
            const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
            // Use a direct approach for getting entity record name
            // Note: This is a simplification. The actual implementation may need to be adjusted
            const recordName = "Record Name"; // This would be populated with the actual record name
            res.json({ recordName });
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Run a view
     */
    async runView(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const viewParams = req.body;
            const user = req['mjUser'];
            // Create RunViewParams from the request body
            const params = {
                EntityName: entityName,
                ...viewParams
            };
            const result = await ViewOperationsHandler.runView(params, user);
            if (result.success) {
                res.json(result.result);
            }
            else {
                res.status(400).json({ error: result.error });
            }
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Run multiple views in batch
     */
    async runViews(req, res) {
        try {
            const { params } = req.body;
            const user = req['mjUser'];
            if (!Array.isArray(params)) {
                res.status(400).json({ error: 'params must be an array of RunViewParams' });
                return;
            }
            // Filter out any views for entities that aren't allowed
            // using our enhanced entity filtering with wildcards and schema support
            const filteredParams = params.filter(p => this.isEntityAllowed(p.EntityName));
            // If all requested entities were filtered out, return an error
            if (filteredParams.length === 0 && params.length > 0) {
                res.status(403).json({
                    error: 'None of the requested entities are allowed through the REST API',
                    details: 'The entities requested are either not included in the allowlist or are explicitly excluded in the REST API configuration'
                });
                return;
            }
            const result = await ViewOperationsHandler.runViews(filteredParams, user);
            if (result.success) {
                res.json(result.results);
            }
            else {
                res.status(400).json({ error: result.error });
            }
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get entity for a view
     */
    async getViewEntity(req, res) {
        try {
            const { ViewID, ViewName } = req.query;
            const user = req['mjUser'];
            if (!ViewID && !ViewName) {
                res.status(400).json({ error: 'Either ViewID or ViewName must be provided' });
                return;
            }
            // Placeholder implementation - this would need to be implemented to lookup view metadata
            const entityName = "SampleEntity"; // This would be determined by looking up the view
            res.json({ entityName });
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get metadata for all entities
     */
    async getEntityMetadata(req, res) {
        try {
            const user = req['mjUser'];
            // Filter entities based on user permissions and REST API configuration
            const md = new Metadata();
            const entities = md.Entities.filter(e => {
                // First check if entity is allowed based on configuration
                if (!this.isEntityAllowed(e.Name)) {
                    return false;
                }
                // Then check user permissions
                const permissions = e.GetUserPermisions(user);
                return permissions.CanRead;
            });
            const result = entities.map(e => ({
                Name: e.Name,
                ClassName: e.ClassName,
                SchemaName: e.SchemaName,
                DisplayName: e.DisplayName,
                Description: e.Description,
                IncludeInAPI: e.IncludeInAPI,
                AllowCreateAPI: e.AllowCreateAPI,
                AllowUpdateAPI: e.AllowUpdateAPI,
                AllowDeleteAPI: e.AllowDeleteAPI
            }));
            res.json(result);
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get field metadata for a specific entity
     */
    async getEntityFieldMetadata(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const user = req['mjUser'];
            const md = new Metadata();
            const entity = md.Entities.find(e => e.Name === entityName);
            if (!entity) {
                res.status(404).json({ error: `Entity '${entityName}' not found` });
                return;
            }
            // Check if user can read this entity
            const permissions = entity.GetUserPermisions(user);
            if (!permissions.CanRead) {
                res.status(403).json({ error: 'Permission denied' });
                return;
            }
            const result = entity.Fields.map(f => ({
                Name: f.Name,
                DisplayName: f.DisplayName,
                Description: f.Description,
                Type: f.Type,
                IsRequired: f.AllowsNull === false,
                IsPrimaryKey: f.IsPrimaryKey,
                IsUnique: f.IsUnique,
                MaxLength: f.MaxLength,
                DefaultValue: f.DefaultValue,
                CodeName: f.CodeName,
                TSType: f.TSType
            }));
            res.json(result);
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get metadata about available views for an entity
     */
    async getViewsMetadata(req, res) {
        try {
            const entityName = this.getStringParam(req.params.entityName);
            const user = req['mjUser'];
            // This would need to be implemented to retrieve available views
            // Placeholder implementation
            const views = []; // Would need to query available views for this entity
            res.json(views);
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Get favorite status for a record
     */
    async getRecordFavoriteStatus(req, res) {
        try {
            const userId = this.getStringParam(req.params.userId);
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const user = req['mjUser'];
            // Get the entity object
            const md = new Metadata();
            const entity = await md.GetEntityObject(entityName, user);
            // Create a composite key
            const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
            // Use a direct approach for getting favorite status
            // Note: This is a simplification. The actual implementation may need to be adjusted
            const isFavorite = false; // This would be populated with the actual favorite status
            res.json({ isFavorite });
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Set favorite status for a record
     */
    async setRecordFavoriteStatus(req, res) {
        try {
            const userId = this.getStringParam(req.params.userId);
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const user = req['mjUser'];
            // Get the entity object
            const md = new Metadata();
            const entity = await md.GetEntityObject(entityName, user);
            // Create a composite key
            const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
            // Use a direct approach for setting favorite status
            // Note: This is a simplification. The actual implementation may need to be adjusted
            // This would set the favorite status to true
            res.status(204).send();
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Remove favorite status for a record
     */
    async removeRecordFavoriteStatus(req, res) {
        try {
            const userId = this.getStringParam(req.params.userId);
            const entityName = this.getStringParam(req.params.entityName);
            const id = this.getStringParam(req.params.id);
            const user = req['mjUser'];
            // Get the entity object
            const md = new Metadata();
            const entity = await md.GetEntityObject(entityName, user);
            // Create a composite key
            const compositeKey = this.createCompositeKey(entity.EntityInfo, id);
            // Use a direct approach for setting favorite status
            // Note: This is a simplification. The actual implementation may need to be adjusted
            // This would set the favorite status to false
            res.status(204).send();
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Execute a transaction
     */
    async executeTransaction(req, res) {
        try {
            // Placeholder implementation - this would need to be implemented to handle transactions
            res.status(501).json({ error: 'Not implemented' });
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Run a report
     */
    async runReport(req, res) {
        try {
            // Placeholder implementation - this would need to be implemented to run reports
            res.status(501).json({ error: 'Not implemented' });
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Run a query
     */
    async runQuery(req, res) {
        try {
            // Placeholder implementation - this would need to be implemented to run queries
            res.status(501).json({ error: 'Not implemented' });
        }
        catch (error) {
            LogError(error);
            res.status(500).json({ error: error?.message || 'Unknown error' });
        }
    }
    /**
     * Helper method to create a composite key from an ID
     */
    createCompositeKey(entityInfo, id) {
        if (entityInfo.PrimaryKeys.length === 1) {
            // Single primary key
            const primaryKeyField = entityInfo.PrimaryKeys[0].Name;
            const compositeKey = new CompositeKey();
            // Use key-value pairs instead of SetValue
            compositeKey.KeyValuePairs = [
                { FieldName: primaryKeyField, Value: id }
            ];
            return compositeKey;
        }
        else {
            // Composite primary key
            // This is a simplification - in a real implementation, we would need to handle composite keys properly
            throw new Error('Composite primary keys are not supported in this implementation');
        }
    }
    /**
     * Get the Express router with all configured routes
     */
    getRouter() {
        return this.router;
    }
}
//# sourceMappingURL=RESTEndpointHandler.js.map