# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Authentication module for backend API.
Handles JWT-based authentication, token generation, and user context.

Multi-Tenant Design:
- Token ist NICHT an einen Mandanten gebunden
- User arbeitet parallel in mehreren Mandanten (z.B. mehrere Browser-Tabs)
- Mandant-Kontext wird per Request-Header (X-Mandate-Id) bestimmt
- Request-Context kapselt User + Mandant + Feature-Instanz + geladene Rollen
"""

from typing import Optional, Dict, Any, Tuple, List
from fastapi import Depends, HTTPException, status, Request, Response, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
import logging
from slowapi import Limiter
from slowapi.util import get_remote_address

from modules.shared.configuration import APP_CONFIG
from modules.security.rootAccess import getRootDbAppConnector, getRootUser
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.datamodels.datamodelUam import User, AuthAuthority, AccessLevel
from modules.datamodels.datamodelSecurity import Token
from modules.datamodels.datamodelRbac import AccessRule

# Get Config Data
SECRET_KEY = APP_CONFIG.get("APP_JWT_KEY_SECRET")
ALGORITHM = APP_CONFIG.get("Auth_ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY"))
REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7"))

# Cookie-based Authentication Setup
class CookieAuth(HTTPBearer):
    """Cookie-based authentication that checks httpOnly cookies first, then Authorization header"""
    def __init__(self, auto_error: bool = True):
        super().__init__(auto_error=auto_error)
    
    async def __call__(self, request: Request) -> Optional[str]:
        # 1. Check httpOnly cookie first (preferred method)
        token = request.cookies.get('auth_token')
        if token:
            return token
        
        # 2. Fallback to Authorization header for API calls
        authorization = request.headers.get("Authorization")
        if authorization and authorization.startswith("Bearer "):
            return authorization.split(" ")[1]
        
        if self.auto_error:
            raise HTTPException(status_code=401, detail="Not authenticated")
        return None

# Initialize cookie-based auth
cookieAuth = CookieAuth(auto_error=False)

# Rate Limiter
limiter = Limiter(key_func=get_remote_address)

# Logger
logger = logging.getLogger(__name__)

# Note: JWT creation and cookie helpers moved to modules.auth.jwtService

def _getUserBase(token: str = Depends(cookieAuth)) -> User:
    """
    Extracts and validates the current user from the JWT token.
    
    Args:
        token: JWT Token from the Authorization header
        
    Returns:
        User model instance
        
    Raises:
        HTTPException: For invalid token or user
    """
    credentialsException = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid authentication credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    
    # Guard: token may be None or malformed when cookie/header is missing or bad
    if not token or not isinstance(token, str):
        logger.warning("Missing JWT Token (no cookie/header)")
        raise credentialsException
    # Basic JWT format check (header.payload.signature)
    try:
        if token.count(".") != 2:
            logger.warning("Malformed JWT token format")
            raise credentialsException
    except Exception:
        # If anything odd happens while checking format, treat as invalid creds
        raise credentialsException

    try:
        # Decode token
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        
        # Extract username from token
        username: str = payload.get("sub")
        if username is None:
            raise credentialsException
            
        # Extract user ID from token
        # MULTI-TENANT: mandateId is NO LONGER in the token - it comes from X-Mandate-Id header
        userId: str = payload.get("userId")
        authority: str = payload.get("authenticationAuthority")
        tokenId: Optional[str] = payload.get("jti")
        sessionId: Optional[str] = payload.get("sid") or payload.get("sessionId")
        
        # Only userId is required in token now (no mandateId)
        if not userId:
            logger.error(f"Missing userId in token")
            raise credentialsException
        
    except JWTError:
        logger.warning("Invalid JWT Token")
        raise credentialsException
    
    # Get root user and interface for database access
    rootUser = getRootUser()
    appInterface = getInterface(rootUser)
    
    # Retrieve user from database
    user = appInterface.getUserByUsername(username)
    
    if user is None:
        logger.warning(f"User {username} not found")
        raise credentialsException
        
    # Check if user is enabled
    if not user.enabled:
        logger.warning(f"User {username} is disabled")
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is disabled")
    
    # Ensure the user ID in token matches the user in database
    # MULTI-TENANT: mandateId is NO LONGER checked here - it comes from headers
    if str(user.id) != str(userId):
        logger.error(f"User ID mismatch: token(userId={userId}) vs user(id={user.id})")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="User context has changed. Please log in again.",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # For LOCAL gateway JWTs, enforce DB-backed token validity and revocation
    try:
        # Normalize authority to string for comparison
        normalized_authority = (str(authority).lower() if authority is not None else None)

        # If we have a token id, check if a corresponding DB token exists for local authority
        db_tokens = []
        if tokenId:
            try:
                dbApp = getRootDbAppConnector()
                db_tokens = dbApp.getRecordset(
                    Token, recordFilter={"id": tokenId}
                )
            except Exception as e:
                # Check if this is a table not found error (token table was deleted)
                if "does not exist" in str(e).lower() or "relation" in str(e).lower():
                    logger.error("Token table does not exist - database may have been reset")
                    raise HTTPException(
                        status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                        detail="Authentication service temporarily unavailable. Please contact administrator."
                    )
                db_tokens = []

        if db_tokens:
            # There is a server record for this token; enforce status and context when local
            db_token = db_tokens[0]
            token_authority = str(db_token.get("authority", "")).lower()
            if token_authority == str(AuthAuthority.LOCAL.value):
                # Must be active and match user/session
                # MULTI-TENANT: mandateId is NOT checked here - tokens are no longer mandate-bound
                active_token = appInterface.findActiveTokenById(
                    tokenId=tokenId,
                    userId=user.id,
                    authority=AuthAuthority.LOCAL,
                    sessionId=sessionId,
                    mandateId=None,  # Token is no longer mandate-bound
                )
                if not active_token:
                    logger.info(
                        f"Local JWT db record not active/valid: jti={tokenId}, userId={user.id}, sessionId={sessionId}"
                    )
                    raise credentialsException
        else:
            # No DB record for this token. If the claim says local (or missing/unknown), require DB record.
            if normalized_authority in (None, "", str(AuthAuthority.LOCAL.value)):
                logger.info("Local JWT without server record or missing authority claim")
                raise credentialsException
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error during local token validation: {str(e)}")
        raise credentialsException
    
    return user

def getCurrentUser(currentUser: User = Depends(_getUserBase)) -> User:
    """Get current active user with additional validation."""
    # Check if current user is enabled
    if not currentUser.enabled:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="User is disabled"
        )
    return currentUser


# =============================================================================
# MULTI-TENANT: Request Context System
# =============================================================================

class RequestContext:
    """
    Request context for multi-tenant operations.
    
    Contains user, mandate context, feature instance context, and loaded role IDs.
    This context is per-request (not persisted) - follows stateless design.
    
    IMPORTANT: SysAdmin also needs explicit membership for mandate context!
    isSysAdmin flag does NOT give implicit access to mandate data.
    """
    
    def __init__(self, user: User):
        self.user: User = user
        self.mandateId: Optional[str] = None
        self.featureInstanceId: Optional[str] = None
        self.roleIds: List[str] = []
        
        # Request-scoped cache: rules loaded only once per request
        self._cachedRules: Optional[List[tuple]] = None
        self._cachedHasSysAdminRole: Optional[bool] = None
    
    def getRules(self) -> List[tuple]:
        """
        Loads rules once per request (not across requests).
        Returns list of (priority, AccessRule) tuples.
        """
        if self._cachedRules is None:
            if not self.mandateId:
                # No mandate context = no rules
                self._cachedRules = []
            else:
                try:
                    rootUser = getRootUser()
                    appInterface = getInterface(rootUser)
                    self._cachedRules = appInterface.rbac.getRulesForUserBulk(
                        self.user.id,
                        self.mandateId,
                        self.featureInstanceId
                    )
                except Exception as e:
                    logger.error(f"Error loading RBAC rules: {e}")
                    self._cachedRules = []
        return self._cachedRules
    
    @property
    def isSysAdmin(self) -> bool:
        """Convenience property to check if user has the isSysAdmin FLAG.
        Category A only: true system operations (tokens, logs, databases)."""
        return getattr(self.user, 'isSysAdmin', False)
    
    @property
    def hasSysAdminRole(self) -> bool:
        """Check if user has sysadmin ROLE in root mandate (cached per request).
        Use for admin operations (Categories B/C/D/E) instead of isSysAdmin flag."""
        if self._cachedHasSysAdminRole is None:
            self._cachedHasSysAdminRole = _hasSysAdminRole(str(self.user.id))
        return self._cachedHasSysAdminRole


def getRequestContext(
    request: Request,
    mandateId: Optional[str] = Header(None, alias="X-Mandate-Id"),
    featureInstanceId: Optional[str] = Header(None, alias="X-Instance-Id"),
    currentUser: User = Depends(getCurrentUser)
) -> RequestContext:
    """
    Determines request context from headers.
    Checks authorization and loads role IDs.
    
    Security Model:
    - Regular users: Must be explicit members of mandates/feature instances
    - SysAdmin users: Can access ANY mandate for administrative operations.
      Root mandate roles (incl. sysadmin role) are loaded for RBAC-based authorization.
      Routes use ctx.hasSysAdminRole for admin checks (not ctx.isSysAdmin flag).
    
    Args:
        request: FastAPI Request object
        mandateId: Mandate ID from X-Mandate-Id header
        featureInstanceId: Feature instance ID from X-Instance-Id header
        currentUser: Current authenticated user
        
    Returns:
        RequestContext with user, mandate, roles
        
    Raises:
        HTTPException 403: If non-SysAdmin user is not member of mandate or has no feature access
    """
    ctx = RequestContext(user=currentUser)
    isSysAdmin = getattr(currentUser, 'isSysAdmin', False)
    
    # Get root interface for membership checks
    rootInterface = getRootInterface()
    
    if mandateId:
        # Check mandate membership
        membership = rootInterface.getUserMandate(currentUser.id, mandateId)
        
        if membership:
            # User is a member - load their roles
            if not membership.enabled:
                raise HTTPException(
                    status_code=status.HTTP_403_FORBIDDEN,
                    detail="Mandate membership is disabled"
                )
            ctx.mandateId = mandateId
            ctx.roleIds = rootInterface.getRoleIdsForUserMandate(membership.id)
        elif isSysAdmin:
            # SysAdmin can access any mandate for admin operations
            # Load root mandate roles for RBAC-based authorization (includes sysadmin role)
            ctx.mandateId = mandateId
            ctx.roleIds = _getRootMandateRoleIds(rootInterface, str(currentUser.id))
            logger.debug(f"SysAdmin {currentUser.id} accessing mandate {mandateId} with root mandate roles")
        else:
            # Regular user without membership - denied
            logger.warning(f"User {currentUser.id} is not member of mandate {mandateId}")
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Not member of mandate"
            )
    
    if featureInstanceId:
        # Check feature access
        access = rootInterface.getFeatureAccess(currentUser.id, featureInstanceId)
        
        if access:
            # User has access - load their instance roles
            if not access.enabled:
                raise HTTPException(
                    status_code=status.HTTP_403_FORBIDDEN,
                    detail="Feature access is disabled"
                )
            ctx.featureInstanceId = featureInstanceId
            instanceRoleIds = rootInterface.getRoleIdsForFeatureAccess(access.id)
            ctx.roleIds.extend(instanceRoleIds)
        elif isSysAdmin:
            # SysAdmin can access any feature instance for admin operations
            ctx.featureInstanceId = featureInstanceId
            # If no roles loaded yet, load root mandate roles
            if not ctx.roleIds:
                ctx.roleIds = _getRootMandateRoleIds(rootInterface, str(currentUser.id))
            logger.debug(f"SysAdmin {currentUser.id} accessing feature instance {featureInstanceId} with root mandate roles")
        else:
            # Regular user without access - denied
            logger.warning(f"User {currentUser.id} has no access to feature instance {featureInstanceId}")
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="No access to feature instance"
            )
    
    return ctx


def requireSysAdmin(currentUser: User = Depends(getCurrentUser)) -> User:
    """
    SysAdmin check for system-level operations.
    
    Use this dependency for endpoints that require SysAdmin privileges.
    SysAdmin has access to system-level operations, but NOT to mandate data.
    
    Args:
        currentUser: Current authenticated user
        
    Returns:
        User if they are a SysAdmin
        
    Raises:
        HTTPException 403: If user is not a SysAdmin
    """
    if not getattr(currentUser, 'isSysAdmin', False):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="SysAdmin privileges required"
        )
    
    # Audit for all SysAdmin actions
    try:
        from modules.shared.auditLogger import audit_logger
        audit_logger.logSecurityEvent(
            userId=str(currentUser.id),
            mandateId="system",
            action="sysadmin_action",
            details="System-level operation"
        )
    except Exception:
        # Don't fail if audit logging fails
        pass
    
    return currentUser


# =============================================================================
# SYSADMIN ROLE: RBAC-based admin checks (hybrid model)
# =============================================================================

def _getRootMandateRoleIds(rootInterface, userId: str) -> List[str]:
    """
    Load the user's role IDs from the root mandate.
    Used by auth middleware to provide RBAC roles for SysAdmin cross-mandate access.
    
    Args:
        rootInterface: Root database interface
        userId: User ID
        
    Returns:
        List of role IDs from root mandate membership, empty list if no membership
    """
    try:
        rootMandateId = rootInterface._getRootMandateId()
        if not rootMandateId:
            return []
        membership = rootInterface.getUserMandate(userId, rootMandateId)
        if not membership:
            return []
        return rootInterface.getRoleIdsForUserMandate(membership.id)
    except Exception as e:
        logger.error(f"Error loading root mandate roles: {e}")
        return []


def _hasSysAdminRole(userId: str) -> bool:
    """
    Check if a user has the sysadmin role in the root mandate.
    
    Standalone check that queries the database directly, independent of
    request context. Used for authorization checks where the sysadmin
    ROLE (not just the isSysAdmin flag) is required.
    
    Args:
        userId: User ID to check
        
    Returns:
        True if user has sysadmin role in root mandate
    """
    try:
        rootInterface = getRootInterface()
        roleIds = _getRootMandateRoleIds(rootInterface, str(userId))
        for roleId in roleIds:
            role = rootInterface.getRole(roleId)
            if role and role.roleLabel == "sysadmin":
                return True
        return False
    except Exception as e:
        logger.error(f"Error checking sysadmin role: {e}")
        return False


def requireSysAdminRole(currentUser: User = Depends(getCurrentUser)) -> User:
    """
    Require sysadmin ROLE for admin operations.
    
    Unlike requireSysAdmin (which checks the isSysAdmin FLAG for system-level ops),
    this dependency checks the sysadmin ROLE in the root mandate.
    Use for admin operations that should be RBAC-controlled (Category E).
    
    Args:
        currentUser: Current authenticated user
        
    Returns:
        User if they have the sysadmin role
        
    Raises:
        HTTPException 403: If user doesn't have sysadmin role
    """
    if not _hasSysAdminRole(str(currentUser.id)):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="SysAdmin role required"
        )
    
    # Audit
    try:
        from modules.shared.auditLogger import audit_logger
        audit_logger.logSecurityEvent(
            userId=str(currentUser.id),
            mandateId="system",
            action="sysadmin_role_action",
            details="Admin operation via sysadmin role"
        )
    except Exception:
        pass
    
    return currentUser

