import os
import copy
import time
import uuid
import logging
from urllib.parse import urlparse, parse_qsl

from django.conf import settings

from ylog.context import log_context

from intranet.search.core.sources.utils import truncate_chars
from intranet.search.core.query import parse_query, ast
from intranet.search.core.utils.domain import get_top_level_domain
from intranet.search.abovemeta import errors
from intranet.search.abovemeta.utils import translate, get_client_ip, get_request_id

log = logging.getLogger(__name__)


class State:
    DEFAULT_SCOPE = 'search'
    DEFAULT_SORTED = 'rlv'

    hilite_start_tag = settings.HILITE_START_TAG
    hilite_end_tag = settings.HILITE_END_TAG
    hilite_save_pure = False  # сохранять ли чистые неподсвеченные значения сниппетов

    def __init__(self, request):
        self.started = time.time()
        self.request_id = uuid.uuid4().hex
        self.request_uri = ''

        if request:
            self.request_uri = request.uri
            self.req_headers = copy.copy(request.headers)
            self.host = os.environ.get('ISEARCH_HOST', self.req_headers.pop('Host', None))
            self.remote_ip = request.remote_ip
            self.user_ip = get_client_ip(request)
            self.domain = get_top_level_domain(request.full_url())
            self.request_id = get_request_id(request, default=self.request_id)

        self.status_code = 200
        self.response_headers = {}
        self.response = None

        self.formulas = {}
        self.external_wizard_rules = {}
        self.revisions = {}
        self.features = {}

        self.fixed_text = ''
        self.searches = {}
        self.spells = []
        self.nomisspell = False  # не переключать на исправленный запрос в случае опечатки
        self.errors = []
        self.warnings = []
        self.debug = False

        self.facets = {}
        self.zones = {}
        self.attrs = {}

        self.text = ''
        self.qtree = None
        self.allow_empty = False
        self.auth_method = None
        self.user = None
        self.user_uid = None
        self.cloud_uid = None
        self.user_language = None
        self.staff_id = None
        self.groups = []
        self.user_departments = []
        self.user_parent_departments = []
        self.user_services = []
        self.user_base_office = None
        self.user_base_floor = None
        self.user_queues = []
        self.user_departments_access = []
        # индентификатор пользователя (логин или uid) в зависимости от типа приложения
        self.user_identifier = None
        self.is_admin_user = False
        self.is_cloud_user = False
        self.is_cloud_organization = False
        self.org_directory_id = None
        self.requested_org_directory_id = None
        self.user_has_org = False
        self.user_has_full_staff_access = None
        self.user_permissions = set()

        # Используются для пользовательских факторов при поиске/саджесте по людям на Стаффе.
        self.user_frequently_searched_people = []
        self.user_recently_searched_people = []

        self.tvm_auth_user_ticket = None  # tvm-тикет пользователя
        self.tvm_auth_service = None  # сервис, авторизованный с помощью tvm
        self.tvm_service_tickets = {}  # сервисные tvm-тикеты нашего приложения

        self.page = 0
        self.form_language = None
        self.auto_translate = False
        self.scope = self.DEFAULT_SCOPE
        self.backend = 'platform'
        self.scopes = settings.ISEARCH['scopes']
        self.sorted = self.DEFAULT_SORTED
        self.sorted_order = 'desc'
        self.acl_restrictions = None
        self.relev_info = False

        # Информация от резалки uaas об АБ экспериментах
        self.ab_info = {}
        # Конкретные выборки, которые запрашиваем из резалки uaas
        self.test_id = ''

        # Имена людей, обнаруженные в запросе,
        # заполняется, если включен BegemotStep
        self.people_names = []

    @property
    def language(self):
        lang = self.form_language or self.user_language
        if lang not in ('ru', 'en'):
            lang = 'ru' if self.domain.endswith('.ru') else 'en'
        return lang

    def set_text(self, data, max_length=1000):
        if len(data['text']) > max_length:
            self.text = truncate_chars(data['text'], max_length, truncate='')
            self.set_error(errors.WARNING_LONG_QUERY)
        else:
            self.text = data['text']

    def set_form_data(self, data):
        self.set_text(data)

        if self.text:
            self.qtree = parse_query(self.text, 5)
        else:
            self.qtree = ast.Text('')
        self.allow_empty = data.get('allow_empty', False)

        self.form_language = data['language']
        self.scope = data['scope'] or self.DEFAULT_SCOPE

        self.backend = data['backend'] or 'platform'
        self.content = data['content']
        self.debug = data['debug'] or False
        self.relev_info = data['relev_info'] or self.debug
        self.nomisspell = data['nomisspell'] or False
        self.test_id = data['test_id']

        self.sorted = data['sorted'] or self.DEFAULT_SORTED
        self.sorted_order = data['sorted_order']
        self.page = data['p'] or 0

        self.facets = data['facet']
        self.zones = data['zone']
        self.attrs = data['attr']

        self.features = data['features']
        self.forced_wizards = data['wizards']

        self.request_id = data.get('request_id') or self.request_id
        self.requested_org_directory_id = data['org_id']

    def validate(self):
        return True

    @property
    def referer(self):
        return self.req_headers.get('Referer') or ''

    @property
    def referer_hostname(self):
        try:
            return urlparse(self.referer).hostname
        except Exception:
            return ''

    @property
    def scope_settings(self):
        return self.scopes.get(self.scope, {})

    @property
    def scope_facets(self):
        return self.scope_settings.get('facets', {})

    @property
    def user_facets(self):
        return {k for k, v in self.scope_facets.items() if v.get('is_user')}

    @property
    def search_results(self):
        """ Результаты поиска по органической выдаче
        """
        return self.searches.get('search_results')

    @property
    def parsed_search_result(self):
        """ Распаршенный результат поиска по органической выдаче
        """
        return self.search_results.get('parsed') if self.search_results else None

    @property
    def has_search_results(self):
        """ Есть ли поисковая выдача (== нашлось что-то) или нет.
        """
        if not self.parsed_search_result:
            return False
        return self.parsed_search_result.count > 0

    @property
    def saas_uid(self):
        return 'is' + str(self.user_uid)

    @property
    def priority_languages(self):
        languages = [self.language]
        if self.language != settings.DEFAULT_LANGUAGE:
            if self.auto_translate:
                languages.append(f'translated_{self.language}')
            languages.append(settings.DEFAULT_LANGUAGE)
        return languages

    @property
    def user_is_superuser(self):
        return 'is_superuser' in self.user_permissions

    def factors_for_search(self, search):
        return {}

    def feature_enabled(self, feature, default=None):
        disabled_values = [None, '0', 'no', 'off', False, 0]
        return self.feature_value(feature, default) not in disabled_values

    def feature_value(self, feature, default=None):
        return self.features.get(feature, default)

    def update_features(self, features_data, high_priority=False):
        if high_priority:
            self.features.update(features_data)
        else:
            features_data.update(self.features)
            self.features = features_data

    def get_feature_name(self, prefix, index, name, suffix):
        name_parts = []
        if prefix == 'meta':
            name_parts.append(index or prefix)
        elif name == 'suggest':
            name_parts.append(name)
            name_parts.append(prefix)
        else:
            name_parts.append(prefix)
        name_parts.append(suffix)
        return '_'.join(name_parts)

    def get_formula(self, search, index, name='default'):
        revision = self.get_revision(search, index)

        if revision is None:
            service = ''
        else:
            service = revision['service']

        feature = self.get_feature_name(search, index, name, 'formula')

        if self.feature_enabled(feature):
            name = self.feature_value(feature)

        for formula_name in (name, 'default'):
            for service_name in (service, ''):
                formula = self.formulas.get((search, index, service_name, formula_name))
                if formula and formula['compiled']:
                    return formula['compiled'], formula['additional']

        return None, None

    def get_revision(self, search, index):
        return self.revisions.get((search, index, self.backend))

    def get_ew_rules(self, search, index, name='default'):
        """ Для пары (источник, индекс) возвращает пару (правила колдунщика, параметры колдунщика)
        Название правила ищет сначала в фичах, если его там нет - берет default
        """
        feature = self.get_feature_name(search, index, name, 'wizard')

        if self.feature_enabled(feature):
            name = self.feature_value(feature)

        for rule_name in (name, 'default'):
            rule = self.external_wizard_rules.get((search, index, rule_name))
            if rule is not None:
                return rule['rule'], dict(parse_qsl(rule['params']))

        return None, None

    def set_error(self, code, message=None):
        log.warning('Set error for state: %s %s', code, message, extra={'state': self})
        error_type, http_code = errors.ERRORS.get(code)
        message = message or translate(code, self)
        error = errors.AbovemetaError(error_type, code, str(message))
        if error_type == errors.TYPE_WARNING:
            self.warnings.append(error)
        else:
            self.errors.append(error)
            self.status_code = http_code

    def apply_user_permissions(self, permissions):
        """
        Применяем общие пермишшены пользователя
        """
        self.user_permissions = set(permissions)

    def apply_search_permissions(self, permissions):
        """
        Применяем пермишшены пользователя к поиску
        :param permissions: разрешённые scopes/layers
        """

    def get_log_context(self):
        return {
            'request_id': self.request_id,
            'scope': self.scope,
            'org_directory_id': self.org_directory_id or 0,
            'user_ip': self.user_ip,
            'user_uid': self.user_uid,
            'url': self.request_uri,
            'test_buckets': self.ab_info.get('buckets') or '',
            'auth': {
                'mechanism': self.auth_method or '',
                'application': self.tvm_auth_service or '',
            },
            'suggest_request_id': self.req_headers.get('X-Suggest-Session-Id'),
        }


