import socket
import time
import logging
import tempfile
from contextlib import contextmanager
from urllib.parse import parse_qsl, urlencode
from datetime import datetime, timedelta
from base64 import b64encode, b64decode
import zlib
import json

import unicodedata
from lxml import etree

from django.http import QueryDict as DjangoQueryDict
from django.utils.encoding import force_str
from django.conf import settings

from ids.registry import registry

from intranet.search.core.errors import RecoverableError
from intranet.search.core.utils import xml
from intranet.search.core.utils.ydeploy import get_celery_workers_config


log = logging.getLogger(__name__)


class QueryDict(DjangoQueryDict):
    def __init__(self, qs=None, mutable=True, encoding='utf-8'):
        DjangoQueryDict.__init__(self, qs, mutable, encoding)

    def pop(self, key, default=None):
        try:
            val = DjangoQueryDict.pop(self, key)
        except KeyError:
            return default

        if val == []:
            return default

        return val[0]

    def poplist(self, *args, **kwargs):
        return DjangoQueryDict.pop(self, *args, **kwargs)


def serialize_snippet(data, format='json'):
    default = None
    if format == 'json':
        def date_to_str(date):
            DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
            if isinstance(date, datetime):
                return date.strftime(DATETIME_FORMAT)
            else:
                raise ValueError()
        default = date_to_str

    return json.dumps(data, default=default)


def deserialize_snippet(data):
    try:
        return json.loads(data)
    except ValueError:
        # в старых-старых документах ещё может быть xml в сниппетах
        xml_data = etree.fromstring(data)
        return xml.deserialize_xml(xml_data)


def _handle_result(result, suppress_stdout):
    stdout = result.stdout.text.strip()
    stderr = result.stderr.text.strip()

    fmt = '(%s -> %s)\n%s'

    if stdout and not suppress_stdout:
        log.info(fmt, result.source, result.returncode, stdout)

    if stderr:
        if result.returncode != 0:
            log.error(fmt, result.source, result.returncode, stderr)
        else:
            log.warning(fmt, result.source, result.returncode, stderr)

    if result.returncode != 0:
        raise RuntimeError(f'`{result.source}` failed with status: {result.returncode}')


def tempdir():
    return tempfile.mkdtemp(prefix='isearch-')


@contextmanager
def timeit(logger, msg):
    ts = time.time()
    yield
    te = time.time()
    logger.debug('{msg} : {time} sec'. format(msg=msg, time=(te-ts)))


class log_time:
    def __init__(self):
        self.start = None
        self.end = None
        self.duration = None

    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end = time.time()
        self.duration = self.end - self.start


def convert_timeout(timeout):
    try:
        return int(timeout)
    except ValueError:
        suffixes = {
            's': 1,
            'm': 60,
            'h': 60 * 60,
            'd': 60 * 60 * 24
        }

        timeout, suffix = timeout[:-1], timeout[-1]

        return int(timeout) * suffixes[suffix]


class reraise_as_recoverable:
    def __init__(self, *exceptions):
        self.exceptions = exceptions

    def __call__(self, f):
        def inner(*args, **kwargs):
            with self:
                return f(*args, **kwargs)
        return inner

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_value, traceback):
        if isinstance(exc_value, self.exceptions):
            new_exc = RecoverableError(exc_value)
            new_exc.__traceback__ = traceback
            raise new_exc


def get_current_node(create_missing=False):
    from intranet.search.core.models import Node

    hostname = socket.gethostname()

    config = [data['suffix'] for _, data in get_celery_workers_config() if data['suffix']]

    info = {'SWARM_WALK_SUFFIX': config[0] if config else ''}

    try:
        node = Node.objects.get(hostname=hostname)
        old_info = node.info or {}
        if old_info != info:
            node.info = info
            node.save()
    except Node.DoesNotExist:
        if create_missing:
            node = Node.objects.create(hostname=hostname, info=info)
        else:
            raise

    return node


def get_active_hosts(delta=timedelta(seconds=60)):
    from intranet.search.core.models import Node
    return list(Node.objects.filter(updated_at__gte=datetime.now() - delta).
                order_by('id').values_list('hostname', flat=True))


