# -*- coding: utf-8 -*-
"""
Commands are called from the entry point generated by the setuptools. The set
of commands supported are:

  * gramola types                  : List of the datasource types supported.
  * gramola datasource             : Show a specific datasource.
  * gramola datasource-test        : Test a specific datasource.
  * gramola datasource-list        : List all datasources.
  * gramola datasource-rm          : Remove one datasource.
  * gramola datasource-add-<type>  : Add a new datasource.
  * gramola datasource-echo-<type> : Echo a datasource.
  * gramola query-<type>           : Run a metrics query.
  * gramola dashboard              : Show a specific dashboard.
  * gramola dashboard-list         : List all dashboards.
  * gramola dashboard-rm           : Remove a dashboard.
  * gramola dashboard-rm-query     : Remove a specific query from one dashboard.
  * gramola dashboard-query        : Run all dashboard metrics.

:moduleauthor: Pau Freixes, pfreixes@gmail.com
"""
from __future__ import print_function

import sys
import optparse
import sparkline

from time import sleep
from json import loads

from gramola import log
from gramola.plot import Plot, DEFAULT_ROWS
from gramola.store import (
    Store,
    NotFound,
    DuplicateEntry
)
from gramola.contrib.subcommand import (
    Subcommand,
    SubcommandsOptionParser
)
from gramola.datasources.base import (
    DataSource,
    InvalidMetricQuery,
    InvalidDataSourceConfig
)


class InvalidParams(Exception):
    def __init__(self, error_params):
        self.error_params = error_params
        Exception.__init__(self)


class GramolaCommand(object):
    # to be overriden by commands implementations
    NAME = None
    DESCRIPTION = None
    USAGE = None

    @staticmethod
    def execute(options, suboptions, *subargs):
        """This method is called by gramola entry point to perform
        the operations regarding a subcommand, overide this method
        with all of this stuff related with the subcomand

        :param options: Options given by the user as gramola options
        :param suboptions: Options given by the user as subcommand options
        :param subargs: Tuple args, each subcommand gives mean to each arg
                        belonging to this list.
        """
        raise NotImplemented()

    @staticmethod
    def options():
        """List of option args and kwargs to be used as a params for the
        parser.add_option function."""
        return []

    @classmethod
    def find(cls, command_name):
        """Returns the Command implementation for a specific command name."""
        try:
            return next(c for c in cls.__subclasses__() if c.NAME == command_name)
        except StopIteration:
            raise KeyError(command_name)

    @classmethod
    def commands(cls):
        """Returns the commands implementations"""
        return cls.__subclasses__()


class DataSourceCommand(GramolaCommand):
    NAME = 'datasource'
    DESCRIPTION = 'View the details of a saved datasource'
    USAGE = '%prog NAME'

    @staticmethod
    def execute(options, suboptions, *subargs):
        """ Returns info about one specific datasource as a dictionary with all
        key values saved.
        """
        try:
            name = subargs[0]
        except IndexError:
            raise InvalidParams("NAME")

        store = options.store and Store(path=options.store) or Store()
        try:
            print(store.datasources(name=name)[0].dumps())
        except IndexError:
            print("Datasource `{}` NOT FOUND".format(name))


class DataSourceTestCommand(GramolaCommand):
    NAME = 'datasource-test'
    DESCRIPTION = 'Test if the service behind a data source is available'
    USAGE = '%prog NAME'

    @staticmethod
    def execute(options, suboptions, *subargs):
        """ Test an already saved datasource."""
        try:
            name = subargs[0]
        except IndexError:
            raise InvalidParams("NAME")

        store = options.store and Store(path=options.store) or Store()
        try:
            config = store.datasources(name=name)[0]
        except IndexError:
            print("Datasource `{}` not found, NOT TESTED".format(name))
            return

        if DataSource.find(config.type)(config).test():
            print("Datasource `{}` seems ok".format(name))
        else:
            print("Datasource `{}` FAILED!".format(name))


class DataSourceRmCommand(GramolaCommand):
    NAME = 'datasource-rm'
    DESCRIPTION = 'Remove an already saved datasource'
    USAGE = '%prog NAME'

    @staticmethod
    def execute(options, suboptions, *subargs):
        """Remove an already saved datasource """
        try:
            name = subargs[0]
        except IndexError:
            raise InvalidParams("NAME")

        store = options.store and Store(path=options.store) or Store()
        try:
            store.rm_datasource(name)
            print("Datasource `{}` removed".format(name))
        except NotFound:
            print("Datasource `{}` not found, NOT REMOVED".format(name))


