<?php

/**
 * SSO Entra ID (Formerly Azure AD) Client Functions
 * 
 * Functions for communicating with Microsoft Graph API
 * 
 * @package SSO
 */

// require_once(dirname(__FILE__) . '/../createprovider.php');
// require_once(dirname(__FILE__) . '/sso-utilities.php');

/**
 * AJAX - Get users from Entra ID (Formerly Azure AD)
 * 
 * @param string 'client_id' -- OAuth client ID
 * @param int 'page_size' -- number of items per page (default: 100)
 * @param string 'search' -- search term for filtering users
 * @param string 'next_link' -- full URL from @odata.nextLink for pagination
 * 
 * @echo json array -- [status, data with users array]
 */
function get_users_AAD(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    $page_size = grab_request_var('page_size', 100);
    $search = grab_request_var('search', '');
    $search_mode = grab_request_var('search_mode', 'startswith');
    $next_link = grab_request_var('next_link', '');
    $page_number = grab_request_var('page_number', null);
    
    // Validate search_mode
    if (!in_array($search_mode, ['startswith', 'contains'])) {
        $search_mode = 'startswith';
    }
    
    // Convert page_number to integer if provided
    if ($page_number !== null) {
        $page_number = intval($page_number);
        if ($page_number < 0) {
            $page_number = 0;
        }
    }
    
    if(empty($client_id)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID is required')]);
        exit;
    }
    
    try {
        // Get OAuth credentials (always needed for authentication, even when using next_link)
        $credentials = get_oauth_credentials($client_id);
        if(!$credentials){
            echo json_encode(['status' => 'failed', 'message' => _('OAuth credentials not found')]);
            exit;
        }
        
        // Create Microsoft Graph API client
        $graph_client = create_graph_client($credentials);
        
        // Initialize session token cache if not exists
        if (!isset($_SESSION['sso_pagination_tokens'])) {
            $_SESSION['sso_pagination_tokens'] = [];
        }
        
        // Create cache key for this query (client_id + search + search_mode)
        $cache_key = md5($client_id . '|' . $search . '|' . $search_mode);
        
        // Initialize cache entry if not exists
        if (!isset($_SESSION['sso_pagination_tokens'][$cache_key])) {
            $_SESSION['sso_pagination_tokens'][$cache_key] = [
                'tokens' => [0 => null], // Page 0 always uses null (no token)
                'last_updated' => time()
            ];
        }
        
        $token_cache = &$_SESSION['sso_pagination_tokens'][$cache_key];
        
        // Helper function to extract $skipToken from a nextLink URL
        $extract_skiptoken = function($next_link_url) {
            if (empty($next_link_url)) {
                return null;
            }
            // Parse the URL to extract $skiptoken parameter
            $parsed_url = parse_url($next_link_url);
            if (isset($parsed_url['query'])) {
                parse_str($parsed_url['query'], $query_params);
                if (isset($query_params['$skiptoken'])) {
                    return $query_params['$skiptoken'];
                }
            }
            return null;
        };
        
        // Helper function to build a request URL with $skiptoken
        $build_request_with_skiptoken = function($base_url, $skiptoken, $page_size, $search, $search_mode) {
            $query_params = ['$top' => $page_size];
            
            if (!empty($search)) {
                $sanitized_search = addslashes($search);
                if ($search_mode === 'contains') {
                    $query_params['$filter'] = "contains(displayName,'$sanitized_search') or contains(mail,'$sanitized_search') or contains(userPrincipalName,'$sanitized_search')";
                } else {
                    $query_params['$filter'] = "startswith(displayName,'$sanitized_search') or startswith(mail,'$sanitized_search') or startswith(userPrincipalName,'$sanitized_search')";
                }
            } else {
                $query_params['$orderby'] = 'displayName';
            }
            
            if ($skiptoken !== null) {
                $query_params['$skiptoken'] = $skiptoken;
            }
            
            return $base_url . '?' . http_build_query($query_params);
        };
        
        // Regular user loading with pagination support
        // Priority: page_number > next_link > default (page 0)
        if ($page_number !== null) {
            // Page number-based navigation
            $target_page = $page_number;
            
            // Check if we have a token for this page
            $token_for_page = isset($token_cache['tokens'][$target_page]) ? $token_cache['tokens'][$target_page] : null;
            
            // If we don't have a token, navigate sequentially from closest cached page
            if ($token_for_page === null && $target_page > 0) {
                // Find the highest cached page number <= target page
                $start_page = 0;
                $start_token = null;
                
                for ($i = $target_page - 1; $i >= 0; $i--) {
                    if (isset($token_cache['tokens'][$i])) {
                        $start_page = $i;
                        $start_token = $token_cache['tokens'][$i];
                        break;
                    }
                }
                
                // Navigate sequentially from start_page to target_page
                $current_nav_page = $start_page;
                $current_nav_token = $start_token;
                
                while ($current_nav_page < $target_page) {
                    if ($current_nav_page === 0 && $current_nav_token === null) {
                        // Fetch page 0 to get token for page 1
                        $query_params = ['$top' => $page_size];
                        
                        if (!empty($search)) {
                            $sanitized_search = addslashes($search);
                            if ($search_mode === 'contains') {
                                $query_params['$filter'] = "contains(displayName,'$sanitized_search') or contains(mail,'$sanitized_search') or contains(userPrincipalName,'$sanitized_search')";
                            } else {
                                $query_params['$filter'] = "startswith(displayName,'$sanitized_search') or startswith(mail,'$sanitized_search') or startswith(userPrincipalName,'$sanitized_search')";
                            }
                        } else {
                            $query_params['$orderby'] = 'displayName';
                        }
                        
                        $headers = [];
                        if (!empty($search)) {
                            $headers['ConsistencyLevel'] = 'eventual';
                        }
                        
                        if (!empty($headers)) {
                            $nav_response = $graph_client->get('v1.0/users?' . http_build_query($query_params), [
                                'headers' => $headers
                            ]);
                        } else {
                            $nav_response = $graph_client->get('v1.0/users?' . http_build_query($query_params));
                        }
                        
                        $nav_data = $nav_response->getBody();
                        $nav_users_data = json_decode($nav_data, true);
                        
                        // Store token for page 0
                        $token_cache['tokens'][0] = null;
                        
                        // Extract token for page 1
                        if (isset($nav_users_data['@odata.nextLink'])) {
                            $next_token = $extract_skiptoken($nav_users_data['@odata.nextLink']);
                            $token_cache['tokens'][1] = $next_token;
                            $current_nav_token = $next_token;
                        } else {
                            throw new Exception('Cannot navigate to target page - reached end of results.');
                        }
                        $current_nav_page = 1;
                    } else {
                        // Check if we already have the token for the next page
                        if (isset($token_cache['tokens'][$current_nav_page + 1])) {
                            $current_nav_page++;
                            $current_nav_token = $token_cache['tokens'][$current_nav_page];
                        } else if ($current_nav_token !== null) {
                            // Navigate forward using the token
                            $request_url = $build_request_with_skiptoken(
                                'v1.0/users',
                                $current_nav_token,
                                $page_size,
                                $search,
                                $search_mode
                            );
                            
                            $headers = [];
                            if (!empty($search)) {
                                $headers['ConsistencyLevel'] = 'eventual';
                            }
                            
                            if (!empty($headers)) {
                                $nav_response = $graph_client->get($request_url, ['headers' => $headers]);
                            } else {
                                $nav_response = $graph_client->get($request_url);
                            }
                            
                            $nav_data = $nav_response->getBody();
                            $nav_users_data = json_decode($nav_data, true);
                            
                            $current_nav_page++;
                            // Store the token we used to get to this page
                            $token_cache['tokens'][$current_nav_page] = $current_nav_token;
                            
                            // Extract token for the next page
                            if (isset($nav_users_data['@odata.nextLink'])) {
                                $next_token = $extract_skiptoken($nav_users_data['@odata.nextLink']);
                                $token_cache['tokens'][$current_nav_page + 1] = $next_token;
                                $current_nav_token = $next_token;
                            } else {
                                if ($current_nav_page < $target_page) {
                                    throw new Exception('Cannot navigate to target page - reached end of results.');
                                }
                                $current_nav_token = null;
                            }
                        } else {
                            throw new Exception('Unable to navigate to target page - missing pagination token.');
                        }
                    }
                }
                
                // Now we should have the token for the target page
                $token_for_page = isset($token_cache['tokens'][$target_page]) ? $token_cache['tokens'][$target_page] : null;
            }
            
            // Fetch the target page
            if ($target_page === 0) {
                // Page 0 - no token needed
                $query_params = ['$top' => $page_size];
                
                if (!empty($search)) {
                    $sanitized_search = addslashes($search);
                    if ($search_mode === 'contains') {
                        $query_params['$filter'] = "contains(displayName,'$sanitized_search') or contains(mail,'$sanitized_search') or contains(userPrincipalName,'$sanitized_search')";
                    } else {
                        $query_params['$filter'] = "startswith(displayName,'$sanitized_search') or startswith(mail,'$sanitized_search') or startswith(userPrincipalName,'$sanitized_search')";
                    }
                } else {
                    $query_params['$orderby'] = 'displayName';
                }
                
                $headers = [];
                if (!empty($search)) {
                    $headers['ConsistencyLevel'] = 'eventual';
                }
                
                if (!empty($headers)) {
                    $response = $graph_client->get('v1.0/users?' . http_build_query($query_params), [
                        'headers' => $headers
                    ]);
                } else {
                    $response = $graph_client->get('v1.0/users?' . http_build_query($query_params));
                }
            } else {
                // Use $skiptoken to fetch the page
                $request_url = $build_request_with_skiptoken(
                    'v1.0/users',
                    $token_for_page,
                    $page_size,
                    $search,
                    $search_mode
                );
                
                $headers = [];
                if (!empty($search)) {
                    $headers['ConsistencyLevel'] = 'eventual';
                }
                
                if (!empty($headers)) {
                    $response = $graph_client->get($request_url, ['headers' => $headers]);
                } else {
                    $response = $graph_client->get($request_url);
                }
            }
            
            // Update cache timestamp
            $token_cache['last_updated'] = time();
            
        } else if(!empty($next_link)){
            // Use the next_link URL directly - it already contains all query parameters
            $response = $graph_client->get($next_link);
        } else {
            $query_params = [
                '$top' => $page_size
            ];
            
            // When using $filter with 'or' conditions, $orderby is not supported by Microsoft Graph API
            // So we only add $orderby when there's no search filter
            if(!empty($search)){
                // Sanitize search term to prevent injection
                $sanitized_search = addslashes($search);
                
                if($search_mode === 'contains'){
                    $query_params['$filter'] = "contains(displayName,'$sanitized_search') or contains(mail,'$sanitized_search') or contains(userPrincipalName,'$sanitized_search')";
                } else {
                    // Default to startswith
                    $query_params['$filter'] = "startswith(displayName,'$sanitized_search') or startswith(mail,'$sanitized_search') or startswith(userPrincipalName,'$sanitized_search')";
                }
            } else {
                $query_params['$orderby'] = 'displayName';
            }
            
            // Get users from Microsoft Graph
            // ConsistencyLevel: eventual header is required when using $filter with search operations
            $headers = [];
            if(!empty($search)){
                $headers['ConsistencyLevel'] = 'eventual';
            }
            
            if(!empty($headers)){
                $response = $graph_client->get('v1.0/users?' . http_build_query($query_params), [
                    'headers' => $headers
                ]);
            } else {
                $response = $graph_client->get('v1.0/users?' . http_build_query($query_params));
            }
        }
        
        $data = $response->getBody();
        $users_data = json_decode($data, true);
        
        // Format users for frontend
        $users = [];
        if(isset($users_data['value'])){
            foreach($users_data['value'] as $user){
                $users[] = [
                    'user_id' => $user['id'],
                    'name' => $user['displayName'],
                    'email' => $user['mail'],
                    'user_id_aad' => $user['id'],
                    'displayName' => $user['displayName'],
                    'userPrincipalName' => $user['userPrincipalName'],
                    'givenName' => $user['givenName'] ?? '',
                    'surname' => $user['surname'] ?? '',
                    'jobTitle' => $user['jobTitle'] ?? '',
                    'department' => $user['department'] ?? '',
                    'officeLocation' => $user['officeLocation'] ?? ''
                ];
            }
        }
        
        // Extract next_link if available
        $next_link_result = isset($users_data['@odata.nextLink']) ? $users_data['@odata.nextLink'] : null;
        $has_more = !empty($next_link_result);
        
        // Update token cache with the next page token if we used page_number
        if ($page_number !== null && $next_link_result !== null) {
            $next_page = $page_number + 1;
            $next_token = $extract_skiptoken($next_link_result);
            $token_cache['tokens'][$next_page] = $next_token;
            $token_cache['last_updated'] = time();
        } else if (empty($next_link) && $page_number === null && $next_link_result !== null) {
            // If we fetched page 0 without page_number, store token for page 1
            $next_token = $extract_skiptoken($next_link_result);
            $token_cache['tokens'][1] = $next_token;
            $token_cache['last_updated'] = time();
        }
        
        $result = [
            'status' => 'success',
            'data' => [
                'users' => $users,
                'nextLink' => $next_link_result,
                'hasMore' => $has_more,
                'pageSize' => $page_size,
                'totalFetched' => count($users)
            ]
        ];
        
        echo json_encode($result);
        exit;
        
    } catch (Exception $e) {
        echo json_encode(['status' => 'failed', 'message' => $e->getMessage()]);
        exit;
    }
}

