import json
import math
from html import unescape
from logging import getLogger

from django.conf import settings
from django.utils.functional import cached_property

from intranet.search.core.snippets import parse_snippet
from intranet.search.core.sources.utils import get_text_content
from intranet.search.core.utils import deserialize_snippet
from intranet.search.abovemeta.utils import string_to_bool

log = getLogger(__name__)


class Doc:
    id = ''
    url = ''
    source = ''
    index = ''
    revision = ''
    relevance = 0
    snippet = None
    factors = None

    def __init__(self, raw, languages=None, context=None):
        self.raw = raw
        self.languages = languages or []
        self.context = context

    def get_snippet(self, highlighter=None):
        snippet = self.snippet
        if highlighter:
            snippet = highlighter.hilite(snippet)
        return snippet

    def get_passages(self, highlighter=None):
        passages = self.raw.get('passages', [])
        if not highlighter:
            return passages

        return [highlighter.hilite(passage) for passage in passages]

    def get_click_urls(self):
        return []

    def get_redir_url(self):
        return self.url


class SaasDoc(Doc):
    """
    Один документ SaaS
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.id = self.raw['docId']
        self.url = self.raw['url']
        self.title = self.raw['title'][0]['text'] if self.raw.get('title') else ''
        self.properties = self.raw['properties']

        self.source = self.properties.get('doc_source', '')
        self.index = self.properties.get('doc_index', '')
        self.revision = self.properties.get('doc_revision', '')

    def get_passages(self, highlighter=None):
        # Не возвращаем пассажи для переведённых документов, потому что не можем
        # исключить из них непереведённый текст
        if self.raw_snippet.get('is_translated'):
            return []

        result = []
        for passage_parts in self.raw.get('passages', []):
            passage = ''
            for part in passage_parts:
                text = part['text']
                if part.get('priority') and highlighter:
                    text = highlighter.hilite_whole_string(text)
                passage += text
            result.append(passage)
        return result

    @cached_property
    def raw_snippet(self):
        snippet_data = None

        for language in self.languages:
            snippet_key = f'doc_snippet_{language}'
            if snippet_key in self.properties:
                snippet_data = self.properties[snippet_key]
                break
        else:
            log.error(
                'Document %s does not have snippet_data for languages %s', self.url, self.languages,
                extra={'context': {'source': self.source, 'index': self.index}}
            )

        # в некоторых старых документах в сниппетах - массив
        snippet_data = snippet_data[0] if isinstance(snippet_data, list) else snippet_data
        return deserialize_snippet(snippet_data)

    def get_snippet(self, highlighter=None):
        snippet = self.raw_snippet

        try:
            parsed_snippet = parse_snippet(self.properties['doc_source'], snippet)
            fields = parsed_snippet._fields_to_highlight
            snippet = parsed_snippet.to_primitive(context=self.context)
        except ValueError:
            fields = None

        if highlighter:
            snippet = highlighter.hilite(snippet, fields=fields)
        return snippet

    def get_saas_click_url(self, type='click'):
        """ Возвращаем ссылку на счетчик просмотров от SaaS
        """
        saas_node = self.properties.get('clickUrl')
        if saas_node is None:
            log.warning('ClickUrl is missed in SaaS response. '
                        'doc_url: %s', self.properties['doc_url'])
            return
        return f'//clck.yandex.ru/{type}/dtype={saas_node}'

    def get_click_urls(self):
        saas_click_url = self.get_saas_click_url()
        return [saas_click_url] if saas_click_url else []

    def get_redir_url(self):
        redir_url = self.get_saas_click_url(type='redir')
        return redir_url if redir_url else self.url

    @cached_property
    def factors(self):
        json_factors = self.properties.get('_JsonFactors')
        try:
            json_factors = json_factors[0] if isinstance(json_factors, list) else json_factors
            factors = {}
            # Приводим факторы к обычному словарю. Из СааСа они возвращаются
            # массивом словарей вида: [{'factor2': 'value'}, {'factor2': 'value'}]
            for factor_dict in json.loads(json_factors):
                for name, value in factor_dict.items():
                    factors[name] = value
        except Exception:
            log.warning('Cannot parse relev factors: %s', json_factors)
            factors = {}
        return factors


class MLDoc(Doc):
    """
    Один документ поиска по рассылкам
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.snippet = self.raw['snippet']

        self.id = self.snippet['id']
        self.url = settings.ISEARCH['urls']['ml'].format(**self.snippet)
        self.title = self.snippet['title']
        self.source = 'ml'
        self.snippet['description'] = self.snippet['text']


class SearchResponseBase:
    """
    Базовый класс ответа от поискового движка
    """
    count = 0
    per_page = 0
    page = 0
    pages_count = 0
    facets = {}
    properties = {}

    has_grouping = False
    request_query = None

    def __init__(self, raw_response, languages=None, services=None, context=None):
        self.raw = raw_response
        self.languages = languages or []

        # список сервисов SaaS, в которые делался запрос
        self.services = list(services) if services else []
        self.context = context

    def get_docs(self):
        raise NotImplementedError

    def get_errata(self, highlighter=None):
        return None