def build_datasource_echo_type(datasource):
    """
    Build the datasource-echo command for one type of datasource, it turns out
    in a new command named as datasource-echo-<type>.
    """
    class DataSourceEchoCommand(GramolaCommand):
        NAME = 'datasource-echo-{}'.format(datasource.TYPE)
        DESCRIPTION = 'Echo a datasource {} configuration'.format(datasource.TYPE)

        # All datasources inherited the `type` and the `name` fields as a
        # required params, typed commands have already the type and they are
        # removed as command args. And in the Echo case also the name.
        USAGE = '%prog {}'.format(" ".join(
            [s.upper() for s in datasource.DATA_SOURCE_CONFIGURATION_CLS.required_keys() if
                s not in ['name', 'type']]))

        @staticmethod
        def execute(options, suboptions, *subargs):
            """ Echo a datasource configuration to be used by query command, the arguments
            depend on the kind of datasource.
            """
            datasource_params = {
                # Type is a required param that is coupled with
                # with the command.
                'type': datasource.TYPE,

                # A echo command doesnt need the name keyword even it is required,
                # we share the same interface than the command to add datasources then
                # we have to give a automatic name to avoid raise an
                # InvalidDataSourceConfig error
                'name': 'stdout'
            }

            datasource_params.update(
                {k: v for v, k in
                    zip(subargs,
                        filter(lambda k: k not in ['name', 'type'],
                               datasource.DATA_SOURCE_CONFIGURATION_CLS.required_keys()))}
            )

            try:
                config = datasource.DATA_SOURCE_CONFIGURATION_CLS(**datasource_params)
            except InvalidDataSourceConfig, e:
                raise InvalidParams(e.errors)

            print(config.dumps())

        @staticmethod
        def options():
            # Command Options
            command_options = []

            # Datasource Options
            datasource_options = [
                ((option.hyphen_name,), {"dest": option.name, "help": option.description})
                for option in datasource.DATA_SOURCE_CONFIGURATION_CLS.optional_keys()]

            return command_options + datasource_options

    return DataSourceEchoCommand


def build_datasource_add_type(datasource_cls):
    """
    Build the datasource-add command for one type of datasource, it turns out
    in a new command named as datasource-add-<type>.
    """
    class DataSourceAddCommand(GramolaCommand):
        NAME = 'datasource-add-{}'.format(datasource_cls.TYPE)
        DESCRIPTION = 'Add a datasource {} configuration'.format(datasource_cls.TYPE)

        # All datasources inherited the `type` and the `name` fields as a
        # required params, typed commands have already the type and they are
        # removed as command args. And in the Echo case also the name.
        USAGE = '%prog {}'.format(" ".join(
            [s.upper() for s in datasource_cls.DATA_SOURCE_CONFIGURATION_CLS.required_keys() if
                s not in ['type']]))

        @staticmethod
        def execute(options, suboptions, *subargs):
            """ Add a datasource configuration to user store """
            datasource_params = {
                # Type is a required param that is coupled with
                # with the command.
                'type': datasource_cls.TYPE,
            }

            datasource_params.update(
                {k: v for v, k in
                    zip(subargs,
                        filter(lambda k: k not in ['type'],
                               datasource_cls.DATA_SOURCE_CONFIGURATION_CLS.required_keys()))}
            )

            # set also the optional keys given as suboptional params
            datasource_params.update(**{str(k): getattr(suboptions, str(k))
                                for k in filter(lambda k: getattr(suboptions, str(k)),
                                datasource_cls.DATA_SOURCE_CONFIGURATION_CLS.optional_keys())})

            try:
                config = datasource_cls.DATA_SOURCE_CONFIGURATION_CLS(**datasource_params)
            except InvalidDataSourceConfig, e:
                raise InvalidParams(e.errors)

            ds = datasource_cls(config)
            if not suboptions.no_test and not ds.test():
                # only save if the test passes
                print("Data source test failed, might the service not being unavailable ?")
                print("THIS DATA SOURCE NOT BE ADDED, use --no-test flag to add it even")
                return

            store = options.store and Store(path=options.store) or Store()
            try:
                store.add_datasource(config)
                print("Datasource `{}` added".format(subargs[0]))
            except DuplicateEntry:
                print("Datasource `{}` already exists, NOT SAVED".format(subargs[0]))


        @staticmethod
        def options():
            # Command Options
            command_options = [
                (("--no-test",), {"action": "store_true", "help": "Dont run the data source test"})]

            # Datasource Options
            datasource_options = [
                ((option.hyphen_name,), {"dest": option.name, "help": option.description})
                for option in datasource_cls.DATA_SOURCE_CONFIGURATION_CLS.optional_keys()]

            return command_options + datasource_options

    return DataSourceAddCommand


class DataSourceListCommand(GramolaCommand):
    NAME = 'datasource-list'
    DESCRIPTION = 'List all saved datasources'
    USAGE = '%prog'

    @staticmethod
    def execute(options, suboptions, *subargs):
        """ List all datasouces."""
        store = options.store and Store(path=options.store) or Store()
        for datasource in store.datasources():
            print("Datasource `{}` ({})".format(datasource.name, datasource.type))


