#!/usr/bin/env python
import sys
import locale
import warnings
import argparse

from os.path import expanduser

from hetzner.robot import Robot

import logging

try:
    from ConfigParser import RawConfigParser
except ImportError:
    from configparser import RawConfigParser


def make_option(*args, **kwargs):
    return (args, kwargs)


class SubCommand(object):
    command = None
    description = None
    long_description = None
    option_list = []
    requires_robot = True

    def __init__(self, configfile):
        self.config = RawConfigParser()
        self.config.read(configfile)

    def putline(self, line):
        data = line + u"\n"
        try:
            sys.stdout.write(data)
        except UnicodeEncodeError:
            preferred = locale.getpreferredencoding()
            sys.stdout.write(data.encode(preferred, 'replace'))

    def execute(self, robot, parser, args):
        pass


class Reboot(SubCommand):
    command = 'reboot'
    description = "Reboot a server"
    option_list = [
        make_option('-m', '--method', dest='method',
                    choices=['soft', 'hard', 'manual'], default='soft',
                    help="The method to use for the reboot"),
        make_option('ip', metavar='IP', nargs='+',
                    help="IP address of the server to reboot"),

    ]

    def execute(self, robot, parser, args):
        for ip in args.ip:
            server = robot.servers.get(ip)
            if server:
                server.reboot(args.method)


class Rescue(SubCommand):
    command = 'rescue'
    description = "Activate rescue system"
    long_description = ("Reboot into rescue system, spawn a shell and"
                        " after the shell is closed, reboot back into"
                        " the normal system.")
    option_list = [
        make_option('-p', '--patience', dest='patience', type=int,
                    default=300, help=("The time to wait between subsequent"
                                       " reboot tries")),
        make_option('-m', '--manual', dest='manual', action='store_true',
                    default=False, help=("If all reboot tries fail,"
                                         " automatically send a support"
                                         " request")),
        make_option('-n', '--noshell', dest='noshell', action='store_true',
                    default=False, help=("Don't drop into a shell, only print"
                                         " rescue password")),
        make_option('ip', metavar='IP', nargs='+',
                    help="IP address of the server to put into rescue system"),
    ]

    def execute(self, robot, parser, args):
        for ip in args.ip:
            server = robot.servers.get(ip)
            if not server:
                continue

            kwargs = {
                'patience': args.patience,
                'manual': args.manual,
            }

            if args.noshell:
                server.rescue.observed_activate(**kwargs)
                msg = u"Password for {0}: {1}".format(server.ip,
                                                      server.rescue.password)
                self.putline(msg)
            else:
                with warnings.catch_warnings():
                    warnings.simplefilter("ignore")
                    server.rescue.shell(**kwargs)


class SetName(SubCommand):
    command = "set-name"
    description = "Change the name of a server"

    option_list = [
        make_option('ip', metavar='IP', help="IP address of the server"),
        make_option('name', metavar='NAME', nargs='?', default='',
                    help="New name of the server"),
    ]

    def execute(self, robot, parser, args):
        robot.servers.get(args.ip).set_name(args.name)


class ListServers(SubCommand):
    command = 'list'
    description = "List all servers"

    def execute(self, robot, parser, args):
        for server in robot.servers:
            info = {
                'id': server.number,
                'model': server.product
            }

            if server.name != "":
                info['name'] = server.name

            infolist = [u"{0}: {1}".format(key, val)
                        for key, val in info.items()]

            self.putline(u"{0} ({1})".format(server.ip, u", ".join(infolist)))


