#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import print_function

import asyncio
import copy
import glob
import json
import os.path
import re
import sys
from collections import OrderedDict
import configparser

from papavisor.aioxmlrpc_client import AioTransport, ProtocolError, ServerProxy

STATUS_REGEX = re.compile('^([\w]+)\s+([\w]+)')


def merge(a, b, path=None):
    """merges b into a
    http://stackoverflow.com/questions/7204805/dictionaries-of-dictionaries-merge
    """
    if path is None:
        path = []

    for key in b:
        if key in a:
            if isinstance(a[key], dict) and isinstance(b[key], dict):
                merge(a[key], b[key], path + [str(key)])
            elif a[key] == b[key]:
                pass  # same leaf value
            else:
                a[key] = b[key]
                # raise Exception('Conflict at %s' % '.'.join(path + [str(key)]))
        else:
            a[key] = b[key]
    return a


class SupervisorCtl(object):

    def __init__(self, config):
        self._name = config['name']
        self._rpc = ServerProxy(
            config['url'],
            AioTransport(
                use_https=False,
                username=config['user'],
                password=config['password']
            )
        )
        self._rpcns = getattr(self._rpc, 'supervisor')

        self._config = config
        self._groups = {}
        self._programs = {}

    @asyncio.coroutine
    def _get_status(self):
        cfg_programs = self._config['programs']

        status = yield from self._rpcns.getAllProcessInfo()
        programs = {}
        for p in status:
            pn = p['name']
            if pn not in cfg_programs:
                print(
                    "%s\tERROR: Unknown service name %r" % (
                        self._name, pn
                    ),
                    file=sys.stderr
                )
                continue

            programs[pn] = p
            programs[pn]['startsecs'] = cfg_programs[pn].get('startsecs', 0)

        if not self._groups:
            groups = {}
            for pn, pstatus in programs.items():
                if pn not in cfg_programs:
                    print(
                        "%s\tERROR: Unknown service name %r" % (
                            self._name, pn
                        ),
                        file=sys.stderr
                    )
                    continue

                sdefs = cfg_programs[pn]
                for sgroup in sdefs['groups']:
                    if sgroup not in groups:
                        groups[sgroup] = {}

                    groups[sgroup][pn] = sdefs['priority']

            for k, g in groups.items():
                self._groups[k] = OrderedDict(
                    sorted(g.items(), key=lambda p: p[1])
                )

        self._programs = programs

    def close(self):
        if self._rpc is not None:
            self._rpc.close()

    @asyncio.coroutine
    def stop(self, group_or_program):
        """Stop the given group name. """
        yield from self._get_status()

        if group_or_program in self._groups:
            yield from self._stop_group(group_or_program)
        elif group_or_program in self._programs:
            yield from self._stop_program(group_or_program)
        else:
            raise ValueError("Unknown group or program %r" % group_or_program)

    @asyncio.coroutine
    def _stop_program(self, p):
        print("Stop program %r from project %r:" % (p, self._name))
        status = self._programs[p]['statename']
        if status == 'RUNNING' or status == 'STARTING':
            print("%s\tstop\t%s" % (self._name, p))
            yield from self._rpcns.stopProcess(p)

    @asyncio.coroutine
    def _stop_group(self, group):
        print("Stop group %r from project %r:" % (group, self._name))
        for p in self._groups[group]:
            yield from self._stop_program(p)

    @asyncio.coroutine
    def start(self, group_or_program):
        """Start the given group name. """
        yield from self._get_status()

        if group_or_program in self._groups:
            yield from self._start_group(group_or_program)
        elif group_or_program in self._programs:
            yield from self._start_program(group_or_program)
        else:
            raise ValueError("Unknown group or program %r" % group_or_program)

    @asyncio.coroutine
    def _start_group(self, group):
        print("Start group %r from project %r:" % (group, self._name))
        for p in self._groups[group]:
            yield from self._start_program(p)

    @asyncio.coroutine
    def _start_program(self, p, force=False, check_or_block=False):
        print("Start program %r from project %r:" % (p, self._name))
        status = self._programs[p]['statename']
        if force or status == 'STOPPED' or status == 'STOPPING':
            print("%s\tstart\t%s" % (self._name, p))
            yield from self._rpcns.startProcess(p)
            if check_or_block:
                st = self._programs[p]['startsecs']
                if st > 0:
                    print("%s\tsleep\t%s\t%d" % (self._name, p, st))
                    yield from asyncio.sleep(st)

    @asyncio.coroutine
    def restart(self, group):
        """Restart the given group name."""
        yield from self._get_status()

        if group not in self._groups:
            raise ValueError("Unknown group %r" % group)

        # check which programs are running and select them
        # for a restart.
        print("Restart group %r from project %r:" % (group, self._name))

        to_restart = []
        for p in self._groups[group].keys():
            pdata = self._programs[p]
            if pdata['statename'] == 'RUNNING':
                to_restart.append(p)

        # Stop selected programs
        for p in to_restart:
            yield from self._stop_program(p)

        # and start them in reversed order again.
        for p in reversed(to_restart):
            yield from self._start_program(p, force=True, check_or_block=True)

    @asyncio.coroutine
    def status(self, group):
        yield from self._get_status()

        if group not in self._groups:
            raise ValueError("Unknown group %r" % group)

        for p in self._groups[group].keys():
            pdata = self._programs[p]
            print("%s\t%s\t%s\t%s" % (
                self._name,
                p + ' ' * (15 - len(p)),
                pdata['statename'],
                pdata['description'])
            )


