from datetime import datetime, date, timedelta
from itertools import chain

from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings

from staff.departments.models import DepartmentRoles
from staff.person.models import Staff, AFFILIATION

from staff.lib.db import atomic
from staff.lib.models.departments_chain import DepartmentsChain
from staff.lib.utils.ordered_choices import OrderedChoices
from staff.lib.utils import diff_dict
from staff.dismissal.models import Dismissal, DISMISSAL_STATUS
from staff.dismissal.permissions import DismissalPerms
from staff.dismissal.services import DismissalService
from staff.dismissal.signals import (
    dismissal_created,
    dismissal_updated,
    dismissal_cancelled,
    dismissal_completed,
    dismissal_not_completed,
    dismissal_date_passed,
)

from staff.person.controllers import PersonCtl

import logging
logger = logging.getLogger('dismissal')

DISMISSAL_TYPE = OrderedChoices(
    ('FULL', 'full'),
    ('SHORT', 'short'),
)


class DismissalCtl(object):
    MAX_MOVE_TO_EXT_DAYS = 15

    class PersonNotMoveToExt(Exception):
        pass

    def __init__(self, dismissal=None, author=None, person=None):
        super(DismissalCtl, self).__init__()
        self.author = author
        self.dismissal = dismissal
        self.person = dismissal.staff if dismissal else person
        self.chiefed_departments = []
        self.deputed_departments = []

    def get_or_create(self, staff):

        active_dismissal = self.get(staff)
        if not active_dismissal:
            return self.create(staff)
        else:
            return active_dismissal

    @staticmethod
    def get(staff):
        """
        Находим последнюю актуальную заявку на увольнение. Таких может быть не
        более одной.
        """
        try:
            return (Dismissal.objects.get(
                intranet_status=1,
                staff=staff,
                status__in=(
                    DISMISSAL_STATUS.NEW,
                    DISMISSAL_STATUS.IN_PROGRESS,
                    DISMISSAL_STATUS.DATE_PASSED,
                ),
            ))
        except ObjectDoesNotExist:
            return None

    def create(self, staff, enable=False):
        self.dismissal = Dismissal.objects.create(
            staff=staff,
            department=staff.department,
            office=staff.office,
            created_by=self.author,
        )

        self.enable() if enable else self.disable()

        self._create_clearance_chit()

        return self.dismissal

    def update(self, data, enable=False):
        d = self.dismissal = Dismissal.objects.get(pk=self.dismissal.pk)
        changes = self._get_changes(new_data=data)

        if enable:
            self.enable()

        fields = (
            'initiator',
            'reason',
            'impression',
            'new_employer',

            'delete_email_address',
            'delete_correspondence',
            'forward_correspondence_to',
            'give_correspondence_to',
            'delete_files',
            'keep_files',
            'give_files_to',
            'rehire_recommendation',

            'deadline',
            'move_to_ext',
            'move_to_ext_issue',
        )

        for field in chain(d.SEARCH_FIELDS, d.HR_FIELDS, fields):
            if field in data:
                setattr(d, field, data[field])

        if not self._is_completed():
            d.quit_date = data['quit_date']
            d.note = data.get('note', '')

            if d.quit_date > date.today() and d.status == DISMISSAL_STATUS.DATE_PASSED:
                d.status = DISMISSAL_STATUS.NEW

            if d.deadline:
                d.status = DISMISSAL_STATUS.IN_PROGRESS

        d.save(force_update=True)

        if enable:
            dismissal_created.send(
                sender=self,
                dismissal=self.wrapper
            )
        elif changes:
            dismissal_updated.send(
                sender=self,
                dismissal=self.wrapper,
                updated_by=self.author,
                changes=changes
            )

    def _get_changes(self, new_data):

        old_data = {
            'quit_date': self.dismissal.quit_date,
            'note': self.dismissal.note,
            'delete_email_address': self.dismissal.delete_email_address,
            'delete_correspondence': self.dismissal.delete_correspondence,
            'forward_correspondence_to': self.dismissal.forward_correspondence_to,
            'give_correspondence_to': self.dismissal.give_correspondence_to,
            'delete_files': self.dismissal.delete_files,
            'keep_files': self.dismissal.keep_files,
            'give_files_to': self.dismissal.give_files_to,
            'rehire_recommendation': self.dismissal.rehire_recommendation,

            'deadline': self.dismissal.deadline,
            'move_to_ext': self.dismissal.move_to_ext,
            'move_to_ext_issue': self.dismissal.move_to_ext_issue,
        }
        old_data.update(
            (field, getattr(self.dismissal, field, ''))
            for field in self.dismissal.NEW_FIELDS
        )
        return diff_dict(old_data, new_data)

    def cancel(self):
        self.dismissal.status = DISMISSAL_STATUS.CANCELLED
        self.dismissal.save()
        self.disable()

        dismissal_cancelled.send(sender=self, dismissal=self.wrapper, cancelled_by=self.author)

    def can_be_cancelled(self):
        """Может быть отменена, если уже не уволен"""
        return self.dismissal.status in (
            DISMISSAL_STATUS.NEW,
            DISMISSAL_STATUS.IN_PROGRESS,
            DISMISSAL_STATUS.DATE_PASSED,
        )

    def enable(self):
        self.dismissal.intranet_status = 1
        self.dismissal.save()

    def disable(self):
        self.dismissal.intranet_status = 0
        self.dismissal.save()

    def can_see_dismissal_link(self, subject, object_):
        """
        Показываем ссылку на увольнение в зависимости есть ли процедура с
        обходным для сотрудника
        """
        if DismissalService.exists_dismissal_procedure_for(object_):
            dismissal = self.get(staff=object_)
            if not dismissal:
                return DismissalPerms.can_create_dismissal(subject, object_)
            else:
                available_dismissals = Dismissal.objects.available_for(subject)
                return dismissal in available_dismissals
        else:
            return DismissalPerms.can_dismiss_from_anketa(subject, object_)

    def complete(self, by_whom, force_dismiss=False):
        if self.dismissal.move_to_ext and not force_dismiss:
            if self.dismissal.staff.affiliation == AFFILIATION.YANDEX:
                raise self.PersonNotMoveToExt
        else:
            self._mark_staff_as_dismissed()

        self.dismissal.quit_datetime_real = datetime.now()
        self.dismissal.completed_by = by_whom
        self.dismissal.save()
        self.update_status()

        self._after_complete()

    def complete_fast(self):
        """Обработка быстрого увольнения с анкеты сотрудника"""
        self._mark_staff_as_dismissed()
        self._after_complete()

    def _after_complete(self):
        self._releive_positions(staff=self.person)
        self._discard_table_reservations(staff=self.person)

        dismissal_completed.send(
            sender=self,
            author=self.author,
            staff=self.person,
            dismissal=self.wrapper,
            chiefed_departments=self.chiefed_departments,
            deputed_departments=self.deputed_departments,
            departments_chains=DepartmentsChain(),
        )

    @classmethod
    def complete_passed(cls):
        today = date.today()
        staff_robot = Staff.objects.get(login=settings.ROBOT_STAFF_LOGIN)

        passed_qs = (
            Dismissal.objects
            .filter(intranet_status=1, deadline__lte=today, quit_datetime_real=None)
        )
        passed_ids = passed_qs.values_list('id', flat=True)

        for dismissal_id in passed_ids:
            with atomic():
                try:
                    dismissal = passed_qs.select_for_update().get(id=dismissal_id)
                except Dismissal.DoesNotExist:
                    continue
                force = (today - dismissal.deadline) >= timedelta(days=cls.MAX_MOVE_TO_EXT_DAYS)
                dismissal_ctl = cls(dismissal=dismissal, author=staff_robot)
                try:
                    dismissal_ctl.complete(by_whom=staff_robot, force_dismiss=force)
                except cls.PersonNotMoveToExt:
                    dismissal_ctl.set_status_passed_and_notify()

    def _is_completed(self):
        return self.dismissal.quit_datetime_real is not None

    def _mark_staff_as_dismissed(self):
        PersonCtl(self.person).dismiss().save(self.author)

    @staticmethod
    def _discard_table_reservations(staff):
        staff.table_reserve.all().delete()

    def _releive_positions(self, staff):
        """Сместить уволенного с позиций руководителя и заместителя"""
        # Вся полезная работа теперь в PersonCtl.dismiss
        for position in staff.departmentstaff_set.all():
            self._remember_position(position)

    def _remember_position(self, position):
        """Запомнить возглавляемые им подразделения для посыла сигнала"""

        if position.role == DepartmentRoles.CHIEF.value:
            self.chiefed_departments.append(position.department)

        if position.role == DepartmentRoles.DEPUTY.value:
            self.deputed_departments.append(position.department)

    def update_status(self):
        """Обновляем статус заявки уволенного сотрудника"""
        if not self.dismissal.staff.is_dismissed:
            return

        if self.dismissal.checkpoints.filter(checked=False).exists():
            if self.dismissal.status != DISMISSAL_STATUS.CHIT_NOT_COMPLETE:
                self.dismissal.status = DISMISSAL_STATUS.CHIT_NOT_COMPLETE
                dismissal_not_completed.send(sender=self, dismissal=self.wrapper)
            else:
                return
        else:
            self.dismissal.status = DISMISSAL_STATUS.DONE

        self.dismissal.save()

    def set_status_passed_and_notify(self):
        if self.dismissal.status != DISMISSAL_STATUS.DATE_PASSED:
            self.dismissal.status = DISMISSAL_STATUS.DATE_PASSED
            self.dismissal.save()
        dismissal_date_passed.send(sender=self, dismissal=self.wrapper)

    @property
    def maillists(self):
        return self.dismissal.maillists.all()

    def update_maillists(self, data):
        for ml in self.dismissal.maillists:
            data_item = data.pop(ml.pk, None)
            if data_item:
                ml.responsible = data_item.get('responsible')
                ml.save(force_update=True)

    def _create_clearance_chit(self):
        """
        Создание обходного по шаблонам пунктов для подразделения и офиса
        увольняемого
        """
        chit_tpl = DismissalService.get_chit_template_for(self.dismissal.staff)
        for cp_tpl in chit_tpl.checkpoints.all():
            cp = self.dismissal.checkpoints.create(
                name=cp_tpl.name,
                name_en=cp_tpl.name_en,
                description=cp_tpl.description,
                description_en=cp_tpl.description_en,
                position=cp_tpl.position,
                template=cp_tpl,
                native_lang=self.dismissal.office.native_lang
            )
            cp.save()

    @property
    def wrapper(self):
        if self.dismissal:
            return DismissalWrapper(dismissal=self.dismissal, author=self.author)
        else:
            return None


