import time
import logging
from collections import OrderedDict

import json

import requests
from django.conf import settings


from intranet.search.abovemeta import errors
from intranet.search.abovemeta.serializers import (
    get_work_phone,
    suggest_serializer_fabric,
    search_serializer_fabric,
)
from intranet.search.abovemeta.steps.base import Step
from intranet.search.abovemeta.utils import CustomJSONEncoder, translate
from intranet.search.core.highlighter import Hiliter
from intranet.search.core.models import Mark
from intranet.search.core.utils.lemmer import generate_query_word_forms


log = logging.getLogger(__name__)


class BaseDressingStep(Step):
    def get_backends(self, state):
        backends = []
        for req_data in state.requests:
            request = req_data['request']
            response = req_data['response']
            backend_params = {
                'attempt': req_data['attempt'],
                'duration': req_data['request_time'],
                'endpoint': request.url,
                'length': len(response.buffer.getvalue()) if response else None,
                'method': request.method,
                'status_code': req_data['code'],
                'started': req_data['started'] - state.started,
                'type': request.type,
                'name': request.name,
            }
            backends.append(backend_params)
        return backends

    def get_page_data(self, search_data):
        if not search_data or not search_data.get('parsed'):
            return {}

        return {
            'page': search_data['search_settings'].get('p', 0),
            'per_page': search_data['parsed'].per_page,
            'pages': search_data['parsed'].pages_count,
            'count': search_data['parsed'].count,
        }

    def get_response(self, state, data=None):
        self.process_response(state, data)
        return json.dumps(state.response, cls=CustomJSONEncoder)

    def swap24(self, state, documents, name=None):
        """ Перестановка документов на второй и четвертой позиции.
        Это стандартный способ валидировать метрики и их чувствительность:
        изменение явно ухудшающее, при этом больно пользователям не становится.
        """
        name = name or state.scope
        if state.feature_enabled(f'{name}_swap24') and len(documents) >= 4:
            documents[1], documents[3] = documents[3], documents[1]
        return documents


class BaseSearchDressingStep(BaseDressingStep):
    """ Базовая стадия подготовки ответа основного поиска
    """
    def get_ordered(self, state, setting):
        if state.scope not in settings.ISEARCH['scopes']:
            return []
        data = state.scope_settings.get(setting)
        return self._order_data(data) if data else []

    def _order_data(self, data):
        return sorted(data.items(), key=lambda k: k[1].get('order', 100))

    def get_scopes(self, state):
        result = []
        for scope, data in self._order_data(state.scopes):
            if data.get('add_to_scopes', True):
                result.append((scope, data))
        return result

    def get_category_data(self, state, category, search_data):
        category_data = {
            'attr': category['attr'],
            'value': category['value']
        }
        try:
            data = search_data.get('group_attrs', {})[category['value']]
            category_data['name'] = data.get('label') or category['value']
        except KeyError:
            log.error('Cannot find group data for category',
                      extra={'category': category, 'state': state})
            category_data['name'] = 'Without group' if state.language == 'en' else 'Без группы'
        return category_data

    def check_search_results(self, state, docs_count, wizards_count):
        if docs_count > 0 or state.errors:
            return

        if not state.text and not state.allow_empty:
            state.set_error(errors.WARNING_EMPTY_QUERY)
        elif wizards_count > 0:
            state.set_error(errors.WARNING_SPECIFY_REQUEST)
        else:
            state.set_error(errors.WARNING_NOTHING_FOUND)


