"""Create new projects from template and administer existing projects
    - defines which components/files will be installed for user requests
    - defines how components/files will be installed according to user requests
"""
import os
import logging
import json
from jinja2 import Environment, PackageLoader, select_autoescape
from jinja2 import Template
from devopstemplate import pkg


class SkipFileError(FileExistsError):
    """Will be raised if a file that is to be created already exists and should
    be skipped according to user flags.
    """


class DevOpsTemplate():
    """Create and modify instances of the DevOps template:

    create: New instance of the template
    manage: Modify an existing instance of the template
    cookiecutter: generate cookiecutter template from the devops template
    """

    def __init__(self, projectdirectory,
                 overwrite_exists=False, skip_exists=False, dry_run=False):
        """Provide configurations that are common to all DevOpsTemplate actions

        Params:
            projectdirectory: String with a (relative) path to the directory
                that contains the instance of the template.
            overwrite_exists: Boolean specifying if existing files should be
                overwritten. An error is raised otherwise.
            skip_exists: Boolean specifying if existing files should be
                skipped/ignores. An error is raised otherwise.
            dry_run: Boolean specifying whether to not perform any actions in
                order to see (in the log) what would have happend.
        """
        self.__projectdir = projectdirectory
        self.__overwrite = overwrite_exists
        self.__skip = skip_exists
        self.__dry_run = dry_run
        with pkg.stream('template.json') as handle:
            self.__template_dict = json.load(handle)
        self.__template_dname = 'template'
        # ATTENTION: using __package__ may only work as long as this module
        # (template.py) is located in the top-level import directory
        loader = PackageLoader(__package__,
                               self.__template_dname)
        self.__env = Environment(loader=loader,
                                 autoescape=select_autoescape(default=True))
        # Create project base directory if not present
        self.__mkdir(projectdirectory)

    def create(self, context, components):
        """Create a new project from the DevOps template given config options.

        Installs components which are defined in template.json

        Params:
            context: Dictionary with configuration flags supported by the
                template (typically generated by the CLI, see main.create).
            components: Template components that should be installed.
        """
        logger = logging.getLogger('DevOpsTemplate.create')
        logger.info('Create project from template')
        logger.info('Project name: %s', context['project_name'])
        logger.info('Package name: %s', context['project_slug'])
        # Install files for components
        for component in components:
            logger.debug(' # %s', component)
            self.__install(component, context)

    def cookiecutter(self, context, components):
        """Create a new cookiecutter template from the DevOps template given
        config options. Config options only affect the default values for the
        cookiecutter template, which are provided in cookiecutter.json

        Generates components which are defined in template.json

        Params:
            context: Dictionary with configuration flags supported by the
                template (flags are defined in ProjectConfig.cookiecutter and
                can be modified based on command-line args, see
                main.cookiecutter)
            components: Template components that should be installed.
        """
        logger = logging.getLogger('DevOpsTemplate.cookiecutter')
        logger.info('Generate cookiecutter template')
        # Generate cookiecutter.json
        # configuration is provided by the context dictionary
        cookiecutter_json_fpath = os.path.join(self.__projectdir,
                                               'cookiecutter.json')
        try:
            self.__check_project_file(cookiecutter_json_fpath)
            if not self.__dry_run:
                with open(cookiecutter_json_fpath, 'w',
                          encoding='utf-8') as handle:
                    json.dump(context, handle, indent=2)
            logger.info('project:%s', cookiecutter_json_fpath)
        except SkipFileError:
            logger.warning('File %s exists, skipping', cookiecutter_json_fpath)

        # Only generate *cookiecutter* readme if not present already
        # Note: a template readme can be installed via template components
        # (see below)
        readme_fpath = os.path.join(self.__projectdir, 'README.md')
        if not os.path.exists(readme_fpath):
            if not self.__dry_run:
                with open(readme_fpath, 'w', encoding='utf-8') as handle:
                    readme_content_list = ['# Cookiecutter PyDevops']
                    handle.writelines(readme_content_list)
            logger.info('project:%s', readme_fpath)

        # Generate hooks directory with pre/post generation scripts if required
        # (might be useful in future)
        #
        # Generate project template
        cookiecutter_project_dname = '{{cookiecutter.project_name}}/'
        self.__mkdir(cookiecutter_project_dname)
        # Adjust projectdirectory such that __install installs to the correct
        # directory (projectdirectory represents cookiecutter template root)
        cookiecutter_rootdir = self.__projectdir
        self.__projectdir = os.path.join(self.__projectdir,
                                         cookiecutter_project_dname)
        # Generate cookiecutterconfig for rendering cookiecutter template
        # variables
        # pylint: disable=consider-using-f-string
        # simplifies handling of jinja2 template syntax {{ }}
        cookiecutterconfig = {key: '{{cookiecutter.%s}}' % key
                              for key in context.keys()}
        # Install all template components
        for comp in components:
            self.__install(comp, cookiecutterconfig)

        # Revert project directory to cookiecutter root directory
        self.__projectdir = cookiecutter_rootdir

    def manage(self, context, components):
        """Add functionality/components to an existing project that has been
        created from the DevOps template given configuration options.

        Params:
            context: Dictionary with configuration flags supported by the
                template (flags are defined in ProjectConfig.managa and
                can be modified based on command-line args, see
                main.manage)
            components: Template components that should be installed.
        """
        logger = logging.getLogger('DevOpsTemplate.manage')
        # Install files for components
        for component in components:
            logger.debug(' # %s', component)
            self.__install(component, context)

    def __install(self, template_component, context):
        """Copy and render files for a template component
        Components, i.e., file to install, are defined in 'template.json' which
        is represented by __template_dict.

        Params:
            template_component: String specifying the component to install.
            context: Dictionary with the context for rendering Jinja2
                templates.
        """
        file_list = self.__template_dict[template_component]
        for template_fpath in file_list:
            # Render file path (paths can contain template variables)
            project_fpath = Template(template_fpath).render(**context)
            self.__render(template_fpath, project_fpath, context)

    def __mkdir(self, project_dname):
        """Create a directory within the project if not present

        Params:
            project_dname: String specifying the name of the directory
        """
        logger = logging.getLogger('DevOpsTemplate.__mkdir')
        project_dpath = os.path.join(self.__projectdir, project_dname)
        if not os.path.exists(project_dpath):
            logger.info('creating directory: %s', project_dpath)
            if not self.__dry_run:
                os.makedirs(project_dpath)
        else:
            logger.debug('directory %s exists', project_dpath)

    def __render(self, pkg_fname, project_fname, context):
        """Render template to project according to overwrite/skip class members
        The source file will be used as a Jinja2 template and rendered before
        the rendering result will be written to the target file.

        Params:
            pkg_fname: String specifying the file in the distribution package
            project_fname: String specifying the target file in the project
            context: Dictionary with the context for rendering Jinja2 templates
        Raises:
            FileNotFoundError: if pkg_fname is not available
            FileExistsError: if project_fname already exists in the project
                and skip-exists=False, overwrite-exists=False
        """
        logger = logging.getLogger('DevOpsTemplate.__render')
        pkg_fpath = os.path.join(self.__template_dname, pkg_fname)
        if not pkg.exists(pkg_fpath):
            raise FileNotFoundError(f'File {pkg_fpath} not available in '
                                    'distribution package')
        project_fpath = os.path.join(self.__projectdir, project_fname)
        try:
            self.__check_project_file(project_fpath)
        except SkipFileError:
            logger.warning('File %s exists, skipping', project_fpath)
            return
        if not self.__dry_run:
            # Create parent directories if not present
            parent_dname = os.path.dirname(project_fpath)
            if not os.path.exists(parent_dname):
                os.makedirs(parent_dname)
            # Load and instantiate template
            template = self.__env.get_template(pkg_fname)
            with open(project_fpath, 'w', encoding='utf-8') as project_fh:
                template.stream(**context).dump(project_fh)
        logger.info('template:%s  ->  project:%s', pkg_fname, project_fpath)

    def __check_project_file(self, project_fpath):
        """Check whether the given file can be created in the project without
        conflict. A conflict arises if the file exists and should not be
        skipped or overwritten.

        Params:
            project_fpath: String specifying the path to the file in the
                project.
        Returns: True if the file can be created without conflict.
        Raises:
            SkipFileError: if the creation of the new file should be skipped.
            FileExistsError: if the file that should be created already exists
                and should not be overwritten.
        """
        if os.path.exists(project_fpath) and self.__skip:
            raise SkipFileError(f'File {project_fpath} already exists, skip.')
        if os.path.exists(project_fpath) and not self.__overwrite:
            raise FileExistsError(f'File {project_fpath} already exists, exit.'
                                  ' (use --skip-exists or --overwrite-exists'
                                  ' to control behavior)')
        return True
