# -*- coding: utf-8 -*-
import datetime
import hashlib
import hmac
import inspect
import lazy_object_proxy
import logging
import os
import re
import requests
import socket
import sys
import time
import uuid

from collections import defaultdict, deque
from contextlib import contextmanager
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models.signals import pre_save, post_save
from django.http import QueryDict
from django.utils.deconstruct import deconstructible
from django.utils.encoding import force_str
from django.utils.translation import get_language
from django.utils.translation.trans_real import parse_accept_lang_header
from functools import wraps
from ids.exceptions import BackendError
from io import StringIO, BytesIO
from itertools import chain, islice
from json import loads as json_loads
from markdown import markdown
from more_itertools import pairwise
from pytils.translit import slugify
from requests.exceptions import RequestException
from time import monotonic
from urllib.parse import urlencode, urlparse
from xhtml2pdf import pisa


logger = logging.getLogger(__name__)


def get_first_object(f):
    """Перехватывает Iterable, который возвращает декорируемый метод,
    и пытается вернуть первый найденный объект из набора.

    Если набор пуст, то возбуждает исключение DoesNotExist.
    Декорировать только методы менеджера.

    """
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        found_set = f(self, *args, **kwargs)
        if found_set:
            return found_set[0]
        else:
            raise self.model.DoesNotExist()

    return wrapper


def add_model_prefix(prefix, to):
    """Добавляет префикс к значению в стиле джанговских моделей

    @type prefix: str
    @type to: str

    @rtype: str

    """
    if prefix:
        return '%s__%s' % (prefix, to)
    else:
        return to


def render_markdown(markdown_text, with_new_line_break=False):
    # todo: test me
    from django.utils.encoding import smart_text

    if markdown_text is None:
        markdown_text = ''

    extensions = []
    if with_new_line_break:
        extensions.append('markdown.extensions.nl2br')
    return markdown(smart_text(markdown_text), extensions=extensions)


def render_to_pdf(html_text):
    # todo: test me forms-666

    fonts = """
        /* Normal */
        @font-face {
           font-family: DejaVuSans;
           src: url('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf');
        }

        /* Bold */
        @font-face {
           font-family: DejaVuSansBold;
           src: url(/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf);
           font-weight: bold;
        }

        /* Italic */
        @font-face {
           font-family: DejaVuSansOblique;
           src: url(/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf);
           font-style: italic;
        }

        /* Bold and italic */
        @font-face {
           font-family: DejaVuSansBoldOblique;
           src: url(/usr/share/fonts/truetype/dejavu/DejaVuSans-BoldOblique.ttf);
           font-weight: bold;
           font-style: italic;
        }

        /* Monospaced */
        @font-face {
           font-family: DejaVuSansMono;
           src: url(/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf);
        }

        /* Monospaced bold */
        @font-face {
           font-family: DejaVuSansMonoBold;
           src: url(/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf);
           font-weight: bold;
        }

        pre, code {
            font-family: DejaVuSansMono;
        }

        * {
            font-family: DejaVuSans;
        }"""

    html_template = "<html>" \
                    "<meta http-equiv='content-type' content='text/html; charset=utf-8'>" \
                    "<style>%(fonts)s</style>" \
                    "<body><div>%(text)s</div></body>" \
                    "</html>"
    html_template = html_template % {'fonts': fonts, 'text': html_text}

    pdf = BytesIO()
    pisa.CreatePDF(html_template.encode('utf-8'), pdf, encoding='utf-8')
    pdf.seek(0)
    return pdf


