#   Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
#   Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/licenses/UPL.

import argparse
import logging
import os
import sys
import time
import requests
try:  # python2
    import xmlrpclib
except ImportError:  # python3
    import xmlrpc.client as xmlrpclib

from osms import actions
from osms import config
from osms import utils
from osms.i18n import bstr
from osms.server import OsmsServer, get_cacert



# Action version we understand
ACTION_VERSION = 2

# SystemExit exception error code
SYSEXIT_CODE = 3

# Action maps
ACTIONS = {
    'errata.update': actions.errata_update,
    'packages.checkNeedUpdate': actions.package_check_need_update,
    'packages.fullUpdate': actions.package_full_update,
    'packages.refresh_list': actions.package_refresh_list,
    'packages.remove': actions.package_remove,
    'packages.update': actions.package_update,
    'packages.verify': actions.package_verify,
    'ksplice.upgrade': actions.ksplice_upgrade,
}

# Actions that will run each time we execute
LOCAL_ACTIONS = [
    ("packages.checkNeedUpdate", ()),
]


class Checker:
    def __init__(self):
        self.server = None
        self.terminate = False

    def run(self):
        if not config.getSystemId():
            raise Exception("Unable to read system id.")
        self.server = OsmsServer()
        self.run_remote_actions()
        self.run_local_actions()

    def rpc(self, method, *args):
        return getattr(self.server, method)(*args)

    def get_action(self, status_report):
        return self.rpc(
            'queue.get',
            config.getSystemId(),
            ACTION_VERSION,
            status_report
        )

    def submit_response(self, action_id, status, message, data):
        return self.rpc(
            'queue.submit',
            config.getSystemId(),
            action_id,
            status,
            message,
            data
        )

    def run_remote_actions(self):
        status_report = self.build_status_report()
        action = self.get_action(status_report)
        while action not in ['', {}]:
            if self.is_valid_action(action):
                self.handle_action(action)
            action = self.get_action(status_report)

    def parse_action_data(self, action):
        """ Parse action data and returns (method, params) """
        data = action['action']
        parser, decoder = xmlrpclib.getparser()
        parser.feed(bstr(data))
        parser.close()
        params = decoder.close()
        method = decoder.getmethodname()
        return method, params

    def handle_action(self, action, cache_only=None):
        logging.info("handle_action: %s", action)
        method, params = self.parse_action_data(action)
        status, message, data = self.run_action(method, params, {'cache_only': cache_only})
        logging.info("Package: %s, Status: %s", params, message)
        ret = 0
        if not cache_only:
            logging.debug("Sending back response: %s, %s, %s", status, message, data)
            ret = self.submit_response(action['id'], status, message, data)
        if self.terminate:
            raise Exception('Action handling terminated.')
        return ret

    def is_valid_action(self, action):
        logging.debug("check_action: %s", action)

        if not isinstance(action, dict):
            raise Exception("Got unparseable action response from server")

        for key in ['id', 'version', 'action']:
            if key not in action:
                raise Exception("Got invalid response - missing '%s'" % key)

        try:
            ver = int(action['version'])
        except ValueError:
            ver = -1

        if ver > ACTION_VERSION or ver < 0:
            logging.info("Got unknown action version %d", ver)
            # the -99 here is kind of magic
            self.submit_response(action["id"], -99, "Can not handle this version", {})
            return False

        return True

    def build_status_report(self):
        status_report = {}
        status_report["uname"] = list(os.uname())
        uptime = utils.get_uptime()
        if uptime:
            status_report['uptime'] = uptime
        effective_kernel = utils.get_effective_kernel()
        if effective_kernel:
            status_report['effective_kernel'] = effective_kernel
        needs_restarting = utils.needs_restarting()
        if needs_restarting:
            status_report['needs_restarting'] = '1'
        else:
            status_report['needs_restarting'] = '0'
        return status_report

    def run_local_actions(self):
        """Hit any actions that we want to always run."""
        for method_params in LOCAL_ACTIONS:
            method = method_params[0]
            params = method_params[1]
            status, message, data = self.run_action(method, params)
            logging.debug("local action status: %s, %s, %s", status, message, data)

    def run_action(self, method, params, kwargs=None):
        kwargs = kwargs or {}
        logging.debug("run_action: %s, %s, %s", method, params, kwargs)
        if method not in ACTIONS:
            logging.error('Invalid function call attempted: %s', method)
            return 6, 'Invalid function call attempted', {}
        try:
            return ACTIONS[method](*params, **kwargs)
        except SystemExit as err:
            self.terminate = True
            # Are we dealing with shutdown script? If yes, send some response to server ASAP.
            if err.code == SYSEXIT_CODE:
                extras = {
                    'output': '',
                    'base64enc': 0,
                    'return_code': 0,
                    'process_start': '',
                    'process_end': ''
                }
                for key in ('process_start', 'process_end'):
                    extras[key] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
                return 0, "Script executed. Termination signal occurred during execution", extras
        except Exception:
            logging.exception('Failed to run action')
            # The action code failed in some way. let's let the server know.
            return 6, 'Fatal error in Python code occurred', {}

