import logging
from datetime import datetime, timedelta
from typing import AnyStr, Dict, List

from staff.gap.workflows.decorators import allowed_states
from staff.gap.workflows.choices import GAP_STATES as GS
from staff.gap.workflows.email_mixin import EmailMixin
from staff.gap.workflows.startrek_mixin import StartrekMixin

from staff.gap.controllers.gap import GapCtl, PeriodicGapCtl
from staff.gap.controllers.utils import get_universal_person, get_chief
from staff.lib.models.roles_chain import direct_hr_partners_for_department
from staff.lib.utils.qs_values import extract_related

logger = logging.getLogger('staff.gap.workflows.base_workflow')


class BaseWorkflow(EmailMixin, StartrekMixin):

    editable_fields = [
        'date_from',
        'date_to',
        'work_in_absence',
        'comment',
        'to_notify',
    ]

    def __init__(
        self,
        modifier_id=None,
        person_id=None,
        gap=None,
    ):
        self.gap_ctl = GapCtl()

        self.gap = gap

        self._modifier_id = modifier_id
        self._modifier = None

        if self.gap is not None and person_id is None:
            self._person_id = self.gap['person_id']
        else:
            self._person_id = person_id

        self._person = None
        self._hrbps = None
        self._chief = None

    @classmethod
    def init_to_new(cls, modifier_id, person_id):
        return cls(
            modifier_id=modifier_id,
            person_id=person_id,
        )

    @classmethod
    def init_to_modify(
            cls,
            modifier_id,
            gap=None,
            gap_id=None,
    ):
        if gap is None and gap_id is not None:
            gap = GapCtl().find_gap_by_id(gap_id)

        return cls(
            modifier_id=modifier_id,
            gap=gap,
        )

    @property
    def modifier(self):
        if self._modifier is None:
            self._modifier = get_universal_person(person_id=self._modifier_id)
        return self._modifier

    @property
    def person(self):
        if self._person is None:
            self._person = get_universal_person(person_id=self._person_id)
        return self._person

    @property
    def hrpb_logins(self):
        # type: () -> List[AnyStr]
        if self._hrbps is None:
            dep = extract_related(self.person, 'department', pop=False)
            result = direct_hr_partners_for_department(dep, ['login'])
            self._hrbps = [hrbp['login'] for hrbp in result] if result else []
        return self._hrbps

    @property
    def chief(self):
        # type: () -> Dict
        if self._chief is None:
            self._chief = get_chief(self.person['login'])
        return self._chief

    def _create_new_gap(self, data):
        return self.gap_ctl.new_gap(self._modifier_id, self._create_gap(data))

    def _create_self_gap(self, data):
        self.gap = self._create_new_gap(data)

    def _set_state(self, state):
        self.gap['state'] = state

    def _update(self):
        self.gap_ctl.update_gap(self._modifier_id, self.gap)

    def _create_gap(self, data):
        gap_data = self._create_base_gap(data)

        if hasattr(self, '_append_gap'):
            gap_data.update(self._append_gap(data))

        return gap_data

    def _create_base_gap(self, data):
        now = datetime.now()

        base_gap = {
            'workflow': self.workflow,
            'gap_type': self.gap_type,
            'created_at': data.get('created_at') or now,
            'created_by_id': self.modifier['id'],
            'created_by_uid': self.modifier['uid'],
            'person_id': self.person['id'],
            'person_login': self.person['login'],
            'date_from': data['date_from'],
            'date_to': data['date_to'],
            'full_day': data['full_day'],
            'state': data.get('state') or 'new',
            'work_in_absence': data.get('work_in_absence', False),
            'comment': data.get('comment', ''),
            'to_notify': data.get('to_notify', []),
            'hactions': {},
            'periodic_gap_id': data.get('periodic_gap_id'),
        }

        return base_gap

    def get_base_default(self, person):
        now = datetime.now()

        base_default = {
            'person': {
                'first_name': person['first_name'],
                'last_name': person['last_name'],
                'login': person['login'],
                'id': person['id'],
            },
            'date_from': now,
            'date_to': now,
            'full_day': True,
            'work_in_absence': False,
            'comment': '',
            'office': person['office_id'],
            'room': person['room_id'],
        }

        if hasattr(self, '_append_base_default'):
            base_default.update(self._append_base_default())

        return base_default

    def _update_editable(self, src, editable_fields):
        updated = {}
        for field in editable_fields:
            old_value = self.gap.get(field)
            new_value = src.get(field)
            if field in src and old_value != new_value:
                updated[field] = {
                    'old': old_value,
                    'new': new_value,
                }
                self.gap[field] = new_value

        # раздельно эти даты не нужны
        if ('date_from' in updated) and ('date_to' not in updated):
            updated['date_to'] = {
                'old': self.gap['date_to'],
                'new': self.gap['date_to'],
            }
        elif ('date_to' in updated) and ('date_from' not in updated):
            updated['date_from'] = {
                'old': self.gap['date_from'],
                'new': self.gap['date_from'],
            }

        return updated

    def _clean_haction(self, haction_name):
        self.gap['hactions'] = {
            h: d for h, d in self.gap.get('hactions', {}).items()
            if d['haction_name'] != haction_name
        }

    def _remove(self):
        _id = self.gap_ctl.find_gap_by_id(self.gap['id'], del_id=False)['_id']
        self.gap_ctl.remove(_id)


