import ConfigParser

import clg
import os
import yaml
import yamlordereddictloader

from cli import exceptions
from cli import logger
from cli import utils

LOG = logger.LOG

SPEC_EXTENSION = '.spec'
BUILTINS_REPLACEMENT = {
    '__DEFAULT__': "default"
}

TRIM_PARAMS = ['default', 'required']


class ValueArgument(object):
    """
    Default argument type for InfraRed Spec
    Resolves "None" Types with priority defaults.
    """
    settings_dir = None
    subcommand = None

    def __init__(self, value=None, required=None):
        self.value = value
        self.arg_name = None
        self.required = None

    @classmethod
    def get_app_attr(cls, attribute):
        """Get class attributes from ancestor tree.

        If attribute isn't initialized, search ancestor tree for it and
        initialize attribute with the first match found.

        :param attribute: class attribute.
        :return:
        """
        if getattr(cls, attribute):
            return getattr(cls, attribute)
        try:
            setattr(cls, attribute,
                    super(ValueArgument, cls).get_app_attr(attribute))
        except AttributeError:
            pass
        return getattr(cls, attribute)

    @classmethod
    def init_missing_args(cls, spec, args, settings_dir=None, subcommand=None):
        """Initialize defaults for ValueArgument class.

        argparse loads all unprovided arguments as None objects, instead of
        the argument type specified in spec file. This method will set
        argument types according to spec, so later we can iterate of arguments
        by type.

        :param spec: dict. spec file
        :param args: dict. argparse arguments.
        :param app_settings_dir: path to the base directory holding the
            application's settings. App can be provisioner\installer\tester
            and the path would be: settings/<app_name>/
        """
        cls.settings_dir = cls.get_app_attr("settings_dir") or settings_dir
        cls.subcommand = cls.get_app_attr("subcommand") or subcommand

        # todo(yfried): consider simply searching for option_name instead of
        # "options"
        for option_tree in utils.search_tree("options", spec):
            for option_name, option_dict in option_tree.iteritems():
                if option_name in args and issubclass(
                        clg.TYPES.get(option_dict.get(
                            "type", object),
                            object),
                        cls) and args[option_name] is None:
                    args[option_name] = clg.TYPES.get(option_dict["type"])(
                        required=option_dict.get("required")
                    )
                    # todo(yfried): this is a good place to trigger
                    # args[option_name].resolve_value()

    def resolve_value(self, arg_name, defaults=None):
        """
        Resolve the argument value with alternative input origins

        Search order:
            1. Given value from CLI parsing
            2. Environment variables:
                [Aa-Zz] -> [A-Z]
                "-" -> "_"
            3. defaults dict

        :param arg_name: argument name from clg
        :param defaults: dict containing default values.
        """
        defaults = defaults or {}
        self.arg_name = arg_name
        # override get default from env variables
        # TODO (aopincar): IR env vars should be more uniques
        if self.value is None:
            self.value = os.getenv(self.arg_name.upper().replace("-", "_"))
        # override get default from conf file
        if self.value is None:
            self.value = defaults.get(self.arg_name)


class YamlFileArgument(ValueArgument):
    """
    YAML file input argument.
    Loads legal YAML from file.
    Will search for files in the settings directory before trying to resolve
        absolute path.

    For the argument name is "arg-name" and of subparser "SUBCOMMAND" of
        application "APP", the default search path would be:

         settings_dir/APP/SUBCOMMAND/arg/name/arg_value
    """

    def resolve_value(self, arg_name, defaults=None):
        super(YamlFileArgument, self).resolve_value(arg_name, defaults)
        search_first = os.path.join(self.get_app_attr("settings_dir"),
                                    self.get_app_attr("subcommand"),
                                    *arg_name.split("-"))
        self.value = utils.load_yaml(self.value, search_first)


class TopologyArgument(ValueArgument):
    """Build topology dict from smaller YAML files by parsing input. """

    def resolve_value(self, arg_name, defaults=None):
        """
        Merges topology files in a single topology dict.

        :param clg_args: Dictionary based on cmd-line args parsed by clg
        :param app_settings_dir: path to the base directory holding the
            application's settings. App can be provisioner\installer\tester
            and the path would be: settings/<app_name>/
        """
        super(TopologyArgument, self).resolve_value(arg_name, defaults)

        # post process topology
        topology_dir = os.path.join(self.get_app_attr("settings_dir"),
                                    'topology')
        topology_dict = {}
        for topology_item in self.value.split(','):
            if '_' in topology_item:
                number, node_type = topology_item.split('_')
            else:
                raise exceptions.IRConfigurationException(
                    "Topology node should be in format  <number>_<node role>. "
                    "Current value: '{}' ".format(topology_item))
            # todo(obaraov): consider moving topology to config on constant.
            topology_dict[node_type] = utils.load_yaml(node_type + ".yaml",
                                                       topology_dir)
            topology_dict[node_type]['amount'] = int(number)

        self.value = topology_dict


class IniFileArgument(object):
    """Creates a dictionary based on given configuration file."""

    def __init__(self, value):
        """Loads dict based on a provided configuration file.

        :param value: Path to configuration file.
        """

        _config = ConfigParser.ConfigParser()
        with open(value) as fd:
            _config.readfp(fd)

        res_dict = {}
        for section in _config.sections():
            res_dict[section] = {}
            for option, val in _config.items(section):
                # todo(obaranov) checking if val is list (for extra-vars).
                # rework that check to be more beautiful later.
                if val.startswith('[') and val.endswith(']'):
                    val = eval(str(val))
                res_dict[section][option] = val

            res_dict[section].pop('__name__', None)

        self.value = res_dict


def parse_args(app_settings_dir, args=None):
    """
    Looks for all the specs for specified app
    and parses the commandline input arguments accordingly.

    Trim clg spec from customized input and modify help data.

    :param app_settings_dir: path to the base directory holding the
        application's settings. App can be provisioner\installer\tester
        and the path would be: settings/<app_name>/
    :param args: the list of arguments used for directing the method to work
        on something other than CLI input (for example, in testing).
    :return: dict. Based on cmd-line args parsed from spec file
    """
    # Dict with the merging result of all app's specs
    app_specs = _get_specs(app_settings_dir)

    # Get the subparsers options as is with all the fields from app's specs.
    # This also trims some custom fields from options t
o pass to clg.
    subparsers_options = _get_subparsers_options(app_specs)

    # Pass trimmed spec to clg with modified help message
    cmd = clg.CommandLine(app_specs)
    clg_args = vars(cmd.parse(args))
    ValueArgument.init_missing_args(app_specs, clg_args, app_settings_dir,
                                    subcommand=clg_args["command0"])

    # Current sub-parser options
    sub_parser_options = subpars
ers_options.get(clg_args['command0'], {})
