#!/usr/bin/python3 -Es
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009-2014 Red Hat, Inc.
#
# Authors:
# Thomas Woerner <twoerner@redhat.com>
# Jiri Popelka <jpopelka@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

from gi.repository import GObject
import sys
sys.modules['gobject'] = GObject

import argparse
import os
import json

from rolekit.client import RolekitClient
from rolekit.errors import INVALID_NAME, NOT_AUTHORIZED, NOT_RUNNING
from rolekit.errors import RolekitError
from rolekit.server.io.rolesettings import RoleSettings
from rolekit.rolectl import generate_nextboot_unit
from rolekit.config import NASCENT

def __print(msg=None):
    if msg and not args.quiet:
        print(msg)

def __print_and_exit(msg=None, exit_code=0):
    FAIL = '\033[91m'
    OK =   '\033[92m'
    END =  '\033[00m'
    if exit_code != 0:
        __print(FAIL + msg + END)
    else:
        __print(msg)
        #__print(OK + msg + END)
    sys.exit(exit_code)

def __fail(msg=None):
    __print_and_exit(msg, 2)

def __print_if_verbose(msg=None):
    if msg and args.verbose:
        print(msg)

def __print_query_result(value):
    if value:
        __print_and_exit("yes")
    else:
        __print_and_exit("no")

def __exception_handler(exception_message):
    if "NotAuthorizedException" in exception_message:
        msg = """Authorization failed.
Make sure polkit agent is running or run the application as superuser."""
        __print_and_exit(msg, NOT_AUTHORIZED)
    else:
        code = RolekitError.get_code(exception_message)
        __print_and_exit("Error: %s" % exception_message, code)

def instance_type(arg):
    if not "/" in arg:
        raise ValueError("Not a valid instance, use <role/instance>")
    return arg

def create_settings_from_file(filename):
    try:
        with open(filename, "r") as f:
            try:
                settings = json.load(f)
            except Exception as e:
                __print_and_exit("Error: '%s' is not a valid json settings file: %s" % (filename, e))
        return settings
    except IOError as e:
        __print_and_exit("Error: Failed to open file '%s'" % filename)


def create_settings_from_stdin():
    try:
        settings = json.loads(''.join(sys.stdin.readlines()))
    except ValueError as e:
        __print_and_exit("Error: input is not valid JSON: %s" % e)
    return settings

parser = argparse.ArgumentParser(description="Rolekit command line control utility")
subparsers = parser.add_subparsers(help="Commands")
#subparsers.required = True


grp_output = parser.add_mutually_exclusive_group()
grp_output.add_argument("-v", "--verbose", action="store_true")
grp_output.add_argument("-q", "--quiet", action="store_true")


grp_version = subparsers.add_parser("version", help="Show rolekit version")
grp_version.set_defaults(parser="version")


grp_list = subparsers.add_parser("list", help="List roles or instances")
grp_list.set_defaults(parser="list")
grp_list_subparsers = grp_list.add_subparsers(help="Commands")


grp_list_roles = grp_list_subparsers.add_parser("roles", help="List roles")
grp_list_roles.set_defaults(list="roles")
grp_list_roles.add_argument("--object-path", action="store_true")


grp_list_instances = grp_list_subparsers.add_parser("instances", help="List instances")
grp_list_instances.set_defaults(list="instances")
grp_list_instances.add_argument("--state", metavar="<state>")
grp_list_instances.add_argument("--object-path", action="store_true")
grp_list_instances.add_argument("--verbose", action='store_const', const=True)


grp_settings = subparsers.add_parser("settings", help="Settings for roles or instances")
grp_settings.set_defaults(parser="settings")
grp_settings.add_argument("what", type=str, metavar="<role>|<role/instance>")
grp_settings.add_argument("--key", metavar="<key>")


grp_deploy = subparsers.add_parser("deploy", help="Deploy a role to create a role instance")
grp_deploy.set_defaults(parser="deploy")
grp_deploy.add_argument("role", type=str, action="store", metavar="<role>")
grp_deploy.add_argument("--name", metavar="<name>", default="")
grp_deploy.add_argument("--settings-file", type=str, metavar="<json settings file>", help="Read settings from a file. Not using a settings file will result in attempting to deploy with default settings. This will fail if the role has mandatory settings.")
grp_deploy.add_argument("--settings-stdin", action="store_true", help="Read settings from stdin. Not specifying settings will result in attempting to deploy with default settings. This will fail if the role has mandatory settings.")
grp_deploy.add_argument("--deferred", action="store_true", help="Configure rolekit to deploy the role on the next system boot. This is mostly useful for automated deployment tools such as kickstart.")

