<?php
/*******************************************************************************************************************
| Software Name        : EasyStream
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
| Software Author      : (c) Sami Ahmed
|*******************************************************************************************************************
|
|*******************************************************************************************************************
| This source file is subject to the EasyStream Proprietary License Agreement.
| 
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|*******************************************************************************************************************
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|*******************************************************************************************************************/

defined('_ISVALID') or header('Location: /error');

/**
 * Role-Based Access Control (RBAC) System
 */
class VRBAC
{
    private static $instance = null;
    private $db;
    private $logger;
    private $cache;
    private $userRoles = [];
    private $rolePermissions = [];
    
    // Default system roles
    const ROLE_GUEST = 'guest';
    const ROLE_MEMBER = 'member';
    const ROLE_VERIFIED = 'verified';
    const ROLE_STREAMER = 'streamer';
    const ROLE_CREATOR = 'creator';
    const ROLE_MODERATOR = 'moderator';
    const ROLE_ADMIN = 'admin';
    const ROLE_SUPER_ADMIN = 'super_admin';
    
    // Permission categories
    const PERM_CONTENT = 'content';
    const PERM_USER = 'user';
    const PERM_ADMIN = 'admin';
    const PERM_SYSTEM = 'system';
    
    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function __construct()
    {
        $this->db = VDatabase::getInstance();
        $this->logger = VLogger::getInstance();
        $this->cache = VRedis::getInstance();
        
        $this->initializeDefaultRoles();
    }
    
    /**
     * Check if user has permission
     * @param int|string $userId User ID or current session user
     * @param string $permission Permission name
     * @param array $context Additional context for permission check
     * @return bool True if user has permission
     */
    /**
     * Flexible permission checker supporting both legacy and new call signatures.
     *
     * Supported usages:
     *  - hasPermission($userId, $permission, $context = [])
     *  - hasPermission($permission, $userId = 'current', $context = [])
     *  - hasPermission($permission, $contextArray) // uses current session user
     *
     * @param mixed $arg1 User ID or permission name depending on signature
     * @param mixed $arg2 Permission name, user ID, or context array
     * @param mixed $arg3 Context array when using legacy signature
     * @return bool
     */
    public function hasPermission($arg1 = null, $arg2 = null, $arg3 = [])
    {
        try {
            $permission = null;
            $userId = null;
            $context = [];

            if (is_string($arg1) && ($arg2 === null || is_scalar($arg2) || is_array($arg2))) {
                // New-style signature: ($permission, $userId?, $context?)
                $permission = $arg1;

                if (is_array($arg2) && ($arg3 === [] || $arg3 === null)) {
                    $context = $arg2;
                    $userId = 'current';
                } else {
                    $userId = $arg2;
                    $context = is_array($arg3) ? $arg3 : [];
                }
            } else {
                // Legacy signature: ($userId, $permission, $context)
                $userId = $arg1;
                $permission = $arg2;
                $context = is_array($arg3) ? $arg3 : [];
            }

            if (is_array($userId)) {
                // Handle accidental signature: hasPermission($permission, $context)
                $context = $userId;
                $userId = 'current';
            }

            if (!is_array($context)) {
                $context = [];
            }

            if (!$permission || !is_string($permission)) {
                $this->logger->warning('Permission check called without a valid permission name', [
                    'arg1' => $arg1,
                    'arg2' => $arg2,
                    'arg3' => $arg3
                ]);
                return false;
            }

            // Handle current user
            if ($userId === 'current' || $userId === null) {
                $userId = $_SESSION['USER_ID'] ?? null;
            }
            
            if (!$userId) {
                // Guest user - check guest permissions
                return $this->checkGuestPermission($permission, $context);
            }
            
            // Get user roles
            $userRoles = $this->getUserRoles($userId);
            
            if (empty($userRoles)) {
                // No roles assigned - treat as guest
                return $this->checkGuestPermission($permission, $context);
            }
            
            // Check if any role has the permission
            foreach ($userRoles as $role) {
                if ($this->roleHasPermission($role, $permission, $context)) {
                    // Log permission grant for audit
                    $this->logPermissionCheck($userId, $permission, true, $role, $context);
                    return true;
                }
            }
            
            // Check for resource-specific permissions
            if (isset($context['resource_type']) && isset($context['resource_id'])) {
                if ($this->hasResourcePermission($userId, $permission, $context['resource_type'], $context['resource_id'])) {
                    $this->logPermissionCheck($userId, $permission, true, 'resource_specific', $context);
                    return true;
                }
            }
            
            // Log permission denial
            $this->logPermissionCheck($userId, $permission, false, implode(',', $userRoles), $context);
            return false;
            
        } catch (Exception $e) {
            $this->logger->error('Permission check failed', [
                'user_id' => $userId,
                'permission' => $permission,
                'context' => $context,
                'error' => $e->getMessage()
            ]);
            
            // Fail secure - deny permission on error
            return false;
        }
    }

