"""SmartEnvSource — pydantic-settings env source with longest-match parsing.

v0.4.2. Closes FEEDBACK V04-04 — the double-underscore env-nesting
delimiter (``MGF_LOGGING__LEVEL``) is awkward; consumers writing
systemd unit files or docker-compose env blocks paste these by
hand. Single-underscore would match the rest of the Python
tooling ecosystem (``PYTEST_PLUGINS``, ``MYPY_CONFIG_FILE``).

The blocker for the simple delimiter switch (Phase A) was field
names containing underscores — ``service_namespace``,
``sample_rate``, ``httpx_instrumented``, ``level_overrides``,
``rotating_max_bytes``, etc. ``MGF_OTEL_SERVICE_NAMESPACE`` is
ambiguous: split on ``_`` it could mean ``otel.service.namespace``
(no such path) or ``otel.service_namespace``.

This source resolves the ambiguity by walking the schema
**longest-match-first**. For each level of nesting it tries every
field name sorted by length, longest first; the first prefix
match wins. Both single- and double-underscore delimiters are
honored (double for v0.1-v0.4.1 backward compat; single is the
new ergonomic).

Examples (all map to the same setting)::

    MGF_LOGGING_LEVEL=DEBUG          → logging.level = DEBUG  (NEW)
    MGF_LOGGING__LEVEL=DEBUG         → logging.level = DEBUG  (legacy)
    MGF_OTEL_SERVICE_NAMESPACE=foo   → otel.service_namespace = foo  (NEW)
    MGF_OTEL__SERVICE_NAMESPACE=foo  → otel.service_namespace = foo  (legacy)
    MGF_LOGGING_LEVEL_OVERRIDES='{"urllib3": "WARNING"}'
                                     → logging.level_overrides = ...  (NEW)

Algorithm correctness pinned by ``tests/unit/test_v04_04_smart_env.py``.

Limitations
-----------
- Genuinely ambiguous environments (a hypothetical
  ``MGF_LOGGING_LEVEL`` where the schema also has a
  ``logging.level_extra`` field) would resolve to the longest
  field name that matches the FULL remainder. If you want a
  shorter match in such a case, use the double-underscore form
  (e.g. ``MGF_LOGGING__LEVEL``).
- Replaces pydantic-settings' default ``EnvSettingsSource`` for
  the schema's MGF_-prefixed env vars; type coercion (JSON for
  complex types, ints / floats / bools) is delegated to pydantic
  at validation time — the same behaviour the default source
  would produce for the resolved nested-dict shape.
"""

from __future__ import annotations

import os
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel
from pydantic_settings.sources import PydanticBaseSettingsSource

if TYPE_CHECKING:
    from pydantic.fields import FieldInfo


class SmartEnvSource(PydanticBaseSettingsSource):
    """Custom env source with longest-field-name-first matching.

    See module docstring for rationale + examples.
    """

    def __init__(self, settings_cls: type) -> None:
        super().__init__(settings_cls)
        prefix = self.config.get("env_prefix") or ""
        self._prefix = prefix
        self._data = self._collect()

    def _collect(self) -> dict[str, Any]:
        """Walk ``os.environ``; return the resolved nested-dict shape."""
        if not self._prefix:
            return {}

        result: dict[str, Any] = {}
        prefix_upper = self._prefix.upper()

        for env_key, env_val in os.environ.items():
            if not env_key.startswith(prefix_upper):
                continue
            remainder = env_key[len(prefix_upper) :]
            if not remainder:
                continue

            path = self._parse_path(remainder, self.settings_cls)
            if path is None:
                continue
            self._set_nested(result, path, env_val)

        return result

    def _parse_path(self, remainder: str, model_cls: type) -> list[str] | None:
        """Find the field path for ``remainder`` in ``model_cls``.

        Returns ``["sub", "field"]`` for ``LOGGING_LEVEL`` against
        :class:`MgfSettings`, or ``None`` if no field-path matches.
        Tries field names longest-first to disambiguate when an
        underscored field name (``service_namespace``) shares a
        prefix with a shorter field (``service``).
        """
        if not isinstance(model_cls, type) or not issubclass(model_cls, BaseModel):
            return None

        # Sort fields by length of UPPER form, longest first.
        fields_by_len = sorted(
            model_cls.model_fields.items(),
            key=lambda kv: len(kv[0]),
            reverse=True,
        )

        for field_name, field_info in fields_by_len:
            field_upper = field_name.upper()

            # Exact match — terminal leaf.
            if remainder == field_upper:
                return [field_name]

            # Nested match — recurse into the field's annotation.
            for sep in ("__", "_"):
                head = field_upper + sep
                if not remainder.startswith(head):
                    continue
                rest = remainder[len(head) :]
                annotation = _unwrap_annotation(field_info)
                if annotation is None:
                    continue
                if isinstance(annotation, type) and issubclass(annotation, BaseModel):
                    sub_path = self._parse_path(rest, annotation)
                    if sub_path is not None:
                        return [field_name, *sub_path]
                # Non-model annotation with extra suffix — that
                # suffix is unexpected; skip this candidate.

        return None

    def _set_nested(
        self,
        data: dict[str, Any],
        path: list[str],
        value: Any,
    ) -> None:
        """Walk ``data`` per ``path``, creating sub-dicts; set leaf."""
        cursor = data
        for segment in path[:-1]:
            existing = cursor.get(segment)
            if not isinstance(existing, dict):
                existing = {}
                cursor[segment] = existing
            cursor = existing
        cursor[path[-1]] = value

    # PydanticBaseSettingsSource required methods
    def get_field_value(
        self,
        field: FieldInfo,
        field_name: str,
    ) -> tuple[Any, str, bool]:
        """Return ``(value, key, value_is_complex)``.

        Complex flag tells pydantic-settings to descend into the
        nested dict (matching the standard env-source behaviour
        for nested sub-models).
        """
        if field_name not in self._data:
            return None, field_name, False
        return self._data[field_name], field_name, True

    def __call__(self) -> dict[str, Any]:
        """Return the resolved nested-dict payload."""
        return dict(self._data)


def _unwrap_annotation(field_info: FieldInfo) -> type | None:
    """Extract a BaseModel-subclass from a field annotation.

    Returns ``None`` for non-model fields (the caller treats those
    as terminal leaves). Currently handles only bare-type
    annotations — every sub-model field on :class:`MgfSettings`
    uses the bare-type shape (``logging: LoggingSettings``, not
    ``logging: LoggingSettings | None``). When a future schema
    adds ``Optional[X]`` or ``Union[A, B]`` sub-model fields, add
    the unwrap branch + a test in the same commit (per the
    project's "no speculative code" rule).
    """
    ann = field_info.annotation
    if ann is None:
        return None
    if isinstance(ann, type) and issubclass(ann, BaseModel):
        return ann
    return None


__all__ = ["SmartEnvSource"]
