from __future__ import print_function
from __future__ import absolute_import
import logging
import copy
from os.path import dirname, basename

from irods.manager import Manager
from irods.message import (MetadataRequest, iRODSMessage, JSON_Message)
from irods.api_number import api_number
from irods.models import (DataObject, Collection, Resource,
                          User, DataObjectMeta, CollectionMeta, ResourceMeta, UserMeta)
from irods.meta import iRODSMeta, AVUOperation
import irods.keywords as kw


logger = logging.getLogger(__name__)


class InvalidAtomicAVURequest(Exception): pass


class MetadataManager(Manager):

    @property
    def use_timestamps(self):
        return getattr(self,'_use_ts',False)

    __kw = {}  # default (empty) keywords

    def _updated_keywords(self,opts):
        kw_ = self.__kw.copy()
        kw_.update(opts)
        return kw_

    def __call__(self, admin = False, timestamps = False, **irods_kw_opt):
        if admin:
            irods_kw_opt.update([(kw.ADMIN_KW,"")])
        new_self = copy.copy(self)
        new_self._use_ts = timestamps
        new_self.__kw = irods_kw_opt
        return new_self

    @staticmethod
    def _model_class_to_resource_type(model_cls):
        return {
            DataObject: 'd',
            Collection: 'C',
            Resource: 'R',
            User: 'u',
        }[model_cls]

    @staticmethod
    def _model_class_to_resource_description(model_cls):
        return {
            DataObject: 'data_object',
            Collection: 'collection',
            Resource: 'resource',
            User: 'user',
        }[model_cls]

    def get(self, model_cls, path):
        resource_type = self._model_class_to_resource_type(model_cls)
        model = {
            'd': DataObjectMeta,
            'C': CollectionMeta,
            'R': ResourceMeta,
            'u': UserMeta
        }[resource_type]
        conditions = {
            'd': [
                Collection.name == dirname(path),
                DataObject.name == basename(path)
            ],
            'C': [Collection.name == path],
            'R': [Resource.name == path],
            'u': [User.name == path]
        }[resource_type]

        columns = (model.id, model.name, model.value, model.units)
        if self.use_timestamps:
            columns += (model.create_time, model.modify_time)
        results = self.sess.query(*columns).filter(*conditions).all()

        def meta_opts(row):
            opts = {'avu_id': row[model.id]}
            if self.use_timestamps:
                opts.update(create_time = row[model.create_time], modify_time=row[model.modify_time])
            return opts

        return [iRODSMeta(
                    row[model.name],
                    row[model.value],
                    row[model.units],
                    **meta_opts(row))
               for row in results]

    def add(self, model_cls, path, meta, **opts):

        resource_type = self._model_class_to_resource_type(model_cls)
        message_body = MetadataRequest(
            "add",
            "-" + resource_type,
            path,
            meta.name,
            meta.value,
            meta.units,
            **self._updated_keywords(opts)
        )
        request = iRODSMessage("RODS_API_REQ", msg=message_body,
                               int_info=api_number['MOD_AVU_METADATA_AN'])
        with self.sess.pool.get_connection() as conn:
            conn.send(request)
            response = conn.recv()
        logger.debug(response.int_info)

    def remove(self, model_cls, path, meta, **opts):
        resource_type = self._model_class_to_resource_type(model_cls)
        message_body = MetadataRequest(
            "rm",
            "-" + resource_type,
            path,
            meta.name,
            meta.value,
            meta.units,
            **self._updated_keywords(opts)
        )
        request = iRODSMessage("RODS_API_REQ", msg=message_body,
                               int_info=api_number['MOD_AVU_METADATA_AN'])
        with self.sess.pool.get_connection() as conn:
            conn.send(request)
            response = conn.recv()
        logger.debug(response.int_info)

    def copy(self, src_model_cls, dest_model_cls, src, dest, **opts):
        src_resource_type = self._model_class_to_resource_type(src_model_cls)
        dest_resource_type = self._model_class_to_resource_type(dest_model_cls)
        message_body = MetadataRequest(
            "cp",
            "-" + src_resource_type,
            "-" + dest_resource_type,
            src,
            dest,
            **self._updated_keywords(opts)
        )
        request = iRODSMessage("RODS_API_REQ", msg=message_body,
                               int_info=api_number['MOD_AVU_METADATA_AN'])

        with self.sess.pool.get_connection() as conn:
            conn.send(request)
            response = conn.recv()
        logger.debug(response.int_info)

    def set(self, model_cls, path, meta, **opts):
        resource_type = self._model_class_to_resource_type(model_cls)
        message_body = MetadataRequest(
            "set",
            "-" + resource_type,
            path,
            meta.name,
            meta.value,
            meta.units,
            **self._updated_keywords(opts)
        )
        request = iRODSMessage("RODS_API_REQ", msg=message_body,
                               int_info=api_number['MOD_AVU_METADATA_AN'])
        with self.sess.pool.get_connection() as conn:
            conn.send(request)
            response = conn.recv()
        logger.debug(response.int_info)

    @staticmethod
    def _avu_operation_to_dict( op ):
        opJSON = { "operation": op.operation,
                   "attribute": op.avu.name,
                   "value": op.avu.value
        }
        if op.avu.units not in ("",None):
            opJSON["units"] = op.avu.units
        return opJSON

    def apply_atomic_operations(self, model_cls, path, *avu_ops):
        if not all(isinstance(op,AVUOperation) for op in avu_ops):
            raise InvalidAtomicAVURequest("avu_ops must contain 1 or more AVUOperations")
        request = {
            "admin_mode": True if kw.ADMIN_KW in self.__kw.keys() else False,
            "entity_name": path,
            "entity_type": self._model_class_to_resource_description(model_cls),
            "operations" : [self._avu_operation_to_dict(op) for op in avu_ops]
        }
        self._call_atomic_metadata_api(request)

    def _call_atomic_metadata_api(self, request_text):
        with self.sess.pool.get_connection() as conn:
            request_msg = iRODSMessage("RODS_API_REQ",  JSON_Message( request_text, conn.server_version ),
                                       int_info=api_number['ATOMIC_APPLY_METADATA_OPERATIONS_APN'])
            conn.send( request_msg )
            response = conn.recv()
        response_msg = response.get_json_encoded_struct()
        logger.debug("in atomic_metadata, server responded with: %r",response_msg)
