import logging
import urllib.parse
from copy import deepcopy

from django.conf import settings
from tvm2.exceptions import NotAllTicketsException

from idm.core import exceptions
from idm.core.models import System
from idm.core.plugins.errors import PluginError, PluginFatalError
from idm.utils import http, curl
from idm.utils.cleansing import hide_secret_params, hide_secret_params_in_url


log = logging.getLogger(__name__)

_DEFAULT_TIMEOUT = 10


def _move_params_into_data_if_post(url, data):
    """ Если мы делаем POST запрос, а некоторые параметры были
    указаны в URL (в админке), то эти параметры следует поместить в
    тело POST запроса и убрать из query.
    """
    if isinstance(data, dict):
        parsed = list(urllib.parse.urlsplit(url))
        query = parsed[3]
        parsed[3] = ''

        data.update(
            (key, value[0])
            for key, value in list(urllib.parse.parse_qs(query).items())
        )

        url = urllib.parse.urlunsplit(parsed)

    return url, data


class RequestMixin:
    system: System

    def _init(self):
        if not self.system.base_url:
            raise exceptions.URLNotDefined()

        base_url = (self.system.base_url or '').strip()

        if base_url and '%s' not in base_url:
            # базовый урл может содержать %s плейсхолдер, для подстановки метода
            # если плейсхолдера нет, то название ручки добавляется в конец урла
            # и завершается слешом
            base_url += '%s/'
        self._base_url = base_url
        split_url = urllib.parse.urlsplit(self._base_url)
        self._host = urllib.parse.urlunsplit((split_url.scheme, split_url.netloc, '', '', ''))

    @property
    def base_url(self):
        if not hasattr(self, '_base_url'):
            self._init()
        return self._base_url

    @property
    def host(self):
        if not hasattr(self, '_host'):
            self._init()
        return self._host

    def _make_request(self, url, method, data, timeout, headers=None):
        """Выполняет запрос по url"""

        http_lib = http if self.system.use_requests else curl
        url, data = _move_params_into_data_if_post(url, data)

        kwargs = {
            'use_client_certificate': self.system.auth_factor == 'cert',
            'check_server_certificate': self.system.check_certificate,
            'tvm_id': self.system.get_tvm_id(url=url) if self.system.auth_factor == 'tvm' else None,
            'timeout': timeout,
            'headers': headers,
        }

        try:
            if method.upper() == 'POST':
                response = http_lib.post(
                    url,
                    data=data,
                    **kwargs
                )
            else:
                response = http_lib.get(
                    url,
                    **kwargs
                )
        except http.RequestException:
            data = hide_secret_params(deepcopy(data))
            url = hide_secret_params_in_url(url)
            log.warning('RequestException raised during fetching "%s", data="%s"', url, data, exc_info=True)
            raise PluginError(499, 'HTTP exception while fetching "%s", data="%s"' % (url, data))
        except NotAllTicketsException:
            log.warning('Couldn\'t get TVM ticket')
            raise PluginError(499, 'Couldn\'t get TVM ticket for service with tvm_id %s' % self.system.get_tvm_id(url=url))
        else:
            return response

    def _check_response_status_code(self, response):
        if not 200 <= response.status_code < 300:
            raise PluginError(response.status_code, 'Wrong HTTP status: %d' % response.status_code,
                              response.content[:1000])

    def _send_data(self, url, method, data=None, timeout=_DEFAULT_TIMEOUT, headers=None,
                   parse_response=None):
        """Выполняет запрос по url, при наличии ответа парсит json ответа и возвращает его.
        При отсутствии ответа пробрасывает исключение"""

        if parse_response is None:
            def _parse_json_response(response):
                return response.json()

            parse_response = _parse_json_response

        response = self._make_request(url, method, data, timeout, headers=headers)
        self._check_response_status_code(response)

        data = hide_secret_params(deepcopy(data))
        url = hide_secret_params_in_url(url)

        log.info('Fetched from %s: url=%s, data=%s: %s', self.system.slug, url, data, response.status_code)

        try:
            result = parse_response(response)
        except Exception:
            log.warning('Cannot parse response: "%s" for role %s', response.content[:1000], data, exc_info=True)
            raise PluginError(600, 'Could not response', data, response.content[:1000])

        self.check_errors(result, url, data)
        self.log_response(result, url)
        return result
    
    def log_response(self, result, url):
        context = result.get('context')
        if context:
            context_for_log = {
                key: '*' if value else 'no value'
                for key, value in context.items()
            }
        else:
            context_for_log = 'no context'
        log.info('Parsed context from %s: url=%s, context=%s: ', self.system.slug, url, context_for_log)

    def check_errors(self, result, url, data):
        """Проверяет, нет ли в результирующем дикте сообщений об ошибках.
        Если ошибки есть, бросает исключения. Предупреждения просто логируются.
        """
        code = result.get('code')
        if code is None:
            log.warning('during fetching url="%s": incorrect answer from system', url)
            raise PluginError(
                settings.DEFAULT_SYSTEM_ERROR_CODE,
                'Некорректный ответ от системы',
                data,
                repr(result)[:1000]
            )

        if code in (0, '0'):
            return  # всё ок, дальше не смотрим никуда

        if code in (207, '207'):
            return  # корректный код при частичном приеме батча

        if 'fatal' in result:
            code, message = result['code'], result['fatal']
            log.warning('during fetching url="%s", type=fatal data=%r, code=%s, message="%s"', url, data, code, message)

            raise PluginFatalError(code, message, data)

        if 'error' in result:
            code, message = result['code'], result['error']
            log.warning('during fetching url="%s", type=error, data=%r, code=%s, message="%s"',
                        url, data, code, message)

            raise PluginError(code, message, data, repr(result)[:1000])

        if 'warning' in result:
            code, message = result['code'], result['warning']
            log.warning('during fetching url="%s", type=warning, '
                        'data=%r, code=%s, message="%s"', url, data, code, message)
            return  # код не 0, но всё ограничилось warning-ом. Считаем, что всё ок

        # код не 0, но ключевых слов нет. Считаем ошибкой.
        log.warning('during fetching url="%s": data=%r, code=%s, no message provided', url, data, code)
        raise PluginError(code, 'Некоректный ответ от системы', data, repr(result)[:1000])
