import os
import re
import fnmatch
import logging
import subprocess
from gpstime import gpstime
from dateutil.tz import tzlocal

from . import util

##################################################

NAME = os.getenv('GUARDNAME', 'guardian')
SYSTEMCTL_CMD = ['systemctl', '--user']
GUARD_TARGET_PATH = os.path.expanduser('~/.config/systemd/user/guardian.target.wants')
UNIT_FMT = '{}@{{}}.service'.format(NAME)
UNIT2NODE_RE = re.compile(UNIT_FMT.format('(.*)'))
JOURNALCTL_TIME_FMT = '%Y-%m-%d %X'

##################################################

class SystemctlError(Exception):
    pass

def systemctl_cmd(*args, nodes='*', check=True, **kwargs):
    cmd = SYSTEMCTL_CMD + list(args)
    if nodes:
        cmd += [UNIT_FMT.format(node) for node in nodes]
    logging.debug('cmd: ' + ' '.join(cmd))
    try:
        proc = subprocess.run(cmd, check=check, **kwargs)
    except subprocess.CalledProcessError as e:
        raise SystemctlError(e)
    return proc

##################################################

def list_nodes(node_filter='*', only_active=False, include_status=False):
    """List nodes and node status.

    A list of node name glob patterns can be supplied in `node_filter`
    to retrict the return nodes to only those matching filters.  By
    default all nodes are returned (filter='*').  If node_filter is
    None or an empty list, then no nodes will be returned.

    If `only_active` is True, only active nodes will be returned.
    If `include_status` is True, list elements will be tuples of node
    (name, status).

    """
    # if not nodes:
    #     nodes = '*'
    # proc = systemctl_cmd('list-units', '--full', '--no-legend', nodes=nodes,
    #                      stdout=subprocess.PIPE, universal_newlines=True, bufsize=1,
    #                      )
    # for line in proc.stdout.split('\n'):
    #     if not line:
    #         continue
    #     unit, load, active, sub, desc = line.split(maxsplit=4)
    #     node_match = UNIT2NODE_RE.match(unit)
    #     # FIXME: should we skip any weird output?
    #     if not node_match:
    #         continue
    #     node = node_match.group(1)
    #     # exclude non-active
    #     if only_running and active != 'active':
    #         continue
    #     if active == 'active':
    #         status = sub
    #     else:
    #         status = active
    #     yield (node, status)

    if not node_filter:
        return

    guard_units = set()

    # find all nodes that systemd knows about.
    proc = systemctl_cmd('list-units', '--all', '--full', '--no-legend', '--plain', '--no-pager',
                         nodes='*',
                         stdout=subprocess.PIPE, universal_newlines=True, bufsize=1,
                         )
    for line in proc.stdout.split('\n'):
        if not line:
            continue
        unit,__ = line.split(maxsplit=1)
        guard_units.add(unit)

    # find all enabled units (linked to appropriate target)
    # HACK: we have to resort to a directory listing here because we
    # want to use "enabled" status to be the definitive list of nodes,
    # but there's no way to list enabled template instances :(
    try:
        guard_units |= set(os.listdir(GUARD_TARGET_PATH))
    except FileNotFoundError:
        pass

    # extract node names from unit names
    all_nodes = set()
    for unit in guard_units:
        node_match = UNIT2NODE_RE.match(unit)
        if not node_match:
            continue
        node = node_match.group(1)
        if not node:
            continue
        all_nodes.add(node)

    if not all_nodes:
        return

    # filter list based on supplied patterns
    matched_nodes = set()
    for pattern in node_filter:
        match = set(fnmatch.filter(all_nodes, pattern))
        if not match:
            raise util.GuardCtrlError("Pattern '{}' matches no known nodes.".format(pattern))
        matched_nodes |= match

    # check status
    if include_status or only_active:
        for node in sorted(matched_nodes):
            active = systemctl_cmd('is-active', nodes=[node],
                                   check=False, universal_newlines=True,
                                   stdout=subprocess.PIPE).stdout.strip()
            if only_active and active != 'active':
                continue
            if include_status:
                enabled = systemctl_cmd('is-enabled', nodes=[node],
                                        check=False, universal_newlines=True,
                                        stdout=subprocess.PIPE).stdout.strip()
                # FIXME: this should be last state change time
                estl = systemctl_cmd('show', '--property=ExecMainStartTimestamp', nodes=[node],
                                     check=False, universal_newlines=True,
                                     stdout=subprocess.PIPE).stdout.strip()
                execstart = gpstime.parse(estl.split('=', maxsplit=1)[1])
                yield node, enabled, active, execstart
            else:
                yield node

    else:
        for node in sorted(matched_nodes):
            yield node


def print_node_status(nodes=None, only_failed=False, print_logs=True):
    """Print node service status.

    With no nodes specified displays full system status with process
    tree.  If nodes are specified their individual status is displayed
    along with their last few lines in the log.

    """
    nodes = list(list_nodes(nodes))
    args = ['status', '--quiet', '--no-pager']
    if only_failed:
        args += ['--state=failed']
    if not nodes:
        args += ['guardian.slice']
    if not print_logs:
        args += ['--line', '0']
    try:
        systemctl_cmd(*args, nodes=nodes)
    except SystemctlError:
        pass


