"""
Authentication Service for Glyphh Runtime.

Handles JWT token validation, authorization checks, and security weight computation.
Supports three deployment modes: local (no auth), self-hosted, and cloud.

Permissions are keyed by org_id. No namespace concept.
"""

import hashlib
import logging
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Set

import jwt
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from domains.models.db_models import Token
from infrastructure.config import TierConfig, get_settings
from shared.exceptions import (
    AuthenticationException,
    AuthorizationException,
)

logger = logging.getLogger(__name__)


class Permission(str, Enum):
    """Permission levels for operations."""
    READ = "read"
    WRITE = "write"
    ADMIN = "admin"


@dataclass
class User:
    """Authenticated user with org-scoped permissions and resolved tier."""
    user_id: str
    org_permissions: Dict[str, Set[Permission]] = field(default_factory=dict)  # org_id -> permissions
    org_id: Optional[str] = None
    email: Optional[str] = None
    token_type: str = "jwt"  # jwt or webhook
    tier: TierConfig = field(default_factory=TierConfig.free)
    
    def has_permission(self, org_id: str, permission: Permission) -> bool:
        """Check if user has permission for org."""
        # Wildcard grants access to all orgs
        if "*" in self.org_permissions:
            if permission in self.org_permissions["*"]:
                return True
        if org_id not in self.org_permissions:
            return False
        return permission in self.org_permissions[org_id]
    
    def can_read(self, org_id: str) -> bool:
        return self.has_permission(org_id, Permission.READ)
    
    def can_write(self, org_id: str) -> bool:
        return self.has_permission(org_id, Permission.WRITE)
    
    def is_admin(self, org_id: str) -> bool:
        return self.has_permission(org_id, Permission.ADMIN)


