import logbook
from munch import munchify
from storage_interfaces.scsi.abstracts import ScsiVolume

from ..core import Field
from ..core.api.special_values import OMIT, Autogenerate
from ..core.bindings import RelatedObjectBinding
from ..core.exceptions import InfiniSDKException
from ..core.object_query import LazyQuery
from ..core.utils import end_reraise_context
from .dataset import Dataset, DatasetTypeBinder
from .lun import LogicalUnit, LogicalUnitContainer
from .scsi_serial import SCSISerial
from .system_object import InfiniBoxObject

_logger = logbook.Logger(__name__)


class VolumesBinder(DatasetTypeBinder):
    def create_group_snapshot(
        self, volumes, snap_prefix=Autogenerate("{short_uuid}_"), snap_suffix=OMIT
    ):
        """
        Creates multiple snapshots with a single consistent point-in-time, returning the snapshots
        in respective order to parent volumes

        :param volumes: list of volumes we should create a snapshot of
        """
        volumes = list(volumes)
        returned = []
        for v in volumes:
            v.trigger_begin_fork()
        try:
            resp = self.system.api.post(
                "volumes/group_snapshot",
                data={
                    "snap_prefix": snap_prefix,
                    "snap_suffix": snap_suffix,
                    "entities": [{"id": v.id} for v in volumes],
                },
            )
        except Exception as e:  # pylint: disable=broad-except, unused-variable
            with end_reraise_context():
                for v in volumes:
                    v.trigger_cancel_fork()
        else:
            snaps_by_parent_id = {}
            for entity in resp.get_result():
                snaps_by_parent_id[entity["parent_id"]] = self.object_type(
                    self.system, entity
                )
            for v in volumes:
                snap = snaps_by_parent_id.get(v.id)
                if snap is None:
                    _logger.warning(
                        "No snapshot was created for {} in group snapshot operation", v
                    )
                    v.trigger_cancel_fork()
                else:
                    v.trigger_finish_fork(snap)
                returned.append(snap)

        return returned

    def get_all_internals(self, internal_type=OMIT, page=None, page_size=None):
        """
        Returns the data of all the internal volumes
        Can be filtered by type
        """
        url = self.get_url_path().add_path("internal_volumes")

        if internal_type is not OMIT:
            url = url.add_path(internal_type.lower())

        if page_size is not None:
            assert page_size > 0, "Page size must be a positive integer value"
            url = url.add_query_param("page_size", page_size)

        if page is not None:
            assert page > 0, "Page must be a positive integer value"
            url = url.add_query_param("page", page)

        return munchify(self.system.api.get(url).get_result())


class Volume(Dataset):

    BINDER_CLASS = VolumesBinder

    FIELDS = [
        Field(
            "name",
            creation_parameter=True,
            mutable=True,
            is_filterable=True,
            is_sortable=True,
            default=Autogenerate("vol_{uuid}"),
        ),
        Field("serial", type=SCSISerial, is_filterable=True, is_sortable=True),
        Field(
            "udid",
            type=int,
            creation_parameter=True,
            optional=True,
            mutable=True,
            is_filterable=True,
            feature_name="openvms",
        ),
        Field(
            "cons_group",
            type="infinisdk.infinibox.cons_group:ConsGroup",
            api_name="cg_id",
            is_filterable=True,
            is_sortable=True,
            binding=RelatedObjectBinding("cons_groups", None),
        ),
        Field(
            "parent",
            type="infinisdk.infinibox.volume:Volume",
            cached=True,
            api_name="parent_id",
            binding=RelatedObjectBinding("volumes"),
            is_filterable=True,
        ),
        Field(
            "data_snapshot_guid",
            is_filterable=True,
            is_sortable=True,
            feature_name="nas_replication",
        ),
        Field("paths_available", type=bool, new_to="5.0"),
        Field("nguid", feature_name="nvme"),
        Field(
            "snapshot_expires_at",
            type=int,
            feature_name="replicate_snapshots",
        ),
        Field(
            "snapshot_retention",
            type=int,
            creation_parameter=True,
            optional=True,
            is_filterable=True,
            is_sortable=True,
            feature_name="replicate_snapshots",
        ),
        Field(
            "source_replicated_sg",
            api_name="source_replicated_sg_id",
            type="infinisdk.infinibox.cons_group:ConsGroup",
            binding=RelatedObjectBinding("cons_groups"),
            is_filterable=True,
            is_sortable=True,
            feature_name="sg_replicate_snapshots",
        ),
    ]

    @classmethod
    def create(cls, system, **fields):
        pool = fields.get("pool")
        if pool and isinstance(pool, InfiniBoxObject):
            pool.invalidate_cache(
                "allocated_physical_capacity",
                "free_physical_capacity",
                "free_virtual_capacity",
                "reserved_capacity",
            )
        return super(Volume, cls).create(system, **fields)

    def own_replication_snapshot(self, name=None):
        if not name:
            name = Autogenerate("vol_{uuid}")
        data = {"name": name}
        child = self._create(
            self.system, self.get_this_url_path().add_path("own_snapshot"), data=data
        )
        return child

    def reset_serial(self):
        return self.system.api.post(self.get_this_url_path().add_path("reset_serial"))

    def _get_luns_data_from_url(self):
        return LazyQuery(
            self.system, self.get_this_url_path().add_path("luns")
        ).to_list()

    def get_lun(self, mapping_object):
        """Given either a host or a host cluster object, returns the single LUN object mapped to this volume.

        An exception is raised if multiple matching LUs are found

        :param mapping_object: Either a host cluster or a host object to be checked
        :returns: None if no lu is found for this entity
        """

        def is_mapping_object_lu(lu_data):
            lu_mapping_id = lu_data["host_id"] or lu_data["host_cluster_id"]
            return lu_mapping_id == mapping_object.id

        lus = [
            LogicalUnit(system=self.system, **lu_data)
            for lu_data in self._get_luns_data_from_url()
            if is_mapping_object_lu(lu_data)
        ]
        if len(lus) > 1:
            raise InfiniSDKException(
                "There shouldn't be multiple luns for volume-mapping object pair"
            )
        return lus[0] if lus else None

    def get_logical_units(self):
        return LogicalUnitContainer.from_dict_list(
            self.system, self._get_luns_data_from_url()
        )

    def unmap(self):
        """Unmaps a volume from its hosts"""
        for lun in self.get_logical_units():
            lun.unmap()
        self.invalidate_cache("mapped")

    def has_children(self):
        return self.get_field("has_children")

    def is_in_cons_group(self):
        return self.get_cons_group() is not None

    def promote_snapshot(self):
        """
        Converts a snapshot into a standalone volume
        """
        url = self.get_this_url_path().add_path("promote")

        # headers need to be added here since MGMT chose
        # to use a json content type even if no content is
        # being sent (no data in post)
        headers = {"Content-type": "application/json"}
        response = self.system.api.post(url, headers=headers)
        response_result = response.get_result()

        return self.system.volumes.get_by_id(response_result["id"])

    def get_all_internals(self, page=None, page_size=None):
        """
        Returns the data of all the internal volumes that are children of the current volume
        """
        url = self.get_this_url_path().add_path("internal_volumes")

        if page_size is not None:
            assert page_size > 0, "Page size must be a positive integer value"
            url = url.add_query_param("page_size", page_size)

        if page is not None:
            assert page > 0, "Page must be a positive integer value"
            url = url.add_query_param("page", page)

        return munchify(self.system.api.get(url).get_result())


ScsiVolume.register(Volume)  # pylint: disable=no-member
