import logging
from collections.abc import Iterable
from typing import Any, Callable, Optional, Protocol
import rules
from django.contrib.auth.models import User, Permission
from django.db.models import QuerySet

from .conf import (
    PERMISSION_TYPES_LABELS,
    Action,
    PermissionType,
)
from .defaults import PermissionStrategy
from .utils import actions_to_list, permissions_divider
from .models import UserGroup, OrganizationalUnit
from django.contrib.contenttypes.models import ContentType

logger = logging.getLogger(__name__)


class PermissionRepository(Protocol):
    def get_user_groups(self, user: User) -> Iterable[UserGroup]: ...
    def get_all_permissions_for_model(
        self, model, fields_included, action
    ) -> Iterable[str]: ...
    def get_permissions_from_user_groups(
        self,
        user_groups: Iterable[UserGroup],
    ) -> set[str]: ...


class PermissionService:
    """Class responsible for checking permissions."""

    # TODO Przeanalizować pod kątem optymalizacji, rozważyć cache/inny czas życia
    def __init__(self, user: User, repository: PermissionRepository):
        self.user = user
        self._repository = repository
        self._user_groups = None

        self.permissions_checker_functions: dict[
            PermissionStrategy, PermissionCheckerStrategy
        ] = {
            PermissionStrategy.MODEL: RegularPermissionCheckerStrategy,
            PermissionStrategy.OBJECT: ObjectPermissionCheckerStrategy,
            PermissionStrategy.HARDCODED: HardcodedPermissionCheckerStrategy,
        }

    @property
    def user_groups(self):
        if self._user_groups is None:
            self._user_groups = self._repository.get_user_groups(self.user)
        return self._user_groups

    def _is_permission_in_user_groups(
        self, permission: str, user_groups: QuerySet[UserGroup]
    ) -> bool:
        """
        Check if permission is in any of user groups.

        Args:
            permission (str): Permission codename in format 'app_label.codename'.
            user_groups (QuerySet): UserGroups queryset.

        Returns:
            bool: True if user has permission, False otherwise.
        """
        permissions_in_user_groups = self._repository.get_permissions_from_user_groups(
            user_groups
        )
        return permission in permissions_in_user_groups

    @staticmethod
    def get_hierarchy_of_organizational_units(
        obj: Any,
    ) -> Iterable[OrganizationalUnit]:
        parent_organizational_unit = obj.parent
        list_of_organizational_units = parent_organizational_unit.get_ancestors(
            ascending=True
        )
        # In test method get_ancestors() with include_self=True doesn't work
        list_of_organizational_units = [parent_organizational_unit] + list(
            list_of_organizational_units
        )
        return list_of_organizational_units

    def _model_level_has_permission(self, permission):
        """Check if user has permission in any of his user groups."""
        return self._is_permission_in_user_groups(permission, self.user_groups)

    def _object_level_has_permission(self, permission, obj) -> bool:
        """Check if user has permission in any of his user groups in scope of organizational units"""
        hierarchy_of_organizational_units = (
            PermissionService.get_hierarchy_of_organizational_units(obj)
        )
        for organizational_unit in hierarchy_of_organizational_units:
            user_groups = UserGroup.objects.filter(
                users=self.user,
                organizational_units=organizational_unit,
            )
            if self._is_permission_in_user_groups(permission, user_groups):
                return True
        return False

    def has_permission(self, permission, obj=None):
        if obj is not None and (not hasattr(obj, "parent") or not obj.parent):
            raise AttributeError(
                f"{obj.__class__.__module__}.{obj.__class__.__name__} doesn't have parent attribute or parent is null."
            )
        return (
            self._object_level_has_permission(permission, obj)
            if obj is not None
            else self._model_level_has_permission(permission)
        )

    def has_perm_to_action(self, model, action: Action, obj=None):
        # print("------------------------------------\n")
        # print("Model", model)
        # print("Obj", obj)
        # print("Action", action)
        # print("------------------------------------\n")
        all_permissions_for_model = self._repository.get_all_permissions_for_model(
            model, False, action
        )
        return self.has_perm_checker(obj, *all_permissions_for_model)

    def has_perm_checker(self, obj, *permissions):
        if self.user.is_superuser:
            return True
        permissions_divider_by_strategy = permissions_divider(*permissions)

        for (
            permission_strategy,
            permission_checker_function,
        ) in self.permissions_checker_functions.items():
            if (
                permissions := permissions_divider_by_strategy.get(
                    permission_strategy.value
                )
            ) and permission_checker_function.check(permissions, obj, self):
                logger.debug("PermissionStrategy: %s", permission_strategy.value)
                return True
        return False

    def has_field_permission_checker(self, model, field_name, obj=None):
        # TODO Walidacja field_name do napisania
        content_type = ContentType.objects.get_for_model(model)
        view_permission, change_permission = (
            self.has_perm_checker(
                obj,
                f"{content_type.app_label}.{PermissionType.FIELD.value}_{field_name}_{action.value}_{model.__name__.lower()}",
            )
            for action in (Action.VIEW, Action.CHANGE)
        )
        return view_permission, change_permission


