# -*- coding: utf-8 -*-
import datetime
import logging
import operator
import re
import smtplib
import socket

from collections import defaultdict, OrderedDict
from django import forms
from django.db import connection, transaction
from django.db.models import Q
from django.conf import settings
from django.core.mail import EmailMessage
from django.utils import timezone
from itertools import groupby

from events.common_app.blackbox_requests import JsonBlackbox
from events.common_app.utils import retry, get_user_ip_address

logger = logging.getLogger(__name__)


class ConditionEvaluator(object):
    set_node_condition_item_false_if_result_data_value_is_false = True

    def __init__(self, condition_nodes, source_data, fields_by_name, result_data=None):
        # todo: test widget
        self.condition_nodes = condition_nodes
        self.source_data = source_data
        if result_data is None:
            result_data = {}
        self.result_data = result_data
        self.fields_by_name = fields_by_name

    def evaluate(self):
        if not self.condition_nodes:
            return None
        else:
            for node_items in self.condition_nodes:
                node_state = None
                for node_condition_item in node_items:
                    node_state = self.apply_node_condition_item_to_data_value(node_state, node_condition_item)
                if node_state:  # как только отработал один из node, сразу можно возвращать True
                    return True
            return False

    def apply_node_condition_item_to_data_value(self, node_state, node_condition_item):
        operator_func = self.get_operator_func(node_condition_item['operator'])
        field_name = node_condition_item['field']
        base_condition_field_name, condition_field_name = self._get_field_name_for_condition_value(
            field_name,
            node_condition_item,
        )
        condition_value = self.evaluate_condition(
            source_data_value=self._get_value(
                data=self.source_data,
                widget_name=field_name,
                field_name=field_name,
                node_condition_item=node_condition_item,
            ),
            result_value=self.result_data.get(node_condition_item['field']),
            exp_value=self._get_value(
                data={condition_field_name: node_condition_item['field_value']},
                widget_name=field_name,
                field_name=base_condition_field_name,
                node_condition_item=node_condition_item,
            ),
            condition=node_condition_item['condition'],
            content_type_attribute=node_condition_item['additional_info']['content_type_attribute'],
        )
        if node_state is None:  # for first comparison
            node_state = condition_value
        else:
            node_state = operator_func(node_state, condition_value)
        return node_state

    def _get_value(self, data, widget_name, field_name, node_condition_item):
        # todo: test me
        field_widget = self.fields_by_name[widget_name].widget
        value = field_widget.value_from_datadict(
            data=data,
            files={},
            name=field_name
        )
        if 'tag_index' in node_condition_item and isinstance(value, list):
            value = value[node_condition_item['tag_index']]
        return value

    def _get_field_name_for_condition_value(self, field_name, node_condition_item):
        # todo: test me
        base_field_name = 'value'
        field_widget = self.fields_by_name[field_name].widget
        if isinstance(field_widget, forms.MultiWidget) and 'tag_index' in node_condition_item:
            field_name = '%s_%s' % (base_field_name, node_condition_item['tag_index'])
        else:
            field_name = base_field_name
        return base_field_name, field_name

    def get_operator_func(self, operator_name):
        return getattr(operator, '%s_' % operator_name)

    def evaluate_condition(self, source_data_value, result_value, exp_value, condition, content_type_attribute):
        if result_value is False and self.set_node_condition_item_false_if_result_data_value_is_false:
            return False

        if not isinstance(source_data_value, list):
            source_data_value = [source_data_value]
        return content_type_attribute.apply(condition, source_data_value, exp_value)


class FormConditionEvaluator(object):
    condition_node_item_data_key_name = None
    condition_node_item_expected_data_value_key_name = None

    def __init__(self, conditions):
        self.conditions = conditions

    def evaluate(self, source_data, field_names, fields_by_name):
        result_data = {}
        for source_data_key in field_names:
            evaluation_result = ConditionEvaluator(
                self.conditions.get(source_data_key),
                source_data=source_data,
                fields_by_name=fields_by_name,
                result_data=result_data,
            ).evaluate()
            if evaluation_result is not None:
                result_data[source_data_key] = evaluation_result
        return result_data


