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

import os
import re
import sys
import json
import time
import gzip
import base64

from datetime import datetime
from ctypes import create_string_buffer

import six

from six import StringIO

import redfish

from redfish.hpilo.risblobstore2 import BlobStore2

from ilorest.rdmc_helper import ReturnCodes, InvalidCommandLineErrorOPTS, \
                    InvalidFileFormattingError, UnableToDecodeError, Encryption, \
                    PathUnavailableError, InvalidFileInputError, NoContentsFoundForOperationError

class IPProfilesCommand():
    """ Raw form of the get command """
    def __init__(self):
        self.ident = {
            'name':'ipprofiles',
            'usage': None,
            'description':'Decodes and lists '
                    'ipprofiles. This is default option. No argument required'
                    '\n\tExample: ipprofiles'
                    '\n\n\tAdds a new ipprofile from the provided json file.'
                    '\n\tNOTE: Path can be absolute or from the '
                    'same path you launch iLOrest.'
                    '\n\tipprofiles <file path>'
                    '\n\n\tDelete an ipprofile or list of profiles.\n\t'
                    'Provide the unique key that corresponds to the ipprofile'
                    ' data you want to delete.\n\tSeveral IDs can be comma-separated'
                    ' with no space in between to delete more than one profile. '
                    '\n\tipprofiles -d ID1,ID2,ID3...'
                    '\n\n\tCopies ip profile with the specified ID into the ip job queue.'
                    'and starts it.\n\texample: ipprofiles --start=<profile ID>',
            'summary':'This is used to manage hpeipprofile data store.',
            'aliases': [],
            'auxcommands': []
        }
        self.path = ''
        self.ipjobs = ''
        self.running_jobs = ''
        self.hvt_output = ''
        self.ipjobtype = ['langsel', 'hvt', 'ssa', 'install', 'rbsu']
        self.cmdbase = None
        self.rdmc = None
        self.auxcommands = dict()

    def run(self, line, help_disp=False):
        """ Main raw get worker function

        :param line: command line input
        :type line: string.

        """
        if help_disp:
            self.parser.print_help()
            return ReturnCodes.SUCCESS
        try:
            (options, args) = self.rdmc.rdmc_parse_arglist(self, line)
        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.getpaths()

        self.validation(options)

        self.ipprofileworkerfunction(options, args)

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

    def ipprofileworkerfunction(self, options, args):
        """
        Ipprofile manager worker function. It calls appropriate function.
        :param options: command line options
        :type options: list.
        :param args: command line args
        :type args: string.
        """

        if options.running_jobs:
            self.get_running_job()
            return ReturnCodes.SUCCESS
        if options.get_hvt:
            self.get_hvt_output()
            return ReturnCodes.SUCCESS
        if options.del_key:
            self.deletekeyfromipprofiledata(options)
            return ReturnCodes.SUCCESS

        if options.start_ip:
            self.addprofileandstartjob(options)
            return ReturnCodes.SUCCESS

        if len(args) == 1:
            self.encodeandpatchipprofiledata(args)
            return ReturnCodes.SUCCESS

        self.getipprofiledataanddecode(options)

    def getipprofiledataanddecode(self, options):
        """
        Retrieves and decodes, if encoded, data from hpeprofile data store
        :param options: command line options
        :type options: list.
        :return returncode: int
        """

        results = self.rdmc.app.get_handler(self.path, silent=True)
        if results.status == 404:
            raise PathUnavailableError("The Intelligent Provisioning resource "
                                       "is not available on this system. You may need"
                                       " to run IP at least once to add the resource.")

        if results and results.status == 200:
            j2python = json.loads(results.read)
            for _, val in enumerate(j2python.keys()):
                if isinstance(val, six.string_types):
                    result = self.decode_base64_string(str(j2python[val]))
                    if result is not None:
                        j2python[val] = result

            results.read = json.dumps(j2python, ensure_ascii=False, sort_keys=True)
            if results.dict:
                if options.filename:
                    output = json.dumps(results.dict, indent=2, cls=redfish.ris.JSONEncoder,
                                        sort_keys=True)

                    filehndl = open(options.filename[0], "w")
                    filehndl.write(output)
                    filehndl.close()

                    self.rdmc.ui.printer("Results written out to '%s'.\n" % options.filename[0])
                else:
                    self.rdmc.ui.print_out_json(results.dict)
        else:
            self.rdmc.ui.warn("No IP profiles found\n")

    def get_running_job(self):
        """
        Retrieves and decodes, running job
        :return returncode: int
        """

        results = self.rdmc.app.get_handler(self.running_jobs, silent=True)
        if results.status == 404:
            raise PathUnavailableError("The Intelligent Provisioning resource "
                                       "is not available on this system. You may need"
                                       " to run IP at least once to add the resource.")

        if results and results.status == 200:
            j2python = json.loads(results.read)
            for _, val in enumerate(list(j2python.keys())):
                if isinstance(val, six.string_types):
                    result = self.decode_base64_string(str(j2python[val]))
                    if result is not None:
                        j2python[val] = result

            results.read = json.dumps(j2python, ensure_ascii=False)
            if results.dict:
                self.rdmc.ui.print_out_json(results.dict)
        else:
            self.rdmc.ui.warn("No IP profiles found\n")

    def get_hvt_output(self):
        """
        Retrieves and decodes, running job
        :return returncode: int
        """
        return_value = {}
        results = self.rdmc.app.get_handler(self.hvt_output, silent=True)
        if results.status == 404:
            raise PathUnavailableError("The Intelligent Provisioning resource "
                                       "is not available on this system. You may need"
                                       " to run IP at least once to add the resource.")

        if results and results.status == 200:
            j2python = json.loads(results.read)
            for _, val in enumerate(list(j2python.keys())):
                if isinstance(val, six.string_types) and '@' not in val:
                    return_value = json.loads(self.decode_base64_string(str(j2python[val])))
            self.rdmc.ui.print_out_json(return_value)
        else:
            self.rdmc.ui.error("No IP profiles found\n")


    def encodeandpatchipprofiledata(self, args):
        """
        Reads file in the given path, encode it,
        and apply it on iLO hpeipprofiles data store.
        :param args: command line args
        :type args: string.
        :retirn returncode: int
        """

        contentsholder = self.encode_base64_string(args)

        if "path" in contentsholder and "body" in contentsholder:
            self.rdmc.app.patch_handler(contentsholder["path"], contentsholder["body"])

        return ReturnCodes.SUCCESS

    def deletekeyfromipprofiledata(self, options):
        """
        Provide a string which represents a valid key in
        hpeipprofiles data store.
        :param options: command line options
        :type options: list.
        :return returncode: int
        """

        get_results = self.rdmc.app.get_handler(self.path, silent=True)

        j2python = json.loads(get_results.read)
        all_keys = options.del_key[0].split(',')
        for key in all_keys:
            if isinstance(key, six.string_types) and j2python.get(key.strip(), False):
                del j2python[key.strip()]
            else:
                raise InvalidFileFormattingError("%s was not found .\n" % key)

        payload = {}
        payload["path"] = self.path
        payload["body"] = j2python

        self.rdmc.app.put_handler(payload["path"], payload["body"])

        return ReturnCodes.SUCCESS

    def addprofileandstartjob(self, options):
        """
        Adds ip profile into the job queue and start it.
        :return returncode: int
        """

        ipprovider = self.hasipprovider()
        if ipprovider is None:
            raise PathUnavailableError("System does not support this feature of IP.\n")

        ipjob = self.hasipjobs()
        if not ipjob:
            raise InvalidFileFormattingError("System does not have any IP"
                                             " profile to copy to the job queue.\n")

        current_state = self.inipstate(ipprovider)
        if current_state is None:
            raise PathUnavailableError("System does not support this feature of IP.\n")

        later_state = False
        ipstate = current_state["InIP"]
        if isinstance(ipstate, bool) and ipstate:
            #make sure we are in IP state.  Reset and monitor
            self.resetinipstate(ipprovider, current_state)
            #if we are in ip, monitor should be fast, use 15 seconds
            later_status = self.monitorinipstate(ipprovider, 3)
            if  later_status:
                self.copyjobtoipqueue(ipjob, options.start_ip)
                self.rdmc.ui.printer("Copy operation was successful...\n")
                return ReturnCodes.SUCCESS

        if not isinstance(ipstate, bool):