    /**
     * Check if a user (or current session) has a specific role.
     * Falls back to guest semantics when no authenticated user is present.
     *
     * @param string $role Target role name
     * @param int|string|null $userId User identifier, `'current'` (default) uses session
     * @return bool
     */
    public function hasRole($role, $userId = 'current')
    {
        try {
            if ($userId === 'current' || $userId === null) {
                $userId = $_SESSION['USER_ID'] ?? null;
            }

            if (!$userId) {
                return $role === self::ROLE_GUEST;
            }

            $roles = $this->getUserRoles($userId);
            return in_array($role, $roles, true);
        } catch (Exception $e) {
            $this->logger->error('Role check failed', [
                'user_id' => $userId,
                'role' => $role,
                'error' => $e->getMessage()
            ]);

            return false;
        }
    }
    
    /**
     * Assign role to user
     * @param int $userId User ID
     * @param string $role Role name
     * @param int $assignedBy User ID who assigned the role
     * @param string $reason Reason for assignment
     * @return bool Success status
     */
    public function assignRole($userId, $role, $assignedBy = null, $reason = '')
    {
        try {
            // Validate role exists
            if (!$this->roleExists($role)) {
                throw new Exception("Role does not exist: {$role}");
            }
            
            // Check if user already has this role
            if ($this->userHasRole($userId, $role)) {
                return true; // Already has role
            }
            
            // Insert role assignment
            $assignmentData = [
                'user_id' => $userId,
                'role_name' => $role,
                'assigned_by' => $assignedBy,
                'assigned_at' => date('Y-m-d H:i:s'),
                'reason' => $reason,
                'status' => 'active'
            ];
            
            $assignmentId = $this->db->doInsert('db_user_roles', $assignmentData);
            
            if ($assignmentId) {
                // Clear user role cache
                $this->clearUserRoleCache($userId);
                
                // Log role assignment
                $this->logger->info('Role assigned to user', [
                    'user_id' => $userId,
                    'role' => $role,
                    'assigned_by' => $assignedBy,
                    'reason' => $reason
                ]);
                
                // Send notification to user
                $this->sendRoleNotification($userId, 'role_assigned', $role);
                
                $this->handleRoleAssignmentSideEffects($userId, $role);
                
                return true;
            }
            
            return false;
            
        } catch (Exception $e) {
            $this->logger->error('Role assignment failed', [
                'user_id' => $userId,
                'role' => $role,
                'assigned_by' => $assignedBy,
                'error' => $e->getMessage()
            ]);
            
            return false;
        }
    }
    
    /**
     * Remove role from user
     * @param int $userId User ID
     * @param string $role Role name
     * @param int $removedBy User ID who removed the role
     * @param string $reason Reason for removal
     * @return bool Success status
     */
    public function removeRole($userId, $role, $removedBy = null, $reason = '')
    {
        try {
            // Update role assignment status
            $updateData = [
                'status' => 'revoked',
                'revoked_by' => $removedBy,
                'revoked_at' => date('Y-m-d H:i:s'),
                'revoke_reason' => $reason
            ];
            
            $result = $this->db->doUpdate(
                'db_user_roles',
                ['user_id', 'role_name', 'status'],
                $updateData,
                [$userId, $role, 'active']
            );
            
            if ($result) {
                // Clear user role cache
                $this->clearUserRoleCache($userId);
                
                // Log role removal
                $this->logger->info('Role removed from user', [
                    'user_id' => $userId,
                    'role' => $role,
                    'removed_by' => $removedBy,
                    'reason' => $reason
                ]);
                
                // Send notification to user
                $this->sendRoleNotification($userId, 'role_removed', $role);
                
                $this->handleRoleRemovalSideEffects($userId, $role);
                
                return true;
            }
            
            return false;
            
        } catch (Exception $e) {
            $this->logger->error('Role removal failed', [
                'user_id' => $userId,
                'role' => $role,
                'removed_by' => $removedBy,
                'error' => $e->getMessage()
            ]);
            
            return false;
        }
    }
    
