from copy import deepcopy
from itertools import zip_longest
from typing import AnyStr, Dict, List

from django.conf import settings
from ids.exceptions import BackendError as idsBackendError

from staff.django_intranet_notifications.startrek import StartrekMessage

from phonenumbers import (
    format_number,
    PhoneNumberFormat,
    parse,
)
from datetime import timedelta
from staff.lib.utils.date import parse_datetime
from staff.person.models import Passport, Visa, DiscountCard

from staff.trip_questionary.controller.notifications import (
    TripMetaStartrekNotification, TripSinglePersonalStartrekNotification
)

from staff.trip_questionary.control import (
    set_issue,
    unset_creation_notification_sent,
)
from staff.trip_questionary.controller.context import (
    make_cities_chain,
    get_departure_city,
)

from staff.trip_questionary.models import EVENT_TYPE, TripPurpose
from staff.trip_questionary.controller.approvers import UnknownApprover
from staff.trip_questionary.utils import if_any_field

from staff.trip_questionary.controller.operations.base import OperationBase
from staff.trip_questionary.utils import get_assignment_line

import logging
logger = logging.getLogger(__name__)


purpose_st_fields = 'purposeOfTripNew1', 'purposeOfTripNew2'
event_roles = settings.EVENT_ROLES


class StartrekIssueCreationOperation(OperationBase):

    def get_approver(self):
        """Возвращает сотрудника, который должен подтвердить командировку."""
        return

    def get_issue_initiators(self):
        """Возвращает сущности, для которых создается тикет.
        Может быть целиком анкета или человек."""
        raise NotImplementedError('Class is abstract')

    def get_notification_class(self):
        """Возвращает класс нотификации"""
        raise NotImplementedError('Class is abstract')

    def get_notification_context(self):
        """Возвращает параметры для инстанцирования нотификации."""
        context = deepcopy(self.trip.data)

        func = getattr(self, 'update_notification_context_for_type', None)
        if func:
            func(context)

        approver = self.get_approver()
        context['approver'] = None if approver == UnknownApprover else approver
        context['is_foreign'] = self.TC.is_foreign
        context['is_trip'] = self.TC.is_trip
        context['form_url'] = self.get_form_url()

        context['purpose'] = list(
            TripPurpose.objects
            .filter(id__in=context.get('purpose', []))
            .values('name', 'name_en')
        )

        return context

    def get_send_params(self):
        """Возвращает параметры для отправки нотификации."""
        data = self.trip.data
        approver = self.get_approver()
        approver = None if approver == UnknownApprover else approver

        params = {
            'author': data['author'],
            'assignee': approver if approver else None,
            'unique': self.get_unique(),
        }

        purpose_ids = data.get('purpose', [])
        if purpose_ids:
            params.update(get_st_purpose_fields(purpose_ids))

        func = getattr(self, 'update_send_params_for_type', None)
        if func:
            func(params)

        func = getattr(self, 'update_employee_send_params_for_type', None)
        if func:
            func(params)

        return params

    def run(self):
        """Полезная работа"""

        context = self.get_notification_context()
        notification_class = self.get_notification_class()
        notification = notification_class(
            trip_uuid=self.trip.uuid,
            is_personal=self.is_personal,
            target='STARTREK_ISSUE',
            context=context,
        )

        params = self.get_send_params()
        try:
            issue = notification.send(**params)
        except idsBackendError as e:

            try:
                status_code = e.extra['statusCode']
            except (TypeError, AttributeError, KeyError):
                status_code = None

            if status_code == 409:  # тикет уже был создан
                repository = StartrekMessage(ids_user_agent=settings.STAFF_USER_AGENT).get_issues_repository()
                issue = repository.find(filter={'unique': self.get_unique()})[0]
            else:
                raise

        for issue_initiator in self.get_issue_initiators():
            set_issue(issue_initiator, issue, self.event_type)

        unset_creation_notification_sent(self.trip)

    def get_form_url(self):
        template = '{protocol}://{host}{path}'
        path = '/trip/?trip_uuid={uuid}'.format(uuid=self.trip.uuid)
        return template.format(
            protocol=settings.STAFF_PROTOCOL,
            host=settings.STAFF_HOST,
            path=path,
        )

    def get_unique(self):
        """Вернуть значение unique для создаваемого тикета"""
        return '{uuid}/{login}/{event_type}'.format(
            uuid=self.trip.uuid,
            login=self.get_login_for_unique(),
            event_type=self.event_type,
        )

    def get_login_for_unique(self):
        raise NotImplementedError('Class is too abstract')


