#!/usr/bin/env python3

import  logging
logging.disable(logging.CRITICAL)

import  os
import  sys
import  json
import  socket
import  requests
import  ast
import  pudb

import  pfmisc
from    chrisclient         import  search
from    argparse            import  Namespace

# pfstorage local dependencies
from    pfmisc._colors      import  Colors
from    pfmisc.debug        import  debug
from    pfmisc.C_snode      import  *
from    pfstate             import  S

class D(S):
    """
    A derived 'pfstate' class that keeps system state.

    See https://github.com/FNNDSC/pfstate for more information.
    """

    def __init__(self, *args, **kwargs):
        """
        An object to hold some generic/global-ish system state, in C_snode
        trees.
        """
        d_sysArgs   : dict      = {}
        d_CUBE      : dict      = {
                "protocol":     "http",
                "port":         "8000",
                "address":      "%HOSTIP",
                "user":         "chris",
                "password":     "chris1234",
        }
        for k,v in kwargs.items():
            if k == 'args': d_sysArgs   = v
        if len(d_sysArgs['str_CUBE']):
            d_CUBE  = ast.literal_eval("".join(d_sysArgs['str_CUBE'].split()))
        if len(d_sysArgs['str_CUBEaddress']):
            d_CUBE['address']   = d_sysArgs['str_CUBEaddress']
        if len(d_sysArgs['str_CUBEport']):
            d_CUBE['port']      = d_sysArgs['str_CUBEport']
        self.state_create(
        {
            "CUBE": d_CUBE,
            "self":
            {
                'httpProxy':
                {
                    'use':      False,
                    'httpSpec': ''
                }
            },
            "this":
            {
                'verbosity':    0,
                'colorize':     False
            }
        },
        *args, **kwargs)

