import logging
import random
import jsonfield
from datetime import datetime
from django.db import models
from django.db.models import Count
from django.contrib.sites.models import Site
from django.contrib.auth.models import User
from django.utils.encoding import smart_unicode
from django.db.models import Q
from django.db.models.fields import FieldDoesNotExist
from django.db.models.signals import post_save, pre_delete
from django.core.exceptions import ValidationError
from avocado.core import utils
from avocado.core.structures import ChoicesDict
from avocado.core.models import Base, BasePlural, PublishArchiveMixin
from avocado.core.cache import post_save_cache, pre_delete_uncache, \
    cached_method
from avocado.conf import settings
from avocado import managers, history
from avocado.query.translators import registry as translators
from avocado.query.operators import registry as operators
from avocado.query import oldparsers as parsers
from avocado.stats.agg import Aggregator
from avocado import formatters


__all__ = ('DataCategory', 'DataConcept', 'DataField',
           'DataContext', 'DataView', 'DataQuery')

log = logging.getLogger(__name__)


class DataCategory(Base, PublishArchiveMixin):
    "A high-level organization for data concepts."
    # A reference to a parent for hierarchical categories
    parent = models.ForeignKey('self', null=True, blank=True,
                               related_name='children',
                               limit_choices_to={'parent__isnull': True},
                               help_text='Sub-categories are limited to '
                               'one-level deep')
    order = models.FloatField(null=True, blank=True, db_column='_order')

    objects = managers.DataCategoryManager()

    class Meta(object):
        ordering = ('parent__order', 'parent__name', 'order', 'name')
        verbose_name_plural = 'data categories'


class DataField(BasePlural, PublishArchiveMixin):
    """Describes the significance and/or meaning behind some data. In addition,
    it defines the natural key of the Django field that represents the location
    of that data e.g. ``library.book.title``.
    """
    # App/model/field represent the natural key of this field based on
    # Django's methods of distinguishing models.
    app_name = models.CharField(max_length=200)
    model_name = models.CharField(max_length=200)
    field_name = models.CharField(max_length=200)

    # Supplementary fields that respresent alternate representations
    # of the base field
    label_field_name = models.CharField(max_length=200, null=True, blank=True,
                                        help_text='Label field to the '
                                                  'reference field')
    search_field_name = models.CharField(max_length=200, null=True, blank=True,
                                         help_text='Search field to the '
                                                   'reference field')

    order_field_name = models.CharField(max_length=200, null=True, blank=True,
                                        help_text='Order field to the '
                                                  'reference field')

    code_field_name = models.CharField(max_length=200, null=True, blank=True,
                                       help_text='Order field to the '
                                                 'reference field')

    # An optional unit for this field's data. In some cases databases may have
    # a separate column which denotes the unit for another column, but this is
    # not always the case. Measurement data, for example, should be
    # standardized in the database to allow for consistent querying, thus not
    # requiring a separate column denoting the unit per value.
    unit = models.CharField(max_length=30, null=True, blank=True)
    unit_plural = models.CharField(max_length=40, null=True, blank=True)

    # Although a category does not technically need to be defined, this is more
    # for workflow reasons than for when the concept is published. Automated
    # prcesses may create concepts on the fly, but not know which category they
    # should be linked to initially.
    category = models.ForeignKey(DataCategory, null=True, blank=True)

    # Set this field to true to make this field's values enumerable. This
    # should only be enabled for data that contains a discrete vocabulary, i.e.
    # no full text data.
    enumerable = models.BooleanField(default=False)

    # Set this field to False if you wish to exclude this field's data from the
    # Haystack index.
    indexable = models.BooleanField(default=True)

    type = models.CharField(max_length=100, blank=True, null=True,
                            help_text='Logical type of this field. Typically '
                                      'used downstream for defining behavior '
                                      'and semantics around the field.')

    # An optional translator which customizes input query conditions
    # to a format which is suitable for the database.
    translator = models.CharField(max_length=100, blank=True, null=True,
                                  choices=translators.choices)

    # This is used for the cache key to check if the cached values is stale.
    data_version = models.IntegerField(default=1, help_text='The current '
                                       'version of the underlying data for '
                                       'this field as of the last '
                                       'modification/update.')

    # Certain fields may not be relevant or appropriate for all
    # sites being deployed. This is primarily for preventing exposure of
    # access to private data from certain sites. For example, there may and
    # internal and external deployment of the same site. The internal site
    # has full access to all fields, while the external may have a limited set.
    # NOTE this is not reliable way to prevent exposure of sensitive data.
    # This should be used to simply hide _access_ to the concepts.
    sites = models.ManyToManyField(Site, blank=True, related_name='fields+')

    # The order of this datafield with respect to the category (if defined).
    order = models.FloatField(null=True, blank=True, db_column='_order')

    objects = managers.DataFieldManager()

    class Meta(object):
        unique_together = ('app_name', 'model_name', 'field_name')
        ordering = ('category__order', 'category__name', 'order', 'name')
        permissions = (
            ('view_datafield', 'Can view datafield'),
        )

    @classmethod
    def init(cls, app_name, model_name=None, field_name=None, **kwargs):
        """Convenience method for initializing a new instance with metadata
        populated directly from the model field instance. This returns an
        _unsaved_ instance.
        """
        # Field instance
        if isinstance(app_name, models.Field):
            field = app_name
            field_name = field.name
            model_name = field.model.module_name
            app_name = field.model._meta.app_label

        # Dot-delimited string
        elif isinstance(app_name, basestring) and '.' in app_name:
            values = app_name.split('.')
            if len(values) != 3:
                raise ValueError("The dot-delimited field format must "
                                 "be 'app.model.field'.")
            app_name, model_name, field_name = values

        defaults = {
            'app_name': app_name,
            'model_name': model_name.lower(),
            'field_name': field_name,
        }

        # Temp instance to validate the model field exists
        f = cls(**defaults)

        if not f.model:
            raise ValueError('Unknown model {0}'.format(f.model_name))

        if not f.field:
            raise ValueError('Unknown field {0} on model {1}.'
                             .format(f.field_name, f.model_name))

        # Add field-derived components
        defaults.update({
            'name': f.field.verbose_name.title(),
            'description': f.field.help_text or None,
        })

        # Update defaults with kwargs
        defaults.update(kwargs)

        return cls(**defaults)

    def __unicode__(self):
        if self.name:
            return self.name
        return u'{0} {1}'.format(self.model._meta.verbose_name,
                                 self.field.verbose_name).title()

    def __len__(self):
        return self.size()

    def __nonzero__(self):
        "Takes precedence over __len__, so it is always truthy."
        return True

    # The natural key should be used any time fields are being exported
    # for integration in another system. It makes it trivial to map to new
    # data models since there are discrete parts (as suppose to using the
    # primary key).
    def natural_key(self):
        return self.app_name, self.model_name, self.field_name

    # Django Model Field-related Properties and Methods

    @property
    def real_model(self):
        "Returns the model class this datafield is associated with."
        if not hasattr(self, '_real_model'):
            self._real_model = models.get_model(self.app_name, self.model_name)