from __future__ import annotations

from collections.abc import Sequence

from sqlalchemy.orm import Session

from app.core.permissions import get_enforcer, invalidate_user_permissions, load_policy
from app.db.session import SessionLocal
from app.models.role import Role
from app.models.tenant import Tenant
from app.models.user import User
from app.models.user_role import UserRole
from common_logging import get_logger

logger = get_logger(__name__)
SYSTEM_ROLE_DEFINITIONS: dict[str, dict[str, str]] = {
    "platform_admin": {"name": "平台管理员", "description": "平台级系统管理员"},
    "platform_user": {"name": "平台用户", "description": "平台级普通用户"},
    "customer_admin": {"name": "企业管理员", "description": "租户级管理员"},
    "customer_user": {"name": "企业用户", "description": "租户级普通用户"},
}
SYSTEM_ROLE_CODES = tuple(SYSTEM_ROLE_DEFINITIONS.keys())
PLATFORM_ROLE_CODES = {"platform_admin", "platform_user"}
TENANT_ROLE_CODES = {"customer_admin", "customer_user"}
PLATFORM_RESOURCES = (
    "users",
    "roles",
    "knowledge_bases",
    "knowledge_categories",
    "knowledge_tags",
    "agents",
    "workflows",
    "documents",
    "audit_logs",
    "menus",
    "providers",
    "models",
)
CUSTOMER_ADMIN_POLICIES = (
    ("users", "read"),
    ("users", "create"),
    ("users", "update"),
    ("users", "delete"),
    ("roles", "read"),
    ("knowledge_bases", "read"),
    ("knowledge_bases", "create"),
    ("knowledge_bases", "update"),
    ("knowledge_bases", "delete"),
    ("knowledge_categories", "read"),
    ("knowledge_categories", "create"),
    ("knowledge_categories", "update"),
    ("knowledge_categories", "delete"),
    ("knowledge_tags", "read"),
    ("knowledge_tags", "create"),
    ("knowledge_tags", "update"),
    ("knowledge_tags", "delete"),
    ("agents", "read"),
    ("agents", "create"),
    ("agents", "update"),
    ("agents", "delete"),
    ("documents", "read"),
    ("documents", "create"),
    ("documents", "update"),
    ("documents", "delete"),
    ("audit_logs", "read"),
    ("providers", "read"),
    ("models", "read"),
    ("menus", "read"),
)
CUSTOMER_USER_POLICIES = (
    ("knowledge_bases", "read"),
    ("knowledge_categories", "read"),
    ("knowledge_tags", "read"),
    ("documents", "read"),
    ("agents", "read"),
    ("providers", "read"),
    ("models", "read"),
    ("menus", "read"),
)
PLATFORM_USER_POLICIES = (
    ("knowledge_bases", "read"),
    ("knowledge_categories", "read"),
    ("knowledge_tags", "read"),
    ("documents", "read"),
    ("agents", "read"),
    ("providers", "read"),
    ("models", "read"),
    ("menus", "read"),
)


