"""
Auto Token Refresh Service - Automatic OAuth Token Renewal

Provides automatic token refresh capabilities for user OAuth credentials.

Features:
- Single credential refresh with `refresh_if_needed()`
- Batch refresh for expiring tokens with `refresh_expiring_tokens()`
- Configurable refresh threshold (default: 1 hour before expiration)
- Automatic status tracking (active, needs_reauth)
- Error handling for revoked tokens and network failures

Example:
    from empowernow_common.vault.user_oauth import (
        PostgresUserOAuthProvider,
        AutoTokenRefreshService,
    )

    # Setup
    provider = PostgresUserOAuthProvider(session_factory, model_class)
    refresh_service = AutoTokenRefreshService(
        provider=provider,
        oauth_config_getter=get_oauth_config,  # Your config lookup function
    )

    # Refresh single credential if needed
    credential = await provider.get_credential(user_arn, system_key)
    refreshed = await refresh_service.refresh_if_needed(credential)

    # Or batch refresh all expiring tokens (for background job)
    stats = await refresh_service.refresh_expiring_tokens()
"""

import logging
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Callable, Dict, List, Optional, Protocol

import httpx

from empowernow_common.vault.user_oauth.base import BaseUserOAuthProvider
from empowernow_common.vault.user_oauth.types import (
    CredentialStatus,
    UserOAuthCredential,
)
from empowernow_common.vault.user_oauth.exceptions import (
    TokenRefreshError,
    RefreshTokenRevokedError,
    CredentialExpiredError,
)

logger = logging.getLogger(__name__)


@dataclass
class OAuthRefreshConfig:
    """Configuration for OAuth token refresh."""

    token_url: str
    client_id: str
    client_secret: Optional[str] = None

    # Additional parameters for some providers
    extra_params: Optional[Dict[str, str]] = None


class OAuthConfigGetter(Protocol):
    """Protocol for OAuth configuration lookup."""

    async def __call__(self, system_key: str) -> Optional[OAuthRefreshConfig]:
        """
        Get OAuth configuration for a system.

        Args:
            system_key: The system identifier (e.g., "salesforce", "jira")

        Returns:
            OAuthRefreshConfig if found, None otherwise
        """
        ...