/**
 * AJAX - Get user count from Entra ID (Formerly Azure AD)
 * 
 * @param string 'client_id' -- OAuth client ID
 * @param string 'search' -- search term for filtering users
 * @param string 'search_mode' -- search mode: 'startswith' or 'contains' (default: 'startswith')
 * 
 * @echo json array -- [status, data with count number]
 */
function get_usercount_AAD(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    $search = grab_request_var('search', '');
    $search_mode = grab_request_var('search_mode', 'startswith');
    
    // Validate search_mode
    if (!in_array($search_mode, ['startswith', 'contains'])) {
        $search_mode = 'startswith';
    }
    
    if(empty($client_id)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID is required')]);
        exit;
    }
    
    try {
        // Get OAuth credentials
        $credentials = get_oauth_credentials($client_id);
        if(!$credentials){
            echo json_encode(['status' => 'failed', 'message' => _('OAuth credentials not found')]);
            exit;
        }
        
        // Create Microsoft Graph API client
        $graph_client = create_graph_client($credentials);
        
        // Use $count=true to get total count without loading all users
        $query_params = [
            '$count' => 'true',
            '$top' => 1  // Only get 1 user to minimize data transfer
        ];
        
        if(!empty($search)){
            // Sanitize search term to prevent injection
            $sanitized_search = addslashes($search);
            
            if($search_mode === 'contains'){
                $query_params['$filter'] = "contains(displayName,'$sanitized_search') or contains(mail,'$sanitized_search') or contains(userPrincipalName,'$sanitized_search')";
            } else {
                // Default to startswith
                $query_params['$filter'] = "startswith(displayName,'$sanitized_search') or startswith(mail,'$sanitized_search') or startswith(userPrincipalName,'$sanitized_search')";
            }
        }
        
        $headers = [
            'ConsistencyLevel' => 'eventual'
        ];
        
        $response = $graph_client->get('v1.0/users?' . http_build_query($query_params), [
            'headers' => $headers
        ]);
        $data = $response->getBody();
        $users_data = json_decode($data, true);
        
        $total_count = 0;
        if(isset($users_data['@odata.count'])){
            $total_count = $users_data['@odata.count'];
        } else {
            // Fallback: if count is not available, estimate from returned data
            if(isset($users_data['value'])) {
                $returned_count = count($users_data['value']);
                if(isset($users_data['@odata.nextLink'])) {
                    // If there's a nextLink, this is just the first page
                    $total_count = $returned_count + 1; // At least this many, likely more
                } else {
                    $total_count = $returned_count;
                }
            }
        }
        
        echo json_encode(['status' => 'success', 'data' => $total_count]);
        exit;
        
    } catch (Exception $e) {
        echo json_encode(['status' => 'failed', 'message' => $e->getMessage()]);
        exit;
    }
}