class RBACBootstrapService:

    @staticmethod
    def ensure_system_roles(db: Session) -> dict[str, Role]:
        roles_by_code: dict[str, Role] = {}
        for code, definition in SYSTEM_ROLE_DEFINITIONS.items():
            role = db.query(Role).filter(Role.code == code).first()
            if role is None:
                role = Role(
                    code=code,
                    name=definition["name"],
                    description=definition["description"],
                    is_system=True,
                    tenant_id=None,
                )
                db.add(role)
                logger.info(f"Created system role: {code}")
            else:
                role.name = definition["name"]
                role.description = definition["description"]
                role.is_system = True
                role.tenant_id = None
                role.is_deleted = False
            roles_by_code[code] = role
        db.flush()
        return roles_by_code

    @staticmethod
    def ensure_default_role_policies(db: Session) -> None:
        enforcer = get_enforcer()
        policy_count = 0
        for resource in PLATFORM_RESOURCES:
            for action in ("read", "create", "update", "delete", "execute"):
                enforcer.add_policy("role:platform_admin", "0", resource, action)
                policy_count += 1
        for resource, action in PLATFORM_USER_POLICIES:
            enforcer.add_policy("role:platform_user", "0", resource, action)
            policy_count += 1
        tenant_ids = RBACBootstrapService._get_active_tenant_ids(db)
        for tenant_id in tenant_ids:
            domain = str(tenant_id)
            for resource, action in CUSTOMER_ADMIN_POLICIES:
                enforcer.add_policy("role:customer_admin", domain, resource, action)
                policy_count += 1
            for resource, action in CUSTOMER_USER_POLICIES:
                enforcer.add_policy("role:customer_user", domain, resource, action)
                policy_count += 1
        logger.info(f"Ensured {policy_count} default role policies for {len(tenant_ids)} tenants")

    @staticmethod
    def sync_user_role_binding(
        db: Session,
        user: User,
        previous_role: str | None = None,
        previous_tenant_id: int | None = None,
    ) -> bool:
        role_code = user.role
        if role_code not in SYSTEM_ROLE_CODES:
            logger.debug(
                "Skipping RBAC bootstrap for non-system role %s on user %s", role_code, user.id
            )
            return False
        if role_code in TENANT_ROLE_CODES and (not user.tenant_id):
            logger.warning(
                "Cannot bind tenant-scoped system role %s for user %s without tenant_id",
                role_code,
                user.id,
            )
            return False
        roles_by_code = RBACBootstrapService.ensure_system_roles(db)
        RBACBootstrapService.ensure_default_role_policies(db)
        desired_role = roles_by_code[role_code]
        desired_domain = RBACBootstrapService._get_role_domain(role_code, user.tenant_id)
        RBACBootstrapService._sync_user_role_records(db, user.id, desired_role.id)
        RBACBootstrapService._sync_grouping_policies(
            user_id=user.id,
            desired_role_code=role_code,
            desired_domain=desired_domain,
            previous_role=previous_role,
            previous_tenant_id=previous_tenant_id,
        )
        db.commit()
        load_policy()
        invalidate_user_permissions(user.id, desired_domain)
        if previous_tenant_id is not None and previous_tenant_id != desired_domain:
            invalidate_user_permissions(user.id, previous_tenant_id)
        return True

    @staticmethod
    def bootstrap_legacy_rbac_state() -> None:
        db = SessionLocal()
        try:
            RBACBootstrapService.ensure_system_roles(db)
            RBACBootstrapService.ensure_default_role_policies(db)
            users = db.query(User).filter(not User.is_deleted).all()
            for user in users:
                if user.role not in SYSTEM_ROLE_CODES:
                    continue
                if user.role in TENANT_ROLE_CODES and (not user.tenant_id):
                    continue
                desired_role_id = db.query(Role.id).filter(Role.code == user.role).scalar()
                if desired_role_id is None:
                    continue
                RBACBootstrapService._sync_user_role_records(db, user.id, desired_role_id)
                desired_domain = RBACBootstrapService._get_role_domain(user.role, user.tenant_id)
                RBACBootstrapService._sync_grouping_policies(
                    user_id=user.id, desired_role_code=user.role, desired_domain=desired_domain
                )
                invalidate_user_permissions(user.id, desired_domain)
            db.commit()
            load_policy()
            logger.info("RBAC bootstrap completed successfully")
        except Exception:
            db.rollback()
            logger.exception("Failed to bootstrap RBAC state")
            raise
        finally:
            db.close()

    @staticmethod
    def _get_active_tenant_ids(db: Session) -> Sequence[int]:
        return [
            tenant_id
            for tenant_id, in db.query(Tenant.id)
            .filter(not Tenant.is_deleted, Tenant.id > 0)
            .all()
        ]

    @staticmethod
    def _get_role_domain(role_code: str, tenant_id: int | None) -> int:
        if role_code in PLATFORM_ROLE_CODES:
            return 0
        return int(tenant_id or 0)

    @staticmethod
    def _sync_user_role_records(db: Session, user_id: int, desired_role_id: int) -> None:
        role_links = db.query(UserRole).filter(UserRole.user_id == user_id).all()
        desired_found = False
        for link in role_links:
            if link.role_id == desired_role_id:
                link.is_deleted = False
                desired_found = True
        if not desired_found:
            db.add(UserRole(user_id=user_id, role_id=desired_role_id))
        system_links = (
            db.query(UserRole)
            .join(Role, Role.id == UserRole.role_id)
            .filter(
                UserRole.user_id == user_id,
                Role.code.in_(SYSTEM_ROLE_CODES),
                Role.id != desired_role_id,
                not UserRole.is_deleted,
            )
            .all()
        )
        for link in system_links:
            link.is_deleted = True
        db.flush()

    @staticmethod
    def _sync_grouping_policies(
        user_id: int,
        desired_role_code: str,
        desired_domain: int,
        previous_role: str | None = None,
        previous_tenant_id: int | None = None,
    ) -> None:
        enforcer = get_enforcer()
        subject = f"user:{user_id}"
        desired_role = f"role:{desired_role_code}"
        cleanup_domains = {0, desired_domain}
        if previous_tenant_id is not None:
            cleanup_domains.add(previous_tenant_id)
        for role_code in SYSTEM_ROLE_CODES:
            for domain in cleanup_domains:
                if role_code == desired_role_code and domain == desired_domain:
                    continue
                enforcer.remove_grouping_policy(subject, f"role:{role_code}", str(domain))
        if previous_role and previous_role != desired_role_code:
            old_domain = RBACBootstrapService._get_role_domain(previous_role, previous_tenant_id)
            enforcer.remove_grouping_policy(subject, f"role:{previous_role}", str(old_domain))
        enforcer.add_grouping_policy(subject, desired_role, str(desired_domain))
