"""
Classes representing asset metadata.
"""

from datetime import datetime
import dateutil.parser
import pytz
import json
from contracts import contract, new_contract
from opaque_keys.edx.keys import CourseKey, AssetKey
from lxml import etree


new_contract('AssetKey', AssetKey)
new_contract('CourseKey', CourseKey)
new_contract('datetime', datetime)
new_contract('basestring', basestring)
new_contract('long', long)
new_contract('AssetElement', lambda x: isinstance(x, etree._Element) and x.tag == "asset")  # pylint: disable=protected-access, no-member
new_contract('AssetsElement', lambda x: isinstance(x, etree._Element) and x.tag == "assets")  # pylint: disable=protected-access, no-member


class AssetMetadata(object):
    """
    Stores the metadata associated with a particular course asset. The asset metadata gets stored
    in the modulestore.
    """

    TOP_LEVEL_ATTRS = ['pathname', 'internal_name', 'locked', 'contenttype', 'thumbnail', 'fields']
    EDIT_INFO_ATTRS = ['curr_version', 'prev_version', 'edited_by', 'edited_by_email', 'edited_on']
    CREATE_INFO_ATTRS = ['created_by', 'created_by_email', 'created_on']
    ATTRS_ALLOWED_TO_UPDATE = TOP_LEVEL_ATTRS + EDIT_INFO_ATTRS
    ASSET_TYPE_ATTR = 'type'
    ASSET_BASENAME_ATTR = 'filename'
    XML_ONLY_ATTRS = [ASSET_TYPE_ATTR, ASSET_BASENAME_ATTR]
    XML_ATTRS = XML_ONLY_ATTRS + ATTRS_ALLOWED_TO_UPDATE + CREATE_INFO_ATTRS

    # Type for assets uploaded by a course author in Studio.
    GENERAL_ASSET_TYPE = 'asset'

    # Asset section XML tag for asset metadata as XML.
    ALL_ASSETS_XML_TAG = 'assets'

    # Individual asset XML tag for asset metadata as XML.
    ASSET_XML_TAG = 'asset'

    # Top-level directory name in exported course XML which holds asset metadata.
    EXPORTED_ASSET_DIR = 'assets'

    # Filename of all asset metadata exported as XML.
    EXPORTED_ASSET_FILENAME = 'assets.xml'

    @contract(asset_id='AssetKey',
              pathname='basestring|None', internal_name='basestring|None',
              locked='bool|None', contenttype='basestring|None',
              thumbnail='basestring|None', fields='dict|None',
              curr_version='basestring|None', prev_version='basestring|None',
              created_by='int|long|None', created_by_email='basestring|None', created_on='datetime|None',
              edited_by='int|long|None', edited_by_email='basestring|None', edited_on='datetime|None')
    def __init__(self, asset_id,
                 pathname=None, internal_name=None,
                 locked=None, contenttype=None,
                 thumbnail=None, fields=None,
                 curr_version=None, prev_version=None,
                 created_by=None, created_by_email=None, created_on=None,
                 edited_by=None, edited_by_email=None, edited_on=None,
                 field_decorator=None,):
        """
        Construct a AssetMetadata object.

        Arguments:
            asset_id (AssetKey): Key identifying this particular asset.
            pathname (str): Original path to file at asset upload time.
            internal_name (str): Name, url, or handle for the storage system to access the file.
            locked (bool): If True, only course participants can access the asset.
            contenttype (str): MIME type of the asset.
            thumbnail (str): the internal_name for the thumbnail if one exists
            fields (dict): fields to save w/ the metadata
            curr_version (str): Current version of the asset.
            prev_version (str): Previous version of the asset.
            created_by (int): User ID of initial user to upload this asset.
            created_by_email (str): Email address of initial user to upload this asset.
            created_on (datetime): Datetime of intial upload of this asset.
            edited_by (int): User ID of last user to upload this asset.
            edited_by_email (str): Email address of last user to upload this asset.
            edited_on (datetime): Datetime of last upload of this asset.
            field_decorator (function): used by strip_key to convert OpaqueKeys to the app's understanding.
                Not saved.
        """
        self.asset_id = asset_id if field_decorator is None else field_decorator(asset_id)
        self.pathname = pathname  # Path w/o filename.
        self.internal_name = internal_name
        self.locked = locked
        self.contenttype = contenttype
        self.thumbnail = thumbnail
        self.curr_version = curr_version
        self.prev_version = prev_version
        now = datetime.now(pytz.utc)
        self.edited_by = edited_by
        self.edited_by_email = edited_by_email
        self.edited_on = edited_on or now
        # created_by, created_by_email, and created_on should only be set here.
        self.created_by = created_by
        self.created_by_email = created_by_email
        self.created_on = created_on or now
        self.fields = fields or {}

    def __repr__(self):
        return """AssetMetadata{!r}""".format((
            self.asset_id,
            self.pathname, self.internal_name,
            self.locked, self.contenttype, self.fields,
            self.curr_version, self.prev_version,
            self.created_by, self.created_by_email, self.created_on,
            self.edited_by, self.edited_by_email, self.edited_on,
        ))

    def update(self, attr_dict):
        """
        Set the attributes on the metadata. Any which are not in ATTRS_ALLOWED_TO_UPDATE get put into
        fields.

        Arguments:
            attr_dict: Prop, val dictionary of all attributes to set.
        """
        for attr, val in attr_dict.iteritems():
            if attr in self.ATTRS_ALLOWED_TO_UPDATE:
                setattr(self, attr, val)
            else:
                self.fields[attr] = val

    def to_storable(self):
        """
        Converts metadata properties into a MongoDB-storable dict.
        """
        return {
            'filename': self.asset_id.path,
            'asset_type': self.asset_id.asset_type,
            'pathname': self.pathname,
            'internal_name': self.internal_name,
            'locked': self.locked,
            'contenttype': self.contenttype,
            'thumbnail': self.thumbnail,
            'fields': self.fields,
            'edit_info': {
                'curr_version': self.curr_version,
                'prev_version': self.prev_version,
                'created_by': self.created_by,
                'created_by_email': self.created_by_email,
                'created_on': self.created_on,
                'edited_by': self.edited_by,
                'edited_by_email': self.edited_by_email,
                'edited_on': self.edited_on
            }
        }

    @contract(asset_doc='dict|None')
    def from_storable(self, asset_doc):
        """
        Fill in all metadata fields from a MongoDB document.

        The asset_id prop is initialized upon construction only.
        """
        if asset_doc is None:
            return
        self.pathname = asset_doc['pathname']
        self.internal_name = asset_doc['internal_name']
        self.locked = asset_doc['locked']
        self.contenttype = asset_doc['contenttype']
        self.thumbnail = asset_doc['thumbnail']
        self.fields = asset_doc['fields']
        self.curr_version = asset_doc['edit_info']['curr_version']
        self.prev_version = asset_doc['edit_info']['prev_version']
        self.created_by = asset_doc['edit_info']['created_by']
        self.created_by_email = asset_doc['edit_info']['created_by_email']
        self.created_on = asset_doc['edit_info']['created_on']
        self.edited_by = asset_doc['edit_info']['edited_by']
        self.edited_by_email = asset_doc['edit_info']['edited_by_email']
        self.edited_on = asset_doc['edit_info']['edited_on']

    @contract(node='AssetElement')
    def from_xml(self, node):
        """
        Walk the etree XML node and fill in the asset metadata.
        The node should be a top-level "asset" element.
        """
        for child in node:
            qname = etree.QName(child)
            tag = qname.localname
            if tag in self.XML_ATTRS:
                value = child.text
                if tag in self.XML_ONLY_ATTRS:
                    # An AssetLocator is constructed separately from these parts.
                    continue
                elif tag == 'locked':
                    # Boolean.
                    value = True if value == "true" else False
                elif value == 'None':
                    # None.
                    value = None
                elif tag in ('created_on', 'edited_on'):
                    # ISO datetime.
                    value = dateutil.parser.parse(value)
                elif tag in ('created_by', 'edited_by'):
                    # Integer representing user id.
                    value = int(value)
                elif tag == 'fields':
                    # Dictionary.
                    value = json.loads(value)
                setattr(self, tag, value)

    @contract(node='AssetElement')
    def to_xml(self, node):
        """
        Add the asset data as XML to the passed-in node.
        The node should already be created as a top-level "asset" element.
        """
        for attr in self.XML_ATTRS:
            child = etree.SubElement(node, attr)
            # Get the value.
            if attr == self.ASSET_TYPE_ATTR:
                value = self.asset_id.asset_type
            elif attr == self.ASSET_BASENAME_ATTR:
                value = self.asset_id.path
            else:
                value = getattr(self, attr)

            # Format the value.
            if isinstance(value, bool):
                value = "true" if value else "false"
            elif isinstance(value, datetime):
                value = value.isoformat()
            elif isinstance(value, dict):
                value = json.dumps(value)
            else:
                value = unicode(value)
            child.text = value

    @staticmethod
    @contract(node='AssetsElement', assets=list)
    def add_all_assets_as_xml(node, assets):
        """
        Take a list of AssetMetadata objects. Add them all to the node.
        The node should already be created as a top-level "assets" element.
        """
        for asset in assets:
            asset_node = etree.SubElement(node, "asset")
            asset.to_xml(asset_node)