class ParamsSetter(object):
    empty_values = ['', None]

    def __init__(self, target, data):
        self.target = target
        self.data = data

    def set_params(self, is_only_for_empty_params=True, is_use_empty_values=False):
        """Устанавливает атрибуты self.target, вытаскивая их значения из self.data

        @param is_only_for_empty_params: вытаскивать в self.target из self.data только те атрибуты, которые не заполнены в self.target.
                                         Например, если у self.target не заполнен атрибут name, то
                                         оно заполнится значением из data.

        @param is_use_empty_values: использовать для self.target дынне из self.data, если они пустые.

        """
        for key, value in self.data.items():
            curr_value = getattr(self.target, key, None)
            if hasattr(self.target, key):
                if (is_only_for_empty_params and curr_value in self.get_empty_values()) or not is_only_for_empty_params:
                    if (value in self.get_empty_values() and is_use_empty_values) or value not in self.get_empty_values():
                        setattr(self.target, key, value)

    def get_empty_values(self):
        return self.empty_values


def clean_escaped_quotes(text):
    pattern = r'&(amp;)*quot;'
    return re.sub(pattern, '"', text)


def use_state(state_name):
    from django_replicated.utils import routers
    from functools import wraps
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            routers.use_state(state_name)
            try:
                response = func(*args, **kwargs)
            finally:
                routers.revert()
            return response
        return wrapper
    return decorator


class YLockCommandMixin(object):
    LOCK_TIMEOUT = 300  # устанавливаем лок на 5 мин, те если монитор не освободили за
    # это время, считается (такова реализация библиотеки ylock.backend.mongodb)
    # что его можно перехватить отсюда есть следствие:
    # если команда работает неожиданно долго (дольше чем timeout) может стартовать
    # вторая команда, она установит новый таймаут, и если в это время первая закончит работу
    # она удалит данные о локе из бд и монитор будет считаться свободным %(

    def handle(self, *args, **kwargs):
        import ylock
        if settings.IS_TEST:
            return super(YLockCommandMixin, self).handle(*args, **kwargs)
        else:
            manager = ylock.create_manager(**settings.YLOCK)
            with manager.lock(self.lock_name, block=False, timeout=self.LOCK_TIMEOUT,
                              block_timeout=5, common_lock_name=True, ) as acquired:
                if acquired:
                    return super(YLockCommandMixin, self).handle(*args, **kwargs)


@contextmanager
def ignore(*exceptions):
    # todo: test me
    try:
        yield
    except exceptions:
        pass


def get_progress(count_all, count_done):
    try:
        percents = int((count_done / float(count_all)) * 100)
    except ZeroDivisionError:
        percents = 0
    return percents


def get_plain_results_from_group_task(task):
    # todo: test me
    return chain.from_iterable(
        (item[1] for i, item in enumerate(task.collect()) if i>1)
    )


def get_clean_ip_address(ip_address=None):
    # todo: test me
    from django.utils.ipv6 import clean_ipv6_address
    from django.core.exceptions import ValidationError

    if ip_address:
        try:
            return clean_ipv6_address(ip_address, True)
        except ValidationError:
            return ip_address
    else:
        return ip_address


@contextmanager
def hide_stdout():
    # todo: test me
    remember_stdout = sys.stdout
    sys.stdout = StringIO()
    yield
    sys.stdout = remember_stdout


@contextmanager
def custom_stdout():
    # todo: test me
    remember_stdout = sys.stdout
    sys.stdout = StringIO()
    yield
    sys.stdout = remember_stdout


def add_minutes_to_time(time, minutes):
    """ @param time: datetime.time
        @param minutes: int
    """
    datetime_object = datetime.datetime.strptime(time.strftime('%H:%M'), '%H:%M') + datetime.timedelta(minutes=minutes)
    return datetime_object.time()


def perform_import(val, setting_name):
    """
    If the given setting is a string import notation,
    then perform the necessary import or imports.
    """
    if isinstance(val, str):
        return import_from_string(val, setting_name)
    elif isinstance(val, (list, tuple)):
        return [import_from_string(item, setting_name) for item in val]
    return val


def import_from_string(val, setting_name):
    """
    Attempt to import a class from a string representation.
    """
    import importlib

    try:
        parts = val.split('.')
        module_path, class_name = '.'.join(parts[:-1]), parts[-1]
        module = importlib.import_module(module_path)
        return getattr(module, class_name)
    except ImportError as e:
        msg = "Could not import '%s' setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
        raise ImportError(msg)


