"""BlackAnt authentication module.

Provides username/password authentication to obtain Bearer tokens
from Keycloak for BlackAnt platform services.
"""

import os
from typing import Optional, Dict, Any, List

import requests

from .tokens import AuthTokenStore
from ..exceptions import BlackAntAuthenticationError
from ..utils.logging import get_logger


class BlackAntAuth:
    """BlackAnt authentication handler.

    Manages username/password authentication with Keycloak to obtain
    Bearer tokens. All BlackAnt services require this token.

    The token is automatically injected into all HTTP requests through
    Nginx proxy which validates it with Keycloak.

    Args:
        user (str): Username for authentication.
        password (str): Password for authentication.
        login_url (str, optional): Keycloak login endpoint URL.

    Examples:
        >>> auth = BlackAntAuth(user="my_name", password="xxx")
        >>> token = auth.get_token()
        >>> # Token is automatically used in subsequent API calls
    """

    def __init__(self, user: str = None, password: str = None,
                 client_id: str = None, client_secret: str = None,
                 login_url: Optional[str] = None):
        """Initialize authentication with credentials.

        Supports two authentication modes:
        1. Username/Password (OAuth2 Password Flow)
        2. Client ID/Secret (OAuth2 Client Credentials Flow)

        Args:
            user: Username for authentication (password flow).
            password: Password for authentication (password flow).
            client_id: Client ID for service account authentication (client credentials flow).
            client_secret: Client secret for service account authentication (client credentials flow).
            login_url: Optional login endpoint URL, defaults to environment variable.

        Raises:
            ValueError: If neither (user+password) nor (client_id+client_secret) provided.
        """
        # Validate that we have either username/password OR client_id/client_secret
        has_user_pass = user and password
        has_client_creds = client_id and client_secret

        if not has_user_pass and not has_client_creds:
            raise ValueError(
                "Either (user+password) or (client_id+client_secret) must be provided"
            )

        if has_user_pass and has_client_creds:
            raise ValueError(
                "Provide either (user+password) OR (client_id+client_secret), not both"
            )

        self.user = user
        self.password = password
        self.client_id = client_id or "blackant-app"
        self.client_secret = client_secret
        self.login_url = login_url or os.getenv(
            "BLACKANT_LOGIN_URL",
            "https://dev.blackant.app/api/auth/login"
        )

        # Initialize attributes that are referenced later
        self.realm = "master"

        # Use existing AuthTokenStore for thread-safe token storage
        self.token_store = AuthTokenStore()
        self.request_timeout = 30.0  # Default timeout for requests
        self.logger = get_logger("auth")

        # Token will be stored after successful authentication
        self._authenticated = False
        self._auth_mode = "client_credentials" if has_client_creds else "password"

    def authenticate(self) -> str:
        """Authenticate with Keycloak and store access token.

        Automatically selects authentication mode based on initialization:
        - Username/Password flow if (user+password) provided
        - Client Credentials flow if (client_id+client_secret) provided

        Returns:
            str: The access token obtained from Keycloak.

        Raises:
            BlackAntAuthenticationError: If login fails or token not received.
        """
        if self._auth_mode == "client_credentials":
            return self.authenticate_with_client_credentials()
        else:
            return self.authenticate_with_password()

    def authenticate_with_password(self) -> str:
        """Authenticate using OAuth2 Password Flow (username/password).

        Sends username/password to Keycloak login endpoint and
        stores the returned Bearer token for future API calls.

        Returns:
            str: The access token obtained from Keycloak.

        Raises:
            BlackAntAuthenticationError: If login fails or token not received.
        """
        self.logger.info(f"Authenticating user with password flow: {self.user}")

        data = {
            "username": self.user,
            "password": self.password
        }

        try:
            # Send login request to Keycloak
            # Determine if we should verify SSL (skip for localhost/dev)
            verify_ssl = not (
                "localhost" in self.login_url or
                "127.0.0.1" in self.login_url or
                "http://" in self.login_url or  # HTTP doesn't need SSL verification
                "dev.blackant.app" in self.login_url  # Dev environment self-signed cert
            )

            response = requests.post(
                self.login_url,
                data=data,
                verify=verify_ssl,
                timeout=30
            )
            response.raise_for_status()

            # Extract access token from response
            response_data = response.json()
            if "access_token" not in response_data:
                raise BlackAntAuthenticationError(
                    f"No access_token in login response: {response_data}"
                )

            access_token = response_data["access_token"]

            # Store token using thread-safe AuthTokenStore
            self.token_store.user_token = access_token
            self._authenticated = True

            self.logger.info("Password flow authentication successful")
            return access_token

        except requests.exceptions.RequestException as error:
            self.logger.error(f"Authentication request failed: {error}")
            raise BlackAntAuthenticationError(f"Login request failed: {error}") from error
        except (KeyError, ValueError) as error:
            self.logger.error(f"Invalid login response: {error}")
            raise BlackAntAuthenticationError(f"Invalid login response: {error}") from error

    def authenticate_with_client_credentials(self) -> str:
        """Authenticate using OAuth2 Client Credentials Flow.

        Uses client_id and client_secret to obtain a service account token
        from Keycloak. This is ideal for automation, CI/CD pipelines, and
        server-to-server communication where no user context is needed.

        Returns:
            str: The access token obtained from Keycloak.

        Raises:
            BlackAntAuthenticationError: If authentication fails or token not received.

        Example:
            >>> auth = BlackAntAuth(
            ...     client_id="my-service-account",
            ...     client_secret="xyz123..."
            ... )
            >>> token = auth.authenticate_with_client_credentials()
            >>> print(f"Service account authenticated: {token[:20]}...")
        """
        self.logger.info(f"Authenticating with client credentials flow: {self.client_id}")

        # OAuth2 Client Credentials Flow
        data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": "openid profile email"
        }

        try:
            # Determine if we should verify SSL (skip for localhost/dev)
            verify_ssl = not (
                "localhost" in self.login_url or
                "127.0.0.1" in self.login_url or
                "http://" in self.login_url or
                "dev.blackant.app" in self.login_url
            )

            # Send client credentials request to Keycloak token endpoint
            response = requests.post(
                self.login_url,
                data=data,
                verify=verify_ssl,
                timeout=self.request_timeout,
                headers={"Content-Type": "application/x-www-form-urlencoded"}
            )
            response.raise_for_status()

            # Extract access token from response
            response_data = response.json()
            if "access_token" not in response_data:
                raise BlackAntAuthenticationError(
                    f"No access_token in client credentials response: {response_data}"
                )

            access_token = response_data["access_token"]

            # Store token using thread-safe AuthTokenStore
            self.token_store.user_token = access_token
            self._authenticated = True

            self.logger.info("Client credentials authentication successful")
            return access_token

        except requests.exceptions.RequestException as error:
            self.logger.error(f"Client credentials authentication failed: {error}")
            raise BlackAntAuthenticationError(
                f"Client credentials authentication failed: {error}"
            ) from error
        except (KeyError, ValueError) as error:
            self.logger.error(f"Invalid client credentials response: {error}")
            raise BlackAntAuthenticationError(
                f"Invalid client credentials response: {error}"
            ) from error

    def get_token(self) -> str:
        """Get current access token, authenticate if needed.

        Returns the stored Bearer token. If not authenticated yet,
        performs authentication first.

        Returns:
            str: The current Bearer access token.

        Raises:
            BlackAntAuthenticationError: If authentication fails.
        """
        if not self._authenticated or not self.token_store.user_token:
            self.authenticate()

        token = self.token_store.user_token
        if not token:
            raise BlackAntAuthenticationError("No token available after authentication")

        return token

    def is_authenticated(self) -> bool:
        """Check if currently authenticated.

        Returns:
            bool: True if authenticated and token is available.
        """
        return self._authenticated and self.token_store.user_token is not None

    def refresh_token(self, refresh_token: str) -> str:
        """Refresh access token using refresh token.

        Args:
            refresh_token: The refresh token from initial login.

        Returns:
            New access token.

        Raises:
            BlackAntAuthenticationError: If token refresh fails.
        """
        try:
            self.logger.info("Refreshing access token")

            refresh_data = {
                "grant_type": "refresh_token",
                "refresh_token": refresh_token,
                "client_id": self.client_id
            }

            response = requests.post(
                f"{self.login_url.replace('/token', '/token')}",  # Ensure correct endpoint
                data=refresh_data,
                timeout=self.request_timeout,
                headers={"Content-Type": "application/x-www-form-urlencoded"}
            )

            response.raise_for_status()
            response_data = response.json()

            new_access_token = response_data["access_token"]
            self.token_store.user_token = new_access_token

            self.logger.info("Token refresh successful")
            return new_access_token

        except requests.exceptions.RequestException as error:
            self.logger.error(f"Token refresh failed: {error}")
            raise BlackAntAuthenticationError(f"Token refresh failed: {error}") from error
        except (KeyError, ValueError) as error:
            self.logger.error(f"Invalid refresh response: {error}")
            raise BlackAntAuthenticationError(f"Invalid refresh response: {error}") from error

    def verify_token(self, token: Optional[str] = None) -> Dict[str, Any]:
        """Verify token with Keycloak userinfo endpoint.

        Args:
            token: Token to verify, uses stored token if None.

        Returns:
            User information from token.

        Raises:
            BlackAntAuthenticationError: If token verification fails.
        """
        try:
            verify_token = token or self.token_store.user_token
            if not verify_token:
                raise BlackAntAuthenticationError("No token available for verification")

            if "/api/auth/login" in self.login_url:
                base_url = self.login_url.replace("/api/auth/login", "")
                userinfo_url = f"{base_url}/api/auth/verify"
            else:
                userinfo_url = self.login_url.replace(
                    "/protocol/openid-connect/token",
                    "/protocol/openid-connect/userinfo"
                )

            # Determine if we should verify SSL (skip for localhost/dev)
            verify_ssl = not (
                "localhost" in userinfo_url or
                "127.0.0.1" in userinfo_url or
                "http://" in userinfo_url  # HTTP doesn't need SSL verification
            )

            response = requests.post(  # POST for /api/auth/verify endpoint
                userinfo_url,
                headers={"Authorization": f"Bearer {verify_token}"},
                timeout=self.request_timeout,
                verify=verify_ssl
            )

            response.raise_for_status()
            user_info = response.json()

            self.logger.debug("Token verification successful")
            return user_info

        except requests.exceptions.RequestException as error:
            self.logger.error(f"Token verification failed: {error}")
            raise BlackAntAuthenticationError(f"Token verification failed: {error}") from error

    def get_admin_token(self, admin_user: str, admin_password: str) -> str:
        """Get admin token for Keycloak Admin API operations.

        Args:
            admin_user: Admin username.
            admin_password: Admin password.

        Returns:
            Admin access token.

        Raises:
            BlackAntAuthenticationError: If admin authentication fails.
        """
        try:
            self.logger.info(f"Getting admin token for user: {admin_user}")

            # Admin login uses master realm
            admin_login_url = self.login_url.replace(
                f"/realms/{self.realm}/",
                "/realms/master/"
            )

            admin_data = {
                "username": admin_user,
                "password": admin_password,
                "grant_type": "password",
                "client_id": "admin-cli"  # Keycloak admin CLI client
            }

            response = requests.post(
                admin_login_url,
                data=admin_data,
                timeout=self.request_timeout,
                headers={"Content-Type": "application/x-www-form-urlencoded"}
            )

            response.raise_for_status()
            response_data = response.json()

            admin_token = response_data["access_token"]
            self.token_store.admin_token = admin_token

            self.logger.info("Admin authentication successful")
            return admin_token

        except requests.exceptions.RequestException as error:
            self.logger.error(f"Admin authentication failed: {error}")
            raise BlackAntAuthenticationError(f"Admin login failed: {error}") from error
        except (KeyError, ValueError) as error:
            self.logger.error(f"Invalid admin login response: {error}")
            raise BlackAntAuthenticationError(f"Invalid admin response: {error}") from error

    def create_user(self, user_data: Dict[str, Any], admin_token: Optional[str] = None) -> str:
        """Create new user using Keycloak Admin API.

        Args:
            user_data: User creation data (username, email, etc.).
            admin_token: Admin token, uses stored if None.

        Returns:
            Created user ID.

        Raises:
            BlackAntAuthenticationError: If user creation fails.
        """
        try:
            token = admin_token or self.token_store.admin_token
            if not token:
                raise BlackAntAuthenticationError("Admin token required for user creation")

            admin_api_url = self.login_url.replace(
                "/protocol/openid-connect/token",
                f"/admin/realms/{self.realm}/users"
            )

            response = requests.post(
                admin_api_url,
                json=user_data,
                headers={
                    "Authorization": f"Bearer {token}",
                    "Content-Type": "application/json"
                },
                timeout=self.request_timeout
            )

            response.raise_for_status()

            # User ID is in Location header
            location = response.headers.get("Location", "")
            user_id = location.split("/")[-1] if location else ""

            self.logger.info(f"User created: {user_data.get('username', 'unknown')}")
            return user_id

        except requests.exceptions.RequestException as error:
            self.logger.error("User creation failed: %s", error)
            raise BlackAntAuthenticationError(f"User creation failed: {error}") from error

    def get_users(self, admin_token: Optional[str] = None, search: Optional[str] = None) -> List[Dict[str, Any]]:
        """List users using Keycloak Admin API.

        Args:
            admin_token: Admin token, uses stored if None.
            search: Optional search filter.

        Returns:
            List of user data.

        Raises:
            BlackAntAuthenticationError: If user listing fails.
        """
        try:
            token = admin_token or self.token_store.admin_token
            if not token:
                raise BlackAntAuthenticationError("Admin token required for user listing")

            admin_api_url = self.login_url.replace(
                "/protocol/openid-connect/token",
                f"/admin/realms/{self.realm}/users"
            )

            params = {}
            if search:
                params["search"] = search

            response = requests.get(
                admin_api_url,
                params=params,
                headers={"Authorization": f"Bearer {token}"},
                timeout=self.request_timeout
            )

            response.raise_for_status()
            users = response.json()

            self.logger.debug(f"Retrieved {len(users)} users")
            return users

        except requests.exceptions.RequestException as error:
            self.logger.error(f"User listing failed: {error}")
            raise BlackAntAuthenticationError(f"User listing failed: {error}") from error

    def logout(self):
        """Clear stored authentication token.

        Removes the stored Bearer token from token store.
        """
        self.logger.info("Logging out user")
        self.token_store.user_token = None
        self._authenticated = False
