# -*- coding: utf-8 -*-
import sys
import json
import six
from funcy import select_keys, cached_property, once, once_per, monkey, wraps
from funcy.py2 import mapcat, map
from .cross import pickle, md5

import django
from django.utils.encoding import smart_str
from django.core.exceptions import ImproperlyConfigured
from django.db import DEFAULT_DB_ALIAS
from django.db.models import Manager, Model
from django.db.models.query import QuerySet
from django.db.models.sql.datastructures import EmptyResultSet
from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed
try:
    from django.db.models.query import MAX_GET_RESULTS
except ImportError:
    MAX_GET_RESULTS = None

from .conf import model_profile, redis_client, handle_connection_failure, LRU, ALL_OPS
from .utils import monkey_mix, get_model_name, stamp_fields, load_script, \
                   func_cache_key, cached_view_fab, get_thread_id, family_has_profile
from .tree import dnfs
from .invalidation import invalidate_obj, invalidate_dict


__all__ = ('cached_as', 'cached_view_as', 'install_cacheops')

_local_get_cache = {}


@handle_connection_failure
def cache_thing(cache_key, data, cond_dnfs, timeout):
    """
    Writes data to cache and creates appropriate invalidators.
    """
    load_script('cache_thing', LRU)(
        keys=[cache_key],
        args=[
            pickle.dumps(data, -1),
            json.dumps(cond_dnfs, default=str),
            timeout
        ]
    )


def cached_as(*samples, **kwargs):
    """
    Caches results of a function and invalidates them same way as given queryset.
    NOTE: Ignores queryset cached ops settings, just caches.
    """
    timeout = kwargs.get('timeout')
    extra = kwargs.get('extra')
    key_func = kwargs.get('key_func', func_cache_key)

    # If we unexpectedly get list instead of queryset return identity decorator.
    # Paginator could do this when page.object_list is empty.
    # TODO: think of better way doing this.
    if len(samples) == 1 and isinstance(samples[0], list):
        return lambda func: func

    def _get_queryset(sample):
        if isinstance(sample, Model):
            queryset = sample.__class__.objects.inplace().filter(pk=sample.pk)
        elif isinstance(sample, type) and issubclass(sample, Model):
            queryset = sample.objects.all()
        else:
            queryset = sample

        queryset._require_cacheprofile()

        return queryset

    querysets = map(_get_queryset, samples)
    cond_dnfs = mapcat(dnfs, querysets)
    key_extra = [qs._cache_key() for qs in querysets]
    key_extra.append(extra)
    if not timeout:
        timeout = min(qs._cacheconf['timeout'] for qs in querysets)

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            cache_key = 'as:' + key_func(func, args, kwargs, key_extra)

            cache_data = redis_client.get(cache_key)
            if cache_data is not None:
                return pickle.loads(cache_data)

            result = func(*args, **kwargs)
            cache_thing(cache_key, result, cond_dnfs, timeout)
            return result

        return wrapper
    return decorator


def cached_view_as(*samples, **kwargs):
    return cached_view_fab(cached_as)(*samples, **kwargs)


class QuerySetMixin(object):
    @cached_property
    def _cacheprofile(self):
        profile = model_profile(self.model)
        if profile:
            self._cacheconf = profile.copy()
            self._cacheconf['write_only'] = False
        return profile

    @cached_property
    def _cloning(self):
        return 1000

    def _require_cacheprofile(self):
        if self._cacheprofile is None:
            raise ImproperlyConfigured(
                'Cacheops is not enabled for %s.%s model.\n'
                'If you don\'t want to cache anything by default '
                'you can configure it with empty ops.'
                    % (self.model._meta.app_label, get_model_name(self.model)))

    def _cache_key(self, extra=''):
        """
        Compute a cache key for this queryset
        """
        md = md5()
        md.update('%s.%s' % (self.__class__.__module__, self.__class__.__name__))
        # Vary cache key for proxy models
        md.update('%s.%s' % (self.model.__module__, self.model.__name__))
        # Protect from field list changes in model
        md.update(stamp_fields(self.model))
        # Use query SQL as part of a key
        try:
            sql, params = self.query.get_compiler(self._db or DEFAULT_DB_ALIAS).as_sql()
            try:
                sql_str = sql % params
            except UnicodeDecodeError:
                sql_str = sql % map(smart_str, params)
            md.update(smart_str(sql_str))
        except EmptyResultSet:
            pass
        # If query results differ depending on database
        if self._cacheprofile and not self._cacheprofile['db_agnostic']:
            md.update(self.db)
        if extra:
            md.update(str(extra))
        # Thing only appeared in Django 1.8 and was renamed in Django 1.9
        it_class = getattr(self, '_iterator_class', None) or getattr(self, '_iterable_class', None)
        if it_class:
            md.update('%s.%s' % (it_class.__module__, it_class.__name__))
        # 'flat' attribute changes results formatting for values_list() in Django 1.8 and earlier
        if hasattr(self, 'flat'):
            md.update(str(self.flat))

        return 'q:%s' % md.hexdigest()

    def _cache_results(self, cache_key, results):
        cond_dnfs = dnfs(self)
        cache_thing(cache_key, results, cond_dnfs, self._cacheconf['timeout'])

    def cache(self, ops=None, timeout=None, write_only=None):
        """
        Enables caching for given ops
            ops        - a subset of {'get', 'fetch', 'count', 'exists'},
                         ops caching to be turned on, all enabled by default
            timeout    - override default cache timeout
            write_only - don't try fetching from cache, still write result there

        NOTE: you actually can disable caching by omiting corresponding ops,
              .cache(ops=[]) disables caching for this queryset.
        """
        self._require_cacheprofile()

        if ops is None or ops == 'all':
            ops = ALL_OPS
        if isinstance(ops, str):
            ops = [ops]
        self._cacheconf['ops'] = set(ops)

        if timeout is not None:
            self._cacheconf['timeout'] = timeout
        if write_only is not None:
            self._cacheconf['write_only'] = write_only

        return self

    def nocache(self):
        """
        Convinience method, turns off caching for this queryset