def lazy_import_from_string(val, setting_name=''):
    return lazy_object_proxy.Proxy(lambda: import_from_string(val, setting_name))


def lazy_re_compile(*args, **kwargs):
    return lazy_object_proxy.Proxy(lambda: re.compile(*args, **kwargs))


def is_simple_callable(obj):
    """
    True if the object is a callable that takes no arguments.
    """
    function = inspect.isfunction(obj)
    method = inspect.ismethod(obj)

    if not (function or method):
        return False

    args, _, _, defaults = inspect.getargspec(obj)
    len_args = len(args) if function else len(args) - 1
    len_defaults = len(defaults) if defaults else 0
    return len_args <= len_defaults


def drill_to_attr(obj, bits):
    result = obj
    for bit in bits[:-1]:
        result = getattr(result, bit)
        if is_simple_callable(result):
            result = result()
    return getattr(result, bits[-1])


def get_query_dict(source_dict):
    query_strings = []
    for key, value in source_dict.items():
        query_strings.append('&'.join(['{0}={1}'.format(force_str(key), force_str(i)) for i in value]))
    query_string = ('&'.join(query_strings)).encode(settings.DEFAULT_CHARSET)
    result = QueryDict(query_string, mutable=True)
    return result


def full_clean_model_instance_and_get_errors_dict(instance, exclude=None):
    # todo: test me
    try:
        instance.full_clean(exclude=exclude)
    except ValidationError as err:
        return err.message_dict
    else:
        return {}


def url_join(*parts, **query_params):
    # todo: test me
    url = '/'.join(part.strip('/') for part in parts if part)

    if query_params:
        url += '?' + urlencode(query_params)

    return url


def get_requests_session():
    max_retries = requests.packages.urllib3.util.retry.Retry(
        settings.DEFAULT_MAX_RETRY,
        method_whitelist=('HEAD', 'GET', 'OPTIONS'),
        status_forcelist=(500, 502, 503, 504),
        raise_on_status=False,
        backoff_factor=0.1
    )
    adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
    session = requests.Session()
    session.mount('http://', adapter)
    session.mount('https://', adapter)
    return session


requests_session = lazy_object_proxy.Proxy(get_requests_session)


def slugify_filename(filename):
    return '.'.join([slugify(force_str(e)).replace('-', '_') for e in filename.split('.')])


@deconstructible
class GetUploadToTranslifiedPath(object):
    def __init__(self, path):
        self.path = path

    def __call__(self, instance, filename):
        return os.path.join(self.path, self.slugify_filename(filename))

    def slugify_filename(self, filename):
        return slugify_filename(filename)


get_upload_to_translified_func_for_path = GetUploadToTranslifiedPath


def get_localhost_ip_address():
    if settings.IS_TEST:
        return '5.255.219.135'  # просто некоторая старая машинка
    hostname = socket.gethostname()
    try:
        return socket.gethostbyaddr(hostname)[2][0]
    except socket.gaierror:
        _, _, _, _, sockaddr = socket.getaddrinfo(hostname, None, socket.AF_INET6)[0]
        return sockaddr[0]


def get_context_ip_address():
    from ylog.context import ContextFormatter
    if ContextFormatter.is_context_exist():
        ctx = ContextFormatter.get_or_create_logging_context()
        user_ip = ctx.get('user_ip')
        if user_ip:
            return user_ip[-1]


def get_user_ip_address():
    return get_context_ip_address() or get_localhost_ip_address()


def flatten(list_of_lists):
    """
    Takes an iterable of iterables, returns a single iterable containing all items
    """
    # todo: test me
    return chain(*list_of_lists)


def is_duplicate_entry_error(exc):
    """
    (1062, "Duplicate entry '1-hello' for key 'surveyme_surveytext_survey_id_7654a1549b1373ab_uniq'")
    """
    if hasattr(exc, 'args'):
        try:
            return exc.args[0] == 1062
        except IndexError:
            pass
    return False