class PermissionCreationService:
    # TODO Poprawić opisy klas/funkcji!
    """Permission Creation Service is used to create codenames and assign rules to permission"""

    @staticmethod
    def create_crud_permissions_by_type(
        model_name: str, permission_type: PermissionType, description: str = None
    ) -> list:
        if (
            permission_type not in PERMISSION_TYPES_LABELS.keys()
            and description is None
        ):
            raise KeyError(
                f"Key {permission_type} doesn't exist in PERMISSION_TYPES_LABELS and description is None."
            )
        if permission_type == PermissionType.FIELD:
            raise TypeError(
                "Argument 'permission_type' cannot be 'PermissionType.FIELD'. Use 'create_fields_permissions' method."
            )
        permissions_list = []
        action_values = actions_to_list(
            Action.ADD,
            Action.VIEW,
            Action.CHANGE,
            Action.DELETE,
        )
        for action_value in action_values:
            permissions_list.append(
                tuple(
                    (
                        f"{permission_type.value}_{action_value}_{model_name}",
                        (
                            PERMISSION_TYPES_LABELS[permission_type](
                                action_value, model_name
                            )
                            if description is None
                            else f"Can {action_value} {model_name} {description}"
                        ),
                    )
                )
            )
        return permissions_list

    @staticmethod
    def create_fields_permissions(model) -> list:
        if PermissionType.FIELD not in PERMISSION_TYPES_LABELS.keys():
            raise KeyError(
                "Key 'PermissionType.FIELD' doesn't exist in PERMISSION_TYPES_LABELS"
            )
        model_name = model.__name__.lower()
        fields = [field.name for field in model._meta.get_fields()]
        permissions_list = []
        action_values = actions_to_list(Action.VIEW, Action.CHANGE)
        for action_value in action_values:
            for field in fields:
                permissions_list.append(
                    tuple(
                        (
                            f"{PermissionType.FIELD.value}_{field}_{action_value}_{model_name}",
                            PERMISSION_TYPES_LABELS[PermissionType.FIELD](
                                action_value, model_name, field
                            ),
                        )
                    )
                )
        return permissions_list

    @staticmethod
    def add_rule_to_permission(
        app_name: str, codename: str, description: str, rule: Callable
    ):
        rules.add_rule(
            f"{app_name}.{codename}",
            rule,
        )
        return codename, description

    @staticmethod
    def add_rules_to_permissions(
        app_name: str,
        codenames_with_descriptions: list[tuple[str, str]],
        rules_to_assign: list[Callable],
    ) -> list[tuple[str, str]]:

        if (
            len(rules_to_assign) != len(codenames_with_descriptions)
            and len(rules_to_assign) != 1
        ):
            raise ValueError(
                "Count of rules and permissions must be the same or rules must be 1"
            )
        permissions_list = []
        for i, (codename, description) in enumerate(codenames_with_descriptions):
            if len(rules_to_assign) == 1:
                rule = rules_to_assign[0]
            else:
                rule = rules_to_assign[i]
            permissions_list.append(
                PermissionCreationService.add_rule_to_permission(
                    app_name, codename, description, rule
                )
            )
        return permissions_list

    @staticmethod
    def add_permissions_to_permissions_groups(
        group_permissions: dict[str, list[dict[str, Any]]],
    ):
        """
        Method to add permissions to groups.
        This method should be called after creating permissions.

        Args:
        group_permissions (dict): A dictionary where keys are role names (str) and values are lists of dictionaries.
                      Each dictionary must contain:
                      - "model": A model class (e.g., Product).
                      - "codenames": A list of permission codenames (list[str]).
        """
        from django.contrib.auth.models import Group

        # Validation of group_permissions structure should be done.
        for group_name, permissions_list in group_permissions.items():
            group, _ = Group.objects.get_or_create(name=group_name)

            for perm_info in permissions_list:
                content_type = ContentType.objects.get_for_model(perm_info["model"])
                for codename in perm_info["codenames"]:
                    permission = Permission.objects.get(
                        codename=codename, content_type=content_type
                    )
                    group.permissions.add(permission)


