#!/usr/bin/env python3

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

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

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

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

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', 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')
    )
    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(
        '--branches', '-b', nargs='*', type=str,
        help=('restrict output to specific branch(es). Supports glob(7)-style '
              'wildcard patterns')
    )
    report_parser.add_argument(
        '--modules', '-m', nargs='*', type=str,
        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', default=False, action='store_true', dest='diffonly',
        help='only show changes'
    )
    interactive_group.add_argument(
        '--non-interactive', '-n', 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(
        '--branches', '-b', nargs='*', type=str,
        help='update only specific branch(es). Default: All branches.'
    )
    update_parser.add_argument(
        '--module', type=str,
        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')
    return parser


def verify_args(args):
    """perform some extended validation of cli arguments"""
    if not 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('crmngr', 'repository')
        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('crmngr')
        config.set('crmngr', 'repository', url)
        with open(crmngr_config, 'w') as config_file:
            config.write(config_file)
        return config
    exit()


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)

    if args.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
                },
            }
        })

    crmngr_dir = verify_crmngr_directory()

    if args.command == 'clean':
        cache = os.path.join(crmngr_dir, 'cache')
        if query_yes_no("Delete cache directory: %s" % cache):
            try:
                shutil.rmtree(cache)
            except OSError:
                pass
        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, 'config'))

    # 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_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()