class BasePeriodicWorkflow(BaseWorkflow):

    def __init__(
        self,
        modifier_id=None,
        person_id=None,
        gap=None,
        periodic_gap=None,
        gap_from=None,
    ):
        """
        Возможны следующе комбинации
        1) есть gap_from/gap_id_from, есть periodic_gap_id/periodic_gap
         - воркфлоу для редактирования периодического и всех связанных с ним одиночных отсутствий начиная с данного
        2) есть gap_from/gap_id_from, нет periodic_gap_id/periodic_gap
         - не возможно
        3) нет gap_from/gap_id_from, есть periodic_gap_id/periodic_gap
         - вокфлоу для редактирования периодического и всех связанных с ним одиночных отсутствий
        4) нет gap_from/gap_id_from, нет periodic_gap_id/periodic_gap
         - вокфлоу для создания периодического отсутствия
        """
        self.gap_ctl = GapCtl()
        self.periodic_gap_ctl = PeriodicGapCtl()

        self.gap_from = gap_from

        self.periodic_gap = periodic_gap

        if self.periodic_gap is not None and person_id is None:
            person_id = self.periodic_gap['person_id']

        super(BasePeriodicWorkflow, self).__init__(
            gap=gap,
            modifier_id=modifier_id,
            person_id=person_id,
        )

    @classmethod
    def init_to_modify_periodic_gap(
            cls,
            modifier_id,
            periodic_gap=None,
            periodic_gap_id=None,
            gap_from=None,
            gap_from_id=None,
    ):
        assert gap_from or gap_from_id
        if gap_from is None and gap_from_id is not None:
            gap_from = GapCtl().find_gap_by_id(gap_from_id)

        assert periodic_gap or periodic_gap_id
        if periodic_gap is None and periodic_gap_id is not None:
            periodic_gap = PeriodicGapCtl().find_gap_by_id(periodic_gap_id)

        return cls(
            modifier_id=modifier_id,
            periodic_gap=periodic_gap,
            gap_from=gap_from,
        )

    @staticmethod
    def generate_dates_from(date_from, data):
        start_weekday = date_from.weekday()
        periodic_date_to = data['periodic_date_to']
        period = data['period']
        periodic_map_weekdays = data['periodic_map_weekdays']

        i = 0
        while True:
            for day in periodic_map_weekdays:
                offset = (i * period * 7) - start_weekday + int(day)
                cur_date = date_from + timedelta(days=offset)
                if cur_date < date_from:
                    continue
                elif cur_date > periodic_date_to:
                    return
                else:
                    yield cur_date
            i += 1

    def _create_gaps_with_period(self, data: dict):
        date_from = data['date_from']
        date_to = data['date_to']

        duration = date_to - date_from

        for _date_from in self.generate_dates_from(date_from, data):
            data['date_from'] = _date_from
            data['date_to'] = _date_from + duration

            gap = self._create_new_gap(data)
            if self.gap is None:
                self.gap = gap

    def _create_self_periodic_gap(self, data):
        self.periodic_gap = self.periodic_gap_ctl.new_gap(self._modifier_id, self._create_periodic_gap(data))
        data['periodic_gap_id'] = self.periodic_gap['id']
        self._create_gaps_with_period(data)

    def _set_state_gaps_with_period(self, state):
        if self.gap_from:
            self.periodic_gap['periodic_date_to'] = self.gap_from['date_from'] - timedelta(days=1)
            self.gap_ctl.update_gaps_with_period_after_gap(self.periodic_gap['id'], self.gap_from, {'state': state})
        else:
            self.gap_ctl.update_all_gaps_with_period(self.periodic_gap['id'], {'state': state})

    def _update_periodic_gap(self):
        self.periodic_gap_ctl.update_gap(self._modifier_id, self.periodic_gap)

    def _create_periodic_gap(self, data):
        now = datetime.now()

        periodic_gap = {
            'workflow': self.workflow,
            'gap_type': self.gap_type,
            'periodic_type': data.get('periodic_type'),
            'period': data['period'],
            'state': data.get('state') or 'new',
            'created_at': data.get('created_at') or now,
            'created_by_id': self.modifier['id'],
            'created_by_uid': self.modifier['uid'],
            'person_id': self.person['id'],
            'person_login': self.person['login'],
            'date_from': data['date_from'],
            'date_to': data['date_to'],
            'periodic_date_to': data['periodic_date_to'],
            'periodic_map_weekdays': data['periodic_map_weekdays'],
            'comment': data.get('comment', ''),
            'to_notify': data.get('to_notify', []),
            'hactions': {},
            'full_day': data.get('full_day', True),
            'work_in_absence': data.get('work_in_absence', True),
        }

        return periodic_gap

    def _remove_repeats(self):
        _id = self.periodic_gap_ctl.find_gap_by_id(self.periodic_gap['id'], del_id=False)['_id']
        self.gap_ctl.remove_all_gaps_with_period(self.periodic_gap['id'])
        self.periodic_gap_ctl.remove(_id)


