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

import utils.common.log as logger
from utils.common.fic_base import TestCase
from utils.common.exception import FCDException
from platforms.project.ProjectUtils import get_project_condition_boolean

from plugins.DistributedStorage.scripts.logic.DeployOperate import DeployOperate
from plugins.DistributedStorage.scripts.logic.InstallOperate import InstallOperate
from plugins.DistributedStorage.scripts.utils.config.device_check import DeviceResource
from plugins.DistributedStorage.scripts.utils.common.DeployConstant import DeployConstant
from plugins.DistributedStorage.scripts.logic.error_msg_handle import PoolInfoCheck, PoolLLDParamsCheck


class ExpandPool(TestCase):
    def __init__(self, project_id, pod_id, fs_args, **kwargs):
        super(ExpandPool, self).__init__(project_id, pod_id)
        self.fs_args = fs_args
        self.operate = DeployOperate(self.fs_args)
        self.install_operate = InstallOperate(project_id, pod_id, fs_args)
        self.device_res = DeviceResource()
        self.fsa_list = self.fs_args.get('fsa_list')
        self.osd_list = self.fs_args.get('osd_list')
        self.update_pwd = self.fs_args.get('dm_update_pwd')
        self.root_pwd = self.fs_args.get('fsm_root_pwd')
        self.main_storage_type = None
        self.cache_type = None
        self.more_args = kwargs
        self.pool_info_check = None

    @staticmethod
    def get_pool_info_by_pool_name(pool_info, pool_info_list, pool_name):
        for pool in pool_info_list:
            if pool.get("poolName") == pool_name:
                pool_info = pool
                break
        return pool_info

    def procedure(self):
        logger.info("Start to expand pool.")
        ex_osd_list = self.fs_args.get('ex_osd_list')
        if len(ex_osd_list) == 0:
            logger.info('There is no osd node for add to pool, return directly')
            return
        self.operate.login(DeployConstant.DM_LOGIN_USER, self.fs_args.get('dm_update_pwd'))
        # 扩容复用场景，先检查FSM沙箱状态，若是关闭，不对FSM节点沙箱状态做任何操作；若是开启的，扩池前关闭，扩池后恢复为开启。
        is_enabled = self.is_enabled_on_fsm_nodes()
        if is_enabled:
            self.install_operate.disable_fsm_sandbox(self.operate, self.root_pwd, self.update_pwd)
        try:
            self.install_operate.config_disk_type_switch(support=True)
            logger.info('Get pool ID')
            pool_id = self.get_pool_id(ex_osd_list)
            logger.info('Succeed to get poolId: %s' % pool_id)

            logger.info('Get data of storage pool')
            pool_config_args = self.get_pool_config_info(ex_osd_list, pool_id)

            logger.info('Expanding pool')
            task_id = self.expand_pool(pool_config_args)
            logger.info('start to expand pool[%s]' % pool_config_args.get('pool_name'))

            logger.info('Query the process of expanding pool')
            self.query_expanding_pool_process(task_id)

            logger.info('Expand pool successful')
        finally:
            if is_enabled:
                self.install_operate.enable_fsm_sandbox(self.operate)
            self.operate.logout()

    def is_enabled_on_fsm_nodes(self):
        is_enabled = False
        rsp_obj = self.operate.query_cluster_servers()
        rsp_code, rsp_result, rsp_data = rsp_obj.get_rsp_data()
        if rsp_code != 0 or rsp_result.get('code') != 0:
            err_msg = "Failed to query cluster nodes info, Error:{}".format(rsp_result)
            logger.error(err_msg)
            raise Exception(err_msg)
        fsm_nodes_status = {}
        for server in rsp_data:
            node_role = server.get('role')
            if 'management' not in node_role:
                continue
            sandbox_status = server.get('sandbox_status')
            node_ip = server.get('management_ip')
            fsm_nodes_status[node_ip] = sandbox_status
            logger.info("FSM node[{}] sandbox status:{}".format(node_ip, sandbox_status))
            if sandbox_status == 0:
                is_enabled = True
                logger.info('management node [%s] sandbox is enable.' % node_ip)
        if len(set(fsm_nodes_status.values())) != 1:
            err_msg = "The sandbox status of the active and standby FSM nodes is different:{}".format(fsm_nodes_status)
            logger.error(err_msg)
            raise Exception(err_msg)
        logger.info("Sandbox status on FSM nodes:{}".format(is_enabled))
        return is_enabled

    def get_pool_id(self, ex_osd_list):
        res_pool = self.operate.query_storage_pool()
        pool_data = res_pool.get_query_data()
        pool_info_list = pool_data.get('storagePools')
        if len(pool_info_list) == 0:
            logger.error('Check pool fail...')
            raise FCDException(626078, str(pool_data))
        # 获取目标存储池info
        pool_name = ex_osd_list[0].get("storage_pool_name_and_slot")
        pool_info = {}
        if pool_name:
            logger.info("The name of the storage pool to be expanded is %s." % pool_name)
            pool_info = self.get_pool_info_by_pool_name(pool_info, pool_info_list, pool_name)
        else:
            pool_info = self.get_pool_info_by_default_pool_name(pool_info, pool_info_list)
        if not pool_info:
            err_msg = "No storage pool named %s is found in the cluster." % pool_name
            logger.error(err_msg)
            raise Exception(err_msg)
        pool_id = pool_info.get('poolId')
        self.main_storage_type = pool_info.get('mediaType')
        self.cache_type = pool_info.get('cacheMediaType')
        return pool_id

    def get_pool_info_by_default_pool_name(self, pool_info, pool_info_list):
        if self.fs_args.get('default_pool_name'):
            for pool in pool_info_list:
                if pool.get('poolName') == self.fs_args.get('default_pool_name'):
                    pool_info = pool
                    break
        else:
            err_msg = "No storage pool name is available in the service storage scenario."
            logger.error(err_msg)
            raise Exception(err_msg)
        return pool_info

    def get_pool_config_info(self, ex_osd_list, pool_id):
        """
        :param ex_osd_list:
        :param pool_id:
        :return: pool_config_args
        {
           存储池ID poolId: pool_id
           盘类型mediaType: ssd_disk
           缓存类型cacheType: none
           服务器列表servers: [{server_dict}, {server_dict}, ...]
          }
        """
        # 初始化存储池信息校验字典
        self.pool_info_check = PoolInfoCheck.pool_info_check_dict_init()
        is_new_business_node = get_project_condition_boolean(self.project_id, 'TenantStorNewNode')
        logger.info('Start to query pool info, is_new_business_node:{}'.format(is_new_business_node))
        all_nodes_disks = self.get_cluster_disk_info()
        if is_new_business_node:
            pool_config_args = self.get_expand_pool_args_for_new_node(ex_osd_list, all_nodes_disks, pool_id)
        else:
            # 扩管理节点入口
            pool_config_args = self.get_expand_pool_args_for_manager(ex_osd_list, all_nodes_disks, pool_id)
        # 统一校验存储池参数包括主存和缓存类型、容量、盘数量（不小于4）校验
        PoolInfoCheck.pool_info_error_check(self.pool_info_check)
        return pool_config_args

    def get_cluster_disk_info(self):
        all_nodes_disks = self.operate.query_all_disk()
        all_disk_data = all_nodes_disks.get_query_data()
        disk_info = all_disk_data.get('disks')
        if not disk_info:
            err_msg = 'No disk information exists.'
            logger.error(err_msg)
            raise FCDException(626074, all_disk_data)
        return all_nodes_disks

    def get_expand_pool_args_for_new_node(self, ex_osd_list, all_nodes_disks, pool_id):
        logger.info('Start to query pool args')
        storage_type = self.get_lld_main_and_cache_type(ex_osd_list, pool_id)
        logger.info('storage type:{}'.format(storage_type))
        server_list = []
        for osd_info in ex_osd_list:
            server_dict = self.get_server_info(osd_info, all_nodes_disks, storage_type, pool_id)
            server_list.append(server_dict)
        logger.info('Server list:{}'.format(server_list))
        pool_config_args = {'poolId': pool_id,
                            'servers': server_list,
                            'mediaType': storage_type.get('mediaType'),
                            'cacheType': storage_type.get('cacheType')
                            }
        logger.info('Pool config args:{}'.format(pool_config_args))
        return pool_config_args

    def get_lld_main_and_cache_type(self, ex_osd_list, pool):
        storage_type = {'mediaType': None, 'cacheType': None}
        lld_check = PoolLLDParamsCheck()
        lld_check.init_record_pool_info(pool)
        for node in ex_osd_list:
            primary_type = node.get('primary_type')
            if not storage_type.get('mediaType'):
                storage_type['mediaType'] = DeployConstant.STORAGE_TYPE_MAP.get(primary_type)
            if not storage_type.get('cacheType'):
                storage_type['cacheType'] = node.get('cache_type')
            lld_check.pub_type_check(pool, primary_type, 'primary_type')
            lld_check.cache_type_check(pool, node)
            lld_check.primary_slot_check(pool, node)
        logger.info('Result analysis')
        lld_check.expand_business_node_check_result_handle()
        if storage_type.get('mediaType') != self.main_storage_type or storage_type.get('cacheType') != self.cache_type:
            pool_main_type_map = DeployConstant.PRODUCT_TO_LLD_MAP.get(self.main_storage_type)
            pool_storage_type = {'mediaType': pool_main_type_map, 'cacheType': self.cache_type}
            logger.error('Disk type error. disk type in the LLD:{}, '
                         'disk type in the pool:{}'.format(storage_type, pool_storage_type))
            raise FCDException(626375, storage_type, pool_storage_type)
        logger.info('Check Passed')
        logger.info('Storage type:{}'.format(storage_type))
        return storage_type

    def get_server_info(self, osd_info, all_nodes_disks, storage_type, pool):
        """
        server_dict:{'nodeMgrIp': '192.168.2.201'
                     'diskSlots': '2-3-4-5-6-7'
                     'cacheESNList': 可选 [ESN1, ESN2, ...]
                     'mediaESNList': 主存为ssd_card必选[ESN1, ESN2, ...]}
        :return:
        """
        server_dict = dict()
        cache_type = storage_type.get('cacheType')
        main_type = storage_type.get('mediaType')
        start_slot, end_slot = osd_info.get('primary_slot').split('-')
        if main_type in ['sas_disk', 'sata_disk', 'ssd_disk']:
            kwargs = {"osd_info": osd_info, "start_slot": start_slot, "end_slot": end_slot, "pool": pool}
            storage_disk_slots = self.get_main_storage_slots(all_nodes_disks, **kwargs)
        else:
            cur_disk_num = int(end_slot) - int(start_slot) + 1
            storage_disk_slots, storage_disk_esn_list = self.get_main_slots_when_all_nvme(osd_info, all_nodes_disks,
                                                                                          cur_disk_num, pool)
            server_dict["mediaESNList"] = storage_disk_esn_list
        if storage_type.get('cacheType') != 'none':
            kwargs = {"osd_info": osd_info, "cache_type": cache_type, "main_storage_type": main_type, "pool": pool}
            server_dict['cacheESNList'] = self.get_cache_disk_list(all_nodes_disks, **kwargs)
        server_dict['nodeMgrIp'] = osd_info.get('om_ip')
        server_dict['diskSlots'] = storage_disk_slots
        return server_dict

    def get_main_slots_when_all_nvme(self, osd_info, all_nodes_disks, disk_num, pool):
        slot_list = []
        storage_disk_esn_list = list()
        node_ip = osd_info.get('om_ip')
        disk_info = all_nodes_disks.get_query_data().get('disks')
        cur_node_disk_info = disk_info.get(node_ip)
        logger.info("All disk on node[%s]: %s" % (node_ip, cur_node_disk_info))
        if not cur_node_disk_info:
            err_msg = 'No disk information exists on the node:[%s]' % node_ip
            logger.error(err_msg)
            self.pool_info_check[pool]['nvme_main_storage']['no_disk_error'].append(node_ip)
            return '', storage_disk_esn_list
        capacity_team = defaultdict(list)
        for slot_disk_info in cur_node_disk_info:
            if slot_disk_info.get('devType') != 'ssd_card' or slot_disk_info.get('devRole') != 'no_use':
                continue
            capacity_team[slot_disk_info.get('devTotalCapacity')].append(slot_disk_info)
        useful_main_storage_info = list(filter(lambda x: len(x) >= 4, capacity_team.values()))
        logger.info('useful disk: {}'.format(useful_main_storage_info))
        if len(useful_main_storage_info) != 1:
            err_msg = 'This node has no available or different capacity nvme disks. node ip:{}'.format(node_ip)
            logger.error(err_msg)
            self.pool_info_check[pool]['nvme_main_storage']['disk_nums_error'].append(node_ip)
            return '', storage_disk_esn_list
        ordered_storage_info = sorted(useful_main_storage_info[0], key=lambda x: x.get('devSlot'))
        logger.info('ordered useful disk:{}'.format(ordered_storage_info))
        disk_capacity = ordered_storage_info[0]['devTotalCapacity']
        self.pool_info_check[pool]['nvme_main_storage']['disk_capacity'][disk_capacity].append(node_ip)
        for slot_disk_info in ordered_storage_info:
            slot_list.append(str(slot_disk_info.get('devSlot')))
            storage_disk_esn_list.append(slot_disk_info.get("devEsn"))
            if len(slot_list) == disk_num:
                break
        return '-'.join(slot_list), storage_disk_esn_list

    def get_expand_pool_args_for_manager(self, osd_list, all_nodes_disks, pool_id):
        logger.info('Start to query pool args')
        max_slot_range = self.manager_lld_params_check(osd_list, pool_id)
        logger.info('Max slot range:{}'.format(max_slot_range))
        # 检查戴尔服务器场景是否安装了扫盘工具
        self.install_operate.check_third_party_server_disk(all_nodes_disks, osd_list)
        server_list = list()
        for osd in osd_list:
            server = dict()
            server['nodeMgrIp'] = osd.get("om_ip")
            start_slot, end_slot = max_slot_range.split('-')
            kwargs = {"osd_info": osd, "start_slot": start_slot, "end_slot": end_slot, "pool": pool_id}
            server['diskSlots'] = self.get_main_storage_slots(all_nodes_disks, **kwargs)
            if self.cache_type != 'none':
                kwargs = {"osd_info": osd, "cache_type": self.cache_type, "main_storage_type": self.main_storage_type,
                          "pool": pool_id}
                server['cacheESNList'] = self.get_cache_disk_list(all_nodes_disks, **kwargs)
            server_list.append(server)
        logger.info('Server list:{}'.format(server_list))
        pool_config_args = {'poolId': pool_id,
                            'servers': server_list,
                            'mediaType': self.main_storage_type,
                            'cacheType': self.cache_type}
        logger.info('Pool config args:{}'.format(pool_config_args))
        return pool_config_args

    def manager_lld_params_check(self, ex_osd_list, pool):
        lld_check = PoolLLDParamsCheck()
        lld_check.init_record_pool_info(pool)
        lld_cache = None
        slot_list = []
        for node in ex_osd_list:
            slot_list.append(node.get('primary_slot'))
            cur_cache = node.get('cache_type')
            if lld_cache is None:
                lld_cache = cur_cache
            lld_check.pub_type_check(pool, cur_cache, 'cache_type')
            lld_check.primary_slot_check(pool, node)
        logger.info('Result analysis')
        lld_check.expand_manager_node_check_result_handle()
        if lld_cache != self.cache_type:
            logger.error('Cache type error. cache type in the LLD:{}, '
                         'cache type in the pool:{}'.format(lld_cache, self.cache_type))
            raise FCDException(626376, lld_cache, self.cache_type)
        logger.info('Check Passed')
        logger.info('Cache type:{}'.format(lld_cache))
        max_slot_list = self.device_res.get_storage_pool_slot(slot_list)
        return max_slot_list

    def get_main_storage_slots(self, all_nodes_disks, **kwargs):
        pool = kwargs.get("pool")
        slot_list = list()
        om_ip = kwargs.get("osd_info").get('om_ip')
        for slot in range(int(kwargs.get("start_slot")), int(kwargs.get("end_slot"))+1):
            disk_meta = all_nodes_disks.get_disks_by_node_slot(om_ip, slot)
            if not disk_meta:
                logger.info("There is no disk on slot[%s] at node[%s]" % (slot, om_ip))
                continue
            capacity = disk_meta.get('devTotalCapacity')
            self.pool_info_check[pool]["main_storage_info"]["type_info"][disk_meta.get('devType')][om_ip].append(slot)
            self.pool_info_check[pool]["main_storage_info"]["capacity_info"][capacity][om_ip].append(slot)
            slot_list.append(str(slot))
        slot_num = len(slot_list)
        self.pool_info_check[pool]["main_storage_info"]["disk_num"][slot_num][om_ip] = slot_list
        return '-'.join(slot_list)

    def get_cache_disk_list(self, all_nodes_disks, **kwargs):
        cache_type = kwargs.get("cache_type")
        main_storage_type = kwargs.get("main_storage_type")
        pool = kwargs.get("pool")
        cache_disk_esn_list = list()
        if cache_type and main_storage_type not in ['ssd_disk', 'ssd_card']:
            om_ip = kwargs.get("osd_info").get('om_ip')
            all_disks = all_nodes_disks.get_disks_by_node(om_ip)
            cache_num = 0
            max_cache_num = 4
            for cache in all_disks:
                if cache.get('devType') == cache_type and cache.get('devRole') == 'no_use':
                    cache_disk_esn_list.append(cache.get('devEsn'))
                    cache_num += 1
                    self.pool_info_check[pool]["cache_info"]["media_type"][cache.get('devType')].append(om_ip)
                    self.pool_info_check[pool]["cache_info"]["media_size"][cache.get('devTotalCapacity')].append(om_ip)
                if cache_num >= max_cache_num:
                    break
            self.pool_info_check[pool]["cache_info"]['cache_num'][len(cache_disk_esn_list)].append(om_ip)
        return cache_disk_esn_list

    def expand_pool(self, pool_config_args):
        res = self.operate.expand_pool(pool_config_args)
        status_code, result, task_id, error_code, error_des = res.get_expand_pool_code()
        if status_code != 200 or result != 0:
            logger.info("Pool args is: %s" % pool_config_args)
            err_msg = "Failed to expand pool, " \
                      "Detail:[status:%s,result:%s,error:%s]%s" % (status_code, result, error_code, error_des)
            logger.error(err_msg)
            raise Exception(err_msg)
        return task_id

    def query_expanding_pool_process(self, task_id):
        wait_time = 13800
        task_status, task_info = None, None
        while wait_time > 0:
            time.sleep(10)
            wait_time -= 10
            res_query = self.operate.query_task_info()
            task_info = res_query.get_task_by_id(task_id)
            task_status = task_info.get('taskStatus')
            entity_name = task_info.get('entityName')
            if task_status == 'success':
                logger.info("Expanding the storage pool[%s] success." % entity_name)
                break
            elif task_status == "failed":
                logger.error("Failed to expand the storage pool[%s].Deatail:%s" % (entity_name, str(task_info)))
                break
            else:
                task_name = task_info.get('taskName')
                task_progress = task_info.get('progress')
                logger.info("Creating the storage pool[taskID:%s, taskName:%s, taskObject:%s, taskStatus:%s, "
                            "taskProgress:%s]" % (task_id, task_name, entity_name, task_status, task_progress))
        if task_status != 'success':
            if wait_time <= 0:
                err_msg = "Get the status of creating pool timeout, Detail:<%s>%s" % (task_status, str(task_info))
            else:
                err_msg = "Failed to expand pool, Detail:<%s>%s" % (task_status, str(task_info))
            logger.error(err_msg)
            raise Exception(err_msg)
