from collections import defaultdict
from datetime import date, datetime, timedelta
import logging
from typing import List, Type, Iterable
from staff.lib import waffle

from django.conf import settings
from django.db.models import Q, QuerySet

from staff.departments.models import DepartmentRoles, Department, DepartmentStaff
from staff.lib.models.mptt import filter_by_heirarchy
from staff.lib.models.roles_chain import direct_chief_for_department, has_roles_for_department
from staff.person.models import Staff
from staff.users.models import User

from staff.preprofile.action_context import ActionContext
from staff.preprofile.controllers import Controller
from staff.preprofile.controller_behaviours import (
    AbstractBehaviour,
    EmployeeBehaviour,
    ExternalConsultantBehaviour,
    MassHireBehaviour,
    OutstaffBehaviour,
    RobotBehaviour,
    RotationBehaviour,
    YamoneyEmployeeBehavior,
    ZombieBehaviour,
)
from staff.preprofile.models import FORM_TYPE, Preprofile, PreprofileABCServices, PREPROFILE_STATUS, CANDIDATE_TYPE
from staff.preprofile.utils import user_is_adopter, outstaff_or_external_department


logger = logging.getLogger(__name__)


def is_valid_form_type(form_type):
    return form_type in [db_value for db_value, text_value in FORM_TYPE]


class NoRightsRepositoryError(Exception):
    pass