class JSONSearchDressingStep(BaseSearchDressingStep):
    """ JSON выдача основного поиска
    """
    def process_response(self, state, data=None):
        response = OrderedDict()

        response['meta'] = self.get_meta(state)
        response['results'] = self.get_search_results(state)
        response['errata'] = self.get_errata(state)
        response['wizards'] = self.get_wizards(state)
        response['scopes'] = self.get_scopes(state)
        response['sorts'] = self.get_sorts(state)
        response['facets'] = self.get_facets(state)
        response['mark_scores'] = self.get_mark_scores(state)

        self.check_search_results(state, response['meta']['count'], len(response['wizards']))
        response['errors'] = self.get_errors(state)

        state.response = response

    def get_errors(self, state):
        result = []
        error_list = list(state.errors or []) + list(state.warnings or [])
        for error in set(error_list):
            result.append({
                'code': error.code,
                'type': error.type,
                'text': translate(error.code, state) or error.message
            })
        return result

    def get_meta(self, state):
        meta = {
            'ab_info': state.ab_info,
            'request': state.request_uri,
            'request_id': state.request_id,
            'organization_id': state.org_directory_id,
            'is_cloud_organization': state.is_cloud_organization,
            'is_admin_user': state.is_admin_user,
            'is_cloud_user': state.is_cloud_user,
            'language': state.language,
            'auto_translate': state.auto_translate,
            'features': state.features,
            'backends': [],

            'page': 0,
            'pages': 0,
            'per_page': 0,
            'count': 0,
        }
        search_data = state.searches.get('search_results')
        if search_data and search_data.get('parsed'):
            meta.update(self.get_page_data(search_data))

        if state.debug:
            meta['backends'] = self.get_backends(state)
            if search_data and search_data.get('parsed'):
                meta['searcher_properties'] = search_data['parsed'].properties
            else:
                meta['searcher_properties'] = {}

        return meta

    def get_errata(self, state):
        search_data = state.searches.get('search_results')
        if search_data and search_data.get('parsed'):
            highlighter = Hiliter(search_data['qtree'], save_pure=state.hilite_save_pure,
                                  start=state.hilite_start_tag, end=state.hilite_end_tag)
            return search_data['parsed'].get_errata(highlighter)
        return None

    def get_scopes(self, state):
        result = []
        for scope, scope_data in self._order_data(state.scopes):
            if (scope_data.get('show', True) or scope == state.scope) and not self._is_old_scope(state, scope):
                result.append({
                    'name': scope_data['name'][state.language],
                    'value': scope,
                })
        return result

    @staticmethod
    def _is_old_scope(state, scope):
        if not state.feature_enabled('remove_old_scopes', False):
            return False
        return settings.ISEARCH['scopes'][scope].get('old_scope', False)

    def get_sorts(self, state):
        result = []
        if not state.has_search_results:
            return result

        for sort_name, sort_data in self.get_ordered(state, 'sort'):
            result.append({
                'name': sort_data['name'][state.language],
                'value': sort_name
            })
        return result

    def get_facets(self, state):
        result = []
        if not state.search_results or not state.search_results.get('facets'):
            return result

        search_facets = state.search_results['facets']
        for facet_name, facet_settings in self.get_ordered(state, 'facets'):
            if not facet_settings.get('show_facet', True) or not search_facets.get(facet_name):
                continue

            name = facet_settings.get('name', {}).get(state.language)
            facet_data = {
                'name': name or '',
                'value': facet_name,
                'groups': []
            }

            if facet_settings.get('is_user'):
                user_facet = search_facets[facet_name].pop(state.user_identifier, None)
                if user_facet:
                    facet_data['groups'].append({
                        'count': int(user_facet.get('doccount', 0)),
                        'name': '{} ({})'.format(
                            translate('Me', state),
                            user_facet.get('label', user_facet['value']),
                        ),
                        'value': state.user_identifier,
                    })

            facet_values = search_facets[facet_name].values()
            for value in sorted(facet_values, key=lambda v: v['doccount'], reverse=True):
                facet_data['groups'].append({
                    'count': int(value.get('doccount', 0)),
                    'name': value.get('label', value['value']),
                    'value':  value['value'],
                })
            result.append(facet_data)
        return result

    def get_search_results(self, state):
        search_data = state.search_results

        result = {'docs': [], 'groups': []}
        if state.debug:
            result['backend'] = self.get_zone_request_info(state, search_data)

        if search_data and search_data.get('parsed'):
            if search_data['parsed'].has_grouping:
                key = 'groups'
                strip_groups = False

                # Эта группировка задается для показа наиболее релеватного параграфа страницы
                # Показываем результат как плоский список документов
                grouping = search_data['parsed'].raw['groupings'][0]
                if grouping['attr'] == 'base_url' and grouping['docs'] == '1':
                    key = 'docs'
                    strip_groups = True

                search_results = self.get_found_groups(
                    state, search_data, 'search_results', strip_groups=strip_groups
                )
            else:
                key = 'docs'
                search_results = self.get_found_docs(state, search_data, 'search_results')

            result[key] = self.swap24(state, search_results)

        return result

    def get_wizards(self, state):
        wizards = [name for name in state.searches if name != 'search_results']
        result = []
        if not wizards:
            return result

        for wizard_name in wizards:
            wizard = state.searches[wizard_name]
            if wizard.get('parsed') and wizard['parsed'].count > 0:
                result.append(self.get_wizard_zone(state, wizard_name))
        return result

    def get_wizard_zone(self, state, search_name):
        search_data = state.searches.get(search_name)
        if not search_data:
            return {}

        wizard_params = {
            'position': int(search_data['position']),
            'type': search_data['wizard_name'],
            'count': search_data['parsed'].count,
            'docs': self.get_found_docs(state, search_data, search_name)
        }

        if state.debug:
            wizard_params['backend'] = self.get_zone_request_info(state, search_data)

        return wizard_params

    def get_found_groups(self, state, search_data, search_name, strip_groups=False):
        """ Возвращает список найденных групп при запросе с группировками
        """
        result = []
        if not search_data.get('parsed'):
            return result

        for group in search_data['parsed'].get_groups():
            if strip_groups:
                result += self.get_found_docs(state, search_data, search_name, docs=group['docs'])
                continue

            result.append({
                'count': group['count'],
                'category': self.get_category_data(state, group['category'], search_data),
                'docs': self.get_found_docs(state, search_data, search_name, docs=group['docs'])
            })

        return result

    def get_found_docs(self, state, search_data, search_name, docs=None):
        """ Возвращает плоский список найденных документов
        """
        result = []
        if not docs and not search_data.get('parsed'):
            return result

        highlighter = Hiliter(search_data['qtree'], save_pure=state.hilite_save_pure,
                              start=state.hilite_start_tag, end=state.hilite_end_tag)
        docs = docs or search_data['parsed'].get_docs()

        for position, doc in enumerate(docs):
            snippet = doc.get_snippet(highlighter=highlighter)
            serializer = search_serializer_fabric(doc.source, doc.index, state.scope)

            doc_data = {
                'id': doc.id,
                'passages': doc.get_passages(highlighter),
                'doc_revision': doc.revision,
                'doc_source': doc.source,
                'doc_index': doc.index or doc.source,
                'click_urls': doc.get_click_urls(),
                'snippet': serializer(snippet, doc),
            }

            if state.relev_info:
                doc_data['relevance'] = self.get_relev_info(doc)

            result.append(doc_data)
        return result

    def get_zone_request_info(self, state, search_data):
        if not search_data:
            return

        if search_data.get('parsed') and search_data['parsed'].request_query:
            text = search_data['parsed'].request_query
        else:
            text = search_data['qtree'].to_string()

        result = {
            'url': search_data['endpoint'],
            'text': text
        }
        return result

    def get_relev_info(self, doc):
        return {
            'value': round(doc.relevance, 3),
            'factors': doc.factors
        }

    def get_mark_scores(self, state):
        feature_value = state.feature_value('enable_marks', '').strip()
        applicable_scopes = [i.strip() for i in feature_value.split(',') if i.strip()]
        return Mark.SCORES if state.scope in applicable_scopes else []


