#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ./m365_user_manager/managers/user_manager.py

from typing import Any, Dict, List, Optional, Set, Tuple, Union
from cw_rpa import HttpClient
import requests
import json
import logging
from datetime import datetime, timedelta
from functools import wraps
from difflib import get_close_matches

from m365_user_manager.exceptions.exceptions import (
    GraphAPIError, 
    InsufficientPermissionsError,
    CacheError
)
from m365_user_manager.managers.password_manager import PasswordManager
from m365_user_manager.managers.environment_manager import EnvironmentManager

class CacheManager:
    """Manages caching for frequently accessed M365 data."""
    
    def __init__(self, ttl_minutes: int = 30, logger: Optional[logging.Logger] = None):
        self._cache = {}
        self.logger = logger
        self._ttl = timedelta(minutes=ttl_minutes)
    
    def get(self, key: str) -> Optional[Any]:
        """Get cached value if not expired."""
        if key in self._cache:
            data, timestamp = self._cache[key]
            if datetime.now() - timestamp < self._ttl:
                self.logger.info(f"Cache hit for key: {key}")
                return data
            self.logger.info(f"Cache entry for key {key} expired")
            del self._cache[key]
        return None
    
    def set(self, key: str, value: Any) -> None:
        """Cache value with current timestamp."""
        self.logger.info(f"Caching data for key: {key}")
        self._cache[key] = (value, datetime.now())
    
    def clear(self) -> None:
        """Clear all cached data."""
        self.logger.info("Clearing cache")
        self._cache.clear()

class RequestManager:
    def __init__(self, access_token: str, graph_endpoint: str, 
                 running_in_asio: bool = False,
                 http_client: Optional[HttpClient] = None,
                 max_retries: int = 3,
                 timeout: int = 30,
                 logger: Optional[logging.Logger] = None):
        self.headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json"
        }
        self.graph_endpoint = graph_endpoint
        self.running_in_asio = running_in_asio
        self.http_client = http_client or HttpClient()
        self.max_retries = max_retries
        self.timeout = timeout
        self.logger = logger
        
        # Add immediate logging test
        self.logger.debug("RequestManager initialized")

    async def make_request(self, method: str, endpoint: str,
                         json_data: Optional[Dict] = None,
                         params: Optional[Dict] = None,
                         retry_count: int = 0) -> requests.Response:
        """Make API request with retry logic and error handling."""
        url = f"{self.graph_endpoint}/{endpoint}"
        
        try:
            if self.running_in_asio:
                integration_client = self.http_client.third_party_integration("azure_o365")
                response = await self._make_asio_request(
                    integration_client, method, url, json_data, params
                )
            else:
                response = requests.request(
                    method=method,
                    url=url,
                    headers=self.headers,
                    json=json_data,
                    params=params,
                    timeout=self.timeout
                )

            if response.status_code == 403:
                raise InsufficientPermissionsError(
                    "Insufficient permissions for this operation",
                    required_permissions=['Directory.ReadWrite.All']
                )

            response.raise_for_status()
            return response

        except requests.exceptions.RequestException as e:
            if retry_count < self.max_retries and self._should_retry(e):
                self.logger.warning(
                    f"Request failed, attempt {retry_count + 1} of {self.max_retries}"
                )
                return await self.make_request(
                    method, endpoint, json_data, params, retry_count + 1
                )
            raise GraphAPIError(str(e), status_code=getattr(e, 'response', {}).status_code or 500)

    def _should_retry(self, error: Exception) -> bool:
        """Determine if request should be retried based on error."""
        if isinstance(error, requests.exceptions.Timeout):
            return True
        if hasattr(error, 'response'):
            return error.response.status_code >= 500
        return False

    async def _make_asio_request(self, client: Any, method: str, 
                                url: str, json_data: Optional[Dict], 
                                params: Optional[Dict]) -> Any:
        """Make request using Asio HTTP client."""
        if method.upper() == "GET":
            return await client.get(url=url, headers=self.headers, params=params)
        elif method.upper() == "POST":
            return await client.post(url=url, headers=self.headers, json=json_data)
        elif method.upper() == "PATCH":
            return await client.patch(url=url, headers=self.headers, json=json_data)
        raise GraphAPIError(f"Unsupported HTTP method: {method}", status_code=400)