def check_survey_tickets_count_consistency(survey):
    """
    FORMS-493
    Создает или удаляет SurveyTicket при установленном maximum_answers для Survey
    """
    from events.surveyme.models import SurveyTicket

    if survey.maximum_answers_count is not None:
        tickets_total_count = SurveyTicket.objects.filter(survey=survey).count()
        if survey.maximum_answers_count > tickets_total_count:
            tickets_to_create = []
            for i in range(survey.maximum_answers_count - tickets_total_count):
                tickets_to_create.append(SurveyTicket(acquired=False, survey_id=survey.pk))
            SurveyTicket.objects.bulk_create(tickets_to_create)
        elif survey.maximum_answers_count < tickets_total_count:
            to_remove_count = tickets_total_count - survey.maximum_answers_count
            free_tickets_count = SurveyTicket.objects.filter(survey=survey, acquired=False).count()
            if to_remove_count >= free_tickets_count:
                SurveyTicket.objects.filter(survey=survey, acquired=False).delete()
            else:
                ids = (
                    SurveyTicket.objects
                                .filter(survey=survey, acquired=False)[:to_remove_count]
                                .values_list('id', flat=True)
                )
                SurveyTicket.objects.filter(pk__in=list(ids)).delete()
    else:
        SurveyTicket.objects.filter(survey=survey, acquired=False).delete()


def get_condition_nodes(obj, questions_dict, condition_queryset,
                        field_name=None, field=None, with_additional_info=False):
    conditions = []
    group_id = None
    if field and field_name:
        group_id = field.group_id

    for node in condition_queryset:
        items = []
        for item in node.items.all():
            question = questions_dict.get(item.survey_question_id)
            if not question:  # на случай удаленного вопроса
                continue
            group_counter = None
            if group_id and group_id == question.group_id:
                group_counter = field_name.split('__')[-1]
            elif not group_id and question.group_id:
                group_counter = '0'
            form_field_name = question.get_form_field_name(group_counter)
            item_data = {
                'operator': item.operator,
                'condition': item.condition,
                'field': form_field_name,
                'field_value': str(item.value),
            }
            if with_additional_info:
                item_data['additional_info'] = {
                    'content_type_attribute': item.content_type_attribute,
                }

            if question.answer_type.slug == 'answer_date' and question.param_date_field_type == 'daterange':
                if item.content_type_attribute.attr == 'answer_date.date_start':  # todo: убрать хак :(
                    item_data['tag_index'] = 0
                elif item.content_type_attribute.attr == 'answer_date.date_end':
                    item_data['tag_index'] = 1

            items.append(item_data)
        if items:
            conditions.append(items)
    return conditions


def sync_questions(questions, only_changed=True):
    page_key = lambda question: question.page
    for new_page, (_, page_group) in enumerate(groupby(questions, key=page_key), start=1):
        # сгруппируем вопросы в серии по полю group_id,
        # потом правильно пронумеруем их в пределах каждой серии
        question_groups = defaultdict(list)
        for question in page_group:
            question_groups[question.group_id].append(question)

        for group in question_groups.values():
            for new_position, question in enumerate(group, start=1):
                changed = new_page != question.page or new_position != question.position
                question.page = new_page
                question.position = new_position
                if changed or not only_changed:
                    yield question


@retry(exceptions=(socket.error, smtplib.SMTPServerDisconnected, smtplib.SMTPDataError), attempts=30, delay=1)
def send_email_with_retry(subject, body, to_emails, from_email=settings.SERVER_EMAIL, subtype='html'):
    if to_emails:
        message = EmailMessage(
            subject=subject,
            body=body,
            from_email=from_email,
            to=to_emails
        )
        message.content_subtype = subtype
        message.send()
    else:
        logger.warning('Empty recipient list')


def check_email_title(email_title):
    return not re.search(r'[@#%]', email_title)


def check_if_internal_host(hostname):
    re_hostname = re.compile(r'\b(yandex\.net|yandex-team\.ru|test\.yandex\.ru|forms-\w+-api\.yandex\.ru)$', re.I)
    return re_hostname.search(hostname) is not None


def order_questions(questions):
    """ Упорядочивает вопросы с учетом позиции внутри серии
        исходит из предположения, что переданный массив вопросов
        заранее не отсортирован
    """
    ordered_questions = OrderedDict()
    grouped_questions = []
    sort_key = lambda question: (question.page, question.position)

    # отделим вопросы верхнего уровня от вопросов членов групп
    for q in sorted(questions, key=sort_key):
        if q.group_id is None:
            ordered_questions[q.pk] = [q]
        else:
            grouped_questions.append(q)

    # добавим вопросы члены групп к их парентам
    for q in grouped_questions:
        if q.group_id in ordered_questions:
            ordered_questions[q.group_id].append(q)

    for qs in ordered_questions.values():
        for q in qs:
            yield q


def get_survey(survey_id, with_deleted=False):
    from events.surveyme.models import Survey
    queryset = Survey.objects
    if with_deleted:
        queryset = Survey.with_deleted_objects
    try:
        return queryset.get(pk=survey_id)
    except Survey.DoesNotExist:
        pass


