# -*- coding: utf-8 -*-
"""Provides support for dealing with Mongo Documents."""
from bson.objectid import ObjectId
from tavi import Connection
from tavi.base.documents import BaseDocument, BaseDocumentMetaClass
from tavi.commands import Insert, Update
from tavi.errors import TaviConnectionError
from tavi.utils.timer import Timer
import inflection
import logging
import pymongo
import re

logger = logging.getLogger(__name__)


class DocumentMetaClass(BaseDocumentMetaClass):
    """MetaClass for Documents. Sets up the database connection, infers the
    collection name by pluralizing and underscoring the class name, and sets
    the collection fo rthe Document.

    """

    def __init__(cls, name, bases, attrs):
        super(DocumentMetaClass, cls).__init__(name, bases, attrs)
        cls._collection_name = inflection.underscore(
            inflection.pluralize(name))

    @property
    def collection(cls):
        """Returns a handle to the Document collection."""
        if not Connection.database:
            raise TaviConnectionError(
                "Cannot connect to MongoDB. Did you call "
                "'tavi.connection.Connection.setup'?")
        return Connection.database[cls._collection_name]

    @property
    def collection_name(cls):
        """Returns the name of the Document collection."""
        return cls._collection_name


class Document(BaseDocument):
    """Represents a Mongo Document. Provides methods for saving and retrieving
    and deleting Documents.

    """
    __metaclass__ = DocumentMetaClass

    __MAX_NAMESPACE_SIZE__ = 127  # bytes
    __UNIQUE_INDEX_SUFFIX__ = "_unique_index"

    def __init__(self, **kwargs):
        self._id = kwargs.pop("_id", None)
        super(Document, self).__init__(**kwargs)
        unique_keys = [
            k for k, v in self._field_descriptors.items() if v.unique]

        if len(unique_keys) > 0:
            key_pairs = [(k, pymongo.ASCENDING) for k in unique_keys]
            max_index_name_length = self.__MAX_NAMESPACE_SIZE__ - \
                len(".$" + self.__class__.collection.full_name
                    + self.__UNIQUE_INDEX_SUFFIX__)

            name = "_".join(unique_keys)[:max_index_name_length] + \
                self.__UNIQUE_INDEX_SUFFIX__

            opts = {"name": name, "unique": True}
            self.__class__.collection.create_index(key_pairs, **opts)

    @property
    def bson_id(self):
        """Returns the BSON Id of the Document."""
        return self._id

    @classmethod
    def count(cls):
        """Returns the total number of documents in the collection."""
        return cls.collection.count()

    def delete(self):
        """Removes the Document from the collection."""
        timer = Timer()
        with timer:
            result = self.__class__.collection.remove({"_id": self._id})

        logger.info(
            "(%ss) %s DELETE %s",
            timer.duration_in_seconds(),
            self.__class__.__name__,
            self._id
        )

        if result.get("err"):
            logger.error(result.get("err"))

    @classmethod
    def find(cls, *args, **kwargs):
        """Returns all Documents in collection that meet criteria. Wraps
        pymongo's *find* method and supports all of the same arguments.

        """
        timer = Timer()
        with timer:
            results = cls.collection.find(*args, **kwargs)

        logger.info(
            "(%ss) %s FIND %s, %s (%s record(s) found)",
            timer.duration_in_seconds(),
            cls.__name__,
            args,
            kwargs,
            results.count()
        )

        return [cls(**result) for result in results]

    @classmethod
    def find_all(cls):
        """Returns all Documents in collection."""
        return cls.find()

    @classmethod
    def find_by_id(cls, id_):
        """Returns the Document that matches *id_* or None if it cannot be
        found.

        """
        return cls.find_one(ObjectId(id_))

    @classmethod
    def find_one(cls, spec_or_id=None, *args, **kwargs):
        """Returns one Document that meets criteria. Wraps pymongo's find_one
        method and supports all of the same arguments.

        """
        timer = Timer()
        with timer:
            result = cls.collection.find_one(spec_or_id, *args, **kwargs)

        found_record, num_found = None, 0

        if result:
            found_record, num_found = cls(**result), 1

        logger.info(
            "(%ss) %s FIND ONE %s, %s, %s (%s record(s) found)",
            timer.duration_in_seconds(),
            cls.__name__,
            spec_or_id,
            args,
            kwargs,
            num_found
        )

        return found_record

    def save(self, w=1, wtimeout=0, j=False):
        """Saves the Document by inserting it into the collection if it does
        not exist or updating it if it does. Returns True if save was
        successful. Ensures that the Document is valid before saving and
        returns False if it was not.

        This function performs an upsert if the model has an ID, but is not in
        the database.

        If the document model has a field named 'created_at', this field's
        value will be set to the current time when the document is inserted.

        Supports the following arguments that are passed to pymongo:

        w: Write concern level. Default is 1
        wtimeout: Timeout in ms for write concern. Default is 0
        j: Journaling option for write concern. Default is False

        See Pymongo docs for more information on these arguments:
        http://api.mongodb.org/python/current/api/pymongo/collection.html

        See the Mongo docs for more information on Write Concern:
        http://docs.mongodb.org/manual/core/write-concern/

        """
        if not self.valid:
            return False

        write_opts = frozenset(["w", "j", "wtimeout"])
        kwargs = {k: v for k, v in locals().iteritems() if k in write_opts}

        operation = Update if self.bson_id else Insert
        operation = operation(self, **kwargs)

        timer = Timer()
        with timer:
            try:
                operation.execute()
            except pymongo.errors.PyMongoError as e:
                operation.reset_fields()

                if isinstance(e, pymongo.errors.DuplicateKeyError):
                    logger.warn(
                        "%s %s failed due to unique index violation (%s)",
                        self.__class__.__name__,
                        operation.name,
                        e.message
                    )
                    f = re.search(r'\$(.+)_unique_index', e.message).group(1)
                    self.errors.add(f, "must be unique")
                    return False
                raise

        self.changed_fields = set()

        logger.info(
            "(%ss) %s %s %s, %s",
            timer.duration_in_seconds(),
            self.__class__.__name__,
            operation.name,
            self.mongo_field_values,
            self._id
        )
        return True


class EmbeddedDocument(BaseDocument):
    """Represents a single EmbeddedDocument. Supports an *owner* attribute that
    indicates the owning Document.

    """
    def __init__(self, **kwargs):
        super(EmbeddedDocument, self).__init__(**kwargs)
        self.owner = None

    def __eq__(self, other):
        return other and self.field_values == other.field_values