def get_duplicate_entry_error_model(exc):
    """
    (1062, "Duplicate entry '1-hello' for key 'surveyme_surveytext_survey_id_7654a1549b1373ab_uniq'")
    """
    from django.apps import apps

    if hasattr(exc, 'args'):
        try:
            unique_key_name = exc.args[1].split("'")[-2]
            splitted_key_name = unique_key_name.split('_')
            for i in range(1, 3):
                try:
                    return apps.get_model('_'.join(splitted_key_name[:i]), splitted_key_name[i])
                except LookupError:
                    pass
        except IndexError:
            pass


def retry(exceptions, attempts=1, delay=0):
    # todo: test me
    def wrapper(func):
        @wraps(func)
        def last_wrapper(*args, **kwargs):
            for i in range(attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if delay:
                        time.sleep(delay)
            raise
        return last_wrapper
    return wrapper


def lower_keys(data):
    return {
        key.lower(): force_str(value)
        for key, value in data.items()
    }


def render_to_xml(data):
    from rest_framework_xml.renderers import XMLRenderer
    return '\n'.join(XMLRenderer().render(data).split('\n')[1:])  # removing <?xml...> header


# illegal XML 1.0 character ranges
# See http://www.w3.org/TR/REC-xml/#charsets
#     http://en.wikipedia.org/wiki/Valid_characters_in_XML
XML_ILLEGALS = '|'.join('[%s-%s]' % (s, e) for s, e in [
    ('\u0000', '\u0008'),             # null and C0 controls
    ('\u000B', '\u000C'),             # vertical tab and form feed
    ('\u000E', '\u001F'),             # shift out / shift in
    ('\u007F', '\u009F'),             # C1 controls
    ('\uD800', '\uDFFF'),             # High and Low surrogate areas
    ('\uFDD0', '\uFDDF'),             # not permitted for interchange
    ('\uFFFE', '\uFFFF'),             # byte order marks
])
RE_SANITIZE_XML = re.compile(XML_ILLEGALS, re.M | re.U)


def sanitize_xml_illegals(data):
    # todo: test me
    if not data:
        return data
    return RE_SANITIZE_XML.sub('', data)


def get_backoffice_url_for_obj(obj, action_text=None):
    # todo: test me
    from django.contrib.contenttypes.models import ContentType
    if settings.APP_TYPE == 'forms_biz':
        page = 'edit'
        if action_text in (settings.MESSAGE_CLOSED_BY_TIME,
                           settings.MESSAGE_OPEN_BY_TIME,
                           ):
            page = 'publish'
        return get_backoffice_url_b2b(object_id=obj.id,
                                      page=page,
                                      )
    return get_backoffice_url(
        content_type_id=ContentType.objects.get_for_model(obj).id,
        object_id=obj.id,
    )


def get_backoffice_url_b2b(object_id, page=None, is_admin=True):
    return '{backoffice_url}forms{admin}/{object_id}/{page}'.format(
        admin='/admin' if is_admin else '',
        backoffice_url=get_backoffice_base_url(hash_bang=False),
        object_id=object_id,
        page=page or '',
    )


def get_backoffice_url(content_type_id, object_id):
    # todo: test me
    return '{backoffice_url}redirect?contentType={content_type_id}&objectId={object_id}'.format(
        backoffice_url=get_backoffice_base_url(),
        content_type_id=content_type_id,
        object_id=object_id,
    )


def get_backoffice_base_url(hash_bang=True):
    return '{scheme}://{backoffice_domain}/{hash_bang}'.format(
        scheme=settings.BACKOFFICE_HTTPS and 'https' or 'http',
        backoffice_domain=settings.BACKOFFICE_DOMAIN,
        hash_bang='#/' if hash_bang else '',
    )


def get_attr_or_key(obj, attr_name):
    if isinstance(obj, dict):
        return obj.get(attr_name, None)
    else:
        return getattr(obj, attr_name, None)


def raise_for_status(response):
    try:
        response.raise_for_status()
    except RequestException as e:
        e.args += (response.content, response.headers)
        raise


class BrowserDetectorRemote(object):
    def __init__(self):
        from ids.registry import registry
        self.browser_detector = registry.get_repository('uatraits', 'detect', user_agent='forms')

    def detect(self, user_agent):
        try:
            return self.browser_detector.get_one({'user_agent': force_str(user_agent)})
        except BackendError:
            logger.exception('Got error while receiving data from uatraits')


browser_detector = BrowserDetectorRemote()


def update_by_format(value, format_args):
    if isinstance(value, dict):
        for k, v in value.items():
            value[k] = update_by_format(v, format_args)
    elif isinstance(value, list):
        for i, it in enumerate(value):
            value[i] = update_by_format(it, format_args)
    elif isinstance(value, str):
        value = value.format(**format_args)
    return value


class SessionsRemover(object):
    session_names = set([
        'session_id',
        'sessionid2',
        'secure_session_id',
        'golem_session',
        'eda1',
        'eda2',
    ])

    def __init__(self):
        self.patterns = self.create_patterns()

    @classmethod
    def create_patterns(cls):
        pattern_formats = [r'{name}=[^;]*;? ?', r'"{name}":"[^"]*",?']
        return [
            re.compile(pattern.format(name=name), re.IGNORECASE)
            for name in cls.session_names
            for pattern in pattern_formats
        ] + [re.compile(r'(SessionId|OAuth) [^\n]*', re.IGNORECASE)]

    def _remove_from_string(self, text):
        _text = text
        for pattern in self.patterns:
            _text = pattern.sub('', _text)
        return _text

    def _remove_from_dict(self, value):
        return {
            key: self.remove(value)
            for key, value in value.items()
            if key not in self.session_names
        }

    def remove(self, value):
        if isinstance(value, str):
            return self._remove_from_string(value)
        if isinstance(value, dict):
            return self._remove_from_dict(value)
        return value


_sessions_remover = SessionsRemover()


def remove_sessions(value):
    return _sessions_remover.remove(value)


def clean_source_request(data):
    lower_keys_for_items = ['headers', 'query_params', 'cookies']
    for lower_key_item in lower_keys_for_items:
        if lower_key_item in data:
            data[lower_key_item] = lower_keys(
                data[lower_key_item]
            )
    return data


def get_cleaned_source_request(request):
    if not request:
        return {
            'lang': get_language(),
        }

    source_request = json_loads(request.data.get('source_request', '{}'))
    if not source_request:
        source_request = {
            'query_params': request.data,
            'headers': request.META,
        }
    source_request['lang'] = get_language()

    source_request = clean_source_request(source_request)
    geobase_id = request.META.get(settings.YANDEX_GEOBASE_ID_HTTP_HEADER_NAME)
    if geobase_id:
        source_request['x-geobase-id'] = geobase_id
    source_request = remove_sessions(source_request)

    if 'ip' not in source_request:
        ip = get_user_ip_address()
        if ip:
            source_request['ip'] = ip
    if 'request_id' not in source_request:
        request_id = (
            request.META.get('HTTP_X_REQ_ID')
            or request.META.get('HTTP_X_REQUEST_ID')
            or request.GET.get('request_id')
        )
        if request_id:
            source_request['request_id'] = request_id
    return source_request


def timeit(func):
    """ Декоратор для измерения времени исполнения функции.
        Пишет результаты измерения в лог дебаг.
    """
    logger = logging.getLogger(func.__module__)

    @wraps(func)
    def wrapper(*args, **kwargs):
        start = monotonic()
        try:
            return func(*args, **kwargs)
        finally:
            duration = monotonic() - start
            logger.debug('"%s" succeeded in %.03f ms', func.__name__, duration * 1000)
    return wrapper


def not_empty_values(list_value):
    def _not_empty_values():
        for value in list_value or []:
            if value is not None:
                stripped_value = force_str(value).strip()
                if stripped_value != '':
                    yield stripped_value
    return list(_not_empty_values())


def reraise(exception_type, message=None):
    _, exc_message, exc_trace = sys.exc_info()
    raise exception_type(message or exc_message).with_traceback(exc_trace)


def get_version():
    import pkg_resources
    return pkg_resources.require('forms-backend')[0].version


def get_lang_from_query_params(request):
    if isinstance(request, dict):
        query_params = request.get('query_params', {})
    else:
        query_params = request.query_params
    lang = query_params.get('lang')
    if isinstance(lang, list):
        lang = lang[0]
    return lang


def get_dict_value(d, keys, default_value=None):
    for key in keys:
        if key in d:
            return d[key]
    return default_value


def get_lang_from_accept_language(request):
    if isinstance(request, dict):
        header = get_dict_value(request.get('headers', {}),
                                ['accept-language', 'accept_language', 'http_accept_language'],
                                default_value='')
    else:
        header = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
    parsed_header = parse_accept_lang_header(header)
    if parsed_header:
        return parsed_header[0][0].split('_')[0].split('-')[0].lower()


def get_language_or_default():
    return get_language() or settings.MODELTRANSLATION_DEFAULT_LANGUAGE


def get_lang_with_fallback(lang=None):
    lang = lang or get_language_or_default()
    fallback_lang = settings.MODELTRANSLATION_FALLBACK_LANGUAGES.get(lang)
    if fallback_lang:
        fallback_lang = fallback_lang[0]
    return lang, fallback_lang


class DisableSignals(object):
    def __init__(self, disabled_signals=None):
        self.stashed_signals = defaultdict(list)
        self.disabled_signals = disabled_signals or (pre_save, post_save, )

    def __enter__(self):
        for signal in self.disabled_signals:
            self.disconnect(signal)

    def __exit__(self, exc_type, exc_val, exc_tb):
        for signal in list(self.stashed_signals.keys()):
            self.reconnect(signal)

    def disconnect(self, signal):
        self.stashed_signals[signal] = signal.receivers
        signal.receivers = []

    def reconnect(self, signal):
        signal.receivers = list(set(chain(signal.receivers, self.stashed_signals.pop(signal, []))))

disable_signals = DisableSignals


def generate_code():
    salt = settings.SECRET_KEY.encode()
    code = str(uuid.uuid4()).encode()
    h = hmac.new(salt, code, hashlib.sha1)
    return h.hexdigest()


def re_escape(text):
    """ Эскейпит строку, так чтобы в ней не осталось управляющих символов
    """
    action_chars = set('^(){}[]\\.+*?$')
    result = StringIO()
    for ch in text:
        if ch in action_chars:
            result.write('\\')
        result.write(ch)
    return result.getvalue()


def chunks(iterable, size=100):
    iterator = iter(iterable)
    for first in iterator:
        yield chain([first], islice(iterator, size - 1))


def class_localcache(func):
    """Кешируем результат функции в атрибутах класса
    """
    name = '_localcache_%s' % func.__name__
    def wrapped(self, *args, **kwargs):
        if hasattr(self, name):
            return getattr(self, name, None)

        result = func(self, *args, **kwargs)
        setattr(self, name, result)
        return result
    return wrapped


@contextmanager
def not_atomic():
    yield None


def dates(start, finish, delta=1):
    day = start
    while day < finish:
        yield day
        day += datetime.timedelta(days=delta)
    yield finish


def calendar(start, finish, delta=1):
    return pairwise(dates(start, finish, delta))


def get_tld(url):
    p = urlparse(url)
    host, *rest = force_str(p.netloc).split(':', 1)
    tld = re.search(r'(\.\w{2,3}(\.\w{2,3})?)$', host)
    if tld:
        return tld.group(1)


def flatlist(a):
    if not isinstance(a, list):
        return a
    q = deque(a)
    while q:
        value = q.popleft()
        if isinstance(value, list):
            for it in value:
                if isinstance(it, list):
                    q.append(it)
                else:
                    yield it
        else:
            yield value