/**
 * AJAX - Get Entra ID (Formerly Azure AD) groups
 * 
 * @param string 'client_id' -- OAuth client ID
 * @param int 'page' -- legacy 1-based page number for pagination
 * @param int 'page_number' -- 0-based page number for pagination
 * @param int 'page_size' -- number of items per page
 * @param string 'search' -- search term for filtering groups
 * @param string 'search_mode' -- search mode: 'startswith' or 'contains' (default: 'startswith')
 * 
 * @echo json array -- [status, data with groups array]
 */
function get_groups_AAD(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    $page = grab_request_var('page', 1); // Legacy 1-based page
    $page_number = grab_request_var('page_number', null); // 0-based page number
    $page_size = grab_request_var('page_size', 100);
    $search = grab_request_var('search', '');
    $search_mode = grab_request_var('search_mode', 'startswith');
    
    // Get filter parameters
    $group_types_raw = grab_request_var('group_types', '');
    $member_count_min = grab_request_var('member_count_min', null);
    $member_count_max = grab_request_var('member_count_max', null);
    $security_enabled = grab_request_var('security_enabled', null);
    $mail_enabled = grab_request_var('mail_enabled', null);
    $sort_by = grab_request_var('sort_by', 'name');
    $sort_order = grab_request_var('sort_order', 'asc');
    
    // Parse group_types if provided
    $group_types = [];
    if (!empty($group_types_raw)) {
        $group_types = json_decode($group_types_raw, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            $group_types = [];
        }
    }
    
    // Validate search_mode
    if (!in_array($search_mode, ['startswith', 'contains'])) {
        $search_mode = 'startswith';
    }
    
    // Validate sort parameters
    if (!in_array($sort_by, ['name', 'memberCount'])) {
        $sort_by = 'name';
    }
    if (!in_array($sort_order, ['asc', 'desc'])) {
        $sort_order = 'asc';
    }
    
    // Normalize page size
    $page_size = max(1, intval($page_size));
    
    // Convert legacy page (1-based) to page_number (0-based) if needed
    if ($page_number === null) {
        $page_number = max(0, intval($page) - 1);
    } else {
        $page_number = intval($page_number);
        if ($page_number < 0) {
            $page_number = 0;
        }
    }
    
    if(empty($client_id)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID is required')]);
        exit;
    }
    
    try {
        $credentials = get_oauth_credentials($client_id);
        if(!$credentials){
            echo json_encode(['status' => 'failed', 'message' => _('OAuth credentials not found')]);
            exit;
        }
        $graph_client = create_graph_client($credentials);
        
        // ConsistencyLevel: eventual is required for $search and for advanced $filter (e.g. groupTypes/any)
        $has_search = !empty($search);
        $has_filter = !empty($group_types) || ($security_enabled !== null && $security_enabled !== '') || ($mail_enabled !== null && $mail_enabled !== '');
        $headers = [];
        if ($has_search || $has_filter) {
            $headers['ConsistencyLevel'] = 'eventual';
        }
        
        // Initialize session token cache if not exists
        if (!isset($_SESSION['sso_group_pagination_tokens'])) {
            $_SESSION['sso_group_pagination_tokens'] = [];
        }
        
        $need_member_count = ($member_count_min !== null || $member_count_max !== null || $sort_by === 'memberCount');
        
        // Create cache key for this query (client_id + search + search_mode + filters)
        $filter_key = json_encode([
            'group_types' => $group_types,
            'security_enabled' => $security_enabled,
            'mail_enabled' => $mail_enabled,
            'sort_by' => $sort_by,
            'sort_order' => $sort_order
        ]);
        $cache_key = md5($client_id . '|' . $search . '|' . $search_mode . '|' . $filter_key);
        
        // Initialize cache entry if not exists
        if (!isset($_SESSION['sso_group_pagination_tokens'][$cache_key])) {
            $_SESSION['sso_group_pagination_tokens'][$cache_key] = [
                'tokens' => [0 => null], // Page 0 always uses null (no token)
                'last_updated' => time()
            ];
        }
        
        $token_cache = &$_SESSION['sso_group_pagination_tokens'][$cache_key];
        
        // Helper function to extract $skiptoken from a nextLink URL
        $extract_skiptoken = function($next_link_url) {
            if (empty($next_link_url)) {
                return null;
            }
            $parsed_url = parse_url($next_link_url);
            if (isset($parsed_url['query'])) {
                parse_str($parsed_url['query'], $query_params);
                if (isset($query_params['$skiptoken'])) {
                    return $query_params['$skiptoken'];
                }
            }
            return null;
        };
        
        // Helper function to build a request URL with $skiptoken and filters
        $build_request_with_skiptoken = function($base_url, $skiptoken, $page_size, $search, $search_mode, $group_types, $security_enabled, $mail_enabled, $sort_by, $sort_order, $need_member_count = false) {
            $query_params = ['$top' => $page_size];
            if ($need_member_count) {
                $query_params['$select'] = 'id,displayName,description,mail,mailEnabled,securityEnabled,groupTypes,memberCount';
            }
            
            // Build $filter query string
            $filter_parts = [];
            
            // Group types filter
            if (!empty($group_types)) {
                $type_filters = [];
                foreach ($group_types as $type) {
                    if ($type === 'Unified') {
                        $type_filters[] = "groupTypes/any(c:c eq 'Unified')";
                    } else if ($type === 'DynamicMembership') {
                        $type_filters[] = "groupTypes/any(c:c eq 'DynamicMembership')";
                    }
                }
                if (!empty($type_filters)) {
                    $filter_parts[] = '(' . implode(' or ', $type_filters) . ')';
                }
            }
            
            // Security enabled filter
            if ($security_enabled !== null && $security_enabled !== '') {
                $sec_value = ($security_enabled === 'true' || $security_enabled === true) ? 'true' : 'false';
                $filter_parts[] = "securityEnabled eq $sec_value";
            }
            
            // Mail enabled filter
            if ($mail_enabled !== null && $mail_enabled !== '') {
                $mail_value = ($mail_enabled === 'true' || $mail_enabled === true) ? 'true' : 'false';
                $filter_parts[] = "mailEnabled eq $mail_value";
            }
            
            // Apply $filter if we have any filter parts
            if (!empty($filter_parts)) {
                $query_params['$filter'] = implode(' and ', $filter_parts);
            }
            
            // Apply search (note: $search and $filter can't be used together in some cases)
            // If we have filters, we can't use $search, so skip search in that case
            if (!empty($search) && empty($filter_parts)) {
                $sanitized_search = addslashes($search);
                // Microsoft Graph groups do NOT support $filter on displayName/description
                // Use $search instead with ConsistencyLevel: eventual header
                $query_params['$search'] = '"displayName:' . $sanitized_search . '"';
            }
            
            // Sorting - NOTE: Graph API does NOT support $orderby with certain $filter queries
            // Specifically, groupTypes/any() filters don't support sorting
            // So we only add $orderby when there are NO filters, and sort in PHP otherwise
            if (empty($filter_parts)) {
                // Only apply server-side sorting when no filters are present
                if ($sort_by === 'name') {
                    $query_params['$orderby'] = 'displayName ' . ($sort_order === 'desc' ? 'desc' : 'asc');
                } else {
                    // For memberCount, we still sort by name server-side, then re-sort in PHP
                    $query_params['$orderby'] = 'displayName';
                }
            }
            // When filters are present, sorting will be done in PHP after fetching
            
            if ($skiptoken !== null) {
                $query_params['$skiptoken'] = $skiptoken;
            }
            
            return $base_url . '?' . http_build_query($query_params);
        };
        
        $groups_data = null;
        $request_url = null;
        
        // Page number-based navigation
        $target_page = $page_number;
        $token_for_page = isset($token_cache['tokens'][$target_page]) ? $token_cache['tokens'][$target_page] : null;
        
        // If we don't have a token, navigate sequentially from closest cached page
        if ($token_for_page === null && $target_page > 0) {
            $start_page = 0;
            $start_token = null;
            
            for ($i = $target_page - 1; $i >= 0; $i--) {
                if (isset($token_cache['tokens'][$i])) {
                    $start_page = $i;
                    $start_token = $token_cache['tokens'][$i];
                    break;
                }
            }
            
            $current_nav_page = $start_page;
            $current_nav_token = $start_token;
            
            while ($current_nav_page < $target_page) {
                if ($current_nav_page === 0 && $current_nav_token === null) {
                    $query_params = ['$top' => $page_size];
                    
                    // Build filters
                    $filter_parts = [];
                    
                    if (!empty($group_types)) {
                        $type_filters = [];
                        foreach ($group_types as $type) {
                            if ($type === 'Unified') {
                                $type_filters[] = "groupTypes/any(c:c eq 'Unified')";
                            } else if ($type === 'DynamicMembership') {
                                $type_filters[] = "groupTypes/any(c:c eq 'DynamicMembership')";
                            }
                        }
                        if (!empty($type_filters)) {
                            $filter_parts[] = '(' . implode(' or ', $type_filters) . ')';
                        }
                    }
                    
                    if ($security_enabled !== null && $security_enabled !== '') {
                        $sec_value = ($security_enabled === 'true' || $security_enabled === true) ? 'true' : 'false';
                        $filter_parts[] = "securityEnabled eq $sec_value";
                    }
                    
                    if ($mail_enabled !== null && $mail_enabled !== '') {
                        $mail_value = ($mail_enabled === 'true' || $mail_enabled === true) ? 'true' : 'false';
                        $filter_parts[] = "mailEnabled eq $mail_value";
                    }
                    
                    if (!empty($filter_parts)) {
                        $query_params['$filter'] = implode(' and ', $filter_parts);
                    }
                    
                    if (!empty($search) && empty($filter_parts)) {
                        $sanitized_search = addslashes($search);
                        $query_params['$search'] = '"displayName:' . $sanitized_search . '"';
                    }
                    
                    // Sorting - only when no filters (Graph API limitation with groupTypes/any())
                    if (empty($filter_parts)) {
                        if ($sort_by === 'name') {
                            $query_params['$orderby'] = 'displayName ' . ($sort_order === 'desc' ? 'desc' : 'asc');
                        } else {
                            $query_params['$orderby'] = 'displayName';
                        }
                    }
                    if ($need_member_count) {
                        $query_params['$select'] = 'id,displayName,description,mail,mailEnabled,securityEnabled,groupTypes,memberCount';
                    }
                    
                    if (!empty($headers)) {
                        $nav_response = $graph_client->get('v1.0/groups?' . http_build_query($query_params), [
                            'headers' => $headers
                        ]);
                    } else {
                        $nav_response = $graph_client->get('v1.0/groups?' . http_build_query($query_params));
                    }
                    $nav_data = $nav_response->getBody();
                    $nav_groups_data = json_decode($nav_data, true);
                    
                    $token_cache['tokens'][0] = null;
                    
                    if (isset($nav_groups_data['@odata.nextLink'])) {
                        $next_token = $extract_skiptoken($nav_groups_data['@odata.nextLink']);
                        $token_cache['tokens'][1] = $next_token;
                        $current_nav_token = $next_token;
                    } else {
                        throw new Exception('Cannot navigate to target page - reached end of results.');
                    }
                    $current_nav_page = 1;
                } else {
                    if (isset($token_cache['tokens'][$current_nav_page + 1])) {
                        $current_nav_page++;
                        $current_nav_token = $token_cache['tokens'][$current_nav_page];
                    } else if ($current_nav_token !== null) {
                        $request_url = $build_request_with_skiptoken(
                            'v1.0/groups',
                            $current_nav_token,
                            $page_size,
                            $search,
                            $search_mode,
                            $group_types,
                            $security_enabled,
                            $mail_enabled,
                            $sort_by,
                            $sort_order,
                            $need_member_count
                        );
                        
                        if (!empty($headers)) {
                            $nav_response = $graph_client->get($request_url, ['headers' => $headers]);
                        } else {
                            $nav_response = $graph_client->get($request_url);
                        }
                        $nav_data = $nav_response->getBody();
                        $nav_groups_data = json_decode($nav_data, true);
                        
                        $current_nav_page++;
                        $token_cache['tokens'][$current_nav_page] = $current_nav_token;
                        
                        if (isset($nav_groups_data['@odata.nextLink'])) {
                            $next_token = $extract_skiptoken($nav_groups_data['@odata.nextLink']);
                            $token_cache['tokens'][$current_nav_page + 1] = $next_token;
                            $current_nav_token = $next_token;
                        } else {
                            $current_nav_token = null;
                        }
                    } else {
                        throw new Exception('Cannot navigate to target page - missing pagination token.');
                    }
                }
            }
            
            $token_for_page = $current_nav_token;
        }
        
        // Build request for target page
        $request_url = $build_request_with_skiptoken(
            'v1.0/groups',
            $token_for_page,
            $page_size,
            $search,
            $search_mode,
            $group_types,
            $security_enabled,
            $mail_enabled,
            $sort_by,
            $sort_order,
            $need_member_count
        );
        
        if (!empty($headers)) {
            $response = $graph_client->get($request_url, ['headers' => $headers]);
        } else {
            $response = $graph_client->get($request_url);
        }
        $data = $response->getBody();
        $groups_data = json_decode($data, true);
        
        // Format groups for frontend with size validation
        $groups = [];
        if(isset($groups_data['value'])){
            $page_groups = $groups_data['value'];
            $ids_needing_count = [];
            $member_counts_by_id = [];
            foreach ($page_groups as $group) {
                if (array_key_exists('memberCount', $group) && is_numeric($group['memberCount'])) {
                    $member_counts_by_id[$group['id']] = (int) $group['memberCount'];
                } else {
                    $ids_needing_count[] = $group['id'];
                }
            }
            if (!empty($ids_needing_count)) {
                foreach (array_chunk($ids_needing_count, 20) as $chunk) {
                    $member_counts_by_id = array_merge($member_counts_by_id, graph_batch_member_counts($graph_client, $chunk));
                }
            }
            foreach ($page_groups as $group) {
                $member_count = isset($member_counts_by_id[$group['id']]) ? (int) $member_counts_by_id[$group['id']] : 0;
                $size_validation = validate_group_size($member_count);
                $groups[] = [
                    'id' => $group['id'],
                    'displayName' => $group['displayName'],
                    'description' => $group['description'] ?? '',
                    'mail' => $group['mail'] ?? '',
                    'mailEnabled' => $group['mailEnabled'] ?? false,
                    'securityEnabled' => $group['securityEnabled'] ?? false,
                    'groupTypes' => $group['groupTypes'] ?? [],
                    'member_count' => $member_count,
                    'size_validation' => $size_validation
                ];
            }
        }
        
        // POST-PROCESSING: Member count filtering (Graph API doesn't support this natively)
        if ($member_count_min !== null || $member_count_max !== null) {
            $filtered_groups = [];
            foreach ($groups as $group) {
                $count = $group['member_count'];
                
                // Apply minimum filter
                if ($member_count_min !== null && $count < intval($member_count_min)) {
                    continue;
                }
                
                // Apply maximum filter
                if ($member_count_max !== null && $count > intval($member_count_max)) {
                    continue;
                }
                
                $filtered_groups[] = $group;
            }
            $groups = $filtered_groups;
        }
        
        // POST-PROCESSING: Sorting (when filters are present, we must sort in PHP)
        // Check if we need to sort (either filters are present OR sorting by member count)
        $has_filters = !empty($group_types) || $security_enabled !== null || $mail_enabled !== null;
        
        if ($sort_by === 'memberCount' || ($sort_by === 'name' && $has_filters)) {
            usort($groups, function($a, $b) use ($sort_by, $sort_order) {
                if ($sort_by === 'memberCount') {
                    // Sort by member count
                    $countA = $a['member_count'] ?? 0;
                    $countB = $b['member_count'] ?? 0;
                    
                    if ($sort_order === 'desc') {
                        return $countB - $countA;
                    } else {
                        return $countA - $countB;
                    }
                } else {
                    // Sort by name
                    $nameA = strtolower($a['displayName'] ?? '');
                    $nameB = strtolower($b['displayName'] ?? '');
                    
                    if ($sort_order === 'desc') {
                        return strcmp($nameB, $nameA);
                    } else {
                        return strcmp($nameA, $nameB);
                    }
                }
            });
        }
        
        // Extract next_link if available
        $next_link_result = isset($groups_data['@odata.nextLink']) ? $groups_data['@odata.nextLink'] : null;
        $has_more = !empty($next_link_result);
        
        // Update token cache with the next page token
        if ($next_link_result !== null) {
            $next_token = $extract_skiptoken($next_link_result);
            $token_cache['tokens'][$page_number + 1] = $next_token;
            $token_cache['last_updated'] = time();
        }
        
        $result = [
            'status' => 'success',
            'data' => [
                'groups' => $groups,
                'nextLink' => $next_link_result,
                'hasMore' => $has_more,
                'pageSize' => $page_size,
                'totalFetched' => count($groups)
            ]
        ];
        
        echo json_encode($result);
        exit;
        
    } catch (Exception $e) {
        echo json_encode(['status' => 'failed', 'message' => $e->getMessage()]);
        exit;
    }
}

