import logging
import copy
from os.path import dirname, basename
from typing import Any, Dict

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


# This was necessarily made separate from the MetadataManager definition
# in order to avoid infinite recursion in iRODSMetaCollection.__getattr__
_MetadataManager_opts_initializer = {'admin': False, 'timestamps': False, 'iRODSMeta_type': iRODSMeta, 'reload': True}


class MetadataManager(Manager):
    def __init__(self, *_):
        self._opts = _MetadataManager_opts_initializer.copy()
        super().__init__(*_)

    @property
    def use_timestamps(self):
        return self._opts['timestamps']

    __kw: Dict[str, Any] = {}  # default (empty) keywords

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

    def get_api_keywords(self):
        return self.__kw.copy()

    def __call__(self, **flags):
        # Make a new shallow copy of the manager object, but update options from parameter list.
        new_self = copy.copy(self)
        new_self._opts = copy.copy(self._opts)

        # Update the flags that do bookkeeping in the returned(new) manager object.
        new_self._opts.update((key, val) for key, val in flags.items() if val is not None)

        # Update the ADMIN_KW flag in the returned(new) object.
        if new_self._opts.get('admin'):
            self.__kw[kw.ADMIN_KW] = ""
        else:
            self.__kw.pop(kw.ADMIN_KW, None)

        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):
        if not path:
            # Short circuit.  This should be of the same type as the object returned at the function's end.
            return []
        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 [
            self._opts['iRODSMeta_type'](None, None, None)._from_column_triple(
                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._to_column_triple(), **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._to_column_triple(), **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._to_column_triple(), **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)
