###
# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. 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.
###

# -*- coding: utf-8 -*-
""" Smart Array Command for rdmc """

import sys
import json
import redfish

from argparse import ArgumentParser, SUPPRESS, RawDescriptionHelpFormatter
from ilorest.rdmc_base_classes import RdmcCommandBase, HARDCODEDLIST
from ilorest.rdmc_helper import ReturnCodes, InvalidCommandLineError, Encryption, \
                    IncompatableServerTypeError, InvalidCommandLineErrorOPTS, UI
from redfish.ris.resp_handler import ResponseHandler
from redfish.ris.utils import iterateandclear

__config_file__ = "smartarray_config.json"
from ilorest.rdmc_base_classes import HARDCODEDLIST

__subparsers__ = ['load', 'save', 'state']

class SmartArrayCommand():
    """ Smart array command """
    def __init__(self):
        self.ident = {
            'name': 'smartarray',
            'usage': None,
            'description': '\tRun without arguments for the '
                    'current list of smart array controllers.\n\texample: '
                    'smartarray\n\n\tTo get more details on a specific controller '
                    'select it by index.\n\texample: smartarray --controller=2'
                    '\n\n\tTo get more details on a specific controller select '
                    'it by location.\n\texample: smartarray --controller "Slot 0"'
                    '\n\n\tIn order to get a list of all physical drives for '
                    'each controller.\n\texample: smartarray --physicaldrives'
                    '\n\n\tTo obtain details about physical drives for a '
                    'specific controller.\n\texample: smartarray --controller=3 '
                    '--physicaldrives\n\n\tTo obtain details about a specific '
                    'physical drive for a specific controller.\n\texample: smartarray '
                    '--controller=3 --pdrive=1I:1:1\n\n\tIn order to get a list of '
                    'all logical drives for the each controller.\n\texample: '
                    'smartarray --logicaldrives\n\n\tTo obtain details about '
                    'logical drives for a specific controller.\n\texample: '
                    'smartarray --controller=3 --logicaldrives\n\n\tTo obtain '
                    'details about a specific logical drive for a specific '
                    'controller.\n\texample: smartarray --controller=3 --ldrive=1\n',
            'summary': 'Discovers all storage controllers installed in the '
                      'server and managed by the SmartStorage.',
            'aliases': [],
            'auxcommands': ["SelectCommand"]
        }
        self.config_file = None
        self.fdata = None
        self.cmdbase = None
        self.rdmc = None
        self.auxcommands = dict()

    def file_handler(self, filename, operation, options, data=None, sk=None):
        """
        Wrapper function to read or write data to a respective file

        :param data: data to be written to output file
        :type data: container (list of dictionaries, dictionary, etc.)
        :param file: filename to be written
        :type file: string (generally this should be self.clone_file or tmp_clone_file
        :param operation: file operation to be performed
        :type operation: string ('w+', 'a+', 'r+')
        :param sk: sort keys flag
        :type sk: boolean
        :param options: command line options
        :type options: attribute
        :returns: json file data
        """
        writeable_ops = ['w', 'w+', 'a', 'a+']
        readable_ops = ['r', 'r+']
        fdata = None

        try:
            if operation in writeable_ops:
                if getattr(options, "encryption", False):
                    with open(filename, operation + 'b') as outfile:
                        outfile.write(Encryption().encrypt_file(json.dumps(data, indent=2,
                                                                           cls=redfish.ris.JSONEncoder, sort_keys=sk),
                                                                getattr(options, "encryption", False)))
                else:
                    with open(filename, operation) as outfile:
                        outfile.write(json.dumps(data, indent=2, cls=redfish.ris.JSONEncoder,
                                                 sort_keys=sk))
            else:
                if getattr(options, "encryption", False):
                    with open(filename, operation + 'b') as file_handle:
                        fdata = json.loads(Encryption().decrypt_file(file_handle.read(),
                                                                     getattr(options, "encryption", False)))
                else:
                    with open(filename, operation) as file_handle:
                        fdata = json.loads(file_handle.read())
                return fdata
        except Exception as excp:
            raise InvalidFileInputError("Unable to open file: %s.\nVerify the file location " \
                                        "and the file has a valid JSON format.\n" % excp)

    def run(self, line, help_disp=False):
        """ Main smart array worker function

        :param line: command line input
        :type line: string.
        """
        if help_disp:
            self.parser.print_help()
            return ReturnCodes.SUCCESS
        try:
            ident_subparser = False
            for cmnd in __subparsers__:
                if cmnd in line:
                    (options, args) = self.rdmc.rdmc_parse_arglist(self, line)
                    ident_subparser = True
                    break
            if not ident_subparser:
                (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True)
        except (InvalidCommandLineErrorOPTS, SystemExit):
            if ("-h" in line) or ("--help" in line):
                return ReturnCodes.SUCCESS
            else:
                raise InvalidCommandLineErrorOPTS("")

        self.smartarrayvalidation(options)

        if options.command == 'state':
            controllers = self.controllers(options, print_ctrl=False, single_use=True)
            for controller in controllers:
                if controllers[controller].get('@Redfish.Settings'):
                    time = controllers[controller]['@Redfish.Settings'].get('Time', "Not Available")
                    sys.stdout.write("Last Configuration Attempt: %s\n" % str(time))
                    for message in controllers[controller]['@Redfish.Settings'].get('Messages', []):
                        ResponseHandler(self.rdmc.app.validationmanager,
                                        self.rdmc.app.typepath.defs.messageregistrytype).message_handler(message,
                                                                                                         2, warn=False)
                else:
                    sys.stdout.write("Previous smart array configuration status messages are "\
                        "not available for controller '%s - %s - %s' (@Redfish.Settings not "\
                        "available).\n" % (controller, controllers[controller].get('Location',
                                                                                   'Unknown'), controllers[controller].get('Model', 'Unknown')))

        if options.command == 'save':
            controllers = self.controllers(options, print_ctrl=False, single_use=True)
            for key, controller in controllers.items():
                physical_drives = self.physical_drives(options, controller,
                                                       print_ctrl=False, single_use=True)
                logical_drives = self.logical_drives(options, controller,
                                                     print_ctrl=False, single_use=True)
                if self.rdmc.app.typepath.defs.isgen10:
                    for drivec_idx, drivec in enumerate(controller.get('PhysicalDrives', [])):
                        for drive in physical_drives:
                            if drivec['Location'] == physical_drives[drive]['Location']:
                                controller['PhysicalDrives'][drivec_idx].\
                                                                    update(physical_drives[drive])
                                break
                    for drivec_idx, drivec in enumerate(controller.get('LogicalDrives', [])):
                        for drive in logical_drives:
                            if drivec['LogicalDriveNumber'] == logical_drives[drive]['Id']:
                                controller['LogicalDrives'][drivec_idx].\
                                                                    update(logical_drives[drive])
                                break
                if controller.get('Links'):
                    controller['Links']['LogicalDrives'] = logical_drives
                    controller['Links']['PhysicalDrives'] = physical_drives
                else:
                    controller['links']['LogicalDrives'] = logical_drives
                    controller['links']['PhysicalDrives'] = physical_drives

            self.file_handler(self.config_file, 'w', options, controllers, sk=True)
            sys.stdout.write("Smart Array configuration saved to '%s'.\n" % self.config_file)

        if options.command == 'load':
            controllers = self.file_handler(self.config_file, operation='rb', options=options)
            for controller in controllers:
                if controllers[controller].get("DataGuard"):
                    controllers[controller]["DataGuard"] = "Disabled"
                    readonly_removed = self.rdmc.app.removereadonlyprops(controllers[controller])
                    readonly_removed['LogicalDrives'] = controllers[controller]['LogicalDrives']
                    readonly_removed['PhysicalDrives'] = controllers[controller]['PhysicalDrives']
                    self.rdmc.app.put_handler(controllers[controller]["@odata.id"],
                                              readonly_removed)
                    sys.stdout.write('Smart Array configuration loaded. Reboot the server to '
                                     'finalize the configuration.\n')
        elif options.command == 'default':
            if getattr(options, "json", False):
                self.controllers(options, print_ctrl=False, single_use=False)
            else:
                self.controllers(options, print_ctrl=True, single_use=False)

        self.cmdbase.logout_routine(self, options)
        #Return code
        return ReturnCodes.SUCCESS

    def controllers(self, options, print_ctrl=False, single_use=False):
        """
        Identify/parse logical drives (and child properties) of a parent array controller

        :param options: command line options (options.controller is a required attribute)
        :type options: object attributes
        :param print_ctrl: flag for console print enablement/disablement (default disabled)
        :type print_ctrl: bool
        :param single_use: singular usage - True or explore mode - False,
        returns dictionary of results to calling function (default disabled) - see returns.
        :type single_use: bool
        :returns: None, dictionary of all controllers identified by 'Id'
        """

        if getattr(options, "controller", False):
            controller_ident = False
        if not getattr(options, "json", False) and not getattr(options, "controller", False) \
                                                                                    and print_ctrl:
            sys.stdout.write("Controllers:\n")

        controller_data = {}

        for sel in self.rdmc.app.select("SmartStorageArrayController", path_refresh=True):
            if 'Collection' not in sel.maj_type:
                controller = sel.dict
                if getattr(options, "controller", False):
                    if getattr(options, "controller", False) == controller["Id"] or \
                                getattr(options, "controller", False) == controller["Location"]:
                        controller_ident = True
                    else:
                        continue

                if self.rdmc.app.typepath.defs.isgen10:
                    for g10controller in self.rdmc.app.select("SmartStorageConfig.",
                                                              path_refresh=True):
                        if g10controller.dict.get('Location') == controller.get('Location'):
                            id = controller.get('Id')
                            controller.update(g10controller.dict)
                            if id: #iLO Bug - overwrite the Id from collection
                                controller['Id'] = id
                            break

                if print_ctrl:
                    sys.stdout.write("[%s]: %s - %s\n" % (controller["Id"],
                                                          controller["Location"], controller["Model"]))
                elif getattr(options, "json", False):
                    UI().print_out_json(sel.dict)

                if getattr(options, "logicaldrives", False) or \
                    getattr(options, "ldrive", False) or single_use:
                    self.logical_drives(options, controller, print_ctrl)

                if getattr(options, "physicaldrives", False) or \
                    getattr(options, "pdrive", False) or single_use:
                    self.physical_drives(options, controller, print_ctrl)

                if single_use:
                    controller_data[controller["Id"]] = controller

        if getattr(options, "controller", False) and not controller_ident and print_ctrl:
            sys.stdout.write("Controller in position '%s' was not found\n" % \
                                                        getattr(options, "controller", False))

        if single_use:
            return controller_data

    def physical_drives(self, options, content, print_ctrl=False, single_use=False):
        """
        Identify/parse physical drives (and child properties) of a parent array controller

        :param options: command line options
        :type options: object attributes
        :param content: dictionary of physical drive href or @odata.id paths (expect @odata.type
        "SmartStorageArrayController", @odata.id /Systems/1/SmartStorage/ArrayControllers/X/)
        :type content: dict
        :param print_ctrl: flag for console print enablement/disablement (default disabled)
        :type print_ctrl: bool
        :param single_use: singular usage - True or explore mode - False,
        returns dictionary of results to calling function (default disabled) - see returns.
        :type single_use: bool
        :returns: None, dictionary of all physical drives identified by 'Id'
        """

        confd = content.get("PhysicalDrives")
        try:
            dd = content["links"]["PhysicalDrives"][self.rdmc.app.typepath.defs.hrefstring]
        except:
            dd = content["Links"]["PhysicalDrives"][self.rdmc.app.typepath.defs.hrefstring]
        finally:
            if not getattr(options, "json", False) and print_ctrl:
                sys.stdout.write("\tPhysical Drives:\n")
            if getattr(options, "pdrive", False):
                pdrive_ident = False
            else:
                pdrive_ident = True
            if single_use:
                physicaldrives = {}
            found_entries = False
            for data in self.rdmc.app.get_handler(dd, silent=True).dict.get('Members', {}):
                try:
                    tmp = data[self.rdmc.app.typepath.defs.hrefstring]
                except:
                    tmp = data[next(iter(data))]
                finally:
                    tmp = self.rdmc.app.get_handler(tmp, silent=True).dict
                    found_entries = True
                if confd:
                    for confdd in confd:
                        if confdd.get("Location") == tmp.get("Location"):
                            tmp.update(confdd)
                            break
                if getattr(options, "pdrive", False):
                    if options.pdrive != tmp['Location']:
                        continue
                    else:
                        pdrive_ident = True
                if single_use:
                    physicaldrives[tmp['Id']] = tmp
                if print_ctrl and pdrive_ident:
                    sys.stdout.write("\t[%s]: Model %s, Location %s, Type %s, Serial %s - %s MiB\n" % (tmp['Location'],
                                                                                                       tmp['Model'], tmp['Location'], tmp['MediaType'], tmp['SerialNumber'], tmp['CapacityMiB']))
                elif getattr(options, "json", False) and pdrive_ident:
                    UI().print_out_json(tmp)
                if getattr(options, "pdrive", False) and pdrive_ident:
                    break
            if getattr(options, "pdrive", False) and not pdrive_ident and print_ctrl:
                sys.stdout.write("\tPhysical drive in position '%s' was not found" % options.pdrive)
            elif not found_entries and print_ctrl:
                sys.stdout.write("\tPhysical drives not found.\n")

            if single_use:
                return physicaldrives


    def logical_drives(self, options, content, print_ctrl=False, single_use=False):
        """
        Identify/parse logical drives (and child properties) of an associated array controller

        :param options: command line options
        :type options: object attributes
        :param content: dictionary of logical drive href or @odata.id paths (expect @odata.type
        "SmartStorageArrayController", @odata.id /Systems/1/SmartStorage/ArrayControllers/X/)
        :type content: dict
        :param print_ctrl: flag for console print enablement/disablement (default disabled)
        :type print_ctrl: bool
        :param single_use: singular usage - True or explore mode - False,
        returns dictionary of results to calling function (default disabled) - see returns.
        :type single_use: bool
        :returns: None, dictionary of all logical drives identified by 'Id'
        """

        confd = content.get("LogicalDrives")
        try:
            dd = content["links"]["LogicalDrives"][self.rdmc.app.typepath.defs.hrefstring]
        except:
            dd = content["Links"]["LogicalDrives"][self.rdmc.app.typepath.defs.hrefstring]
        finally:
            if not getattr(options, "json", False) and print_ctrl:
                sys.stdout.write("\tLogical Drives:\n")
            if getattr(options, "ldrive", False):
                ldrive_ident = False
            else:
                ldrive_ident = True
            if single_use:
                logicaldrives = {}
            found_entries = False
            for data in self.rdmc.app.get_handler(dd, silent=True).dict.get('Members', {}):
                try:
                    tmp = data[self.rdmc.app.typepath.defs.hrefstring]
                except:
                    tmp = data[next(iter(data))]
                finally:
                    tmp = self.rdmc.app.get_handler(tmp, silent=True).dict
                    found_entries = True
                if confd:
                    for confdd in confd:
                        if confdd.get("LogicalDriveNumber") == tmp.get("LogicalDriveNumber"):
                            tmp.update(confdd)
                            break
                if getattr(options, "ldrive", False):
                    if options.ldrive != tmp['Id']:
                        continue
                    else:
                        ldrive_ident = True
                if print_ctrl and ldrive_ident:
                    sys.stdout.write("\t[%s]: Name %s VUID %s - %s MiB\n" % (tmp['Id'],
                                                                             tmp['LogicalDriveName'], tmp['VolumeUniqueIdentifier'], tmp['CapacityMiB']))
                elif getattr(options, "json", False) and ldrive_ident:
                    UI().print_out_json(tmp)
                try:
                    tmp2 = tmp['Links']['DataDrives'][self.rdmc.app.typepath.defs.hrefstring]
                except:
                    tmp2 = tmp['links']['DataDrives'][next(iter(tmp['links']['DataDrives']))]
                finally:
                    tmp2 = self.rdmc.app.get_handler(tmp2, silent=True).dict
                if ldrive_ident:
                    data_drives = self.get_data_drives(options, tmp2, print_ctrl, single_use)
                    try:
                        tmp['links']['DataDrives'] = data_drives
                    except:
                        tmp['Links']['DataDrives'] = data_drives
                if single_use:
                    logicaldrives[tmp['Id']] = tmp
                if getattr(options, "ldrive", False) and ldrive_ident:
                    break
            if getattr(options, "ldrive", False) and not ldrive_ident:
                sys.stdout.write("\tLogical drive in position '%s' was not found" % options.pdrive)
            elif not found_entries and print_ctrl:
                sys.stdout.write("\tLogical drives not found.\n")

            if single_use:
                return logicaldrives

    def get_data_drives(self, options, drives, print_ctrl=False, single_use=False):
        """
        Identify/parse a physical component drive collection of a respective logical drive. The
        physical disk properties as well as some logical RAID parameters within each respective
        member.

        :param options: command line options
        :type options: object attributes
        :param content: collection of data drives href or @odata.id paths (as members) as attributed
        to a respective logical drive.
        :type content: dict
        :param print_ctrl: flag for console print enablement/disablement (default disabled)
        :type print_ctrl: bool
        :param single_use: singular usage, returns dictionary of results to calling function
        (default disabled)
        :type single_use: bool
        :returns: None, dictionary of all physical drives composing a parent logical drive,
        each instance identified by 'Id'
        """

        if not (getattr(options, "json", False)) and print_ctrl:
                sys.stdout.write("\t\tData Drives:\n")
        if single_use:
            subsetdrives = {}
        found_entries = False
        for member in drives.get('Members', {}):
            try:
                tmp = member[self.rdmc.app.typepath.defs.hrefstring]
            except:
                tmp = member[next(iter(member))]
            finally:
                tmp = self.rdmc.app.get_handler(tmp, silent=True).dict
                found_entries = True
            if single_use:
                subsetdrives[tmp['Id']] = tmp
            if print_ctrl:
                sys.stdout.write("\t\t[%s]: Model %s, Serial %s - %s MiB\n" % (tmp['Location'],
                                                                               tmp['Model'], tmp['SerialNumber'], tmp['CapacityMiB']))
                sys.stdout.write("\t\t\tLocation: %s\n" % (tmp['Location']))
            elif getattr(options, "json", False):
                UI().print_out_json(tmp)
        if not found_entries and print_ctrl:
            sys.stdout.write("\t\tComponent drives not found.\n")

        if single_use:
            return subsetdrives

    def smartarrayvalidation(self, options):
        """ Smart array validation function

        :param options: command line options
        :type options: list.
        """
        self.cmdbase.login_select_validation(self, options)

        if getattr(options, "sa_conf_filename", False):
            if options.command == 'save':
                open(options.sa_conf_filename, 'w+b')
            if options.command == 'load':
                open(options.sa_conf_filename, 'r+b')
            self.config_file = options.sa_conf_filename
        else:
            self.config_file = __config_file__

    def definearguments(self, customparser):
        """ Wrapper function for new command main function

        :param customparser: command line input
        :type customparser: parser.
        """
        if not customparser:
            return

        self.cmdbase.add_login_arguments_group(customparser)
        subcommand_parser = customparser.add_subparsers(dest='command')

        default_parser = subcommand_parser.add_parser(
            'default',
            help="Running without any sub-command will return smart array configuration data\n"
            "on the currently logged in server. Additional optional arguments will narrow the\n"
            "scope of returned data to individual controllers, physical or logical drives"
        )
        default_parser.add_argument(
            '--controller',
            dest='controller',
            help="Use this flag to select the corresponding controller using either the slot "
                "number or index. \n\tExamples:\n\t1. To get more details on a specific "
                "controller, select it by index.\tsmartarray --controller=2"
                "\n\t2. To get more details on a specific controller select "
                "it by location.\tsmartarray --controller=\'Slot 0\'",
            default=None
        )
        default_parser.add_argument(
            '--physicaldrives',
            dest='physicaldrives',
            action="store_true",
            help="Use this flag to return the physical drives for the controller selected."
                "\n\tExamples:\n\t1. smartarray --physicaldrives\n\t2. To obtain details about "
                "physical drives for a specific controller.\tsmartarray --controller=3 "
                "--physicaldrives",
            default=None
        )
        default_parser.add_argument(
            '--logicaldrives',
            dest='logicaldrives',
            action="store_true",
            help="Use this flag to return the logical drives for the controller selected.\n\t "
                 "\n\tExamples:\n\t1. smartarray --logicaldrives\n\t2. To obtain details about "
                 "logical drives for a specific controller.\tsmartarray --controller=3 "
                 "--logicaldrives",
            default=None
        )
        default_parser.add_argument(
            '--pdrive',
            dest='pdrive',
            help="Use this flag to select the corresponding physical disk.\n\tExamples:\n\t "
                 "1. To obtain details about a specific physical drive for a specific controller."
                 "\tsmartarray --controller=3 --pdrive=1I:1:1",
            default=None
        )
        default_parser.add_argument(
            '--ldrive',
            dest='ldrive',
            help="Use this flag to select the corresponding logical disk.\n\tExamples:\n\t "
                 "1. To obtain details about a specific physical drive for a specific controller."
                 "\tsmartarray --controller=3 --ldrive=1",
            default=None
        )
        default_parser.add_argument(
            '-j',
            '--json',
            dest='json',
            action="store_true",
            help="""Use this flag to output data in JSON format.""",
            default=None
        )
        self.cmdbase.add_login_arguments_group(default_parser)
        state_help='Print state/event information from @Redfish.Settings (if available)'
        state_parser = subcommand_parser.add_parser(
            'state',
            help=state_help,
            description=state_help + '\n\tExample: smartarray state',
            formatter_class=RawDescriptionHelpFormatter
        )
        state_parser.add_argument(
            '--controller',
            dest='controller',
            help="Use this flag to select the corresponding controller using either the slot "
                "number or index. \n\tExamples:\n\t1. To get more details on a specific "
                "controller, select it by index.\tsmartarray state --controller=2"
                "\n\t2. To get more details on a specific controller select "
                "it by location.\tsmartarray state --controller=\'Slot 0\'",
            default=None
        )
        self.cmdbase.add_login_arguments_group(state_parser)
        json_save_help='Save a JSON file with all current configurations (all controllers, logical and \nphysical drives).'
        #json save sub-parser
        json_save_parser = subcommand_parser.add_parser(
            'save',
            help=json_save_help,
            description=json_save_help + '\n\tExample: smartarray save -f <filename>',
            formatter_class=RawDescriptionHelpFormatter
        )
        json_save_parser.add_argument(
            '-f',
            dest='sa_conf_filename',
            help="Specify a filename for saving the smartarray configuration. (Default: "
                 "\'smartarray_config.json\')",
            default=None
        )
        self.cmdbase.add_login_arguments_group(json_save_parser)
        json_load_help="Load a JSON file with modified smartarray configurations (All read-only "\
            "properties\nare discarded)."
        #json load sub-parser
        json_load_parser = subcommand_parser.add_parser(
            'load',
            help=json_load_help,
            description=json_load_help + '\n\tExample: smartarray load -f <filename>',
            formatter_class=RawDescriptionHelpFormatter
        )
        json_load_parser.add_argument(
            '-f',
            dest='sa_conf_filename',
            help="Specify a filename for loading a smartarray configuration. (Default: "
                 "\'smartarray_config.json\')",
            default=None
        )
        self.cmdbase.add_login_arguments_group(json_load_parser)