class CourseAssetsFromStorage(object):
    """
    Wrapper class for asset metadata lists returned from modulestore storage.
    """
    @contract(course_id='CourseKey', asset_md=dict)
    def __init__(self, course_id, doc_id, asset_md):
        """
        Params:
            course_id: Course ID for which the asset metadata is stored.
            doc_id: ObjectId of MongoDB document
            asset_md: Dict with asset types as keys and lists of storable asset metadata as values.
        """
        self.course_id = course_id
        self._doc_id = doc_id
        self.asset_md = asset_md

    @property
    def doc_id(self):
        """
        Returns the ID associated with the MongoDB document which stores these course assets.
        """
        return self._doc_id

    def setdefault(self, item, default=None):
        """
        Provides dict-equivalent setdefault functionality.
        """
        return self.asset_md.setdefault(item, default)

    def __getitem__(self, item):
        return self.asset_md[item]

    def __delitem__(self, item):
        del self.asset_md[item]

    def __len__(self):
        return len(self.asset_md)

    def __setitem__(self, key, value):
        self.asset_md[key] = value

    def get(self, item, default=None):
        """
        Provides dict-equivalent get functionality.
        """
        return self.asset_md.get(item, default)

    def iteritems(self):
        """
        Iterates over the items of the asset dict.
        """
        return self.asset_md.iteritems()
