# -*- coding: utf-8 -*-
'''
SaltStack Extend
~~~~~~~~~~~~~~~~

A templating tool for extending SaltStack.

Takes a template directory and merges it into a SaltStack source code
directory. This tool uses Jinja2 for templating.

This tool is accessed using `salt-extend`

    :codeauthor: Anthony Shaw <anthonyshaw@apache.org>
'''

# Import Python libs
from __future__ import absolute_import, unicode_literals, print_function

from datetime import date
import logging
import tempfile
import os
import sys
import shutil
from jinja2 import Template

# Import Salt libs
from salt.serializers.yaml import deserialize
from salt.ext.six.moves import zip
from salt.utils.odict import OrderedDict
import salt.utils.files
import salt.version

log = logging.getLogger(__name__)

try:
    import click
    HAS_CLICK = True
except ImportError as ie:
    HAS_CLICK = False

TEMPLATE_FILE_NAME = 'template.yml'


def _get_template(path, option_key):
    '''
    Get the contents of a template file and provide it as a module type

    :param path: path to the template.yml file
    :type  path: ``str``

    :param option_key: The unique key of this template
    :type  option_key: ``str``

    :returns: Details about the template
    :rtype: ``tuple``
    '''
    with salt.utils.files.fopen(path, 'r') as template_f:
        template = deserialize(template_f)
        info = (option_key, template.get('description', ''), template)
    return info


def _fetch_templates(src):
    '''
    Fetch all of the templates in the src directory

    :param src: The source path
    :type  src: ``str``

    :rtype: ``list`` of ``tuple``
    :returns: ``list`` of ('key', 'description')
    '''
    templates = []
    log.debug('Listing contents of %s', src)
    for item in os.listdir(src):
        s = os.path.join(src, item)
        if os.path.isdir(s):
            template_path = os.path.join(s, TEMPLATE_FILE_NAME)
            if os.path.isfile(template_path):
                templates.append(_get_template(template_path, item))
            else:
                log.debug("Directory does not contain %s %s", template_path,
                          TEMPLATE_FILE_NAME)
    return templates


def _mergetree(src, dst):
    '''
    Akin to shutils.copytree but over existing directories, does a recursive merge copy.

    :param src: The source path
    :type  src: ``str``

    :param dst: The destination path
    :type  dst: ``str``
    '''
    for item in os.listdir(src):
        s = os.path.join(src, item)
        d = os.path.join(dst, item)
        if os.path.isdir(s):
            log.info("Copying folder %s to %s", s, d)
            if os.path.exists(d):
                _mergetree(s, d)
            else:
                shutil.copytree(s, d)
        else:
            log.info("Copying file %s to %s", s, d)
            shutil.copy2(s, d)


def _mergetreejinja(src, dst, context):
    '''
    Merge directory A to directory B, apply Jinja2 templating to both
    the file/folder names AND to the contents of the files

    :param src: The source path
    :type  src: ``str``

    :param dst: The destination path
    :type  dst: ``str``

    :param context: The dictionary to inject into the Jinja template as context
    :type  context: ``dict``
    '''
    for item in os.listdir(src):
        s = os.path.join(src, item)
        d = os.path.join(dst, item)
        if os.path.isdir(s):
            log.info("Copying folder %s to %s", s, d)
            if os.path.exists(d):
                _mergetreejinja(s, d, context)
            else:
                os.mkdir(d)
                _mergetreejinja(s, d, context)
        else:
            if item != TEMPLATE_FILE_NAME:
                d = Template(d).render(context)
                log.info("Copying file %s to %s", s, d)
                with salt.utils.files.fopen(s, 'r') as source_file:
                    src_contents = salt.utils.stringutils.to_unicode(source_file.read())
                    dest_contents = Template(src_contents).render(context)
                with salt.utils.files.fopen(d, 'w') as dest_file:
                    dest_file.write(salt.utils.stringutils.to_str(dest_contents))


def _prompt_user_variable(var_name, default_value):
    '''
    Prompt the user to enter the value of a variable

    :param var_name: The question to ask the user
    :type  var_name: ``str``

    :param default_value: The default value
    :type  default_value: ``str``

    :rtype: ``str``
    :returns: the value from the user
    '''
    return click.prompt(var_name, default=default_value)