    /**
     * Get user roles
     * @param int $userId User ID
     * @return array User roles
     */
    public function getUserRoles($userId)
    {
        if (isset($this->userRoles[$userId])) {
            return $this->userRoles[$userId];
        }
        
        // Try cache first
        $cacheKey = "user_roles_{$userId}";
        if ($this->cache->isConnected()) {
            $cachedRoles = $this->cache->get($cacheKey);
            if ($cachedRoles !== false) {
                $this->userRoles[$userId] = $cachedRoles;
                return $cachedRoles;
            }
        }
        
        // Get from database
        $query = "SELECT role_name FROM db_user_roles WHERE user_id = ? AND status = 'active'";
        $result = $this->db->doQuery($query, [$userId]);
        
        $roles = [];
        while ($row = $this->db->doFetch($result)) {
            $roles[] = $row['role_name'];
        }
        
        // Add default member role if user has no roles
        if (empty($roles) && $this->userExists($userId)) {
            $roles[] = self::ROLE_MEMBER;
        }
        
        // Cache roles
        $this->userRoles[$userId] = $roles;
        if ($this->cache->isConnected()) {
            $this->cache->set($cacheKey, $roles, 3600); // Cache for 1 hour
        }
        
        return $roles;
    }
    
    /**
     * Check if role has permission
     * @param string $role Role name
     * @param string $permission Permission name
     * @param array $context Permission context
     * @return bool True if role has permission
     */
    public function roleHasPermission($role, $permission, $context = [])
    {
        // Get role permissions
        $rolePermissions = $this->getRolePermissions($role);
        
        // Check direct permission
        if (in_array($permission, $rolePermissions)) {
            return true;
        }
        
        // Check wildcard permissions
        $permissionParts = explode('.', $permission);
        for ($i = count($permissionParts) - 1; $i > 0; $i--) {
            $wildcardPerm = implode('.', array_slice($permissionParts, 0, $i)) . '.*';
            if (in_array($wildcardPerm, $rolePermissions)) {
                return true;
            }
        }
        
        // Check context-specific permissions
        if (!empty($context)) {
            return $this->checkContextualPermission($role, $permission, $context);
        }
        
        return false;
    }
    
    /**
     * Get role permissions
     * @param string $role Role name
     * @return array Role permissions
     */
    public function getRolePermissions($role)
    {
        if (isset($this->rolePermissions[$role])) {
            return $this->rolePermissions[$role];
        }
        
        // Try cache first
        $cacheKey = "role_permissions_{$role}";
        if ($this->cache->isConnected()) {
            $cachedPermissions = $this->cache->get($cacheKey);
            if ($cachedPermissions !== false) {
                $this->rolePermissions[$role] = $cachedPermissions;
                return $cachedPermissions;
            }
        }
        
        // Get from database
        $query = "SELECT permission_name FROM db_role_permissions WHERE role_name = ? AND status = 'active'";
        $result = $this->db->doQuery($query, [$role]);
        
        $permissions = [];
        while ($row = $this->db->doFetch($result)) {
            $permissions[] = $row['permission_name'];
        }
        
        // Add default permissions for built-in roles
        $permissions = array_merge($permissions, $this->getDefaultRolePermissions($role));
        
        // Cache permissions
        $this->rolePermissions[$role] = $permissions;
        if ($this->cache->isConnected()) {
            $this->cache->set($cacheKey, $permissions, 3600); // Cache for 1 hour
        }
        
        return $permissions;
    }
    
