# -*- coding: utf-8 -*-
# Copyright (c) Huawei Technologies Co., Ltd. 2022-2023. All rights reserved.
import traceback
from collections import defaultdict

from utils.common.fic_base import TestCase
import utils.common.log as logger
from utils.common.message import Message
from utils.common.exception import FCDException

from plugins.DistributedStorage.scripts.logic.DeployOperate import DeployOperate
from plugins.DistributedStorage.scripts.utils.common.DeployConstant import DeployConstant


class CreateCluster(TestCase):
    def __init__(self, project_id, pod_id, fs_args, **kwargs):
        super(CreateCluster, self).__init__(project_id, pod_id)
        self.opr = DeployOperate(fs_args)
        self.update_pwd = fs_args['dm_update_pwd']
        self.osd_list = fs_args.get('osd_list')
        self.first_metadata_node = None
        self.first_metadata_type = None
        self.first_metadata_size = None
        self.all_flash = False
        self.more_args = kwargs

    @staticmethod
    def get_node_disk_team_by_capacity(zk_node_disk_infos):
        """
        将所有zk节点的ssd_card磁盘按容量分组；如果某一容量的盘有节点不存在，过滤掉这一容量的磁盘
        useful_teams example:
            {capacity1: defaultdict({'ip1': [disk1], 'ip2': [disk1, disk2], 'ip3': [disk1],...}),
             capacity2: defaultdict({'ip1': [disk2, disk3, disk4, disk5], 'ip2': [disk2, disk3, disk4, disk5],
                                     'ip3': [disk2, disk3, disk4, disk5],...}),
             ...
            }
        :param zk_node_disk_infos:
        :return: dict
        """
        logger.info('Start grouping the disks')
        capacity_team_infos = dict()
        not_exist_nvme_node = dict()
        for ip, disk_infos in zk_node_disk_infos.items():
            not_exist_nvme_flag = True
            for disk_info in disk_infos:
                if disk_info.get('mediaRole') != 'no_use' or disk_info.get('mediaType') != 'ssd_card':
                    continue
                not_exist_nvme_flag = False
                cur_capacity = disk_info.get('mediaSize')
                if not capacity_team_infos.get(cur_capacity):
                    capacity_team_infos[cur_capacity] = defaultdict(list)
                try:
                    capacity_team_infos[cur_capacity][ip].append(disk_info)
                except KeyError as e:
                    logger.error('Key not exist. Detail:%s' % str(e))
                    raise e
            if not_exist_nvme_flag:
                not_exist_nvme_node[ip] = disk_infos
        if not_exist_nvme_node:
            logger.error('The following nodes do not have available nvme disks:{}'.format(not_exist_nvme_node))
            raise FCDException(626349, list(not_exist_nvme_node.keys()), not_exist_nvme_node)
        # 过滤节点缺少某一容量的磁盘
        useful_teams = dict(filter(lambda x: len(x[1]) == len(zk_node_disk_infos), capacity_team_infos.items()))
        logger.info('All available nvme disks on the nodes:{}'.format(useful_teams))
        return useful_teams

    @staticmethod
    def check_zk_disk_info_by_slot(zk_node_disk_infos, zk_ip_slot, zk_disk_type):
        """
        根据槽位号获取节点元数据磁盘信息

        :param zk_node_disk_infos:
        :param zk_ip_slot:
        :param zk_disk_type:
        :return: dict
        """
        zk_disk = dict()
        zk_disk_capacity = set()
        not_exist_disk_node = dict()
        for node_ip, slot in zk_ip_slot.items():
            cur_node_disk_infos = zk_node_disk_infos.get(node_ip)
            logger.info("All disk on node {}: {}".format(node_ip, cur_node_disk_infos))
            if not cur_node_disk_infos:
                logger.error('Failed to obtain the disks of node:{}'.format(node_ip))
                not_exist_disk_node[node_ip] = cur_node_disk_infos
                continue
            cur_slot_disk_info = None
            for disk_info in cur_node_disk_infos:
                disk_role = disk_info.get('mediaRole')
                disk_slot = disk_info.get('phySlotId')
                disk_type = disk_info.get('mediaType')
                if disk_slot == int(slot) and disk_role == 'no_use' and disk_type == zk_disk_type:
                    cur_slot_disk_info = disk_info
                    zk_disk_capacity.add(disk_info.get('mediaSize'))
                    break
            if cur_slot_disk_info is None:
                logger.error('No disk available for this node:{}, disk info:{}'.format(node_ip, cur_node_disk_infos))
                not_exist_disk_node[node_ip] = cur_node_disk_infos
                continue
            zk_disk[node_ip] = cur_slot_disk_info
        if not_exist_disk_node:
            logger.error('The following nodes do not have available {}:{}'.format(zk_disk_type, not_exist_disk_node))
            raise FCDException(626351, list(not_exist_disk_node.keys()), zk_disk_type, not_exist_disk_node)
        if len(zk_disk_capacity) > 1:
            logger.error('The capacity of the metadata disk is different:{}'.format(zk_disk))
            raise FCDException(626350, list(zk_disk.keys()), zk_disk)
        logger.info('Disk info of zk cluster:{}'.format(zk_disk))

    def procedure(self):
        """
        一、检查LLD表参数, 从LLD表中，获取zk节点IP、盘类型及zk槽位号
        二、获取zk磁盘参数：server list
            1、调用rest接口查询出节点所有的zk盘类型磁盘信息
            2、根据盘类型，获取指定的磁盘信息：
              ssd_card:
                选取所有节点上都存在，容量一致、数量最少的盘作为zk盘
              非ssd_card:
                根据槽位，获取磁盘信息，检查磁盘容量一致性
        三、创控制集群
        :return:
        """
        logger.info("Start to create cluster.")
        try:
            self.opr.login(DeployConstant.DM_LOGIN_USER, self.update_pwd)
        except FCDException as e:
            logger.error(traceback.format_exc())
            return Message(500, e)
        except Exception as e:
            return Message(500, FCDException(626002, str(e)))

        logger.info("Check lld params and get metadata nodes info.")
        try:
            zk_disk_type, zk_ip_slot = self.get_cluster_lld_params()
        except FCDException as e:
            logger.error(traceback.format_exc())
            return Message(500, e)
        except Exception as e:
            return Message(500, FCDException(626002, str(e)))

        logger.error('Get cluster parameters.')
        try:
            server_info = self.get_zk_server_info(zk_disk_type, zk_ip_slot)
        except FCDException as e:
            logger.error(traceback.format_exc())
            return Message(500, e)
        except Exception as e:
            return Message(500, FCDException(626002, str(e)))

        logger.info('Creating cluster.')
        try:
            self.creating_cluster(server_info)
        except FCDException as e:
            logger.error(traceback.format_exc())
            return Message(500, e)
        except Exception as e:
            return Message(500, FCDException(626002, str(e)))

        logger.info('Create cluster successful')
        self.opr.login_out(DeployConstant.DM_LOGIN_USER, self.update_pwd)
        return Message()

    def creating_cluster(self, server_info):
        res = self.opr.create_cluster('cluster01', server_info)
        result, res_detail = res.get_query_detail()
        if result != 0:
            err_msg = "Failed to Create Cluster, Result:%s, Detail:%s" % (result, res_detail)
            logger.error(err_msg)
            raise FCDException(626008, res_detail)

    def get_cluster_lld_params(self):
        """
        获取LLD表填写的zk盘类型、槽位号，和节点管理IP
        :return:
        """
        zk_disk_types = set()
        zk_ip_slot = dict()
        node_infos = self.db.get_install_os_list_info(pod_id=self.pod_id)
        for node_info in node_infos:
            if node_info.get('is_metadata_node') == '1':
                zk_disk_types.add(node_info.get('zk_type'))
                zk_ip_slot[node_info.get('manageIp')] = node_info.get('zk_slot')
        if len(zk_disk_types) != 1:
            logger.error('The disk type of metadata nodes is incorrect in the LLD, details: %s.' % zk_disk_types)
            raise FCDException(626349, DeployConstant.BASIC_SERVICE_METADATA_SUPPORT_DISK_TYPE)
        zk_disk_type = zk_disk_types.pop()
        if zk_disk_type not in DeployConstant.BASIC_SERVICE_METADATA_SUPPORT_DISK_TYPE:
            logger.error('The disk type of metadata nodes is incorrect in the LLD, details: %s.' % zk_disk_types)
            raise FCDException(626349, DeployConstant.BASIC_SERVICE_METADATA_SUPPORT_DISK_TYPE)
        if len(zk_ip_slot) not in [3, 5, 7]:
            logger.error('the number of metadata nodes is required to be 3 or 5 or 7. '
                         'Current quantity:{}'.format(len(zk_ip_slot)))
            raise FCDException(626206, len(zk_ip_slot))
        product_type_name = DeployConstant.STORAGE_TYPE_MAP.get(zk_disk_type)
        return product_type_name, zk_ip_slot

    def get_zk_server_info(self, zk_disk_type, zk_ip_slot):
        res = self.opr.scan_server_media(list(zk_ip_slot.keys()))
        all_node_disk_infos = res.get_query_data().get('servers')
        if not all_node_disk_infos:
            logger.error('The disk information is empty, details:{}'.format(res.get_query_data()))
            raise FCDException(626346, list(zk_ip_slot.keys()))
        logger.info('All disk info of zk nodes:{}'.format(all_node_disk_infos))
        server_info = list()
        if zk_disk_type == 'ssd_card':
            logger.info('Start to parse disk info and obtain the zkDiskEsn.')
            zk_disk_info = self.get_zk_disk_info_when_nvme_card(all_node_disk_infos)
            for node_ip, _ in zk_ip_slot.items():
                zk_disk_esn = zk_disk_info.get(node_ip).get('phyDevEsn')
                pyh_slot = zk_disk_info.get(node_ip).get('phySlotId')
                server = {'nodeMgrIp': node_ip, 'zkType': 'ssd_card', 'zkDiskSlot': pyh_slot, 'zkDiskEsn': zk_disk_esn}
                server_info.append(server)
        else:
            logger.info('Start to check disk status, disk type and disk capacity by slot.')
            self.check_zk_disk_info_by_slot(all_node_disk_infos, zk_ip_slot, zk_disk_type)
            server_info = [{'nodeMgrIp': node_ip, 'zkType': zk_disk_type, 'zkDiskSlot': slot}
                           for node_ip, slot in zk_ip_slot.items()]
        logger.info('Server info:{}'.format(server_info))
        return server_info

    def get_zk_disk_info_when_nvme_card(self, zk_node_disk_infos):
        """
        nvme无法根据槽位获取磁盘设备名称，取盘原则：
            1、获取元数据节点上所有状态为[no_use]的nvme盘
            2、根据容量的对盘进行分组：capacity_team_infos
            3、当某一个容量capacityX的盘，有节点不存在时，把这一容量分组过滤掉：useful_teams
            4、对各容量分组下，每一个节点的盘信息列表按槽位号升序排列，取第一个节点的最小槽位号和盘数量
            5、按标志节点的盘数量，升序排列：ordered_capacity_team_infos
            6、最后取数量最少的盘的最小槽位号，作为控制集群盘。
        ordered_capacity_team_infos example:
            {capacity1: defaultdict({'flag_node': ['ip1', 2], 'ip1': [disk1], 'ip2': [disk1, disk2],
                                     'ip3': [disk1],...}),
             capacity2: defaultdict({'flag_node': ['ip1', 4], 'ip1': [disk2, disk3, disk4, disk5],
                                     'ip2': [disk2, disk3, disk4, disk5], 'ip3': [disk2, disk3, disk4, disk5],...}),...
            }
        diskX example:
            {'devName': 'nvme0n1',
            'devId': 'major: 259;first_minor: 1',
            'phyDevEsn': '***',
            'phyDevModel': '***',
            'phySlotId': 44,
            'mediaSize': 1800,
            'mediaType': 'ssd_card',
            'usedCacheSize': 0,
            'vendor': 13,
            'mediaRole': 'no_use',
            'poolIds': [],
            'health': 0,
            'mediaCapacityForByte': 1800000000000,
            'supportEncrypt': 0,
            'sectorSize': 512,
            'logicSectorSize': 512,
            'controllerId': 0,
            'metadataSize': 0,
            'slotDesc': '44',
            'vNodeId': 0}

        :param zk_node_disk_infos: dict
        :return: dict
        """
        useful_teams = self.get_node_disk_team_by_capacity(zk_node_disk_infos)
        logger.info('Start to select disk from the available nvme disks')
        for _, same_capacity_team in useful_teams.items():
            flag_node = [None, None]
            for node_ip, same_capacity_on_each_node in same_capacity_team.items():
                same_capacity_on_each_node.sort(key=lambda x: x.get('phySlotId'))
                if flag_node[0] is None:
                    flag_node[0], flag_node[1] = node_ip, len(same_capacity_on_each_node)
            same_capacity_team['flag_node'].extend(flag_node)
        # 按标记节点的磁盘数量升序排列
        ordered_capacity_team_infos = sorted(useful_teams.items(), key=lambda x: x[1].get('flag_node')[1])
        logger.info('Available nvme disk information sorted by quantity: {}'.format(ordered_capacity_team_infos))
        # 取盘数量最少的，作为zk盘
        zk_disk_capacity, disk_infos = ordered_capacity_team_infos[0]
        disk_infos.pop('flag_node')
        select_zk_disk = dict((node_ip, disk_lst[0]) for node_ip, disk_lst in disk_infos.items())
        logger.info('Selected disk info: {}, disk capacity: {}'.format(select_zk_disk, zk_disk_capacity))
        return select_zk_disk