class DjangoPermissionRepository:
    @staticmethod
    def get_all_permissions_for_model(model, fields_included=False, action=None):
        """Get all permissions for model. Use ``action`` argument to filter all permissions"""
        content_type = ContentType.objects.get_for_model(model)
        permissions = Permission.objects.filter(content_type=content_type)
        if action:
            permissions = permissions.filter(codename__contains=action.value)
        if not fields_included:
            permissions = permissions.exclude(
                codename__startswith=PermissionType.FIELD.value
            )
        return [
            f"{content_type.app_label}.{permission.codename}"
            for permission in permissions
        ]

    @staticmethod
    def get_user_groups(user: User) -> Iterable[UserGroup]:
        return UserGroup.objects.filter(users=user).prefetch_related(
            "permission_groups", "organizational_units"
        )

    @staticmethod
    def get_permissions_from_user_groups(
        user_groups: Iterable[UserGroup],
    ) -> set[str]:
        permission_groups_set = set()
        for user_group in user_groups:
            permission_groups_set.update(user_group.permission_groups.all())
        permissions_set = set()
        for group in permission_groups_set:
            perms = group.permissions.all()
            formatted_perms = {
                f"{perm.content_type.app_label}.{perm.codename}" for perm in perms
            }
            permissions_set.update(formatted_perms)
        return permissions_set

class PermissionCheckerStrategy(Protocol):
    @staticmethod
    def check(
        permissions: Iterable[str], obj: Optional[Any], service: PermissionService
    ) -> bool: ...


class RegularPermissionCheckerStrategy:
    @staticmethod
    def check(
        permissions: Iterable[str], obj: Optional[Any], service: PermissionService
    ) -> bool:
        """Check regular permissions"""
        return any(
            service.has_permission(permission, obj) for permission in permissions
        )


class ObjectPermissionCheckerStrategy:
    @staticmethod
    def check(
        permissions: Iterable[str], obj: Optional[Any], service: PermissionService
    ) -> bool:
        """Check OLP (Object Level Permission)"""
        # print(f"Sprawdzane olp uprawnień: {permissions}")
        if obj is None:
            return False  # could be unnecessary
        for permission in permissions:
            has_perm = service.has_permission(permission, obj)
            logger.debug("permission: %s has_perm: %s", permission, has_perm)
            if has_perm:
                if test_rule := rules.test_rule(permission, service.user, obj):
                    logger.debug("permission: %s test_rule: %s", permission, test_rule)
                    return True
        return False


class HardcodedPermissionCheckerStrategy:
    @staticmethod
    def check(
        permissions: Iterable[str], obj: Optional[Any], service: PermissionService
    ) -> bool:
        # TODO Koncept i zasada działania do ponownego przemyślenia
        logger.warning("Checking of hardcoded permissions are not implemented yet.")
        return False

        # return any(self.user.has_perm(permission, obj) for permission in permissions)