/**
 * Fetch member counts for up to 20 groups via Microsoft Graph JSON batching.
 * Reduces round trips when member count filter is used (batch limit is 20).
 *
 * @param object $graph_client -- Guzzle HTTP client (from create_graph_client)
 * @param array $group_ids -- Array of group IDs (max 20 per Graph batch limit)
 * @return array Map of group_id => member count (int). Failed/429 items get 0.
 */
function graph_batch_member_counts($graph_client, $group_ids)
{
    $group_ids = array_slice($group_ids, 0, 20);
    if (empty($group_ids)) {
        return [];
    }
    $requests = [];
    foreach ($group_ids as $i => $id) {
        $requests[] = [
            'id' => (string) $i,
            'method' => 'GET',
            'url' => '/groups/' . $id . '/members/$count',
            'headers' => ['ConsistencyLevel' => 'eventual']
        ];
    }
    $batch_body = ['requests' => $requests];
    try {
        $response = $graph_client->post('v1.0/$batch', [
            'json' => $batch_body
        ]);
    } catch (Exception $e) {
        log_sso('graph_batch_member_counts: batch request failed: ' . $e->getMessage());
        return array_fill_keys($group_ids, 0);
    }
    $data = json_decode($response->getBody(), true);
    $result = array_fill_keys($group_ids, 0);
    if (empty($data['responses'])) {
        return $result;
    }
    foreach ($data['responses'] as $r) {
        $idx = isset($r['id']) ? intval($r['id']) : -1;
        if ($idx < 0 || $idx >= count($group_ids)) {
            continue;
        }
        $gid = $group_ids[$idx];
        if (isset($r['status']) && $r['status'] === 200 && array_key_exists('body', $r)) {
            $result[$gid] = (int) $r['body'];
        }
    }
    return $result;
}

