#!/usr/bin/env python
from WMCore.WebTools.Page import exposexml, exposejson
from WMCore.WebTools.Page import exposedasplist, exposedasjson, exposedasxml
from WMCore.WebTools.DatabasePage import DatabasePage
from Utils.Utilities import encodeUnicodeToBytes
from cherrypy import expose, HTTPRedirect
from Utils.CPMetrics import promMetrics
import sys
import traceback

# third-party libraries
from cherrypy.lib import cpstats

class WebAPI(DatabasePage):
    __version__ = 1
    def __init__(self, config = {}):
        """
        __WebAPI__
        To create an auto-documenting API, make a subclass of the WebAPI class,
        add in methods that represent the api calls (no need to expose them),
        document them clearly in the methods doc strings and then add to the
        self.methods dictionary as follows:

        self.methods = {'method1':{'args':[],'call':self.method1},
                        'method2':{'args':['id'], 'call': self.method2}}

        WebAPI is a subclass of the DatabasePage to make a database connection
        available to the class in a standard way.

        binds = {'id': 123}
        sql = "select * from table where id = :id"
        result = self.dbi.processData(sql, binds)
        return self.formatDict(result)

        This can be used in the API methods. You may want to have a read-only
        API and a write-only API (with appropriately permissions on the DB
        connections) to minimise risk of SQL injection attacks (always use binds
        variables!).
        """
        DatabasePage.__init__(self, config)
        self.methods = {'status':{'args':[],
                                 'call':self.status,
                                 'version': 1}}

    def status(self):
        """
        Return the application name and it's version. Suitable for a ping.
        """
        return {'application': self.config.application,
                'version': self.__version__}

    @expose
    def stats(self):
        """
        Return CherryPy stats dict about underlying service activities
        """
        return cpstats.StatsPage().data()

    @expose
    def metrics(self):
        """
        Return CherryPy stats following the prometheus metrics structure
        """
        name = self.__class__.__name__
        metrics = promMetrics(cpstats.StatsPage().data(), name)
        return encodeUnicodeToBytes(metrics)

    @expose
    def index(self, **kwargs):
        """
        Return the auto-generated documentation for the API
        """
        return self.templatepage('API', methods = self.methods,
                                 webapp = self.config.application)

    @expose
    def default(self, *args, **kwargs):
        """
        Return the autogenerated documentation for the API (by calling index())
        TODO: decide whether to use args to call a method...
        """
        return self.index()

    #TODO: Validate input for json, das and xml
    @exposejson
    def json(self, *args, **kwargs):
        """
        The json output. args is assumed to be length 1 and contain the method
        name, kwargs are passed to the method
        """
        if len(args) > 0:
#            dict = self.runMethod(args[0], kwargs)
#            return dict
            res = self.runMethod(args[0], kwargs)
            if  isinstance(res, str):
                res = eval(res)
            return res
        else:
            raise HTTPRedirect("doc")

    def dasOutput(self, method, kwargs):
        """
        Make the API call and build a DAS compatible dictionary.
        """
        idict = {}
        try:
            idict['request_version'] = self.methods[method]['version']
        except:
            idict['request_version'] = 0
        idict['request_call'] = method
        idict[method] = self.runMethod(method, kwargs)
        return dict

    @exposedasjson
    def das(self, *args, **kwargs):
        """
        The das compatible json output. args is assumed to be length 1 and
        contain the method name, kwargs are passed to the method
        """
        try:
            return self.dasOutput(args[0], kwargs)
        except:
            raise HTTPRedirect("doc") from None

    @exposedasxml
    def dasxml(self, *args, **kwargs):
        """
        The das compatible xml output. args is assumed to be length 1 and
        contain the method name, kwargs are passed to the method
        """
        try:
            return self.dasOutput(args[0], kwargs)
        except:
            raise HTTPRedirect("doc")

    @exposedasplist
    def plist(self, *args, **kwargs):
        """
        The das compatible xml output. args is assumed to be length 1 and
        contain the method name, kwargs are passed to the method
        """
        try:
            res = self.runMethod(args[0], kwargs)
            if  isinstance(res, str):
                res = eval(res)
            return res
        except:
            raise HTTPRedirect("doc")

    @exposexml
    def xml(self, *args, **kwargs):
        """
        The xml output. args is assumed to be length 1 and contain the method
        name, kwargs are passed to the method
        """
        if len(args) > 0:
            res = self.runMethod(args[0], kwargs)
            if  isinstance(res, str):
                res = eval(res)
            return res
        else:
            raise HTTPRedirect("doc")

    def runMethod(self, method, userInput):
        """
        Run the specified method with the provided input, return a dict
        containing the result of the call or an exception wrapped in a dict.
        """
        idict = {}
        try:
            assert method in self.methods, "Unknown method called"
            if len(input):
                dict = self.methods[method]['call'](**input)
            else:
                idict = self.methods[method]['call']()
        except Exception as e:
            error = e.__str__()
            self.debug(error)
            self.debug(traceback.print_exc())
#            self.debug("%s:%s" % (sys.exc_type, sys.exc_value))
            idict = {'Exception':{'Exception_thrown_in': method,
                       'Exception_type': '%s' % sys.exc_info()[0],
                       'Exception_detail':error,
                       'Exception_arguments': input,
                       'Exception_dict':dict}}
        return idict