class BaseSuggestDressingStep(BaseDressingStep):

    def format_people_data(self, state):
        search = state.searches.get('people', {})
        result = []
        if not search.get('parsed'):
            return result

        for doc in search['parsed'].get_docs():
            snippet = doc.get_snippet()

            if snippet['is_dismissed'] and not snippet['is_memorial']:
                dep = 'Бывшие сотрудники'
            else:
                dep = snippet.get('department') or {}
                dep = dep.get('dep_name')

            person_data = {
                'department': dep,
                'phone': get_work_phone(snippet),
                'login': snippet['login'],
                'href': '//staff.yandex-team.ru/%s' % snippet['login'],
                'title': '{} {}'.format(snippet['name']['first'], snippet['name']['last'])
            }
            self.add_click_urls(state, doc, person_data)
            result.append(person_data)
        return result

    def add_click_urls(self, state, doc, data, layer='people'):
        if state.feature_enabled('suggest_v0_click_urls'):
            if state.feature_enabled('suggest_v0_%s_redir_click_urls' % layer):
                data['href'] = doc.get_redir_url()
            else:
                data['click_urls'] = doc.get_click_urls()

    def get_full_info(self, state):
        if state.feature_enabled('full_suggest_info'):
            result = {
                'requests': self.get_backends(state),
                'duration': time.time() - state.started,
            }
            return result
        else:
            return {}

    def is_wiki_request(self, state):
        host = state.referer_hostname
        if not host:
            return False
        wiki_hosts = {
            'wiki.yandex-team.ru',
            'wiki.test.yandex-team.ru',
            'wiki.local.yandex-team.ru',
        }
        return host in wiki_hosts

    def get_meta(self, state):
        meta = {}
        # ISEARCH-5810: meta features нужна только если запрос приходит с wiki
        if self.is_wiki_request(state):
            meta.update({
                'features': state.features,
            })
        if state.feature_enabled('suggest_get_word_forms'):
            try:
                word_forms = generate_query_word_forms(state.qtree)
            except Exception:
                log.exception('Cannot parse query word forms')
                word_forms = []
            meta.update({
                'word_forms': ' '.join(word_forms)
            })
        return meta


class OpenSearchDressingStep(BaseSuggestDressingStep):
    def create_people_os_response(self, state):
        people = self.format_people_data(state)

        titles = []
        hrefs = []

        for staff in people:
            title = '{} ({}) {}, {}'.format(
                staff['title'],
                staff['login'],
                staff['phone'],
                staff['department'],
            )
            titles.append(title)
            hrefs.append(staff.get('href', ''))

        data = [state.text, titles, [], hrefs]
        return data

    def process_response(self, state, data):
        state.response = self.create_people_os_response(state)


