"""
LDAP Authentication Service with Group-Based Access Control
Handles LDAP authentication and group membership verification
"""

from typing import Optional, Dict, List, Tuple
import logging
from datetime import datetime

# Optional LDAP imports
try:
    import ldap3
    from ldap3 import Server, Connection, ALL, SUBTREE
    from ldap3.core.exceptions import LDAPException, LDAPBindError
    LDAP3_AVAILABLE = True
except ImportError:
    LDAP3_AVAILABLE = False

from config.settings import settings

logger = logging.getLogger(__name__)

class LDAPAUTHService:
    """LDAP Authentication Service with group membership verification"""
    
    def __init__(self):
        self.server = None
        self.connection = None
        self._initialized = False
        
    def _initialize_server(self):
        """Initialize LDAP server connection"""
        if self._initialized:
            return
        
        try:
            # Create LDAP server
            if settings.ldap_use_ssl:
                protocol = "ldaps"
                port = 636  # Default LDAPS port
            else:
                protocol = "ldap"
                port = settings.ldap_port
            
            server_url = f"{protocol}://{settings.ldap_server}:{port}"
            self.server = Server(server_url, get_info=ALL)
            
            # Test connection with bind DN if available
            if settings.ldap_bind_dn and settings.ldap_bind_password:
                self.connection = Connection(
                    self.server,
                    user=settings.ldap_bind_dn,
                    password=settings.ldap_bind_password,
                    auto_bind=True
                )
                logger.info("LDAP connection established with bind DN")
            else:
                # Anonymous bind for testing
                self.connection = Connection(self.server, auto_bind=True)
                logger.info("LDAP connection established with anonymous bind")
                
            self._initialized = True
            logger.info("LDAP server initialized successfully")
            
        except LDAPException as e:
            logger.error(f"LDAP initialization failed: {e}")
            raise
    
    def authenticate_user(self, username: str, password: str) -> Tuple[bool, Optional[str], Optional[Dict]]:
        """
        Authenticate user against LDAP and check group membership
        
        Returns:
            Tuple of (success, user_dn, user_info)
        """
        if not LDAP3_AVAILABLE:
            logger.warning("LDAP3 not available - LDAP authentication disabled")
            return False, None, None
            
        if not settings.ldap_server:
            logger.warning("LDAP not configured")
            return False, None, None
        
        try:
            self._initialize_server()
            
            # Search for user
            user_dn = self._find_user_dn(username)
            if not user_dn:
                logger.warning(f"User {username} not found in LDAP")
                return False, None, None
            
            # Bind as user to verify password
            user_conn = Connection(self.server, user=user_dn, password=password, auto_bind=True)
            
            if not user_conn.bound:
                logger.warning(f"Authentication failed for user {username}")
                return False, None, None
            
            # Get user information
            user_info = self._get_user_info(username, user_dn)
            
            # Check group membership
            if not self._check_group_membership(username, user_dn):
                logger.warning(f"User {username} is not authorized - not in KPI group")
                user_conn.unbind()
                return False, None, None
            
            logger.info(f"User {username} authenticated and authorized")
            user_conn.unbind()
            return True, user_dn, user_info
            
        except LDAPBindError as e:
            logger.warning(f"Authentication failed for user {username}: {e}")
            return False, None, None
        except Exception as e:
            logger.error(f"LDAP authentication error: {e}")
            return False, None, None
    
    def _find_user_dn(self, username: str) -> Optional[str]:
        """Find user DN in LDAP directory"""
        try:
            search_base = settings.ldap_base_dn or settings.ldap_group_base_dn
            search_filter = settings.ldap_user_search_filter.format(username=username)
            
            self.connection.search(
                search_base=search_base,
                search_filter=search_filter,
                search_scope=SUBTREE,
                attributes=['dn', 'cn', 'mail', 'uid']
            )
            
            if self.connection.entries:
                return str(self.connection.entries[0].entry_dn)
            
            return None
            
        except LDAPException as e:
            logger.error(f"User search failed: {e}")
            return None
    
    def _get_user_info(self, username: str, user_dn: str) -> Optional[Dict]:
        """Get user information from LDAP"""
        try:
            self.connection.search(
                search_base=user_dn,
                search_filter='(objectClass=person)',
                search_scope=ldap3.BASE,
                attributes=['cn', 'mail', 'uid', 'givenName', 'sn', 'telephoneNumber']
            )
            
            if self.connection.entries:
                entry = self.connection.entries[0]
                return {
                    'username': getattr(entry, 'uid', [username])[0] if hasattr(entry, 'uid') else username,
                    'full_name': str(getattr(entry, 'cn', [''])[0]) if hasattr(entry, 'cn') else '',
                    'email': str(getattr(entry, 'mail', [''])[0]) if hasattr(entry, 'mail') else '',
                    'first_name': str(getattr(entry, 'givenName', [''])[0]) if hasattr(entry, 'givenName') else '',
                    'last_name': str(getattr(entry, 'sn', [''])[0]) if hasattr(entry, 'sn') else '',
                    'phone': str(getattr(entry, 'telephoneNumber', [''])[0]) if hasattr(entry, 'telephoneNumber') else '',
                    'ldap_dn': user_dn
                }
            
            return None
            
        except LDAPException as e:
            logger.error(f"User info retrieval failed: {e}")
            return None
    
    def _check_group_membership(self, username: str, user_dn: str) -> bool:
        """Check if user is member of the authorized KPI group"""
        if not settings.ldap_kpi_group_dn:
            logger.warning("No KPI group configured - access denied")
            return False
        
        try:
            # Method 1: Check if user DN is in group members
            if self._check_user_in_group_by_member(user_dn):
                return True
            
            # Method 2: Check if group DN is in user's memberOf attribute
            if self._check_user_member_of_group(username, user_dn):
                return True
            
            logger.warning(f"User {username} not found in KPI group {settings.ldap_kpi_group_dn}")
            return False
            
        except Exception as e:
            logger.error(f"Group membership check failed: {e}")
            return False
    
    def _check_user_in_group_by_member(self, user_dn: str) -> bool:
        """Check if user DN is in group members"""
        try:
            self.connection.search(
                search_base=settings.ldap_kpi_group_dn,
                search_filter='(objectClass=group)',
                search_scope=ldap3.BASE,
                attributes=[settings.ldap_group_member_attribute]
            )
            
            if self.connection.entries:
                entry = self.connection.entries[0]
                members = getattr(entry, settings.ldap_group_member_attribute, [])
                
                for member in members:
                    if str(member) == user_dn:
                        return True
            
            return False
            
        except LDAPException as e:
            logger.error(f"Group member check failed: {e}")
            return False
    
    def _check_user_member_of_group(self, username: str, user_dn: str) -> bool:
        """Check if group DN is in user's memberOf attribute"""
        try:
            self.connection.search(
                search_base=user_dn,
                search_filter='(objectClass=person)',
                search_scope=ldap3.BASE,
                attributes=[settings.ldap_user_member_attribute]
            )
            
            if self.connection.entries:
                entry = self.connection.entries[0]
                member_of = getattr(entry, settings.ldap_user_member_attribute, [])
                
                for group_dn in member_of:
                    if str(group_dn) == settings.ldap_kpi_group_dn:
                        return True
            
            return False
            
        except LDAPException as e:
            logger.error(f"User memberOf check failed: {e}")
            return False
    
    def get_authorized_users(self) -> List[Dict]:
        """Get list of all users in the authorized KPI group"""
        if not settings.ldap_kpi_group_dn:
            return []
        
        try:
            self._initialize_server()
            
            # Get group members
            self.connection.search(
                search_base=settings.ldap_kpi_group_dn,
                search_filter='(objectClass=group)',
                search_scope=ldap3.BASE,
                attributes=[settings.ldap_group_member_attribute]
            )
            
            if not self.connection.entries:
                return []
            
            entry = self.connection.entries[0]
            members = getattr(entry, settings.ldap_group_member_attribute, [])
            
            authorized_users = []
            
            for member_dn in members:
                # Get user info for each member
                try:
                    member_info = self._get_user_info_by_dn(str(member_dn))
                    if member_info:
                        authorized_users.append(member_info)
                except Exception as e:
                    logger.warning(f"Failed to get info for member {member_dn}: {e}")
                    continue
            
            return authorized_users
            
        except Exception as e:
            logger.error(f"Failed to get authorized users: {e}")
            return []
    
    def _get_user_info_by_dn(self, user_dn: str) -> Optional[Dict]:
        """Get user information by DN"""
        try:
            self.connection.search(
                search_base=user_dn,
                search_filter='(objectClass=person)',
                search_scope=ldap3.BASE,
                attributes=['cn', 'mail', 'uid', 'givenName', 'sn']
            )
            
            if self.connection.entries:
                entry = self.connection.entries[0]
                return {
                    'username': getattr(entry, 'uid', [''])[0] if hasattr(entry, 'uid') else '',
                    'full_name': str(getattr(entry, 'cn', [''])[0]) if hasattr(entry, 'cn') else '',
                    'email': str(getattr(entry, 'mail', [''])[0]) if hasattr(entry, 'mail') else '',
                    'first_name': str(getattr(entry, 'givenName', [''])[0]) if hasattr(entry, 'givenName') else '',
                    'last_name': str(getattr(entry, 'sn', [''])[0]) if hasattr(entry, 'sn') else '',
                    'ldap_dn': user_dn
                }
            
            return None
            
        except Exception as e:
            logger.error(f"Failed to get user info by DN {user_dn}: {e}")
            return None
    
    def test_connection(self) -> Tuple[bool, str]:
        """Test LDAP connection and configuration"""
        if not LDAP3_AVAILABLE:
            return False, "LDAP3 not available - cannot test LDAP connection"
            
        try:
            self._initialize_server()
            
            # Test with a simple search
            if settings.ldap_base_dn:
                search_base = settings.ldap_base_dn
            elif settings.ldap_group_base_dn:
                search_base = settings.ldap_group_base_dn
            else:
                return False, "No search base DN configured"
            
            self.connection.search(
                search_base=search_base,
                search_filter='(objectClass=*)',
                search_scope=ldap3.BASE,
                attributes=[]
            )
            
            return True, "LDAP connection successful"
            
        except Exception as e:
            return False, f"LDAP connection failed: {str(e)}"
    
    def test_group_access(self) -> Tuple[bool, str]:
        """Test group access configuration"""
        if not LDAP3_AVAILABLE:
            return False, "LDAP3 not available - cannot test group access"
            
        if not settings.ldap_kpi_group_dn:
            return False, "No KPI group DN configured"
        
        try:
            self._initialize_server()
            
            # Test access to group
            self.connection.search(
                search_base=settings.ldap_kpi_group_dn,
                search_filter='(objectClass=group)',
                search_scope=ldap3.BASE,
                attributes=[settings.ldap_group_member_attribute]
            )
            
            if not self.connection.entries:
                return False, f"Group {settings.ldap_kpi_group_dn} not found"
            
            entry = self.connection.entries[0]
            members = getattr(entry, settings.ldap_group_member_attribute, [])
            
            return True, f"Group access successful. Found {len(members)} members"
            
        except Exception as e:
            return False, f"Group access test failed: {str(e)}"
    
    def __del__(self):
        """Cleanup LDAP connection"""
        if self.connection and self.connection.bound:
            try:
                self.connection.unbind()
            except:
                pass

# Global LDAP auth service instance
ldap_auth_service = LDAPAUTHService()