"""Registration for keywords, checks, and reporters."""

import inspect
import os
import pkgutil
from collections.abc import Mapping
from functools import partial
from importlib import import_module

from snakeoil import klass
from snakeoil.mappings import ImmutableDict

try:
    # This is a file written during installation;
    # if it exists, we defer to it. If it doesn't, then we're
    # running from a git checkout or a tarball.
    from . import _objects as _defaults
except ImportError:  # pragma: no cover
    _defaults = klass.sentinel


def _find_modules(module):  # pragma: no cover
    """Generator of all public modules under a given module."""
    if getattr(module, '__path__', False):
        for _imp, name, _ in pkgutil.walk_packages(module.__path__, module.__name__ + '.'):
            # skip "private" modules
            if name.rsplit('.', 1)[1][0] == '_':
                continue
            try:
                yield import_module(name)
            except ImportError as e:
                raise Exception(f'failed importing {name!r}: {e}')
    else:
        yield module


def _find_classes(module, matching_cls, skip=()):  # pragma: no cover
    """Generator of all subclasses of a selected class under a given module."""
    for _name, cls in inspect.getmembers(module):
        if (inspect.isclass(cls) and issubclass(cls, matching_cls)
                and cls.__name__[0] != '_' and cls not in skip):
            yield cls


def _find_obj_classes(module_name, target_cls):  # pragma: no cover
    """Determine mapping of object class names to class objects."""
    module = import_module(f'.{module_name}', 'pkgcheck')
    cls_module, cls_name = target_cls.rsplit('.', 1)
    matching_cls = getattr(import_module(f'.{cls_module}', 'pkgcheck'), cls_name)

    # skip top-level, base classes
    base_classes = {matching_cls}
    if os.path.basename(module.__file__) == '__init__.py':
        base_classes.update(_find_classes(module, matching_cls))

    classes = {}
    for m in _find_modules(module):
        for cls in _find_classes(m, matching_cls, skip=base_classes):
            if cls.__name__ in classes and classes[cls.__name__] != cls:
                raise Exception(f'object name overlap: {cls} and {classes[cls.__name__]}')
            classes[cls.__name__] = cls

    return classes


class _LazyDict(Mapping):
    """Lazy dictionary of object mappings.

    Used to stall module imports to avoid cyclic import issues."""

    def __init__(self, attr, func_args):
        self._attr = attr
        self._func_args = func_args

        # Forcibly collapse mapping when running from the git repo, used to
        # force cache registration to occur as related modules are imported.
        if _defaults is klass.sentinel:  # pragma: no cover
            self._dict

    @klass.jit_attr
    def _dict(self):
        try:
            result = getattr(_defaults, self._attr)
        except AttributeError:  # pragma: no cover
            result = _find_obj_classes(*self._func_args)
        return ImmutableDict(result)

    def __iter__(self):
        return iter(self._dict.keys())

    def __len__(self):
        return len(list(self._dict.keys()))

    def __getitem__(self, key):
        return self._dict[key]

    def keys(self):
        return iter(self._dict.keys())

    def values(self):
        return iter(self._dict.values())

    def items(self):
        return iter(self._dict.items())

    def select(self, cls):
        """Return mapping of object classes inheriting a given class."""
        return {k: v for k, v in self._dict.items() if issubclass(v, cls)}


def _keyword_alias(alias=None):
    """Decorator to register keyword aliases."""

    class decorator:
        """Decorator with access to the class of a decorated function."""

        def __init__(self, func):
            self.func = func

        def __set_name__(self, cls, name):
            key = alias if alias is not None else name
            cls._alias_keywords.add(key)
            jit_attr = klass.jit_attr_named(f'_{self.func.__name__}')
            func = jit_attr(partial(self.func))
            setattr(cls, name, func)

    return decorator


class _KeywordsLazyDict(_LazyDict):
    """Lazy dictionary of keyword mappings with added filtered attributes."""

    _alias_keywords = set()

    @klass.jit_attr
    def aliases(self):
        """Mapping of aliases to their respective mappings."""
        from . import results
        alias_map = {x: getattr(self, x) for x in self._alias_keywords}
        # support class-based aliasing
        for k, v in self._dict.items():
            if results.AliasResult in v.__bases__:
                alias_map[k] = self.select(v)
        return ImmutableDict(alias_map)

    @_keyword_alias()
    def error(self):
        """Mapping of all error level keywords."""
        from . import results
        return ImmutableDict(self.select(results.Error))

    @_keyword_alias()
    def warning(self):
        """Mapping of all warning level keywords."""
        from . import results
        return ImmutableDict(self.select(results.Warning))

    @_keyword_alias()
    def style(self):
        """Mapping of all style level keywords."""
        from . import results
        return ImmutableDict(self.select(results.Style))

    @_keyword_alias()
    def info(self):
        """Mapping of all info level keywords."""
        from . import results
        return ImmutableDict(self.select(results.Info))

    @klass.jit_attr
    def filter(self):
        """Mapping of default result filters."""
        return ImmutableDict()


class _ChecksLazyDict(_LazyDict):
    """Lazy dictionary of keyword mappings with added filtered attributes."""

    @klass.jit_attr
    def default(self):
        """Mapping of all default-enabled checks."""
        from . import checks
        return ImmutableDict({
            k: v for k, v in self._dict.items()
            if not issubclass(v, checks.OptionalCheck)})


KEYWORDS = _KeywordsLazyDict('KEYWORDS', ('checks', 'results.Result'))
CHECKS = _ChecksLazyDict('CHECKS', ('checks', 'checks.Check'))
REPORTERS = _LazyDict('REPORTERS', ('reporters', 'reporters.Reporter'))