class SaaSSearchResponse(SearchResponseBase):
    """
    JSON ответа от SaaS поискового бэкенда
    Документация: https://doc.yandex-team.ru/Search/saas/saas-overview/concepts/searching.html
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._groups = None

    @property
    def request_query(self):
        """ Возвращает переколдованный поисковый запрос
        """
        return self.raw.get('request_query', '')

    @property
    def page(self):
        return int(self.raw.get('page', 0))

    @property
    def per_page(self):
        return int(self._groupings.get('groups-on-page', 10))

    @cached_property
    def pages_count(self):
        return int(math.ceil(float(self.count) / float(self.per_page)))

    @property
    def count(self):
        data = self._results if self.has_grouping else self.raw.get('response', {})
        return int(data.get('found', {}).get('all', 0))

    @property
    def properties(self):
        return self.raw.get('response', {}).get('searcher_properties', {})

    @cached_property
    def revisions(self):
        """ Возвращает множество ревизий, в которых найдены документы
        """
        return {doc.revision for doc in self.get_docs()}

    @cached_property
    def facets(self):
        """ Список фасетов со значениями
        """
        facets = {}
        facet_prefix = 'facet_s_'
        for key, value in self.properties.items():
            if key.startswith(facet_prefix) and value:
                facet_name = key[len(facet_prefix):]
                values = (v.split(':') for v in value.split(';'))
                facets[facet_name] = {v: {'value': v, 'doccount': int(k)} for v, k in values}
        return facets

    def get_errata(self, highlighter=None):
        """ Возвращает исправления опечаток в виде словаря:
        {'applied': True/False,
         'original': string, 'fixed': string, 'original_pure': string, 'fixed_pure': string}
        Где applied означает, был ли уже произведен поиск по замене или нет
            original/fixed - оригинальный запрос и исправление с подстветкой исправления
            original_pure/fixed_pure - чистый текст оригинального запроса и исправления
        """
        # Если был запрос сразу к нескольким сервисам, то исправления опечаток придут
        # в соответствующих сервису полях. Мы берем данные из поля для одного сервиса, вики или
        # другого, т.к. у нас одинаковые настройки для них
        suffix = ''
        if not self.properties.get('MisspellFixed') and len(self.services) > 1:
            # в первую очередь выбираем сервис вики, если его нет - то любой
            services = sorted(self.services, key=lambda a: int(not a.endswith('wiki')))
            suffix = f'_{services[0]}'

        def key(k):
            return f'{k}{suffix}'

        if not self.properties.get(key('MisspellFixed')) or not self.count:
            return None

        original = self.properties.get(key('MisspellSrcText')) or ''
        fixed = self.properties.get(key('MisspellFixed')) or ''
        original_hl = highlighter.rehilite(original, 'fix') if highlighter else original
        fixed_hl = highlighter.rehilite(fixed, 'fix') if highlighter else fixed

        if highlighter.start not in original_hl and highlighter.start not in fixed_hl:
            original_hl = highlighter.hilite_whole_string(original_hl)
            fixed_hl = highlighter.hilite_whole_string(fixed_hl)

        errata = {
            'applied': string_to_bool(self.properties.get(key('MisspellApplied'))),
            'original': unescape(original_hl),
            'fixed': unescape(fixed_hl),
            'original_pure': unescape(get_text_content(original)),
            'fixed_pure': unescape(get_text_content(fixed)),
        }
        return errata

    @property
    def has_grouping(self):
        return bool(self.group_by)

    @property
    def group_by(self):
        return self._results.get('attr')

    def get_groups(self):
        """ Список всех найденных групп с документами внутри
        """
        if not self._groups:
            data = []
            for group in self._results.get('groups', []):
                data.append({
                    'count': int(group['found']['all']),
                    'docs': [SaasDoc(doc, self.languages, self.context) for doc in group['documents']],
                    'category': {'attr': self.group_by, 'value': group['group_attr']},
                })
            self._groups = data
        return self._groups

    def get_docs(self):
        """ Список всех документов без группировок
        """
        result = []
        for group in self.get_groups():
            result.extend(group['docs'])
        return result

    @property
    def _results(self):
        return self.raw['response']['results'][0] if self.raw.get('response', {}).get('results') else {}

    @property
    def _groupings(self):
        return self.raw['groupings'][0] if self.raw.get('groupings') else {}


class MLSearchResponse(SearchResponseBase):
    """
    Ответ поискового движка рассылок
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        meta = self.raw['meta']
        self.per_page = meta.get('per_page', 0)
        self.page = meta.get('page', 0)
        self.count = meta.get('doc_count', 0)
        self.pages_count = meta.get('pages_count', 0)

    def get_docs(self):
        return [MLDoc(raw_doc) for raw_doc in self.raw['results']]
