# coding=utf-8
"""
Blob. References to files stored in a on-disk repository
"""
from __future__ import absolute_import, division, print_function

import hashlib
import uuid

import sqlalchemy as sa
from flask_sqlalchemy import BaseQuery
from sqlalchemy.schema import Column
from sqlalchemy.types import Integer

from abilian.core.models.base import Model
from abilian.core.sqlalchemy import UUID, JSONDict


class BlobQuery(BaseQuery):
    """Query class for :class:`Blob` objects
    """

    def by_uuid(self, uuid):
        """
    Like `.get()` but by uuid

    :param uuid: a `string` or an `uuid`.
    """
        try:
            return self.filter_by(uuid=uuid).one()
        except sa.orm.exc.NoResultFound:
            return None


class Blob(Model):
    """Model for storing large file content.

    Files are stored on-disk, named after their uuid. Repository is located in
    instance folder/data/files.
    """
    __tablename__ = "blob"
    query_class = BlobQuery

    id = Column(Integer(), primary_key=True, autoincrement=True)
    uuid = Column(UUID(), unique=True, nullable=False, default=uuid.uuid4)
    meta = Column(JSONDict(), nullable=False, default=dict)

    def __init__(self, value=None, *args, **kwargs):
        super(Blob, self).__init__(*args, **kwargs)
        if self.uuid is None:
            self.uuid = uuid.uuid4()

        if self.meta is None:
            self.meta = dict()

        if value is not None:
            self.value = value

    @property
    def file(self):
        """Return :class:`pathlib.Path` object used for storing value.
        """
        from abilian.services.repository import session_repository as repository
        return repository.get(self, self.uuid)

    @property
    def size(self):
        """Return size in bytes of value.
        """
        f = self.file
        return f.stat().st_size if f is not None else 0

    @property
    def value(self):
        """Binary value content.
        """
        v = self.file
        return v.open('rb').read() if v is not None else v

    @value.setter
    def value(self, value, encoding='utf-8'):
        """Store binary content to applications's repository and update
        `self.meta['md5']`.

        :param:content: string, bytes, or any object with a `read()` method
        :param:encoding: encoding to use when content is unicode
        """
        from abilian.services.repository import session_repository as repository
        repository.set(self, self.uuid, value)
        self.meta['md5'] = unicode(hashlib.md5(self.value).hexdigest())

        if hasattr(value, 'filename'):
            filename = getattr(value, 'filename')
            if isinstance(filename, bytes):
                filename = filename.decode('utf-8')
            self.meta['filename'] = filename

        if hasattr(value, 'content_type'):
            self.meta['mimetype'] = getattr(value, 'content_type')

    @value.deleter
    def value(self):
        """Remove value from repository.
        """
        from abilian.services.repository import session_repository as repository
        repository.delete(self, self.uuid)

    @property
    def md5(self):
        """Return md5 from meta, or compute it if absent.
        """
        md5 = self.meta.get('md5')
        if md5 is None:
            md5 = unicode(hashlib.md5(self.value).hexdigest())

        return md5

    def __nonzero__(self):
        """A blob is considered null if it has no file.
        """
        return self.file is not None and self.file.exists()


@sa.event.listens_for(sa.orm.Session, 'after_flush')
def _blob_propagate_delete_content(session, flush_context):
    deleted = (obj for obj in session.deleted if isinstance(obj, Blob))
    for blob in deleted:
        del blob.value