def parse_json_config(directory_path, config_object):
    if not os.path.exists(directory_path):
        return

    cfg_files = glob.glob(directory_path + '/*.json')
    for cfg_file in cfg_files:
        with open(cfg_file, 'r') as fp:
            try:
                tmp_cfg = json.load(fp, object_pairs_hook=OrderedDict)
            except ValueError:
                print(cfg_file)
                raise

        merge(config_object, tmp_cfg)


@asyncio.coroutine
def _run_task_async(config, action='restart', group='python'):
    sctl = SupervisorCtl(config)

    try:
        if action.lower() == 'restart':
            yield from sctl.restart(group)

        elif action.lower() == 'start':
            yield from sctl.start(group)

        elif action.lower() == 'stop':
            yield from sctl.stop(group)

        elif action.lower() == 'status':
            yield from sctl.status(group)

    except ProtocolError:
        print('ERROR: %r can\'t connect.' % (
            config['name']
        ))
    finally:
        sctl.close()


def main(argv):
    config_path = os.path.join(
        os.path.dirname(__file__),
        os.path.pardir,
        os.path.pardir,
        os.path.pardir,
        'config'
    )

    if not os.path.exists(config_path):
        config_path = '/etc/papavisor'

    config = OrderedDict()
    parse_json_config(config_path, config)

    service_defaults = config['__defaults__']
    del config['__defaults__']

    # env CONFIG_FILES for apapavisor
    if 'CONFIG_FILES' in os.environ:  # and os.environ['CONFIG_FILES']:
        files = os.environ['CONFIG_FILES'].rstrip(';').split(';')
        try:
            d = OrderedDict(
                [f.split(':') for f in files]
            )
            for name, cfgfile in d.items():
                if name in config:
                    # Do not overwrite a manual configured entry.
                    continue

                parser = configparser.ConfigParser()
                parser.read(cfgfile)
                cfg_opts = {
                    'url': parser['supervisorctl']['serverurl'],
                    'user': parser['supervisorctl']['username'],
                    'password': parser['supervisorctl']['password']
                }

                config[name] = cfg_opts
        except ValueError:
            print(
                "ERROR: Wrong input form apapavisor: %r" % (
                    os.environ['CONFIG_FILES'],
                ),
                file=sys.stderr
            )

    to_run = config  # default all projects
    if len(sys.argv) > 1 and sys.argv[1].lower() != 'all':
        projects = sys.argv[1].lower()
        to_run = {}
        for k in config.keys():
            # use startswith so users can provide "half" project names.
            if k.startswith(projects):
                to_run[k] = config[k]

    if not to_run:
        return True

    action = 'status'  # default run status
    if len(sys.argv) > 2 and sys.argv[2].lower() != action:
        action = sys.argv[2].lower()

    group = 'all'  # default "group" all
    if len(sys.argv) > 3 and sys.argv[3].lower() != group:
        group = sys.argv[3].lower()

    # print('Running action %r on group %r' % (action, group))

    tasks = []
    for sname, sconfig in to_run.items():

        local_config = copy.deepcopy(service_defaults)
        merge(local_config, sconfig)

        if 'types' in local_config:
            for tk, tv in local_config['types'].items():
                for pk, pv in local_config['programs'].items():
                    if pv['type'] == tk:
                        for tvk, tvv in tv.items():
                            pv[tvk] = tvv

            del(local_config['types'])

        local_config['name'] = sname
        tasks.append(asyncio.async(
            _run_task_async(local_config, action, group)
        ))

    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    loop.stop()


if __name__ == '__main__':
    main(sys.argv)
