# Copyright (c) 2016 by Kaminario Technologies, Ltd.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
"""Volume driver for Kaminario K2 all-flash arrays."""
from oslo_log import log as logging

from cinder.common import constants
from cinder import coordination
from cinder.i18n import _
from cinder.objects import fields
from cinder.volume.drivers.kaminario import kaminario_common as common
from cinder.volume import volume_utils
from cinder.zonemanager import utils as fczm_utils

K2_REP_FAILED_OVER = fields.ReplicationStatus.FAILED_OVER
LOG = logging.getLogger(__name__)


class KaminarioFCDriver(common.KaminarioCinderDriver):
    """Kaminario K2 FC Volume Driver.

    Version history:
        1.0 - Initial driver
        1.1 - Added manage/unmanage and extra-specs support for nodedup
        1.2 - Added replication support
        1.3 - Added retype support
        1.4 - Added replication failback support
    """

    VERSION = '1.4'

    # ThirdPartySystems wiki page name
    CI_WIKI_NAME = "Kaminario_K2_CI"

    @volume_utils.trace
    def __init__(self, *args, **kwargs):
        super(KaminarioFCDriver, self).__init__(*args, **kwargs)
        self._protocol = constants.FC
        self.lookup_service = fczm_utils.create_lookup_service()

    @volume_utils.trace
    @coordination.synchronized('{self.k2_lock_name}')
    def initialize_connection(self, volume, connector):
        """Attach K2 volume to host."""
        # Check wwpns in host connector.
        if not connector.get('wwpns'):
            msg = _("No wwpns found in host connector.")
            LOG.error(msg)
            raise common.KaminarioCinderDriverException(reason=msg)
        # To support replication failback
        temp_client = None
        if (hasattr(volume, 'replication_status') and
                volume.replication_status == K2_REP_FAILED_OVER):
            temp_client = self.client
            self.client = self.target
        # Get target wwpns.
        target_wwpns = self.get_target_info(volume)
        # Map volume.
        lun = self.k2_initialize_connection(volume, connector)
        # Create initiator-target mapping.
        target_wwpns, init_target_map = self._build_initiator_target_map(
            connector, target_wwpns)
        # To support replication failback
        if temp_client:
            self.client = temp_client
        # Return target volume information.
        conn_info = {'driver_volume_type': 'fibre_channel',
                     'data': {"target_discovered": True,
                              "target_lun": lun,
                              "target_wwn": target_wwpns,
                              "initiator_target_map": init_target_map}}
        fczm_utils.add_fc_zone(conn_info)
        return conn_info

    def get_hostname_initiator_pwwn(self, volume):
        init_host = None
        init_host_name = ""
        init_pwwn = []
        vol_map_rs = self.client.search("mappings", {"volume": volume})
        if hasattr(vol_map_rs, "hits") and vol_map_rs.total != 0:
            init_host = vol_map_rs.hits[0].host
            init_host_name = init_host.name
        if init_host is not None:
            host_fc_ports = self.client.search("host_fc_ports", host=init_host)
            if hasattr(host_fc_ports, "hits") and host_fc_ports.total != 0:
                for port in host_fc_ports.hits:
                    if port.pwwn:
                        init_pwwn.append((port.pwwn).replace(':', ''))
        return init_host_name, init_pwwn

    @volume_utils.trace
    @coordination.synchronized('{self.k2_lock_name}')
    def terminate_connection(self, volume, connector, **kwargs):
        if connector is None:
            host_name, init_pwwn = self.get_hostname_initiator_pwwn(volume)
        else:
            host_name = self.get_initiator_host_name(connector)

        # To support replication failback
        temp_client = None
        if (hasattr(volume, 'replication_status') and
                volume.replication_status == K2_REP_FAILED_OVER):
            temp_client = self.client
            self.client = self.target
        super(KaminarioFCDriver, self).terminate_connection(volume, connector)
        properties = {"driver_volume_type": "fibre_channel", "data": {}}
        host_rs = self.client.search("hosts", name=host_name)
        # In terminate_connection, host_entry is deleted if host
        # is not attached to any volume
        if host_rs.total == 0:
            # Get target wwpns.
            target_wwpns = self.get_target_info(volume)
            if connector is None:
                connector = {'wwpns': init_pwwn}
            target_wwpns, init_target_map = self._build_initiator_target_map(
                connector, target_wwpns)
            properties["data"] = {"target_wwn": target_wwpns,
                                  "initiator_target_map": init_target_map}
            fczm_utils.remove_fc_zone(properties)

        # To support replication failback
        if temp_client:
            self.client = temp_client
        return properties

    def get_target_info(self, volume):
        LOG.debug("Searching target wwpns in K2.")
        fc_ports_rs = self.client.search("system/fc_ports")
        target_wwpns = []
        if hasattr(fc_ports_rs, 'hits') and fc_ports_rs.total != 0:
            for port in fc_ports_rs.hits:
                if port.pwwn:
                    target_wwpns.append((port.pwwn).replace(':', ''))
        if not target_wwpns:
            msg = _("Unable to get FC target wwpns from K2.")
            LOG.error(msg)
            raise common.KaminarioCinderDriverException(reason=msg)
        return target_wwpns

    @volume_utils.trace
    def _get_host_object(self, connector):
        host_name = self.get_initiator_host_name(connector)
        LOG.debug("Searching initiator hostname: %s in K2.", host_name)
        host_rs = self.client.search("hosts", name=host_name)
        host_wwpns = connector['wwpns']
        if host_rs.total == 0:
            try:
                LOG.debug("Creating initiator hostname: %s in K2.", host_name)
                host = self.client.new("hosts", name=host_name,
                                       type="Linux").save()
            except Exception as ex:
                LOG.exception("Unable to create host : %s in K2.",
                              host_name)
                raise common.KaminarioCinderDriverException(reason=ex)
        else:
            # Use existing host.
            LOG.debug("Use existing initiator hostname: %s in K2.", host_name)
            host = host_rs.hits[0]
        # Adding host wwpn.
        for wwpn in host_wwpns:
            wwpn = ":".join([wwpn[i:i + 2] for i in range(0, len(wwpn), 2)])
            if self.client.search("host_fc_ports", pwwn=wwpn,
                                  host=host).total == 0:
                LOG.debug("Adding wwpn: %(wwpn)s to host: "
                          "%(host)s in K2.", {'wwpn': wwpn,
                                              'host': host_name})
                try:
                    self.client.new("host_fc_ports", pwwn=wwpn,
                                    host=host).save()
                except Exception as ex:
                    if host_rs.total == 0:
                        self._delete_host_by_name(host_name)
                    LOG.exception("Unable to add wwpn : %(wwpn)s to "
                                  "host: %(host)s in K2.",
                                  {'wwpn': wwpn, 'host': host_name})
                    raise common.KaminarioCinderDriverException(reason=ex)
        return host, host_rs, host_name

    def _build_initiator_target_map(self, connector, all_target_wwns):
        """Build the target_wwns and the initiator target map."""
        target_wwns = []
        init_targ_map = {}

        if self.lookup_service is not None:
            # use FC san lookup.
            dev_map = self.lookup_service.get_device_mapping_from_network(
                connector.get('wwpns'),
                all_target_wwns)

            for fabric_name in dev_map:
                fabric = dev_map[fabric_name]
                target_wwns += fabric['target_port_wwn_list']
                for initiator in fabric['initiator_port_wwn_list']:
                    if initiator not in init_targ_map:
                        init_targ_map[initiator] = []
                    init_targ_map[initiator] += fabric['target_port_wwn_list']
                    init_targ_map[initiator] = list(set(
                        init_targ_map[initiator]))
            target_wwns = list(set(target_wwns))
        else:
            initiator_wwns = connector.get('wwpns', [])
            target_wwns = all_target_wwns

            for initiator in initiator_wwns:
                init_targ_map[initiator] = target_wwns

        return target_wwns, init_targ_map