class ShowServer(SubCommand):
    command = 'show'
    description = "Show details about a server"
    option_list = [
        make_option('ip', nargs='+', metavar='IP',
                    help="IP address of the server"),
    ]

    def execute(self, robot, parser, args):
        for ip in args.ip:
            self.print_serverinfo(robot.servers.get(ip))

    def print_line(self, key, val):
        self.putline(u"{0:<15}{1}".format(key + u":", val))

    def print_serverinfo(self, server):
        info = [
            ("Number", server.number),
            ("Main IP", server.ip),
            ("Name", server.name),
            ("Product", server.product),
            ("Data center", server.datacenter),
            ("Traffic", server.traffic),
            ("Status", server.status),
            ("Cancelled", server.cancelled),
            ("Paid until", server.paid_until),
        ]

        for key, val in info:
            self.print_line(key, val)

        for ip in server.ips:
            if ip.rdns.ptr is None:
                addr = ip.ip
            else:
                addr = u"{0} (rPTR: {1})".format(ip.ip, ip.rdns.ptr)
            self.print_line(u"IP address", addr)

        for net in server.subnets:
            addrtype = u"IPv6" if net.is_ipv6 else u"IPv4"
            addr = u"{0}/{1} ({2})".format(net.net_ip, net.mask, addrtype)
            self.print_line(u"Subnet", addr)
            self.print_line(u"Gateway", net.gateway)

        for rdns in server.rdns:
            rptr = u"{0} -> {1}".format(rdns.ip, rdns.ptr)
            self.print_line(u"Reverse PTR", rptr)


class ReverseDNS(SubCommand):
    command = 'rdns'
    description = "List and set reverse DNS records"
    option_list = [
        make_option('-s', '--set', dest='setptr', action='store_true',
                    default=False, help="Set a new reverse PTR"),
        make_option('-d', '--delete', dest='delptr', action='store_true',
                    default=False, help="Delete reverse PTR"),
        make_option('ip', metavar='IP', nargs='?', default=None,
                    help="IP address of the server"),
        make_option('value', metavar='RPTR', nargs='?', default=None,
                    help="New reverse record to set"),
    ]

    def execute(self, robot, parser, args):
        if args.ip is None:
            for rdns in robot.rdns:
                self.putline("{0} -> {1}".format(rdns.ip, rdns.ptr))
        elif args.delptr:
            robot.rdns.get(args.ip).remove()
        elif args.setptr:
            if args.ip is None or args.value is None:
                parser.error("Need exactly two arguments: IP address and new"
                             " reverse FQDN.")
            else:
                rdns = robot.rdns.get(args.ip)
                rdns.set(args.value)
        else:
            rdns = robot.rdns.get(args.ip)
            if rdns.ptr is None:
                self.putline("No reverse record set for {0}.".format(rdns.ip))
            else:
                self.putline("{0} -> {1}".format(rdns.ip, rdns.ptr))


class Failover(SubCommand):
    command = 'failover'
    description = 'List and set failover IP addresses'

    option_list = [
        make_option('-s', '--set', dest='setfailover', action='store_true',
                    default=False,
                    help="Assign failover IP address to server"),
        make_option('ip', nargs='?', default=None,
                    help="Failover IP address to assign"),
        make_option('destination', nargs='?', default=None,
                    help="IP address of new failover destination")
    ]

    def execute(self, robot, parser, args):
        if args.setfailover:
            errs = []
            if not args.ip:
                errs.append("Error: you need to set the failover IP you"
                            " want to assign. Option 'ip'")
            if not args.destination:
                errs.append("Error: you need to set the new destination of"
                            " the failover IP. Option 'dest'")
            if len(errs) > 0:
                for err in errs:
                    self.putline(err)
            else:
                failover = robot.failover.set(args.ip, args.destination)
                self.putline("Failover IP successfully assigned to new"
                             " destination")
                self.putline(str(failover))
        else:
            failovers = robot.failover.list()
            if len(failovers) > 0:
                self.putline("Found %s failover IPs" % len(failovers))
            for failover in failovers.values():
                self.putline(str(failover))


