from datetime import datetime
from typing import Optional

import pymongo

from staff.gap.controllers.counter import CounterCtl
from staff.gap.exceptions import GapNotFoundError
from staff.gap.controllers.query_builder import QueryBuilder
from staff.gap.controllers.mongo import MongoCtl
from staff.gap.controllers.utils import datetime_to_utc


class GapCtl(MongoCtl):

    MONGO_COLLECTION = 'gaps'
    COUNTER_NAME = 'gap'

    def _insert_modify_data(self, modifier_id, gap):
        if modifier_id is not None:
            gap['modified_by_id'] = modifier_id
            gap['modified_at'] = datetime.now()

            self._append_log(gap)

    def new_gap(self, modifier_id, gap, return_gap=True, del_id=False) -> Optional[dict]:
        _id = int(CounterCtl().get_counter(self.COUNTER_NAME))

        gap['id'] = _id
        gap['log'] = []

        self._insert_modify_data(modifier_id, gap)
        self._insert(gap)

        if return_gap:
            return self.find_gap_by_id(_id, del_id=del_id)

    def update_gap(self, modifier_id, gap) -> dict:
        self._insert_modify_data(modifier_id, gap)
        self._update(gap)

        return self.find_gap_by_id(gap['id'], del_id=False)

    def find_gap_by_id(self, gap_id, del_id=True) -> dict:
        query = {'id': int(gap_id)}
        gap = self._collection().find_one(query)
        if not gap:
            raise GapNotFoundError(query)
        if gap and del_id and '_id' in gap:
            del gap['_id']
        return gap

    def find_gap_extended(self, query=None, fields=None, sorting=None, del_id=True) -> Optional[dict]:
        gap = self.find(query=query, fields=fields, sorting=sorting, del_id=del_id).limit(1)
        if gap and gap.count():
            return gap[0]

    def find_gaps(self, query=None, fields=None, sorting=None, del_id=True) -> pymongo.cursor.Cursor:
        sorting = sorting or [('id', pymongo.ASCENDING)]

        return self.find(
            query=query,
            fields=fields,
            del_id=del_id,
            sorting=sorting,
        )

    @staticmethod
    def _append_log(gap):
        gap['log'].append({
            'modified_at': gap['modified_at'],
            'modified_by_id': gap['modified_by_id'],
            'state': gap['state'],
        })

    def remove_all_gaps_with_period(self, periodic_gap_id):
        self._collection().delete_many({'periodic_gap_id': periodic_gap_id})

    def update_all_gaps_with_period(self, periodic_gap_id, data):
        self._collection().update_many({'periodic_gap_id': periodic_gap_id}, {'$set': data})

    def update_gaps_with_period_after_gap(self, periodic_gap_id, gap_from, data):
        self._collection().update_many(
            {
                'periodic_gap_id': periodic_gap_id,
                'date_from': {'$gte': gap_from['date_from']}
            },
            {'$set': data},
        )


class PeriodicGapCtl(GapCtl):

    MONGO_COLLECTION = 'periodic_gaps'
    COUNTER_NAME = 'periodic_gap'


class GapQueryBuilder(QueryBuilder):

    def __init__(self):
        super(GapQueryBuilder, self).__init__()

    def person_login(self, person_login):
        return self.op_eq('person_login', person_login)

    def person_logins(self, person_logins):
        return self.op_in('person_login', person_logins)

    def person_id(self, person_id):
        return self.op_eq('person_id', person_id)

    def person_ids(self, person_ids):
        return self.op_in('person_id', person_ids)

    def gap_id(self, gap_id):
        return self.op_eq('id', gap_id)

    def gap_ids(self, gap_ids):
        return self.op_in('id', gap_ids)

    def workflow(self, workflow):
        return self.op_eq('workflow', workflow)

    def workflows(self, workflows):
        return self.op_in('workflow', workflows)

    def state(self, state):
        return self.op_eq('state', state)

    def gaps_with_period(self, periodic_gap_id):
        return self.op_eq('periodic_gap_id', periodic_gap_id)

    def states(self, states):
        return self.op_in('state', states)

    def date_from(self, date_from):
        return self.op_gte('date_from', date_from)

    def date_to(self, date_to):
        return self.op_lte('date_to', date_to)

    def work_in_absence(self, work_in_absence):
        return self.op_eq('work_in_absence', work_in_absence)

    def full_day(self, full_day):
        return self.op_eq('full_day', full_day)

    def date_in_range(self, date_name, date_from, date_to):
        self.op_gte(date_name, date_from)
        self.op_lte(date_name, date_to)
        return self

    def sent_aftertrip_reminder(self, data):
        if data:
            return self.op_eq('sent_aftertrip_reminder', data)
        else:
            self._stack.append({'$or': [
                {'sent_aftertrip_reminder': {'$exists': False}},
                {'sent_aftertrip_reminder': False},
            ]})
            return self

    def commented_vacation_statement_reminder(self, data):
        if data:
            return self.op_eq('commented_vacation_statement_reminder', data)
        else:
            self._stack.append({'$or': [
                {'commented_vacation_statement_reminder': {'$exists': False}},
                {'commented_vacation_statement_reminder': False},
            ]})
            return self

    def last_day_when_commented_vacation_approval_reminder(self, day):
        # за сколько дней до начала отпуска был последний пинг
        self._stack.append({'$or': [
            {'last_day_when_commented_vacation_approval_reminder': {'$exists': False}},
            {'last_day_when_commented_vacation_approval_reminder': {'$gt': day}},
            {'last_day_when_commented_vacation_approval_reminder': None},
        ]})
        return self

    def dates_not_strict(self, date_from, date_to):
        # Рассуждать проще от обратного. Отрезки НЕ пересекаются если:
        # начало второго позднее, чем конец первого
        # ----df1*****dt1-----df2*******dt2------------>
        # или конец второго раньше, чем начало первого
        # ----df2*****dt2-----df1*******dt1------------>
        # Получается
        # (dt1 < df2) or (df1 > dt2)
        # нам же нужно обратное условие
        # not((dt1 < df2) or (df1 > dt2))
        # избавившись от not, получим
        # (dt1 >= df2) and (df1 <= dt2)
        self._stack.append({
            '$and': [
                {'date_to': {'$gte': date_from}},
                {'date_from': {'$lte': date_to}},
            ]
        })
        return self

    def dates_not_strict_with_tz(self, date_from, date_to, tz):
        self.op_or([
            {
                '$and': [
                    {'date_to': {'$gte': date_from}},
                    {'date_from': {'$lte': date_to}},
                    {'full_day': True},
                ]
            },
            {
                '$and': [
                    {'date_to': {'$gte': datetime_to_utc(date_from, tz)}},
                    {'date_from': {'$lte': datetime_to_utc(date_to, tz)}},
                    {'full_day': False},
                ]
            },
        ])
        return self

    def date_interval_intersect(self, date_from, date_to):
        self._stack.append({
            '$or': [
                {'date_to': {'$gte': date_from}},
                {'date_from': {'$lte': date_to}},
            ]
        })
        return self

    def workflows_states(self, data):
        query = []
        for workflows, states in data:
            query.append({'$and': [
                {'workflow': {'$in': workflows}},
                {'state': {'$in': states}},
            ]})
        return self.op_or(query)
