from typing import List, Optional

from django.db.models import Q, QuerySet

from staff.lib.models.mptt import get_heirarchy_filter_query

from staff.budget_position.models.budget_position import BudgetPosition, BudgetPositionAssignment
from staff.departments.models import Department, DepartmentRoles, DepartmentStaff, InstanceClass
from staff.departments.models.headcount import HeadcountPosition
from staff.person.models import Staff


class Permissions:
    permission_fullname = 'departments.can_view_headcounts'
    chief_roles = (DepartmentRoles.CHIEF.value, DepartmentRoles.HR_PARTNER.value)

    def __init__(self, observer: Staff) -> None:
        self.observer: Staff = observer
        self._has_special_role_value: Optional[bool] = None

    def has_access_to_person(self, person: Staff) -> bool:
        if self.has_special_role():
            return True

        if not self.observer.user.has_perm(self.permission_fullname):
            return False

        if self.observer.id == person.id:
            return True

        return self.filter_by_observer(Department.objects.filter(id=person.department_id)).exists()

    def has_access_to_credit_management_application(self, application_author_login: str) -> bool:
        if self.has_special_role():
            return True

        if application_author_login is None:
            return False

        if self.observer.login == application_author_login:
            return True

        return False

    def can_create_applications(self, budget_position_ids: List[int]) -> bool:
        if self.has_special_role():
            return True

        codes = set(BudgetPosition.objects.filter(id__in=budget_position_ids).values_list('code', flat=True))
        if len(codes) < len(budget_position_ids):
            return False

        available_codes = set(
            self.available_budget_position_assignments()
            .filter(budget_position__code__in=codes)
            .values_list('budget_position__code', flat=True)
        )
        if available_codes != codes:
            return False
        return available_codes == codes

    def can_cancel_applications(self):
        if self.has_special_role():
            return True

        return self.observer.user.has_perm('headcounts.can_cancel_applications')

    def can_confirm_applications(self):
        if self.has_special_role():
            return True

        return self.observer.user.has_perm('headcounts.can_confirm_applications')

    def _has_can_view_headcounts_perm(self):
        return self.observer.user.has_perm(self.permission_fullname)

    def has_management_options(self) -> bool:
        if self.has_special_role():
            return True

        if self._has_can_view_headcounts_perm() and self._chief_or_hr_partner_qs().exists():
            return True

        return Department.objects.filter(self._by_departments_perm_filter()).exists()

    def has_access_to_department_url(self, department_url: Optional[str]) -> bool:
        if self.has_special_role():
            return True

        return self.filter_by_observer(Department.all_types.filter(url=department_url)).exists()

    def has_special_role(self) -> bool:
        if self._has_special_role_value is None:
            special_roles = (DepartmentRoles.HR_ANALYST.value, DepartmentRoles.GENERAL_DIRECTOR.value)
            has_special_role = (
                DepartmentStaff.objects
                .filter(role_id__in=special_roles, staff=self.observer)
                .exists()
            )

            robot_with_special_perm = (
                self.observer.is_robot and self.observer.user.has_perm('departments.can_export_ceilings')
            )

            self._has_special_role_value = (
                has_special_role or robot_with_special_perm or self.observer.user.is_superuser
            )

        return self._has_special_role_value

    def departments_with_headcount_permission(self) -> QuerySet:
        if not self._has_can_view_headcounts_perm():
            return Department.objects.none()

        instance_classes = {
            InstanceClass.DEPARTMENT.value,
            InstanceClass.VALUESTREAM.value,
            InstanceClass.GEOGRAPHY.value,
        }
        by_roles_with_permissions = (
            self.observer
            .departments_by_permission(self.permission_fullname, instance_classes=instance_classes)
        )
        by_chief_roles = (
            Department.objects
            .filter(departmentstaff__staff=self.observer, departmentstaff__role_id__in=self.chief_roles)
        )
        return by_roles_with_permissions | by_chief_roles

    def filter_by_observer(self, departments_qs: QuerySet) -> QuerySet:
        if self.has_special_role():
            return departments_qs

        return departments_qs.filter(self._by_departments_perm_filter() | self._by_observer_chief_roles_filter())

    # TODO: STAFF-17443: Remove in favour of available_budget_position_assignments
    def available_headcounts(self) -> QuerySet:
        by_department_filter_q = Q(department_id__in=self.filter_by_observer(Department.objects.all()))
        by_value_stream_filter_q = Q(valuestream_id__in=self.filter_by_observer(Department.valuestreams.all()))
        return HeadcountPosition.objects.filter(by_department_filter_q | by_value_stream_filter_q)

    def available_budget_position_assignments(self) -> QuerySet:
        by_department_filter_q = Q(department_id__in=self.filter_by_observer(Department.objects.all()))
        by_value_stream_filter_q = Q(value_stream_id__in=self.filter_by_observer(Department.valuestreams.all()))
        return BudgetPositionAssignment.objects.active().filter(by_department_filter_q | by_value_stream_filter_q)

    def _by_departments_perm_filter(self):
        return self.observer.departments_by_perm_query(
            perm=self.permission_fullname,
            by_children=True,
            instance_classes={InstanceClass.DEPARTMENT.value, InstanceClass.VALUESTREAM.value},
        )

    def _chief_or_hr_partner_qs(self) -> QuerySet:
        return (
            DepartmentStaff.objects
            .filter(role_id__in=self.chief_roles, staff=self.observer)
        )

    def _by_observer_chief_roles_filter(self):
        none_q = Q(id=None)  # Filters out all rows

        if not self._has_can_view_headcounts_perm():
            return none_q

        roles_qs = self._chief_or_hr_partner_qs().prefetch_related('department')

        observer_departments_with_roles = [role.department for role in roles_qs]

        if observer_departments_with_roles:
            return get_heirarchy_filter_query(
                observer_departments_with_roles,
                by_children=True,
                include_self=True,
            )

        return none_q

    def person(self) -> Staff:
        return self.observer
