"""Reusable :class:`BaseAppSettings` for consumer projects to subclass.

Each consumer project subclasses :class:`BaseAppSettings` to declare
its own typed fields and its own ``env_prefix``. The base contributes:

  - the layered source order (rule CF-02): kwargs > env > .env >
    YAML > file_secrets;
  - the YAML-file override hook (set by :func:`mgf.common.config.load_settings`
    before constructing the model);
  - the ``version`` field + version-check validator (rule CF-03);
  - a ``to_yaml_dict`` helper that round-trips through :func:`load_settings`.

Subclasses MUST set their own ``env_prefix`` in ``model_config``.
Use :func:`settings_config` to get the recommended baseline plus
your own overrides — this is the canonical pattern::

    from mgf.common.config import BaseAppSettings, settings_config

    class AppConfig(BaseAppSettings):
        model_config = settings_config(env_prefix="VMANAGER_")

        libvirt_uri: str = "qemu:///system"
        ...

The ``**BaseAppSettings.model_config`` spread does NOT work because
pydantic-settings pre-populates ``model_config`` with ~40 defaults
(including ``env_prefix=""``); spreading them and then passing
``env_prefix="VMANAGER_"`` raises ``TypeError: got multiple values
for keyword argument 'env_prefix'``. The helper is the safe path.

History: the legacy ``make_settings_config`` alias for
:func:`settings_config` was removed in v0.33.0 per its AP-12 cycle.

See ``docs/standards/CONFIGURATION.md`` for the full standard.
"""

from __future__ import annotations

from pathlib import Path  # noqa: TC003 — pydantic needs runtime resolution
from typing import TYPE_CHECKING, Any, ClassVar, cast

from pydantic import field_validator
from pydantic_settings import (
    BaseSettings,
    SettingsConfigDict,
    YamlConfigSettingsSource,
)

if TYPE_CHECKING:
    from pydantic_settings import PydanticBaseSettingsSource


def settings_config(**overrides: Any) -> SettingsConfigDict:
    """Build a :class:`SettingsConfigDict` with mgf.common's recommended baseline.

    v0.3 canonical name. The recommended shape is the
    subclass-kwarg pattern (closes FEEDBACK API-06)::

        class AppConfig(BaseAppSettings, env_prefix="MYAPP_"):
            ...

    Use this helper directly when the subclass body needs a
    SettingsConfigDict value the kwarg pattern doesn't yet cover,
    or when constructing a :class:`SettingsConfigDict` outside the
    BaseAppSettings inheritance.

    The helper applies these defaults first, then your
    ``overrides`` on top::

        env_file              = ".env"
        env_file_encoding     = "utf-8"
        extra                 = "forbid"        # rule CF-06
        case_sensitive        = False
        validate_default      = True
        arbitrary_types_allowed = True
        dotenv_filtering      = "match_prefix"  # BOOTSTRAP-01

    Why a helper instead of ``**BaseAppSettings.model_config``:
    pydantic fully materialises ``model_config`` with every
    setting's default (``env_prefix=""`` among them), so spreading
    it and re-supplying ``env_prefix="VMANAGER_"`` raises
    ``TypeError: got multiple values for keyword argument
    'env_prefix'``. The helper sidesteps that by only passing the
    keys we explicitly opt into.

    BOOTSTRAP-01 — ``dotenv_filtering="match_prefix"`` makes the
    ``.env`` source honour ``env_prefix``: keys that do not start
    with the prefix are silently ignored instead of routed to
    ``extra="forbid"`` and raising ``ValidationError``. This lets
    a ``.env`` be shared between the consumer (Django ``SECRET_KEY``,
    SQLAlchemy ``DB_*``, etc.) and ``mgf-common`` (``MGF_*``)
    without one trying to validate the other's keys. Consumers
    whose ``.env`` is single-tenant and want the legacy permissive
    behaviour can pass ``dotenv_filtering=None`` in their override.
    """
    defaults: dict[str, Any] = {
        "env_file": ".env",
        "env_file_encoding": "utf-8",
        # Reject unknown keys (rule CF-06). Typos in the YAML file
        # surface as a single ValidationError listing every bad key.
        "extra": "forbid",
        "case_sensitive": False,
        "validate_default": True,
        "arbitrary_types_allowed": True,
        # BOOTSTRAP-01: respect env_prefix when scanning .env so a
        # shared file (Django SECRET_KEY + DB_*, MGF_* alongside)
        # doesn't trip extra=forbid on unprefixed keys. The prefix
        # is the contract; unprefixed keys belong to other tools.
        "dotenv_filtering": "match_prefix",
    }
    defaults.update(overrides)
    # ``SettingsConfigDict`` is a TypedDict — at runtime just a dict —
    # so cast is the cheapest way to carry our dynamic-key dict across
    # the type boundary. Mypy can't validate the keys here because
    # ``overrides`` is typed ``Any``; pydantic-settings validates the
    # keys when it actually consumes ``model_config`` on the subclass.
    return cast("SettingsConfigDict", defaults)


