"""Blueprint representing raw template module."""
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import

import hashlib
import json
import os
import sys

from jinja2 import Template

from ..util import parse_cloudformation_template
from ..exceptions import InvalidConfig, UnresolvedVariable
from .base import Blueprint


def get_template_path(filename):
    """Find raw template in working directory or in sys.path.

    template_path from config may refer to templates colocated with the Stacker
    config, or files in remote package_sources. Here, we emulate python module
    loading to find the path to the template.

    Args:
        filename (str): Template filename.

    Returns:
        Optional[str]: Path to file, or None if no file found

    """
    if os.path.isfile(filename):
        return os.path.abspath(filename)
    for i in sys.path:
        if os.path.isfile(os.path.join(i, filename)):
            return os.path.abspath(os.path.join(i, filename))

    return None


def get_template_params(template):
    """Parse a CFN template for defined parameters.

    Args:
        template (dict): Parsed CFN template.

    Returns:
        dict: Template parameters.

    """
    params = {}

    if 'Parameters' in template:
        params = template['Parameters']
    return params


def resolve_variable(provided_variable, blueprint_name):
    """Resolve a provided variable value against the variable definition.

    This acts as a subset of resolve_variable logic in the base module, leaving
    out everything that doesn't apply to CFN parameters.

    Args:
        provided_variable (:class:`stacker.variables.Variable`): The variable
            value provided to the blueprint.
        blueprint_name (str): The name of the blueprint that the variable is
            being applied to.

    Returns:
        object: The resolved variable string value.

    Raises:
        UnresolvedVariable: Raised when the provided variable is not already
            resolved.

    """
    value = None
    if provided_variable:
        if not provided_variable.resolved:
            raise UnresolvedVariable(blueprint_name, provided_variable)

        value = provided_variable.value

    return value


class RawTemplateBlueprint(Blueprint):
    """Blueprint class for blueprints auto-generated from raw templates."""

    def __init__(self, name, context, raw_template_path, mappings=None, # noqa pylint: disable=too-many-arguments
                 description=None):  # pylint: disable=unused-argument
        """Initialize RawTemplateBlueprint object."""
        self.name = name
        self.context = context
        self.mappings = mappings
        self.resolved_variables = None
        self.raw_template_path = raw_template_path
        self._rendered = None
        self._version = None

    def to_json(self, variables=None):  # pylint: disable=unused-argument
        """Return the template in JSON.

        Args:
            variables (dict):
                Unused in this subclass (variables won't affect the template).

        Returns:
            str: the rendered CFN JSON template

        """
        # load -> dumps will produce json from json or yaml templates
        return json.dumps(self.to_dict(), sort_keys=True, indent=4)

    def to_dict(self):
        """Return the template as a python dictionary.

        Returns:
            dict: the loaded template as a python dictionary

        """
        return parse_cloudformation_template(self.rendered)

    def render_template(self):
        """Load template and generate its md5 hash."""
        return (self.version, self.rendered)

    def get_parameter_definitions(self):
        """Get the parameter definitions to submit to CloudFormation.

        Returns:
            dict: parameter definitions. Keys are parameter names, the values
                are dicts containing key/values for various parameter
                properties.

        """
        return get_template_params(self.to_dict())

    def resolve_variables(self, provided_variables):
        """Resolve the values of the blueprint variables.

        This will resolve the values of the template parameters with values
        from the env file, the config, and any lookups resolved. The
        resolution is run twice, in case the blueprint is jinja2 templated
        and requires provided variables to render.

        Args:
            provided_variables (list of :class:`stacker.variables.Variable`):
                list of provided variables

        """
        # Pass 1 to set resolved_variables to provided variables
        self.resolved_variables = {}
        variable_dict = dict((var.name, var) for var in provided_variables)
        for var_name, _var_def in variable_dict.items():
            value = resolve_variable(
                variable_dict.get(var_name),
                self.name
            )
            if value is not None:
                self.resolved_variables[var_name] = value

        # Pass 2 to render the blueprint and set resolved_variables according
        # to defined variables
        defined_variables = self.get_parameter_definitions()
        self.resolved_variables = {}
        variable_dict = dict((var.name, var) for var in provided_variables)
        for var_name, _var_def in defined_variables.items():
            value = resolve_variable(
                variable_dict.get(var_name),
                self.name
            )
            if value is not None:
                self.resolved_variables[var_name] = value

    def get_parameter_values(self):
        """Return a dictionary of variables with `type` :class:`CFNType`.

        Returns:
            dict: variables that need to be submitted as CloudFormation
                Parameters. Will be a dictionary of <parameter name>:
                <parameter value>.

        """
        return self.resolved_variables

    @property
    def requires_change_set(self):
        """Return True if the underlying template has transforms."""
        return bool("Transform" in self.to_dict())

    @property
    def rendered(self):
        """Return (generating first if needed) rendered template."""
        if not self._rendered:
            template_path = get_template_path(self.raw_template_path)
            if template_path:
                with open(template_path, 'r') as template:
                    if len(os.path.splitext(template_path)) == 2 and (
                            os.path.splitext(template_path)[1] == '.j2'):
                        self._rendered = Template(template.read()).render(
                            context=self.context,
                            mappings=self.mappings,
                            name=self.name,
                            variables=self.resolved_variables
                        )
                    else:
                        self._rendered = template.read()
            else:
                raise InvalidConfig(
                    'Could not find template %s' % self.raw_template_path
                )

        return self._rendered

    @property
    def version(self):
        """Return (generating first if needed) version hash."""
        if not self._version:
            self._version = hashlib.md5(self.rendered.encode()).hexdigest()[:8]
        return self._version
