# Confumo — Python config framework
# Copyright (C) 2025 Aaron Colichia
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# See <https://www.gnu.org/licenses/> for details.

# confumo.py
import abc, argparse, os, yaml, confuse
from typing import Dict 

from platformdirs import user_config_dir

class Confumo(abc.ABC):
    _registry: Dict[str, "Confumo"] = {}
    _mixins: list = []

    # ------------------------------------------------------------------
    # single public accessor
    # ------------------------------------------------------------------
    @classmethod
    def get(cls, app_name: str, concrete_cls: "type[Confumo]"):
        """
        Return the singleton for *app_name*.
        • If it exists, reuse it (even if created by another subclass).
        • Otherwise instantiate *concrete_cls* and remember it.
        """
        inst = cls._registry.get(app_name)
        if inst is None:
            inst = concrete_cls(app_name)
            cls._registry[app_name] = inst
        return inst

    def __init__(self, app_name: str):
        self.app_name       = app_name
        self.default_cfgdir = user_config_dir(app_name)

        # ① build the parser from **all** classes and mix-ins
        self.args           = self._parse_args()

        # ② hook Confuse onto the same namespace
        self.cfg            = confuse.Configuration(app_name, __name__)
        if self.args.config_file:
            self.cfg.set_file(self.args.config_file)
        self.cfg.set_args(vars(self.args))
        
        # env vars
        self.cfg.set_env()

        self.config_dir = os.path.abspath(self.args.config_dir)

        # ③ run only the leaf’s custom bootstrap
        self._init_subclass()          # subclasses may still call super()

    # -------- central change: collect flags from the whole MRO -------------
    def _parse_args(self):
        parser = argparse.ArgumentParser(prog=self.app_name, add_help=False)
        parser.add_argument('-h', '--help', action='store_true',
                    dest='_confumo_help', help='show this help and exit')
        parser.add_argument('-c', '--config', dest='config_file',
                            help='YAML config file path')
        parser.add_argument('--config_dir', default=self.default_cfgdir,
                            help='Where to load/save config data')

        # 1) mix-ins first (remain override-able by real classes)
        for mixin in getattr(self.__class__, '_mixins', []):
            mixin.add_args(self, parser)

        # 2) every real class in MRO order: Base → … → Leaf
        for cls in reversed(self.__class__.mro()):
            if cls in (Confumo, object):
                continue
            if 'add_args' in cls.__dict__:      # defined *on* that class
                cls.add_args(self, parser)

        args, _ = parser.parse_known_args()
        self._parser = parser
        return args

    @abc.abstractmethod
    def add_args(self, parser: argparse.ArgumentParser):
        """Add app-specific CLI flags."""

    @abc.abstractmethod
    def _init_subclass(self):
        """Pull values from self.cfg and self.args, and set up attrs."""

    def save_yaml(self, filename=None):
        os.makedirs(self.config_dir, exist_ok=True)
        fn = filename or f"{self.app_name}_config.yaml"
        path = os.path.join(self.config_dir, fn)
        with open(path, 'w') as f:
            yaml.safe_dump(self.to_dict(), f)
        return path

    @abc.abstractmethod
    def to_dict(self):
        """Return a flat dict of the merged settings for dumping."""


    def copy(self):
        """
        Return a shallow clone of this config object.
        All of self.__dict__ is copied, so subclass attributes
        carry over automatically.
        """
        new = object.__new__(self.__class__)
        # Shallow-copy the attribute dict so modifications to the clone
        # don’t affect the original.
        new.__dict__ = self.__dict__.copy()
        return new

    @classmethod
    def expose(
        cls,
        app_name: str,
        concrete_cls: "type[Confumo]",
        mod_globals: dict,
        *,
        publish_to: tuple[str, ...] = (),
        root: bool = True):
        """
        1) Get the singleton for *app_name* (creates if missing).
        2) Install __getattr__/__dir__ on the *calling module* so
           attribute access forwards to that singleton.
        3) Optionally publish the same module-level view under each
           dotted-path given in *publish_to* (e.g. 'glavnaqt.core.config').
        4) Return the singleton.
        """
        inst = Confumo.get(app_name, concrete_cls)

        # -- forwarding dunders ---------------------------------------
        def _ga(name): return getattr(inst, name)
        def _dr():     return sorted(set(mod_globals.keys()) | set(dir(inst)))

        mod_globals['__getattr__'] = _ga
        mod_globals['__dir__']     = _dr
        mod_globals['config']      = inst          # convenience symbol

        # -- optional aliases ----------------------------------------
        import importlib, sys, types
        for dotted in publish_to:
            if dotted in sys.modules:                      # already imported
                alias_mod = sys.modules[dotted]
            else:                                          # create shell
                alias_mod = types.ModuleType(dotted)
                sys.modules[dotted] = alias_mod
            alias_mod.config      = inst
            alias_mod.__getattr__ = _ga
            alias_mod.__dir__     = _dr

        # --help help
        if root and getattr(inst.args, '_confumo_help', False):
            inst._parser.print_help()
            import sys as _sys
            _sys.exit(0)

        return inst
