import os
from collections.abc import Generator
from importlib.machinery import SourceFileLoader
from importlib.util import module_from_spec, spec_from_loader
from typing import Any

from jinja2 import Environment, FileSystemLoader

from polkadot.logging import logger

REQUIRED_ATTRIBUTES: set[str] = {
    "DOTFILES",
}


def load_config(path: str, dry_run: bool, extras: dict[str, str]) -> Generator[Any]:
    """
    load a source file into a dictionary keeping only names that are uppercase and don't start with _

    provides some default/initial options and handles missing options.

    :param path: path to the configuration file
    :param dry_run: if True don't make changes
    :param extras: optional extra arguments and options
    """
    src = SourceFileLoader("polkadot.config", path)
    spec = spec_from_loader("polkadot.config", src)
    assert spec is not None
    mod = module_from_spec(spec)

    try:
        assert spec.loader is not None
        spec.loader.exec_module(mod)
    except FileNotFoundError:
        logger.error(f"Couldn't open configuration file: {path}")
        return

    config: dict[str, Any] = {
        "DOTFILES_DRY_RUN": dry_run,
        "DOTFILES_HOME_DIRECTORY": os.getenv("HOME"),
        "DOTFILES_OUTPUT_DIRECTORY": os.getenv(
            "DOTFILES_OUTPUT_DIRECTORY", os.getenv("HOME")
        ),
        "DOTFILES_WORKING_DIRECTORY": os.path.dirname(os.path.abspath(path)),
        "DOTFILES_JINJA_ENV": Environment(
            loader=FileSystemLoader("./"),
        ),
    }
    for attr in dir(mod):
        if not attr.startswith("_") and attr == attr.upper():
            config[attr] = getattr(mod, attr)

    missing = REQUIRED_ATTRIBUTES.difference(config.keys())
    if missing:
        for attr in sorted(missing):
            logger.error(f"Expected config file to define {attr}.")
        return
    else:
        yield from execute_config({**config, **extras})


def execute_config(config: dict[str, Any]) -> Generator[Any]:
    """
    run through each generator and evaluate the file changes

    :param config: dictionary form of the configuration file combined with extra arguments and options
    """
    for dotfile in config["DOTFILES"]:
        files = iter(dotfile)
        next(files)
        files.send(config)
        yield from files