class MetaStOperation(StartrekIssueCreationOperation):
    """Базовый класс для мета тикета.
    Создается для всей поездки, если несколько человек"""

    is_personal = False

    def __init__(self, *args, **kwargs):
        super(MetaStOperation, self).__init__(*args, **kwargs)
        # TODO разобраться зачем, кажется эти строки не нужны.
        self.employee_list = [
            e['employee'] for e in self.trip.data['employee_list']
        ]

    def get_issue_initiators(self):
        yield self.trip.data

    def get_send_params(self):
        params = super(MetaStOperation, self).get_send_params()

        params['tripuuid'] = self.trip.uuid

        followers = params.setdefault('followers', [])

        # Вся группа участников в наблюдатели
        for employee in self.employee_list:
            followers.append(employee.login)

        access = params.setdefault('access', [])
        # Автор на всякий случай в доступ, что бы могли обновлять тикет
        access.append(self.trip.data['author'].login)
        # Все HRBP  в доступ
        for hrbp_list in self.trip.get_hr_partners().values():
            for hrbp in hrbp_list:
                access.append(hrbp['login'])

        return params

    def get_login_for_unique(self):
        return '__meta__'


class BasePersonalStOperation(StartrekIssueCreationOperation):

    is_personal = True

    def get_login_for_unique(self):
        return self.employee.login

    def get_approver(self):
        return self.trip.get_approver(self.employee)

    def get_recipients(self):
        return self.trip.get_employee_recipients(self.employee)

    def get_accessors(self):
        return self.trip.get_employee_accessors(self.employee)

    def get_send_params(self):
        params = super(BasePersonalStOperation, self).get_send_params()

        params['tripuuid'] = self.trip.uuid

        followers = params.setdefault('followers', [])
        # Участник в наблюдатели своего тикета
        followers.append(self.employee.login)

        access = params.setdefault('access', [])
        # Все HRBP сотрудника в доступе
        for hrbp in self.trip.get_employee_hr_partners(self.employee):
            access.append(hrbp['login'])
        # Непосредстенный руководитель в доступе
        direct_chief = self.trip.get_employee_chief(self.employee)
        if direct_chief:
            access.append(direct_chief.login)

        accessors_getter = self.get_recipients
        if issubclass(self.get_notification_class(), (
            TripMetaStartrekNotification,
            TripSinglePersonalStartrekNotification,
        )):
            accessors_getter = self.get_accessors

        for accessor in accessors_getter():
            access.append(accessor.login)

        access.append(self.trip.data['author'].login)

        if self.trip.data['event_type'] == EVENT_TYPE.TRIP:
            tags = params.setdefault('tags', [])
            tags.append('поездка')

        return params

    def get_notification_context(self):
        context = (
            super(BasePersonalStOperation, self).get_notification_context()
        )
        login = self.employee_data['employee'].login

        context['employee_data'] = deepcopy(self.employee_data)
        context['need_approve'] = self.TC.is_employee_needs_approve
        context['employee_personal_data'] = self.get_personal_data(login)

        func = getattr(self, 'update_employee_notification_context_for_type', None)
        if func:
            func(context)

        return context

    @staticmethod
    def get_personal_data(login):  # type: (AnyStr) -> Dict[AnyStr, List[Dict]]
        passports = Passport.objects.filter(person__login=login, is_active=True).values()
        visas = Visa.objects.filter(person__login=login, is_active=True).values()
        discountcards = DiscountCard.objects.filter(person__login=login, is_active=True).values()
        return {
            'passports': list(passports),
            'visas': list(visas),
            'discountcards': list(discountcards),
        }


