"""
This is the core module of the cookiecutter workflow, it defines how to create a template, prompt the user for
values of the context and how to generate a project from the given template and provided context
"""
import logging
from typing import Dict, List, Optional

from cookiecutter.exceptions import RepositoryNotFound, UnknownRepoType
from cookiecutter.main import cookiecutter

from samcli.commands.exceptions import UserException
from samcli.lib.init.arbitrary_project import generate_non_cookiecutter_project

from .exceptions import GenerateProjectFailedError, InvalidLocationError, PostprocessingError, PreprocessingError
from .interactive_flow import InteractiveFlow
from .plugin import Plugin
from .processor import Processor

LOG = logging.getLogger(__name__)


class Template:
    """
    A class for project generation based on cookiecutter template

    Attributes
    ----------
    location: str
        Template location (git, mercurial, http(s), path)
    interactive_flows: Optional[List[InteractiveFlow]]
        An optional series of interactive questions to be asked to the user where the answers are used to fulfill
        the values of the cookiecutter.json keys, i.e. the cookiecutter context
    preprocessors: Optional[List[Processor]]
        An optional series of processes to be executed before generating the cookiecutter project. These processes
        are usually used to manipulate the cookiecutter context or doing some bootstrapping before generating
        the cookiecutter project.
        preprocessors are different than cookiecutter's hooks in the sense that they can mutate the context while
        the pre_gen_project hooks can't.
    postprocessors: Optional[List[Processor]]
        An optional series of processes to be executed after generating the cookiecutter project. These processes
        are usually used to clean up resources and to print messages to the user after successfully generating
        the cookiecutter project.
    plugins: Optional[List[Plugin]]
        An optional series of plugins to be plugged in. A plugin defines its own interactive_flow, preprocessor
        and postprocessor. A plugin is a sub-set of the template, if there is a common behavior among multiple
        templates, it is better to be extracted to a plugin that can then be plugged in to each of these templates.
    metadata: Optional[Dict]
        An optional dictionary with extra information about the template

    Methods
    -------
    run_interactive_flows() -> Dict:
        runs the interactive_flows(questions) one be one then combines and returns the answers in one dictionary
        in the form of {question.key: answer}
    generate_project(context) -> None:
        runs the preprocessors chain passing in the given context to the first processor which process it then produce
        a new context which is passed to the next preprocessor and so on.
        After that, it execute the cookiecutter generator to generate the project with the final preprocessed context.
        Finally, it runs the postprocessors chain with the same manner like the preprocessors.
    """

    def __init__(
        self,
        location: str,
        interactive_flows: Optional[List[InteractiveFlow]] = None,
        preprocessors: Optional[List[Processor]] = None,
        postprocessors: Optional[List[Processor]] = None,
        plugins: Optional[List[Plugin]] = None,
        metadata: Optional[Dict] = None,
    ):
        """
        Initialize the class

        Parameters
        ----------
        location: str
            Template location (git, mercurial, http(s), path)
        interactive_flows: Optional[List[InteractiveFlow]]
            An optional series of interactive questions to be asked to the user where the answers are used to fulfill
            the values of the cookiecutter.json keys, i.e. the cookiecutter context
        preprocessors: Optional[List[Processor]]
            An optional series of processes to be executed before generating the cookiecutter project. These processes
            are usually used to manipulate the cookiecutter context or doing some bootstrapping before generating
            the cookiecutter project.
        postprocessors: Optional[List[Processor]]
            An optional series of processes to be executed after generating the cookiecutter project. These processes
            are usually used to clean up resources and to print messages to the user after successfully generating
            the cookiecutter project.
        plugins: Optional[List[Plugin]]
            An optional series of plugins to be plugged in. A plugin defines its own interactive_flow, preprocessor
            and postprocessor. A plugin is a sub-set of the template, if there is a common behavior among multiple
            templates, it is better to be extracted to a plugin that can then be plugged in to each of these templates.
        metadata: Optional[Dict]
            An optional dictionary with extra information about the template
        """
        self._location = location
        self._interactive_flows = interactive_flows or []
        self._preprocessors = preprocessors or []
        self._postprocessors = postprocessors or []
        self._plugins = plugins or []
        for plugin in self._plugins:
            if plugin.interactive_flow:
                self._interactive_flows.append(plugin.interactive_flow)
            if plugin.preprocessor:
                self._preprocessors.append(plugin.preprocessor)
            if plugin.postprocessor:
                self._postprocessors.append(plugin.postprocessor)
        self.metadata = metadata

    def run_interactive_flows(self, context: Optional[Dict] = None) -> Dict:
        """
        prompt the user a series of questions' flows and gather the answers to create the cookiecutter context.
        The questions are identified by keys. If multiple questions, whether within the same flow or across
        different flows, have the same key, the last question will override the others and we will get only the
        answer of this question.

        Raises:
            UserException(ClickException) if anything went wrong.

        Returns:
            A Dictionary in the form of {question.key: answer} representing user's answers to the flows' questions
        """
        try:
            context = context if context else {}
            context["shared_values"] = "default"
            for flow in self._interactive_flows:
                context = flow.run(context)
            return context
        except Exception as e:
            raise UserException(str(e), wrapped_from=e.__class__.__name__) from e

    def generate_project(self, context: Dict, output_dir: str) -> None:
        """
        Generates a project based on this cookiecutter template and the given context. The context is first
        processed and manipulated by series of preprocessors(if any) then the project is generated and finally
        the series of postprocessors(if any) are executed.

        Parameters
        ----------
        context: Dict
            the cookiecutter context to fulfill the values of cookiecutter.json keys
        output_dir: str
            the directory where project will be generated in

        Raises
        ------
        PreprocessingError
            xxx
        InvalidLocationError
            if the given location is not from a known repo type
        GenerateProjectFailedError
            if something else went wrong
        PostprocessingError
            yyy
        """
        try:
            LOG.debug("preprocessing the cookiecutter context")
            for processor in self._preprocessors:
                context = processor.run(context)
        except Exception as e:
            raise PreprocessingError(template=self._location, provider_error=e) from e

        try:
            LOG.debug("Baking a new template with cookiecutter with all parameters")
            cookiecutter(
                template=self._location,
                output_dir=output_dir,
                no_input=True,
                extra_context=context,
                overwrite_if_exists=True,
            )
        except RepositoryNotFound:
            # cookiecutter.json is not found in the template. Let's just clone it directly without
            # using cookiecutter and call it done.
            LOG.debug(
                "Unable to find cookiecutter.json in the project. Downloading it directly without treating "
                "it as a cookiecutter template"
            )
            generate_non_cookiecutter_project(location=self._location, output_dir=".")
        except UnknownRepoType as e:
            raise InvalidLocationError(template=self._location) from e
        except Exception as e:
            raise GenerateProjectFailedError(template=self._location, provider_error=e) from e

        try:
            LOG.debug("postprocessing the cookiecutter context")
            for processor in self._postprocessors:
                context = processor.run(context)
        except Exception as e:
            raise PostprocessingError(template=self._location, provider_error=e) from e