class Repository:
    behaviours = {
        FORM_TYPE.ZOMBIE: ZombieBehaviour,
        FORM_TYPE.ROBOT: RobotBehaviour,
        FORM_TYPE.EMPLOYEE: EmployeeBehaviour,
        FORM_TYPE.MONEY: YamoneyEmployeeBehavior,
        FORM_TYPE.EXTERNAL: ExternalConsultantBehaviour,
        FORM_TYPE.OUTSTAFF: OutstaffBehaviour,
        FORM_TYPE.ROTATION: RotationBehaviour,
        FORM_TYPE.MASS_HIRE: MassHireBehaviour,
    }

    def __init__(self, staff: Staff):
        self._staff = staff

    def _create_behaviour(self, form_type) -> AbstractBehaviour:
        behaviour_class = self.behaviours.get(form_type)

        if not behaviour_class:
            raise NotImplementedError()

        return behaviour_class()

    def _create_behaviour_for_preprofile(self, preprofile):
        return self._create_behaviour(preprofile.form_type)

    def _check_rights_for_preprofile(self, preprofile: Preprofile):
        if self._user_can_view_all_preprofiles() or user_is_adopter(self._staff):
            return

        if preprofile.recruiter == self._staff:
            return

        if preprofile.is_robot_or_zombie:
            return

        roles = [DepartmentRoles.HR_PARTNER.value, DepartmentRoles.CHIEF.value]

        if has_roles_for_department(preprofile.department, self._staff, roles):
            return

        if preprofile.form_type == FORM_TYPE.MONEY and self._user_can_create_yamoney():
            return

        if preprofile.form_type in [FORM_TYPE.OUTSTAFF, FORM_TYPE.EXTERNAL]:
            roles = [DepartmentRoles.CHIEF.value, DepartmentRoles.DEPUTY.value]

            if has_roles_for_department(preprofile.department, self._staff, roles):
                return

        if preprofile.form_type == FORM_TYPE.EMPLOYEE and outstaff_or_external_department(preprofile.department):
            roles = [DepartmentRoles.CHIEF.value, DepartmentRoles.DEPUTY.value]

            if has_roles_for_department(preprofile.department, self._staff, roles):
                return

        if preprofile.form_type == FORM_TYPE.MASS_HIRE and self._user_can_manage_mass_hire():
            return

        if preprofile.status == PREPROFILE_STATUS.APPROVED and self._user_is_staff_robot():
            return

        raise NoRightsRepositoryError()

    def new(self, form_type):
        assert is_valid_form_type(form_type)

        if not self._user_can_create_preprofile(form_type):
            logger.info('User %s is not allowed to create preprofiles of type %s', self._staff.login, form_type)
            raise NoRightsRepositoryError()

        return Controller(self._create_behaviour(form_type), ActionContext(None, self._staff))

    def existing(
        self,
        preprofile_id: int = None,
        behaviour_factory: Type[AbstractBehaviour] = None,
        preprofile: Preprofile = None
    ) -> Controller:
        preprofile = preprofile or Preprofile.objects.get(id=preprofile_id)
        not_found = (
            preprofile.status in (PREPROFILE_STATUS.CLOSED, PREPROFILE_STATUS.CANCELLED) and
            waffle.switch_is_active('rkn_mode') and
            Staff.objects.filter(login=preprofile.login, is_dismissed=True).exists()
        )
        if not_found:
            raise Preprofile.DoesNotExist()
        self._check_rights_for_preprofile(preprofile)
        behaviour_factory = behaviour_factory or self._create_behaviour_for_preprofile
        behaviour = behaviour_factory(preprofile)
        return Controller(behaviour, ActionContext(preprofile, self._staff))

    def preprofile_id_by_femida_offer_id(self, femida_offer_id):
        preprofile = Preprofile.objects.get(femida_offer_id=femida_offer_id)
        return preprofile.id

    def preprofiles_for_masshire_export(self, tags: List[str]) -> QuerySet:
        if not self._user_can_manage_mass_hire():
            raise NoRightsRepositoryError()

        qs = (
            Preprofile.objects
            .filter(form_type=FORM_TYPE.MASS_HIRE)
        )

        if tags:
            qs = qs.filter(masshire_tag__in=tags)
        else:
            qs = qs.filter(id=None)

        return qs

    @classmethod
    def preprofiles_for_helpdesk(cls):
        fields = [f.name for f in Preprofile._meta.fields]

        fields += ['hardware_profile__profile_id']

        qs = (
            Preprofile.objects
            .exclude(cls._query_for_closed_preprofiles_older_than(days=7))
            .exclude(cls._query_for_cancelled_preprofiles_older_than(days=7))
            .order_by('-created_at')
            .values(*fields)
        )

        preprofile_services = (
            PreprofileABCServices.objects
            .filter(preprofile_id__in=[item['id'] for item in qs])
            .select_related('group')
            .values_list('preprofile_id', 'group__service_id')
        )
        preprofile_to_services = defaultdict(list)

        for preprofile_id, service_id in preprofile_services:
            preprofile_to_services[preprofile_id].append(service_id)

        for item in qs:
            item['abc_services'] = preprofile_to_services[item['id']]

        return qs

    def preprofiles_for_idm(self):
        if not self._user_can_view_all_preprofiles():
            raise NoRightsRepositoryError()

        qs = (
            Preprofile.objects
            .filter(candidate_type=CANDIDATE_TYPE.FORMER_EMPLOYEE)
            .filter(status__in=[PREPROFILE_STATUS.READY, PREPROFILE_STATUS.CLOSED])
            .values('login', 'join_at', 'status')
        )

        return qs

    def preprofiles_for_puncher(self):
        if not self._user_can_view_all_preprofiles():
            raise NoRightsRepositoryError()

        plus_three_days = date.today() + timedelta(days=3)

        qs = (
            Preprofile.objects
            .filter(status__in=[PREPROFILE_STATUS.READY, PREPROFILE_STATUS.CLOSED])
            .filter(join_at__gte=date.today(), join_at__lt=plus_three_days)
            .values('login', 'status', 'id', 'department__group__id')
        )

        return qs

    def preprofiles_for_reminder(self, remind_days_left: int, urgent_remind_days_left: int) -> QuerySet:
        today = date.today()
        simple_remind_from = today + timedelta(days=remind_days_left)
        urgent_remind_from = today + timedelta(days=urgent_remind_days_left)

        dates_q = Q(join_at=simple_remind_from) | Q(join_at=urgent_remind_from) | Q(join_at__lte=today)

        result = (
            Preprofile.objects
            .filter(
                dates_q,
                form_type=FORM_TYPE.EMPLOYEE,
                status=PREPROFILE_STATUS.NEW,
            )
            .select_related('recruiter', 'department')
        )

        return result

    def existing_approved(self) -> Iterable[Controller]:
        for p in Preprofile.objects.filter(status=PREPROFILE_STATUS.APPROVED):
            yield self.existing(preprofile=p)

    @staticmethod
    def _query_for_closed_preprofiles_older_than(days: int) -> Q:
        return Q(status=PREPROFILE_STATUS.CLOSED) & Q(modified_at__lt=datetime.now() - timedelta(days=days))

    @staticmethod
    def _query_for_cancelled_preprofiles_older_than(days: int) -> Q:
        return Q(status=PREPROFILE_STATUS.CANCELLED) & Q(modified_at__lt=datetime.now() - timedelta(days=days))

    def preprofiles_for_certificator(self):
        if not self._user_can_view_all_preprofiles():
            raise NoRightsRepositoryError()

        qs = (
            Preprofile.objects
            .filter(status__in=[PREPROFILE_STATUS.READY, PREPROFILE_STATUS.APPROVED, PREPROFILE_STATUS.CLOSED])
            .exclude(self._query_for_closed_preprofiles_older_than(days=7))
            .values(
                'login',
                'first_name',
                'first_name_en',
                'last_name',
                'last_name_en',
                'join_at',
                'office_id',
                'department__url',
            )
        )

        return qs

    def preprofile_for_femida(self, preprofile_id):
        if not self._user_can_view_all_preprofiles():
            raise NoRightsRepositoryError()
        preprofile = Preprofile.objects.select_related('department', 'recruiter').get(id=preprofile_id)

        fields = [
            'candidate_type',
            'department_id',
            'femida_offer_id',
            'join_at',
            'hdrfs_ticket',
            'login',
            'modified_at',
            'organization_id',
            'status',
            'supply_ticket',
            'date_completion_internship',
        ]

        preprofile_dict = {
            field: getattr(preprofile, field)
            for field in fields
        }

        preprofile_dict.update({
            'position': preprofile.position_staff_text,
            'recruiter_login': preprofile.recruiter.login,
        })

        direct_chief = direct_chief_for_department(preprofile.department, fields=['login'])
        if direct_chief:
            preprofile_dict['chief_login'] = direct_chief.get('login')

        return preprofile_dict

    def preprofiles_qs(self) -> QuerySet:
        if self._user_can_view_all_preprofiles():
            return Preprofile.objects.all()

        if user_is_adopter(self._staff):
            return Preprofile.objects.exclude(form_type=FORM_TYPE.ROTATION)

        filter_q = self._where_user_is_recruiter()

        return (
            self._where_user_is_chief()
            | self._where_user_is_chief_or_deputy_for_outstaff_and_external()
            | self._yamoney_if_has_permission()
            | Preprofile.objects.filter(filter_q)
        ).exclude(form_type=FORM_TYPE.MASS_HIRE).order_by('id')

    def preprofiles_waiting_chief_to_confirm(self):
        return self._where_user_is_chief().filter(
            status=PREPROFILE_STATUS.NEW,
            form_type=FORM_TYPE.EMPLOYEE,
        )

    def preprofiles_waiting_direct_chief_to_confirm(self) -> QuerySet:
        return Preprofile.objects.filter(
            department__in=self._departments_where_user_is_direct_chief(),
            status__in=(PREPROFILE_STATUS.NEW, PREPROFILE_STATUS.PREPARED),
        )

    def _where_user_is_chief(self):
        departments = self._departments_where_user_is_chief_or_hrbp()

        if not departments:
            return Preprofile.objects.none()

        return filter_by_heirarchy(
            query_set=Preprofile.objects.all(),
            filter_prefix='department__',
            mptt_objects=departments,
            by_children=True,
            include_self=True,
        )

    def _yamoney_if_has_permission(self):
        if not self._user_can_create_yamoney():
            return Preprofile.objects.none()

        return Preprofile.objects.filter(form_type=FORM_TYPE.MONEY)

    def _departments_where_user_is_direct_chief(self) -> List[Department]:
        qs = (
            DepartmentStaff.objects
            .filter(staff=self._staff, role_id=DepartmentRoles.CHIEF.value)
        )

        return [role.department for role in qs]

    def _departments_where_user_is_chief_or_hrbp(self):
        qs = (
            self._staff
            .departmentstaff_set
            .filter(role_id__in=[DepartmentRoles.CHIEF.value, DepartmentRoles.HR_PARTNER.value])
        )

        return [role.department for role in qs]

    def _where_user_is_chief_or_deputy_for_outstaff_and_external(self):
        departments = self._departments_where_user_is_chief_or_deputy()

        if not departments:
            return Preprofile.objects.none()

        queryset = filter_by_heirarchy(
            query_set=Preprofile.objects.filter(form_type__in=[FORM_TYPE.EXTERNAL, FORM_TYPE.OUTSTAFF]),
            mptt_objects=departments,
            filter_prefix='department__',
            by_children=True,
            include_self=True,
        )
        outstaff_and_external_departments = [
            department
            for department in departments
            if department.id in self.outstaff_and_external_department_ids
        ]
        if outstaff_and_external_departments:
            queryset |= filter_by_heirarchy(
                query_set=Preprofile.objects.filter(form_type__in=[FORM_TYPE.EMPLOYEE]),
                mptt_objects=outstaff_and_external_departments,
                filter_prefix='department__',
                by_children=True,
                include_self=True,
            )
        return queryset

    def _departments_where_user_is_chief_or_deputy(self):
        qs = (
            self._staff
            .departmentstaff_set
            .filter(role_id__in=[DepartmentRoles.CHIEF.value, DepartmentRoles.DEPUTY.value])
        )

        return [role.department for role in qs]

    def _user_can_view_all_preprofiles(self):
        return self.user_can_view_all_preprofiles(self._staff.user)

    @staticmethod
    def user_can_view_all_preprofiles(user: User):
        return user.has_perm('preprofile.can_view_all_preprofiles')

    def _user_can_manage_mass_hire(self):
        return self._staff.user.has_perm('preprofile.can_outstaff')

    def _user_can_create_preprofile(self, form_type) -> bool:
        if form_type in [FORM_TYPE.ROBOT, FORM_TYPE.ZOMBIE, FORM_TYPE.EXTERNAL]:
            return True

        if form_type == FORM_TYPE.MONEY:
            return self._user_can_create_yamoney()

        if form_type == FORM_TYPE.OUTSTAFF:
            return self._user_can_create_outstaff()

        if form_type == FORM_TYPE.MASS_HIRE:
            return self._user_can_manage_mass_hire()

        return self._staff.user.has_perm('preprofile.add_preprofile')

    def _user_can_create_yamoney(self):
        return self._staff.user.has_perm('preprofile.can_create_yamoney')

    def _user_can_create_outstaff(self):
        return (
            self._staff.user.has_perm('preprofile.can_outstaff')
            or self._staff.user.has_perm('preprofile.can_create_outstaff_by_api')
        )

    def _where_user_is_recruiter(self):
        return Q(recruiter=self._staff)

    def _user_is_staff_robot(self):
        return self._staff.login == settings.ROBOT_STAFF_LOGIN

    @property
    def outstaff_and_external_department_ids(self):
        ext_ids = (settings.OUTSTAFF_DEPARTMENT_ID, settings.EXT_DEPARTMENT_ID)
        outstaff_or_external_root_departments = Department.objects.filter(id__in=ext_ids)
        return set(
            Department.objects.get_queryset_descendants(
                queryset=outstaff_or_external_root_departments,
                include_self=True,
            )
            .values_list('id', flat=True)
        )