    /**
     * Create custom role
     * @param string $roleName Role name
     * @param string $displayName Display name
     * @param string $description Role description
     * @param array $permissions Initial permissions
     * @param int $createdBy User ID who created the role
     * @return bool Success status
     */
    public function createRole($roleName, $displayName, $description = '', $permissions = [], $createdBy = null)
    {
        try {
            // Check if role already exists
            if ($this->roleExists($roleName)) {
                throw new Exception("Role already exists: {$roleName}");
            }
            
            // Insert role
            $roleData = [
                'role_name' => $roleName,
                'display_name' => $displayName,
                'description' => $description,
                'created_by' => $createdBy,
                'created_at' => date('Y-m-d H:i:s'),
                'status' => 'active'
            ];
            
            $roleId = $this->db->doInsert('db_roles', $roleData);
            
            if ($roleId) {
                // Add permissions
                foreach ($permissions as $permission) {
                    $this->addRolePermission($roleName, $permission);
                }
                
                $this->logger->info('Custom role created', [
                    'role_name' => $roleName,
                    'display_name' => $displayName,
                    'permissions' => $permissions,
                    'created_by' => $createdBy
                ]);
                
                return true;
            }
            
            return false;
            
        } catch (Exception $e) {
            $this->logger->error('Role creation failed', [
                'role_name' => $roleName,
                'error' => $e->getMessage()
            ]);
            
            return false;
        }
    }
    
    /**
     * Add permission to role
     * @param string $role Role name
     * @param string $permission Permission name
     * @return bool Success status
     */
    public function addRolePermission($role, $permission)
    {
        try {
            $permissionData = [
                'role_name' => $role,
                'permission_name' => $permission,
                'granted_at' => date('Y-m-d H:i:s'),
                'status' => 'active'
            ];
            
            $result = $this->db->doInsert('db_role_permissions', $permissionData);
            
            if ($result) {
                // Clear role permission cache
                $this->clearRolePermissionCache($role);
                
                $this->logger->info('Permission added to role', [
                    'role' => $role,
                    'permission' => $permission
                ]);
                
                return true;
            }
            
            return false;
            
        } catch (Exception $e) {
            $this->logger->error('Failed to add role permission', [
                'role' => $role,
                'permission' => $permission,
                'error' => $e->getMessage()
            ]);
            
            return false;
        }
    }
    
    /**
     * Initialize default system roles and permissions
     */
    private function initializeDefaultRoles()
    {
        $defaultRoles = [
            self::ROLE_GUEST => [
                'display_name' => 'Guest',
                'description' => 'Non-registered users',
                'permissions' => [
                    'content.view',
                    'content.search',
                    'user.register'
                ]
            ],
            self::ROLE_MEMBER => [
                'display_name' => 'Member',
                'description' => 'Registered users',
                'permissions' => [
                    'content.*',
                    'user.profile.edit',
                    'user.upload.basic',
                    'user.comment',
                    'user.like',
                    'user.subscribe'
                ]
            ],
            self::ROLE_VERIFIED => [
                'display_name' => 'Verified User',
                'description' => 'Email verified users',
                'permissions' => [
                    'content.*',
                    'user.*',
                    'upload.advanced'
                ]
            ],
            self::ROLE_STREAMER => [
                'display_name' => 'Streamer',
                'description' => 'Users allowed to broadcast live streams',
                'permissions' => [
                    'live.stream.basic',
                    'live.stream.advanced',
                    'live.stream.record',
                    'live.stream.quality'
                ]
            ],
            self::ROLE_CREATOR => [
                'display_name' => 'Content Creator',
                'description' => 'Content creators with enhanced publishing features',
                'permissions' => [
                    'content.*',
                    'user.*',
                    'upload.*',
                    'analytics.view',
                    'monetization.basic'
                ]
            ],
            self::ROLE_MODERATOR => [
                'display_name' => 'Moderator',
                'description' => 'Content moderators',
                'permissions' => [
                    'content.*',
                    'user.*',
                    'moderation.*',
                    'admin.content.moderate',
                    'admin.users.moderate'
                ]
            ],
            self::ROLE_ADMIN => [
                'display_name' => 'Administrator',
                'description' => 'Site administrators',
                'permissions' => [
                    'content.*',
                    'user.*',
                    'admin.*',
                    'system.settings',
                    'system.logs.view'
                ]
            ],
            self::ROLE_SUPER_ADMIN => [
                'display_name' => 'Super Administrator',
                'description' => 'Full system access',
                'permissions' => [
                    '*' // All permissions
                ]
            ]
        ];
        
        // This would typically be run during installation/setup
        // For now, we'll store the default permissions in memory
        foreach ($defaultRoles as $roleName => $roleData) {
            $this->rolePermissions[$roleName] = $roleData['permissions'];
        }
    }
    