grp_redeploy = subparsers.add_parser("redeploy", help="Redeploy a role instance")
grp_redeploy.set_defaults(parser="redeploy")
grp_redeploy.add_argument("instance", type=instance_type, action="store", metavar="<role/instance>")
grp_redeploy.add_argument("--settings-file", type=str, metavar="<json settings file>", help="Read settings from a file. Not using a settings file will result in attempting to deploy with default settings. This will fail if the role has mandatory settings.")
grp_redeploy.add_argument("--settings-stdin", action="store_true", help="Read settings from stdin. Not specifying settings will result in attempting to deploy with default settings. This will fail if the role has mandatory settings.")


grp_status = subparsers.add_parser("status", help="Get status for role/instance")
grp_status.set_defaults(parser="status")
grp_status.add_argument("instance", type=instance_type, metavar="<role/instance>")


grp_start = subparsers.add_parser("start", help="Start a role instance")
grp_start.set_defaults(parser="start")
grp_start.add_argument("instance", type=instance_type, action="store", metavar="<role/instance>")


grp_stop = subparsers.add_parser("stop", help="Stop a role instance")
grp_stop.set_defaults(parser="stop")
grp_stop.add_argument("instance", type=instance_type, action="store", metavar="<role/instance>")


grp_restart = subparsers.add_parser("restart", help="Restart a role instance")
grp_restart.set_defaults(parser="restart")
grp_restart.add_argument("instance", type=instance_type, action="store", metavar="<role/instance>")


grp_update = subparsers.add_parser("update", help="Update a role instance")
grp_update.set_defaults(parser="update")
grp_update.add_argument("instance", type=instance_type, action="store", metavar="<role/instance>")


grp_sanitize = subparsers.add_parser("sanitize", help="Sanitize role settings")
grp_sanitize.set_defaults(parser="sanitize")
grp_sanitize.add_argument("instance", type=instance_type, action="store", metavar="<role/instance>")


grp_reset_error = subparsers.add_parser("reset-error", help="Reset error state in a role instance")
grp_reset_error.set_defaults(parser="reset-error")
grp_reset_error.add_argument("instance", type=instance_type, action="store", metavar="<role/instance>")


grp_decommission = subparsers.add_parser("decommission", help="Decommission a role instance")
grp_decommission.set_defaults(parser="decommission")
grp_decommission.add_argument("instance", type=instance_type, action="store", metavar="<role/instance>")
grp_decommission.add_argument("--force", action="store_true")


args = parser.parse_args()

# Check various impossible combinations of options

# use rolekit

try:
    # Skip connecting to the D-BUS if we are setting up for
    # next-boot. This is because the D-BUS probably isn't
    # present at all.
    connect_client = not args.deferred
except AttributeError:
    connect_client = True

if connect_client:
    rk = RolekitClient()
    rk.setExceptionHandler(__exception_handler)
    if rk.connected == False:
        # If we're queuing actions for the next boot, we will not
        # contact DBUS at all, so this is okay to ignore
        __print_and_exit ("Rolekit is not running", NOT_RUNNING)

try:
    command = args.parser
except AttributeError:
    # Python 3 does not handle this the same way; treat this similarly to --help
    parser.error("missing command")

# version
if command == "version":
    __print_and_exit(rk.get_property("version"))

#list
elif command == "list":
    try:
        list_type = args.list
    except AttributeError:
        grp_list.error("must specify 'roles' or 'instances'")

    if list_type == "roles":
        if args.object_path:
            __print_and_exit("\n".join(sorted(rk.getRoles())))
        __print_and_exit("\n".join(sorted(rk.getRoleNames())))
    elif list_type == "instances":
        if args.state:
            instances = rk.getAllRoleInstancesByState(args.state)
        else:
            instances = rk.getAllRoleInstances()

        if args.object_path:
            __print_and_exit("\n".join(instances))

        names = [ ]
        for i in instances:
            o = rk.getRoleInstanceObj(i)
            if args.verbose:
                names.append("%s/%s: %s" % (o.get_property("type"),
                                            o.get_property("name"),
                                            o.get_property("state")))
            else:
                names.append("%s/%s" % (o.get_property("type"),
                                        o.get_property("name")))
        __print_and_exit("\n".join(names))