class BaseTripWorkflow(BaseWorkflow):

    @allowed_states([GS.NEW, GS.CONFIRMED])
    def edit_gap(self, data):
        updated = self._update_editable(data, ['work_in_absence'])
        if not updated:
            return

        self._update()
        if self.gap['state'] == GS.CONFIRMED:
            self._edit_gap_email(updated, approver=self.chief)

    def _append_gap(self, data):
        return {
            'master_issue': data.get('master_issue'),
            'slave_issues': data.get('slave_issues', []),
            'form_key': data.get('form_key'),
        }

    def _append_base_default(self):
        return {
            'master_issue': None,
            'slave_issues': [],
            'form_key': None,
        }

    def _check_new_form_key(self, form_key, person_id):
        gaps = list(self.gap_ctl.find_gaps(query={'form_key': form_key, 'person_id': person_id}, fields=['id']))
        if gaps:
            return gaps[0]['id']

    def tq_edit_gap(self, data):
        updated = self._update_editable(data, BaseWorkflow.editable_fields)
        if not updated:
            return

        self._update()
        self._edit_gap_email(updated, approver=get_chief(self.gap['person_login']))

    def tq_renew_gap(self):
        if self.gap['state'] == GS.NEW:
            return
        self._set_state(GS.NEW)
        self._update()

    def tq_confirm_gap(self):
        if self.gap['state'] == GS.CONFIRMED:
            return
        self._set_state(GS.CONFIRMED)
        self._update()
        self._new_gap_email()

    def tq_cancel_gap(self):
        if self.gap['state'] == GS.CANCELED:
            return
        was_confirmed = self.gap['state'] == GS.CONFIRMED
        self._set_state(GS.CANCELED)
        self._update()
        if was_confirmed:
            self._cancel_gap_email(approver=get_chief(self.gap['person_login']))
