#!/usr/bin/env python3

"""manage a r10k-style control repository"""

import argparse
import configparser
import logging
import logging.config
import os
import shutil
import sys
import tempfile

from collections import OrderedDict

from crmngrlib import Report
from crmngrlib.puppetmodules import PuppetModule
from crmngrlib.utils import query_yes_no, cprint

__author__ = "Andre Keller <andre.keller@vshn.ch>"
__copyright__ = "Copyright (c) 2015, VSHN AG, info@vshn.ch"
__license__ = 'BSD'

LOG = logging.getLogger(__name__)


def verify_crmngr_directory():
    """make sure crmngr config/cache directory exists"""
    directory = os.path.join(os.path.expanduser('~'), '.crmngr')
    try:
        os.mkdir(directory)
    except FileExistsError:
        pass
    return directory


def get_arg_parser():
    """Get argument parser"""
    descr = 'manage a r10k-style control repository'
    parser = argparse.ArgumentParser(description=descr)
    parser.add_argument(
        '--debug', '-d',
        dest='debug', action='store_true', default=False,
        help='enable debug output'
    )
    parser.add_argument(
        '--no-version-check',
        dest='version_check', action='store_false', default=True,
        help=('do not check for latest versions. Default behaviour is to '
              'fetch current version information for every module found in '
              'every Puppetfile. This may take a considerable amount of time, '
              'especially on first run')
    )
    parser.add_argument(
        '--no-version-check-cache',
        dest='version_check_cache', action='store_false', default=True,
        help=('Do not cache latest versions. Default behaviour is to cache '
              'all version information for 24 hours')
    )
    parser.add_argument(
        '--profile', '-p',
        dest='profile', default='default',
        help='crmngr configuration profile'
    )
    subparsers = parser.add_subparsers(title='commands',
                                       dest='command',
                                       description='valid commands')
    subparsers.required = True

    report_parser = subparsers.add_parser(
        'report',
        help='Puppetfile reporting (-h for usage details)'
    )
    report_parser.add_argument(
        '--report-unused',
        dest='report_unused', action='store_true', default=False,
        help='additionally list branches that are not using a certain module'
    )
    report_parser.add_argument(
        '--environments', '--environment', '--env', '-e',
        nargs='*', type=str, dest='branches', metavar='ENVIRONMENT',
        help=('restrict output to specific environment(s) / branch(es). '
              'Supports glob(7)-style wildcard patterns')
    )
    report_parser.add_argument(
        '--module', '--modules', '--mod', '-m',
        nargs='*', type=str, dest='modules',
        help=('restrict output to specific module(s). Supports glob(7)-style '
              'wildcard patterns')
    )
    update_parser = subparsers.add_parser(
        'update',
        help='Puppetfile manipulation (-h for usage details)'
    )
    interactive_group = update_parser.add_mutually_exclusive_group()
    interactive_group.add_argument(
        '--diff-only', '--dry-run', '-n',
        default=False, action='store_true', dest='diffonly',
        help='only show changes'
    )
    interactive_group.add_argument(
        '--non-interactive',
        default=False, action='store_true', dest='noninteractive',
        help=('In non-interactive mode, crmngr will neither ask for '
              'confirmation before commit or push, nor will it show diffs '
              'of what will be changed. Use with care!')
    )
    mode_group = update_parser.add_mutually_exclusive_group()
    mode_group.add_argument(
        '--add',
        default=False, action='store_true',
        help=('add module if not already in Puppetfile. '
              'Default behaviour is to only update module in branches it '
              'is already defined.')
    )
    mode_group.add_argument(
        '--remove',
        default=False, action='store_true',
        help=('remove module from Puppetfile. Version identifying parameters '
              '(--version, --tag, --commit, --branch) are NOT taken into '
              'account. All module versions are removed!')
    )
    update_parser.add_argument(
        '--environment', '--environments', '--env', '-e',
        nargs='*', type=str, dest='branches', metavar='ENVIRONMENT',
        help='update only specific environment(s) / branch(es). Default: All.'
    )
    update_parser.add_argument(
        '--module', '--mod', '-m',
        type=str, dest='module',
        help=('module to update/add/remove, for modules fetched from '
              'forge.puppetlabs.com the format needs to be author/modulename')
    )
    source_group = update_parser.add_mutually_exclusive_group()
    source_group.add_argument(
        '--git',
        type=str, metavar="URL",
        help=('git URL of module\'s repository. If not specified, the '
              'module is fetched from forge.puppetlabs.com')
    )
    source_group.add_argument(
        '--version',
        nargs='?', const="LATEST_FORGE_VERSION", type=str,
        help=('version of forge.puppetlabs.com module. If parameter is '
              'specified without VERSION, latest available version from '
              'forge.puppetlabs.com will be used instead')
    )
    version_group = update_parser.add_mutually_exclusive_group()
    version_group.add_argument(
        '--tag',
        nargs='?', const="LATEST_GIT_TAG", type=str,
        help=('tag of git module. If parameter is specified without TAG, '
              'latest tag from repository is used instead')
    )
    version_group.add_argument(
        '--commit',
        type=str,
        help='commit of git module'
    )
    version_group.add_argument(
        '--branch',
        type=str,
        help='branch of git module'
    )
    subparsers.add_parser('clean', help='Clean cache')
    subparsers.add_parser(
        'profiles', help='List available configuration profiles'
    )
    return parser