    /**
     * Get default permissions for built-in roles
     * @param string $role Role name
     * @return array Default permissions
     */
    private function getDefaultRolePermissions($role)
    {
        $defaultPermissions = [
            self::ROLE_GUEST => [
                'content.view',
                'content.search',
                'user.register'
            ],
            self::ROLE_MEMBER => [
                'content.view',
                'content.search',
                'user.profile.edit',
                'user.upload.basic',
                'user.comment',
                'user.like',
                'user.subscribe'
            ],
            self::ROLE_VERIFIED => [
                'content.view',
                'content.search',
                'user.profile.edit',
                'user.upload.basic',
                'user.upload.advanced',
                'user.comment',
                'user.like',
                'user.subscribe'
            ],
            self::ROLE_STREAMER => [
                'live.stream.basic',
                'live.stream.advanced',
                'live.stream.record',
                'live.stream.quality'
            ],
            self::ROLE_CREATOR => [
                'content.view',
                'content.search',
                'user.profile.edit',
                'user.upload.basic',
                'user.upload.advanced',
                'user.comment',
                'user.like',
                'user.subscribe',
                'analytics.view',
                'monetization.basic'
            ],
            self::ROLE_MODERATOR => [
                'content.view',
                'content.search',
                'content.moderate',
                'user.profile.edit',
                'user.moderate',
                'admin.content.moderate',
                'admin.users.moderate'
            ],
            self::ROLE_ADMIN => [
                'content.*',
                'user.*',
                'admin.*',
                'system.settings',
                'system.logs.view'
            ],
            self::ROLE_SUPER_ADMIN => [
                '*'
            ]
        ];
        
        return $defaultPermissions[$role] ?? [];
    }
    
    /**
     * Helper methods
     */
    private function checkGuestPermission($permission, $context)
    {
        $guestPermissions = $this->getDefaultRolePermissions(self::ROLE_GUEST);
        return in_array($permission, $guestPermissions) || in_array('*', $guestPermissions);
    }
    
    private function roleGrantsStreaming($role)
    {
        $streamingRoles = [
            self::ROLE_STREAMER,
            self::ROLE_ADMIN,
            self::ROLE_SUPER_ADMIN
        ];
        
        return in_array($role, $streamingRoles, true);
    }
    
    private function handleRoleAssignmentSideEffects($userId, $role)
    {
        if (!$this->roleGrantsStreaming($role)) {
            return;
        }
        
        try {
            VStreamKeyManager::getInstance()->activateStreaming($userId);
        } catch (Exception $e) {
            $this->logger->error('Failed to activate streaming during role assignment', [
                'user_id' => $userId,
                'role' => $role,
                'error' => $e->getMessage()
            ]);
        }
    }
    
    private function handleRoleRemovalSideEffects($userId, $role)
    {
        if (!$this->roleGrantsStreaming($role)) {
            return;
        }
        
        try {
            // If the user still has streaming permission via another role, do not disable.
            if ($this->hasPermission($userId, 'live.stream.basic')) {
                return;
            }
            
            VStreamKeyManager::getInstance()->deactivateStreaming($userId);
        } catch (Exception $e) {
            $this->logger->error('Failed to deactivate streaming during role removal', [
                'user_id' => $userId,
                'role' => $role,
                'error' => $e->getMessage()
            ]);
        }
    }
    
    private function roleExists($role)
    {
        $query = "SELECT COUNT(*) as count FROM db_roles WHERE role_name = ? AND status = 'active'";
        $result = $this->db->doQuery($query, [$role]);
        $row = $this->db->doFetch($result);
        
        return ($row['count'] > 0) || in_array($role, [
            self::ROLE_GUEST, self::ROLE_MEMBER, self::ROLE_VERIFIED,
            self::ROLE_CREATOR, self::ROLE_MODERATOR, self::ROLE_ADMIN, self::ROLE_SUPER_ADMIN
        ]);
    }
    