/**
 * AJAX - Get Entra ID (Formerly Azure AD) group count
 * 
 * @param string 'client_id' -- OAuth client ID
 * @param string 'search' -- search term for filtering groups
 * @param string 'search_mode' -- search mode: 'startswith' or 'contains' (default: 'startswith')
 * 
 * @echo json array -- [status, data with count number]
 */
function get_groupcount_AAD(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    $search = grab_request_var('search', '');
    $search_mode = grab_request_var('search_mode', 'startswith');
    
    // Get filter parameters
    $group_types_raw = grab_request_var('group_types', '');
    $member_count_min = grab_request_var('member_count_min', null);
    $member_count_max = grab_request_var('member_count_max', null);
    $security_enabled = grab_request_var('security_enabled', null);
    $mail_enabled = grab_request_var('mail_enabled', null);
    
    // Parse group_types if provided
    $group_types = [];
    if (!empty($group_types_raw)) {
        $group_types = json_decode($group_types_raw, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            $group_types = [];
        }
    }
    
    // Validate search_mode
    if (!in_array($search_mode, ['startswith', 'contains'])) {
        $search_mode = 'startswith';
    }
    
    
    if(empty($client_id)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID is required')]);
        exit;
    }
    
    try {
        $credentials = get_oauth_credentials($client_id);
        if(!$credentials){
            echo json_encode(['status' => 'failed', 'message' => _('OAuth credentials not found')]);
            exit;
        }
        
        $graph_client = create_graph_client($credentials);
        
        // Single quick count request: $count=true and $top=1. Do NOT fetch all groups.
        $query_params = [
            '$count' => 'true',
            '$top' => 1
        ];
        
        // Build $filter query string for Graph API supported filters
        $filter_parts = [];
        
        // Group types filter
        if (!empty($group_types)) {
            $type_filters = [];
            foreach ($group_types as $type) {
                if ($type === 'Unified') {
                    $type_filters[] = "groupTypes/any(c:c eq 'Unified')";
                } else if ($type === 'DynamicMembership') {
                    $type_filters[] = "groupTypes/any(c:c eq 'DynamicMembership')";
                }
            }
            if (!empty($type_filters)) {
                $filter_parts[] = '(' . implode(' or ', $type_filters) . ')';
            }
        }
        
        // Security enabled filter
        if ($security_enabled !== null && $security_enabled !== '') {
            $sec_value = ($security_enabled === 'true' || $security_enabled === true) ? 'true' : 'false';
            $filter_parts[] = "securityEnabled eq $sec_value";
        }
        
        // Mail enabled filter
        if ($mail_enabled !== null && $mail_enabled !== '') {
            $mail_value = ($mail_enabled === 'true' || $mail_enabled === true) ? 'true' : 'false';
            $filter_parts[] = "mailEnabled eq $mail_value";
        }
        
        // Apply $filter if we have any filter parts
        if (!empty($filter_parts)) {
            $query_params['$filter'] = implode(' and ', $filter_parts);
        }
        
        // Apply search (note: $search and $filter can't be used together easily)
        // If we have filters, we can't use $search, so skip search in that case
        if(!empty($search) && empty($filter_parts)){
            $sanitized_search = addslashes($search);
            // Microsoft Graph groups do NOT support $filter on displayName/description
            // Use $search instead with ConsistencyLevel: eventual header
            $query_params['$search'] = '"displayName:' . $sanitized_search . '"';
        }
        
        $headers = [
            'ConsistencyLevel' => 'eventual'
        ];
        
        $response = $graph_client->get('v1.0/groups?' . http_build_query($query_params), [
            'headers' => $headers
        ]);
        $data = $response->getBody();
        $groups_data = json_decode($data, true);
        
        $total_count = 0;
        if (isset($groups_data['@odata.count'])) {
            $total_count = (int) $groups_data['@odata.count'];
        } elseif (isset($groups_data['value'])) {
            $returned_count = count($groups_data['value']);
            if (isset($groups_data['@odata.nextLink'])) {
                $total_count = $returned_count + 1;
            } else {
                $total_count = $returned_count;
            }
        }

        // When member count filter was requested, Graph cannot filter by it in one call; count is total (other filters only).
        $member_count_filter_requested = ($member_count_min !== null || $member_count_max !== null);
        if ($member_count_filter_requested) {
            echo json_encode([
                'status' => 'success',
                'data' => [
                    'count' => $total_count,
                    'isApproximate' => true,
                ],
            ]);
        } else {
            echo json_encode(['status' => 'success', 'data' => $total_count]);
        }
        exit;
        
    } catch (Exception $e) {
        echo json_encode(['status' => 'failed', 'message' => $e->getMessage()]);
        exit;
    }
}

