#!/usr/bin/python
"""
simplevisor program
"""
import argparse
import os
import re
import sys

import simplevisor.mtb as mtb
from mtb.conf import read_apache_config, unify_keys
from mtb.modules import json
import mtb.log as log
import simplevisor
from simplevisor.Simplevisor import Simplevisor, \
    QUICK_COMMANDS, SERVICE_COMMANDS, OTHER_COMMANDS

COMMANDS = list(QUICK_COMMANDS)
COMMANDS.extend(SERVICE_COMMANDS)
COMMANDS.extend(OTHER_COMMANDS)
COMMANDS.extend(["pod", "rst", "help"])
COMMANDS = sorted(set(COMMANDS))
CHECK_ARG_COMMAND_RE = re.compile("(%s)" % "|".join(COMMANDS))
LOG_SYSTEMS = "null file syslog stdout".split()
CHECK_ARG_LOG_RE = re.compile("(%s)" % ("|".join(LOG_SYSTEMS), ))
CONF_GEN_OPTS = {
    "IncludeAgain":    True,
    "IncludeGlob":     True,
    "IncludeRelative": True,
    "InterPolateVars": True,
}

PROG = "simplevisor"
SHORT_DESCRIPTION = "simple daemons supervisor"
DESCRIPTION = """Simplevisor is a simple daemons supervisor, it is inspired
by Erlang OTP and it can supervise hierarchies of services.

COMMANDS

If a path is given or only one service entry is given:

for a given X command
    run the service X command where service is the only entry provided
    or the entry identified by its path

If a path is given and the root entry is a supervisor:

restart_child
    tell a running simplevisor process to restart the child identified
    by the given path; it is different from the restart command as
    described above because, this way, we are sure that the running
    simplevisor will not attempt to check/start/stop the child while
    we restart it

If a path is not given and the root entry is a supervisor:

start
    start the simplevisor process which start the supervision.
    It can be used with --daemon if you want it as daemon

stop
    stop the simplevisor process and all its children, if running

status
    return the status of the simplevisor process

check
    return the comparison between the expected state and the actual state.
    0 -> everything is fine
    1 -> warning, not expected

single
    execute one cycle of supervision and exit.
    Useful to be run in a cron script

wake_up
    tell a running simplevisor process to wake up and supervise

stop_supervisor
    only stop the simplevisor process but not the children

stop_children
    only stop the children but not the simplevisor process

check_configuration
    only check the configuration file

pod
    generate pod format help to be used by pod2man to generate man page

rst
    generate rst format help to be used in the web doc

help
    same as -h/--help, print help page


"""
EXAMPLES = """Create and edit the main configuration file::

    ## look for simplevisor.conf.example in the examples.

Run it::

    simplevisor --conf /path/to/simplevisor.conf start

to run it in daemon mode::

    simplevisor --conf /path/to/simplevisor.conf --daemon start

For other commands::

    simplevisor --help

Given the example configuration, to start the httpd service::

    simplevisor --conf /path/to/simplevisor.conf start svisor1/httpd
"""


def check_arg_command(command):
    """ Check command argument validity. """
    match = CHECK_ARG_COMMAND_RE.match(command)
    if match is None:
        msg = "command must be one of %s" % ", ".join(COMMANDS)
        raise argparse.ArgumentTypeError(msg)
    return command


class CommandAction(argparse._StoreAction):
    """ Intercept pod, rst and help actions. """
    def __call__(self, parser, namespace, values, option_string=None):
        if values == "pod":
            print_pod()
            sys.exit(0)
        elif values == "rst":
            print_rst()
            sys.exit(0)
        elif values == "help":
            parser.print_help()
            sys.exit(0)
        super(CommandAction, self).__call__(parser,
                                            namespace,
                                            values, option_string=None)
#        setattr(namespace, self.dest, values)


def check_arg_log(log_type):
    """ Check log argument validity. """
    match = CHECK_ARG_LOG_RE.match(log_type)
    if match is None:
        msg = "log must be one of %s" % ",".join(LOG_SYSTEMS)
        raise argparse.ArgumentTypeError(msg)
    return log_type


ARGUMENTS = [
    ("conf", {
        "long": "--conf",
        "help": "configuration file", }),
    ("conftype", {
        "long": "--conftype",
        "help": "configuration file type", }),
    ("daemon", {
        "long": "--daemon",
        "help": "daemonize, ONLY with start",
        "action": "store_true"}),
    ("interval", {
        "long": "--interval",
        "type": int,
        "help": "interval to wait between supervision cycles"}),
    ("help", {
        "short": "-h",
        "long": "--help",
        "action": "help",
        "help": "print the help page"}),
    ("log", {
        "long": "--log",
        "help": "available: %s" % (", ".join(LOG_SYSTEMS), ),
        "type": check_arg_log}),
    ("logfile", {
        "long": "--logfile",
        "help": "log file, ONLY for file"}),
    ("loglevel", {
        "long": "--loglevel",
        "help": "log level"}),
    ("logname", {
        "long": "--logname",
        "help": "log name"}),
    ("pidfile", {
        "short": "-p",
        "long": "--pidfile",
        "help": "the pidfile"}),
    ("store", {
        "long": "--store",
        "help": "file where to store the state, it is not mandatory, "
                "however recommended to store the simplevisor nodes "
                "status between restarts"}),
    ("version", {
        "long": "--version",
        "action": "version",
        "version": "%s %s" % (PROG, simplevisor.VERSION),
        "help": "print the program version"}),
    ("command", {
        "positional": True,
        "long": "command",
        "type": check_arg_command,
        "action": CommandAction,
        "help": "%s" % ", ".join(COMMANDS)}),
    ("path", {
        "positional": True,
        "long": "path",
        "nargs": "?",
        "help": "path to a service, subset of commands available: %s" %
                ", ".join(SERVICE_COMMANDS)}),
]