class SelfTest:

    @staticmethod
    def create(server_url=None):
        if not server_url:
            server_url = config.getServerlURL()[0]
            server_url = server_url.replace('XMLRPC', 'agent-self-test')
        return SelfTest(server_url)

    def __init__(self, url, proxies=None, signer=None, ssl=True):
        self.url = url

    def __do_request(self, headers={}, proxies=None, auth=None, verify=True):
        return requests.get(self.url, headers=headers, verify=verify, proxies=proxies)


    def run(self):
        errmsg = "Unable to connect to server. " \
                                   "Please verify network connectivity and/or network security policies. Machines managed by " \
                                   "OSMS require one of the following: \n" \
                                   "1. A private subnet with a service gateway that uses the All <region> Services in Oracle Services Network CIDR label. \n" \
                                   "2. A private subnet with a NAT gateway. \n " \
                                   "3. A public subnet with an internet gateway."
        try:
            print("Performing Agent Self Test with service ...")
            resp = self.__do_request()
            doclink = "https://docs.oracle.com/en-us/iaas/os-management/osms/osms-getstarted.htm"
            msg = "There does not appear to be any network or connectivity issues." \
                              "If you are having issues with OSMS, check your Policies and Dynamic Group setup. " \
                              "See {0} for further details.".format(doclink)
        except requests.exceptions.ConnectionError:
            # In the event of a network problem (e.g. DNS failure, refused connection, etc),
            # Requests will raise a ConnectionError exception.
            msg = errmsg

        except requests.exceptions.Timeout:
            msg = errmsg
        finally:
            print("#########OSMS Connectivity Test Completed#########")
            print(msg)
            print("##################################################")





def main(cmds):
    epilog = 'See "%(prog)s COMMAND -h" for help on a specific command.'
    parser = argparse.ArgumentParser(epilog=epilog)
    parser.add_argument(
        '--log-file',
        default='/var/log/oracle-cloud-agent/plugins/osms/osms.log',
        help='Log file (default: %(default)s)'
    )
    parser.add_argument(
        '--log-level',
        default='INFO',
        choices=('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'),
        help='Log level (default: %(default)s)'
    )
    subparsers = parser.add_subparsers(title='Subcommands', metavar='')
    subparsers.required = True

    for name, cmd in sorted(cmds.items()):
        subparser = subparsers.add_parser(name, help=cmd['help'], description=cmd['help'])
        for cmdargs, kwargs in cmd.get('args', []):
            if kwargs.get('required'):
                kwargs['help'] += ' [required]'
            subparser.add_argument(*cmdargs, **kwargs)
        subparser.set_defaults(func=cmd['func'])

    args = parser.parse_args()
    utils.init_log(args.log_file, args.log_level)
    try:
        args.func(args)
    except Exception as err:
        logging.exception("Command failed")
        print('Command failed: %s' % err)
        sys.exit(1)