/**
 * AJAX - Get Entra ID (Formerly Azure AD) groups by specific type
 * 
 * @param string 'client_id' -- OAuth client ID
 * @param string 'group_type' -- Type of group to filter (microsoft365, security, distribution, dynamic, cloud, onpremises)
 * @param int 'page' -- page number for pagination
 * @param int 'page_size' -- number of items per page
 * @param string 'search' -- search term for filtering groups
 * 
 * @echo json array -- [status, data with groups array]
 */
function get_groups_AAD_by_type(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    $group_type = grab_request_var('group_type', '');
    $page = grab_request_var('page', 1);
    $page_size = grab_request_var('page_size', 100);
    $search = grab_request_var('search', '');
    
    if(empty($client_id) || empty($group_type)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID and group type are required')]);
        exit;
    }
    
    try {
        // Get OAuth credentials
        $credentials = get_oauth_credentials($client_id);
        if(!$credentials){
            echo json_encode(['status' => 'failed', 'message' => _('OAuth credentials not found')]);
            exit;
        }
        
        // Create Microsoft Graph API client
        $graph_client = create_graph_client($credentials);
        
        // Build query parameters with type-specific filters
        $query_params = [
            '$top' => $page_size,
            '$orderby' => 'displayName'
        ];
        
        // Add type-specific filters based on Microsoft Graph API
        $type_filters = [];
        switch($group_type) {
            case 'microsoft365':
                $type_filters[] = "groupTypes/any(c:c eq 'Unified')";
                break;
            case 'security':
                $type_filters[] = "securityEnabled eq true";
                break;
            case 'distribution':
                $type_filters[] = "mailEnabled eq true";
                break;
            case 'dynamic':
                $type_filters[] = "groupTypes/any(c:c eq 'DynamicMembership')";
                break;
            case 'cloud':
                $type_filters[] = "onPremisesSyncEnabled eq null";
                break;
            case 'onpremises':
                $type_filters[] = "onPremisesSyncEnabled eq true";
                break;
            default:
                echo json_encode(['status' => 'failed', 'message' => _('Invalid group type')]);
                exit;
        }
        
        // Combine type filter with search filter
        $all_filters = [];
        if(!empty($type_filters)) {
            $all_filters[] = '(' . implode(' and ', $type_filters) . ')';
        }
        if(!empty($search)){
            $all_filters[] = "(startswith(displayName,'$search') or startswith(description,'$search'))";
        }
        
        if(!empty($all_filters)) {
            $query_params['$filter'] = implode(' and ', $all_filters);
        }
        $query_params['$select'] = 'id,displayName,description,mail,mailEnabled,securityEnabled,groupTypes,memberCount';
        
        // Get groups from Microsoft Graph
        $response = $graph_client->get('v1.0/groups?' . http_build_query($query_params));
        $data = $response->getBody();
        $groups_data = json_decode($data, true);
        
        // Format groups for frontend with size validation (use memberCount from list or batch)
        $groups = [];
        if(isset($groups_data['value'])){
            $page_groups = $groups_data['value'];
            $ids_needing_count = [];
            $member_counts_by_id = [];
            foreach ($page_groups as $group) {
                if (array_key_exists('memberCount', $group) && is_numeric($group['memberCount'])) {
                    $member_counts_by_id[$group['id']] = (int) $group['memberCount'];
                } else {
                    $ids_needing_count[] = $group['id'];
                }
            }
            if (!empty($ids_needing_count)) {
                foreach (array_chunk($ids_needing_count, 20) as $chunk) {
                    $member_counts_by_id = array_merge($member_counts_by_id, graph_batch_member_counts($graph_client, $chunk));
                }
            }
            foreach ($page_groups as $group) {
                $member_count = isset($member_counts_by_id[$group['id']]) ? (int) $member_counts_by_id[$group['id']] : 0;
                $size_validation = validate_group_size($member_count);
                $groups[] = [
                    'id' => $group['id'],
                    'displayName' => $group['displayName'],
                    'description' => $group['description'] ?? '',
                    'mail' => $group['mail'] ?? '',
                    'mailEnabled' => $group['mailEnabled'] ?? false,
                    'securityEnabled' => $group['securityEnabled'] ?? false,
                    'groupTypes' => $group['groupTypes'] ?? [],
                    'member_count' => $member_count,
                    'size_validation' => $size_validation,
                    'is_security_group' => $group['securityEnabled'] ?? false,
                    'is_distribution_group' => $group['mailEnabled'] ?? false
                ];
            }
        }
        
        $result = [
            'status' => 'success',
            'data' => [
                'groups' => $groups,
                'total' => count($groups),
                'page' => 1,
                'page_size' => $page_size,
                'group_type' => $group_type,
                'note' => 'Microsoft Graph API does not support $skip parameter. Showing first ' . $page_size . ' results for ' . $group_type . ' groups.'
            ]
        ];
        
        echo json_encode($result);
        exit;
        
    } catch (Exception $e) {
        echo json_encode(['status' => 'failed', 'message' => $e->getMessage()]);
        exit;
    }
}

/**
 * AJAX - Get Entra ID (Formerly Azure AD) groups by multiple types
 * 
 * @param string 'client_id' -- OAuth client ID
 * @param array 'group_types' -- Array of group types to filter
 * @param int 'page' -- page number for pagination
 * @param int 'page_size' -- number of items per page
 * @param string 'search' -- search term for filtering groups
 * 
 * @echo json array -- [status, data with groups array]
 */
