# 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 iSCSI module for Hitachi HBSD Driver."""

from oslo_log import log as logging

from cinder.volume.drivers.hitachi import hbsd_rest as rest
from cinder.volume.drivers.hitachi import hbsd_utils as utils

_ISCSI_HMO_REPORT_FULL_PORTAL = 83
_ISCSI_HMO_DISABLE_IO = 91

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


class HBSDRESTISCSI(rest.HBSDREST):
    """REST interface iscsi class for Hitachi HBSD Driver."""

    def _set_target_portal(self, port):
        """Get port info and store it in an instance variable."""
        result = self.client.get_port(port)
        ipv4_addr = result.get('ipv4Address')
        tcp_port = result.get('tcpPort')
        if not ipv4_addr or not tcp_port:
            return False, ipv4_addr, tcp_port
        self.storage_info['portals'][port] = '%(ip)s:%(port)s' % {
            'ip': ipv4_addr,
            'port': tcp_port,
        }
        return True, ipv4_addr, tcp_port

    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_iscsis = self.client.get_hba_iscsis(port, groupNum)
            if hba_iscsis:
                targets = [hba_iscsi['iscsiName'] for hba_iscsi in hba_iscsis]
            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_iscsis = self.client.get_hba_iscsis_by_name(port, group)
            if hba_iscsis:
                gid = hba_iscsis[0]['hostGroupNumber']
                targets = [hba_iscsi['iscsiName'] for hba_iscsi in hba_iscsis]
                storage_iqn = self.client.get_host_grp(port, gid)['iscsiName']
                return (gid, {'iscsiName': storage_iqn}), 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'],
                    {'iscsiName': data['iscsiName']}) 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 = []

        super(HBSDRESTISCSI, self).connect_storage()
        # The port type must be ISCSI and the port attributes must contain TAR.
        params = {'portType': 'ISCSI',
                  '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='(portType, '
                    'portAttributes): not (ISCSI, 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
            has_addr = True
            if not port_data['lunSecuritySetting']:
                addr_info = ""
            elif port in set(target_ports + compute_target_ports):
                has_addr, ipv4_addr, tcp_port = self._set_target_portal(port)
                if not has_addr:
                    addr_info = (', ipv4Address: %s, tcpPort: %s' %
                                 (ipv4_addr, tcp_port))
            if not port_data['lunSecuritySetting'] or not has_addr:
                self.output_log(
                    MSG.INVALID_PORT, port=port,
                    additional_info='portType: %s, lunSecuritySetting: %s%s' %
                    (port_data['portType'], port_data['lunSecuritySetting'],
                     addr_info))
            if not port_data['lunSecuritySetting']:
                continue
            if target_ports and port in target_ports and has_addr:
                self.storage_info['controller_ports'].append(port)
            if (compute_target_ports and port in compute_target_ports and
                    has_addr):
                self.storage_info['compute_ports'].append(port)
            if pair_target_ports and port in pair_target_ports:
                self.storage_info['pair_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-<IP address:port> list',
                        value=self.storage_info['portals'])

    def create_target_to_storage(self, port, connector, hba_ids):
        """Create an iSCSI target on the specified port."""
        target_name = self.create_target_name(connector)
        body = {'portId': port, 'hostGroupName': target_name}
        if hba_ids:
            body['iscsiName'] = '%(id)s%(suffix)s' % {
                'id': hba_ids,
                'suffix': self.driver_info['target_iqn_suffix'],
            }
        try:
            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']
            else:
                raise
        return target_name, gid

    def set_hba_ids(self, port, gid, hba_ids):
        """Connect the specified HBA with the specified port."""
        self.client.add_hba_iscsi(port, gid, hba_ids)

    def set_target_mode(self, port, gid):
        """Configure the iSCSI target to meet the environment."""
        body = {'hostMode': 'LINUX/IRIX',
                'hostModeOptions': [_ISCSI_HMO_REPORT_FULL_PORTAL]}
        if self.conf.hitachi_rest_disable_io_wait:
            body['hostModeOptions'].append(_ISCSI_HMO_DISABLE_IO)
        if self.conf.hitachi_host_mode_options:
            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)

    def find_targets_from_storage(self, targets, connector, target_ports):

        """Find mapped ports, memorize them and return unmapped port count."""

        iqn = self.get_hba_ids_from_connector(connector)
        target_names = []
        if 'ip' in connector:
            target_names.append(self.create_target_name(connector))

        def _worker(port, iqn, target_names):
            try:
                groupAndMeta = self.connector_searcher.find(port, [iqn],
                                                            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, iqn, 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, meta = result
                storage_iqn = meta['iscsiName']
                targets['info'][port] = True
                if (port, group) not in appended_set:
                    targets['list'].append((port, group))
                    appended_set.add((port, group))
                targets['iqns'][(port, group)] = storage_iqn

        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(HBSDRESTISCSI, self).initialize_connection(
            volume, connector, is_snapshot, lun)
        return conn_info

    def terminate_connection(self, volume, connector, is_mirror=False):
        """Terminate connection between the server and the volume."""
        return super(HBSDRESTISCSI, self).terminate_connection(
            volume, connector)

    def get_properties_iscsi(self, targets, multipath):
        """Return iSCSI-specific server-LDEV connection info."""
        if not multipath:
            target_list = targets['list'][:1]
        else:
            target_list = targets['list'][:]

        for target in target_list:
            if target not in targets['iqns']:
                port, gid = target
                target_info = self.client.get_host_grp(port, gid)
                iqn = target_info.get('iscsiName') if target_info else None
                if not iqn:
                    msg = self.output_log(MSG.RESOURCE_NOT_FOUND,
                                          resource='Target IQN')
                    self.raise_error(msg)
                targets['iqns'][target] = iqn
                LOG.debug(
                    'Found target iqn of host group. (port: %(port)s, '
                    'gid: %(gid)s, target iqn: %(iqn)s)',
                    {'port': port, 'gid': gid, 'iqn': iqn})
        return super(HBSDRESTISCSI, self).get_properties_iscsi(
            targets, multipath)

    def _get_iqn(self, port, hostgroup):
        """Get IQN from a port and the ISCSI target."""
        hba_iscsis = self.client.get_hba_iscsis_by_name(port, hostgroup)
        return hba_iscsis[0]['iscsiName']

    def set_terminate_target(self, fake_connector, port_hostgroup_map):
        """Set necessary information in connector in terminate."""
        for port, hostgroups in port_hostgroup_map.items():
            for hostgroup in hostgroups:
                iqn = self._get_iqn(port, hostgroup)
                if iqn:
                    fake_connector['initiator'] = iqn
                    return
