"""Django DDP models."""
from __future__ import absolute_import

import collections
import os

from django.db import models, transaction
from django.db.models.fields import NOT_PROVIDED
from django.conf import settings
from django.contrib.contenttypes.fields import (
    GenericRelation, GenericForeignKey,
)
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.utils.encoding import python_2_unicode_compatible
import ejson
from dddp import meteor_random_id


def get_meteor_id(obj_or_model, obj_pk=None):
    """Return an Alea ID for the given object."""
    if obj_or_model is None:
        return None
    # Django model._meta is now public API -> pylint: disable=W0212
    meta = obj_or_model._meta
    model = meta.model
    if model is ObjectMapping:
        # this doesn't make sense - raise TypeError
        raise TypeError("Can't map ObjectMapping instances through self.")

    # try getting value of AleaIdField straight from instance if possible
    if isinstance(obj_or_model, model):
        # obj_or_model is an instance, not a model.
        if isinstance(meta.pk, AleaIdField):
            return obj_or_model.pk
        if obj_pk is None:
            # fall back to primary key, but coerce as string type for lookup.
            obj_pk = str(obj_or_model.pk)
    alea_unique_fields = [
        field
        for field in meta.local_fields
        if isinstance(field, AleaIdField) and field.unique
    ]
    if len(alea_unique_fields) == 1:
        # found an AleaIdField with unique=True, assume it's got the value.
        aid = alea_unique_fields[0].attname
        if isinstance(obj_or_model, model):
            val = getattr(obj_or_model, aid)
        elif obj_pk is None:
            val = None
        else:
            val = model.objects.values_list(aid, flat=True).get(
                pk=obj_pk,
            )
        if val:
            return val

    if obj_pk is None:
        # bail out if args are (model, pk) but pk is None.
        return None

    # fallback to using AleaIdField from ObjectMapping model.
    content_type = ContentType.objects.get_for_model(model)
    try:
        return ObjectMapping.objects.values_list(
            'meteor_id', flat=True,
        ).get(
            content_type=content_type,
            object_id=obj_pk,
        )
    except ObjectDoesNotExist:
        return ObjectMapping.objects.create(
            content_type=content_type,
            object_id=obj_pk,
            meteor_id=meteor_random_id('/collection/%s' % meta),
        ).meteor_id
get_meteor_id.short_description = 'DDP ID'  # nice title for admin list_display


def get_meteor_ids(model, object_ids):
    """Return Alea ID mapping for all given ids of specified model."""
    # Django model._meta is now public API -> pylint: disable=W0212
    meta = model._meta
    result = collections.OrderedDict(
        (str(obj_pk), None)
        for obj_pk
        in object_ids
    )
    if isinstance(meta.pk, AleaIdField):
        # primary_key is an AleaIdField, use it.
        return collections.OrderedDict(
            (obj_pk, obj_pk) for obj_pk in object_ids
        )
    alea_unique_fields = [
        field
        for field in meta.local_fields
        if isinstance(field, AleaIdField) and field.unique and not field.null
    ]
    if len(alea_unique_fields) == 1:
        aid = alea_unique_fields[0].name
        query = model.objects.filter(
            pk__in=object_ids,
        ).values_list('pk', aid)
    else:
        content_type = ContentType.objects.get_for_model(model)
        query = ObjectMapping.objects.filter(
            content_type=content_type,
            object_id__in=list(result)
        ).values_list('object_id', 'meteor_id')
    for obj_pk, meteor_id in query:
        result[str(obj_pk)] = meteor_id
    for obj_pk, meteor_id in result.items():
        if meteor_id is None:
            result[obj_pk] = get_meteor_id(model, obj_pk)
    return result


def get_object_id(model, meteor_id):
    """Return an object ID for the given meteor_id."""
    if meteor_id is None:
        return None
    # Django model._meta is now public API -> pylint: disable=W0212
    meta = model._meta

    if model is ObjectMapping:
        # this doesn't make sense - raise TypeError
        raise TypeError("Can't map ObjectMapping instances through self.")

    if isinstance(meta.pk, AleaIdField):
        # meteor_id is the primary key
        return meteor_id

    alea_unique_fields = [
        field
        for field in meta.local_fields
        if isinstance(field, AleaIdField) and field.unique
    ]
    if len(alea_unique_fields) == 1:
        # found an AleaIdField with unique=True, assume it's got the value.
        val = model.objects.values_list(
            'pk', flat=True,
        ).get(**{
            alea_unique_fields[0].attname: meteor_id,
        })
        if val:
            return val

    content_type = ContentType.objects.get_for_model(model)
    return ObjectMapping.objects.filter(
        content_type=content_type,
        meteor_id=meteor_id,
    ).values_list('object_id', flat=True).get()


def get_object_ids(model, meteor_ids):
    """Return all object IDs for the given meteor_ids."""
    if model is ObjectMapping:
        # this doesn't make sense - raise TypeError
        raise TypeError("Can't map ObjectMapping instances through self.")
    # Django model._meta is now public API -> pylint: disable=W0212
    meta = model._meta
    alea_unique_fields = [
        field
        for field in meta.local_fields
        if isinstance(field, AleaIdField) and field.unique and not field.null
    ]
    result = collections.OrderedDict(
        (str(meteor_id), None)
        for meteor_id
        in meteor_ids
    )
    if len(alea_unique_fields) == 1:
        aid = alea_unique_fields[0].name
        query = model.objects.filter(**{
            '%s__in' % aid: meteor_ids,
        }).values_list(aid, 'pk')
    else:
        content_type = ContentType.objects.get_for_model(model)
        query = ObjectMapping.objects.filter(
                content_type=content_type,
                meteor_id__in=meteor_ids,
        ).values_list('meteor_id', 'object_id')
    for meteor_id, object_id in query:
        result[meteor_id] = object_id
    return result


