"""
RBAC (Role-Based Access Control) using Casbin.

This module provides a complete RBAC implementation with:
- Role hierarchy support
- Resource-based permissions
- Domain/tenant isolation (optional)
- Async enforcement
- FastAPI integration

Usage:
    from app.core.rbac import enforcer, require_permission

    # Check permission
    if await enforcer.enforce(user_id, resource, action):
        ...

    # As FastAPI dependency
    @router.get("/users/{user_id}")
    async def get_user(
        user_id: int,
        _: None = Depends(require_permission("users", "read")),
    ):
        ...

    # Role management
    await enforcer.add_role_for_user(user_id, "admin")
    await enforcer.delete_role_for_user(user_id, "admin")
    roles = await enforcer.get_roles_for_user(user_id)
"""
import asyncio
from pathlib import Path
from typing import Any, List, Optional, Union

from fastapi import HTTPException, Request, status

from app.core.logger import logger
from app.core.singleton import Singleton

CASBIN_AVAILABLE = False

try:
    import casbin

    CASBIN_AVAILABLE = True
except ImportError:
    casbin = None  # type: ignore[assignment]


# Default RBAC model definition (ACL with role hierarchy)
DEFAULT_MODEL = """
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
"""

# RBAC model with domain/tenant support
DOMAIN_MODEL = """
[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
"""


class RBACEnforcer(Singleton):
    """
    Async RBAC enforcer using Casbin.

    Supports:
    - File-based policy storage (for development)
    - Database-based policy storage (for production)
    - In-memory policy (for testing)
    """

    _enforcer: Optional[Any] = None
    _initialized: bool = False
    _init_lock: Optional[asyncio.Lock] = None

    def __init__(self) -> None:
        if self._init_lock is None:
            self._init_lock = asyncio.Lock()

    async def init(
        self,
        model: Optional[str] = None,
        adapter: Optional[str] = None,
        use_domain: bool = False,
    ) -> None:
        """
        Initialize the RBAC enforcer.

        Args:
            model: Path to model file or model string (uses default if None)
            adapter: Database URL for policy storage (file path or "memory")
            use_domain: Whether to use domain/tenant model
        """
        if not CASBIN_AVAILABLE or casbin is None:
            logger.warning(
                "Casbin not installed. RBAC disabled. "
                "Install with: pip install casbin casbin-async-sqlalchemy-adapter"
            )
            return

        if self._init_lock is None:
            self._init_lock = asyncio.Lock()

        async with self._init_lock:
            if self._initialized:
                return

            model_text = model or (DOMAIN_MODEL if use_domain else DEFAULT_MODEL)

            import tempfile
            with tempfile.NamedTemporaryFile(
                mode="w", suffix=".conf", delete=False
            ) as f:
                f.write(model_text)
                model_path = f.name

            try:
                if adapter is None or adapter == "memory":
                    enforcer_instance = casbin.AsyncEnforcer(model_path)
                elif adapter.startswith(("postgresql", "mysql", "sqlite")):
                    from casbin_async_sqlalchemy_adapter import Adapter

                    db_adapter = Adapter(adapter)
                    enforcer_instance = casbin.AsyncEnforcer(model_path, db_adapter)
                else:
                    enforcer_instance = casbin.AsyncEnforcer(model_path, adapter)

                await enforcer_instance.load_policy()
                self._enforcer = enforcer_instance
                self._initialized = True
                logger.info("RBAC enforcer initialized")

            finally:
                Path(model_path).unlink(missing_ok=True)

    async def enforce(
        self,
        subject: Union[str, int],
        resource: str,
        action: str,
        domain: Optional[str] = None,
    ) -> bool:
        """
        Check if subject has permission to perform action on resource.

        Args:
            subject: User ID or role name
            resource: Resource identifier (e.g., "users", "posts")
            action: Action name (e.g., "read", "write", "delete")
            domain: Optional domain/tenant for multi-tenant apps

        Returns:
            True if permission granted, False otherwise
        """
        if not self._initialized or not self._enforcer:
            logger.warning("RBAC not initialized, allowing by default")
            return True

        sub = str(subject)

        try:
            if domain:
                return await self._enforcer.enforce(sub, domain, resource, action)
            return await self._enforcer.enforce(sub, resource, action)
        except Exception as e:
            logger.error(f"RBAC enforce error: {e}")
            return False

    async def add_policy(
        self,
        subject: Union[str, int],
        resource: str,
        action: str,
        domain: Optional[str] = None,
    ) -> bool:
        """Add a permission policy."""
        if not self._initialized or not self._enforcer:
            return False

        sub = str(subject)

        try:
            if domain:
                return await self._enforcer.add_policy(sub, domain, resource, action)
            return await self._enforcer.add_policy(sub, resource, action)
        except Exception as e:
            logger.error(f"RBAC add_policy error: {e}")
            return False

    async def remove_policy(
        self,
        subject: Union[str, int],
        resource: str,
        action: str,
        domain: Optional[str] = None,
    ) -> bool:
        """Remove a permission policy."""
        if not self._initialized or not self._enforcer:
            return False

        sub = str(subject)

        try:
            if domain:
                return await self._enforcer.remove_policy(sub, domain, resource, action)
            return await self._enforcer.remove_policy(sub, resource, action)
        except Exception as e:
            logger.error(f"RBAC remove_policy error: {e}")
            return False

    async def add_role_for_user(
        self,
        user: Union[str, int],
        role: str,
        domain: Optional[str] = None,
    ) -> bool:
        """Assign a role to a user."""
        if not self._initialized or not self._enforcer:
            return False

        user_str = str(user)

        try:
            if domain:
                return await self._enforcer.add_role_for_user_in_domain(
                    user_str, role, domain
                )
            return await self._enforcer.add_role_for_user(user_str, role)
        except Exception as e:
            logger.error(f"RBAC add_role_for_user error: {e}")
            return False

    async def delete_role_for_user(
        self,
        user: Union[str, int],
        role: str,
        domain: Optional[str] = None,
    ) -> bool:
        """Remove a role from a user."""
        if not self._initialized or not self._enforcer:
            return False

        user_str = str(user)

        try:
            if domain:
                return await self._enforcer.delete_role_for_user_in_domain(
                    user_str, role, domain
                )
            return await self._enforcer.delete_role_for_user(user_str, role)
        except Exception as e:
            logger.error(f"RBAC delete_role_for_user error: {e}")
            return False

    async def get_roles_for_user(
        self,
        user: Union[str, int],
        domain: Optional[str] = None,
    ) -> List[str]:
        """Get all roles for a user."""
        if not self._initialized or not self._enforcer:
            return []

        user_str = str(user)

        try:
            if domain:
                return await self._enforcer.get_roles_for_user_in_domain(
                    user_str, domain
                )
            return await self._enforcer.get_roles_for_user(user_str)
        except Exception as e:
            logger.error(f"RBAC get_roles_for_user error: {e}")
            return []

    async def get_users_for_role(
        self,
        role: str,
        domain: Optional[str] = None,
    ) -> List[str]:
        """Get all users with a specific role."""
        if not self._initialized or not self._enforcer:
            return []

        try:
            if domain:
                return await self._enforcer.get_users_for_role_in_domain(role, domain)
            return await self._enforcer.get_users_for_role(role)
        except Exception as e:
            logger.error(f"RBAC get_users_for_role error: {e}")
            return []

    async def has_role_for_user(
        self,
        user: Union[str, int],
        role: str,
        domain: Optional[str] = None,
    ) -> bool:
        """Check if user has a specific role."""
        if not self._initialized or not self._enforcer:
            return False

        user_str = str(user)

        try:
            if domain:
                roles = await self.get_roles_for_user(user_str, domain)
                return role in roles
            return await self._enforcer.has_role_for_user(user_str, role)
        except Exception as e:
            logger.error(f"RBAC has_role_for_user error: {e}")
            return False

    async def get_permissions_for_user(
        self,
        user: Union[str, int],
        domain: Optional[str] = None,
    ) -> List[List[str]]:
        """Get all permissions for a user (including inherited from roles)."""
        if not self._initialized or not self._enforcer:
            return []

        user_str = str(user)

        try:
            if domain:
                return await self._enforcer.get_implicit_permissions_for_user(
                    user_str, domain
                )
            return await self._enforcer.get_implicit_permissions_for_user(user_str)
        except Exception as e:
            logger.error(f"RBAC get_permissions_for_user error: {e}")
            return []

    async def delete_user(self, user: Union[str, int]) -> bool:
        """Remove all roles and permissions for a user."""
        if not self._initialized or not self._enforcer:
            return False

        user_str = str(user)

        try:
            return await self._enforcer.delete_user(user_str)
        except Exception as e:
            logger.error(f"RBAC delete_user error: {e}")
            return False

    async def delete_role(self, role: str) -> bool:
        """Remove a role and all its assignments."""
        if not self._initialized or not self._enforcer:
            return False

        try:
            return await self._enforcer.delete_role(role)
        except Exception as e:
            logger.error(f"RBAC delete_role error: {e}")
            return False


