import errno
import jinja2
import json
import logging
import os
import shutil

from collections import Mapping
from copy import deepcopy

from neckbeard.loader import NeckbeardLoader
from neckbeard.scaling import MinScalingBackend  # TODO: Don't hardcode this

logger = logging.getLogger('configuration')


class CircularSeedEnvironmentError(Exception):
    pass


class InfiniteEmptyStringDict(object):
    """
    An infinitely-nested dictionary where all individual values evaluate to ""
    when converted to a string.
    """
    def __str__(self):
        return ''

    def __unicode__(self):
        return ''

    def __len__(self):
        return 0

    def __getitem__(self, key):
        return InfiniteEmptyStringDict()

    def __contains__(self, key):
        return True

    def get(self, key, default=None):
        return self[key]


def mkdir_p(path):
    """
    Borrowed from: http://stackoverflow.com/a/600612/386925
    """
    try:
        os.makedirs(path)
    except OSError as exc:  # Python >2.5
        if exc.errno != errno.EEXIST or not os.path.isdir(path):
            raise


def evaluate_configuration_templates(configuration, context, debug_trace=''):
    """
    For the given `configuration` (a nested dictionary), walk the dictionary,
    evaluating any string values for Jinja2 template usage and walking any maps
    (dictionary-ish things) or lists to find any strings that need template
    expansion. The result is a dictionary with the same structure as the
    `configuration`, but with any Jinja2 template syntax fully rendered.

    Note: This function is recursive, but users of the function probably
    shouldn't attempt to take advantage of that fact.

    `context` is the template context which Jinja2 will use for evaluation.

    `debug_trace` is a dot-separated list to track how a key is nested for
    template evaluation (eg. 'neckbeard_meta.resource_tracker.path') so
    that error messages about template problems can point users to the exact
    place in their `configuration` where the error occurred. The recursive
    calls build this up.
    """
    if configuration is None:
        return None
    if isinstance(configuration, basestring):
        # TODO: Add validation here to catch the exception for
        # undefined
        env = jinja2.Environment(undefined=jinja2.StrictUndefined)
        template = env.from_string(configuration)
        try:
            return template.render(context)
        except jinja2.UndefinedError:
            logger.warning(
                "Error evaluating the template for: %s",
                debug_trace,
            )
            logger.warning("Template: %s", configuration)
            logger.warning("Context didn't contain a referenced variable")
            raise
        except jinja2.TemplateSyntaxError:
            logger.warning(
                "Jinja2 syntax error in the configuration template: %s",
                debug_trace,
            )
            logger.warning("Template: %s", configuration)
            raise

    constant_types = [bool, int, float]
    # These types can't have any strings nested inside of them, so we can just
    # use their current value directly
    for constant_type in constant_types:
        if isinstance(configuration, constant_type):
            return configuration

    evaluated_config = deepcopy(configuration)
    # Everything else is either a dictionary-like `Mapping` or an iterable,
    # either of which could contain strings that need template evaluation.
    # Recursively evaluate their children/members.
    if isinstance(configuration, Mapping):
        for key, value in configuration.iteritems():
            evaluated_config[key] = evaluate_configuration_templates(
                configuration=value,
                context=context,
                debug_trace="%s.%s" % (debug_trace, key),
            )
    else:
        for index, item in enumerate(configuration):
            evaluated_config[index] = evaluate_configuration_templates(
                configuration=item,
                context=context,
                debug_trace="%s.%s" % (debug_trace, index),
            )

    return evaluated_config


