from typing import Any

from gen_epix.commondb.app_impl_details import AppImplDetails
from gen_epix.commondb.domain import command, enum, model
from gen_epix.commondb.domain.policy import BaseUpdateUserPolicy
from gen_epix.commondb.domain.service import BaseAbacService
from gen_epix.fastapp import Command


class UpdateUserPolicy(BaseUpdateUserPolicy):
    def __init__(self, abac_service: BaseAbacService, **kwargs: Any):
        super().__init__(abac_service, **kwargs)

        app_impl: AppImplDetails = abac_service.app.impl
        self.user_class: type[model.User] = app_impl.get_mapped_class(model.User)
        self.role_map = app_impl.role_map
        self.role_set_map = app_impl.role_set_map

    def is_allowed(self, cmd: Command) -> bool:
        user: model.User | None = cmd.user  # type: ignore[assignment]
        if user is None or user.id is None:
            return False
        roles = user.roles
        is_root = len(roles.intersection(self.role_set_map[enum.RoleSet.ROOT])) > 0

        tgt_user: model.User
        if isinstance(cmd, command.InviteUserCommand):
            if user.key == cmd.key:
                # User cannot invite themselves
                return False
            tgt_user = self.user_class(**cmd.model_dump())
            tgt_roles_union = set(tgt_user.roles)
            is_organization_update = False
        elif isinstance(cmd, command.UpdateUserCommand):
            tgt_user = self.abac_service.app.user_manager.retrieve_user_by_id(
                cmd.tgt_user_id
            )  # type: ignore[assignment]
            tgt_roles_union = set(tgt_user.roles)
            if cmd.roles is not None:
                tgt_roles_union.update(cmd.roles)
            is_organization_update = (
                cmd.organization_id is not None
                and cmd.organization_id != tgt_user.organization_id
            )
        else:
            raise NotImplementedError

        # Handle ROOT, >=APP_ADMIN, <ORG_ADMIN
        if is_root:
            # Root user can invite/update anyone with any permissions
            return True
        tgt_user.roles = tgt_roles_union
        if len(roles.intersection(self.role_set_map[enum.RoleSet.GE_APP_ADMIN])) > 0:
            # APP_ADMIN users and above can invite/update anyone with less permissions
            # (so not another APP_ADMIN), and only if the new set of permissions is also
            # less than their own permissions
            return self._has_more_permissions(user, tgt_user)
        if not roles.intersection(self.role_set_map[enum.RoleSet.GE_ORG_ADMIN]):
            # Only ORG_ADMIN users and above can invite/update users
            return False

        # ORG_ADMIN users can invite/update users of the organization(s) they are admin
        # of, but only if they have less permissions (so not another ORG_ADMIN), and
        # only if the new set of permissions is also less than their own permissions.
        # They cannot update the organization.
        if not tgt_roles_union.issubset(self.role_set_map[enum.RoleSet.LT_ORG_ADMIN]):
            # Target user has at least one role that is not under ORG_ADMIN
            return False
        if is_organization_update:
            # User cannot update the organization
            return False
        user_admin_organization_ids = (
            self.abac_service.retrieve_organizations_under_admin(
                command.RetrieveOrganizationsUnderAdminCommand(user=user)
            )
        )
        if tgt_user.organization_id not in user_admin_organization_ids:
            # User is not admin of the organization(s) the target user is part of
            return False
        return self._has_more_permissions(user, tgt_user)

    def _has_more_permissions(self, user: model.User, tgt_user: model.User) -> bool:
        permissions = self.abac_service.app.user_manager.retrieve_user_permissions(user)
        tgt_permissions = self.abac_service.app.user_manager.retrieve_user_permissions(
            tgt_user
        )
        return tgt_permissions.issubset(permissions) and len(permissions) > len(
            tgt_permissions
        )
