###
# 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 -*-
""" Add Federation Command for rdmc """

import json
import getpass

from argparse import Action, RawDescriptionHelpFormatter

from redfish.ris.ris import SessionExpired
from redfish.ris.rmc_helper import IdTokenError

from ilorest.rdmc_helper import ReturnCodes, InvalidCommandLineError, ResourceExists,\
                InvalidCommandLineErrorOPTS, NoContentsFoundForOperationError,\
                IncompatibleiLOVersionError, UsernamePasswordRequiredError, Encryption

__subparsers__ = ['add', 'modify', 'changekey', 'delete']

class _FederationParse(Action):
    def __init__(self, option_strings, dest, nargs, **kwargs):
        super(_FederationParse, self).__init__(option_strings, dest, nargs, **kwargs)
    def __call__(self, parser, namespace, values, option_strings):
        """ Federation privileges option helper"""

        privkey = {1: 'LoginPriv', 2: 'RemoteConsolePriv', 3:'UserConfigPriv', 4:'iLOConfigPriv',
                   5: 'VirtualMediaPriv', 6: 'VirtualPowerAndResetPriv', 7: 'HostNICConfigPriv',
                   8: 'HostBIOSConfigPriv', 9: 'HostStorageConfigPriv', 10: 'SystemRecoveryConfigPriv'}

        for priv in next(iter(values)).split(','):
            try:
                priv = int(priv)
            except ValueError:
                try:
                    parser.error("Invalid privilege entered: %s. Privileges must " \
                                           "be numbers." % priv)
                except:
                    raise InvalidCommandLineErrorOPTS("")

            try:
                if not isinstance(namespace.optprivs, list):
                    namespace.optprivs = list()
                if option_strings.startswith('--add'):
                    namespace.optprivs.append({privkey[priv]: True})
                elif option_strings.startswith('--remove'):
                    namespace.optprivs.append({privkey[priv]: False})
            except KeyError:
                try:
                    parser.error("Invalid privilege entered: %s. Number does not " \
                                           "match an available privlege." % priv)
                except:
                    raise InvalidCommandLineErrorOPTS("")