class DismissalWrapper(object):

    def __init__(self, dismissal, author=None):
        self.dismissal = dismissal
        self.author = author

    def __getattr__(self, item):
        return getattr(self.dismissal, item)

    @property
    def departments_chain(self):
        return DepartmentsChain().get_chain_as_list(
            department=self.dismissal.department)

    @property
    def checkpoints(self):
        return self.dismissal.checkpoints.order_by('position')

    @property
    def clearance_chit(self):
        return [CheckpointCtl(self.author, cp) for cp in self.checkpoints]

    @property
    def is_active(self):
        return (self.dismissal.intranet_status == 1 and
                self.dismissal.status != DISMISSAL_STATUS.CANCELLED)

    @property
    def is_completed(self):
        return self.dismissal.quit_datetime_real is not None

    @property
    def is_done(self):
        return self.dismissal.status == DISMISSAL_STATUS.DONE

    @property
    def mail_and_files_completed(self):
        d = self.dismissal
        return (d.delete_email_address or
                d.delete_correspondence or
                d.forward_correspondence_to is not None or
                d.give_correspondence_to is not None or
                d.delete_files or
                d.keep_files or
                d.give_files_to is not None)


class CheckpointCtl(object):

    def __init__(self, author=None, checkpoint=None):
        self.author = author
        self.checkpoint = checkpoint

    def check(self, checkpoint):
        checkpoint.checked = True
        checkpoint.checked_at = datetime.now()
        checkpoint.checked_by = self.author
        checkpoint.save(force_update=True)

        # после отметки последнего чекпоинта для уже уволенного статус должен
        # измениться с CHIT_NOT_COMPLETE на DONE
        DismissalCtl(author=self.author, dismissal=self.dismissal).update_status()

        return self

    @property
    def is_checkable(self):
        return not self.checked and DismissalPerms.can_check(
            subject=self.author, checkpoint=self.checkpoint)

    @property
    def dismissal(self):
        return self.checkpoint.dismissal

    @property
    def pk(self):
        return self.checkpoint.pk

    @property
    def i_name(self):
        return self.checkpoint.i_name

    @property
    def i_description(self):
        return self.checkpoint.i_description

    @property
    def checked(self):
        return self.checkpoint.checked

    @property
    def checked_at(self):
        return self.checkpoint.checked_at

    @property
    def checked_by(self):
        return self.checkpoint.checked_by