DEFAULT_OPTIONS = {
    "conftype": "apache",
    "daemon":   False,
    "log":      "stdout",
    "loglevel": "warning",
    "logname":  "simplevisor",
    "interval": 60,
    "path":     None,
    "pidfile":  None,
    "store":    None,
}


def get_parser():
    """ Create the parser. """
    parser = argparse.ArgumentParser(
        prog=PROG,
        description="%s\n\nEXAMPLES\n\n%s\n\nOPTIONS\n" %
        (DESCRIPTION, EXAMPLES),
        epilog="AUTHOR\n\n%s\n\n%s" %
        (simplevisor.AUTHOR, simplevisor.COPYRIGHT),
        argument_default=argparse.SUPPRESS,
        formatter_class=lambda prog:
        argparse.RawDescriptionHelpFormatter(
            prog, max_help_position=33))
    for name, elopts in ARGUMENTS:
        if name == "help":
            continue
        t_args = list()
        t_kwargs = elopts.copy()
        for arg in ["short", "long"]:
            if arg in t_kwargs:
                t_args.append(t_kwargs.pop(arg))
        t_kwargs.pop("positional", None)
        parser.add_argument(*t_args, **t_kwargs)
    return parser


def read_args():
    """ Read the arguments. """
    return vars(get_parser().parse_args())


def print_rst():
    """ Print the rst for the web page. """
    out = "simplevisor command\n"
    out += "===================\n\n"
    out += "%s %s - %s\n\n" % (PROG,
                               simplevisor.VERSION, SHORT_DESCRIPTION,)
    out += "SYNOPSIS\n"
    out += "--------\n\n"
    out += "**%s**\n" % PROG
    positional = ""
    optional = ""
    for _, elopts in ARGUMENTS:
        if elopts.get("positional", False):
            if elopts.get("nargs", None) == "?":
                positional += "[%s] " % (elopts.get("long"),)
            else:
                positional = "%s " % (elopts.get("long"),) + positional
        else:
            if elopts.get("action", None) is None:
                sname = " %s" % elopts.get("long").replace("-", "").upper()
            else:
                sname = ""
            if elopts.get("required ", False):
                optional += "%s%s" % \
                    (elopts.get("short", elopts.get("long")), sname)
            else:
                optional += "[%s%s] " % \
                    (elopts.get("short", elopts.get("long")), sname)
    out += "%s\n" % optional
    out += "%s\n\n" % positional
    out += "DESCRIPTION\n"
    out += "-----------\n\n"
    out += "%s\n\n" % DESCRIPTION
    out += "OPTIONS\n"
    out += "-------\n\n"
    positional = ""
    optional = ""
    for _, elopts in ARGUMENTS:
        if elopts.get("positional", False):
            part = "**%s**\n\t%s\n\n" % (elopts.get("long"),
                                         elopts.get("help"))
            if elopts.get("nargs", None) == "?":
                positional += part
            else:
                positional = part + positional
        else:
            clean_name = elopts["long"].replace("--", "")
            optional += "**"
            if "short" in elopts:
                optional += "%s, " % elopts["short"]
            optional += elopts["long"]
            if elopts.get("action", None) is None:
                optional += " %s" % elopts.get("long").replace("-", "").upper()
            else:
                optional += ""
            optional += "**\n\t%s" % elopts.get("help", "")
            if (clean_name in DEFAULT_OPTIONS and
                    elopts.get("action") != "store_true" and
                    DEFAULT_OPTIONS.get(clean_name) is not None):
                optional += " (default: %s)" % \
                    (DEFAULT_OPTIONS[clean_name])
            optional += "\n\n"
    out += "**positional arguments:**\n\n"
    out += positional
    out += "**optional arguments:**\n\n"
    out += optional
    for title, text in [("EXAMPLES", EXAMPLES),
                        ("AUTHOR", "%s - %s" % (simplevisor.AUTHOR,
                                                simplevisor.COPYRIGHT))]:
        out += "%s\n%s\n\n%s\n\n" % (title, len(title) * "-", text)
    out = out.replace("<{LIST_BEGIN}>", "")\
             .replace("<{LIST_END}>", "")
    print(out)


