# Copyright 2020 Spotify AB
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import logging

import attr
import yaml

from klio_core.config import _converters as converters

logger = logging.getLogger("klio")


def field(**kwargs):
    """Convienence function to create an attribute with parameters tuned for
    config objects.

    """
    # fill in some useful defaults
    if "default" not in kwargs:
        kwargs["default"] = converters.UNSET_REQUIRED_VALUE
    if "repr" not in kwargs:
        kwargs["repr"] = False

    return attr.attrib(**kwargs)


@attr.attrs
class WrappedValidator(object):
    """A Simple validator that just wraps another validator and prepends the
    full config key to any error
    """

    key = attr.attrib()
    inner_validator = attr.attrib()

    def __call__(self, cls, attrib, value):
        try:
            self.inner_validator(cls, attrib, value)
        except Exception as e:
            raise Exception("{}: {}".format(self.key, e))


class config_object(object):
    """Class used as a decorator to add some specialized functionality to
    classes that parse config data.  It is intended to be used with attrs and
    adds settings to any detected attributes defined with `attr.attrib()`.

    The class' __init__ method generated by attrs is augmented to accept a
    special `config_dict` parameter which will do some pre-processing and then
    initialize the class

    Any attrib without a default set will have it set to UNSET_REQUIRED_VALUE,
    which is then detected in the converter that's added to raise a properly
    formatted error message when a value isn't provided by the user.

    If the attrib specifies a recognized `type` (see Converters), a converter
    for that type will be added to ensure any provided values are of that type
    (or can be easily coerced to that type).

    The decorated class may define a `__config_post_init__(self, config_dict)`
    method which will be called after initialization (and after attrs own
    `__attrs_post_init__`), where `config_dict` is the original unmodified
    dict.

    Args:
        key_prefix (str): prefix prepended to any key names in error messages

        allow_unknown_keys (bool): if True, any keys in `config_dict` that are
            not registered `attrib()`s will be ignored and not raise an unknown
            key error.

        ignore_keys (list): list of keys to explicitly ignore (only needed when
            `allow_unknown_keys` si `True`

    """

    def __init__(
        self, key_prefix=None, allow_unknown_keys=False, ignore_keys=None
    ):
        self.key_prefix = key_prefix
        self.allow_unknown_keys = allow_unknown_keys
        self.ignore_keys = [] if ignore_keys is None else ignore_keys

    def _full_key(self, key):
        if self.key_prefix is None:
            return key
        return self.key_prefix + "." + key

    def _prepare_config_dict(self, known_keys, config_dict, **extra_kwargs):
        if self.allow_unknown_keys:
            actual_keys = extra_kwargs
            for key in known_keys:
                if key in config_dict:
                    actual_keys[key] = config_dict[key]
        else:
            actual_keys = config_dict.copy()
            for key, value in extra_kwargs.items():
                actual_keys[key] = extra_kwargs[key]

            for key, value in actual_keys.items():
                if key not in known_keys:
                    raise Exception(
                        "Unexpected config key {}".format(self._full_key(key))
                    )

        for key in self.ignore_keys:
            actual_keys.pop(key, None)

        return actual_keys

    def __call__(self, cls):

        known_keys = []
        # add type checking converters to attribs before running through
        # attr.attrs()
        for key, attrib in cls.__dict__.items():
            if hasattr(attrib, "_default") and hasattr(attrib, "converter"):
                if attrib._default == attr.NOTHING:
                    attrib._default = converters.UNSET_REQUIRED_VALUE

                # wrap any validator in one that includes the key in the error
                # message
                if attrib._validator is not None:
                    attrib._validator = WrappedValidator(
                        self._full_key(key), attrib._validator
                    )

                if attrib.type is not None:
                    attrib.converter = converters.Converters.for_type(
                        attrib.type, self._full_key(key)
                    )
                else:
                    attrib.converter = converters.ConfigValueConverter(
                        self._full_key(key)
                    ).validate

                known_keys.append(key)

        # now we can let attrs work its magic
        attrib_cls = attr.attrs(cls)

        # lastly we'll replace the __init__ provided by attrs (which accepts
        # arguments directly) with one that accepts a dict along with
        # additional kargs, merges and cleans it up before passing it to attrs

        original_init = attrib_cls.__init__

        # need to alias self so we don't confuse the decorated class with this
        # decorator class!
        me = self

        def init_from_dict(self, config_dict={}, **extra_kwargs):
            original_init(
                self,
                **me._prepare_config_dict(
                    known_keys, config_dict, **extra_kwargs
                )
            )
            if hasattr(self, "__config_post_init__"):
                self.__config_post_init__(config_dict)

        attrib_cls.__init__ = init_from_dict
        attrib_cls.from_values = original_init
        return attrib_cls


class IndentListDumper(yaml.Dumper):
    """Force indentation for lists for better visual understanding.

    Instead of this:

        foo:
          bar:
          - one
          - two
          - three

    Format list indentations like this:
        foo:
          bar:
            - one
            - two
            - three
    """

    def increase_indent(self, flow=False, indentless=False):
        return super(IndentListDumper, self).increase_indent(flow, False)