class ConfigurationManager(object):
    """
    ConfigurationManager accepts the already-parsed configuration
    (probably from ``NeckbeardLoader``) and creates an expanded configuration
    for each individual `CloudResource` in an environment. It:
        * Validates the configuration for required options and internal
          consistency
        * Allows `resource_addons` and `environment_addons` to validate their
          specific slice of configuration
        * Generates and properly scopes the template context required for
          expanding individual configuration options. eg.
            * environment.constants
            * environment.secrets
            * seed_environment.constants
            * seed_environment.secrets
            * node
            * seed_node
        * Determines the current scaling state for each `scaling_group` to
          determine how many nodes should exist
          (using the `ScalingCoordinator`).
        * The end results combines the scaling state, configuration, and
          context to generate an individual expanded configuration for each
          `CloudResource` that should exist.

          The entire context is defined by `_get_config_context_for_resource`.
    """
    def __init__(
        self,
        scaling_backend,
        environments,
        constants=None,
        neckbeard_meta=None,
        secrets=None,
        secrets_tpl=None,
        node_templates=None,
    ):
        self.scaling_backend = scaling_backend
        self.environments = environments

        self.constants = constants or {}
        self.neckbeard_meta = neckbeard_meta or {}
        self.secrets = secrets or {}
        self.secrets_tpl = secrets_tpl or {}
        self.node_templates = node_templates or {}

        self._expanded_configuration = {}

    @classmethod
    def from_loader(cls, loader):
        """
        Create a new `ConfigurationManager` from an existing
        ``NeckbeardLoader``.
        """
        raw_config = loader.raw_configuration
        configuration = cls(
            environments=raw_config['environments'],
            scaling_backend=MinScalingBackend(),
            constants=raw_config.get('constants', {}),
            neckbeard_meta=raw_config.get('neckbeard_meta', {}),
            secrets=raw_config.get('secrets', {}),
            secrets_tpl=raw_config.get('secrets_tpl', {}),
            node_templates=raw_config.get('node_templates', {}),
        )

        return configuration

    def is_valid(self):
        # TODO: Actually do some like, you know, validation
        return True

    def print_validation_errors(self):
        pass

    def _get_environment_constants(self, environment_name):
        environments = self.constants.get('environments', {})
        return environments.get(environment_name, {})

    def _get_environment_secrets(self, environment_name):
        environments = self.secrets.get('environments', {})
        return environments.get(environment_name, {})

    def _get_seed_environment_constants(self, environment_name):
        seed_environment_name = self.get_seed_environment_name(
            environment_name,
        )

        return self._get_environment_constants(seed_environment_name)

    def _get_seed_environment_secrets(self, environment_name):
        seed_environment_name = self.get_seed_environment_name(
            environment_name,
        )

        return self._get_environment_secrets(seed_environment_name)

    def get_seed_environment_name(
        self,
        environment_name,
        check_circular_reference=True,
    ):
        """
        Get the `seed_environment` name for the given environment.
        """
        environment = self.environments[environment_name]
        seed_environment_name = environment.get(
            'seed_environment_name',
            None,
        )
        if seed_environment_name is None:
            return None
        # Now let's make sure that this `seed_environment` actually exists
        if seed_environment_name not in self.environments:
            # TODO: This should actually be caught by requiring validation
            # before doing any of this stuff and this should be a type of
            # validation error.
            raise Exception(
                "seed_environment %s does not exist" % seed_environment_name)
        # TODO: This should actually be caught by requiring validation
        # before doing any of this stuff and this should be a type of
        # validation error.
        if check_circular_reference:
            seeds_seed = self.get_seed_environment_name(
                seed_environment_name,
                check_circular_reference=False,
            )
            if seeds_seed is not None:
                raise CircularSeedEnvironmentError()

        return seed_environment_name

    def _get_resource_context(
        self,
        environment_name,
        resource_type,
        resource_name,
        scaling_index,
    ):
        """
        Returns a dictionary with the following values for the given node.

          * environment_name
          * seed_environment_name
          * resource_type
          * name
          * scaling_index
        """
        context = {
            'environment_name': environment_name,
            'resource_type': resource_type,
            'name': resource_name,
        }

        context['seed_environment_name'] = self.get_seed_environment_name(
            environment_name,
        )
        context['scaling_index'] = scaling_index

        return context

    def _get_seed_node_context(
        self,
        environment_name,
        resource_type,
        resource_name,
        scaling_index,
    ):
        environment = self.environments[environment_name]
        resource_types = environment['aws_nodes'][resource_type]