class IloFederationCommand():
    """ Add a new ilo federation to the server """
    def __init__(self):
        self.ident = {
            'name':'ilofederation',
            'usage': None,
            'description':'View, Add, Remove and Modify iLO Federation accoutns based on the '
                          'sub-command used.\nTo view help on specific sub-commands run: ilofederation '
                          '<sub-command> -h\n\nExample: ilofederation add -h\n'
                          'NOTE 1: By default only the login privilege is added to the newly created\n\t\t'
                          'federation group with role "ReadOnly" in iLO 5 and no privileges in iLO 4.\n\t'
                          'NOTE 2: Federation credentials are case-sensitive.',
            'summary':'Adds / deletes an iLO federation group on the currently logged in server.',
            'aliases': [],
            'auxcommands': []
        }

        self.cmdbase = None
        self.rdmc = None
        self.auxcommands = dict()

    def run(self, line, help_disp=False):
        """ Main addfederation function
        :param line: string of arguments passed in
        :type line: str.
        """
        if help_disp:
            self.parser.print_help()
            return ReturnCodes.SUCCESS
        body = dict()
        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):
                # self.rdmc.ui.printer(self.ident['usage'])
                return ReturnCodes.SUCCESS
            else:
                raise InvalidCommandLineErrorOPTS("")

        self.ilofederationvalidation(options)

        redfish = self.rdmc.app.monolith.is_redfish
        path = self.rdmc.app.typepath.defs.federationpath
        results = self.rdmc.app.get_handler(path, service=True, silent=True).dict

        if redfish:
            results = results['Members']
        else:
            if 'Member' in results['links']:
                results = results['links']['Member']
            else:
                results = list()

        if not results:
            raise NoContentsFoundForOperationError("No iLO Federation accounts were found.")

        mod_fed = None
        path = None
        for indx, acct in enumerate(results):
            fed = self.rdmc.app.get_handler(acct[self.rdmc.app.typepath.defs.hrefstring],
                                             service=True, silent=True).dict
            try:
                if hasattr(options, 'fedname'):
                    if fed['Name'].lower() == options.fedname.lower():
                        if redfish:
                            path = acct['@odata.id']
                        else:
                            path = acct['links']['self']['href']
                        mod_fed = fed
                elif options.command == 'default':
                    results[indx] = fed
                else:
                    raise KeyError
            except KeyError:
                continue
            else:
                if mod_fed:
                    break

        if mod_fed:
            if options.command.lower() == 'add':
                raise ResourceExists('Federation name %s is already in use.' % options.fedname)
        else:
            if options.command.lower() != 'add' and options.command.lower() != 'default':
                raise InvalidCommandLineError("Unable to find the specified federation %s." % options.fedname)

        if options.command.lower() == 'add':
            privs = self.getprivs(options)
            path = self.rdmc.app.typepath.defs.federationpath

            body = {"Name": options.fedname, "Key": options.fedkey}
            if privs:
                body.update({"Privileges": privs})
            self.addvalidation(options.fedname, options.fedkey, results)

            if path and body:
                resp = self.rdmc.app.post_handler(path, body, silent=False, service=False)

            if resp and resp.dict:
                if 'resourcealreadyexist' in str(resp.dict).lower():
                    raise ResourceExists('')

        elif options.command.lower() == 'changekey':

            try:
                name = options.fedname
                newkey = options.fedkey
            except:
                raise InvalidCommandLineError('Invalid number of parameters.')

            body = {'Key': newkey}

            if path and body:
                self.rdmc.app.patch_handler(path, body, service=True)
            else:
                raise NoContentsFoundForOperationError('Unable to find '
                                                       'the specified federation.')
        elif options.command.lower() == 'modify':

            if options.optprivs:
                body.update({'Privileges': {}})
                if any(priv for priv in options.optprivs if 'SystemRecoveryConfigPriv' in priv) \
                                                        and 'SystemRecoveryConfigPriv' not in \
                                                        list(self.getsesprivs().keys()):
                    raise IdTokenError("The currently logged in federation must have The System "
                                       "Recovery Config privilege to add the System Recovery "
                                       "Config privilege.")
                privs = self.getprivs(options)
                body['Privileges'] = privs

            self.rdmc.app.patch_handler(path, body)

        elif options.command.lower() == 'delete':
            if path:
                self.rdmc.app.delete_handler(path)
        else:
            self.rdmc.ui.printer("iLO Federation Id list with Privileges:\n")
            if options.json:
                outdict = dict()
                for fed in sorted(results, key=lambda k: k['Name']):
                    outdict[fed['Name']] = fed['Privileges']
                self.rdmc.ui.print_out_json_ordered(str(json.dumps(outdict, indent=2)))
            else:
                for fed in sorted(results, key=lambda k: k['Name']):
                    privstr = ""
                    privs = fed['Privileges']
                    for priv in privs:
                        privstr += priv + '=' + str(privs[priv]) + '\n'
                    self.rdmc.ui.printer("\nName=%s:\n%s" % (fed['Name'], privstr))

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

    def getprivs(self, options):
        """ find and return the privileges to set
        :param options: command line options
        :type options: list.
        """
        sesprivs = self.getsesprivs()
        setprivs = {}
        availableprivs = self.getsesprivs(availableprivsopts=True)

        if not 'UserConfigPriv' in list(sesprivs.keys()):
                raise IdTokenError("The currently logged in federation does not have the Config"
                                   "Privilege and cannot add or modify federations.")

        if options.optprivs:
            for priv in options.optprivs:
                priv = next(iter(list(priv.keys())))
                if not options.user or not options.password:
                    if priv == "SystemRecoveryConfigPriv" and self.rdmc.app.current_client.base_url == "blobstore://.":
                        raise UsernamePasswordRequiredError("Privilege %s need username and password to be specified." % priv)
                if priv not in availableprivs:
                    raise IncompatibleiLOVersionError("Privilege %s is not available on this "\
                                                      "iLO version." % priv)

            if all(priv.values() for priv in options.optprivs):
                if any(priv for priv in options.optprivs if 'SystemRecoveryConfigPriv' in priv) and\
                                            'SystemRecoveryConfigPriv' not in sesprivs.keys():
                    raise IdTokenError("The currently logged in account must have The System "
                                       "Recovery Config privilege to add the System Recovery "
                                       "Config privilege.")
                else:
                    setprivs = {}
            for priv in options.optprivs:
                setprivs.update(priv)

        return setprivs

    def getsesprivs(self, availableprivsopts=False):
        """Finds and returns the current session's privileges
        :param availableprivsopts: return available privileges
        :type availableprivsopts: boolean.
        """
        if self.rdmc.app.current_client:
            sespath = self.rdmc.app.current_client.session_location
            sespath = self.rdmc.app.current_client.default_prefix + \
                                sespath.split(self.rdmc.app.current_client.\
                                              default_prefix)[-1]

            ses = self.rdmc.app.get_handler(sespath, service=False, silent=True)

            if ses.status != 200:
                raise SessionExpired("Invalid session. Please logout and "
                                     "log back in or include credentials.")

            sesprivs = {'HostBIOSConfigPriv': True, 'HostNICConfigPriv': True, 'HostStorageConfigPriv': True,
                        'LoginPriv': True, 'RemoteConsolePriv': True, 'SystemRecoveryConfigPriv': True,
                        'UserConfigPriv': True, 'VirtualMediaPriv': True, 'VirtualPowerAndResetPriv': True, 'iLOConfigPriv': True}
            if 'Oem' in ses.dict:
                sesoemhp = ses.dict['Oem'][self.rdmc.app.typepath.defs.oemhp]
                if 'Privileges' in list(sesoemhp.keys()):
                    sesprivs = sesoemhp['Privileges']
            availableprivs = list(sesprivs.keys())
            keepprivs = dict()
            for priv, val in sesprivs.items():
                if val:
                    keepprivs[priv] = sesprivs[priv]
            sesprivs = keepprivs
        else:
            sesprivs = None

        if availableprivsopts:
            return availableprivs
        else:
            return sesprivs

    def addvalidation(self, username, key, feds):
        """ add validation function
        :param username: username to be added
        :type username: str.
        :param key: key to be added
        :type key: str.
        :param feds: list of federation accounts
        :type feds: list.
        """

        if len(username) >= 32:
            raise InvalidCommandLineError('User name exceeds maximum length.')
        elif len(key) >= 32 or len(key) <= 7:
            raise InvalidCommandLineError('Password is invalid length.')

    def ilofederationvalidation(self, options):
        """ addfederation validation function
        :param options: command line options
        :type options: list.
        """
        self.cmdbase.login_select_validation(self, options)

    @staticmethod
    def options_addprivs_argument_group(parser):
        """ Define optional arguments group
        :param parser: The parser to add the addprivs option group to
        :type parser: ArgumentParser/OptionParser
        """
        group = parser.add_argument_group('GLOBAL OPTION:', 'Option(s) are available for'
                                                            ' all arguments within the scope of this subcommand.')

        parser.add_argument(
            '--addprivs',
            dest='optprivs',
            nargs='*',
            action=_FederationParse,
            type=str,
            help="Optionally include this flag if you wish to specify "\
            "which privileges you want added to the iLO federation. This overrides the default of "\
            "duplicating privileges of the currently logged in federation on the new federation. "\
            "Pick privileges from the privilege list in the above help text. EX: --addprivs=1,2,4",
            default=None
        )

    @staticmethod
    def options_removeprivs_argument_group(parser):
        """ Additional argument
        :param parser: The parser to add the removeprivs option group to
        :type parser: ArgumentParser/OptionParser
        """

        group = parser.add_argument_group('GLOBAL OPTION:', 'Option(s) are available'
                                                            'for all arguments within the scope of this command.')
        parser.add_argument(
            '--removeprivs',
            dest='optprivs',
            nargs='*',
            action=_FederationParse,
            type=str,
            help="Optionally include this flag if you wish to specify "\
            "which privileges you want removed from the iLO federation. This overrides the default"\
            " of duplicating privileges of the currently logged in federation on the new "\
            "federation. Pick privileges from the privilege list in the above help text. "\
            "EX: --removeprivs=1,2,4",
            default=None
        )

    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')
        privilege_help='\n\nPRIVILEGES:\n\t1: Login\n\t2: Remote Console\n\t3: User Config\n\t4:'\
            ' iLO Config\n\t5: Virtual Media\n\t6: Virtual Power and Reset\n\n\tiLO 5 added '\
            'privileges:\n\t7: Host NIC Config\n\t8: Host Bios Config\n\t9: Host Storage Config'\
            '\n\t10: System Recovery Config'
        #default sub-parser
        default_parser = subcommand_parser.add_parser(
            'default',
            help='Running without any sub-command will return all federation group information '\
            ' on the currently logged in server.'
        )
        default_parser.add_argument(
            '-j',
            '--json',
            dest='json',
            action="store_true",
            help="Optionally include this flag if you wish to change the"\
            " displayed output to JSON format. Preserving the JSON data"\
            " structure makes the information easier to parse.",
            default=False
        )
        self.cmdbase.add_login_arguments_group(default_parser)        #add sub-parser
        add_help='Adds an iLO federation group to the currently logged in server. Federation '\
                 'group privileges may be specified with\n\"--addprivs\". If a federation key '\
                 'is not '\
                 'provided, the user will be prompted to provide one prior to account creation.'
        add_parser = subcommand_parser.add_parser(
            __subparsers__[0],
            help=add_help,
            description=add_help+'\n\tilofederation add [FEDERATIONNAME] [FEDERATIONKEY] ' +\
            privilege_help+ '\n\tilofederation add newilofedname thisfedkey --addprivs 1,3,4',
            formatter_class=RawDescriptionHelpFormatter
        )
        add_parser.add_argument(
            'fedname',
            help= 'Federation name of the federation group to add.',
            type= str,
            metavar='FEDERATION KEY',
        )
        add_parser.add_argument(
            'fedkey',
            help= 'Federation key of the federation group to add.',
            type= str,
            metavar='FEDERATION KEY',
        )
        self.options_addprivs_argument_group(add_parser)
        self.cmdbase.add_login_arguments_group(add_parser)

        modify_help='Modify the privileges on an existing federation group.'
        modify_parser = subcommand_parser.add_parser(
            __subparsers__[1],
            help=modify_help,
            description=modify_help+"\n\nTo add privileges:\n\tilofederation modify "\
            "[FEDNAME] --addprivs <list of numbered privileges>\n\nTo remove privileges:\n\t"\
            "ilofederation modify [FEDNAME] --removeprivs <list of numbered privileges>\n\n"\
            +privilege_help,
            formatter_class=RawDescriptionHelpFormatter
        )
        modify_parser.add_argument(
            'fedname',
            help='The federation name of the iLO account to modify.',
            metavar='FEDERATION NAME',
            type= str
        )
        self.options_addprivs_argument_group(modify_parser)
        self.options_removeprivs_argument_group(modify_parser)
        self.cmdbase.add_login_arguments_group(modify_parser)

        #changepass sub-parser
        changekey_help='Change the key of an iLO federation group on the currently logged in '\
                       'server.'
        changekey_parser = subcommand_parser.add_parser(
            __subparsers__[2],
            help=changekey_help,
            description=changekey_help+'\n\nexample:ilofederation changekey [FEDNAME] [NEWFEDKEY]',
            formatter_class=RawDescriptionHelpFormatter
        )
        changekey_parser.add_argument(
            'fedname',
            help='The iLO federation account to be updated with a new federation key (password).',
            metavar='FEDERATION NAME',
            type= str
        )
        changekey_parser.add_argument(
            'fedkey',
            help='The federation key (password) to be altered for the selected iLO federation '\
            ' account. If you do not include a federation key, you will be prompted to enter one.',
            metavar='FEDERATION KEY',
            type= str,
            nargs='?',
            default=''
        )
        self.cmdbase.add_login_arguments_group(changekey_parser)

        #delete sub-parser
        delete_help='Deletes the provided iLO user account on the currently logged in server.'
        delete_parser = subcommand_parser.add_parser(
            __subparsers__[3],
            help=delete_help,
            description=delete_help+'\n\nexample: ilofederation delete fedname',
            formatter_class=RawDescriptionHelpFormatter
        )
        delete_parser.add_argument(
            'fedname',
            help='The iLO federation account to delete.',
            metavar='FEDERATION NAME',
            type= str
        )
        self.cmdbase.add_login_arguments_group(delete_parser)