class SingleStOperation(BasePersonalStOperation):
    """Базовый класс для тикета, когда едет один человек."""

    def __init__(self, *args, **kwargs):
        kwargs['employee_data'] = kwargs['trip'].data['employee_list'][0]
        kwargs['employee'] = kwargs['employee_data']['employee']
        super(SingleStOperation, self).__init__(*args, **kwargs)

    def get_issue_initiators(self):
        yield self.trip.data
        yield self.employee_data


class PersonalStOperation(BasePersonalStOperation):
    """Базовый класс для для персонального тикета.
    Создается на каждого человека, когда их несколько."""

    def __init__(self, *args, **kwargs):
        super(PersonalStOperation, self).__init__(*args, **kwargs)
        self.employee = self.employee_data['employee']

    def get_issue_initiators(self):
        yield self.employee_data

    def get_parent_issue(self):
        return self.trip.data[self.event_type + '_issue']['key']

    def get_send_params(self):
        params = super(PersonalStOperation, self).get_send_params()
        params['parent'] = self.get_parent_issue()
        return params


def get_st_purpose_fields(purpose_ids=None):
    purpose_ids = purpose_ids or []
    purpose_names = (
        TripPurpose.objects
        .filter(id__in=purpose_ids[:len(purpose_st_fields)])
        .values_list('name', flat=True)
    )
    return dict(zip_longest(purpose_st_fields, purpose_names))


class TripMixin(object):

    event_type = 'trip'

    def update_send_params_for_type(self, params):
        if self.get_approver() is None:
            type_id = settings.ISSUE_TYPES['TRIP']
        else:
            type_id = settings.ISSUE_TYPES['TASK']

        data = self.trip.data

        params.update({
            'queue': settings.TRAVEL_QUEUE_ID,
            'type': type_id,
            'travelStartDate': data['trip_date_from'].isoformat(),
            'travelEndDate': data['trip_date_to'].isoformat(),
        })
        params.update(get_st_purpose_fields(data.get('purpose', [])))

        if self.trip.data['event_type'] == EVENT_TYPE.TRIP_CONF:
            tags = params.setdefault('tags', [])
            tags.append('конференция')

    def update_notification_context_for_type(self, context):
        context['cities_chain'] = make_cities_chain(self.trip.data)
        context['departure_city'] = get_departure_city(
            self.trip.data['city_list']
        )


