"""
OAuth 2.1 Token Manager
JWT generation, validation, and refresh token rotation
"""

import logging
import os
import secrets
import time
from datetime import UTC, datetime, timedelta
from typing import Any

import jwt

from .schemas import AccessToken, RefreshToken
from .storage import get_storage

logger = logging.getLogger(__name__)


class SecurityError(Exception):
    """Security-related error (e.g., token reuse)"""

    pass


class TokenManager:
    """
    OAuth 2.1 Token Manager.

    Features:
        - JWT access tokens (1 hour default)
        - Refresh tokens with rotation (7 days default)
        - Refresh token reuse detection
        - Audit logging
    """

    def __init__(self):
        self.storage = get_storage()

        # JWT Configuration — require explicit secret or generate random fallback
        self.jwt_secret = os.getenv("OAUTH_JWT_SECRET_KEY")
        if not self.jwt_secret:
            self.jwt_secret = secrets.token_urlsafe(64)
            logger.warning(
                "OAUTH_JWT_SECRET_KEY not set. Generated random JWT secret. "
                "All tokens will be invalidated on restart. "
                "Set OAUTH_JWT_SECRET_KEY in your .env for persistent tokens."
            )
        self.jwt_algorithm = os.getenv("OAUTH_JWT_ALGORITHM", "HS256")

        # Token TTLs (seconds)
        self.access_token_ttl = int(os.getenv("OAUTH_ACCESS_TOKEN_TTL", "3600"))  # 1 hour
        self.refresh_token_ttl = int(os.getenv("OAUTH_REFRESH_TOKEN_TTL", "604800"))  # 7 days

    def generate_access_token(
        self,
        client_id: str,
        scope: str,
        user_id: str | None = None,
        project_id: str = "*",
        resource: str | None = None,
    ) -> str:
        """
        Generate JWT access token.

        Args:
            client_id: OAuth client ID
            scope: Granted scopes (space-separated)
            user_id: User ID (optional, for user-based auth)
            project_id: Project ID for scoping (default: "*" for global)
            resource: Resource indicator for aud claim (RFC 8707)

        Returns:
            JWT access token
        """
        now = datetime.now(UTC)
        now_ts = int(time.time())
        exp_ts = now_ts + self.access_token_ttl
        expires_at = now + timedelta(seconds=self.access_token_ttl)

        # JWT payload (use time.time() for correct UTC timestamps)
        payload = {
            "client_id": client_id,
            "scope": scope,
            "project_id": project_id,
            "iat": now_ts,
            "exp": exp_ts,
            "nbf": now_ts,  # Not before
            "jti": secrets.token_urlsafe(16),  # JWT ID (unique)
        }

        if user_id:
            payload["sub"] = user_id  # Subject (user ID)

        if resource:
            payload["aud"] = resource

        # Encode JWT
        token = jwt.encode(payload, self.jwt_secret, algorithm=self.jwt_algorithm)

        # Store token metadata
        token_data = AccessToken(
            token=token,
            client_id=client_id,
            scope=scope,
            expires_at=expires_at,
            user_id=user_id,
            project_id=project_id,
            issued_at=now,
        )
        self.storage.save_access_token(token_data)

        logger.info(f"Generated access token for client {client_id} (scope: {scope})")

        return token

    def validate_access_token(self, token: str) -> dict[str, Any]:
        """
        Validate JWT access token.

        Args:
            token: JWT access token

        Returns:
            Decoded payload

        Raises:
            jwt.ExpiredSignatureError: Token expired
            jwt.InvalidTokenError: Invalid token
        """
        try:
            # Decode and verify JWT
            payload = jwt.decode(
                token,
                self.jwt_secret,
                algorithms=[self.jwt_algorithm],
                options={
                    "verify_signature": True,
                    "verify_exp": True,
                    "verify_nbf": True,
                    "verify_aud": False,  # Server is the resource server; no external aud check needed
                },
            )

            return payload

        except jwt.ExpiredSignatureError:
            logger.warning("Expired access token")
            raise
        except jwt.InvalidTokenError as e:
            logger.debug(f"Invalid access token: {e}")
            raise

    def generate_refresh_token(self, client_id: str, access_token: str) -> str:
        """
        Generate refresh token.

        Args:
            client_id: OAuth client ID
            access_token: Associated access token

        Returns:
            Refresh token (secure random string)
        """
        now = datetime.now(UTC)
        expires_at = now + timedelta(seconds=self.refresh_token_ttl)

        # Generate secure random token
        token = f"rt_{secrets.token_urlsafe(32)}"

        # Store refresh token
        token_data = RefreshToken(
            token=token,
            client_id=client_id,
            access_token=access_token,
            expires_at=expires_at,
            revoked=False,
            rotation_count=0,
            issued_at=now,
        )
        self.storage.save_refresh_token(token_data)

        logger.info(f"Generated refresh token for client {client_id}")

        return token

    def rotate_refresh_token(self, refresh_token: str, client_id: str) -> dict[str, Any]:
        """
        Rotate refresh token (OAuth 2.1 best practice).

        Security:
            - Old refresh token is immediately revoked
            - Reuse detection: if old token is used again, revoke all tokens

        Args:
            refresh_token: Current refresh token
            client_id: OAuth client ID

        Returns:
            {
                "access_token": str,
                "refresh_token": str,
                "token_type": "Bearer",
                "expires_in": int,
                "scope": str
            }

        Raises:
            ValueError: Invalid/expired refresh token
            SecurityError: Refresh token reuse detected
        """
        # Get refresh token (include revoked for reuse detection)
        token_data = self.storage.get_refresh_token(refresh_token, include_revoked=True)

        if not token_data:
            raise ValueError("Invalid or expired refresh token")

        # Check if revoked (reuse detection!)
        if token_data.revoked:
            logger.critical(
                f"Refresh token reuse detected for client {client_id}! "
                f"Revoking all tokens for this client."
            )

            # Log security event
            try:
                from core.audit_log import LogLevel, get_audit_logger

                audit_logger = get_audit_logger()
                audit_logger.log_system_event(
                    event=f"SECURITY: Refresh token reuse detected: {client_id}",
                    details={"client_id": client_id, "token": refresh_token[:20] + "..."},
                    level=LogLevel.CRITICAL,
                )
            except (ImportError, Exception):
                # Audit logging not available or failed
                pass

            # Revoke all tokens for this client (TODO: implement)
            # For now, just raise error
            raise SecurityError("Refresh token reuse detected - all tokens revoked")

        # Validate client_id match
        if token_data.client_id != client_id:
            raise ValueError("Client ID mismatch")

        # Get scope from old access token (if available)
        scope = "read"  # Default
        if token_data.access_token:
            try:
                old_payload = self.validate_access_token(token_data.access_token)
                scope = old_payload.get("scope", "read")
            except Exception:
                pass  # Old token might be expired, use default scope

        # Generate new tokens
        new_access_token = self.generate_access_token(
            client_id=client_id,
            scope=scope,
            user_id=None,  # TODO: get from old token
            project_id="*",
        )

        new_refresh_token = self.generate_refresh_token(
            client_id=client_id, access_token=new_access_token
        )

        # Revoke old refresh token
        self.storage.revoke_refresh_token(refresh_token)

        # Increment rotation count
        token_data.rotation_count += 1

        logger.info(
            f"Rotated refresh token for client {client_id} "
            f"(rotation #{token_data.rotation_count})"
        )

        # Log audit event
        try:
            from core.audit_log import LogLevel, get_audit_logger

            audit_logger = get_audit_logger()
            audit_logger.log_system_event(
                event=f"Refresh token rotated for {client_id}",
                details={"client_id": client_id, "rotation_count": token_data.rotation_count},
                level=LogLevel.INFO,
            )
        except (ImportError, Exception):
            # Audit logging not available or failed
            pass

        return {
            "access_token": new_access_token,
            "refresh_token": new_refresh_token,
            "token_type": "Bearer",
            "expires_in": self.access_token_ttl,
            "scope": scope,
        }

    def revoke_token(self, token: str, token_type: str = "refresh"):
        """
        Revoke token.

        Args:
            token: Token to revoke
            token_type: "refresh" or "access"
        """
        if token_type == "refresh":
            self.storage.revoke_refresh_token(token)
            logger.info("Revoked refresh token")

        # Access tokens cannot be revoked (JWT stateless)
        # They expire naturally after TTL


# Singleton
_token_manager: TokenManager | None = None


def get_token_manager() -> TokenManager:
    """Get singleton TokenManager instance"""
    global _token_manager
    if _token_manager is None:
        _token_manager = TokenManager()
    return _token_manager