def _prompt_choice(var_name, options):
    '''
    Prompt the user to choose between a list of options, index each one by adding an enumerator
    based on https://github.com/audreyr/cookiecutter/blob/master/cookiecutter/prompt.py#L51

    :param var_name: The question to ask the user
    :type  var_name: ``str``

    :param options: A list of options
    :type  options: ``list`` of ``tupple``

    :rtype: ``tuple``
    :returns: The selected user
    '''
    choice_map = OrderedDict(
        ('{0}'.format(i), value) for i, value in enumerate(options, 1) if value[0] != 'test'
    )
    choices = choice_map.keys()
    default = '1'

    choice_lines = ['{0} - {1} - {2}'.format(c[0], c[1][0], c[1][1]) for c in choice_map.items()]
    prompt = '\n'.join((
        'Select {0}:'.format(var_name),
        '\n'.join(choice_lines),
        'Choose from {0}'.format(', '.join(choices))
    ))

    user_choice = click.prompt(
        prompt, type=click.Choice(choices), default=default
    )
    return choice_map[user_choice]


def apply_template(template_dir, output_dir, context):
    '''
    Apply the template from the template directory to the output
    using the supplied context dict.

    :param src: The source path
    :type  src: ``str``

    :param dst: The destination path
    :type  dst: ``str``

    :param context: The dictionary to inject into the Jinja template as context
    :type  context: ``dict``
    '''
    _mergetreejinja(template_dir, output_dir, context)


def run(extension=None, name=None, description=None, salt_dir=None, merge=False, temp_dir=None):
    '''
    A template factory for extending the salt ecosystem

    :param extension: The extension type, e.g. 'module', 'state', if omitted, user will be prompted
    :type  extension: ``str``

    :param name: Python-friendly name for the module, if omitted, user will be prompted
    :type  name: ``str``

    :param description: A description of the extension, if omitted, user will be prompted
    :type  description: ``str``

    :param salt_dir: The targeted Salt source directory
    :type  salt_dir: ``str``

    :param merge: Merge with salt directory, `False` to keep separate, `True` to merge trees.
    :type  merge: ``bool``

    :param temp_dir: The directory for generated code, if omitted, system temp will be used
    :type  temp_dir: ``str``
    '''
    if not HAS_CLICK:
        print("click is not installed, please install using pip")
        sys.exit(1)

    if salt_dir is None:
        salt_dir = '.'

    MODULE_OPTIONS = _fetch_templates(os.path.join(salt_dir, 'templates'))

    if extension is None:
        print('Choose which type of extension you are developing for SaltStack')
        extension_type = 'Extension type'
        chosen_extension = _prompt_choice(extension_type, MODULE_OPTIONS)
    else:
        if extension not in list(zip(*MODULE_OPTIONS))[0]:
            print("Module extension option not valid")
            sys.exit(1)

        chosen_extension = [m for m in MODULE_OPTIONS if m[0] == extension][0]

    extension_type = chosen_extension[0]
    extension_context = chosen_extension[2]

    if name is None:
        print('Enter the short name for the module (e.g. mymodule)')
        name = _prompt_user_variable('Module name', '')

    if description is None:
        description = _prompt_user_variable('Short description of the module', '')

    template_dir = 'templates/{0}'.format(extension_type)
    module_name = name

    param_dict = {
        "version": salt.version.SaltStackVersion.next_release().name,
        "module_name": module_name,
        "short_description": description,
        "release_date": date.today().strftime('%Y-%m-%d'),
        "year": date.today().strftime('%Y'),
    }

    # get additional questions from template
    additional_context = {}
    for key, val in extension_context.get('questions', {}).items():
        # allow templates to be used in default values.
        default = Template(val.get('default', '')).render(param_dict)

        prompt_var = _prompt_user_variable(val['question'], default)
        additional_context[key] = prompt_var

    context = param_dict.copy()
    context.update(extension_context)
    context.update(additional_context)

    if temp_dir is None:
        temp_dir = tempfile.mkdtemp()

    apply_template(
        template_dir,
        temp_dir,
        context)

    if not merge:
        path = temp_dir
    else:
        _mergetree(temp_dir, salt_dir)
        path = salt_dir

    log.info('New module stored in %s', path)
    return path


if __name__ == '__main__':
    run()