function get_groups_AAD_by_types(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    $group_types_raw = grab_request_var('group_types', []);
    $page = grab_request_var('page', 1);
    $page_size = grab_request_var('page_size', 100);
    $search = grab_request_var('search', '');
    
    // Decode JSON string if it's a string
    if (is_string($group_types_raw)) {
        $group_types = json_decode($group_types_raw, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            echo json_encode(['status' => 'failed', 'message' => _('Invalid group types format')]);
            exit;
        }
    } else {
        $group_types = $group_types_raw;
    }
    
    if(empty($client_id) || empty($group_types)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID and group types are required')]);
        exit;
    }
    
    try {
        // Get OAuth credentials
        $credentials = get_oauth_credentials($client_id);
        if(!$credentials){
            echo json_encode(['status' => 'failed', 'message' => _('OAuth credentials not found')]);
            exit;
        }
        
        // Create Microsoft Graph API client
        $graph_client = create_graph_client($credentials);
        
        $all_groups = [];
        $group_map = []; // To deduplicate groups
        
        // Get groups for each type
        foreach($group_types as $group_type) {
            $query_params = [
                '$top' => $page_size,
                '$orderby' => 'displayName'
            ];
            
            // Add type-specific filters
            $type_filters = [];
            switch($group_type) {
                case 'Microsoft365':
                    $type_filters[] = "groupTypes/any(c:c eq 'Unified')";
                    break;
                case 'Security':
                    $type_filters[] = "securityEnabled eq true";
                    break;
                case 'Distribution':
                    $type_filters[] = "mailEnabled eq true";
                    break;
                case 'Dynamic':
                    $type_filters[] = "groupTypes/any(c:c eq 'DynamicMembership')";
                    break;
                case 'Cloud':
                    $type_filters[] = "onPremisesSyncEnabled eq null";
                    break;
                case 'OnPremises':
                    $type_filters[] = "onPremisesSyncEnabled eq true";
                    break;
                default:
                    log_sso("Invalid group type: $group_type");
                    break; // Skip invalid types
            }
            
            // Combine type filter with search filter
            $all_filters = [];
            if(!empty($type_filters)) {
                $all_filters[] = '(' . implode(' and ', $type_filters) . ')';
            }
            if(!empty($search)){
                $all_filters[] = "(startswith(displayName,'$search') or startswith(description,'$search'))";
            }
            
            if(!empty($all_filters)) {
                $query_params['$filter'] = implode(' and ', $all_filters);
            }
            $query_params['$select'] = 'id,displayName,description,mail,mailEnabled,securityEnabled,groupTypes,memberCount';
            
            try {
                // Get groups from Microsoft Graph
                $response = $graph_client->get('v1.0/groups?' . http_build_query($query_params));
                $data = $response->getBody();
                $groups_data = json_decode($data, true);
                
                if(isset($groups_data['value'])){
                    $page_groups = [];
                    foreach ($groups_data['value'] as $group) {
                        if (isset($group_map[$group['id']])) {
                            continue;
                        }
                        $page_groups[] = $group;
                    }
                    $ids_needing_count = [];
                    $member_counts_by_id = [];
                    foreach ($page_groups as $group) {
                        if (array_key_exists('memberCount', $group) && is_numeric($group['memberCount'])) {
                            $member_counts_by_id[$group['id']] = (int) $group['memberCount'];
                        } else {
                            $ids_needing_count[] = $group['id'];
                        }
                    }
                    if (!empty($ids_needing_count)) {
                        foreach (array_chunk($ids_needing_count, 20) as $chunk) {
                            $member_counts_by_id = array_merge($member_counts_by_id, graph_batch_member_counts($graph_client, $chunk));
                        }
                    }
                    foreach ($page_groups as $group) {
                        $member_count = isset($member_counts_by_id[$group['id']]) ? (int) $member_counts_by_id[$group['id']] : 0;
                        $size_validation = validate_group_size($member_count);
                        $group_data = [
                            'id' => $group['id'],
                            'displayName' => $group['displayName'],
                            'description' => $group['description'] ?? '',
                            'mail' => $group['mail'] ?? '',
                            'mailEnabled' => $group['mailEnabled'] ?? false,
                            'securityEnabled' => $group['securityEnabled'] ?? false,
                            'groupTypes' => $group['groupTypes'] ?? [],
                            'member_count' => $member_count,
                            'size_validation' => $size_validation,
                            'is_security_group' => $group['securityEnabled'] ?? false,
                            'is_distribution_group' => $group['mailEnabled'] ?? false
                        ];
                        $group_map[$group['id']] = $group_data;
                    }
                }
            } catch (Exception $e) {
                log_sso("Error getting groups for type $group_type: " . $e->getMessage());
                continue; // Continue with other types
            }
        }
        
        $all_groups = array_values($group_map);
        
        $result = [
            'status' => 'success',
            'data' => [
                'groups' => $all_groups,
                'total' => count($all_groups),
                'page' => 1,
                'page_size' => $page_size,
                'group_types' => $group_types,
                'note' => 'Microsoft Graph API does not support $skip parameter. Showing first ' . $page_size . ' results for selected group types.'
            ]
        ];
        
        echo json_encode($result);
        exit;
        
    } catch (Exception $e) {
        echo json_encode(['status' => 'failed', 'message' => $e->getMessage()]);
        exit;
    }
}

/**
 * AJAX - Get users by group IDs
 * 
 * @param string 'client_id' -- OAuth client ID
 * @param array 'group_ids' -- array of group IDs to get users for
 * 
 * @echo json array -- [status, data with users array]
 */
function get_users_by_groups(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    $group_ids_raw = grab_request_var('group_ids', []);
    $page = grab_request_var('page', 1);
    $page_size = min(grab_request_var('page_size', 50), 100); // Cap at 100 users per request
    
    // Decode JSON string if it's a string
    if (is_string($group_ids_raw)) {
        $group_ids = json_decode($group_ids_raw, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            log_sso("JSON decode error for group_ids: " . json_last_error_msg());
            echo json_encode(['status' => 'failed', 'message' => _('Invalid group IDs format')]);
            exit;
        }
    } else {
        $group_ids = $group_ids_raw;
    }
    
    if(empty($client_id) || empty($group_ids)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID and group IDs are required')]);
        exit;
    }
        
    try {
        // Get OAuth credentials
        $credentials = get_oauth_credentials($client_id);
        if(!$credentials){
            echo json_encode(['status' => 'failed', 'message' => _('OAuth credentials not found')]);
            exit;
        }
        
        // Create Microsoft Graph API client
        $graph_client = create_graph_client($credentials);
        
        $all_users = [];
        
        // Get users for each group with proper Graph API pagination
        $group_next_links = []; // Track nextLink for each group
        $group_page_counts = []; // Track current page for each group
        
        foreach($group_ids as $group_id){
            try {
                // For the first page, get the initial results
                if ($page == 1) {
                    $headers = [
                        'ConsistencyLevel' => 'eventual'
                    ];
                    $response = $graph_client->get("v1.0/groups/$group_id/members?\$select=id,displayName,mail,userPrincipalName,givenName,surname,jobTitle,department,officeLocation&\$top=$page_size", [
                        'headers' => $headers
                    ]);
                } else {
                    // For subsequent pages, we need the nextLink from the previous request
                    // TODO: Implement full pagination by storing nextLink tokens in session/database
                    // and using them for subsequent requests
                    // For now, pagination beyond page 1 is not fully supported
                    log_sso("Pagination beyond page 1 requested for group $group_id but not fully implemented. Returning empty results for page $page.");
                    $members_data = ['value' => []];
                    continue;
                }
                
                $data = $response->getBody();
                $members_data = json_decode($data, true);
                
                // Store nextLink for future pagination
                if (isset($members_data['@odata.nextLink'])) {
                    $group_next_links[$group_id] = $members_data['@odata.nextLink'];
                }
                
            } catch (Exception $e) {
                log_sso("Error getting members for group $group_id: " . $e->getMessage());
                continue;
            }
            
            if(isset($members_data['value'])){
                foreach($members_data['value'] as $member){
                    // Only include user objects (not groups)
                    if(isset($member['@odata.type']) && $member['@odata.type'] === '#microsoft.graph.user'){
                        $all_users[] = [
                            'user_id' => $member['id'],
                            'name' => $member['displayName'],
                            'email' => $member['mail'],
                            'user_id_aad' => $member['id'],
                            'displayName' => $member['displayName'],
                            'userPrincipalName' => $member['userPrincipalName'],
                            'givenName' => $member['givenName'] ?? '',
                            'surname' => $member['surname'] ?? '',
                            'jobTitle' => $member['jobTitle'] ?? '',
                            'department' => $member['department'] ?? '',
                            'officeLocation' => $member['officeLocation'] ?? '',
                            'groups' => [$group_id] // Track which group this user came from
                        ];
                    }
                }
            } else {
                log_sso("No 'value' key in response for group $group_id");
            }
        }
        
        // Remove duplicates based on user_id
        $unique_users = [];
        $seen_ids = [];
        foreach($all_users as $user){
            if(!in_array($user['user_id'], $seen_ids)){
                $unique_users[] = $user;
                $seen_ids[] = $user['user_id'];
            } else {
                // Merge groups for duplicate users
                $existing_index = array_search($user['user_id'], $seen_ids);
                if($existing_index !== false){
                    $unique_users[$existing_index]['groups'] = array_unique(array_merge($unique_users[$existing_index]['groups'], $user['groups']));
                }
            }
        }
        
        // Determine if there are more pages by checking for @odata.nextLink
        $has_more = !empty($group_next_links);
        
        $result = [
            'status' => 'success',
            'data' => [
                'users' => $unique_users,
                'pagination' => [
                    'page' => $page,
                    'page_size' => $page_size,
                    'has_more' => $has_more,
                    'loaded_count' => count($unique_users)
                ]
            ]
        ];
        echo json_encode($result);
        exit;
        
    } catch (Exception $e) {
        echo json_encode(['status' => 'failed', 'message' => $e->getMessage()]);
        exit;
    }
}

