import requests
import logging
import traceback

from django.conf import settings
from tornado import web, gen, simple_httpclient

from intranet.search.core.utils import QueryDict
from intranet.search.abovemeta import steps, errors
from intranet.search.abovemeta.errors import ERROR_ACL
from intranet.search.abovemeta.forms import SearchForm, SuggestForm
from intranet.search.abovemeta.mixins import CorsMixin
from intranet.search.abovemeta.request import Request
from intranet.search.abovemeta.requester import Requester
from intranet.search.abovemeta.state import SuggestState, SearchState, IsearchSuggestState

log = logging.getLogger(__name__)


class MainHandler(CorsMixin, web.RequestHandler):
    state_class = None
    form_class = None
    requester_class = Requester

    decider_step_cls = None
    search_step_cls = None

    def write_error(self, status_code, **kwargs):
        if 'exc_info' in kwargs:
            error = ''.join(traceback.format_exception(*kwargs['exc_info']))
        else:
            error = 'Internal Error'

        self.state.status_code = status_code
        self.state.set_error(errors.ERROR_UNKNOWN, error)
        log.error('Abovemeta unknown exception', exc_info=kwargs.get('exc_info'),
                  extra={'state': self.state})
        try:
            self.create_response()
        except Exception:
            log.exception('Error while processing error', extra={'state': self.state})

    def create_response(self):
        self.state.requests = self.requester.requests
        self.set_status(self.state.status_code)

        if self.state.errors and self.state.status_code != requests.codes.OK:
            errors = ' '.join(e.code for e in self.state.errors)
            log.warning('Abovemeta response error. Code: %s. Error code: %s.',
                        self.state.status_code, errors,
                        extra={'state': self.state})

        self.create_spec_response()

    def create_spec_response(self):
        raise NotImplementedError

    def get_auth_steps(self):
        raise NotImplementedError()

    def get_presearch_steps(self):
        raise NotImplementedError()

    def get_postsearch_steps(self):
        pass

    def get_extra_steps(self, step_settings=None):
        """ Возвращает дополнительные шаги для конкретного поиска
        Шаги задаются в настройках scope в списке extra_steps.
        По умолчанию все step будут выполнены параллельно, для последовательного
        выполнения их нужно объединить в словарь по ключу chained.
        Например,
            extra_steps:
                - GroupStep
                - chained:
                    - DirectoryStep
                    - ServicesStep
            DirectoryStep и ServicesStep будут выполнены последовательно друг за другом,
            GroupStep - параллельно им
        """
        steps_settings = step_settings or self.state.scope_settings.get('extra_steps', [])
        if not steps_settings:
            return []

        def _load_step(step):
            if isinstance(step, dict):
                chained = []
                for s in step.get('chained', []):
                    chained.append(_load_step(s))
                return steps.ChainStep(chained)
            else:
                return getattr(steps, step)()

        extra_steps = []
        for step in steps_settings:
            extra_steps.append(_load_step(step))

        return extra_steps

    def create_steps(self):
        search_steps = [steps.GetServiceTicketStep()]
        if (settings.TVM2_SERVICE_HEADER in self.state.req_headers
                or settings.TVM2_USER_HEADER in self.state.req_headers):
            search_steps.append(steps.CheckServiceTicketStep())

        search_steps.extend([
            self.get_auth_steps(),
            self.get_presearch_steps(),
        ])

        if not self.state.text and not self.state.allow_empty:
            return steps.ChainStep(search_steps)

        search_steps.extend([
            self.decider_step_cls(),
            self.search_step_cls(),
        ])
        postsearch_steps = self.get_postsearch_steps()
        if postsearch_steps:
            search_steps.append(postsearch_steps)
        return steps.ChainStep(search_steps)

    def prepare_data(self):
        self.requester = self.requester_class()
        self.form = self.form_class(QueryDict(self.request.query))

    def set_state(self):
        self.state = self.state_class(self.request)
        self.state.user_session_id = self.get_cookie('Session_id')

        if not self.form.validate():
            self.state.set_error(errors.ERROR_VALIDATION, self.form.errors)
            return False

        self.state.set_form_data(self.form.data)

        if not self.state.validate():
            self.state.set_error(errors.REDIRECT_UNKNOWN_SCOPE, 'Scope "%s" does not exist' % self.state.scope)
            return False

        return True

    @gen.coroutine
    def get(self):
        try:
            self.prepare_data()
            if self.set_state():
                search_steps = self.create_steps()
                yield search_steps.execute(self.state, self.requester)

            self.set_cors_headers()
            self.create_response()
        except Exception as e:
            self.set_status(500)
            log.exception(e, extra={'state': getattr(self, 'state', None)})


class BaseSearchHandler(MainHandler):
    state_class = SearchState
    form_class = SearchForm
    dresser_class = steps.JSONSearchDressingStep

    decider_step_cls = steps.DeciderStep
    search_step_cls = steps.FullSearchStep

    def create_spec_response(self):
        dressing_step = self.dresser_class()
        response = dressing_step.get_response(self.state)
        self.add_header('content-type', 'application/json; charset=utf-8')

        self.finish(response)

    def search_data(self, search_name):
        search = self.state.searches[search_name]
        return {'docs_find': search['parsed'].count if search.get('parsed') else 0}

    def get_postsearch_steps(self):
        return steps.ParallelStep([
            steps.FacetsStep(),
            steps.FullGroupAttrs(),
        ])