class TripEmployeeMixin(object):

    def update_employee_send_params_for_type(self, params):
        direction = self.operations_cache.direction(self.employee)
        if direction:
            params['department'] = direction.name
        params['employee'] = self.employee.login
        params['itinerary'] = make_cities_chain(self.trip.data)
        if self.trip.data['receiver_side']:
            params['hostCompany'] = self.trip.data['receiver_side']
        params['purpose'] = self.trip.data['objective']
        params.update(get_st_purpose_fields(self.trip.data.get('purpose', [])))

        city_as_component = self.operations_cache.city_as_component(self.employee)
        params['components'] = [city_as_component.component]
        params['accounting'] = city_as_component.money_person.login
        params['humanResources'] = city_as_component.hr_person.login

        tags = params.setdefault('tags', [])
        cities = self.trip.data['city_list']

        cities_len = len(cities)
        if cities_len == 2:
            route_type = settings.ST_TRAVEL_FIELD_ROUTE_TYPE.get('there')
        elif cities_len == 3:
            route_type = settings.ST_TRAVEL_FIELD_ROUTE_TYPE.get('there_and_backward')
        else:
            route_type = settings.ST_TRAVEL_FIELD_ROUTE_TYPE.get('complex')
        params['routeType'] = route_type

        transports = set(city['transport'] for city in cities[1:])
        for transport in transports:
            tags.append(transport)

        first_city_from = cities[1]
        params['countryFrom'] = cities[0]['country']
        params['cityFrom'] = cities[0]['city']
        params['transportFrom'] = settings.ST_TRAVEL_FIELD_TRANSPORT.get(first_city_from['transport'])

        last_city_to = cities[-2]
        params['countryTo'] = last_city_to['country']
        params['cityTo'] = last_city_to['city']
        # тут тонкий момент, т.к. транспорт именно "обратно", судя по тикету
        params['transport'] = settings.ST_TRAVEL_FIELD_TRANSPORT.get(cities[-1]['transport'])

        last_not_return_city_index = -1 if cities[-1]['is_return_route'] else None
        intermediate_cities = cities[1:last_not_return_city_index]

        params['hotelNeeded'] = if_any_field(intermediate_cities, 'need_hotel', 'Да', 'Нет')
        params['ticketOrHotelUpgrade'] = if_any_field(cities, 'ready_to_upgrade', 'Да', 'Нет')
        if first_city_from['has_tickets']:
            params['selfPurchasedTicketsMoney'] = '%s %s' % (
                first_city_from['tickets_cost_currency'], first_city_from['tickets_cost']
            )
        assignment_id = self.employee_data.get('employee_assignment')
        if assignment_id:
            params['assignmentID'] = int(assignment_id)

        if self.employee_data['need_taxi']:
            trip_date_from = parse_datetime(params['travelStartDate'])
            trip_date_to = parse_datetime(params['travelEndDate'])
            taxi_date_from = trip_date_from - timedelta(days=1)
            taxi_date_to = trip_date_to + timedelta(days=1)

            params['yaStart'] = taxi_date_from.strftime('%Y-%m-%d')
            params['yaEnd'] = taxi_date_to.strftime('%Y-%m-%d')

    def update_employee_notification_context_for_type(self, context):
        if self.employee_data['need_taxi']:
            context['employee_data']['mobile_number_for_taxi'] = format_number(
                parse(self.employee_data['mobile_number_for_taxi']),
                PhoneNumberFormat.INTERNATIONAL,
            )
        chosen_assignment = self.operations_cache.get_employee_assignment(self.employee.login)
        if chosen_assignment:
            context['employee_data']['employee_assignment_line'] = get_assignment_line(chosen_assignment)


class ConfMixin(object):

    event_type = 'conf'

    def update_send_params_for_type(self, params):
        if self.get_approver() is None:
            type_id = settings.ISSUE_TYPES['CONF']
        else:
            type_id = settings.ISSUE_TYPES['EVENT']

        params.update({
            'queue': settings.INTERCONF_QUEUE_ID,
            'type': type_id,
        })


class ConfEmployeeMixin(object):

    def update_employee_send_params_for_type(self, params):
        followers = params.setdefault('followers', [])
        # Все HRBP сотрудника в наблюдатели
        for hrbp in self.trip.get_employee_hr_partners(self.employee):
            followers.append(hrbp['login'])
        # Непосредстенный руководитель в наблюдатели
        direct_chief = self.trip.get_employee_chief(self.employee)
        if direct_chief:
            followers.append(direct_chief.login)

        components = params.setdefault('components', [])
        department_component = (
            self.operations_cache.direction_as_component(self.employee)
        )
        if department_component:
            components.append(department_component)

        role = self.employee_data['event_role']
        if role in event_roles:
            components.append(event_roles[role])

        def date_to_str(date):
            return date.strftime('%Y-%m-%d')

        params['start'] = date_to_str(self.trip.data['event_date_from'])
        params['end'] = date_to_str(self.trip.data['event_date_to'])
        params['eventCost'] = self.trip.data['event_cost']
        params['cities'] = make_cities_chain(self.trip.data)
        params['conference'] = self.trip.data['event_name']
        params['employee'] = self.employee_data['employee'].login