class AutoTokenRefreshService:
    """
    Automatically refreshes OAuth tokens before they expire.

    This service handles the token refresh flow:
    1. Check if credential is expiring soon
    2. Call OAuth provider's token endpoint with refresh_token grant
    3. Store new tokens via the provider
    4. Handle errors (revoked tokens, network failures)

    Usage:
        service = AutoTokenRefreshService(
            provider=postgres_provider,
            oauth_config_getter=my_config_getter,
        )

        # Single credential refresh
        credential = await provider.get_credential(user_arn, system_key)
        if credential and credential.needs_refresh:
            refreshed = await service.refresh_if_needed(credential)

        # Batch refresh (for background job)
        stats = await service.refresh_expiring_tokens()
    """

    def __init__(
        self,
        provider: BaseUserOAuthProvider,
        oauth_config_getter: OAuthConfigGetter,
        refresh_threshold_minutes: int = 60,
        http_client: Optional[httpx.AsyncClient] = None,
    ):
        """
        Initialize auto-refresh service.

        Args:
            provider: User OAuth provider for credential storage
            oauth_config_getter: Async callable to get OAuth config for a system
            refresh_threshold_minutes: Minutes before expiration to trigger refresh
            http_client: Optional httpx client (creates one if not provided)
        """
        self._provider = provider
        self._get_oauth_config = oauth_config_getter
        self._refresh_threshold = timedelta(minutes=refresh_threshold_minutes)
        self._http_client = http_client
        self._owns_http_client = http_client is None

    async def _get_http_client(self) -> httpx.AsyncClient:
        """Get or create HTTP client."""
        if self._http_client is None:
            self._http_client = httpx.AsyncClient(timeout=30.0)
        return self._http_client

    async def close(self) -> None:
        """Close HTTP client if we created it."""
        if self._owns_http_client and self._http_client is not None:
            await self._http_client.aclose()
            self._http_client = None

    def should_refresh(self, credential: UserOAuthCredential) -> bool:
        """
        Check if credential should be refreshed.

        Returns True if:
        - Token expires within threshold period
        - Credential has a refresh_token
        - Status is active

        Args:
            credential: The credential to check

        Returns:
            True if refresh is needed
        """
        # Must have refresh token
        if not credential.refresh_token:
            return False

        # Must be active
        if credential.status != CredentialStatus.ACTIVE:
            return False

        # Must have expiration time
        if not credential.expires_at:
            return False

        # Check if expiring soon
        threshold_time = datetime.now(timezone.utc) + self._refresh_threshold
        return credential.expires_at < threshold_time

    async def refresh_if_needed(
        self,
        credential: UserOAuthCredential,
        tenant_id: Optional[str] = None,
    ) -> bool:
        """
        Refresh credential if it's expiring soon.

        Args:
            credential: The credential to potentially refresh
            tenant_id: Optional tenant ID for multi-tenant scenarios

        Returns:
            True if refresh was performed, False if not needed

        Raises:
            TokenRefreshError: If refresh fails
            RefreshTokenRevokedError: If refresh token was revoked
        """
        if not self.should_refresh(credential):
            return False

        await self.refresh_credential(
            user_arn=credential.user_arn,
            system_key=credential.system_key,
            refresh_token=credential.refresh_token,
            tenant_id=tenant_id or credential.tenant_id,
        )

        return True

    async def refresh_credential(
        self,
        user_arn: str,
        system_key: str,
        refresh_token: str,
        tenant_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Refresh OAuth tokens for a credential.

        Args:
            user_arn: User ARN
            system_key: System identifier
            refresh_token: The refresh token to use
            tenant_id: Optional tenant ID

        Returns:
            New token data from OAuth provider

        Raises:
            TokenRefreshError: If refresh fails
            RefreshTokenRevokedError: If refresh token was revoked
        """
        # Get OAuth config for this system
        oauth_config = await self._get_oauth_config(system_key)

        if not oauth_config:
            raise TokenRefreshError(
                user_arn=user_arn,
                system_key=system_key,
                reason=f"No OAuth configuration found for system '{system_key}'",
                retryable=False,
            )

        logger.info(
            "Refreshing OAuth token",
            extra={
                "user_arn": user_arn,
                "system_key": system_key,
                "token_url": oauth_config.token_url,
            }
        )

        # Build refresh request
        request_data = {
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "client_id": oauth_config.client_id,
        }

        if oauth_config.client_secret:
            request_data["client_secret"] = oauth_config.client_secret

        if oauth_config.extra_params:
            request_data.update(oauth_config.extra_params)

        # Make refresh request
        try:
            client = await self._get_http_client()
            response = await client.post(
                oauth_config.token_url,
                data=request_data,
                headers={
                    "Accept": "application/json",
                    "Content-Type": "application/x-www-form-urlencoded",
                },
            )
        except httpx.RequestError as e:
            logger.error(
                "Network error during token refresh",
                extra={
                    "user_arn": user_arn,
                    "system_key": system_key,
                    "error": str(e),
                }
            )
            # Increment failure count but don't mark needs_reauth yet
            await self._provider.increment_refresh_failure(
                user_arn, system_key, tenant_id
            )
            raise TokenRefreshError(
                user_arn=user_arn,
                system_key=system_key,
                reason=f"Network error: {e}",
                retryable=True,
            ) from e

        # Handle error responses
        if response.status_code != 200:
            error_data = self._parse_error_response(response)
            error_code = error_data.get("error", "")
            error_desc = error_data.get(
                "error_description",
                error_data.get("message", "Token refresh failed")
            )

            logger.error(
                "Token refresh failed",
                extra={
                    "user_arn": user_arn,
                    "system_key": system_key,
                    "status_code": response.status_code,
                    "error": error_code,
                    "error_description": error_desc,
                }
            )

            # Check for revoked token errors
            if error_code in ("invalid_grant", "invalid_token", "unauthorized"):
                await self._provider.mark_needs_reauth(
                    user_arn, system_key, tenant_id
                )
                raise RefreshTokenRevokedError(
                    user_arn=user_arn,
                    system_key=system_key,
                    reason=error_desc,
                )

            # Other errors - increment failure count
            await self._provider.increment_refresh_failure(
                user_arn, system_key, tenant_id
            )

            raise TokenRefreshError(
                user_arn=user_arn,
                system_key=system_key,
                reason=f"HTTP {response.status_code}: {error_desc}",
                retryable=response.status_code >= 500,  # Server errors are retryable
            )

        # Parse successful response
        try:
            token_data = response.json()
        except Exception as e:
            raise TokenRefreshError(
                user_arn=user_arn,
                system_key=system_key,
                reason=f"Invalid JSON response: {e}",
                retryable=False,
            ) from e

        # Validate response has access_token
        if "access_token" not in token_data:
            raise TokenRefreshError(
                user_arn=user_arn,
                system_key=system_key,
                reason="Response missing access_token",
                retryable=False,
            )

        # Store new tokens
        # Note: Some providers return a new refresh_token, some don't
        tokens_to_store = {
            "access_token": token_data["access_token"],
            "token_type": token_data.get("token_type", "Bearer"),
            "expires_in": token_data.get("expires_in", 3600),
            "scope": token_data.get("scope", ""),
        }

        # Use new refresh_token if provided, otherwise keep the old one
        if "refresh_token" in token_data:
            tokens_to_store["refresh_token"] = token_data["refresh_token"]
        else:
            tokens_to_store["refresh_token"] = refresh_token

        await self._provider.store_credential(
            user_arn=user_arn,
            system_key=system_key,
            tokens=tokens_to_store,
            tenant_id=tenant_id,
        )

        logger.info(
            "Token refreshed successfully",
            extra={
                "user_arn": user_arn,
                "system_key": system_key,
                "new_expires_in": token_data.get("expires_in"),
            }
        )

        return token_data

    def _parse_error_response(self, response: httpx.Response) -> Dict[str, Any]:
        """Parse error response from OAuth provider."""
        try:
            content_type = response.headers.get("content-type", "")
            if "application/json" in content_type:
                return response.json()
            return {"error_description": response.text[:500]}
        except Exception:
            return {"error_description": "Unknown error"}

    async def refresh_expiring_tokens(
        self,
        credentials: List[UserOAuthCredential],
    ) -> Dict[str, Any]:
        """
        Refresh multiple credentials that are expiring soon.

        This is designed for background job usage.

        Args:
            credentials: List of credentials to check and refresh

        Returns:
            Statistics dict:
            {
                "checked": 100,
                "refreshed": 5,
                "failed": 1,
                "skipped": 94,
            }
        """
        stats = {
            "checked": len(credentials),
            "refreshed": 0,
            "failed": 0,
            "skipped": 0,
        }

        for credential in credentials:
            try:
                if not self.should_refresh(credential):
                    stats["skipped"] += 1
                    continue

                await self.refresh_credential(
                    user_arn=credential.user_arn,
                    system_key=credential.system_key,
                    refresh_token=credential.refresh_token,
                    tenant_id=credential.tenant_id,
                )
                stats["refreshed"] += 1

            except Exception as e:
                stats["failed"] += 1
                logger.error(
                    "Failed to refresh credential",
                    extra={
                        "user_arn": credential.user_arn,
                        "system_key": credential.system_key,
                        "error": str(e),
                    }
                )

        logger.info(
            "Batch token refresh complete",
            extra=stats,
        )

        return stats


__all__ = [
    "AutoTokenRefreshService",
    "OAuthRefreshConfig",
    "OAuthConfigGetter",
]
