import io
import datetime
import xlwt
import pytz

from collections import OrderedDict, defaultdict

from django.conf import settings
from django.db.models import Value as V, Case, When, CharField
from django.db.models.functions import Concat, Cast
from django.utils.functional import cached_property

from intranet.femida.src.actionlog.models import Snapshot, SNAPSHOT_REASONS
from intranet.femida.src.offers import choices
from intranet.femida.src.offers.choices import SOURCES_TRANSLATIONS, REJECTIONS_SIDE_TRANSLATIONS
from intranet.femida.src.offers.startrek.choices import REJECTION_REASONS_TRANSLATIONS
from intranet.femida.src.staff.choices import DEPARTMENT_ROLES
from intranet.femida.src.staff.models import Department, DepartmentUser
from intranet.femida.src.core.db import PythonConverterExpression


local_timezone = pytz.timezone('Europe/Moscow')


class XLSheetProxy:

    def __init__(self, sheet):
        self.sheet = sheet
        self.last_row = 0

    def writerow(self, data):
        for c, v in enumerate(data):
            self.sheet.write(self.last_row, c, v)
        self.last_row += 1

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


class DepartmentHelper:
    """
    Вспомогательный класс для получения данных
    на основе подразделений.
    Нужен, чтобы иметь расширенные возможности при работе
    с QuerySet.(values,values_list).
    Кэширует все подразделения, поэтому в рамках
    одного основного запроса в БД, мы не ходим за подразделениями много раз.

    Все методы fetch_* работают как пост-обработчики сырых данных
    полученных из БД.
    Например, чтобы получить руководителя на оффере,
    нам достаточно в values указать DepartmentHelper(...).fetch_boss(),
    хотя фактически руководитель на оффере - это прямой руководитель
    подразделения привязанного к офферу, либо ближайшего родительского
    подразделения. Т.е. в рамках самого запроса такое делать неоправданно сложно.

    Пример использования:
    # 'related_field__department' - lookup до подразделения,
    # на основе которого мы будем получать данные
    helper = DepartmentHelper('related_field__department')
    qs.values(
        dep_ancestors_chain=helper.fetch_department_ancestors_chain(),
        boss=helper.fetch_boss(),
    )
    """
    def __init__(self, department_lookup_field):
        self.department_lookup_field = department_lookup_field
        self.departments_map = Department.objects.in_bulk()

    @cached_property
    def department_chief_map(self):
        department_chiefs_qs = (
            DepartmentUser.objects
            .filter(
                role=DEPARTMENT_ROLES.chief,
                is_direct=True,
            )
            .select_related('user')
        )
        return {d.department_id: d.user for d in department_chiefs_qs}

    def fetch_department_ancestor_name(self, level):
        """
        Получает название родительского подразделения на уровне `level`
        """
        def _inner(value):
            try:
                value = value[level]
            except (IndexError, KeyError, TypeError):
                return None
            else:
                department = self.departments_map.get(value)
                return department.name if department else None

        return PythonConverterExpression(
            self.department_lookup_field + '__ancestors',
            convert_function=_inner,
        )

    def fetch_department_ancestors_chain(self):
        """
        Получает цепочку названий родительских подразделений разделенных ` / `
        """
        def _inner(value):
            if value:
                return ' / '.join([
                    self.departments_map[i].name if i in self.departments_map else '-'
                    for i in value
                ])

        return PythonConverterExpression(
            self.department_lookup_field + '__ancestors',
            convert_function=_inner,
        )

    def fetch_boss(self):
        """
        Получает руководителя подразделения.
        Это ближайший прямой руководитель вверх по дереву.
        """
        def _inner(value):
            if value not in self.departments_map:
                return ''
            department_ids = [value] + self.departments_map[value].ancestors[::-1]
            for department_id in department_ids:
                if department_id in self.department_chief_map:
                    return '{x.first_name} {x.last_name} ({x.username})'.format(
                        x=self.department_chief_map[department_id],
                    )

        return PythonConverterExpression(
            self.department_lookup_field,
            convert_function=_inner,
        )


def _encode(s):
    if isinstance(s, datetime.datetime):
        return s.astimezone(local_timezone).strftime('%d.%m.%Y %H:%M:%S')
    elif isinstance(s, datetime.date):
        return s.strftime('%d.%m.%Y')
    return s


def _bulk_encode(strings):
    return [_encode(s) for s in strings]


def _url(id_field, collection):
    return Concat(
        V('{protocol}://{host}/{collection}/'.format(
            protocol=settings.FEMIDA_PROTOCOL,
            host=settings.FEMIDA_HOST,
            collection=collection,
        )),
        id_field,
    )


def _translation(field, translations):
    return Case(
        output_field=CharField(),
        default=field,
        *[When(**{field: k, 'then': V(v)}) for k, v in translations.items()]
    )


def _person(field):
    return Concat(
        '{field}__first_name'.format(field=field), V(' '),
        '{field}__last_name'.format(field=field), V(' ('),
        '{field}__username'.format(field=field), V(')'),
    )