class AuthService:
    """
    Authentication and authorization service.
    
    Permissions are keyed by org_id. Access checks take org_id and model_id
    as separate parameters.
    """
    
    def __init__(self, session: Optional[AsyncSession] = None):
        self._session = session
        self._settings = get_settings()

    async def validate_token(self, token: str) -> User:
        """Validate a token and return the authenticated user."""
        if self._settings.deployment_mode == "local":
            logger.debug("Local mode: bypassing authentication")
            return self._create_local_user()
        
        if not token:
            raise AuthenticationException("No token provided")
        
        if token.startswith("Bearer "):
            token = token[7:]
        
        # Try JWT first
        try:
            return await self._validate_jwt_token(token)
        except jwt.InvalidTokenError:
            pass
        
        # Try webhook token
        if self._session:
            try:
                return await self._validate_webhook_token(token)
            except AuthenticationException:
                pass
        
        raise AuthenticationException("Invalid or expired token")
    
    async def _validate_jwt_token(self, token: str) -> User:
        """Validate a JWT token."""
        if not self._settings.jwt_secret_key:
            raise AuthenticationException("JWT authentication not configured")
        
        try:
            payload = jwt.decode(
                token,
                self._settings.jwt_secret_key,
                algorithms=[self._settings.jwt_algorithm],
            )
        except jwt.ExpiredSignatureError:
            raise AuthenticationException("Token has expired")
        except jwt.InvalidTokenError as e:
            raise AuthenticationException(f"Invalid token: {e}")
        
        user_id = payload.get("sub")
        if not user_id:
            raise AuthenticationException("Token missing 'sub' claim")
        
        org_permissions = self._parse_org_permissions(payload)
        
        return User(
            user_id=user_id,
            org_permissions=org_permissions,
            org_id=payload.get("org_id"),
            email=payload.get("email"),
            token_type="jwt",
            tier=TierConfig.from_jwt_claims(payload),
        )
    
    async def _validate_webhook_token(self, token: str) -> User:
        """Validate a webhook token against the database."""
        if not self._session:
            raise AuthenticationException("Webhook token validation not available")
        
        token_hash = self._hash_token(token)
        
        result = await self._session.execute(
            select(Token).where(
                Token.token_hash == token_hash,
                Token.status == "active",
            )
        )
        db_token = result.scalar_one_or_none()
        
        if not db_token:
            raise AuthenticationException("Invalid webhook token")
        
        if db_token.expires_at and db_token.expires_at < datetime.utcnow():
            raise AuthenticationException("Webhook token has expired")
        
        permissions = {Permission(p) for p in db_token.permissions}
        
        # Token is scoped to org_id (required) and optionally model_id
        if db_token.org_id:
            org_permissions = {db_token.org_id: permissions}
        else:
            # Global token
            org_permissions = {"*": permissions}
        
        return User(
            user_id=f"webhook:{db_token.id}",
            org_permissions=org_permissions,
            org_id=db_token.org_id,
            token_type="webhook",
        )
    
    def _parse_org_permissions(
        self,
        payload: Dict[str, Any]
    ) -> Dict[str, Set[Permission]]:
        """
        Parse org permissions from JWT payload.
        
        Supports formats:
        - {"org_id": "abc", "permissions": ["read", "write"]}
        - {"orgs": ["abc", "def"], "permissions": {"abc": ["read"], "def": ["write"]}}
        - {"permissions": ["read"]}  (global)
        """
        org_permissions: Dict[str, Set[Permission]] = {}
        
        # Single org format
        if "org_id" in payload and isinstance(payload.get("permissions"), list):
            org = payload["org_id"]
            perms = {Permission(p) for p in payload["permissions"] if p in Permission.__members__.values()}
            org_permissions[org] = perms
            return org_permissions
        
        # Multi-org format
        org_list = payload.get("orgs", [])
        perm_dict = payload.get("permissions", {})
        
        for org in org_list:
            if org in perm_dict:
                perms = {Permission(p) for p in perm_dict[org] if p in Permission.__members__.values()}
            else:
                perms = {Permission.READ}
            org_permissions[org] = perms
        
        # Global permissions fallback
        if not org_permissions and "permissions" in payload:
            if isinstance(payload["permissions"], list):
                perms = {Permission(p) for p in payload["permissions"] if p in Permission.__members__.values()}
                org_permissions["*"] = perms
        
        return org_permissions
    
    def _create_local_user(self) -> User:
        """Create a user with full permissions for local mode."""
        return User(
            user_id="local",
            org_permissions={"*": {Permission.READ, Permission.WRITE, Permission.ADMIN}},
            token_type="local",
        )
    
    def _hash_token(self, token: str) -> str:
        return hashlib.sha256(token.encode()).hexdigest()

    async def check_access(
        self,
        user: User,
        org_id: str,
        model_id: str,
        operation: str,
    ) -> bool:
        """
        Check if user has permission for operation on org/model.
        
        Raises AuthorizationException if not authorized.
        """
        permission_map = {
            "read": Permission.READ,
            "write": Permission.WRITE,
            "admin": Permission.ADMIN,
            "search": Permission.READ,
            "create": Permission.WRITE,
            "update": Permission.WRITE,
            "delete": Permission.WRITE,
            "deploy": Permission.ADMIN,
            "unload": Permission.ADMIN,
            "config": Permission.ADMIN,
        }
        
        required_permission = permission_map.get(operation, Permission.READ)
        
        # Check wildcard access
        if "*" in user.org_permissions:
            if required_permission in user.org_permissions["*"]:
                return True
        
        # Check specific org access
        if user.has_permission(org_id, required_permission):
            return True
        
        # Admin implies all
        if user.is_admin(org_id):
            return True
        
        # Write implies read
        if required_permission == Permission.READ and user.can_write(org_id):
            return True
        
        raise AuthorizationException(
            user_id=user.user_id,
            org_id=org_id,
            model_id=model_id,
            operation=operation,
        )
    
    def compute_security_weight(
        self,
        user: User,
        glyph_metadata: Dict[str, Any],
    ) -> float:
        """
        Compute security weight for a glyph based on user permissions.
        
        Returns 0.0 (no access) to 1.0 (full access).
        """
        security_level = glyph_metadata.get("security_level", 0)
        required_clearance = glyph_metadata.get("required_clearance", [])
        
        if user.token_type == "local":
            return 1.0
        
        if "*" in user.org_permissions and Permission.ADMIN in user.org_permissions["*"]:
            return 1.0
        
        user_orgs = set(user.org_permissions.keys())
        if required_clearance:
            if not any(c in user_orgs for c in required_clearance):
                return 0.0
        
        if security_level == 0:
            return 1.0
        elif security_level == 1:
            return 0.8 if Permission.READ in user.org_permissions.get("*", set()) else 0.5
        elif security_level == 2:
            return 0.6 if Permission.WRITE in user.org_permissions.get("*", set()) else 0.3
        else:
            return 0.4 if Permission.ADMIN in user.org_permissions.get("*", set()) else 0.1
    
    def require_auth(self) -> bool:
        return self._settings.deployment_mode != "local"