# settings
elif command == "settings":
    if "/" in args.what:
        splits = args.what.split("/")
        obj = rk.getNamedRoleObj(splits[0]).getNamedInstanceObj(splits[1])
    else:
        obj = rk.getNamedRoleObj(args.what)

    if args.key:
        if "/" in args.what:
            __print_and_exit(obj.get_property(args.key))
        else:
            props = obj.get_properties()["DEFAULTS"]
            if args.key not in props:
                __print_and_exit("Error: No property '%s' in role '%s'" % \
                                 (args.key, args.what))
            __print_and_exit(props[args.key])

    if "/" in args.what:
        props = obj.get_properties()
    else:
        props = obj.get_properties()["DEFAULTS"]

    str = ""
    for key,value in props.items():
        str += "%s = %s\n" % (key, value)
    __print_and_exit(str)


# deploy
elif command == "deploy":
    if args.settings_file and args.settings_stdin:
        __print_and_exit("Only one of --settings-file or --settings-stdin may be specified.")

    if args.settings_file:
        settings = create_settings_from_file(args.settings_file)
    elif args.settings_stdin:
        settings = create_settings_from_stdin()
    else:
        settings = {}

    if args.deferred:
        # We've been asked to deploy this on the next boot.
        # Save the settings to the deferredroles directory

        # First ensure that we are root, since no other user
        # should be permitted to create deferred roles.
        # We cannot determine these permissions using polkit
        # because we are probably running in a system-
        # installer environment where DBUS is unavailable.
        if not os.geteuid() == 0:
            __print_and_exit("Only root is permitted to create roles on the next boot.")

        # If a name was specified, make sure it is not in use
        if args.name:
            if args.name in RoleSettings.get_instances(args.role):
                __print_and_exit("Instance name already in use", INVALID_NAME)

        try:
            deferredsettings = RoleSettings(args.role, args.name, deferred=True)
        except Exception as e:
            # Handle unexpected errors gracefully
            __fail(str(e))

        deferredsettings.update(settings)

        # Explicitly unset the state
        # If we are carrying a state value here, it will fail when we actually
        # do the deployment later.
        try:
            del deferredsettings["state"]
        except KeyError:
            # Don't fail if it wasn't set
            pass

        deferredsettings.write()

        # Create a oneshot systemd unit file to perform the deployment
        generate_nextboot_unit(args.role, deferredsettings.get_name(),
                               deferredsettings.filepath)
        __print("Deployment is prepped and will run on the next boot.")

    else:
        # TODO: Implement progress meter
        obj = rk.getNamedRoleObj(args.role)

        __print("Deployment can take a long time. To monitor the progress, run \n"
                "journalctl -ef -u rolekit")
        obj.deploy(args.name, settings)

elif command in [ "redeploy", "status", "start", "stop", "restart", "update",
                  "sanitize", "reset-error", "decommission" ]:

    splits = args.instance.split("/")
    obj = rk.getNamedRoleObj(splits[0]).getNamedInstanceObj(splits[1])


    # redeploy
    if command == "redeploy":
        if args.settings_file:
            obj.redeploy(create_settings_from_file(args.settings_file))
        elif args.settings_stdin:
            obj.redeploy(create_settings_from_stdin())
        else:
            obj.redeploy({ })


    # status
    elif command == "status":
        __print_and_exit(obj.get_property("state"))


    # start
    elif command == "start":
        obj.start()


    # stop
    elif command == "stop":
        obj.stop()


    # restart
    elif command == "restart":
        obj.restart()


    # update
    elif command == "update":
        obj.update()


    # sanitize
    elif command == "sanitize":
        obj.sanitize()


    # reset-error
    elif command == "reset-error":
        obj.resetError()


    # decommission
    elif command == "decommission":
        # TODO: Implement progress meter

        if obj.get_property("state") == "running":
            __print("Stopping services before decommissioning")
            obj.stop()

        __print("Decommissioning can take a long time. To monitor the "
                "progress, run \n"
                "journalctl -ef -u rolekit")
        obj.decommission(args.force)


else:
    __fail("Uncaught argument")


__print_and_exit("success")