def print_pod():
    """ Print the pod for the man page. """
    out = "=head1 NAME\n\n"
    out += "%s %s - %s\n\n" % (PROG,
                               simplevisor.VERSION, SHORT_DESCRIPTION,)
    out += "=head1 SYNOPSIS\n\n"
    out += "B<%s>\n" % PROG
    positional = ""
    optional = ""
    for _, elopts in ARGUMENTS:
        if elopts.get("positional", False):
            if elopts.get("nargs", None) == "?":
                positional += "[%s]" % (elopts.get("long"),)
            else:
                positional = "%s " % (elopts.get("long"),) + positional
        else:
            if elopts.get("action", None) is None:
                sname = " %s" % elopts.get("long").replace("-", "").upper()
            else:
                sname = ""
            if elopts.get("required", False):
                optional += "%s%s" % \
                    (elopts.get("short", elopts.get("long")), sname)
            else:
                optional += "[%s%s]" % \
                    (elopts.get("short", elopts.get("long")), sname)
    out += "%s\n" % optional
    out += "%s\n\n" % positional
    out += "=head1 DESCRIPTION\n\n"
    out += "%s\n\n" % DESCRIPTION
    out += "=head1 OPTIONS\n\n"
    positional = ""
    optional = ""
    for _, elopts in ARGUMENTS:
        if elopts.get("positional", False):
            part = "B<%s> %s\n\n" % (elopts.get("long"),
                                     elopts.get("help"))
            if elopts.get("nargs", None) == "?":
                positional += part
            else:
                positional = part + positional
        else:
            clean_name = elopts["long"].replace("--", "")
            optional += "B<"
            if "short" in elopts:
                optional += "%s, " % elopts["short"]
            optional += elopts["long"]
            if elopts.get("action", None) is None:
                optional += " %s" % elopts.get("long").replace("-", "").upper()
            else:
                optional += ""
            optional += "> %s" % elopts.get("help", "")
            if (clean_name in DEFAULT_OPTIONS and
                    elopts.get("action") != "store_true" and
                    DEFAULT_OPTIONS.get(clean_name) is not None):
                optional += " (default: %s)" % \
                    (DEFAULT_OPTIONS[clean_name])
            optional += "\n\n"
    out += "B<positional arguments:>\n<{LIST_BEGIN}>\n"
    out += positional
    out += "<{LIST_END}>\n"
    out += "B<optional arguments:>\n<{LIST_BEGIN}>\n\n"
    out += optional
    out += "<{LIST_END}>\n"
    for title, text in [("EXAMPLES", EXAMPLES),
                        ("AUTHOR", "%s\n\n%s" % (simplevisor.AUTHOR,
                                                 simplevisor.COPYRIGHT))]:
        out += "=head1 %s\n\n%s\n\n" % (title, text)
    out = out.replace("::", ":")\
             .replace("**%s**" % PROG, "B<%s>" % PROG)\
             .replace("<{LIST_BEGIN}>", "\n=over\n")\
             .replace("<{LIST_END}>", "\n=back\n")\
             .replace("\n- ", "\n=item *\n\n")
    print(out)


SIMPLEVISOR_CONFIGURATION_FIELDS = [
    "command", "conftype", "daemon", "interval", "log",
    "logfile", "loglevel", "logname", "path", "pidfile", "store",
]


def check_restrictions(arguments):
    """ Check arguments restriction. """
    for key in arguments.keys():
        if key not in SIMPLEVISOR_CONFIGURATION_FIELDS:
            raise ValueError(
                "%s is not a valid configuration property" % key)
    if (arguments["daemon"] and
            arguments["path"] is not None and
            arguments["command"] != "start"):
        raise argparse.ArgumentTypeError(
            "daemon option make sense only with start command")


def merge_dicts(first, second):
    """ Merge two dictionaries. """
    merge = first.copy()
    merge.update(second)
    return merge


def initialize_log(config):
    """ Initialize the log system. """
    handler_options = dict()
    if config["log"] == "file":
        if "logfile" not in config:
            raise AttributeError("logfile required for file log system")
        handler_options["filename"] = config["logfile"]
    extra = {"handler_options": handler_options}
    log.setup_log(config["logname"], config["log"], config["loglevel"], extra)


def main():
    """ Do the work. """
    args = read_args()
    conf_file = args.pop("conf", None)
    if conf_file is None:
        conf = dict()
    else:
        conf_path = os.path.abspath(conf_file)
        conf_type = args.pop("conftype", DEFAULT_OPTIONS["conftype"])
        if conf_type == "apache":
            conf = read_apache_config(conf_path, CONF_GEN_OPTS)
        elif conf_type == "json":
            conf_fp = open(conf_path, "r")
            conf = json.load(conf_fp)
            conf_fp.close()
        else:
            raise ValueError("invalid configuration type: %s" % (conf_type, ))
    entry = conf.pop("entry", None)
    unify_keys(entry)
    sconf = conf.pop("simplevisor", dict())
    sconf = merge_dicts(merge_dicts(DEFAULT_OPTIONS, sconf), args)
    unify_keys(sconf)
    check_restrictions(sconf)
    if conf:
        raise ValueError("invalid configuration: %s" % (conf, ))
    initialize_log(sconf)
    svisor = Simplevisor(config=sconf, child_config=entry)
    svisor.work()


if __name__ == "__main__":
    main()