class MembershipManager:
    """Handles user group and role membership operations."""
    
    def __init__(self, request_manager: RequestManager, cache_manager: CacheManager, logger: Optional[logging.Logger] = None):
        self.request_manager = request_manager
        self.cache_manager = cache_manager
        self.logger = logger

    async def get_user_memberships(self, user_id: str) -> Dict[str, List[Dict]]:
        """Get user's group and role memberships."""
        try:
            response = await self.request_manager.make_request(
                "GET", f"users/{user_id}/memberOf"
            )
            memberships = response.json().get('value', [])
            
            return {
                'groups': [m for m in memberships if m.get('@odata.type') == '#microsoft.graph.group'],
                'roles': [m for m in memberships if m.get('@odata.type') == '#microsoft.graph.directoryRole']
            }
        except Exception as e:
            self.logger.error(f"Failed to get user memberships: {e}")
            return {'groups': [], 'roles': []}

    async def add_user_to_group(self, user_id: str, group_id: str) -> bool:
        """Add user to specified group."""
        try:
            await self.request_manager.make_request(
                "POST",
                f"groups/{group_id}/members/$ref",
                json_data={
                    "@odata.id": f"{self.request_manager.graph_endpoint}/directoryObjects/{user_id}"
                }
            )
            return True
        except Exception as e:
            self.logger.error(f"Failed to add user to group: {e}")
            return False