class BaseAppSettings(BaseSettings):
    """Base typed application configuration (rule CF-01).

    Subclasses :class:`pydantic_settings.BaseSettings`. Subclasses
    set their own ``env_prefix`` via the v0.3 subclass-kwarg pattern
    (closes FEEDBACK API-06)::

        class AppConfig(BaseAppSettings, env_prefix="MYAPP_"):
            backend_uri: str = "qemu:///system"

    The settings-config helper form (set on the subclass directly)
    also works::

        class AppConfig(BaseAppSettings):
            model_config = settings_config(env_prefix="MYAPP_")

    The helper form keeps working because ``__init_subclass__``
    only sets ``model_config`` when (a) the subclass declared
    SettingsConfigDict-relevant kwargs AND (b) the subclass body
    did not already set ``model_config`` itself.
    """

    model_config = settings_config()

    # SettingsConfigDict keys we accept as subclass-kwargs.
    # Mirrors the helper's recognised set; adding more here is a
    # one-line edit when pydantic-settings adds new options.
    _SUBCLASS_KWARGS: ClassVar[frozenset[str]] = frozenset(
        {
            "env_prefix",
            "env_nested_delimiter",
            "env_file",
            "env_file_encoding",
            "extra",
            "case_sensitive",
            "validate_default",
            "arbitrary_types_allowed",
        }
    )

    # Per-call YAML override. :func:`load_settings` sets this before
    # constructing the model and resets it after. The set/construct/
    # clear sequence is serialised by ``_LOAD_SETTINGS_LOCK`` in
    # ``mgf.common.config._loader`` (RA-04) so concurrent
    # ``load_settings`` calls against the same subclass do not
    # cross-contaminate the override.
    # ``ClassVar`` tells pydantic this is a real class attribute
    # (not a model field or private attribute), so it lives on the
    # class itself and :meth:`settings_customise_sources` can read it.
    _yaml_file_override: ClassVar[Path | None] = None

    # Current schema version. Subclasses MAY override.
    CURRENT_VERSION: ClassVar[int] = 1

    # ── Schema version field ───────────────────────────────────────────────
    version: int = 1

    # ── v0.3 subclass-kwarg pattern (closes FEEDBACK API-06) ──────────
    def __init_subclass__(cls, **kwargs: Any) -> None:
        """Accept ``SettingsConfigDict`` keys as subclass kwargs.

        Closes FEEDBACK API-06 (Option A). Subclasses can declare
        ``env_prefix=`` (and other settings_config keys) inline
        with the base, eliminating the ``settings_config()``
        boilerplate::

            class AppConfig(BaseAppSettings, env_prefix="MYAPP_"):
                backend_uri: str = "..."

        We extract the recognised keys into ``settings_overrides``
        and apply via :func:`settings_config` ONLY if the
        subclass body did not already set ``model_config`` itself.
        Either path is valid; the kwarg path is the closed-box-
        friendly shape v0.3 prescribes.

        Unrecognised kwargs are forwarded to ``super()`` so other
        cooperative-mixin code (rare in pydantic but possible) keeps
        working.
        """
        settings_overrides: dict[str, Any] = {
            k: kwargs.pop(k) for k in list(kwargs) if k in cls._SUBCLASS_KWARGS
        }
        super().__init_subclass__(**kwargs)

        if not settings_overrides:
            # No override kwargs declared — preserve whatever the
            # subclass body set (or inherited from the base).
            return

        if "model_config" in cls.__dict__:
            # Subclass body explicitly set model_config. Respect it
            # and DO NOT override — the v0.1 path keeps working
            # exactly as before.
            return

        cls.model_config = settings_config(**settings_overrides)

    # ── v0.5 Phase C — friendly error messages ────────────────────────────
    def __init__(self, **values: Any) -> None:
        """Construct settings with consumer-friendly error attribution.

        Wraps pydantic's :class:`ValidationError` into
        :class:`mgf.common.exceptions.AppConfigError` with PEP 678
        notes naming the env var the consumer most likely needs to
        fix. The original ``ValidationError`` is preserved as
        ``__cause__`` for full traceback fidelity.

        Closes the v0.5 strategic-review finding "stack traces point
        inside mgf-common, not at the consumer's mistake."
        """
        from pydantic import ValidationError

        from mgf.common._errors import _wrap_pydantic_validation_error

        try:
            super().__init__(**values)
        except ValidationError as exc:
            # Resolve env_prefix without triggering more validation.
            env_prefix = ""
            mc = type(self).model_config
            if isinstance(mc, dict):
                env_prefix = str(mc.get("env_prefix", ""))
            elif hasattr(mc, "get"):
                env_prefix = str(mc.get("env_prefix", "") or "")
            wrapped = _wrap_pydantic_validation_error(
                exc,
                settings_class_name=type(self).__name__,
                env_prefix=env_prefix,
            )
            raise wrapped from exc

    @field_validator("version")
    @classmethod
    def _check_version(cls, v: int) -> int:
        if v > cls.CURRENT_VERSION:
            raise ValueError(
                f"config.yaml version {v} is newer than supported "
                f"version {cls.CURRENT_VERSION}; upgrade the application"
            )
        return v

    # ── Source customisation — wire YAML in at the right precedence ──────

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        """Define source precedence (rule CF-02 — leftmost wins).

        ``init_settings`` (kwargs) > ``env_settings`` >
        ``dotenv_settings`` > YAML > ``file_secret_settings``
        (Docker-style file mounts).

        The YAML source is only attached when
        :attr:`_yaml_file_override` is set (by :func:`load_settings`)
        and the file actually exists. This keeps direct
        ``Settings()`` construction free of file IO and lets tests
        construct settings without touching disk.
        """
        sources: list[PydanticBaseSettingsSource] = [
            init_settings,
            env_settings,
            dotenv_settings,
        ]
        path = cls._yaml_file_override
        if path is not None and path.exists():
            sources.append(YamlConfigSettingsSource(settings_cls, yaml_file=path))
        sources.append(file_secret_settings)
        return tuple(sources)

    # ── Serialisation helper ───────────────────────────────────────────────

    def to_yaml_dict(self) -> dict[str, Any]:
        """Return a plain dict suitable for ``yaml.safe_dump``.

        Closes CB-06 (the v0.1 shallow-recursion limitation):
        nested ``BaseModel`` sub-fields are now serialised
        recursively. ``Path`` fields → POSIX strings; enums /
        Literals → underlying values. Result round-trips through
        :func:`load_settings`.

        Equivalent to ``self.model_dump(mode="json")`` —
        delegates to pydantic's recursive serialiser which handles
        Path / Enum / discriminated unions / nested BaseModels
        cleanly. The legacy v0.1 implementation iterated
        ``model_fields`` directly and only special-cased ``Path``
        at the top level, which broke ``save_settings(s)`` on any
        Settings class with nested sub-models (CONFIGURATION.md
        §8.4 documented the workaround; this is the fix).
        """
        return self.model_dump(mode="json")


__all__ = ["BaseAppSettings", "settings_config"]