def build_datasource_query_type(datasource_cls):
    """
    Build the query command for one type of datasource_cls, it turns out
    in a new command named as query-<type>.
    """

    class QueryCommand(GramolaCommand):
        NAME = 'query-{}'.format(datasource_cls.TYPE)
        DESCRIPTION = 'Query for a specific metric.'
        USAGE = '%prog {}'.format(" ".join(
            ['DATASOURCE_NAME'] +
            [s.upper() for s in datasource_cls.METRIC_QUERY_CLS.required_keys()]))

        @staticmethod
        def execute(options, suboptions, *subargs):
            """ Runs a query using a datasource_cls and prints it as a char graphic."""
            try:
                name = subargs[0]
            except IndexError:
                raise InvalidParams("NAME")

            if name == '-':
                buffer_ = sys.stdin.read()
                config = loads(buffer_)
            else:
                store = options.store and Store(path=options.store) or Store()
                try:
                    config = store.datasources(name=name)[0]
                except IndexError:
                    print("Datasource {} not found".format(name), file=sys.stderr)
                    return

            query_params = {
                k: v for v, k in zip(subargs[1:],
                                     datasource_cls.METRIC_QUERY_CLS.required_keys())
            }

            # set also the optional keys given as suboptional params
            query_params.update(**{str(k): getattr(suboptions, str(k))
                                for k in filter(lambda k: getattr(suboptions, str(k)),
                                datasource_cls.METRIC_QUERY_CLS.optional_keys())})

            try:
                query = datasource_cls.METRIC_QUERY_CLS(**query_params)
            except InvalidMetricQuery, e:
                raise InvalidParams(e.errors)

            try:
                datasource = datasource_cls(config)
            except InvalidDataSourceConfig, e:
                print("Datasource config invalid {}".format(e.errors), file=sys.stderr)
            else:
                plot = Plot(max_x=suboptions.plot_maxx)
                while True:
                    plot.draw(datasource.datapoints(query, maxdatapoints=plot.width()))
                    if not suboptions.refresh:
                        break
                    try:
                        sleep(int(suboptions.refresh_freq))
                    except KeyboardInterrupt:
                        break

        @staticmethod
        def options():
            # Command Options
            command_options = [
                (("--refresh",), {"action": "store_true", "default": False,
                                  "help": "Keep graphing forever, default False "}),
                (("--refresh-freq",), {"action": "store", "type": "int", "default": 10,
                                       "help": "Refresh frequency in seconds, default 10s"}),
                (("--plot-maxx",), {"action": "store", "type": "int", "default": None,
                                    "help": "Configure the maxium value X expected, otherwise the plot"+
                                    " will use the maxium value got by the time window" }),
                (("--plot-rows",), {"action": "store", "type": "int", "default": DEFAULT_ROWS,
                                    "help": "Configure the maxium value X expected, otherwise the plot"+
                                    " will use the maxium value got by the time window" }),
            ]

            # Datasource Options
            datasource_options = [
                ((option.hyphen_name,), {"dest": option.name, "help": option.description})
                for option in datasource_cls.METRIC_QUERY_CLS.optional_keys()]

            return command_options + datasource_options

    return QueryCommand


def gramola():
    """ Entry point called from binary generated by setuptools. Beyond
    the main command Gramola immplements a sub set of commands that each one
    implements an specific opeartion. The anatomy of a Gramola command looks like:

        $ gramola <global options> <subcommand> <subcommand options> <args ..>
    """
    # Build as many datasource-echo commands as many types of datasources there are.
    echo_commands = [build_datasource_echo_type(datasource)
                     for datasource in DataSource.implementations()]

    # Build as many datasource-add commands as many types of datasources there are.
    add_commands = [build_datasource_add_type(datasource)
                    for datasource in DataSource.implementations()]

    # Build as many query commands as many types of datasources there are.
    query_commands = [build_datasource_query_type(datasource)
                      for datasource in DataSource.implementations()]

    # Use the gramola.contrib.subcommands implementation to wraper the
    # GramolaCommands as a subcommands availables from the main command.
    subcommands = []
    for gramola_subcommand in GramolaCommand.commands():
        cmd = Subcommand(gramola_subcommand.NAME,
                         optparse.OptionParser(usage=gramola_subcommand.USAGE),
                         gramola_subcommand.DESCRIPTION)
        for option_args, option_kwargs in gramola_subcommand.options():
            cmd.parser.add_option(*option_args, **option_kwargs)

        subcommands.append(cmd)

    parser = SubcommandsOptionParser(subcommands=subcommands)
    parser.add_option('-s', '--store', dest='store',
                      help='alternative store directory, default ~/.gramola')
    parser.add_option('-q', dest='quite', help='Be quite', action='store_true')
    parser.add_option('-v', dest='verbose', help='Be verbose', action='store_true')

    options, subcommand, suboptions, subargs = parser.parse_args()
    log.setup(verbose=options.verbose, quite=options.quite)

    try:
        cmd = GramolaCommand.find(subcommand.name)
    except KeyError:
        print("Command not found {}".format(subcommand.name))
        print("")
        parser.print_help()
        sys.exit(1)

    try:
        cmd.execute(options, suboptions, *subargs)
    except InvalidParams, e:
        print("Invalid params for {} command, error: {}".format(subcommand.name, e.error_params))
        print("Get help with gramola {} --help".format(subcommand.name))
        sys.exit(1)