/**
 * AJAX - Get administrative units (future feature)
 * 
 * @param string 'client_id' -- OAuth client ID
 * 
 * @echo json array -- [status, data with administrative units array]
 */
function get_administrative_units(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    
    if(empty($client_id)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID is required')]);
        exit;
    }
    
    // This is a future feature - return empty for now
    $result = [
        'status' => 'success',
        'data' => [
            'administrative_units' => [],
            'total' => 0
        ]
    ];
    
    echo json_encode($result);
    exit;
}

/**
 * AJAX - Get users by administrative unit (future feature)
 * 
 * @param string 'client_id' -- OAuth client ID
 * @param string 'unit_id' -- administrative unit ID
 * 
 * @echo json array -- [status, data with users array]
 */
function get_users_by_administrative_unit(){
    check_nagios_session_protector();
    
    $client_id = grab_request_var('client_id', '');
    $unit_id = grab_request_var('unit_id', '');
    
    if(empty($client_id) || empty($unit_id)){
        echo json_encode(['status' => 'failed', 'message' => _('Client ID and unit ID are required')]);
        exit;
    }
    
    // This is a future feature - return empty for now
    $result = [
        'status' => 'success',
        'data' => [
            'users' => [],
            'total' => 0
        ]
    ];
    
    echo json_encode($result);
    exit;
}

/**
 * Helper function to create Microsoft Graph API client
 * 
 * @param array $credentials -- OAuth credentials
 * @return object -- Graph API client
 */
function create_graph_client($credentials)
{
    require_once(dirname(__FILE__) . '/../createprovider.php');
    require_once(dirname(__FILE__) . '/../sso-azure-params.php');
    $tenant_data = [
        'client_id'     => $credentials['clientId'],
        'client_secret' => $credentials['clientSecret'],
        'tenant_id'     => isset($credentials['tenantId']) ? $credentials['tenantId'] : '',
    ];
    if (!empty($credentials['azure_environment'])) {
        $tenant_data['azure_environment'] = $credentials['azure_environment'];
    }
    $params = sso_azure_provider_params($tenant_data);
    $provider = getProvider('azure', $params);
    $graph = sso_azure_graph_settings($tenant_data);
    $token = $provider->getAccessToken('client_credentials', ['scope' => $graph['scope']]);

    $http_client = new \GuzzleHttp\Client([
        'base_uri' => $graph['base'] . '/',
        'headers'  => [
            'Authorization' => 'Bearer ' . $token->getToken(),
            'Content-Type'   => 'application/json',
        ],
    ]);

    return $http_client;
}

/**
 * Get OAuth credentials for the given client_id from the stored tenants file.
 *
 * Returns an array in the form:
 * [
 *   'clientId' => ...,
 *   'clientSecret' => ...,
 *   'tenantId' => ...,
 * ]
 *
 * @param string $client_id
 * @return array|false Returns credentials array if found, false otherwise
 */
function get_oauth_credentials($client_id)
{
    // Read and decrypt tenants credentials file
    $tenants_path = '/usr/local/nagiosxi/etc/components/oauth2/providers/AAD/tenants';
    if (!file_exists($tenants_path)) {
        return false;
    }

    $encrypted = file_get_contents($tenants_path);
    $json = decrypt_data($encrypted);
    if (!$json) {
        return false;
    }

    $tenants = json_decode($json, true);
    if (!is_array($tenants) || !array_key_exists($client_id, $tenants)) {
        return false;
    }

    $tenant = $tenants[$client_id];

    // Migrate or ensure keys for compatibility
    $credentials = [
        'clientId' => !empty($tenant['client_id']) ? $tenant['client_id'] : (isset($tenant['clientId']) ? $tenant['clientId'] : ''),
        'clientSecret' => !empty($tenant['client_secret']) ? $tenant['client_secret'] : (isset($tenant['clientSecret']) ? $tenant['clientSecret'] : ''),
        'tenantId' => !empty($tenant['tenant_id']) ? $tenant['tenant_id'] : (isset($tenant['tenantId']) ? $tenant['tenantId'] : ''),
    ];
    if (!empty($tenant['azure_environment'])) {
        $credentials['azure_environment'] = $tenant['azure_environment'];
    }

    if (empty($credentials['clientId']) || empty($credentials['clientSecret']) || empty($credentials['tenantId'])) {
        return false;
    }

    return $credentials;
}

// function get_user_info_AAD($client_id, $tenant_id, $client_secret)
// {
//     try {
//         $provider = getProvider('azure', $params = array(
//             'tenantId' => $tenant_id,
//             'clientId' => $client_id,
//             'clientSecret' => $client_secret
//         ));

//         $token = $provider->getAccessToken('client_credentials', [
//             'scope' => '.default'
//         ]);

//         if (strlen($token->getToken()) === 0) {
//             throw new Exception('Failed to authenticate with Azure AD.');
//         }

//         $usersRequest = $provider->getAuthenticatedRequest(
//             'GET',
//             'v1.0/users',
//             $token
//         );

//         $usersResponse = $provider->getResponse($usersRequest);
//         $users = json_decode($usersResponse->getBody()->getContents(), true);

//         if ($users === null || !is_array($users)) {
//             throw new Exception('Failed to retrieve users from Azure AD.');
//         }

//         return $users;
//     } catch (Exception $e) {
//         // echo json_encode(array('status' => 'error', 'message' => 'Failed to get user info from Azure AD.'));
//         log_sso('Failed to get user info from Azure AD: ' . $e->getMessage());
//         echo json_encode(array('status' => 'error', 'message' => $e->getMessage()));
//         exit();
//     }
// }

function get_deltas_from_AAD()
{
    /*
     * This function will get the deltas from AAD for individuals, groups, and administrative units.
     * 
     * TODO: This function is not yet implemented. It requires:
     * 1. Storage of delta tokens (deltaLink) from previous syncs
     * 2. Implementation of Microsoft Graph API delta query endpoints
     * 3. Handling of pagination for delta results
     * 4. Proper error handling and token refresh
     * 
     * For now, this function returns an empty array to prevent fatal errors.
     * When implemented, it should use Microsoft Graph API delta queries:
     * - v1.0/users/delta
     * - v1.0/groups/delta
     * 
     * @return array Array of delta changes with structure:
     *   [
     *     ['type' => 'new', 'user' => [...]],
     *     ['type' => 'deleted', 'user' => [...]],
     *     ['type' => 'updated', 'user' => [...]]
     *   ]
     */
    // FIXED: Removed infinite recursion - was calling itself
    // Return empty array until proper implementation
    log_sso('get_deltas_from_AAD() called but not yet implemented. Delta synchronization is not available.');
    return [];
}