def enable_nodes(nodes, now=False):
    """Enable nodes.

    Enabled nodes are registered by the system and auto-started at
    boot.

    """
    util.test_chanfile()
    for node in nodes:
        util.check_system(node)
    logging.info("enabling nodes: {}".format(' '.join(nodes)))
    cmd = ['enable']
    if now:
        cmd += ['--now']
    try:
        systemctl_cmd(*cmd, nodes=nodes)
    except SystemctlError as e:
        raise util.GuardCtrlError("Failed to enable node: {}".format(e.stderr))
    util.update_global_chanfile(list(list_nodes()))


def start_nodes(nodes):
    """Start nodes.

    Start not running nodes.  Nodes must be enabled before starting.
    Is a no-op for already running nodes.

    """
    nodes = list(list_nodes(nodes))
    if not nodes:
        raise util.GuardCtrlError("Pattern(s) match no enabled node(s).  Please 'enable' before 'start'.")
    logging.info("starting nodes: {}".format(' '.join(nodes)))
    try:
        systemctl_cmd('start', nodes=nodes)
    except SystemctlError:
        print_node_status(nodes=nodes, only_failed=True, print_logs=True)
        # reset failed status and clear node
        # FIXME: is this the right way to handle this?
        # systemctl_cmd('reset-failed', nodes=nodes)
        raise util.GuardCtrlError("Failed to start node(s).")


def restart_nodes(nodes):
    """Restart nodes.

    Restart running nodes, or start nodes that aren't running.  Nodes
    must be enabled before starting.

    """
    nodes = list(list_nodes(nodes))
    if not nodes:
        raise util.GuardCtrlError("Pattern(s) match no enabled node(s).")
    logging.info("restarting nodes: {}".format(' '.join(nodes)))
    try:
        systemctl_cmd('restart', nodes=nodes)
    except SystemctlError:
        print_node_status(nodes=nodes, only_failed=True, print_logs=True)
        # reset failed status and clear node
        # FIXME: is this the right way to handle this?
        # systemctl_cmd('reset-failed', nodes=nodes)
        raise util.GuardCtrlError("Failed to restart node(s).")


def stop_nodes(nodes):
    """Stop nodes.

    Stop running nodes.  No-op for non-running nodes.

    """
    nodes = list(list_nodes(nodes))
    if not nodes:
        raise util.GuardCtrlError("Pattern(s) match no enabled node(s).")
    logging.info("stopping nodes: {}".format(' '.join(nodes)))
    try:
        systemctl_cmd('stop', nodes=nodes)
    except SystemctlError as e:
        print_node_status(nodes=nodes, only_failed=True, print_logs=True)
        raise util.GuardCtrlError("Failed to stop node.")


def disable_nodes(nodes, now=False):
    """Disable nodes.

    Disabling nodes removes them from the site registering.  If the
    node is running it will be stopped.

    """
    util.test_chanfile()
    nodes = list(list_nodes(nodes))
    if not nodes:
        raise util.GuardCtrlError("Pattern(s) match no enabled node(s).")
    logging.info("disabling nodes: {}".format(' '.join(nodes)))
    cmd = ['disable']
    if now:
        cmd += ['--now']
    try:
        systemctl_cmd(*cmd, nodes=nodes)
    except SystemctlError as e:
        print_node_status(nodes=nodes, only_failed=True, print_logs=True)
        raise util.GuardCtrlError("Failed to disable node.")
    systemctl_cmd('reset-failed', nodes=nodes, check=False, stderr=subprocess.PIPE)
    util.update_global_chanfile(list(list_nodes()))

##################################################

def node_logs(nodes=None,
              nlines=None, after=None, before=None,
              follow=False):
    """Generator of node log messages.

    yields (gpstime, message) tuples.

    `after` and `before` options expect 'gpstime' objects.

    """
    cmd = ['journalctl', #'--user',
           '--no-pager', '--quiet', '--no-hostname',
           '--output', 'short-unix',
           ]

    if nlines:
        cmd += ['--lines', str(nlines)]
    if after:
        cmd += ['--since', after.astimezone(tzlocal()).strftime(JOURNALCTL_TIME_FMT)]
    if before:
        cmd += ['--until', before.astimezone(tzlocal()).strftime(JOURNALCTL_TIME_FMT)]
    if follow:
        cmd += ['--follow']
    for node in nodes:
        cmd += ['--user-unit={}'.format(UNIT_FMT.format(node))]
    logging.debug('cmd: ' + ' '.join(cmd))

    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
                            universal_newlines=True, bufsize=1)
    for line in proc.stdout:
        line = line.rstrip()
        try:
            time, name, msg = line.split(' ', maxsplit=2)
            dt = gpstime.fromtimestamp(float(time), tz=tzlocal())
        except:
            dt = None
            msg = line
        yield dt, msg