class SearchState(State):
    hilite_save_pure = True
    hilite_start_tag = '@hlword@'
    hilite_end_tag = '@/hlword@'

    def validate(self):
        return self.scope in self.scopes

    def apply_search_permissions(self, permissions):
        self.scopes = {k: v for k, v in self.scopes.items() if k in permissions}
        if self.scope not in self.scopes:
            if self.scope in settings.ISEARCH['scopes']:
                error_code = errors.ERROR_ACL
            else:
                error_code = errors.ERROR_SEARCH_DECIDER
            self.set_error(error_code, 'Scope is not included in available scopes')


class SuggestState(State):
    DEFAULT_SCOPE = 'suggest'

    def __init__(self, request):
        super().__init__(request)
        self.backend = 'platform'
        self.layers = {}
        self.version = 0
        self.callback = None
        self.nomisspell = True

    def set_form_data(self, data):
        self.version = data.get('version', 0)
        self.set_text(data)
        if self.text:
            self.qtree = parse_query(self.text, 5)

        self.form_language = data['language']
        self.features = data['features']
        self.callback = data['callback']
        self.requested_org_directory_id = data['org_id']
        self.allow_empty = data.get('allow_empty', False)
        self.debug = data.get('debug', False)

        with log_context(data=data):
            if self.debug:
                log.info('SuggestState.set_form_data data')

        suggest_settings = settings.ISEARCH['suggest']

        source_settings = suggest_settings['sources'][data['source'] or 'default']

        if self.version == 0:
            cur_layers = data['s'] or data['s[]']
            if 'people' not in cur_layers:
                cur_layers.append('people')
        else:
            cur_layers = data['layers'] or source_settings['layers']

        with log_context(layers=cur_layers):
            if self.debug:
                log.info('SuggestState cur_layers')

        for layer in cur_layers:
            layer_settings = copy.copy(suggest_settings['layers'][layer])
            if self.version > 0:
                layer_settings.update(source_settings.get(layer, {}))
            layer_settings.update(data[layer])

            self.layers[layer] = layer_settings


class IsearchSuggestState(SuggestState):

    def apply_search_permissions(self, permissions):
        self.layers = {k: v for k, v in self.layers.items() if k in permissions}