    private function userHasRole($userId, $role)
    {
        $userRoles = $this->getUserRoles($userId);
        return in_array($role, $userRoles);
    }
    
    private function userExists($userId)
    {
        $query = "SELECT COUNT(*) as count FROM db_accountuser WHERE usr_id = ?";
        $result = $this->db->doQuery($query, [$userId]);
        $row = $this->db->doFetch($result);
        
        return $row['count'] > 0;
    }
    
    private function hasResourcePermission($userId, $permission, $resourceType, $resourceId)
    {
        // Check if user owns the resource
        if ($this->userOwnsResource($userId, $resourceType, $resourceId)) {
            return true;
        }
        
        // Check resource-specific permissions
        $query = "SELECT COUNT(*) as count FROM db_resource_permissions 
                  WHERE user_id = ? AND resource_type = ? AND resource_id = ? 
                  AND permission_name = ? AND status = 'active'";
        $result = $this->db->doQuery($query, [$userId, $resourceType, $resourceId, $permission]);
        $row = $this->db->doFetch($result);
        
        return $row['count'] > 0;
    }
    
    private function userOwnsResource($userId, $resourceType, $resourceId)
    {
        $ownershipQueries = [
            'video' => "SELECT COUNT(*) as count FROM db_videofiles WHERE file_key = ? AND usr_id = ?",
            'channel' => "SELECT COUNT(*) as count FROM db_channels WHERE ch_id = ? AND usr_id = ?",
            'playlist' => "SELECT COUNT(*) as count FROM db_playlists WHERE pl_id = ? AND usr_id = ?"
        ];
        
        if (!isset($ownershipQueries[$resourceType])) {
            return false;
        }
        
        $result = $this->db->doQuery($ownershipQueries[$resourceType], [$resourceId, $userId]);
        $row = $this->db->doFetch($result);
        
        return $row['count'] > 0;
    }
    
    private function checkContextualPermission($role, $permission, $context)
    {
        // Implement context-specific permission logic
        // For example, moderators can only moderate content in their assigned categories
        
        if ($role === self::ROLE_MODERATOR && isset($context['category'])) {
            // Check if moderator is assigned to this category
            $query = "SELECT COUNT(*) as count FROM db_moderator_categories 
                      WHERE role_name = ? AND category = ? AND status = 'active'";
            $result = $this->db->doQuery($query, [$role, $context['category']]);
            $row = $this->db->doFetch($result);
            
            return $row['count'] > 0;
        }
        
        return false;
    }
    
    private function clearUserRoleCache($userId)
    {
        unset($this->userRoles[$userId]);
        if ($this->cache->isConnected()) {
            $this->cache->delete("user_roles_{$userId}");
        }
    }
    
    private function clearRolePermissionCache($role)
    {
        unset($this->rolePermissions[$role]);
        if ($this->cache->isConnected()) {
            $this->cache->delete("role_permissions_{$role}");
        }
    }
    
    private function logPermissionCheck($userId, $permission, $granted, $role, $context)
    {
        $this->logger->info('Permission check', [
            'user_id' => $userId,
            'permission' => $permission,
            'granted' => $granted,
            'role' => $role,
            'context' => $context,
            'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
        ]);
    }
    
    private function sendRoleNotification($userId, $type, $role)
    {
        try {
            $queue = new VQueue();
            
            $notificationData = [
                'type' => 'in_app',
                'recipient' => $userId,
                'subject' => $type === 'role_assigned' ? 'Role Assigned' : 'Role Removed',
                'message' => $type === 'role_assigned' 
                    ? "You have been assigned the role: {$role}"
                    : "Your role has been removed: {$role}",
                'template_data' => [
                    'notification_type' => $type,
                    'data' => ['role' => $role]
                ]
            ];
            
            $queue->enqueue('NotificationJob', $notificationData, 'notifications');
            
        } catch (Exception $e) {
            $this->logger->error('Failed to send role notification', [
                'user_id' => $userId,
                'type' => $type,
                'role' => $role,
                'error' => $e->getMessage()
            ]);
        }
    }
}
