from configparser import RawConfigParser
from decimal import Decimal
from pathlib import Path
from typing import TYPE_CHECKING, Any, Generator, Hashable, List, Tuple

if TYPE_CHECKING:
    from .milc import MILC

from .attrdict import AttrDict


class Configuration(AttrDict):
    """Represents the running configuration.

    This class never raises IndexError, instead it will return None if a
    section or option does not yet exist.
    """
    def __getitem__(self, key: Hashable) -> Any:
        """Returns a config section, creating it if it doesn't exist yet.
        """
        if key not in self._data:
            self._data[key] = ConfigurationSection(self, str(key))

        return self._data[key]


class ConfigurationSection(Configuration):
    def __init__(self, parent: AttrDict, section_name: str, *args: Any, **kwargs: Any) -> None:
        super(ConfigurationSection, self).__init__(*args, **kwargs)
        self._parent = parent
        self._section_name = section_name

    def __getitem__(self, key: Hashable) -> Any:
        """Returns a config value, pulling from the `user` section as a fallback.
        This is called when the attribute is accessed either via the get method or through [ ] index.
        """
        val = self._data.get(key)
        if val is not None:
            return val

        elif key in self._parent.user:
            return self._parent.user[key]

        return None

    def __getattr__(self, key: str) -> Any:
        """Returns the config value from the `user` section.
        This is called when the attribute is accessed via dot notation but does not exist.
        """
        if key[0] != '_':
            return self.__getitem__(key)

        return None

    def __setattr__(self, key: str, value: Any) -> None:
        """Sets dictionary value when an attribute is set.
        """
        if key[0] != '_':
            self._data[key] = value
        else:
            object.__setattr__(self, key, value)


def _config_navigate(config_root: 'Configuration', dotted_path: str) -> 'Configuration':
    """Navigate config to the ConfigurationSection at dotted_path, creating as needed.

    Consistent with the lazy-creation behavior of Configuration.__getitem__:
    intermediate sections are always created if they don't exist.
    """
    parts = dotted_path.split('.')
    section: Configuration = config_root
    for i, part in enumerate(parts):
        existing = section._data.get(part)
        if isinstance(existing, ConfigurationSection):
            section = existing
        else:
            path_so_far = '.'.join(parts[:i + 1])
            new_section = ConfigurationSection(config_root, path_so_far)
            section._data[part] = new_section
            section = new_section
    return section


def _read_config_file(config_file: Path, config: Configuration, config_source: Configuration) -> None:
    """Merge a configuration file into the running configuration."""
    raw_config = RawConfigParser()

    raw_config.read(str(config_file))

    # Iterate over the config file options and write them into config.
    # Section names may be dotted (e.g. [remote.add]) for nested subcommands.
    for section in raw_config.sections():
        config_section = _config_navigate(config, section)
        config_source_section = _config_navigate(config_source, section)

        for option in raw_config.options(section):
            value = raw_config.get(section, option)

            if value.lower() in ['yes', 'true', 'on']:
                value = True
            elif value.lower() in ['no', 'false', 'off']:
                value = False
            elif value.lower() in ['none']:
                continue
            elif value.replace('.', '').isdigit():
                if '.' in value:
                    value = Decimal(value)
                else:
                    value = int(value)

            config_section[option] = value
            config_source_section[option] = 'config_file'


def _collect_config_sections(section: 'Configuration', prefix: str = '') -> Generator[Tuple[str, str, Any], None, None]:
    """Recursively yield (dotted_section_name, option_name, value) for all leaf values.

    Used by _save_config_file to emit flat INI sections with dotted names.
    """
    for key, value in section._data.items():
        if isinstance(value, ConfigurationSection):
            sub_prefix = f"{prefix}.{key}" if prefix else key
            yield from _collect_config_sections(value, sub_prefix)
        else:
            yield prefix, key, value


class SubparserWrapper(object):
    """Wrap subparsers so we can track what options the user passed.
    """

    # We type `cli` as Any instead of MILC to avoid a circular import
    def __init__(self, cli: Any, submodule: Any, subparser: Any) -> None:
        self.cli = cli
        self.submodule = submodule
        self.subparser = subparser
        self._arg_parser = self.cli._arg_parser
        self._child_subparsers = None

        for attr in dir(subparser):
            if not hasattr(self, attr):
                setattr(self, attr, getattr(subparser, attr))

    def get_child_subparsers(self) -> Any:
        """Lazily create and return the _SubParsersAction for nested sub-subcommands."""
        if self._child_subparsers is None:
            self._child_subparsers = self.subparser.add_subparsers(title='Sub-commands', dest='subparsers', metavar="")
        return self._child_subparsers

    def completer(self, completer: Any) -> None:
        """Add an arpcomplete completer to this subcommand.
        """
        self.subparser.completer = completer

    def add_argument(self, *args: Any, **kwargs: Any) -> None:
        """Add an argument for this subcommand.

        This also stores the default for the argument in `self.cli.default_arguments`.
        """
        if kwargs.get('action') == 'store_boolean':
            # Store boolean will call us again with the enable/disable flag arguments
            handle_store_boolean(self, *args, **kwargs)

        else:
            completer = None

            if kwargs.get('completer'):
                completer = kwargs['completer']
                del kwargs['completer']

            self.cli.acquire_lock()
            argument_name = get_argument_name(self.cli._arg_parser, *args, **kwargs)

            if completer:
                self.subparser.add_argument(*args, **kwargs).completer = completer
            else:
                self.subparser.add_argument(*args, **kwargs)

            if kwargs.get('action') == 'store_false':
                self.cli._config_store_false.append(argument_name)

            if kwargs.get('action') == 'store_true':
                self.cli._config_store_true.append(argument_name)

            if self.submodule not in self.cli.default_arguments:
                self.cli.default_arguments[self.submodule] = {}

            self.cli.default_arguments[self.submodule][argument_name] = kwargs.get('default')
            self.cli.release_lock()


def get_argument_strings(arg_parser: Any, *args: Any, **kwargs: Any) -> List[str]:
    """Takes argparse arguments and returns a list of argument strings or positional names.
    """
    try:
        return arg_parser._get_optional_kwargs(*args, **kwargs)['option_strings']  # type: ignore[no-any-return]

    except ValueError:
        return [arg_parser._get_positional_kwargs(*args, **kwargs)['dest']]


def get_argument_name(arg_parser: Any, *args: Any, **kwargs: Any) -> Any:
    """Takes argparse arguments and returns the dest name.
    """
    try:
        return arg_parser._get_optional_kwargs(*args, **kwargs)['dest']

    except ValueError:
        return arg_parser._get_positional_kwargs(*args, **kwargs)['dest']


def handle_store_boolean(self: 'MILC | SubparserWrapper', *args: Any, **kwargs: Any) -> Any:
    """Does the add_argument for action='store_boolean'.
    """
    disabled_args = None
    disabled_kwargs = kwargs.copy()
    disabled_kwargs['action'] = 'store_false'
    disabled_kwargs['dest'] = get_argument_name(self._arg_parser, *args, **kwargs)
    disabled_kwargs['help'] = 'Disable ' + kwargs['help']
    kwargs['action'] = 'store_true'
    kwargs['help'] = 'Enable ' + kwargs['help']

    for flag in args:
        if flag[:2] == '--':
            disabled_args = ('--no-' + flag[2:],)
            break

    if disabled_args is None:
        raise ValueError("action='store_boolean' requires at least one long flag (e.g. '--flag')")

    self.add_argument(*args, **kwargs)
    self.add_argument(*disabled_args, **disabled_kwargs)

    return (args, kwargs, disabled_args, disabled_kwargs)
