#!/usr/bin/env python

import argparse
import os
import sys
import traceback

import netjsonconfig

description = """
Converts a NetJSON DeviceConfiguration object to native router configurations.

Exhaustive documentation is available at: http://netjsonconfig.openwisp.org/
"""

license = """
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

parser = argparse.ArgumentParser(description=description,
                                 epilog=license,
                                 prog='netjsonconfig')

config = parser.add_argument_group('input')

config.add_argument('--config', '-c',
                    action='store',
                    type=str,
                    default=None,
                    help='config file or string, must be valid NetJSON DeviceConfiguration')

config.add_argument('--templates', '-t',
                    nargs='*',  # zero or more
                    action='store',
                    type=str,
                    default=[],
                    help='list of template config files or strings separated by space')

config.add_argument('--native', '-n',
                    action='store',
                    type=str,
                    default=None,
                    help='path to native configuration file or archive')

output = parser.add_argument_group('output')

output.add_argument('--backend', '-b',
                    required=True,
                    choices=netjsonconfig.get_backends().keys(),
                    action='store',
                    type=str,
                    help='Configuration backend')

output.add_argument('--method', '-m',
                    required=True,
                    choices=['render', 'generate', 'write', 'validate', 'json'],
                    action='store',
                    help='Backend method to use. '
                         '"render" returns the configuration in text format; '
                         '"generate" returns a tar.gz archive as output; '
                         '"write" is like generate but writes to disk; '
                         '"validate" validates the combination of config '
                         'and templates passed in input; '
                         '"json" returns NetJSON output; ')

output.add_argument('--args', '-a',
                    nargs='*',  # zero or more
                    action='store',
                    type=str,
                    default=[],
                    help='Optional arguments that can be passed to methods')

debug = parser.add_argument_group('debug')

debug.add_argument('--verbose',
                   action='store_true',
                   default=False,
                   help='verbose output')

debug.add_argument('--version', '-v',
                   action='version',
                   version=netjsonconfig.get_version())


def _load(config, read=True):
    """
    if config argument does not look like a JSON string
    try to read the contents of a file
    """
    if not config.strip().startswith('{'):
        try:
            f = open(config, 'r')
            return f.read() if read else f
        except IOError:
            print('netjsonconfig: cannot open "{0}": '
                  'file not found'.format(config))
            sys.exit(1)
    else:
        return config.strip()


def parse_method_arguments(arguments):
    """
    ensures arguments format is correct
    """
    kwargs = {}
    for method_arg in arguments:
        if method_arg.count('=') != 1:
            message = '--arg option expects arguments in the following format: '\
                      'arg1=val1 arg2=val2'
            print('netjsonconfig: {0}'.format(message))
            sys.exit(3)
        key, val = method_arg.split('=')
        kwargs[key] = recognize_method_argument(val)
    return kwargs


def recognize_method_argument(arg_string):
    """
    allows to recognize booleans
    """
    if arg_string in ['True', '1']:
        return True
    elif arg_string in ['False', '0']:
        return False
    return arg_string


def print_output(output):
    """
    prints result to standard output
    """
    # if file object, get bytes
    if hasattr(output, 'getvalue'):
        output = output.getvalue()
    if isinstance(output, str):
        print(output)
    else:
        sys.stdout.buffer.write(output)


args = parser.parse_args()
if args.config:
    config = _load(args.config)
elif args.native:
    native = _load(args.native, read=False)
else:
    print('Expected one of the following parameters: "config" or "native"; none found')
    sys.exit(1)
templates = [_load(template) for template in args.templates]
context = dict(os.environ)
method = args.method
method_arguments = parse_method_arguments(args.args)


backend_class = netjsonconfig.get_backends()[args.backend]
try:
    options = dict(templates=templates, context=context)
    if args.config:
        options['config'] = config
    else:
        options['native'] = native
    instance = backend_class(**options)
except TypeError as e:
    print('netjsonconfig: invalid JSON passed in config or templates')
    sys.exit(2)

try:
    output = getattr(instance, method)(**method_arguments)
    if output:
        print_output(output)
except netjsonconfig.exceptions.ValidationError as e:
    message = 'netjsonconfig: JSON Schema violation\n'
    if not args.verbose:
        info = 'For more information repeat the command using --verbose'
    else:
        info = str(e)
    print(message + info)
    sys.exit(4)
except TypeError as e:
    if args.verbose:
        traceback.print_exc()

    print('netjsonconfig: {0}'.format(e))
    sys.exit(5)
