# Copyright (C) 2020, 2024, Hitachi, Ltd.
# Copyright (C) 2025, Hitachi Vantara
#
#    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.
#
"""REST interface fibre channel module for Hitachi HBSD Driver."""

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils

from cinder import exception
from cinder.volume import configuration
from cinder.volume.drivers.hitachi import hbsd_rest as rest
from cinder.volume.drivers.hitachi import hbsd_rest_api as rest_api
from cinder.volume.drivers.hitachi import hbsd_utils as utils
from cinder.zonemanager import utils as fczm_utils

FC_VOLUME_OPTS = [
    cfg.BoolOpt(
        'hitachi_zoning_request',
        default=False,
        help='If True, the driver will configure FC zoning between the server '
             'and the storage system provided that FC zoning manager is '
             'enabled.'),
]

_FC_HMO_DISABLE_IO = 91

_MSG_EXCEED_HOST_GROUP_MAX = "could not find empty Host group ID for adding."

LOG = logging.getLogger(__name__)
MSG = utils.HBSDMsg

CONF = cfg.CONF
CONF.register_opts(FC_VOLUME_OPTS, group=configuration.SHARED_CONF_GROUP)


class HBSDRESTFC(rest.HBSDREST):
    """REST interface fibre channel class for Hitachi HBSD Driver."""

    def __init__(self, conf, storage_protocol, db):
        """Initialize instance variables."""
        super(HBSDRESTFC, self).__init__(conf, storage_protocol, db)
        self._lookup_service = fczm_utils.create_lookup_service()

    def connector_searcher_query(self, port: str, group: int | str | None):
        # See hbsd_utils.HostConnectorSearcher for a description of
        # this method behavior.

        def _lookup_group_targets(port: str, groupNum: int) -> list[str]:
            # Returns list of WWNs/targets.

            targets = []
            hba_wwns = self.client.get_hba_wwns(port, groupNum)
            if hba_wwns:
                targets = [hba_wwn['hostWwn'] for hba_wwn in hba_wwns]
            return targets

        def _lookup_group_by_name(port: str,
                                  group: str) -> tuple[int, list[str]]:
            # Returns tuple of group number/metadata tuple and targets.
            # None if group not found (or no targets in group).
            hba_wwns = self.client.get_hba_wwns_by_name(port, group)
            if hba_wwns:
                gid = hba_wwns[0]['hostGroupNumber']
                targets = [hba_wwn['hostWwn'] for hba_wwn in hba_wwns]
                return (gid, None), targets

            return None

        def _lookup_all_groups(port: str) -> list[int]:
            # Returns list of groups by int.
            params = {'portId': port}
            host_grp_list = self.client.get_host_grps(params)
            return [(data['hostGroupNumber'], None) for data in host_grp_list]

        if isinstance(group, int):
            return _lookup_group_targets(port, group)
        elif isinstance(group, str):
            return _lookup_group_by_name(port, group)

        return _lookup_all_groups(port)

    def connect_storage(self):
        """Prepare for using the storage."""
        target_ports = self.conf.hitachi_target_ports
        compute_target_ports = self.conf.hitachi_compute_target_ports
        if hasattr(
                self.conf,
                self.driver_info['param_prefix'] + '_rest_pair_target_ports'):
            pair_target_ports = self.conf.hitachi_rest_pair_target_ports
        else:
            pair_target_ports = []
        available_ports = []
        available_compute_ports = []

        super(HBSDRESTFC, self).connect_storage()
        # The port attributes must contain TAR.
        params = {'portAttributes': 'TAR'}
        port_list = self.client.get_ports(params=params)
        for port in set(target_ports + compute_target_ports +
                        pair_target_ports):
            if port not in [port_data['portId'] for port_data in port_list]:
                self.output_log(MSG.INVALID_PORT, port=port,
                                additional_info='portAttributes: not TAR')
        for port_data in port_list:
            port = port_data['portId']
            if port not in set(target_ports + compute_target_ports +
                               pair_target_ports):
                continue
            secure_fc_port = True
            can_port_schedule = True
            if hasattr(
                    self.conf,
                    self.driver_info['param_prefix'] + '_port_scheduler'):
                port_scheduler_param = self.conf.hitachi_port_scheduler
            else:
                port_scheduler_param = False
            if (port_data['portType'] not in ['FIBRE', 'FCoE'] or
                    not port_data['lunSecuritySetting']):
                secure_fc_port = False
            elif (port in set(target_ports + compute_target_ports) and
                  port_scheduler_param and not (
                      port_data.get('fabricMode') and
                      port_data.get('portConnection') == 'PtoP')):
                can_port_schedule = False
            if not secure_fc_port or not can_port_schedule:
                self.output_log(
                    MSG.INVALID_PORT, port=port,
                    additional_info='portType: %s, lunSecuritySetting: %s, '
                    'fabricMode: %s, portConnection: %s' %
                    (port_data['portType'],
                     port_data.get('lunSecuritySetting'),
                     port_data.get('fabricMode'),
                     port_data.get('portConnection')))
            if not secure_fc_port:
                continue
            wwn = port_data.get('wwn')
            if target_ports and port in target_ports and can_port_schedule:
                available_ports.append(port)
                self.storage_info['wwns'][port] = wwn
            if (compute_target_ports and port in compute_target_ports and
                    can_port_schedule):
                available_compute_ports.append(port)
                self.storage_info['wwns'][port] = wwn
            if pair_target_ports and port in pair_target_ports:
                self.storage_info['pair_ports'].append(port)

        if target_ports:
            for port in target_ports:
                if port in available_ports:
                    self.storage_info['controller_ports'].append(port)
        if compute_target_ports:
            for port in compute_target_ports:
                if port in available_compute_ports:
                    self.storage_info['compute_ports'].append(port)

        self.check_ports_info()
        if pair_target_ports and not self.storage_info['pair_ports']:
            msg = self.output_log(
                MSG.RESOURCE_NOT_FOUND, resource="Pair target ports")
            self.raise_error(msg)
        self.output_log(MSG.SET_CONFIG_VALUE, object='pair_target_ports',
                        value=self.storage_info['pair_ports'])
        self.output_log(MSG.SET_CONFIG_VALUE, object='port-wwn list',
                        value=self.storage_info['wwns'])

    def check_param(self):
        """Check parameter values and consistency among them."""
        super(HBSDRESTFC, self).check_param()
        self.check_opts(self.conf, FC_VOLUME_OPTS)

    def create_target_to_storage(self, port, connector, hba_ids):
        """Create a host group on the specified port."""
        target_name = self.create_target_name(connector)
        try:
            body = {'portId': port,
                    'hostGroupName': target_name}
            gid = self.client.add_host_grp(body, no_log=True)
        except Exception:
            params = {'portId': port}
            host_grp_list = self.client.get_host_grps(params)
            for host_grp_data in host_grp_list:
                if host_grp_data['hostGroupName'] == target_name:
                    return target_name, host_grp_data['hostGroupNumber']
            raise
        return target_name, gid

    def set_hba_ids(self, port, gid, hba_ids):
        """Connect all specified HBAs with the specified port."""
        registered_wwns = []
        for wwn in hba_ids:
            try:
                self.client.add_hba_wwn(port, gid, wwn, no_log=True)
                registered_wwns.append(wwn)
            except exception.VolumeDriverException as ex:
                self.output_log(MSG.ADD_HBA_WWN_FAILED, port=port, gid=gid,
                                wwn=wwn)
                if (self.get_port_scheduler_param() and
                        utils.safe_get_err_code(ex.kwargs.get('errobj'))
                        == rest_api.EXCEED_WWN_MAX):
                    raise ex
        if not registered_wwns:
            msg = self.output_log(MSG.NO_HBA_WWN_ADDED_TO_HOST_GRP, port=port,
                                  gid=gid)
            self.raise_error(msg)

    def set_target_mode(self, port, gid):
        """Configure the host group to meet the environment."""
        body = {'hostMode': 'LINUX/IRIX'}
        if self.conf.hitachi_rest_disable_io_wait:
            body['hostModeOptions'] = [_FC_HMO_DISABLE_IO]
        if self.conf.hitachi_host_mode_options:
            if 'hostModeOptions' not in body:
                body['hostModeOptions'] = []
            for opt in self.conf.hitachi_host_mode_options:
                if int(opt) not in body['hostModeOptions']:
                    body['hostModeOptions'].append(int(opt))
        self.client.modify_host_grp(port, gid, body, ignore_all_errors=True)

    def find_targets_from_storage(
            self, targets, connector, target_ports):
        """Find mapped ports, memorize them and return unmapped port count."""
        wwpns = self.get_hba_ids_from_connector(connector)
        target_names = [self.create_target_name(connector)]
        if 'ip' in connector:
            target_names.append(
                '%(prefix)s-%(ip)s' % {
                    'prefix': self.driver_info['driver_prefix'],
                    'ip': connector['ip'],
                }
            )

        def _worker(port, wwpns, target_names):
            try:
                groupAndMeta = self.connector_searcher.find(port, wwpns,
                                                            target_names)
                if groupAndMeta is not None:
                    group, meta = groupAndMeta
                    return (port, group, meta)
                return None
            except Exception as ex:
                return ex

        futures = []
        for port in target_ports:
            targets['info'][port] = False
            future = self.request_thread_pool_executor.submit(
                _worker, port, wwpns, target_names)
            futures.append(future)

        not_found_count = 0
        appended_set = set()
        for future in futures:
            result = future.result()
            if result is None:
                not_found_count += 1
            elif isinstance(result, Exception):
                raise result
            else:
                port, group, _ = result
                targets['info'][port] = True
                if (port, group) not in appended_set:
                    targets['list'].append((port, group))
                    appended_set.add((port, group))

        if self.get_port_scheduler_param():
            """
            When port scheduler feature is enabled,
            it is OK to find any mapped port. so:
            - return 0, if any mapped port is found
            - return port count, if no mapped port is found.
            It is no case with both not_found_count and len(target_ports) are
            zero, bcz it must be failed in param checker if any target ports
            are not defined.
            """
            return (not_found_count if not_found_count == len(target_ports)
                    else 0)

        return not_found_count

    def initialize_connection(
            self, volume, connector, is_snapshot=False, lun=None,
            is_mirror=False):
        """Initialize connection between the server and the volume."""
        conn_info, map_info = super(HBSDRESTFC, self).initialize_connection(
            volume, connector, is_snapshot, lun)
        if self.conf.hitachi_zoning_request:
            if (self.get_port_scheduler_param() and
                    not self.is_controller(connector)):
                init_targ_map = map_info
            else:
                init_targ_map = utils.build_initiator_target_map(
                    connector, conn_info['data']['target_wwn'],
                    self._lookup_service)
            if init_targ_map:
                conn_info['data']['initiator_target_map'] = init_targ_map
            if not is_mirror:
                fczm_utils.add_fc_zone(conn_info)
        return conn_info

    def terminate_connection(self, volume, connector, is_mirror=False):
        """Terminate connection between the server and the volume."""
        conn_info = super(HBSDRESTFC, self).terminate_connection(
            volume, connector)
        if self.conf.hitachi_zoning_request:
            if conn_info and conn_info['data']['target_wwn']:
                init_targ_map = utils.build_initiator_target_map(
                    connector, conn_info['data']['target_wwn'],
                    self._lookup_service)
                if init_targ_map:
                    conn_info['data']['initiator_target_map'] = init_targ_map
            if not is_mirror:
                fczm_utils.remove_fc_zone(conn_info)
        return conn_info

    def _get_wwpns(self, port, hostgroup):
        """Get WWPN from a port and the host group."""
        wwpns = []
        hba_wwns = self.client.get_hba_wwns_by_name(port, hostgroup)
        for hba_wwn in hba_wwns:
            wwpns.append(hba_wwn['hostWwn'])
        return wwpns

    def set_terminate_target(self, fake_connector, port_hostgroup_map):
        """Set necessary information in connector in terminate."""
        wwpns = set()
        for port, hostgroups in port_hostgroup_map.items():
            for hostgroup in hostgroups:
                wwpns.update(self._get_wwpns(port, hostgroup))
        fake_connector['wwpns'] = list(wwpns)

    def set_device_map(self, targets, hba_ids, volume):
        active_hba_ids = []
        target_wwns = []
        active_target_wwns = []
        vol_id = volume['id'] if volume and 'id' in volume.keys() else ""

        if not self.get_port_scheduler_param():
            return None, hba_ids

        for port in targets['info'].keys():
            target_wwns.append(self.storage_info['wwns'][port])

        devmap = self._lookup_service.get_device_mapping_from_network(
            hba_ids, target_wwns)

        for fabric_name in devmap.keys():
            active_hba_ids.extend(
                devmap[fabric_name]['initiator_port_wwn_list'])
            active_target_wwns.extend(
                devmap[fabric_name]['target_port_wwn_list'])

        active_hba_ids = list(set(active_hba_ids))
        if not active_hba_ids:
            msg = self.output_log(MSG.NO_ACTIVE_WWN, wwn=', '.join(hba_ids),
                                  volume=vol_id)
            self.raise_error(msg)

        active_target_wwns = list(set(active_target_wwns))
        if not active_target_wwns:
            port_wwns = ""
            for port in targets['info'].keys():
                if port_wwns:
                    port_wwns += ", "
                port_wwns += ("port, WWN: " + port +
                              ", " + self.storage_info['wwns'][port])
            msg = self.output_log(
                MSG.NO_PORT_WITH_ACTIVE_WWN, port_wwns=port_wwns,
                volume=vol_id)
            self.raise_error(msg)

        return devmap, active_hba_ids

    def build_wwpn_groups(self, wwpns, connector):
        count = 1
        return ([wwpns[i:i + count] for i in range(0, len(wwpns), count)])

    def _create_target_to_any_port(
            self, targets, ports, connector, hba_ids, fabric_name):
        for port in ports:
            index = self.get_port_index_to_be_used(ports, fabric_name)
            try:
                self.create_target(
                    targets, ports[index], connector, hba_ids)
                return
            except exception.VolumeDriverException as ex:
                if ((utils.safe_get_message_id(ex.kwargs.get('errobj'))
                        == rest_api.MSGID_SPECIFIED_OBJECT_DOES_NOT_EXIST)
                    or (_MSG_EXCEED_HOST_GROUP_MAX
                        in utils.safe_get_message(ex.kwargs.get('errobj')))):
                    self.output_log(
                        MSG.HOST_GROUP_NUMBER_IS_MAXIMUM, port=ports[index])
                elif (utils.safe_get_err_code(ex.kwargs.get('errobj'))
                        == rest_api.EXCEED_WWN_MAX):
                    self.output_log(
                        MSG.WWN_NUMBER_IS_MAXIMUM, port=ports[index],
                        wwn=", ". join(hba_ids))
                else:
                    raise ex

        msg = self.output_log(
            MSG.HOST_GROUP_OR_WWN_IS_NOT_AVAILABLE, ports=', '.join(ports))
        self.raise_error(msg)

    def create_target_by_port_scheduler(
            self, devmap, targets, connector, volume):
        available_ports = []
        active_ports = []

        if not devmap:
            msg = self.output_log(MSG.ZONE_MANAGER_IS_NOT_AVAILABLE)
            self.raise_error(msg)
        for fabric_name in devmap.keys():
            available_ports = []
            active_ports = []
            active_initiator_wwns = devmap[
                fabric_name]['initiator_port_wwn_list']
            wwpn_groups = self.build_wwpn_groups(
                active_initiator_wwns, connector)
            for port, wwn in self.storage_info['wwns'].items():
                if wwn in devmap[fabric_name]['target_port_wwn_list']:
                    available_ports.append(port)
            target_ports = self.get_target_ports(connector)
            filter_ports = self.filter_target_ports(target_ports, volume)
            for port in target_ports:
                if port in available_ports and port in filter_ports:
                    active_ports.append(port)
                elif port not in available_ports and port in filter_ports:
                    self.output_log(
                        MSG.INVALID_PORT_BY_ZONE_MANAGER, port=port)
            for wwpns in wwpn_groups:
                try:
                    self._create_target_to_any_port(
                        targets, active_ports, connector, wwpns, fabric_name)
                except exception.VolumeDriverException:
                    with excutils.save_and_reraise_exception():
                        self.clean_mapping_targets(targets)

    def set_target_map_info(self, targets, hba_ids, port):
        for hba_id in hba_ids:
            target_map = {hba_id: [self.storage_info['wwns'][port]]}
            targets['target_map'].update(target_map)