class IsearchMixin:

    def get_auth_steps(self):
        auth_step = steps.AuthStep()
        if settings.ISEARCH_ENABLE_SCOPE_PERMISSIONS:
            auth_step = steps.ChainStep([auth_step, steps.PermissionsStep()])
        return auth_step

    def get_presearch_steps(self):
        presearch_steps = steps.ParallelStep([
            steps.ExternalWizardStep(),
            steps.FormulasStep(),
            steps.ChainStep([
                steps.ParallelStep([
                    steps.ChainStep([
                        steps.GroupsStep(),
                        steps.FeaturesStep(),
                    ]),
                    steps.ABInfoStep(),
                ]),
                steps.RevisionsStep(),
            ]),
        ])
        presearch_steps.steps.extend(self.get_extra_steps())
        return presearch_steps

    def get_postsearch_steps(self):
        return steps.ParallelStep([
            steps.FacetsStep(),
            steps.FullGroupAttrs(),
            steps.GapStep(),
        ])


class BisearchMixin:
    def get_auth_steps(self):
        return steps.ChainStep([
            steps.BisearchAuthStep(),
            steps.DirectoryStep(),
        ])

    def get_presearch_steps(self):
        presearch_steps = steps.ParallelStep([
            steps.ExternalWizardStep(),
            steps.FormulasStep(),
            steps.RevisionsStep(),
        ])
        presearch_steps.steps.extend(self.get_extra_steps())
        return presearch_steps

    def get_postsearch_steps(self):
        return steps.ParallelStep([
            steps.FacetsStep(),
            steps.AvatarsStep(),
        ])


class IsearchHandler(IsearchMixin, BaseSearchHandler):
    pass


class BisearchHandler(BisearchMixin, BaseSearchHandler):
    pass


class BaseSuggestHandler(MainHandler):
    state_class = SuggestState
    form_class = SuggestForm
    dresser_class = steps.SuggestV1DressingStep
    dresser_classes = {
        0: steps.SuggestDressingStep,
        1: steps.SuggestV1DressingStep,
        2: steps.SuggestV2DressingStep,
    }

    decider_step_cls = steps.SuggestDeciderStep
    search_step_cls = steps.FullSuggestSearchStep

    def is_jsonp(self):
        return self.get_argument('callback', False)

    def is_allowed_jsonp(self):
        """ Проверяем referer в запросе с jsonp
        """
        if not self.cors_conf.get('allow_jsonp', True):
            return False

        if not self.cors_conf.get('check_jsonp_referer'):
            return True

        referer = self.state.referer
        if referer and not self.origin_found_in_white_lists(referer):
            return False

        return True

    def set_state(self):
        version = self.form.data.get('version')
        self.dresser_class = self.dresser_classes.get(version, self.dresser_class)

        result = super().set_state()
        self.state.version = version

        if self.is_jsonp() and not self.is_allowed_jsonp():
            self.state.callback = None
            self.state.set_error(ERROR_ACL)
            return False

        return result

    def get_extra_steps(self, steps_settings=None):
        """ Возвращает дополнительные шаги для конкретного саджеста
        """
        steps_settings = set(steps_settings or [])
        for layer_settings in self.state.layers.values():
            steps_settings.update(layer_settings.get('extra_steps', []))
        return super().get_extra_steps(steps_settings)

    def create_spec_response(self):
        dressing = self.dresser_class()
        response = dressing.get_response(self.state, None)

        callback = self.state.callback
        if callback:
            response = f'/**/ {callback}({response})'
            content_type = 'application/javascript'
        else:
            content_type = 'application/json'

        self.set_header('Content-Type', '%s; charset=utf-8' % content_type)

        self.finish(response)


class IsearchSuggestHandler(IsearchMixin, BaseSuggestHandler):
    state_class = IsearchSuggestState


class BisearchSuggestHandler(BisearchMixin, BaseSuggestHandler):
    pass


class BaseOpenSearchHandler(BaseSuggestHandler):
    def create_spec_response(self):
        response = steps.OpenSearchDressingStep().get_response(self.state, None)
        self.set_header('Content-Type', 'application/javascript; charset=utf-8')
        self.finish(response)


class IsearchOpenSearchHandler(IsearchMixin, BaseOpenSearchHandler):
    pass


class BisearchOpenSearchHandler(BisearchMixin, BaseOpenSearchHandler):
    pass


class PingHandler(web.RequestHandler):
    @web.asynchronous
    @gen.coroutine
    def get(self):
        http_client = simple_httpclient.AsyncHTTPClient()

        endpoint = settings.ISEARCH['abovemeta']['ping']

        request = Request(
            url=endpoint.url(),
            type_='api',
            name='ping',
            request_timeout=settings.ISEARCH['abovemeta']['timeouts']['ping'],
        )

        try:
            response = yield http_client.fetch(request.get_tornado_request())
        except simple_httpclient.HTTPError as e:
            response = e.response
            code = response.code if response else 599
        else:
            code = response.code

        if code == 200:
            self.finish('ok')
        else:
            log.error('API ping returned: %s', response.body if response else '<empty>')

            self.set_status(500)
            self.finish('error')