enforcer = RBACEnforcer()


class RequirePermission:
    """
    FastAPI dependency for permission checking.

    Usage:
        @router.get("/users")
        async def list_users(
            _: None = Depends(RequirePermission("users", "read")),
        ):
            ...

        # Or use the pre-configured instance
        @router.delete("/users/{user_id}")
        async def delete_user(
            user_id: int,
            _: None = Depends(require_permission("users", "delete")),
        ):
            ...
    """

    def __init__(
        self,
        resource: str,
        action: str,
        domain: Optional[str] = None,
    ) -> None:
        self.resource = resource
        self.action = action
        self.domain = domain

    async def __call__(self, request: Request) -> None:
        user_id = getattr(request.state, "user_id", None)

        if user_id is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Authentication required",
            )

        domain = self.domain
        if domain is None:
            domain = getattr(request.state, "domain", None)

        if not await enforcer.enforce(user_id, self.resource, self.action, domain):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Permission denied: {self.action} on {self.resource}",
            )


def require_permission(
    resource: str,
    action: str,
    domain: Optional[str] = None,
) -> RequirePermission:
    """
    Factory function for permission dependency.

    Usage:
        @router.get("/posts")
        async def list_posts(
            _: None = Depends(require_permission("posts", "read")),
        ):
            ...
    """
    return RequirePermission(resource, action, domain)


def require_role(role: str, domain: Optional[str] = None):
    """
    FastAPI dependency for role checking.

    Usage:
        @router.get("/admin/dashboard")
        async def admin_dashboard(
            _: None = Depends(require_role("admin")),
        ):
            ...
    """

    async def check_role(request: Request) -> None:
        user_id = getattr(request.state, "user_id", None)

        if user_id is None:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Authentication required",
            )

        check_domain = domain or getattr(request.state, "domain", None)

        if not await enforcer.has_role_for_user(user_id, role, check_domain):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Role required: {role}",
            )

    return check_role