class Admin(SubCommand):
    command = 'admin'
    description = "Create/delete dedicated admin accounts"
    option_list = [
        make_option('-C', '--create', dest='addadmin', action='store_true',
                    default=False, help="Create admin account"),
        make_option('-d', '--delete', dest='deladmin', action='store_true',
                    default=False, help="Delete admin account"),
        make_option('-p', '--password', dest='admpasswd', metavar='PASSWORD',
                    help="Use this password instead of generating one"),
        make_option('ip', metavar='IP', nargs='+', default=None,
                    help="IP address of the server"),
    ]

    def execute(self, robot, parser, args):
        for ip in args.ip:
            server = robot.servers.get(ip)
            if args.addadmin:
                login, passwd = server.admin.create(passwd=args.admpasswd)
                msg = "{0}: {1} -> {2}".format(server.ip, login, passwd)
                self.putline(msg)
            elif args.deladmin:
                server.admin.delete()
            else:
                if server.admin.exists:
                    msg = "{0}: {1}".format(server.ip, server.admin.login)
                else:
                    msg = "No admin account for {0}.".format(server.ip)
                self.putline(msg)


class Config(SubCommand):
    command = 'config'
    description = "Get or set options"
    long_description = ("Set options by just using `config section.option"
                        " value' or list options by not providing any"
                        " arguments.")
    option_list = [
        make_option('-d', '--delete', dest='delete', action='store_true',
                    default=False, help="Delete an option"),
        make_option('name', nargs='?', help="Section and name of the option"),
        make_option('value', nargs='?', default=None,
                    help="New value of the option"),
    ]
    requires_robot = False

    def execute(self, robot, parser, args):
        if args.name is None:
            for section in self.config.sections():
                for key, value in self.config.items(section):
                    self.putline("{0}.{1}={2!r}".format(section, key, value))
        else:
            if '.' not in args.name:
                parser.error("Option name needs to be in the form"
                             " <section>.<name>.")
            section, name = args.name.split('.', 1)
            if args.value is None:
                if not args.delete:
                    parser.error("In order to delete/unset an option, please"
                                 " use -d.")
                self.config.remove_option(section, name)
                if len(self.config.options(section)) == 0:
                    self.config.remove_section(section)
            else:
                if not self.config.has_section(section):
                    self.config.add_section(section)
                self.config.set(section, name, args.value)
            with open(args.configfile, 'w') as fp:
                self.config.write(fp)


def main():
    subcommands = [
        Config,
        Reboot,
        Rescue,
        SetName,
        ListServers,
        ShowServer,
        ReverseDNS,
        Admin,
        Failover,
    ]

    common_parser = argparse.ArgumentParser(
        description="Common options",
        add_help=False
    )
    global_options = common_parser.add_argument_group(title="global options")
    global_options.add_argument('-c', '--config', dest='configfile',
                                default='~/.hetznerrc', type=expanduser,
                                help="The location of the configuration file")
    global_options.add_argument('--debug', action='store_true',
                                help="Show debug output.")

    parser = argparse.ArgumentParser(
        description="Hetzner Robot commandline interface",
        prog='hetznerctl',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        parents=[common_parser]
    )

    subparsers = parser.add_subparsers(
        title="available commands",
        metavar="command",
        help="description",
    )

    for cmd in subcommands:
        subparser = subparsers.add_parser(
            cmd.command,
            help=cmd.description,
            description=cmd.long_description,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            parents=[common_parser]
        )
        for args, kwargs in cmd.option_list:
            subparser.add_argument(*args, **kwargs)
        subparser.set_defaults(cmdclass=cmd)

    args = parser.parse_args()

    logging.basicConfig(format='%(name)s: %(message)s',
                        level=logging.DEBUG if args.debug else logging.INFO)

    if getattr(args, 'cmdclass', None) is None:
        parser.print_help()
        parser.exit(1)
    subcommand = args.cmdclass(args.configfile)

    if subcommand.requires_robot:
        if not subcommand.config.has_option('login', 'username') or \
           not subcommand.config.has_option('login', 'password'):
            parser.error((
                "You need to set a user and password in {0} in order to"
                " continue with this operation. You can do this using"
                " `hetznerctl config login.username <your-robot-username>' and"
                " `hetznerctl config login.password <your-robot-password>'."
            ).format(args.configfile))
        robot = Robot(
            subcommand.config.get('login', 'username'),
            subcommand.config.get('login', 'password'),
        )
    else:
        robot = None
    subcommand.execute(robot, parser, args)


if __name__ == '__main__':
    main()