def verify_args(args):
    """perform some extended validation of cli arguments"""
    if args.command != 'update':
        return

    if args.add or args.remove:
        if not args.module:
            raise RuntimeError(
                "It does not make sense to specify --add or --remove without "
                "--module."
            )

    if not args.git:
        if args.branch or args.commit or args.tag:
            raise RuntimeError(
                "It does not make sense to specify --branch/--commit/--tag "
                "without --git"
            )

    if args.module:
        if not args.git:
            if not args.remove:
                if "/" not in args.module:
                    raise RuntimeError(
                        "When adding or updating forge modules, --module has "
                        "to be in author/module format"
                    )
    else:
        if args.version or args.git:
            raise RuntimeError(
                "It does not make sense to specify --git/--version without "
                "specifying --module"
            )


def verify_config_file(crmngr_config):
    """Verify configuration file, create one if not existing"""
    config = configparser.ConfigParser(allow_no_value=True)
    try:
        config.read(crmngr_config)
        _ = config.get('default', 'repository')
        if config.has_section('crmngr'):
            raise RuntimeError(
                'crmngr is a reserved section, do not specify in configuration'
            )
        config.add_section('crmngr')
        return config
    except (configparser.NoSectionError, configparser.NoOptionError):
        pass

    print("No valid configuration file found!")
    print("Enter git url of control repositoriy to create one.")
    print("Leave empty to abort")
    print()
    print("Control Repository URL: ", end="")
    url = input().strip()
    if url:
        config = configparser.ConfigParser()
        config.add_section('default')
        config.set('default', 'repository', url)
        with open(crmngr_config, 'w') as config_file:
            config.write(config_file)
        return config
    exit()


def read_profiles(config):
    """read available profiles from configuration"""
    profiles = OrderedDict()
    for profile in sorted(config.sections()):
        if config.has_option(profile, 'repository'):
            profiles[profile] = config.get(profile, 'repository')
    return profiles


def setup_logging(debug):
    """setup logging configuration"""
    if debug:
        logging.config.dictConfig({
            'version': 1,
            'disable_existing_loggers': False,
            'formatters': {
                'standard': {
                    'format': '%(asctime)s - %(levelname)s - %(message)s'
                },
            },
            'handlers': {
                'default': {
                    'formatter': 'standard',
                    'class': 'logging.StreamHandler',
                },
            },
            'loggers': {
                '': {
                    'handlers': ['default'],
                    'level': 'DEBUG',
                    'propagate': True
                },
            }
        })


def main():
    """main function"""
    parser = get_arg_parser()
    args = parser.parse_args()

    try:
        verify_args(args)
    except RuntimeError as exc:
        parser.print_help()
        sys.stderr.write("\nerror: %s\n" % exc)
        sys.exit(1)

    setup_logging(args.debug)

    crmngr_dir = verify_crmngr_directory()

    if args.command == 'clean':
        do_clean(os.path.join(crmngr_dir, 'cache'))
        sys.exit()

    if args.version_check_cache:
        cache = None
        try:
            cache = os.path.join(crmngr_dir, 'cache')
            os.mkdir(cache)
        except FileExistsError:
            pass
        except OSError:
            cache = None
    else:
        cache = None

    # verify configuration file, create on if it not exists yet
    config = verify_config_file(os.path.join(crmngr_dir, 'profiles'))

    # read profiles from configuration
    profiles = read_profiles(config)

    if args.command == 'profiles':
        do_profiles(profiles)
        sys.exit()

    if args.profile in profiles:
        config.set('crmngr', 'repository', profiles[args.profile])
    else:
        raise RuntimeError(
            "%s is not a valid configuration profile" % args.profile
        )

    # create temporary directory
    tmpdir = tempfile.mkdtemp(prefix='crmngr_')
    LOG.debug('created temporary directory: %s', tmpdir)

    try:
        report = Report(
            config,
            cache=cache,
            tmpdir=tmpdir,
            args=args,
        )

        if args.command == 'report':
            do_report(report, args)
        elif args.command == 'update':
            do_update(report, args)
    except KeyboardInterrupt:
        pass

    # delete temporary directory
    shutil.rmtree(tmpdir)
    LOG.debug('removed temporary directory: %s', tmpdir)


def do_clean(cache):
    """clean action"""
    if query_yes_no("Delete cache directory: %s" % cache):
        try:
            shutil.rmtree(cache)
        except OSError:
            pass


def do_profiles(profiles):
    """profiles action"""
    cprint.white_bold('Available profiles:')
    for profile, url in profiles.items():
        cprint.white(" - %s: %s" % (profile, url))


def do_report(report, args):
    """report action"""
    report.print_module_report(
        args.modules, args.branches, args.report_unused
    )


def do_update(report, args):
    """update action"""
    module = report.module_from_args(args)
    LOG.debug('Module object parsed from cli args: %s', module)
    if args.add:
        mode = 'add'
        commit_message = "Add or update "
    elif args.remove:
        mode = 'remove'
        commit_message = "Remove "
    else:
        mode = 'update'
        commit_message = "Update "
    puppetfiles = report.prepare_puppetfiles(mode, args.branches, module)
    if isinstance(module, PuppetModule):
        if module.version and not args.remove:
            commit_message += "%s module (%s)" % (
                module.name, module.version
            )
        else:
            commit_message += "%s module" % module.name
    else:
        commit_message = "Standardize Puppetfile"

    report.update_puppetfiles(puppetfiles, commit_message)


if __name__ == '__main__':
    main()
