from datetime import date, datetime
import logging
from typing import Callable, Set

import yenv

from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ObjectDoesNotExist

from staff.audit.factory import create_log
from staff.departments.models import Department
from staff.emails.handlers import delete_email_redirections, update_staff_emails
# Без этого импорта не ловятся сигналы в ленте
from staff.lenta.handlers import person_extra_fields_log_changes, person_log_changes  # noqa
from staff.users.models import User

from staff.person import effects
from staff.person.models import Staff, StaffExtraFields
from staff.person.passport.internal import sync_with_internal_passport


logger = logging.getLogger(__name__)


class Person:
    _extra = None

    def __init__(self, staff):
        """@type staff: Staff"""
        self.instance = staff

    def _get_extra(self, create=False):
        if not self._extra:
            try:
                self._extra = self.instance.extra
            except StaffExtraFields.DoesNotExist:
                if create:
                    self._extra = StaffExtraFields(staff=self.instance)
                else:
                    self._extra = None
        return self._extra

    def save(self):
        if self._get_extra():
            self._get_extra().save()

    @property
    def nda_end_date(self):
        return self._get_extra() and self._get_extra().nda_end_date

    @nda_end_date.setter
    def nda_end_date(self, value):
        self._get_extra(create=True).nda_end_date = value

    @property
    def contract_end_date(self):
        return self._get_extra() and self._get_extra().contract_end_date

    @contract_end_date.setter
    def contract_end_date(self, value):
        self._get_extra(create=True).contract_end_date = value

    @property
    def date_survey_letter(self):
        return self._get_extra() and self._get_extra().date_survey_letter

    @date_survey_letter.setter
    def date_survey_letter(self, value):
        self._get_extra(create=True).date_survey_letter = value

    @property
    def oebs_first_name(self):
        return self._get_extra() and self._get_extra().oebs_first_name

    @oebs_first_name.setter
    def oebs_first_name(self, value):
        self._get_extra(create=True).oebs_first_name = value

    @property
    def oebs_middle_name(self):
        return self._get_extra() and self._get_extra().oebs_middle_name

    @oebs_middle_name.setter
    def oebs_middle_name(self, value):
        self._get_extra(create=True).oebs_middle_name = value

    @property
    def oebs_last_name(self):
        return self._get_extra() and self._get_extra().oebs_last_name

    @oebs_last_name.setter
    def oebs_last_name(self, value):
        self._get_extra(create=True).oebs_last_name = value

    @property
    def oebs_address(self):
        return self._get_extra() and self._get_extra().oebs_address

    @oebs_address.setter
    def oebs_address(self, value):
        self._get_extra(create=True).oebs_address = value

    @property
    def oebs_manage_org_name(self):
        return self._get_extra() and self._get_extra().oebs_manage_org_name

    @oebs_manage_org_name.setter
    def oebs_manage_org_name(self, value):
        self._get_extra(create=True).oebs_manage_org_name = value

    @property
    def oebs_headcount(self):
        return self._get_extra() and self._get_extra().oebs_headcount

    @oebs_headcount.setter
    def oebs_headcount(self, value):
        self._get_extra(create=True).oebs_headcount = value

    @property
    def is_welcome_mail_sent(self):
        return self._get_extra() and self._get_extra().is_welcome_mail_sent

    @is_welcome_mail_sent.setter
    def is_welcome_mail_sent(self, value):
        self._get_extra(create=True).is_welcome_mail_sent = value

    @property
    def is_offer_avatar_uploaded(self):
        return self._get_extra() and self._get_extra().is_offer_avatar_uploaded

    @is_offer_avatar_uploaded.setter
    def is_offer_avatar_uploaded(self, value):
        self._get_extra(create=True).is_offer_avatar_uploaded = value

    @property
    def occupation(self):
        return self._get_extra() and self._get_extra().occupation

    @occupation.setter
    def occupation(self, value):
        self._get_extra(create=True).occupation = value

    byod_access = property(
        fget=lambda self: self._get_extra() and self._get_extra().byod_access,
        fset=lambda self, byod_access: setattr(
            self._get_extra(create=True),
            'byod_access',
            byod_access,
        ),
    )

    @property
    def wiretap(self):
        return self._get_extra() and self._get_extra().wiretap

    @wiretap.setter
    def wiretap(self, value):
        self._get_extra(create=True).wiretap = value

    @property
    def staff_agreement(self):
        return self._get_extra() and self._get_extra().staff_agreement

    @staff_agreement.setter
    def staff_agreement(self, value):
        self._get_extra(create=True).staff_agreement = value

    @property
    def staff_biometric_agreement(self):
        return self._get_extra() and self._get_extra().staff_biometric_agreement

    @staff_biometric_agreement.setter
    def staff_biometric_agreement(self, value):
        self._get_extra(create=True).staff_biometric_agreement = value

    @property
    def dismissal_date(self):
        return self.instance.quit_at if self.instance.is_dismissed else None