class SurveyCommand:
    start_epoch = datetime.date(2019, 4, 1)

    def __init__(self, survey, days=30, max_count=1000, max_created=90):
        self.survey = survey
        self.days = days
        self.max_count = max_count
        self.max_created = max_created  # in days
        self.started = max(self.survey.date_created.date(), self.start_epoch)
        self.finished = datetime.date.today() + datetime.timedelta(days=1)

    def execute(self):
        from events.common_app.utils import calendar
        if (
            self.survey.answercount.count > self.max_count
            and self.started < self.finished - datetime.timedelta(days=self.max_created)
        ):
            for started, finished in calendar(self.started, self.finished, self.days):
                self.doit(started, finished)
        else:
            self.doit(self.started, self.finished)

    def doit(self, started, finished):
        raise NotImplementedError


class PrepareAnswersToExportCommand(SurveyCommand):
    def doit(self, started, finished):
        sql = '''
            insert into surveyme_answerexportytstatus(answer_id, exported)
            select t.id, false
            from surveyme_profilesurveyanswer t
            where t.survey_id = %s
            and t.date_created >= %s and t.date_created < %s
            on conflict(answer_id) do update set exported = false
        '''
        with connection.cursor() as c:
            args = (self.survey.pk, started, finished)
            c.execute(sql, args)


class RemoveAnswersFromExportCommand(SurveyCommand):
    def doit(self, started, finished):
        sql = '''
            delete from surveyme_answerexportytstatus
            where answer_id in (
                select t.id
                from surveyme_profilesurveyanswer t
                where t.survey_id = %s
                and t.date_created >= %s and t.date_created < %s
            )
        '''
        with connection.cursor() as c:
            args = (self.survey.pk, started, finished)
            c.execute(sql, args)


class RemoveTableFromYtCommand:
    def __init__(self, survey):
        self.survey = survey

    def execute(self):
        from events.common_app.yt import utils
        client = utils.get_client()
        path = utils.get_yt_path(self.survey.pk)
        utils.delete_path(client, path)


class SurveyAutoPublicationCommand:
    max_surveys = 25

    def __init__(self, max_surveys=None):
        self.max_surveys = max_surveys or self.max_surveys
        self.blocked_uids = set()
        self.non_blocked_uids = set()

    def _update_blocked_uids(self, uids):
        if not settings.IS_BUSINESS_SITE:
            self.non_blocked_uids |= uids
        else:
            kwargs = settings.EXTERNAL_SITE_BLACKBOX_KWARGS
            bb = JsonBlackbox(**kwargs)
            params = {
                'userip': get_user_ip_address(),
                'uid': ','.join(uids),
            }
            response = bb.userinfo(**params)
            for user in response.get('users') or []:
                if user['karma']['value'] >= 85:
                    self.blocked_uids.add(user['id'])
                else:
                    self.non_blocked_uids.add(user['id'])

    def execute(self):
        from events.history.models import HistoryRawEntry
        from events.history.utils import create_publication_history_entry
        from events.surveyme.models import Survey
        history_entries = []
        with transaction.atomic():
            now = timezone.now()
            qs = (
                Survey.objects.filter(auto_control_publication_status=True)
                .filter(
                    Q(datetime_auto_open__lte=now, is_published_external=False)
                    | Q(datetime_auto_close__lte=now, is_published_external=True)
                )
                .select_for_update(skip_locked=True, of=('self', ))
                .values_list(
                    'pk', 'is_published_external', 'user__uid',
                    'datetime_auto_open', 'datetime_auto_close',
                    named=True,
                )
            )
            surveys = list(qs[:self.max_surveys])
            if not surveys:
                return 0

            uids = {survey.user__uid for survey in surveys if survey.user__uid}
            uids -= self.non_blocked_uids
            uids -= self.blocked_uids
            if uids:
                self._update_blocked_uids(uids)

            for survey in surveys:
                params = {}
                if survey.datetime_auto_close and survey.datetime_auto_close <= now:
                    params['auto_control_publication_status'] = False
                    if survey.is_published_external:
                        params['is_published_external'] = False
                elif survey.datetime_auto_open and survey.datetime_auto_open <= now:
                    if not survey.datetime_auto_close:
                        params['auto_control_publication_status'] = False
                    if not survey.is_published_external:
                        if survey.user__uid not in self.blocked_uids:
                            params['is_published_external'] = True
                        else:
                            params['auto_control_publication_status'] = False
                            logger.warn('User %s denied by karma', survey.user__uid)

                if 'is_published_external' in params:
                    publication_status = params['is_published_external']
                    history_entries.append(create_publication_history_entry(survey.pk, publication_status))
                if params:
                    Survey.objects.filter(pk=survey.pk).update(**params)

            if history_entries:
                HistoryRawEntry.objects.bulk_create(history_entries)

            return len(surveys)
