"""OAuth DCR Authentication Manager implementation.

This module implements RFC 7591 Dynamic Client Registration and manages
the OAuth client credentials lifecycle and token management.
"""

import asyncio
import logging
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
from urllib.parse import urljoin

import aiohttp

from ..exceptions import (
    DcrError,
    TokenError,
    TokenRefreshError,
    InvalidCredentialsError,
)
from ..models import (
    AuthenticationState,
    ClientCredentials,
    OAuthTokenResponse,
    ProxyConfig,
)
from ..logging_config import get_logger

logger = get_logger(__name__)


class AuthenticationManagerImpl:
    """Implementation of OAuth DCR and token management.
    
    This class handles:
    - OAuth Dynamic Client Registration (RFC 7591)
    - Client credentials storage and lifecycle management
    - Access token acquisition and refresh
    - Token validation and expiration handling
    - Failure tracking and diagnostic logging
    """
    
    # Thresholds for diagnostic warnings
    MAX_DCR_RETRIES_WARNING = 3
    MAX_TOKEN_FAILURES_WARNING = 5
    FAILURE_WINDOW_SECONDS = 300  # 5 minutes

    def __init__(self, config: ProxyConfig):
        """Initialize the authentication manager.
        
        Args:
            config: Proxy configuration containing OAuth provider details
        """
        self.config = config
        self._state = AuthenticationState()
        self._session: Optional[aiohttp.ClientSession] = None
        self._lock = asyncio.Lock()
        self._refresh_task: Optional[asyncio.Task] = None
        self._shutdown_event = asyncio.Event()
        
        # OAuth endpoints
        self._dcr_endpoint = urljoin(str(config.oauth_provider_url), "/register")
        self._token_endpoint = urljoin(str(config.oauth_provider_url), "/token")
        
        # Failure tracking for diagnostics (Requirement 6.4)
        self._dcr_failures: List[Dict[str, Any]] = []
        self._token_failures: List[Dict[str, Any]] = []
        self._last_diagnostic_log: Optional[datetime] = None
        
        logger.info(
            "Authentication manager initialized",
            oauth_provider=str(config.oauth_provider_url),
            dcr_endpoint=self._dcr_endpoint,
            token_endpoint=self._token_endpoint,
            client_name=config.client_name,
            scopes=config.scopes,
        )

    async def initialize(self) -> None:
        """Initialize authentication manager and perform DCR if needed.
        
        This method:
        1. Creates an HTTP session for OAuth requests
        2. Performs Dynamic Client Registration if no credentials exist
        3. Obtains an initial access token
        4. Starts automatic token refresh background task
        
        Raises:
            DcrError: If client registration fails
            TokenError: If initial token acquisition fails
        """
        async with self._lock:
            if self._session is None:
                self._session = aiohttp.ClientSession(
                    timeout=aiohttp.ClientTimeout(total=self.config.connection_timeout)
                )
            
            # Perform DCR if we don't have credentials
            if self._state.client_credentials is None:
                logger.info("No client credentials found, performing DCR")
                await self._perform_dcr_internal()
            
            # Get initial access token
            if not self._state.is_token_valid():
                logger.info("Obtaining initial access token")
                await self._obtain_token()
        
        # Start automatic token refresh background task
        if self._refresh_task is None or self._refresh_task.done():
            self._shutdown_event.clear()
            self._refresh_task = asyncio.create_task(
                self._auto_refresh_loop(),
                name="token-auto-refresh"
            )
            logger.info("Started automatic token refresh background task")

    async def get_access_token(self) -> str:
        """Get a valid access token, refreshing if necessary.
        
        This method ensures a valid token is always returned by:
        1. Checking if current token is valid
        2. Refreshing token if expired or near expiration
        3. Re-performing DCR if refresh fails
        
        Returns:
            Valid access token
            
        Raises:
            TokenError: If unable to obtain a valid token
        """
        async with self._lock:
            # Check if current token is valid
            if self._state.is_token_valid():
                return self._state.access_token  # type: ignore
            
            # Token needs refresh
            logger.info("Access token expired or invalid, refreshing")
            try:
                await self._obtain_token()
                return self._state.access_token  # type: ignore
            except TokenError as e:
                # If token refresh fails, try re-performing DCR
                logger.warning(f"Token refresh failed: {e}, attempting DCR")
                await self._perform_dcr_internal()
                await self._obtain_token()
                return self._state.access_token  # type: ignore

    async def refresh_token(self) -> str:
        """Refresh the access token.
        
        Returns:
            New access token
            
        Raises:
            TokenRefreshError: If token refresh fails
        """
        async with self._lock:
            await self._obtain_token()
            return self._state.access_token  # type: ignore

    async def perform_dcr(self) -> ClientCredentials:
        """Perform OAuth Dynamic Client Registration.
        
        Implements RFC 7591 Dynamic Client Registration Protocol.
        Sends a registration request to the OAuth provider and stores
        the returned client credentials.
        
        Returns:
            Client credentials from successful registration
            
        Raises:
            DcrError: If registration fails
        """
        async with self._lock:
            return await self._perform_dcr_internal()

    def is_token_valid(self) -> bool:
        """Check if current token is valid.
        
        Returns:
            True if token exists and is not expired
        """
        return self._state.is_token_valid()

    def get_state(self) -> AuthenticationState:
        """Get current authentication state.
        
        Returns:
            Current authentication state
        """
        return self._state.model_copy(deep=True)

    async def close(self) -> None:
        """Close the authentication manager and cleanup resources."""
        # Signal shutdown to background task
        self._shutdown_event.set()
        
        # Cancel and wait for refresh task
        if self._refresh_task and not self._refresh_task.done():
            self._refresh_task.cancel()
            try:
                await self._refresh_task
            except asyncio.CancelledError:
                logger.debug("Token refresh task cancelled")
        
        # Close HTTP session
        if self._session:
            await self._session.close()
            self._session = None
        
        logger.info(
            "Authentication manager closed",
            total_dcr_failures=len(self._dcr_failures),
            total_token_failures=len(self._token_failures),
        )

    # ========================================================================
    # Diagnostic Methods (Requirement 6.3, 6.4)
    # ========================================================================

    def _record_dcr_failure(self, error: Exception, details: Optional[Dict[str, Any]] = None) -> None:
        """Record a DCR failure for diagnostic tracking.
        
        Args:
            error: The exception that occurred
            details: Additional failure details
        """
        failure_record = {
            "timestamp": datetime.now(),
            "error_type": type(error).__name__,
            "error_message": str(error),
            "details": details or {},
            "retry_count": self._state.dcr_retry_count,
        }
        self._dcr_failures.append(failure_record)
        
        # Keep only recent failures (within window)
        cutoff = datetime.now() - timedelta(seconds=self.FAILURE_WINDOW_SECONDS)
        self._dcr_failures = [
            f for f in self._dcr_failures if f["timestamp"] > cutoff
        ]
        
        # Log diagnostic information if threshold exceeded
        if len(self._dcr_failures) >= self.MAX_DCR_RETRIES_WARNING:
            self._log_dcr_diagnostics()

    def _record_token_failure(self, error: Exception, details: Optional[Dict[str, Any]] = None) -> None:
        """Record a token acquisition failure for diagnostic tracking.
        
        Args:
            error: The exception that occurred
            details: Additional failure details
        """
        failure_record = {
            "timestamp": datetime.now(),
            "error_type": type(error).__name__,
            "error_message": str(error),
            "details": details or {},
        }
        self._token_failures.append(failure_record)
        
        # Keep only recent failures (within window)
        cutoff = datetime.now() - timedelta(seconds=self.FAILURE_WINDOW_SECONDS)
        self._token_failures = [
            f for f in self._token_failures if f["timestamp"] > cutoff
        ]
        
        # Log diagnostic information if threshold exceeded
        if len(self._token_failures) >= self.MAX_TOKEN_FAILURES_WARNING:
            self._log_token_diagnostics()

    def _log_dcr_diagnostics(self) -> None:
        """Log comprehensive diagnostic information for repeated DCR failures.
        
        Implements Requirement 6.4: Provide clear diagnostic information for repeated failures.
        """
        # Avoid spamming logs - only log once per minute
        if self._last_diagnostic_log:
            time_since_last = (datetime.now() - self._last_diagnostic_log).total_seconds()
            if time_since_last < 60:
                return
        
        self._last_diagnostic_log = datetime.now()
        
        # Analyze failure patterns
        error_types = {}
        for failure in self._dcr_failures:
            error_type = failure["error_type"]
            error_types[error_type] = error_types.get(error_type, 0) + 1
        
        # Get most recent failure details
        recent_failure = self._dcr_failures[-1] if self._dcr_failures else None
        
        logger.error(
            "DIAGNOSTIC: Repeated DCR failures detected",
            failure_count=len(self._dcr_failures),
            failure_window_seconds=self.FAILURE_WINDOW_SECONDS,
            retry_count=self._state.dcr_retry_count,
            error_types=error_types,
            most_recent_error=recent_failure["error_message"] if recent_failure else None,
            most_recent_details=recent_failure["details"] if recent_failure else None,
            dcr_endpoint=self._dcr_endpoint,
            oauth_provider=str(self.config.oauth_provider_url),
            client_name=self.config.client_name,
            scopes=self.config.scopes,
            last_dcr_attempt=self._state.last_dcr_attempt.isoformat() if self._state.last_dcr_attempt else None,
            has_credentials=self._state.client_credentials is not None,
            diagnostic_suggestions=[
                "Check OAuth provider availability and endpoint URLs",
                "Verify client_name and scopes are valid for the OAuth provider",
                "Check network connectivity to OAuth provider",
                "Review OAuth provider logs for registration errors",
                "Verify OAuth provider supports RFC 7591 Dynamic Client Registration",
            ],
        )

    def _log_token_diagnostics(self) -> None:
        """Log comprehensive diagnostic information for repeated token failures.
        
        Implements Requirement 6.4: Provide clear diagnostic information for repeated failures.
        """
        # Avoid spamming logs - only log once per minute
        if self._last_diagnostic_log:
            time_since_last = (datetime.now() - self._last_diagnostic_log).total_seconds()
            if time_since_last < 60:
                return
        
        self._last_diagnostic_log = datetime.now()
        
        # Analyze failure patterns
        error_types = {}
        for failure in self._token_failures:
            error_type = failure["error_type"]
            error_types[error_type] = error_types.get(error_type, 0) + 1
        
        # Get most recent failure details
        recent_failure = self._token_failures[-1] if self._token_failures else None
        
        # Check credential status
        credentials = self._state.client_credentials
        credentials_expired = credentials.is_expired() if credentials else None
        
        logger.error(
            "DIAGNOSTIC: Repeated token acquisition failures detected",
            failure_count=len(self._token_failures),
            failure_window_seconds=self.FAILURE_WINDOW_SECONDS,
            error_types=error_types,
            most_recent_error=recent_failure["error_message"] if recent_failure else None,
            most_recent_details=recent_failure["details"] if recent_failure else None,
            token_endpoint=self._token_endpoint,
            oauth_provider=str(self.config.oauth_provider_url),
            has_credentials=credentials is not None,
            credentials_expired=credentials_expired,
            client_id=credentials.client_id if credentials else None,
            scopes=self.config.scopes,
            token_expires_at=self._state.token_expires_at.isoformat() if self._state.token_expires_at else None,
            is_authenticated=self._state.is_authenticated,
            diagnostic_suggestions=[
                "Check if client credentials are valid and not expired",
                "Verify token endpoint URL is correct",
                "Check network connectivity to OAuth provider",
                "Review OAuth provider logs for token errors",
                "Verify requested scopes are authorized for the client",
                "Consider re-performing DCR to obtain fresh credentials",
            ],
        )

    def get_diagnostic_info(self) -> Dict[str, Any]:
        """Get diagnostic information about authentication state.
        
        Returns:
            Dictionary with diagnostic information
        """
        credentials = self._state.client_credentials
        
        return {
            "is_authenticated": self._state.is_authenticated,
            "is_token_valid": self._state.is_token_valid(),
            "token_valid": self._state.is_token_valid(),  # Backward compatibility
            "has_credentials": credentials is not None,
            "client_id": credentials.client_id if credentials else None,
            "credentials_expired": credentials.is_expired() if credentials else None,
            "dcr_retry_count": self._state.dcr_retry_count,
            "last_dcr_attempt": self._state.last_dcr_attempt.isoformat() if self._state.last_dcr_attempt else None,
            "token_expires_at": self._state.token_expires_at.isoformat() if self._state.token_expires_at else None,
            "recent_dcr_failures": len(self._dcr_failures),
            "recent_token_failures": len(self._token_failures),
            "dcr_endpoint": self._dcr_endpoint,
            "token_endpoint": self._token_endpoint,
            "oauth_provider": str(self.config.oauth_provider_url),
        }

    # ========================================================================
    # Internal Methods
    # ========================================================================

    async def _auto_refresh_loop(self) -> None:
        """Background task that automatically refreshes tokens before expiration.
        
        This task runs continuously and:
        1. Checks token expiration status periodically
        2. Refreshes tokens proactively before they expire
        3. Handles refresh failures gracefully
        4. Respects shutdown signals
        """
        logger.info("Token auto-refresh loop started")
        
        while not self._shutdown_event.is_set():
            try:
                # Calculate sleep duration based on token expiration
                sleep_duration = self._calculate_refresh_interval()
                
                # Wait for either shutdown signal or refresh interval
                try:
                    await asyncio.wait_for(
                        self._shutdown_event.wait(),
                        timeout=sleep_duration
                    )
                    # Shutdown event was set
                    break
                except asyncio.TimeoutError:
                    # Time to check/refresh token
                    pass
                
                # Check if token needs refresh
                if self._state.needs_token_refresh():
                    logger.info("Token needs refresh, refreshing proactively")
                    try:
                        async with self._lock:
                            await self._obtain_token()
                        logger.info("Token refreshed successfully")
                    except Exception as e:
                        logger.error(f"Failed to refresh token in background: {e}")
                        # Continue loop - will retry on next iteration
                
            except asyncio.CancelledError:
                logger.info("Token auto-refresh loop cancelled")
                break
            except Exception as e:
                logger.error(f"Unexpected error in token auto-refresh loop: {e}")
                # Sleep briefly before retrying to avoid tight loop on persistent errors
                await asyncio.sleep(5)
        
        logger.info("Token auto-refresh loop stopped")

    def _calculate_refresh_interval(self) -> float:
        """Calculate how long to wait before next token refresh check.
        
        Returns:
            Sleep duration in seconds
        """
        if not self._state.token_expires_at:
            # No token yet, check again soon
            return 10.0
        
        # Calculate time until token expires
        time_until_expiry = (
            self._state.token_expires_at - datetime.now()
        ).total_seconds()
        
        # Refresh when 60 seconds remain (or sooner if token expires soon)
        # Check at least every 30 seconds to catch any issues
        if time_until_expiry <= 60:
            # Token expires soon or already expired, refresh immediately
            return 0.1
        elif time_until_expiry <= 120:
            # Less than 2 minutes, check frequently
            return 10.0
        else:
            # Refresh when 60 seconds remain
            # Sleep until that time, but check at least every 30 seconds
            sleep_time = time_until_expiry - 60
            return min(sleep_time, 30.0)

    async def _perform_dcr_internal(self) -> ClientCredentials:
        """Internal method to perform DCR without lock (lock must be held).
        
        Implements RFC 7591 Dynamic Client Registration:
        1. Sends POST request to registration endpoint
        2. Includes client metadata (name, scopes, grant types)
        3. Parses response and stores credentials
        
        Returns:
            Client credentials from registration
            
        Raises:
            DcrError: If registration fails
        """
        if self._session is None:
            raise DcrError("Session not initialized")
        
        # Build DCR request according to RFC 7591
        # Support both client_credentials and authorization_code flows
        dcr_request = {
            "client_name": self.config.client_name,
            "grant_types": ["authorization_code", "refresh_token"],
            "token_endpoint_auth_method": "client_secret_post",
            "scope": " ".join(self.config.scopes),
            "redirect_uris": ["http://localhost:8080/callback"],
        }
        
        logger.info(
            "Performing DCR",
            endpoint=self._dcr_endpoint,
            client_name=self.config.client_name,
            scopes=self.config.scopes,
            retry_count=self._state.dcr_retry_count,
        )
        
        try:
            async with self._session.post(
                self._dcr_endpoint,
                json=dcr_request,
                headers={"Content-Type": "application/json"},
            ) as response:
                response_data = await response.json()
                
                if response.status != 201:
                    error_msg = response_data.get("error_description", "DCR failed")
                    error_code = response_data.get("error", "unknown_error")
                    
                    logger.error(
                        "DCR failed",
                        status=response.status,
                        error_code=error_code,
                        error_description=error_msg,
                        response_data=response_data,
                        endpoint=self._dcr_endpoint,
                        retry_count=self._state.dcr_retry_count,
                    )
                    
                    self._state.dcr_retry_count += 1
                    self._state.last_dcr_attempt = datetime.now()
                    
                    error = DcrError(
                        f"DCR failed with status {response.status}: {error_msg}",
                        details=response_data
                    )
                    self._record_dcr_failure(error, {
                        "status": response.status,
                        "error_code": error_code,
                        "endpoint": self._dcr_endpoint,
                    })
                    raise error
                
                # Parse DCR response according to RFC 7591
                client_id = response_data.get("client_id")
                client_secret = response_data.get("client_secret")
                
                if not client_id or not client_secret:
                    error = DcrError(
                        "Invalid DCR response: missing client_id or client_secret",
                        details=response_data
                    )
                    self._record_dcr_failure(error, {
                        "status": response.status,
                        "response_data": response_data,
                    })
                    raise error
                
                # Handle optional client_secret_expires_at
                expires_at = None
                if "client_secret_expires_at" in response_data:
                    expires_timestamp = response_data["client_secret_expires_at"]
                    if expires_timestamp > 0:  # 0 means never expires
                        expires_at = datetime.fromtimestamp(expires_timestamp)
                
                credentials = ClientCredentials(
                    client_id=client_id,
                    client_secret=client_secret,
                    expires_at=expires_at,
                )
                
                # Update state
                self._state.client_credentials = credentials
                self._state.last_dcr_attempt = datetime.now()
                self._state.dcr_retry_count = 0
                
                logger.info(
                    "DCR successful",
                    client_id=client_id,
                    expires_at=expires_at.isoformat() if expires_at else "never",
                    endpoint=self._dcr_endpoint,
                )
                return credentials
                
        except aiohttp.ClientError as e:
            logger.error(
                "Network error during DCR",
                error=str(e),
                error_type=type(e).__name__,
                endpoint=self._dcr_endpoint,
                retry_count=self._state.dcr_retry_count,
            )
            self._state.dcr_retry_count += 1
            self._state.last_dcr_attempt = datetime.now()
            
            error = DcrError(f"Network error during DCR: {e}")
            self._record_dcr_failure(error, {
                "error_type": type(e).__name__,
                "endpoint": self._dcr_endpoint,
            })
            raise error
        except Exception as e:
            if isinstance(e, DcrError):
                raise
            logger.error(
                "Unexpected error during DCR",
                error=str(e),
                error_type=type(e).__name__,
                endpoint=self._dcr_endpoint,
                retry_count=self._state.dcr_retry_count,
                exc_info=True,
            )
            self._state.dcr_retry_count += 1
            self._state.last_dcr_attempt = datetime.now()
            
            error = DcrError(f"Unexpected error during DCR: {e}")
            self._record_dcr_failure(error, {
                "error_type": type(e).__name__,
                "endpoint": self._dcr_endpoint,
            })
            raise error

    async def _perform_authorization_code_flow(self) -> None:
        """Perform OAuth authorization code flow with browser-based login.
        
        This method:
        1. Generates a code verifier and challenge for PKCE
        2. Opens a browser to the authorization URL
        3. Starts a local HTTP server to receive the callback
        4. Exchanges the authorization code for tokens
        
        Raises:
            TokenError: If authorization fails
        """
        import secrets
        import hashlib
        import base64
        import webbrowser
        from http.server import HTTPServer, BaseHTTPRequestHandler
        from urllib.parse import urlencode, parse_qs, urlparse
        
        if self._state.client_credentials is None:
            raise TokenError("No client credentials available for authorization")
        
        credentials = self._state.client_credentials
        
        # Generate PKCE code verifier and challenge
        code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')
        code_challenge = base64.urlsafe_b64encode(
            hashlib.sha256(code_verifier.encode('utf-8')).digest()
        ).decode('utf-8').rstrip('=')
        
        # Build authorization URL with resource parameter per RFC 8707
        auth_params = {
            "client_id": credentials.client_id,
            "response_type": "code",
            "redirect_uri": "http://localhost:8080/callback",
            "scope": " ".join(self.config.scopes),
            "code_challenge": code_challenge,
            "code_challenge_method": "S256",
            "resource": str(self.config.mcp_server_url),  # RFC 8707 resource indicator
        }
        
        auth_url = f"{self.config.oauth_provider_url}authorize?{urlencode(auth_params)}"
        
        logger.info(
            "Starting authorization code flow",
            auth_url=auth_url,
            redirect_uri="http://localhost:8080/callback",
        )
        
        # Container to store the authorization code
        auth_code_container = {"code": None, "error": None}
        
        # Create callback handler
        class CallbackHandler(BaseHTTPRequestHandler):
            def do_GET(self):
                # Parse query parameters
                query = parse_qs(urlparse(self.path).query)
                
                if "code" in query:
                    auth_code_container["code"] = query["code"][0]
                    self.send_response(200)
                    self.send_header("Content-type", "text/html")
                    self.end_headers()
                    self.wfile.write(b"<html><body><h1>Authorization successful!</h1><p>You can close this window and return to the application.</p></body></html>")
                elif "error" in query:
                    auth_code_container["error"] = query.get("error_description", ["Unknown error"])[0]
                    self.send_response(400)
                    self.send_header("Content-type", "text/html")
                    self.end_headers()
                    self.wfile.write(f"<html><body><h1>Authorization failed</h1><p>{auth_code_container['error']}</p></body></html>".encode())
                else:
                    self.send_response(400)
                    self.send_header("Content-type", "text/html")
                    self.end_headers()
                    self.wfile.write(b"<html><body><h1>Invalid callback</h1></body></html>")
            
            def log_message(self, format, *args):
                # Suppress HTTP server logs
                pass
        
        # Start local HTTP server for callback
        server = HTTPServer(("localhost", 8080), CallbackHandler)
        
        # Open browser
        print(f"\n{'='*60}")
        print("Opening browser for authentication...")
        print(f"If the browser doesn't open automatically, visit:")
        print(f"  {auth_url}")
        print(f"{'='*60}\n")
        
        webbrowser.open(auth_url)
        
        # Wait for callback (with timeout)
        import time
        timeout = 300  # 5 minutes
        start_time = time.time()
        
        while auth_code_container["code"] is None and auth_code_container["error"] is None:
            server.handle_request()
            if time.time() - start_time > timeout:
                raise TokenError("Authorization timeout - no response received within 5 minutes")
        
        server.server_close()
        
        # Check for errors
        if auth_code_container["error"]:
            raise TokenError(f"Authorization failed: {auth_code_container['error']}")
        
        if not auth_code_container["code"]:
            raise TokenError("No authorization code received")
        
        auth_code = auth_code_container["code"]
        logger.info("Authorization code received, exchanging for tokens")
        
        # Exchange authorization code for tokens
        # Include resource parameter per RFC 8707 and MCP spec
        # Use client_secret_post authentication (credentials in body, not Basic auth)
        token_request = {
            "grant_type": "authorization_code",
            "code": auth_code,
            "redirect_uri": "http://localhost:8080/callback",
            "code_verifier": code_verifier,
            "client_id": credentials.client_id,
            "client_secret": credentials.client_secret,  # client_secret_post method
            "resource": str(self.config.mcp_server_url),  # RFC 8707 - bind token to MCP server
        }
        
        try:
            async with self._session.post(
                self._token_endpoint,
                data=token_request,
                headers={"Content-Type": "application/x-www-form-urlencoded"},
            ) as response:
                response_data = await response.json()
                
                if response.status != 200:
                    error_msg = response_data.get("error_description", "Token exchange failed")
                    raise TokenError(f"Token exchange failed: {error_msg}")
                
                # Parse token response
                access_token = response_data.get("access_token")
                if not access_token:
                    raise TokenError("No access token in response")
                
                # Update state with new token
                expires_in = response_data.get("expires_in", 3600)
                expires_at = datetime.now() + timedelta(seconds=expires_in)
                
                self._state.access_token = access_token
                self._state.token_expires_at = expires_at
                self._state.refresh_token = response_data.get("refresh_token")
                self._state.is_authenticated = True
                
                logger.info(
                    "Access token obtained successfully",
                    expires_in=expires_in,
                    has_refresh_token=self._state.refresh_token is not None,
                )
                
        except Exception as e:
            error = TokenError(f"Token exchange failed: {e}")
            self._record_token_failure(error)
            raise error

    async def _obtain_token(self) -> None:
        """Internal method to obtain access token.
        
        First tries to use refresh token if available, otherwise falls back to
        authorization code flow with PKCE for user authentication.
        
        Raises:
            TokenError: If token acquisition fails
            InvalidCredentialsError: If client credentials are missing or invalid
        """
        if self._session is None:
            raise TokenError("Session not initialized")
        
        if self._state.client_credentials is None:
            raise InvalidCredentialsError("No client credentials available")
        
        credentials = self._state.client_credentials
        
        # Check if credentials are expired
        if credentials.is_expired():
            logger.warning(
                "Client credentials expired, performing DCR",
                expires_at=credentials.expires_at.isoformat() if credentials.expires_at else None,
            )
            await self._perform_dcr_internal()
            credentials = self._state.client_credentials
            if credentials is None:
                raise InvalidCredentialsError("Failed to obtain valid credentials")
        
        # Try to use refresh token first if available
        if self._state.refresh_token:
            logger.info("Attempting to use refresh token")
            try:
                await self._use_refresh_token()
                return
            except TokenError as e:
                logger.warning(f"Refresh token failed: {e}, falling back to authorization code flow")
                # Clear the invalid refresh token
                self._state.refresh_token = None
        
        # Fall back to authorization code flow with browser-based login
        await self._perform_authorization_code_flow()

    async def _use_refresh_token(self) -> None:
        """Use refresh token to obtain a new access token.
        
        Raises:
            TokenError: If refresh token exchange fails
        """
        if not self._state.refresh_token:
            raise TokenError("No refresh token available")
        
        if not self._state.client_credentials:
            raise TokenError("No client credentials available")
        
        credentials = self._state.client_credentials
        
        logger.info("Exchanging refresh token for new access token")
        
        # Use client_secret_post authentication (credentials in body, not Basic auth)
        token_request = {
            "grant_type": "refresh_token",
            "refresh_token": self._state.refresh_token,
            "client_id": credentials.client_id,
            "client_secret": credentials.client_secret,  # client_secret_post method
            "resource": str(self.config.mcp_server_url),  # RFC 8707 - bind token to MCP server
        }
        
        try:
            async with self._session.post(
                self._token_endpoint,
                data=token_request,
                headers={"Content-Type": "application/x-www-form-urlencoded"},
            ) as response:
                response_data = await response.json()
                
                if response.status != 200:
                    error_msg = response_data.get("error_description", "Refresh token exchange failed")
                    error = TokenError(f"Refresh token exchange failed: {error_msg}")
                    self._record_token_failure(error)
                    raise error
                
                # Parse token response
                access_token = response_data.get("access_token")
                if not access_token:
                    error = TokenError("No access token in refresh response")
                    self._record_token_failure(error)
                    raise error
                
                # Update state with new token
                expires_in = response_data.get("expires_in", 3600)
                expires_at = datetime.now() + timedelta(seconds=expires_in)
                
                self._state.access_token = access_token
                self._state.token_expires_at = expires_at
                self._state.is_authenticated = True
                
                # Update refresh token if a new one was provided
                new_refresh_token = response_data.get("refresh_token")
                if new_refresh_token:
                    self._state.refresh_token = new_refresh_token
                
                logger.info(
                    "Access token refreshed successfully",
                    expires_in=expires_in,
                    new_refresh_token_provided=new_refresh_token is not None,
                )
                
        except aiohttp.ClientError as e:
            error = TokenError(f"Refresh token exchange failed: {e}")
            self._record_token_failure(error)
            raise error

    def __repr__(self) -> str:
        """String representation of the authentication manager."""
        return (
            f"AuthenticationManagerImpl("
            f"authenticated={self._state.is_authenticated}, "
            f"token_valid={self._state.is_token_valid()})"
        )
