"""Authentication service for Microsoft Entra ID authentication.

This module provides the AuthService class for handling certificate-based
and client-secret authentication with Microsoft Graph API.
"""

import os
from datetime import datetime, timedelta, timezone
from pathlib import Path

from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from msal import ConfidentialClientApplication  # type: ignore[import-untyped]

from teams_phone.constants import TOKEN_EXPIRY_BUFFER_SECONDS
from teams_phone.exceptions import (
    AuthenticationError,
    ConfigurationError,
    NotFoundError,
)
from teams_phone.infrastructure import CacheManager, ConfigManager
from teams_phone.models import (
    AuthMethod,
    AuthStatus,
    AuthStatusType,
    CachedToken,
    TenantProfile,
)

# Microsoft Graph API default scope
SCOPES: list[str] = ["https://graph.microsoft.com/.default"]

# Environment variable name for client secret authentication
CLIENT_SECRET_ENV_VAR: str = "TEAMS_PHONE_CLIENT_SECRET"
"""Environment variable name for client secret authentication.

Note: Client-secret authentication is less secure than certificate-based
authentication and should only be used for development and testing purposes.
"""


class AuthService:
    """Service for authenticating with Microsoft Entra ID.

    Handles certificate-based and client-secret authentication methods,
    token caching, and authentication status management.

    Attributes:
        config_manager: Manager for tenant configuration.
        cache_manager: Manager for token caching.
    """

    def __init__(
        self, config_manager: ConfigManager, cache_manager: CacheManager
    ) -> None:
        """Initialize the AuthService.

        Args:
            config_manager: ConfigManager instance for tenant configuration.
            cache_manager: CacheManager instance for token storage.
        """
        self.config_manager = config_manager
        self.cache_manager = cache_manager

    def _load_certificate(self, cert_path: str) -> dict[str, str]:
        """Load certificate and extract private key and thumbprint.

        Reads a PEM file containing both the X.509 certificate and private key,
        extracts the SHA-1 thumbprint for MSAL authentication.

        Args:
            cert_path: Path to the PEM file containing certificate and private key.

        Returns:
            Dictionary with 'private_key' (PEM string) and 'thumbprint' (hex string).

        Raises:
            AuthenticationError: If the certificate file cannot be read or parsed.
        """
        path = Path(cert_path).expanduser()

        try:
            pem_data = path.read_bytes()
        except FileNotFoundError as e:
            raise AuthenticationError(
                f"Certificate file not found: {cert_path}",
                remediation=(
                    "Verify the certificate path in your tenant configuration.\n"
                    f"Expected path: {path}\n"
                    "Run 'teams-phone tenants show <name>' to check the configured cert_path."
                ),
            ) from e
        except PermissionError as e:
            raise AuthenticationError(
                f"Cannot read certificate file: {cert_path}",
                remediation=(
                    f"Check that you have read permissions for {path}.\n"
                    "The certificate file must be readable by the current user."
                ),
            ) from e

        # Load certificate for thumbprint extraction
        try:
            cert = x509.load_pem_x509_certificate(pem_data)
            thumbprint = cert.fingerprint(hashes.SHA1()).hex().upper()
        except ValueError as e:
            raise AuthenticationError(
                f"Invalid certificate format in {cert_path}: {e}",
                remediation=(
                    "The certificate file must be in PEM format.\n"
                    "Ensure it contains a valid X.509 certificate starting with "
                    "'-----BEGIN CERTIFICATE-----'."
                ),
            ) from e

        # Load private key
        try:
            private_key = serialization.load_pem_private_key(pem_data, password=None)
            private_key_pem = private_key.private_bytes(
                encoding=serialization.Encoding.PEM,
                format=serialization.PrivateFormat.PKCS8,
                encryption_algorithm=serialization.NoEncryption(),
            ).decode("utf-8")
        except ValueError as e:
            raise AuthenticationError(
                f"Invalid private key format in {cert_path}: {e}",
                remediation=(
                    "The PEM file must contain a valid private key.\n"
                    "Ensure it includes a section starting with "
                    "'-----BEGIN PRIVATE KEY-----' or '-----BEGIN RSA PRIVATE KEY-----'."
                ),
            ) from e
        except TypeError as e:
            # Raised when password is required but not provided
            raise AuthenticationError(
                f"Encrypted private key in {cert_path} requires a passphrase",
                remediation=(
                    "The private key in this certificate is encrypted.\n"
                    "Please provide an unencrypted certificate file, or decrypt the key:\n"
                    "  openssl rsa -in encrypted.pem -out decrypted.pem"
                ),
            ) from e

        return {
            "private_key": private_key_pem,
            "thumbprint": thumbprint,
        }

    def _get_client_secret(self) -> str:
        """Get client secret from environment variable.

        Client-secret authentication is less secure than certificate-based
        authentication and is intended for development/testing environments only.

        Returns:
            The client secret string from the environment variable.

        Raises:
            AuthenticationError: If the environment variable is not set or is empty.
        """
        secret = os.environ.get(CLIENT_SECRET_ENV_VAR)
        if not secret:  # Handles None and empty string
            raise AuthenticationError(
                f"Client secret not found in environment variable {CLIENT_SECRET_ENV_VAR}",
                remediation=(
                    f"Set the {CLIENT_SECRET_ENV_VAR} environment variable:\n"
                    f"  export {CLIENT_SECRET_ENV_VAR}='your-client-secret'\n"
                    "Note: Client-secret auth is for dev/test only. "
                    "Use certificate auth in production."
                ),
            )
        return secret

    def _create_msal_app(self, profile: TenantProfile) -> ConfidentialClientApplication:
        """Create an MSAL ConfidentialClientApplication for authentication.

        Supports both certificate-based and client-secret authentication methods.
        Certificate auth is recommended for production; client-secret is intended
        for development and testing only.

        Args:
            profile: Tenant profile with authentication configuration.

        Returns:
            Configured MSAL ConfidentialClientApplication instance.

        Raises:
            AuthenticationError: If certificate loading fails, client secret is
                missing, or an unsupported auth method is specified.
        """
        authority = f"https://login.microsoftonline.com/{profile.tenant_id}"

        if profile.auth_method == AuthMethod.CERTIFICATE:
            if profile.cert_path is None:
                raise AuthenticationError(
                    f"Certificate path not configured for tenant '{profile.name}'",
                    remediation=(
                        "Add cert_path to the tenant configuration:\n"
                        f"  teams-phone tenants add {profile.name} --cert-path /path/to/cert.pem"
                    ),
                )

            cert_credential = self._load_certificate(profile.cert_path)

            return ConfidentialClientApplication(
                client_id=str(profile.client_id),
                client_credential=cert_credential,
                authority=authority,
            )

        elif profile.auth_method == AuthMethod.CLIENT_SECRET:
            client_secret = self._get_client_secret()

            return ConfidentialClientApplication(
                client_id=str(profile.client_id),
                client_credential=client_secret,
                authority=authority,
            )

        # Unreachable with current AuthMethod enum, but defensive for future additions
        raise AuthenticationError(
            f"Auth method '{profile.auth_method.value}' is not supported",
            remediation=(
                "Supported auth methods are 'certificate' and 'client-secret'.\n"
                "Update your tenant configuration to use a supported method."
            ),
        )

    def _resolve_tenant(self, tenant_name: str | None) -> TenantProfile:
        """Resolve tenant name to a TenantProfile.

        If tenant_name is None, uses the default tenant from configuration.

        Args:
            tenant_name: The tenant profile name, or None to use default.

        Returns:
            TenantProfile for the specified or default tenant.

        Raises:
            ConfigurationError: If tenant_name is None and no default tenant is set.
            NotFoundError: If the specified tenant does not exist.
        """
        if tenant_name is None:
            default_name = self.config_manager.get_default_tenant()
            if default_name is None:
                raise ConfigurationError(
                    "No tenant specified and no default tenant configured",
                    remediation=(
                        "Either specify a tenant name or set a default tenant:\n"
                        "  teams-phone tenants default <tenant-name>"
                    ),
                )
            tenant_name = default_name

        return self.config_manager.get_tenant(tenant_name)

    def _needs_refresh(self, token: CachedToken) -> bool:
        """Check if token needs to be refreshed.

        Returns True if the token is expired or within the buffer period
        before expiration.

        Args:
            token: The cached token to check.

        Returns:
            True if token should be refreshed, False otherwise.
        """
        if token.is_expired():
            return True

        # Check if within buffer period before expiry
        buffer = timedelta(seconds=TOKEN_EXPIRY_BUFFER_SECONDS)
        now = datetime.now(timezone.utc)
        expires = token.expires_at
        if expires.tzinfo is None:
            expires = expires.replace(tzinfo=timezone.utc)

        return now >= (expires - buffer)

    def refresh_token(self, tenant_name: str | None = None) -> CachedToken:
        """Acquire a new token via MSAL and save to cache.

        Performs token acquisition using the configured authentication method
        (certificate or client-secret) and persists the token to cache.

        Args:
            tenant_name: The tenant profile name, or None to use default.

        Returns:
            CachedToken containing the new access token.

        Raises:
            ConfigurationError: If tenant_name is None and no default is set.
            NotFoundError: If the specified tenant does not exist.
            AuthenticationError: If MSAL authentication fails.
        """
        profile = self._resolve_tenant(tenant_name)
        resolved_name = profile.name

        app = self._create_msal_app(profile)
        result = app.acquire_token_for_client(scopes=SCOPES)

        if "access_token" not in result:
            error = result.get("error", "unknown_error")
            error_description = result.get(
                "error_description", "No description provided"
            )

            # Map common MSAL errors to helpful remediation
            remediation = self._get_error_remediation(error, error_description, profile)

            raise AuthenticationError(
                f"Failed to acquire token: {error} - {error_description}",
                remediation=remediation,
            )

        # Calculate expiration time
        expires_in = result.get("expires_in", 3600)  # Default to 1 hour
        expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in)

        # Create and save the cached token
        cached_token = CachedToken(
            access_token=result["access_token"],
            expires_at=expires_at,
            tenant_id=str(profile.tenant_id),
            scopes=SCOPES,
        )

        self.cache_manager.save_token(resolved_name, cached_token)

        return cached_token

    def _get_error_remediation(
        self, error: str, error_description: str, profile: TenantProfile
    ) -> str:
        """Generate remediation guidance for MSAL errors.

        Args:
            error: The MSAL error code.
            error_description: The MSAL error description.
            profile: The tenant profile being used.

        Returns:
            Human-readable remediation guidance.
        """
        if error == "invalid_client":
            if profile.auth_method == AuthMethod.CERTIFICATE:
                return (
                    "The certificate may be invalid or not registered with the app.\n"
                    "Verify that:\n"
                    "  1. The certificate is uploaded to Azure AD app registrations\n"
                    "  2. The certificate has not expired\n"
                    "  3. The thumbprint matches the registered certificate"
                )
            else:
                return (
                    "The client secret may be invalid or expired.\n"
                    "Verify that:\n"
                    "  1. The secret value is correct\n"
                    "  2. The secret has not expired in Azure AD\n"
                    "  3. You're using the secret value, not the secret ID"
                )
        elif error == "invalid_grant":
            return (
                "The authentication grant is invalid.\n"
                "This may indicate:\n"
                "  1. The tenant ID is incorrect\n"
                "  2. The app registration is not configured for this tenant\n"
                "  3. Required permissions have not been granted admin consent"
            )
        elif error == "unauthorized_client":
            return (
                "The client is not authorized for this operation.\n"
                "Ensure the app registration has:\n"
                "  1. 'Allow public client flows' set appropriately\n"
                "  2. Required API permissions configured\n"
                "  3. Admin consent granted for the required permissions"
            )
        else:
            return (
                f"Authentication failed with error '{error}'.\n"
                "Check the Azure AD app registration configuration and ensure\n"
                "all required permissions have been granted admin consent."
            )

    def get_token(self, tenant_name: str | None = None) -> str:
        """Get a valid access token, refreshing if necessary.

        Returns a cached token if still valid, otherwise acquires a new token
        via MSAL. Tokens are proactively refreshed when within 5 minutes of
        expiration to avoid using tokens that might expire mid-request.

        Args:
            tenant_name: The tenant profile name, or None to use default.

        Returns:
            A valid access token string.

        Raises:
            ConfigurationError: If tenant_name is None and no default is set.
            NotFoundError: If the specified tenant does not exist.
            AuthenticationError: If token acquisition fails.
        """
        profile = self._resolve_tenant(tenant_name)
        resolved_name = profile.name

        # Check for cached token
        cached_token = self.cache_manager.get_token(resolved_name)

        if cached_token is not None and not self._needs_refresh(cached_token):
            return cached_token.access_token

        # Token is missing, expired, or within buffer - refresh it
        new_token = self.refresh_token(resolved_name)
        return new_token.access_token

    def is_authenticated(self, tenant_name: str | None = None) -> bool:
        """Check if currently authenticated with a valid token.

        Returns True if there is a cached token that doesn't need refresh.
        This method never raises exceptions - it returns False for any error.

        Args:
            tenant_name: The tenant profile name, or None to use default.

        Returns:
            True if authenticated with valid token, False otherwise.
        """
        try:
            profile = self._resolve_tenant(tenant_name)
            resolved_name = profile.name

            cached_token = self.cache_manager.get_token(resolved_name)
            if cached_token is None:
                return False

            return not self._needs_refresh(cached_token)
        except (ConfigurationError, NotFoundError, AuthenticationError):
            return False

    def get_status(self, tenant_name: str | None = None) -> AuthStatus:
        """Get the current authentication status for a tenant.

        Returns detailed status information including authentication state,
        token expiration time, and error messages when applicable.

        Args:
            tenant_name: The tenant profile name, or None to use default.

        Returns:
            AuthStatus with current authentication state and details.

        Raises:
            ConfigurationError: If tenant_name is None and no default is set.
            NotFoundError: If the specified tenant does not exist.
        """
        profile = self._resolve_tenant(tenant_name)
        resolved_name = profile.name

        cached_token = self.cache_manager.get_token(resolved_name)

        if cached_token is None:
            return AuthStatus(
                status=AuthStatusType.UNAUTHENTICATED,
                tenant_name=resolved_name,
            )

        if cached_token.is_expired():
            return AuthStatus(
                status=AuthStatusType.EXPIRED,
                tenant_name=resolved_name,
                tenant_id=cached_token.tenant_id,
                expires_at=cached_token.expires_at,
            )

        return AuthStatus(
            status=AuthStatusType.AUTHENTICATED,
            tenant_name=resolved_name,
            tenant_id=cached_token.tenant_id,
            expires_at=cached_token.expires_at,
        )

    def login(self, tenant_name: str | None = None, force: bool = False) -> CachedToken:
        """Login and acquire an authentication token.

        Returns an existing valid token if already authenticated, unless force
        is True which always acquires a new token.

        Args:
            tenant_name: The tenant profile name, or None to use default.
            force: If True, acquire a new token even if already authenticated.

        Returns:
            CachedToken containing the access token.

        Raises:
            ConfigurationError: If tenant_name is None and no default is set.
            NotFoundError: If the specified tenant does not exist.
            AuthenticationError: If token acquisition fails.
        """
        profile = self._resolve_tenant(tenant_name)
        resolved_name = profile.name

        if not force:
            cached_token = self.cache_manager.get_token(resolved_name)
            if cached_token is not None and not self._needs_refresh(cached_token):
                return cached_token

        return self.refresh_token(resolved_name)

    def logout(self, tenant_name: str | None = None, all_tenants: bool = False) -> None:
        """Logout and clear cached tokens.

        Clears the cached token for a specific tenant or all tenants.

        Args:
            tenant_name: The tenant profile name, or None to use default.
                Ignored if all_tenants is True.
            all_tenants: If True, clear tokens for all configured tenants.

        Raises:
            ConfigurationError: If tenant_name is None, no default is set,
                and all_tenants is False.
            NotFoundError: If the specified tenant does not exist and
                all_tenants is False.
        """
        if all_tenants:
            self.cache_manager.clear_tokens()
            return

        profile = self._resolve_tenant(tenant_name)
        resolved_name = profile.name
        self.cache_manager.clear_tokens(resolved_name)