#       inip is in an unknown state, so ...
        #patch to false, reboot, then monitor...if it turns true later...
        #then we are in IP state otherwise, manually check system...
            self.resetinipstate(ipprovider, current_state)
            later_status = self.monitorinipstate(ipprovider, 3)
            if  later_status:
                self.copyjobtoipqueue(ipjob, options.start_ip)
                self.rdmc.ui.printer("Copy operation was successful...\n")
                return ReturnCodes.SUCCESS

        try:
            self.bootorderobj.run("--onetimeboot=Utilities "
                                  "--reboot=ColdBoot --commit")
        except:
            raise InvalidFileFormattingError("System failed to reboot")

        #After reboot, login again
        self.validation(options)

        later_state = self.monitorinipstate(ipprovider)
        if later_state:
            self.copyjobtoipqueue(ipjob, options.start_ip)
            self.rdmc.ui.printer("Copy operation was successful...\n")
        else:
            raise InvalidFileFormattingError("\nSystem reboot took longer than 4 minutes."
                                             "something is wrong. You need to physically check this system.\n")

        return ReturnCodes.SUCCESS

    def resetinipstate(self, ipprovider, current_state):
        """
        Regardless of previous value, sets InIP value to False
        :param ipprovider: url path of heip.
        :type ipprovider: string.
        :param current_state: the value of InIP.
        :type current_state: dict
        """

        current_state['InIP'] = False

        payload = {}
        payload["path"] = ipprovider
        payload["body"] = current_state

        self.rdmc.app.put_handler(payload["path"], payload["body"], silent=True)

    def monitorinipstate(self, ipprovider, timer=48):
        """
        Monitor InIP value every 5 seconds until it turns true or time expires.
        :param ipprovider: url path of heip.
        :type ipprovider: string.
        :param timer: time it takes iLO to boot into F10 assuming we are in boot state.
        :type timer: int
        :return ipstate: boolean
        """

        retry = timer # 48 * 5 = 4 minutes
        ipstate = False
        progress = self.progressbar()
        self.rdmc.ui.printer('\n')
        while retry > 0:
            time.sleep(5)
            next(progress)
            status = self.inipstate(ipprovider)
            if isinstance(status['InIP'], bool) and status['InIP']:
                ipstate = True
                break
            retry = retry - 1
        self.rdmc.ui.printer('\n')

        return ipstate

    def progressbar(self):
        """
        An on demand function use to output the progress while iLO is booting into F10.
        """
        while True:
            yield self.rdmc.ui.printer('>>>')

    def copyjobtoipqueue(self, ipjobs, jobkey):
        """
        Copies HpeIpJob to Job queue. Function assumes there is a job to copy.
        A check was already done to make sure we have a job to copy in hasipjobs()
        :param ipjobs: url path of heip.
        :type ipjobs: list of dictionary.
        :param jobkey: key of job to copy.
        :type jokbey: str.
        """

        get_results = self.rdmc.app.get_handler(self.path, silent=True)

        j2python = json.loads(get_results.read)
        copy_job = {}
        for ipj in j2python:
            if jobkey == ipj:
                _decode = self.decode_base64_string(j2python[ipj])
                if _decode is not None:
                    _critical_props = {
                        "log": "[{\"msg\": \"WAITINGTOBEPROCESSED\", \"percent\": 0}]",
                        "status": "waiting"
                    }
                    copy_job.update({k: v.update(_critical_props) or v \
                        for k, v in json.loads(_decode).items() if k in self.ipjobtype})
                else:
                    raise NoContentsFoundForOperationError(
                        "Not supported profile content")
                break
        if not copy_job:
            raise NoContentsFoundForOperationError(
                "The ID %s does not match any ipprofile" % jobkey)
        payload = {}
        payload["path"] = self.ipjobs
        payload["body"] = copy_job

        self.rdmc.app.put_handler(payload["path"], payload["body"])

    def inipstate(self, ipprovider):
        """
        A check is done to determine if this version of iLO has InIP profile.
        :param ipprovider: url path of heip.
        :type ipprovider: string.
        :return is_inip: None or dict
        """

        if ipprovider.startswith('/redfish/'):
            get_results = self.rdmc.app.get_handler(ipprovider, silent=True)
            result = json.loads(get_results.read)

            is_inip = None
            try:
                if 'InIP' in list(result.keys()):
                    is_inip = result
            except KeyError:
                pass

        return is_inip

    def hasipjobs(self):
        """
        A check is done to determine if there is a job in HpeIpJobs we can
        copy to IP job queue
        :param options: command line options
        :type options: list.
        :return list_dict: list of dicts
        """

        results = self.rdmc.app.get_handler(self.path, silent=True)

        j2python = json.loads(results.read)
        for _, val in enumerate(j2python.keys()):
            if isinstance(val, six.string_types):
                result = self.decode_base64_string(str(j2python[val]))
                if result is not None:
                    j2python[val] = result

        list_dict = []

        for key, value in j2python.items():
            if not re.match('@odata', key):
                if len(key) >= 13 and key.isdigit():
                    list_dict.append({key:value}) # list of dict with valid key/value

        return list_dict

    def hasipprovider(self):
        """
        A check is done here to determine if this version of iLO has IP provider
        profile path using the  "Oem.Hpe.Links.HpeIpProvider"

        :return is_provider: None or string.
        """
        path = self.rdmc.app.typepath.defs.systempath
        get_results = self.rdmc.app.get_handler(path, silent=True)

        result = json.loads(get_results.read)

        is_ipprovider = None
        try:
            is_ipprovider = list(result['Oem']['Hpe']['Links']\
                ['HpeIpProvider'].values())[0]
        except KeyError:
            pass

        return is_ipprovider

    def validation(self, options):
        """ IPProfiles validation function

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

    def decode_base64_string(self, str_b64):
        """
        Decodes a given string that was encoded with base64 and gzipped.
        :param str_b64: a string that was base64 encoded and the  gzipped
        :type str_b64: string.
        """

        read_data = None
        if isinstance(str_b64, six.string_types) and str_b64:
            try:
                decoded_str = base64.decodestring(str(str_b64))
                inbuffer = StringIO(decoded_str)
                gzffile = gzip.GzipFile(mode='rb', fileobj=inbuffer)
                read_data = ""
                for line in gzffile.readlines():
                    read_data = read_data + line
            except:
                pass

            return read_data

    def encode_base64_string(self, args):
        """
        Encode a given string  with base64 and gzip it.
        :param args: command line args
        :type args: string.
        """

        payload = {}
        filename = args[0]
        if filename:
            if not os.path.isfile(filename):
                raise InvalidFileInputError("File '%s' doesn't exist. "\
                    "Please create file by running 'save' command." % filename)

            try:
                with open(filename, 'r') as fh:
                    contentsholder = json.loads(fh.read())
            except:
                raise InvalidFileFormattingError("Input file '%s' was not "\
                                                 "format properly." % filename)

            try:
                text = json.dumps(contentsholder)
                buf = StringIO()
                gzfile = gzip.GzipFile(mode='wb', fileobj=buf)
                gzfile.write(text)
                gzfile.close()

                en_text = base64.encodestring(buf.getvalue())

                epoch = utcfromtimestamp(0)
                now = utcnow()
                delta = now - epoch
                time_stamp = delta.total_seconds() * 1000
                time_stamp = repr(time_stamp).split('.')[0]

                body_text = {time_stamp: en_text.strip()}
                payload["body"] = body_text
                payload["path"] = self.path
            except:
                raise UnableToDecodeError("Error while encoding string.")

        return payload

    def getpaths(self):
        """ Get paths for ipprofiles command """
        if not all(iter([self.path, self.ipjobs, self.running_jobs, self.hvt_output])):
            dll = BlobStore2.gethprestchifhandle()

            profiles_path = create_string_buffer(50)
            jobs_path = create_string_buffer(50)
            running_jobs_path = create_string_buffer(50)
            hvt_output_path = create_string_buffer(50)

            dll.get_ip_profiles(profiles_path)
            dll.get_ip_jobs(jobs_path)
            dll.get_running_jobs(running_jobs_path)
            dll.get_hvt_output(hvt_output_path)

            self.path = profiles_path.value
            self.ipjobs = jobs_path.value
            self.running_jobs = running_jobs_path.value
            self.hvt_output = hvt_output_path.value

    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)

        customparser.add_argument(
            '-r',
            '--running',
            dest='running_jobs',
            default=False,
            action="store_true",
            help="""Show status of the currently running or last job executed""",
        )
        customparser.add_argument(
            '-D',
            '--diags',
            help="""Get result of last HVT (diagnostics) run as part of an ipprofile job""",
            default=False,
            action='store_true',
            dest='get_hvt'
        )
        customparser.add_argument(
            '-f',
            '--filename',
            dest='filename',
            help="""Write results to the specified file.""",
            action="append",
            default=None,
        )
        customparser.add_argument(
            '-d',
            '--delete',
            dest='del_key',
            action='append',
            help='Look for the key or keys in the ipprofile manager and delete',
            default=None,
        )
        customparser.add_argument(
            '-s',
            '--start',
            dest='start_ip',
            help='Copies the specified ip profile into the job queue and starts it',
            default=None,
        )