class SuggestDressingStep(BaseSuggestDressingStep):
    def create_people_response(self, state):
        return self.swap24(state, self.format_people_data(state), name='suggest_people')

    def create_nav_response(self, state):
        search = state.searches.get('nav', {})
        result = []
        if not search.get('parsed'):
            return result

        for doc in search['parsed'].get_docs():
            data = doc.get_snippet()
            self.add_click_urls(state, doc, data, layer='nav')
            result.append(data)

        return self.swap24(state, result, name='suggest_nav')

    def process_response(self, state, data):
        result = {
            'people': self.create_people_response(state),
        }

        nav = self.create_nav_response(state)
        if nav:
            result['nav'] = nav

        result.update(self.get_full_info(state))
        meta = self.get_meta(state)
        if meta:
            result['meta'] = meta

        state.response = result


class SuggestV1DressingStep(SuggestDressingStep):

    def create_layer_response(self, layer, layer_data, state, highlighter=None):
        result = []

        if not layer_data.get('parsed'):
            return result

        is_lite_serializer_needed = (
            layer == 'people'
            and not state.user_has_full_staff_access
            and state.feature_enabled('enable_robot_restrictions')
        )
        serializer = suggest_serializer_fabric(
            'people_lite' if is_lite_serializer_needed else layer
        )
        for doc in layer_data['parsed'].get_docs():
            data = self.serialize_doc(doc, layer, serializer, state)
            result.append(data)

        if highlighter is not None:
            result = highlighter.hilite(result, exclude=('id', 'url', 'click_urls', 'layer'))

        return self.swap24(state, result, name=f'suggest_{layer}')

    def serialize_doc(self, doc, layer, serializer, state):
        data = serializer(doc.get_snippet(), doc)
        data['layer'] = layer

        if state.feature_enabled('suggest_%s_redir_click_urls' % layer):
            data['url'] = doc.get_redir_url()
            data['click_urls'] = []
        else:
            data['click_urls'] = doc.get_click_urls()

        return data

    def set_error(self, state, data):
        details = [e.message for e in state.errors]

        if len(state.errors) > 0:
            msg, code = state.errors[0].message, state.errors[0].code
        else:
            msg, code = 'Unknown error', errors.ERROR_UNKNOWN

        if state.status_code > requests.codes.INTERNAL_SERVER_ERROR and not settings.DEBUG:
            # Скрываем трейсы 500 не в debug режиме
            details = []

        state.response = {'error_message': msg, 'error_code': code, 'details': details}

    def process_response(self, state, data):
        if state.status_code != requests.codes.OK:
            self.set_error(state, data)
            return

        state.response = self.create_response(state)

    def get_highlighter(self, state, qtree):
        highlighter = None
        if state.feature_enabled('enable_highlight_suggest'):
            highlighter = Hiliter(
                query=qtree,
                save_pure=state.hilite_save_pure,
                start=state.hilite_start_tag,
                end=state.hilite_end_tag
            )
        return highlighter

    def create_response(self, state):
        result = []
        layers = sorted(state.layers.items(), key=lambda k: k[1].get('order', 0))
        for layer, _ in layers:
            layer_data = state.searches.get(layer)
            if not layer_data:
                log.warning('Cannot get suggest for layer %s', layer, extra={'state': state})
                continue

            highlighter = self.get_highlighter(state, layer_data['qtree'])

            result.append({
                'layer': layer,
                'result': self.create_layer_response(layer, layer_data, state, highlighter),
                'pagination': self.get_page_data(layer_data),
            })

        request_info = self.get_full_info(state)
        if request_info:
            result.append({'layer': 'full_suggest_info', 'result': request_info})
        meta = self.get_meta(state)
        if meta:
            result.append({'layer': 'meta', 'result': meta})
        return result


class SuggestV2DressingStep(SuggestV1DressingStep):
    """ Вторая версия саджеста. Отличается от первого словарями вместо массивов словарей
    """
    def serialize_doc(self, doc, layer, serializer, state):
        data = super().serialize_doc(doc, layer, serializer, state)

        # превращаем fields в словарь и добавляем его к документу
        fields = data.pop('fields', [])
        extra_data = {t['type']: t['value'] for t in fields}
        data.update(extra_data)
        return data

    def create_response(self, state):
        result = {}

        for layer in state.layers:
            layer_data = state.searches.get(layer)
            if not layer_data:
                log.warning('Cannot get suggest for layer %s', layer, extra={'state': state})
                continue

            highlighter = self.get_highlighter(state, layer_data['qtree'])

            result[layer] = {
                'result': self.create_layer_response(layer, layer_data, state, highlighter),
                'pagination': self.get_page_data(layer_data),
            }

        request_info = self.get_full_info(state)
        if request_info:
            result['full_suggest_info'] = request_info
        meta = self.get_meta(state)
        if meta:
            result['meta'] = meta
        return result