def get_possible_suffixes():
    from intranet.search.core.models import Node
    nodes_info = Node.objects.filter(
        updated_at__gte=datetime.now() - timedelta(seconds=60),
    ).values_list('info', flat=True)

    nodes_json = (info or {} for info in nodes_info)

    suffixes = {info.get('SWARM_WALK_SUFFIX', '') for info in nodes_json}
    suffixes.add('')
    suffixes = list(suffixes)

    suffixes.sort()

    return suffixes


def group_ids(data):
    """ Вытаскивает id'шники групп из json'а который возвращает api стаффа
    """
    groups_set = set()

    for grp in data['groups']:
        groups_set.add(grp['group']['id'])
        for anc in grp['group']['ancestors']:
            groups_set.add(anc['id'])

    return sorted(groups_set)


def format_directory_group(group):
    return '{group[type]}_{group[id]}'.format(group=group)


def query2string(obj):
    res = urlencode(obj)
    res = zlib.compress(res, 9)
    return b64encode(res, b'-_')


def string2query(string):
    return dict(
        parse_qsl(b64decode(string.encode('utf-8'), b'-_').decode('zlib'))
    )


def get_scopes():
    scopes = settings.ISEARCH['scopes']
    return sorted(scopes, key=lambda x: scopes[x].get('order', 100))


CONTROL_CHARACTERS_MAP = dict.fromkeys(range(32))


def remove_control_characters(text):
    """ Удаляет служебные символы из строки
    """
    return force_str(text).translate(CONTROL_CHARACTERS_MAP)


def get_x_api_key_header(token):
    return {'X-API-Key': f'{token}'}


def get_oauth_header(token):
    return {'Authorization': f'OAuth {token}'}


def get_oauth_token(token_type):
    return settings.ISEARCH_OAUTH.get(token_type, '')


def get_ids_repository(service, resource_type, **kwargs):
    """Обертка над registry.get_repository, добавляет токен из settings.yaml
    """
    from intranet.search.core.blackbox import blackbox_client

    kwargs.setdefault('user_agent', 'Intrasearch')
    oauth_token = kwargs.pop('oauth_token', None)
    user_ticket_needed = kwargs.pop('user_ticket_needed', False)

    if oauth_token is None:
        try:
            oauth_token_type = settings.ISEARCH['api'][service]['oauth_token']
        except KeyError:
            oauth_token_type = settings.ISEARCH['api']['oauth_token']

        oauth_token = get_oauth_token(oauth_token_type)

    if user_ticket_needed:
        kwargs['user_ticket'] = blackbox_client.get_user_ticket(oauth_token)

    return registry.get_repository(service, resource_type,
                                   oauth_token=oauth_token,
                                   **kwargs)


def shave_diacritic_marks(txt):
    """ Удаляет знаки диактрики из слов
    >>> print(shave_diacritic_marks(u'Все́волод Вели́чко'))
    Всеволод Величко
    >>> print(shave_diacritic_marks(u'Алёна'))
    Алена
    """
    try:
        txt = force_str(txt)
    except Exception:
        log.exception('Cannot convert string to unicode: %s', txt)
        return txt

    norm_txt = unicodedata.normalize('NFD', txt)
    shaved = ''.join(c for c in norm_txt
                     if not unicodedata.combining(c))
    return unicodedata.normalize('NFC', shaved)


def get_kps(revision_id):
    """ Возвращает kps сааса по id ревизии
    """
    kps_prefix = settings.ISEARCH_SAAS_KPS_PREFIX or ''
    return int(f'{kps_prefix}{revision_id}')


def generate_prefix(sep, schema, **kwargs):
    """ Создает префикс для ключа с разделителем в виде sep по схеме
    в schema и данными из **kwargs
    """
    items = [kwargs.pop(item, None) for item in schema]
    assert not kwargs, (sep, schema, kwargs)
    return sep.join(str(i) for i in items if i is not None)


def parse_prefix(sep, schema, prefix):
    """Парсит префикс ключа с разделителем в виде sep и по схеме schema"""
    prefix = force_str(prefix)
    return {name: item for name, item in zip(schema, prefix.split(sep, len(schema) - 1))}