def generate_rejected_offers_csv(queryset):
    """
    Список отказов от оффера в CSV
    """
    result = io.BytesIO()
    wb = xlwt.Workbook(encoding='utf8')
    writer = XLSheetProxy(wb.add_sheet('Отказы от офферов'))
    department_helper = DepartmentHelper('offer__department')

    data = OrderedDict((
        ('URL оффера', _url('offer_id', 'offers')),
        ('Статус оффера', _translation('offer__status', choices.OFFER_STATUSES_TRANSLATIONS)),
        ('Дата выхода', 'offer__join_at'),
        ('Дата отказа', 'modified'),
        ('Рекрутер', _person('offer__creator')),
        ('URL кандидата', _url('offer__candidate_id', 'candidates')),
        ('ФИО кандидата', 'offer__full_name'),
        ('URL вакансии', _url('offer__vacancy_id', 'vacancies')),
        ('JOB-тикет', 'offer__vacancy__startrek_key'),
        ('БП', 'offer__vacancy__budget_position_id'),
        ('Подразделение', 'offer__department__name'),
        ('Подразделение 1го уровня', department_helper.fetch_department_ancestor_name(0)),
        ('Подразделение 2го уровня', department_helper.fetch_department_ancestor_name(1)),
        ('Подразделение 3го уровня', department_helper.fetch_department_ancestor_name(2)),
        ('Подразделение 4го уровня', department_helper.fetch_department_ancestor_name(3)),
        ('Цепочка подразделений', department_helper.fetch_department_ancestors_chain()),
        ('Руководитель', department_helper.fetch_boss()),
        ('Офис', 'offer__office__name_ru'),
        ('Профессия', 'offer__profession__name'),
        ('Зарплатные ожидания', Concat(
            Cast('offer__salary_expectations', output_field=CharField(max_length=16)), V(' '),
            'offer__salary_expectations_currency__code',
        )),
        ('Тип договора', _translation('offer__contract_type', choices.CONTRACT_TYPES_TRANSLATIONS)),
        ('Грейд', 'offer__grade'),
        ('Зарплата', 'offer__salary'),
        ('Валюта', 'offer__payment_currency__code'),
        ('Ставка', 'offer__work_hours_weekly'),
        ('SignUP', 'offer__signup_bonus'),
        ('RSU', 'offer__rsu'),
        ('Стоимость опциона', 'offer__rsu_cost'),
        ('Другие выплаты', 'offer__other_payments'),
        ('Причина отказа', _translation('rejection_reason', REJECTION_REASONS_TRANSLATIONS)),
        ('Конкурирующая компания', 'competing_company'),
        ('Условия конкурирующего оффера', 'competing_offer_conditions'),
        ('Текущая компания', 'offer__current_company'),
        ('Источник', _translation('offer__source', SOURCES_TRANSLATIONS)),
        ('Описание источника', 'offer__source_description'),
        ('Сторона отказа', _translation('rejection_side', REJECTIONS_SIDE_TRANSLATIONS)),
        ('Комментарий', 'comment'),
    ))

    writer.writerow(_bulk_encode(data.keys()))
    for rejection in queryset.values_list(*data.values()):
        writer.writerow(_bulk_encode(rejection))

    wb.save(result)
    return result.getvalue()


def offers_actionlog_gen(offers_data, actions, cache_size=100):
    """
    Генератор, который возвращает данные по офферу,
    но для каждого оффера из actionlog'а добавляется дата
    необходимых действий
    :param offers_data: список значений офферов
    :param actions: действия, которые хотим получить в формате:
      (
        (название действия, порядок),
        ('offer_reject', 1),
        ('offer_approve', -1),
      ),
      где:
      1 -> asc
      -1 -> desc
    :param cache_size: столько офферов мы за раз получаем из mongodb
    """

    def _fetch_actionlog_data(offer_ids):
        data = defaultdict(dict)
        snapshots = (
            Snapshot.objects
            .filter(
                log_record__action_name__in=actions.keys(),
                obj_str='offer',
                obj_id__in=offer_ids,
            )
            .exclude(reason=SNAPSHOT_REASONS.nothing)
            .values(
                'log_record__action_name',
                'log_record__action_time',
                'obj_id',
            )
        )
        for sn in snapshots:
            action_name = sn['log_record__action_name']
            action_time = sn['log_record__action_time']
            action_order = actions.get(action_name)
            if action_order == -1 or (action_order == 1 and action_name not in data[sn['obj_id']]):
                data[sn['obj_id']][action_name] = action_time
        return data

    chunks = (offers_data[i:i + cache_size] for i in range(0, len(offers_data), cache_size))
    for chunk in chunks:
        actionlog_data = _fetch_actionlog_data([o[0] for o in chunk])
        for offer_data in chunk:
            offer_data = list(offer_data)
            for action_name in actions:
                offer_data.append(actionlog_data.get(offer_data[0], {}).get(action_name))
            yield offer_data


def generate_offers_csv(queryset):
    """
    Список офферов в CSV
    """
    result = io.BytesIO()
    wb = xlwt.Workbook(encoding='utf8')
    writer = XLSheetProxy(wb.add_sheet('Офферы'))
    department_helper = DepartmentHelper('department')

    data = OrderedDict((
        ('ID оффера', 'id'),
        ('Кандидат', 'full_name'),
        ('Рекрутер', _person('creator')),
        ('ID вакансии', 'vacancy_id'),
        ('ID в Наниматоре', 'newhire_id'),
        ('Руководитель', department_helper.fetch_boss()),
        ('Подразделение', 'department__name'),
        ('Дата выхода', 'join_at'),
        ('Дата заполнения анкеты кандидатом', 'profile__created'),
    ))

    actionlog_data = OrderedDict((
        # Заголовок, (название action'а, порядок)
        ('Дата первой отправки на согласование', ('offer_approve', 1)),
        ('Дата отправки кандидату', ('offer_send', -1)),
        ('Дата подтверждения рекрутером', ('offer_confirm', -1)),
        ('Дата закрытия оффера', ('offer_close', -1)),
    ))

    offers_values = list(queryset.values_list(*data.values()))
    offers_data = []

    for offer_data in offers_actionlog_gen(offers_values, OrderedDict(actionlog_data.values())):
        offers_data.append(offer_data)

    writer.writerow(_bulk_encode(list(data.keys()) + list(actionlog_data.keys())))
    for offer in offers_data:
        writer.writerow(_bulk_encode(offer))

    wb.save(result)
    return result.getvalue()
