import argparse
import sys
import re

from docker import APIClient as Client
from docker.errors import APIError
from fnmatch import fnmatch

from .image import Image
from .container import Container
from .util import print_status_stream, make_registration_decorator, \
    log_action, log_any_error, log_issue, log_issues, log_warnings, \
    confirm_action
from .check import check_latest_image, check_dangling_volumes, \
    check_untagged_images, untagged_images_with_usage, \
    check_stopped_containers, stopped_containers, \
    dangling_volumes

register_command, command_fns = make_registration_decorator()


@register_command
def pull_all(args):
    "pull all images"
    def status_title(tag, pad=None):
        title = 'pull %s' % (tag)
        if pad:
            title = '%*s' % (pad, title)
        return title

    def pull_tags(tags):
        pad = max(len(status_title(t)) for t in tags)
        for tag in sorted(tags):
            log_action("pulling tag: %s" % (tag))
            try:
                print_status_stream(
                    status_title(tag, pad),
                    cli.pull(tag, stream=True))
            except APIError as err:
                print(err)

    cli = Client()
    tags = Image.repotags(cli)
    if tags:
        pull_tags(tags)


@register_command
def kill_all(args):
    "kill all running containers"
    cli = Client()
    to_kill = list(sorted(Container.all(cli), key=repr))
    if not to_kill:
        return
    background = ['The following running containers will be killed:\n']
    background += [' - %s\n' % (container) for container in to_kill]
    if not args.force and not confirm_action(
            ''.join(background), 'Kill containers?'):
        return
    for container in to_kill:
        log_action("killing container: %s" % (container))
        log_any_error(lambda: cli.kill(container.get('Id')))


@register_command
def rm_stopped(args):
    "remove all containers which are not running"
    cli = Client()
    to_remove = list(stopped_containers(cli))
    if not to_remove:
        return
    background = ['The following stopped containers will be removed:\n']
    background += [' - %s\n' % (container) for container in to_remove]
    if not args.force and not confirm_action(
            ''.join(background), 'Remove containers?'):
        return
    for container in to_remove:
        log_action("removing container: %s" % (container))
        log_any_error(lambda: cli.remove_container(container.get('Id')))


@register_command
def rmv_dangling(args):
    "remove all dangling volumes"
    cli = Client()
    to_remove = list(dangling_volumes(cli))
    if not to_remove:
        return
    background = ['The following dangling volumes will be removed:\n']
    background += [' - %s\n' % (volume) for volume in to_remove]
    if not args.force and not confirm_action(
            ''.join(background), 'Remove volumes?'):
        return
    for volume in to_remove:
        log_action("removing dangling volume: %s" % (volume))
        cli.remove_volume(volume.name)


@register_command
def rmi_dangling(args):
    "remove all dangling (untagged) images"
    cli = Client()
    to_remove = []
    for image, used_by in untagged_images_with_usage(cli):
        if used_by:
            log_issue("not removing image: %s (in use by %s)" % (image, used_by))
        else:
            to_remove.append(image)
    if not to_remove:
        return
    background = ['The following dangling images will be removed:\n']
    background += [' - %s\n' % (image) for image in to_remove]
    if not args.force and not confirm_action(
            ''.join(background), 'Remove images?'):
        return
    for image in to_remove:
        log_action("removing dangling image: %s" % (image))
        log_any_error(lambda: cli.remove_image(image.get('Id')))


@register_command
def scrub(args):
    "remove all stopped containers, dangling images and volumes"
    rm_stopped(args)
    rmi_dangling(args)
    rmv_dangling(args)


def setup_force(subparser):
    subparser.add_argument('--force', help='don\'t ask to confirm', action='store_true')

kill_all.setup = setup_force
rm_stopped.setup = setup_force
rmi_dangling.setup = setup_force
rmv_dangling.setup = setup_force
scrub.setup = setup_force


def match_iterator_glob_or_regexp(args, iterator, apply_fn):
    """
    returns the matching objects from `iterator`, using fnmatch
    unless args.e is set, in which case regular expression is used.
    `apply_fn` is applied to each object, returning a string to
    check match against.
    """
    if args.e:
        r = re.compile(args.pattern)

        def matcher(s):
            return r.match(s)
    else:
        def matcher(s):
            return fnmatch(s, args.pattern)

    for obj in iterator:
        match = apply_fn(obj)
        if matcher(match):
            yield obj


@register_command
def rm_matching(args):
    "remove containers whose name matches `pattern'"
    cli = Client()
    to_remove = list(
        match_iterator_glob_or_regexp(
            args,
            stopped_containers(cli),
            lambda c: c.name))
    if not to_remove:
        return
    background = ['The following containers will be deleted:\n']
    for container in sorted(to_remove, key=repr):
        background.append(' - %s\n' % (container))
    if not args.force and not confirm_action(
            ''.join(background), 'Delete matching containers?'):
        return
    for container in to_remove:
        log_action("removing container via tag: %s" % (container))
        log_any_error(lambda: cli.remove_container(container.get('Id')))


@register_command
def rmi_matching(args):
    "remove images which have tags matching `pattern'"
    cli = Client()

    def all_image_tags():
        for image in Image.all(cli):
            for tag in image.tags:
                yield tag
    to_remove = list(
        match_iterator_glob_or_regexp(
            args,
            all_image_tags(),
            lambda t: t))
    if not to_remove:
        return
    background = ['Images with the following tags will be deleted:\n']
    for tag in sorted(to_remove):
        background.append(' - %s\n' % (tag))
    if not args.force and not confirm_action(
            ''.join(background), 'Delete matching images?'):
        return
    for tag in to_remove:
        log_action("removing image via tag: %s" % (tag))
        log_any_error(lambda: cli.remove_image(tag))


@register_command
def doctor(args):
    "check for common issues"
    cli = Client()
    log_issues("containers running from old version of tag", "restart containers", check_latest_image(cli))
    log_issues("dangling volumes", "wrfy rmv-dangling", check_dangling_volumes(cli))
    log_issues("dangling images", "wrfy rmi-dangling", check_untagged_images(cli))
    log_warnings("stopped containers", "wrfy rm-stopped", check_stopped_containers(cli))


def setup_globrm(subparser):
    subparser.add_argument('pattern', help='pattern (by default, glob)')
    subparser.add_argument('-e', help='treat pattern as regular expression', action='store_true')
    subparser.add_argument('--force', help='don\'t ask to confirm', action='store_true')
rm_matching.setup = setup_globrm
rmi_matching.setup = setup_globrm


def version():
    import pkg_resources
    version = pkg_resources.require("wrfy")[0].version
    print('''\
wrfy, version %s

Copyright © 2016 Grahame Bowland
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.''' % (version))
    sys.exit(0)


def usage(parser):
    parser.print_usage()
    sys.exit(0)


def commands():
    for fn in command_fns:
        name = fn.__name__.replace('_', '-')
        yield name, fn, getattr(fn, 'setup', None), fn.__doc__


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '--version', action='store_true',
        help='print version and exit')

    subparsers = parser.add_subparsers(dest='name')
    for name, fn, setup_fn, help_text in sorted(commands()):
        subparser = subparsers.add_parser(name, help=help_text)
        subparser.set_defaults(func=fn)
        if setup_fn is not None:
            setup_fn(subparser)
    args = parser.parse_args()
    if args.version:
        version()
    if 'func' not in args:
        usage(parser)
    args.func(args)