class M365UserManager:
    """Enhanced Microsoft 365 user management with caching and retry logic."""
    
    def __init__(self, access_token: str, 
                 logger: Optional[logging.Logger] = None,
                 product_licenses: Optional[List[Dict[str, Any]]] = None,
                 running_in_asio: bool = False,
                 http_client: Optional[HttpClient] = None,
                 discord_webhook_url: Optional[str] = None):
        """Initialize user manager with enhanced components."""
        self.logger = logger
        
        self.graph_api_endpoint = "https://graph.microsoft.com/v1.0"
        self.request_manager = RequestManager(
            access_token=access_token,
            graph_endpoint=self.graph_api_endpoint,
            running_in_asio=running_in_asio,
            http_client=http_client,
            logger=self.logger
        )
        
        self.cache_manager = CacheManager(logger=self.logger)
        self.membership_manager = MembershipManager(
            self.request_manager,
            self.cache_manager
        )
        
        self.password_manager = PasswordManager(
            discord_webhook_url=discord_webhook_url
        )
        self.product_licenses = product_licenses or []



    # Utility methods
    def _split_display_name(self, display_name: str) -> Tuple[str, str]:
        """Split display name into first and last names."""
        parts = display_name.split(" ", 1)
        return parts[0], parts[1] if len(parts) > 1 else ""

    def _format_phone_number(self, phone_number: str) -> str:
        """Format phone number with extension support."""
        if not phone_number:
            return ""

        # Extract extension if present
        extension = ""
        for ext_marker in ['x', 'ext', 'ext.', 'extension']:
            if ext_marker in phone_number.lower():
                base, *ext_parts = phone_number.lower().split(ext_marker)
                phone_number = base
                if ext_parts:
                    extension = ext_parts[0].strip(' .')
                break

        # Clean and format main number
        digits = ''.join(c for c in phone_number if c.isdigit() or c == '+')
        
        if not digits.startswith('+'):
            digits = '+1' + digits if len(digits) == 10 else '+' + digits

        # Format based on number type
        if digits.startswith('+1'):
            formatted = f"{digits[:2]} ({digits[2:5]}) {digits[5:8]}-{digits[8:12]}"
            return f"{formatted} x{extension}" if extension else formatted
        else:
            formatted = digits
            return f"{formatted};ext={extension}" if extension else formatted

    def _empty_license_result(self) -> Dict[str, Any]:
        """Generate empty license result structure."""
        return {
            "assigned": [],
            "unavailable": [],
            "available_str": "",
            "unavailable_str": ""
        }
        
    async def _get_user_licenses(self, user_id: str) -> List[Dict[str, str]]:
        """Get user's current licenses with debug logging."""
        try:
            response = await self.request_manager.make_request(
                "GET",
                f"users/{user_id}/licenseDetails"
            )
            raw_response = response.json()
            self.logger.debug(f"Raw license response: {json.dumps(raw_response, indent=2)}")
            
            return response.json().get('value', [])
        except Exception as e:
            self.logger.error(f"License fetch failed: {str(e)}")
            return []
        
    async def create_base_user(self, display_name: str, email_address: str,
                            password: Optional[str] = None) -> Optional[Dict[str, Any]]:
        """Create base user with required properties."""
        try:
            first_name, last_name = self._split_display_name(display_name)
            
            user_payload = {
                "accountEnabled": True,
                "displayName": display_name,
                "mailNickname": email_address.split("@")[0],
                "userPrincipalName": email_address,
                "passwordProfile": {
                    "forceChangePasswordNextSignIn": True,
                    "password": password or self.password_manager.generate_secure_password()
                },
                "usageLocation": "US",
                "givenName": first_name,
                "surname": last_name
            }

            response = await self.request_manager.make_request(
                "POST", "users", json_data=user_payload
            )
            created_user = response.json()
            
            return {
                "id": created_user["id"],
                "email": email_address,
                "password": user_payload["passwordProfile"]["password"],
                "displayName": display_name
            }
            
        except Exception as e:
            self.logger.error(f"Failed to create base user: {e}")
            return None

    async def create_user(self, display_name: str, email_address: str, 
                         **kwargs) -> Optional[Dict[str, Any]]:
        """Create new M365 user with enhanced error handling and validation."""
        try:
            # Create base user
            user_result = await self.create_base_user(
                display_name=display_name,
                email_address=email_address,
                password=kwargs.get('password')
            )

            if not user_result:
                return None

            # Handle user copying if specified
            if user_to_copy := kwargs.get('user_to_copy'):
                await self._handle_user_copying(
                    user_result['id'],
                    user_to_copy,
                    kwargs.get('copy_licenses', False)
                )

            # Update additional properties
            await self._update_user_properties(user_result['id'], kwargs)

            # Create secure password link
            user_result['password_link'] = await self._create_password_link(
                user_result['password']
            )

            # Handle license assignment
            if license_skus := kwargs.get('license_skus'):
                user_result['licenses'] = await self.assign_licenses(
                    user_result['id'],
                    self._normalize_license_list(license_skus)
                )

            # Handle group assignment
            if groups := kwargs.get('groups'):
                user_result['groups'] = await self._add_user_to_groups(
                    user_result['id'],
                    self._normalize_group_list(groups)
                )

            return user_result

        except Exception as e:
            self.logger.error(f"Failed to create user: {e}")
            return None

    async def _handle_user_copying(self, target_user_id: str, source_user_identifier: str,
                                copy_licenses: bool = False) -> Dict[str, Any]:
        """Copy properties and memberships from source to target user."""
        self.logger.info(f"Starting user copy process from {source_user_identifier}")
        self.logger.info(f"Copy licenses flag: {copy_licenses}")
        
        results = {
            "properties": {"copied": [], "failed": []},
            "groups": {"copied": [], "failed": []},
            "roles": {"copied": [], "failed": []},
            "licenses": {"copied": [], "failed": []}
        }
        
        try:
            # Get source user details
            source_user = await self._get_user_details(source_user_identifier)
            if not source_user:
                error_msg = f"Source user not found: {source_user_identifier}"
                self.logger.error(error_msg)
                raise ValueError(error_msg)

            self.logger.info(f"Found source user: {source_user.get('displayName')}")

            # Get source user memberships first
            self.logger.info("Fetching source user memberships...")
            membership_results = await self.membership_manager.get_user_memberships(
                source_user['id']
            )
            
            self.logger.info(f"Found {len(membership_results['groups'])} groups to copy")
            
            # Process each group
            for group in membership_results['groups']:
                try:
                    self.logger.info(f"Attempting to add user to group: {group.get('displayName')}")
                    success = await self.membership_manager.add_user_to_group(
                        target_user_id, group['id']
                    )
                    if success:
                        results['groups']['copied'].append(group['displayName'])
                        self.logger.info(f"Successfully added to group: {group['displayName']}")
                    else:
                        results['groups']['failed'].append(group['displayName'])
                        self.logger.error(f"Failed to add to group: {group['displayName']}")
                except Exception as e:
                    self.logger.error(f"Error adding to group {group.get('displayName')}: {e}")
                    results['groups']['failed'].append(f"{group.get('displayName')} (error: {str(e)})")

            # Handle license copying if requested
            if copy_licenses:
                try:
                    self.logger.debug("Starting license copy process")
                    # First get raw license details
                    raw_licenses = await self._get_user_licenses(source_user['id'])
                    self.logger.debug(f"Raw source user licenses: {json.dumps(raw_licenses, indent=2)}")
                    
                    if raw_licenses:
                        # Extract SKU IDs
                        sku_ids = [lic.get('skuId') for lic in raw_licenses]
                        self.logger.debug(f"Found SKU IDs to copy: {sku_ids}")
                        
                        # Build license assignment payload
                        license_payload = {
                            "addLicenses": [{"skuId": sku_id, "disabledPlans": []} for sku_id in sku_ids],
                            "removeLicenses": []
                        }
                        
                        self.logger.debug(f"License assignment payload: {json.dumps(license_payload, indent=2)}")
                        
                        # Direct license assignment
                        try:
                            await self.request_manager.make_request(
                                "POST",
                                f"users/{target_user_id}/assignLicense",
                                json_data=license_payload
                            )
                            self.logger.info(f"Successfully copied {len(sku_ids)} licenses")
                            results['licenses']['copied'].extend(sku_ids)
                        except Exception as e:
                            self.logger.error(f"License assignment failed: {str(e)}")
                            results['licenses']['failed'].extend(sku_ids)
                except Exception as e:
                    self.logger.error(f"License copy process failed: {str(e)}")

            return results

        except Exception as e:
            self.logger.error(f"User copying process failed: {e}", exc_info=True)
            return results

    async def assign_licenses(self, user_id: str, 
                            licenses_to_assign: Set[str]) -> Dict[str, Any]:
        """Assign licenses to user with validation and availability checking."""
        if not licenses_to_assign:
            return self._empty_license_result()

        try:
            # Get cached SKUs or fetch from API
            tenant_skus = await self._get_tenant_skus()
            
            # Process each license request
            processed_licenses = [
                await self._process_license_request(license_sku, tenant_skus)
                for license_sku in licenses_to_assign
                if license_sku
            ]
            
            # Format and execute assignment
            results = self._format_license_results(processed_licenses, tenant_skus)
            
            if results['available']:
                success = await self._assign_licenses_internal(
                    user_id, results['available']
                )
                if not success:
                    return self._failed_license_result(licenses_to_assign)
                    
            return results

        except Exception as e:
            self.logger.error(f"License assignment failed: {e}")
            return self._failed_license_result(licenses_to_assign)
        
    async def _resolve_license_match(self, license_info: Dict[str, str], 
                                tenant_skus: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Resolve license match against tenant SKUs."""
        try:
            matching_sku = next(
                (sku for sku in tenant_skus 
                if sku['skuPartNumber'].upper() == license_info["StringID"].upper()),
                None
            )
            
            if matching_sku:
                self.logger.debug(f"Found matching SKU: {matching_sku['friendlyName']}")
                return {
                    'matched': True,
                    'type': 'exact_match',
                    'resolved_to': matching_sku['friendlyName'],
                    'sku_id': matching_sku['skuId'],
                    'remaining': matching_sku.get('remaining', 0)
                }
            
            self.logger.debug(f"License {license_info['ProductName']} not available in tenant")
            return {
                'matched': False,
                'type': 'not_in_tenant',
                'resolved_to': license_info['ProductName'],
                'remaining': 0
            }
        except Exception as e:
            self.logger.error(f"Error resolving license match: {e}")
            return {
                'matched': False,
                'type': 'error',
                'resolved_to': license_info.get('ProductName', 'Unknown'),
                'remaining': 0
            }
            
    async def _diagnostic_license_check(self, user_id: str):
        """Diagnostic method to verify license state."""
        try:
            # Get user's current licenses
            current_licenses = await self._get_user_licenses(user_id)
            self.logger.debug(f"Current licenses for user {user_id}:")
            self.logger.debug(json.dumps(current_licenses, indent=2))

            # Get tenant SKUs
            tenant_skus = await self._get_tenant_skus()
            self.logger.debug("Available tenant SKUs:")
            self.logger.debug(json.dumps(tenant_skus, indent=2))

            return current_licenses, tenant_skus
        except Exception as e:
            self.logger.error(f"Diagnostic check failed: {str(e)}")
            return None, None
        
    async def _assign_licenses_internal(self, user_id: str, 
                                    license_details: List[Dict[str, Any]]) -> bool:
        """Internal method to assign licenses to a user."""
        self.logger.info(f"Starting internal license assignment for user: {user_id}")
        self.logger.info(f"Licenses to assign: {json.dumps(license_details, indent=2)}")
        
        try:
            if not license_details:
                self.logger.info("No licenses to assign")
                return True
                
            license_payload = {
                "addLicenses": [
                    {
                        "skuId": sku['skuId'],
                        "disabledPlans": []
                    } for sku in license_details
                ],
                "removeLicenses": []
            }
            
            self.logger.info(f"License assignment payload: {json.dumps(license_payload, indent=2)}")
            
            await self.request_manager.make_request(
                "POST",
                f"users/{user_id}/assignLicense",
                json_data=license_payload
            )
            
            self.logger.info(
                f"Successfully assigned licenses: {[sku['friendlyName'] for sku in license_details]}"
            )
            return True
            
        except Exception as e:
            self.logger.error(f"License assignment failed: {str(e)}", exc_info=True)
            if hasattr(e, 'response'):
                self.logger.error(f"Error response: {e.response.text}")
            return False

    async def _get_tenant_skus(self) -> List[Dict[str, Any]]:
        """Get SKUs with caching."""
        cache_key = 'tenant_skus'
        if cached_skus := self.cache_manager.get(cache_key):
            return cached_skus

        try:
            response = await self.request_manager.make_request("GET", "subscribedSkus")
            tenant_skus = response.json().get('value', [])
            
            processed_skus = self._process_tenant_skus(tenant_skus)
            self.cache_manager.set(cache_key, processed_skus)
            
            return processed_skus

        except Exception as e:
            self.logger.error(f"Failed to get tenant SKUs: {e}")
            raise CacheError(f"Failed to fetch and cache tenant SKUs: {e}")

    def _process_tenant_skus(self, raw_skus: List[Dict]) -> List[Dict[str, Any]]:
        """Process raw SKU data into standardized format."""
        processed_skus = []
        for sku in raw_skus:
            sku_id = sku['skuId'].lower()
            sku_part_number = sku['skuPartNumber'].upper()
            
            friendly_name = next(
                (info["ProductName"] for info in self.product_licenses
                if info["StringID"].upper() == sku_part_number),
                sku_part_number
            )
            
            remaining = sku['prepaidUnits']['enabled'] - sku['consumedUnits']
            
            processed_skus.append({
                'skuId': sku_id,
                'skuPartNumber': sku_part_number,
                'friendlyName': friendly_name,
                'remaining': remaining,
                'enabled': sku['prepaidUnits']['enabled'],
                'consumed': sku['consumedUnits']
            })
            
        return processed_skus

    async def _process_license_request(self, requested_license: str,
                                tenant_skus: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Process single license request with improved matching."""
        self.logger.info(f"Processing license request: {requested_license}")
        self.logger.info(f"Available tenant SKUs: {json.dumps(tenant_skus, indent=2)}")

        if not isinstance(requested_license, str):
            self.logger.warning(f"Invalid license format: {requested_license}")
            return self._invalid_license_result(requested_license)

        requested_license = requested_license.strip()
        self.logger.info(f"Available product licenses: {json.dumps(self.product_licenses, indent=2)}")
        
        # Try exact match first
        for license_info in self.product_licenses:
            if (license_info["StringID"].upper() == requested_license.upper() or
                license_info["ProductName"].upper() == requested_license.upper()):
                return await self._resolve_license_match(license_info, tenant_skus)

        # Try fuzzy matching
        product_names = [info["ProductName"] for info in self.product_licenses]
        if closest_matches := get_close_matches(requested_license, product_names, n=1, cutoff=0.75):
            matching_license = next(
                info for info in self.product_licenses
                if info["ProductName"] == closest_matches[0]
            )
            return await self._resolve_license_match(matching_license, tenant_skus)

        return self._unknown_license_result(requested_license)

    async def _update_user_properties(self, user_id: str, 
                                    properties: Dict[str, Any]) -> None:
        """Update user properties with validation."""
        update_payload = {}
        
        # Handle phone numbers
        if business_phone := properties.get('business_phone'):
            update_payload['businessPhones'] = [
                self._format_phone_number(business_phone)
            ]
        if mobile_phone := properties.get('mobile_phone'):
            update_payload['mobilePhone'] = self._format_phone_number(mobile_phone)

        # Handle other properties
        property_mapping = {
            'job_title': 'jobTitle',
            'department': 'department',
            'office_location': 'officeLocation',
            'city': 'city',
            'state': 'state'
        }

        for source_key, target_key in property_mapping.items():
            if value := properties.get(source_key):
                update_payload[target_key] = value

        if update_payload:
            try:
                await self.request_manager.make_request(
                    "PATCH",
                    f"users/{user_id}",
                    json_data=update_payload
                )
            except Exception as e:
                self.logger.error(f"Failed to update user properties: {e}")
                raise

    def _failed_license_result(self, requested_licenses: Set[str]) -> Dict[str, Any]:
        """Generate result structure for failed license assignment."""
        return {
            "assigned": [],
            "unavailable": list(requested_licenses),
            "available_str": "",
            "unavailable_str": "License assignment failed"
        }

    async def _get_user_details(self, user_identifier: str) -> Optional[Dict[str, Any]]:
        """Get user details with caching support."""
        cache_key = f"user_details_{user_identifier.lower()}"
        if cached_user := self.cache_manager.get(cache_key):
            return cached_user

        try:
            # Optimize query with specific fields
            params = {
                "$select": "id,displayName,userPrincipalName",
                "$filter": (f"userPrincipalName eq '{user_identifier}'" if '@' in user_identifier
                          else f"startswith(displayName, '{user_identifier}')")
            }

            response = await self.request_manager.make_request(
                "GET", "users", params=params
            )
            users = response.json().get('value', [])

            if not users:
                return None

            # For email lookups, prefer exact match
            if '@' in user_identifier:
                user = next(
                    (u for u in users if u['userPrincipalName'].lower() == user_identifier.lower()),
                    users[0]
                )
            else:
                # For name lookups, use fuzzy matching
                user = self._find_best_user_match(users, user_identifier)

            if user:
                self.cache_manager.set(cache_key, user)
                return user

            return None

        except Exception as e:
            self.logger.error(f"Failed to get user details: {e}")
            return None

    def _find_best_user_match(self, users: List[Dict], search_term: str) -> Optional[Dict]:
        """Find best matching user using fuzzy matching."""
        name_map = {u['displayName']: u for u in users}
        if matches := get_close_matches(search_term, list(name_map.keys()), n=1, cutoff=0.8):
            return name_map[matches[0]]
        return None if not users else users[0]

    async def _get_group_by_name(self, group_name: str) -> Optional[Dict[str, Any]]:
        """Get group details with caching and fuzzy matching."""
        cache_key = f"group_{group_name.lower()}"
        if cached_group := self.cache_manager.get(cache_key):
            return cached_group

        try:
            params = {
                "$select": "id,displayName",
                "$filter": f"startswith(displayName, '{group_name}')"
            }

            response = await self.request_manager.make_request(
                "GET", "groups", params=params
            )
            groups = response.json().get('value', [])

            if not groups:
                return None

            # Try exact match first
            if exact_match := next(
                (g for g in groups if g['displayName'].lower() == group_name.lower()),
                None
            ):
                self.cache_manager.set(cache_key, exact_match)
                return exact_match

            # Fall back to fuzzy matching
            group_map = {g['displayName']: g for g in groups}
            if matches := get_close_matches(group_name, list(group_map.keys()), n=1, cutoff=0.8):
                matched_group = group_map[matches[0]]
                self.cache_manager.set(cache_key, matched_group)
                return matched_group

            return None

        except Exception as e:
            self.logger.error(f"Failed to get group details: {e}")
            return None

    async def _add_user_to_groups(self, user_id: str, 
                                group_names: List[str]) -> Dict[str, List[str]]:
        """Add user to specified groups with optimized error handling."""
        results = {
            "successful": [],
            "failed": []
        }

        if not group_names:
            return results

        try:
            # Get current memberships first
            memberships = await self.membership_manager.get_user_memberships(user_id)
            current_groups = {g['displayName'].lower(): g['id'] 
                            for g in memberships['groups']}

            for group_name in group_names:
                try:
                    if not group_name.strip():
                        continue

                    # Skip if already a member
                    if group_name.lower() in current_groups:
                        results["successful"].append(
                            f"{group_name} (already member)"
                        )
                        continue

                    # Get group details
                    group = await self._get_group_by_name(group_name)
                    if not group:
                        results["failed"].append(f"{group_name} (not found)")
                        continue

                    # Add to group
                    success = await self.membership_manager.add_user_to_group(
                        user_id, group['id']
                    )
                    
                    if success:
                        results["successful"].append(group['displayName'])
                    else:
                        results["failed"].append(
                            f"{group['displayName']} (addition failed)"
                        )

                except Exception as e:
                    self.logger.error(f"Failed to add to group {group_name}: {e}")
                    results["failed"].append(f"{group_name} (error: {str(e)})")

        except Exception as e:
            self.logger.error(f"Group assignment process failed: {e}")
            results["failed"].append(f"Assignment process error: {str(e)}")

        return results

    async def get_user_licenses(self, user_id: str) -> List[Dict[str, str]]:
        """Get user's current licenses with caching."""
        self.logger.info(f"Fetching licenses for user: {user_id}")
        cache_key = f"user_licenses_{user_id}"
        
        if cached_licenses := self.cache_manager.get(cache_key):
            self.logger.info(f"Found cached licenses: {json.dumps(cached_licenses, indent=2)}")
            return cached_licenses

        try:
            response = await self.request_manager.make_request(
                "GET",
                f"users/{user_id}/licenseDetails"
            )
            
            raw_licenses = response.json().get('value', [])
            self.logger.info(f"Raw license response: {json.dumps(raw_licenses, indent=2)}")
            
            licenses = []
            for sku in raw_licenses:
                friendly_name = next(
                    (info["ProductName"] for info in self.product_licenses 
                    if info["StringID"].upper() == sku.get('skuPartNumber', '').upper()),
                    sku.get('skuPartNumber', '')
                )
                
                license_info = {
                    'skuId': sku['skuId'],
                    'friendlyName': friendly_name,
                    'skuPartNumber': sku.get('skuPartNumber', '')
                }
                licenses.append(license_info)
                self.logger.info(f"Processed license: {json.dumps(license_info, indent=2)}")

            self.cache_manager.set(cache_key, licenses)
            return licenses

        except Exception as e:
            self.logger.error(f"Failed to get user licenses: {str(e)}", exc_info=True)
            return []

    def _normalize_license_list(self, licenses: Union[str, List[str], Set[str]]) -> Set[str]:
        """Normalize license input to a set of strings."""
        if isinstance(licenses, str):
            return {lic.strip() for lic in licenses.split(',') if lic.strip()}
        return set(licenses)

    def _normalize_group_list(self, groups: Union[str, List[str]]) -> List[str]:
        """Normalize group input to a list of strings."""
        if isinstance(groups, str):
            return [g.strip() for g in groups.split(',') if g.strip()]
        return list(groups)

    async def _create_password_link(self, password: str) -> Optional[str]:
        """Create secure password sharing link with error handling."""
        try:
            return self.password_manager.create_secure_link(password)
        except Exception as e:
            self.logger.error(f"Failed to create password link: {e}")
            return None
          
    def _format_license_results(self, processed_licenses: list[dict[str, Any]], 
                            tenant_skus: list[dict[str, Any]]) -> dict[str, Any]:
        """Format the results of license processing into a standardized output."""
        available = []
        unavailable = set()
        available_str_list = set()
        unavailable_str_list = set()
        
        for license_result in processed_licenses:
            if not license_result:  # Handle None results
                continue
                
            resolved_name = license_result.get('resolved_to', 'Unknown License')
            
            if license_result['matched']:
                sku_id = license_result['sku_id']
                matching_sku = next(
                    (sku for sku in tenant_skus if sku['skuId'] == sku_id),
                    None
                )
                
                if matching_sku and matching_sku['remaining'] > 0:
                    available.append(matching_sku)
                    available_str_list.add(
                        f"{matching_sku['friendlyName']} ({matching_sku['remaining']} available)"
                    )
                else:
                    unavailable.add(resolved_name)
                    unavailable_str_list.add(
                        f"{resolved_name} (no licenses available)"
                    )
            else:
                unavailable.add(resolved_name)
                # Get specific reason for unavailability
                reason = {
                    'not_in_tenant': 'license not available in tenant',
                    'unknown_license': 'unknown license',
                    'invalid': 'invalid license format',
                    'error': 'processing error'
                }.get(license_result['type'], 'license unavailable')
                
                unavailable_str_list.add(
                    f"{resolved_name} ({reason})"
                )
        
        return {
            'available': available,
            'unavailable': list(unavailable),
            'available_str': "\n".join(sorted(available_str_list)) if available_str_list else "No licenses available",
            'unavailable_str': "\n".join(sorted(unavailable_str_list)) if unavailable_str_list else "No unavailable licenses"
        }

    @staticmethod
    def _validate_email(email: str) -> bool:
        """Validate email format."""
        import re
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'


        return bool(re.match(pattern, email))