class PluginRun(object):
    """
    A class that interacts with CUBE via the collection+json API
    and is specialized to "run" a plugin via POST requests to the
    backend.
    """

    def S(self, *args):
        """
        set/get components of the state object
        """
        if len(args) == 1:
            return self.state.T.cat(args[0])
        else:
            self.state.T.touch(args[0], args[1])

    def CUBE_IPspec(self):
        """
        Check and update the %HOSTIP if required
        """

        IP = self.S('/CUBE/address')
        if IP == "%HOSTIP":
            self.S('/CUBE/address', self.d_meta['defIP'])

    def __init__(self, d_meta, *args, **kwargs):
        """
        Class constructor.
        """

        # Structures to contain the "CLI" of the plugin
        # to be scheduled/run
        self.d_args         : dict  = vars(*args)
        self.d_CLIargs      : dict  = {}
        self.d_CLItemplate  : dict  = {}

        # Some additional "massaging" from these CLI
        # to CLI appropriate for the search module:
        self.d_args['str_using']    = self.d_args['str_pluginSpec']
        self.d_args['str_for']      = 'id'
        ns                          = Namespace(**self.d_args)

        # The search module -- used to determine the plugin ID
        # in ChRIS/CUBE
        self.query                  = search.PluginSearch(d_meta, ns)
        self.str_pluginID   : str   = ''

        # Generic "state" data, mostly describing the
        # compute environment in which CUBE exists
        self.d_meta     = d_meta
        self.state      = D(
            version     = d_meta['version'],
            name        = d_meta['name'],
            desc        = d_meta['desc'],
            args        = vars(*args)
        )

        # A debug/print object
        self.dp         = pfmisc.debug(
            verbosity   = int(self.d_args['verbosity']),
            within      = d_meta['name'],
            syslog      = self.d_args['b_syslog'],
            colorize    = self.state.T.cat('/this/colorize')
        )

        # Quick housekeeping
        self.CUBE_IPspec()

    def pluginCLIargs_parse(self):
        """
        Parse the string of CLI args into a dictionary
        structure.
        """
        l_args      :   list    = []
        l_keyval    :   list    = []
        b_add       :   bool    = True
        str_message :   str     = "'--args' is empty!"

        if len(self.d_args['str_args']):
            l_args  = self.d_args['str_args'].split(';')
            for str_keyval in l_args:
                l_keyval    = str_keyval.split('=')
                if len(l_keyval) == 1:
                    key         = l_keyval[0]
                    val         = True
                if len(l_keyval) == 2:
                    (key, val)  = l_keyval
                    val         = val.rstrip('"-=')
                if b_add:
                    # For the set [ " - = ], remove any leading/
                    # trailing hits in the <key> as well as any
                    # trailing hits in the <val>.
                    key         = "".join(key.split())
                    key         = key.strip('"-=')
                    self.d_CLIargs.update({key: val})
        if b_add:
            str_message     = '%d args parsed' % len(l_args)
        return {
            'status':   b_add,
            'message':  str_message,
            'CLIdict':  self.d_CLIargs
        }

    def pluginArgs_templateCreate(self, d_argsParse):
        """
        Create a collectio+json template from the intenal
        plugin argument dictionary
        """
        b_status    : bool  = True
        self.d_CLItemplate  = {'data' : []}
        str_message : str   = 'template creation failed due to earlier error'
        keyCount    : int   = 0

        if d_argsParse['status']:
            for key in self.d_CLIargs:
                self.d_CLItemplate['data'].append(
                    {
                        'name':     key,
                        'value':    self.d_CLIargs[key]
                    }
                )
                keyCount += 1
        if b_status:
            str_message  = '%s key(s) parsed into template' % keyCount
        return {
            'status':       b_status,
            'message':      str_message,
            'template':     self.d_CLItemplate,
            'argsParse':    d_argsParse
        }

    def pluginRun_CUBEAPIcall(self, d_templatize):
        """

        This method POSTs the call to the CUBE API to perform
        the actual plugin execution.

        """
        d_resp              : dict  = {}
        b_status            : bool  = False
        str_message         : str   = 'CUBE API not called because of earlier error'

        if d_templatize['status']:
            d_headers           : dict = {
                'Accept':       'application/vnd.collection+json',
                'Content-Type': 'application/vnd.collection+json'
            }
            str_dataServiceAddr : str  = "%s://%s:%s" % (
                                    self.S('/CUBE/protocol'),
                                    self.S('/CUBE/address'),
                                    self.S('/CUBE/port')
                                )
            str_dataServiceURL  : str  = 'api/v1/plugins/%s/instances/' % \
                                    self.str_pluginID
            str_user            : str  = self.S('/CUBE/user')
            str_passwd          : str  = self.S('/CUBE/password')
            str_URL             : str  = '%s/%s' % (
                                    str_dataServiceAddr,
                                    str_dataServiceURL
                                )
            try:
                resp = requests.post(
                                    str_URL,
                                    data    = json.dumps(
                                        {'template' : self.d_CLItemplate}
                                    ),
                                    auth    = (str_user, str_passwd),
                                    timeout = 30,
                                    headers = d_headers
                        )
                b_status    = True
                str_message     = "CUBE call return a response"
            except (requests.exceptions.Timeout,
                    requests.exceptions.RequestException) as e:
                logging.error(str(e))
                str_message     = "CUBE call return some error"
                raise
            d_resp = resp.json()
        return {
            'status':       b_status,
            'templatize':   d_templatize,
            'response':     d_resp,
            'message':      str_message
        }

    def pluginInstanceID_find(self, d_run):
        """
        Once a job has been POSTed, parse the return from
        CUBE for the plugin instance ID.
        """
        b_status        :   bool    = False
        l_thistarget    :   list    = []
        l_target        :   list    = []
        str_message     :   str     = 'No instance ID found due to earlier error'

        if d_run['status']:
            if 'error' in d_run['response']['collection'].keys():
                str_message = d_run['response']['collection']['error']['message']
            if 'items' in d_run['response']['collection'].keys():
                for d_hit in d_run['response']['collection']['items']:
                    l_data  :   list = d_hit['data']
                    l_thistarget     = []
                    for str_desired in self.d_args['str_for'].split(','):
                        l_hit   :   list = list(
                                    filter(
                                        lambda info: info['name'] == str_desired, l_data
                                        )
                                    )
                        if len(l_hit):
                            l_thistarget.append(l_hit[0])
                            b_status    = True
                    l_target.append(l_thistarget)
                str_message     = '%d instance ID(s) found' % len(l_target)
        return {
            'status':   b_status,
            'run':      d_run,
            'target':   l_target,
            'message':  str_message
        }

    def do(self):
        """
        Main entry point to this class.
        """
        b_status    :   bool    = False
        str_message :   str     = ''
        d_query     :   dict    = {}
        d_targetID  :   dict    = {}
        d_run       :   dict    = {}

        # First, find the plugin ID for the desired plugin
        # to run
        d_query             = self.query.do()
        if len(d_query['target']) == 1:
            d_targetID          = d_query['target'][0][0]
            self.str_pluginID   = d_targetID['value']

            # and now, run it!
            d_run:  dict    = self.pluginInstanceID_find(
                                self.pluginRun_CUBEAPIcall(
                                    self.pluginArgs_templateCreate(
                                        self.pluginCLIargs_parse()
                                    )
                                )
                            )
            if d_run['status']:
                str_message = 'plugin scheduled successfully'
                b_status    = True
            else:
                str_message = 'plugin run NOT scheduled'
        else:
            str_message     = "plugin query MUST return a single hit"
        return {
            'status':       b_status,
            'query':        d_query,
            'run':          d_run,
            'message':      str_message
        }


