"""Keycloak Admin API Manager

Wrapper around python-keycloak library for role management operations.
Handles service account authentication, token management, and role assignments.

Author: Balázs Milán (milan.balazs@uni-obuda.hu)
"""

from typing import Optional, List, Dict, Any
import logging
from keycloak import KeycloakAdmin
from keycloak.exceptions import KeycloakError, KeycloakAuthenticationError

from ..exceptions import (
    KeycloakConnectionError,
    KeycloakAuthenticationError as BlackAntKeycloakAuthError,
    RoleAssignmentError
)
from ..config.keycloak_admin_config import KeycloakAdminConfig


logger = logging.getLogger(__name__)


class KeycloakManager:
    """Keycloak Admin API manager for role operations.

    Provides high-level interface for Keycloak role management using
    service account authentication. Handles token refresh automatically.

    Args:
        config: KeycloakAdminConfig instance with service account credentials

    Example:
        >>> config = KeycloakAdminConfig.from_env()
        >>> manager = KeycloakManager(config)
        >>> manager.assign_role_to_user("user-uuid", "science_module")
    """

    def __init__(self, config: KeycloakAdminConfig):
        """Initialize Keycloak manager with admin config.

        Args:
            config: Keycloak admin configuration

        Raises:
            KeycloakConnectionError: If connection to Keycloak fails
            KeycloakAuthenticationError: If service account auth fails
        """
        self.config = config
        self.logger = logger
        self._admin_client: Optional[KeycloakAdmin] = None
        self._initialize_admin_client()

    def _initialize_admin_client(self) -> None:
        """Initialize Keycloak admin client with service account.

        Uses client credentials grant flow for service account authentication.
        The python-keycloak library handles token refresh automatically.

        Raises:
            KeycloakConnectionError: If connection fails
            KeycloakAuthenticationError: If authentication fails
        """
        try:
            self._admin_client = KeycloakAdmin(
                server_url=self.config.server_url,
                realm_name=self.config.realm_name,
                client_id=self.config.client_id,
                client_secret_key=self.config.client_secret,
                verify=self.config.verify_ssl,
                timeout=self.config.timeout
            )

            # Test connection with a simple API call
            self._admin_client.get_realm_roles()

            self.logger.info(
                f"Keycloak admin client initialized successfully "
                f"(realm: {self.config.realm_name})"
            )

        except KeycloakAuthenticationError as e:
            self.logger.error(f"Keycloak authentication failed: {e}")
            raise BlackAntKeycloakAuthError(
                f"Failed to authenticate with Keycloak service account: {e}"
            )
        except Exception as e:
            self.logger.error(f"Failed to connect to Keycloak: {e}")
            raise KeycloakConnectionError(
                f"Could not establish connection to Keycloak server: {e}"
            )

    def get_user_roles(self, user_id: str) -> List[Dict[str, Any]]:
        """Get all roles assigned to a user.

        Args:
            user_id: Keycloak user UUID

        Returns:
            List of role dictionaries with keys: id, name, description

        Raises:
            RoleAssignmentError: If role query fails

        Example:
            >>> roles = manager.get_user_roles("user-uuid")
            >>> print([r['name'] for r in roles])
            ['default-roles', 'science_module']
        """
        try:
            # Get realm-level roles
            realm_roles = self._admin_client.get_realm_roles_of_user(user_id)

            self.logger.debug(
                f"Retrieved {len(realm_roles)} roles for user {user_id}"
            )

            return realm_roles

        except KeycloakError as e:
            self.logger.error(f"Failed to get user roles: {e}")
            raise RoleAssignmentError(
                f"Could not retrieve roles for user {user_id}: {e}"
            )

    def has_role(self, user_id: str, role_name: str) -> bool:
        """Check if user has a specific role.

        Args:
            user_id: Keycloak user UUID
            role_name: Role name to check (e.g., "science_module")

        Returns:
            True if user has the role, False otherwise

        Example:
            >>> if manager.has_role("user-uuid", "science_module"):
            ...     print("User has access to science modules")
        """
        try:
            user_roles = self.get_user_roles(user_id)
            role_names = [role['name'] for role in user_roles]
            has_role = role_name in role_names

            self.logger.debug(
                f"User {user_id} {'has' if has_role else 'does not have'} "
                f"role '{role_name}'"
            )

            return has_role

        except RoleAssignmentError:
            # If we can't get roles, assume user doesn't have the role
            self.logger.warning(
                f"Could not check role for user {user_id}, assuming no role"
            )
            return False

    def assign_role_to_user(
        self,
        user_id: str,
        role_name: str,
        idempotent: bool = True
    ) -> bool:
        """Assign a role to a user.

        Args:
            user_id: Keycloak user UUID
            role_name: Role name to assign (e.g., "science_module")
            idempotent: If True, skip assignment if user already has role

        Returns:
            True if role was assigned or already exists, False otherwise

        Raises:
            RoleAssignmentError: If role assignment fails

        Example:
            >>> success = manager.assign_role_to_user("user-uuid", "science_module")
            >>> if success:
            ...     print("Role assigned successfully")
        """
        try:
            # Check if role already exists (idempotent operation)
            if idempotent and self.has_role(user_id, role_name):
                self.logger.info(
                    f"User {user_id} already has role '{role_name}', skipping"
                )
                return True

            # Get role object
            role = self._admin_client.get_realm_role(role_name)
            if not role:
                raise RoleAssignmentError(
                    f"Role '{role_name}' does not exist in realm "
                    f"{self.config.realm_name}"
                )

            # Assign role to user (realm-level role)
            self._admin_client.assign_realm_roles(
                user_id=user_id,
                roles=[role]
            )

            self.logger.info(
                f"Successfully assigned role '{role_name}' to user {user_id}"
            )

            return True

        except KeycloakError as e:
            self.logger.error(
                f"Failed to assign role '{role_name}' to user {user_id}: {e}"
            )
            raise RoleAssignmentError(
                f"Could not assign role '{role_name}': {e}"
            )

    def remove_role_from_user(self, user_id: str, role_name: str) -> bool:
        """Remove a role from a user.

        Args:
            user_id: Keycloak user UUID
            role_name: Role name to remove

        Returns:
            True if role was removed, False if user didn't have the role

        Raises:
            RoleAssignmentError: If role removal fails
        """
        try:
            # Check if user has the role
            if not self.has_role(user_id, role_name):
                self.logger.info(
                    f"User {user_id} doesn't have role '{role_name}', skipping"
                )
                return False

            # Get role object
            role = self._admin_client.get_realm_role(role_name)

            # Remove role from user
            self._admin_client.delete_realm_roles_of_user(
                user_id=user_id,
                roles=[role]
            )

            self.logger.info(
                f"Successfully removed role '{role_name}' from user {user_id}"
            )

            return True

        except KeycloakError as e:
            self.logger.error(
                f"Failed to remove role '{role_name}' from user {user_id}: {e}"
            )
            raise RoleAssignmentError(
                f"Could not remove role '{role_name}': {e}"
            )

    def get_users_with_role(self, role_name: str) -> List[Dict[str, Any]]:
        """Get all users that have a specific role.

        Args:
            role_name: Role name to query

        Returns:
            List of user dictionaries

        Raises:
            RoleAssignmentError: If query fails
        """
        try:
            # Get role object
            role = self._admin_client.get_realm_role(role_name)

            # Get users with this role
            users = self._admin_client.get_realm_role_members(role['id'])

            self.logger.debug(
                f"Found {len(users)} users with role '{role_name}'"
            )

            return users

        except KeycloakError as e:
            self.logger.error(f"Failed to get users with role '{role_name}': {e}")
            raise RoleAssignmentError(
                f"Could not retrieve users with role '{role_name}': {e}"
            )

    def health_check(self) -> bool:
        """Check if Keycloak admin connection is healthy.

        Returns:
            True if connection is healthy, False otherwise
        """
        try:
            # Simple API call to verify connection
            self._admin_client.get_realm_roles()
            return True
        except Exception as e:
            self.logger.error(f"Keycloak health check failed: {e}")
            return False

    def role_exists(self, role_name) -> bool:
        """Checks if given role exists in the realm

        Returns:
            True if role exists, false otherwise
        """
        try:
            self._admin_client.get_role(role_name)
            return True
        except Exception:
            return False

    def create_realm_role(self, role_name, description="") -> bool:
        role = {
            "name": role_name,
            "description": description,
        }
        try:
            self._admin_client.create_realm_role(role)
            print(f"Created realm role: {role_name}")
            return True
        except Exception as e:
            self.logger.error(f"Keycloak realm role creation failed: {e}")
            return False

    def delete_realm_role(self, role_name) -> bool:
        try:
            self._admin_client.delete_realm_role(role_name)
            print(f"Deleted realm role: {role_name}")
            return True
        except Exception as e:
            self.logger.error(f"Keycloak realm role deletion failed: {e}")
            return False

# Singleton instance (optional - can be instantiated per-use)
_keycloak_manager: Optional[KeycloakManager] = None


def get_keycloak_manager(config: Optional[KeycloakAdminConfig] = None) -> KeycloakManager:
    """Get or create KeycloakManager singleton instance.

    Args:
        config: Optional config override (uses env config if None)

    Returns:
        KeycloakManager instance
    """
    global _keycloak_manager

    if _keycloak_manager is None or config is not None:
        if config is None:
            config = KeycloakAdminConfig.from_env()
        _keycloak_manager = KeycloakManager(config)

    return _keycloak_manager