def get_object(model, meteor_id, *args, **kwargs):
    """Return an object for the given meteor_id."""
    # Django model._meta is now public API -> pylint: disable=W0212
    meta = model._meta
    if isinstance(meta.pk, AleaIdField):
        # meteor_id is the primary key
        return model.objects.filter(*args, **kwargs).get(pk=meteor_id)

    alea_unique_fields = [
        field
        for field in meta.local_fields
        if isinstance(field, AleaIdField) and field.unique and not field.null
    ]
    if len(alea_unique_fields) == 1:
        return model.objects.filter(*args, **kwargs).get(**{
            alea_unique_fields[0].name: meteor_id,
        })

    return model.objects.filter(*args, **kwargs).get(
        pk=get_object_id(model, meteor_id),
    )


class AleaIdField(models.CharField):

    """CharField that generates its own values using Alea PRNG before INSERT."""

    def __init__(self, *args, **kwargs):
        """Assume max_length of 17 to match Meteor implementation."""
        kwargs['blank'] = True
        kwargs.setdefault('verbose_name', 'Alea ID')
        kwargs.setdefault('max_length', 17)
        super(AleaIdField, self).__init__(*args, **kwargs)

    def deconstruct(self):
        """Return arguments to pass to __init__() to re-create this field."""
        name, path, args, kwargs = super(AleaIdField, self).deconstruct()
        del kwargs['blank']
        return name, path, args, kwargs

    def get_seeded_value(self, instance):
        """Generate a syncronised value."""
        # Django model._meta is public API -> pylint: disable=W0212
        return meteor_random_id(
            '/collection/%s' % instance._meta, self.max_length,
        )

    def get_pk_value_on_save(self, instance):
        """Generate ID if required."""
        value = super(AleaIdField, self).get_pk_value_on_save(instance)
        if not value:
            value = self.get_seeded_value(instance)
        return value

    def pre_save(self, model_instance, add):
        """Generate ID if required."""
        value = super(AleaIdField, self).pre_save(model_instance, add)
        if (not value) and self.default in (meteor_random_id, NOT_PROVIDED):
            value = self.get_seeded_value(model_instance)
            setattr(model_instance, self.attname, value)
        return value


# Please don't hate me...
AID_KWARGS = {}

if os.environ.get('AID_MIGRATE_STEP', '') == '1':
    AID_KWARGS['null'] = True  # ...please?


class AleaIdMixin(models.Model):

    """Django model mixin that provides AleaIdField field (as aid)."""

    aid = AleaIdField(unique=True, editable=True, **AID_KWARGS)

    class Meta(object):

        """Model meta options for AleaIdMixin."""

        abstract = True


@python_2_unicode_compatible
class ObjectMapping(models.Model):

    """Mapping from regular Django model primary keys to Meteor object IDs."""

    meteor_id = AleaIdField()
    content_type = models.ForeignKey(ContentType, db_index=True)
    object_id = models.CharField(max_length=255)
    content_object = GenericForeignKey('content_type', 'object_id')

    def __str__(self):
        """Text representation of a mapping."""
        return '%s: %s[%s]' % (
            self.meteor_id, self.content_type, self.object_id,
        )

    class Meta(object):

        """Meta info for ObjectMapping model."""

        unique_together = [
            ['content_type', 'meteor_id'],
        ]
        index_together = [
            ['content_type', 'object_id'],
            ['content_type', 'meteor_id'],
        ]


@python_2_unicode_compatible
class Connection(models.Model, object):

    """Django DDP connection instance."""

    connection_id = AleaIdField()
    server_addr = models.CharField(max_length=255)
    remote_addr = models.CharField(max_length=255)
    version = models.CharField(max_length=255)

    def __str__(self):
        """Text representation of subscription."""
        return u'%s/\u200b%s' % (
            self.connection_id,
            self.remote_addr,
        )


@python_2_unicode_compatible
class Subscription(models.Model, object):

    """Subscription to a publication with params."""

    _publication_cache = {}
    connection = models.ForeignKey(Connection)
    sub_id = models.CharField(max_length=17)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True)
    publication = models.CharField(max_length=255)
    params_ejson = models.TextField(default='{}')

    class Meta(object):

        """Subscription model meta."""

        unique_together = [
            ['connection', 'sub_id'],
        ]

    def __str__(self):
        """Text representation of subscription."""
        return u'%s/\u200b%s/\u200b%s: %s%s' % (
            self.user,
            self.connection_id,
            self.sub_id,
            self.publication,
            self.params_ejson,
        )

    def get_params(self):
        """Get params dict."""
        return ejson.loads(self.params_ejson or '{}')

    def set_params(self, vals):
        """Set params dict."""
        self.params_ejson = ejson.dumps(vals or {})

    params = property(get_params, set_params)


@python_2_unicode_compatible
class SubscriptionCollection(models.Model):

    """Collections for a subscription."""

    subscription = models.ForeignKey(Subscription, related_name='collections')
    model_name = models.CharField(max_length=255)
    collection_name = models.CharField(max_length=255)

    def __str__(self):
        """Human readable representation of colleciton for a subscription."""
        return u'%s \u200b %s (%s)' % (
            self.subscription,
            self.collection_name,
            self.model_name,
        )


class ObjectMappingMixin(models.Model):

    """Model mixin that provides GenericRelation back to ObjectMapping model."""

    object_mapping = GenericRelation(ObjectMapping)

    class Meta(object):

        """ObjectMappingMixin model meta options."""

        abstract = True