class CtlField(object):
    def __init__(self, field_name, pre_effects=None, post_effects=None, delayed_effects=None):
        self.field_name = field_name
        self.pre_effects = pre_effects if pre_effects else []
        self.post_effects = post_effects if post_effects else []
        self.delayed_effects = delayed_effects if delayed_effects else []

    def __get__(self, instance: 'PersonCtl', owner):
        return getattr(instance.instance, self.field_name)

    def run_pre_set(self, instance: 'PersonCtl', value):
        for effect in self.pre_effects:
            effect(instance, value)

    def run_post_set(self, instance: 'PersonCtl', value):
        for effect in self.post_effects:
            effect(instance)

        for effect in self.delayed_effects:
            instance._delayed_effects.add(effect)

    def __set__(self, instance, value):
        try:
            old_value = getattr(instance.instance, self.field_name)
        except ObjectDoesNotExist:
            old_value = None

        if old_value == value:
            return

        self.run_pre_set(instance, value)
        setattr(instance.instance, self.field_name, value)
        self.run_post_set(instance, value)


class PersonCtl:
    IMMUTABLE_FIELDS = {'pk', 'id', 'created_at', 'login', 'join_at'}
    ALL_FIELDS = {f.name for f in Staff._meta.fields}
    EDITABLE_FIELDS = ALL_FIELDS - IMMUTABLE_FIELDS

    def __init__(self, person):
        self.instance = person
        self.CTL_FIELDS = [
            f
            for f in self.ALL_FIELDS
            if f in PersonCtl.__dict__
        ]
        self._preprofile_id = None
        self._delayed_effects: Set[Callable[['PersonCtl'], None]] = set()

    @property
    def old_state(self):
        if not hasattr(self, '_old_state'):
            if self.instance.id:
                self._old_state = Staff.objects.filter(id=self.instance.id).first()
            else:
                self._old_state = None
        return self._old_state

    def __getattr__(self, name):
        try:
            return super(PersonCtl, self).__getattr__(name)
        except AttributeError:
            if name in self.CTL_FIELDS:
                raise
            return getattr(self.instance, name)

    def __setattr__(self, name, value):
        is_person_attr = (
            name != 'instance'
            and hasattr(self.instance, name)
            and name not in self.CTL_FIELDS
        )

        if is_person_attr:
            setattr(self.instance, name, value)
        else:
            super(PersonCtl, self).__setattr__(name, value)

    tz = CtlField('tz', delayed_effects=[sync_with_internal_passport])
    lang_ui = CtlField('lang_ui', delayed_effects=[sync_with_internal_passport, effects.push_person_data_to_ad])
    birthday = CtlField('birthday', delayed_effects=[sync_with_internal_passport])
    gender = CtlField('gender', delayed_effects=[sync_with_internal_passport])
    date_completion_internship = CtlField('date_completion_internship')
    department = CtlField(
        'department',
        pre_effects=[effects.check_department],
        post_effects=[effects.actualize_is_big_boss, effects.actualize_affiliation],
        delayed_effects=[effects.actualize_group, effects.push_person_data_to_ad],
    )
    affiliation = CtlField(
        'affiliation',
        delayed_effects=[effects.sync_external_logins, effects.actualize_yandex_disk, effects.push_person_data_to_ad],
    )
    position_en = CtlField('position_en', delayed_effects=[effects.push_person_data_to_ad])
    first_name = CtlField(
        'first_name',
        delayed_effects=[effects.actualize_django_user, sync_with_internal_passport, effects.push_person_data_to_ad],
    )
    first_name_en = CtlField('first_name_en', delayed_effects=[effects.push_person_data_to_ad])
    last_name = CtlField(
        'last_name',
        delayed_effects=[effects.actualize_django_user, sync_with_internal_passport, effects.push_person_data_to_ad],
    )
    last_name_en = CtlField('last_name_en', delayed_effects=[effects.push_person_data_to_ad])
    home_email = CtlField('home_email', delayed_effects=[update_staff_emails])
    work_email = CtlField('work_email', delayed_effects=[effects.actualize_django_user, effects.actualize_gravatar])
    office = CtlField(
        'office',
        post_effects=[
            effects.actualize_is_homeworker,
            effects.actualize_is_robot,
            effects.actualize_tz,
            effects.actualize_table_by_office,
            effects.actualize_room_by_office,
        ],
        delayed_effects=[
            effects.push_person_data_to_ad,
        ],
    )
    domain = CtlField('domain', post_effects=[effects.actualize_work_email])
    table = CtlField(
        'table',
        pre_effects=[effects.check_office],
        post_effects=[effects.actualize_table_by_table, effects.actualize_room_by_table],
    )
    is_login_passport_confirmed = CtlField('is_login_passport_confirmed', delayed_effects=[update_staff_emails])
    join_at = CtlField(
        'join_at',
        delayed_effects=[
            effects.give_achievement_beginner,
            effects.give_achievement_employee,
            effects.deprive_achievement_beginner,
            effects.deprive_achievement_employee,
            effects.change_level_achievement_employee,
        ],
    )

    def _perform_delayed_effects(self):
        """Тут отрабатывают эфекты, которые меняют связанные сущности.
        Важно!! Они не должны менять саму сущность иначе надо усложнять логику.
        """
        for effect in self._delayed_effects:
            effect(self)
        self._delayed_effects = set()

    def save(self, author_user=None):
        effects.actualize_phones(self)
        self.modified_at = datetime.now()
        self.old_state  # форсирую получение старого состояния до сохранения
        self.instance.save()
        logger.info('person %s updated', self.instance.login)

        self._perform_delayed_effects()

        if isinstance(author_user, Staff):
            author_user = author_user.user

        if not (author_user is None or isinstance(author_user, AnonymousUser)):
            create_log(
                objects=[self.instance],
                who=author_user,
                action='staff_updated',
                primary_key=self.id
            )

        return self

    def update(self, data):
        self._preprofile_id = data.pop('preprofile_id', None)
        for attr, value in data.items():
            if attr in self.EDITABLE_FIELDS or attr == 'preprofile_id':
                setattr(self, attr, value)
        return self

    @classmethod
    def create(cls, person_data):
        login = person_data['login']
        user = User.objects.create(username=login)
        created_at = datetime.now()
        instance = cls(Staff())

        instance._preprofile_id = person_data.pop('preprofile_id', None)
        instance.user = user
        instance.native_lang = 'ru'
        instance.created_at = created_at
        instance.modified_at = created_at
        instance.join_at = date.today()
        instance.from_staff_id = 0
        instance.login = login
        instance.login_ld = login
        instance.lang_ui = settings.DEFAULT_LANG_UI
        instance.tz = settings.TIME_ZONE

        instance._delayed_effects.add(effects.start_emailing)
        instance._delayed_effects.add(effects.actualize_work_phone)
        instance._delayed_effects.add(effects.set_avatar_from_preprofile)
        instance._delayed_effects.add(effects.actualize_yandex_disk)
        instance._delayed_effects.add(effects.push_person_data_to_ad)

        if yenv.type == 'production':
            instance._delayed_effects.add(effects.create_user_in_testing)

        if 'home_email' in person_data:
            instance._delayed_effects.add(effects.create_personal_email)

        instance.update(person_data)

        effects.actualize_robot_shell(instance)

        return instance

    def dismiss(self):
        effects.delete_roles(self)

        self.is_dismissed = True
        self.quit_at = date.today()

        effects.actualize_is_big_boss(self)
        self._delayed_effects |= {
            effects.sync_external_logins,
            effects.actualize_yandex_disk,
            effects.block_internal_passport,
            effects.unmark_phones_for_digital_sign,
        }

        return self

    def restore(self):
        self.is_dismissed = False
        self.join_at = date.today()
        self.vacation = None
        self.extra_vacation = None

        extra = Person(self.instance)
        extra.is_offer_avatar_uploaded = False
        extra.save()

        delete_email_redirections(self.instance)
        self._delayed_effects |= {
            effects.activate_user,
            effects.give_achievement_restored,
            effects.sync_external_logins,
            effects.actualize_work_phone,
            effects.set_avatar_from_preprofile,
            effects.create_personal_email,
            effects.actualize_yandex_disk,
            effects.unblock_login_in_passport,
            effects.push_person_data_to_ad,
        }
        effects.actualize_robot_shell(self)

        return self

    def adopt_external_to_yandex_or_outstaff(self, person_data):
        self._preprofile_id = person_data.pop('preprofile_id', None)

        self._check_adoption_data(person_data)

        self._delayed_effects.add(effects.set_avatar_from_preprofile)
        self._delayed_effects.add(effects.actualize_yandex_disk)
        self._delayed_effects.add(effects.drop_department_roles)
        self._delayed_effects.add(effects.actualize_table_by_table)

        if 'home_email' in person_data:
            self._delayed_effects.add(effects.create_personal_email)

        # explicit changing department
        self.department = person_data.pop('department')

        # updating rest fields
        self.update(person_data)

        self.join_at = date.today()

        return self

    def rotate(self, person_data):
        self._preprofile_id = person_data.pop('preprofile_id', None)
        self._check_adoption_data(person_data)

        self._delayed_effects.add(effects.drop_department_roles)
        self._delayed_effects.add(effects.actualize_table_by_table)
        self._delayed_effects.add(effects.push_person_data_to_ad)
        self.update(person_data)
        return self

    def _check_adoption_data(self, person_data):
        yandex_department = Department.objects.get(intranet_status=1, id=settings.YANDEX_DEPARTMENT_ID)
        outstaff_department = Department.objects.get(intranet_status=1, id=settings.OUTSTAFF_DEPARTMENT_ID)

        constant_fields = {'login', 'uid', 'guid'} & set(person_data.keys())
        for field in constant_fields:
            if getattr(self, field) != person_data.pop(field, None):
                raise RuntimeError(
                    'Error in adoption. Trying to change field `{}` '
                    'preprofile_id {}'.format(field, self._preprofile_id)
                )

        right_move = (
            person_data['department'].is_descendant_of(yandex_department, include_self=True)
            or person_data['department'].is_descendant_of(outstaff_department, include_self=True)
        )
        if not right_move:
            raise RuntimeError(
                'Trying to adopt person not to yandex nor outstaff preprofile_id {}'.format(self._preprofile_id)
            )

        extra_fields = set(person_data.keys()) - self.EDITABLE_FIELDS
        if extra_fields:
            raise RuntimeError(
                'Trying to change forbidden fields: {}'.format(list(extra_fields))
            )

    def actualize_is_big_boss(self):
        effects.actualize_is_big_boss(self)
        return self
