"""``MgfSettings`` — the library's own typed configuration root.

See: ``docs/reference/settings.md``.

Single source of truth for every behavioural knob in
``mgf-common``. Consumers tweak it via
``mgf.common.bootstrap(settings=MgfSettings(...))`` or via
``MGF_*`` environment-variable aliases. ``mgf-common explain``
(future) prints the active state with each field's source.

Schema layout::

    MgfSettings
    ├── logging:    LoggingSettings    — level / fmt / file / sinks / level_overrides
    ├── otel:       OtelSettings        — endpoint / sample_rate / resource_attributes
    ├── crash:      CrashSettings       — sinks / sentry_dsn / state_dir
    ├── redaction:  RedactionSettings   — extra_keys / strict_mode / pattern groups
    ├── vault:      VaultSettings       — backend / config (Phase 5)
    └── preset:     PresetName          — explicit | dev | systemd | docker | auto

The six sub-models are independently typed pydantic ``BaseModel``
subclasses with ``extra="forbid"`` (rule **CF-06**) so a typo in
a config file fails loud at load time.

Per FEEDBACK design ref §14.15: when ``bootstrap()`` is called
WITHOUT a ``settings=`` argument, the default is
``MgfSettings(preset="auto")`` to preserve v0.1 auto-detection
behaviour for legacy consumers. Explicit construction
(``MgfSettings(...)``) gets ``preset="explicit"`` — the new-project
default that surfaces every knob.

Phase 1 of the v0.3 closed-box reshape lands the schema + the
preset enum. The presets themselves (their applied semantics) are
still being shaped — Phase 6 will wire them. Until then ``preset``
is a typed field with no behavioural effect, which is the right
place to hold the seam.

See ``docs/CLOSED_BOX_REDESIGN.md`` §4.2 + §6 for the full design.
"""

from __future__ import annotations

import os
import sys
from pathlib import Path  # noqa: TC003 — pydantic needs runtime resolution
from typing import Any, ClassVar, Literal, Self

from pydantic import BaseModel, ConfigDict, Field, model_validator

from mgf.common.config._settings import BaseAppSettings, settings_config

# ---------------------------------------------------------------------------
# Type aliases — stable across the public API
# ---------------------------------------------------------------------------

LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
"""Stable log-level enum. Matches the stdlib level names."""

LogFormat = Literal["text", "json", "auto"]
"""Console formatter selector. ``auto`` resolves to ``text`` on a
TTY and ``json`` otherwise (rule LG-02)."""

PresetName = Literal["explicit", "dev", "systemd", "docker", "auto"]
"""Bundle of behavioural defaults applied before per-field overrides.

- ``explicit``  — no auto-detection; every default is the explicit
  one stated in the sub-model. The new-project default; the
  closed-box-friendly shape.
- ``dev``        — text format on console; DEBUG level; no remote sinks.
- ``systemd``    — journald + JSON; no console handler.
- ``docker``     — JSON to stdout; no rotating-file sink (the
  container runtime captures stdout).
- ``auto``       — preserves v0.1 auto-detection (TTY-detect,
  JOURNAL_STREAM-detect, OTEL_ENDPOINT-detect). Default when
  ``bootstrap()`` is called without a ``settings=`` argument.
"""


# ---------------------------------------------------------------------------
# Sub-models
# ---------------------------------------------------------------------------


class LoggingSettings(BaseModel):
    """Logging-related fields. Read by the wired logging stack."""

    model_config = ConfigDict(extra="forbid")

    level: LogLevel = "INFO"
    """Root logger level. Per-component overrides via
    :attr:`level_overrides`."""

    format: LogFormat = "auto"
    """Console formatter. The rotating file sink ALWAYS emits JSON
    regardless of this field — JSON is the contract every mainstream
    aggregator (Loki / ELK / Datadog) parses."""

    file: Path | None = None
    """Override the rotating-file sink path. ``None`` →
    ``<state>/logs/<app_name>.log``."""

    journald: bool = False
    """Attach a systemd journald handler. Auto-detected when
    ``preset="auto"`` AND ``JOURNAL_STREAM`` env var is set; explicit
    otherwise."""

    syslog_address: str | None = None
    """``host:port`` for an opt-in syslog sink. ``None`` →
    no syslog sink."""

    http_sink: str | None = None
    """HTTP webhook URL for an opt-in JSON-POST sink. ``None`` →
    no HTTP sink."""

    rotating_max_bytes: int = Field(default=10 * 1024 * 1024, ge=1024)
    """Per-file size before rotation (default 10 MiB)."""

    rotating_backup_count: int = Field(default=5, ge=0)
    """Number of rotated files to retain (default 5)."""

    level_overrides: dict[str, LogLevel] = Field(default_factory=dict)
    """Per-logger-name level overrides. Logger name is the canonical
    hierarchical path (e.g. ``"mgf.common.observability"`` or
    ``"urllib3.connectionpool"``). Overrides apply on top of
    :attr:`level`. Closes FEEDBACK design ref §14.6 — eliminates
    the ``import logging`` closed-box leak for per-component levels.
    """


class OtelSettings(BaseModel):
    """OpenTelemetry tracing + metrics fields."""

    model_config = ConfigDict(extra="forbid")

    enabled: bool = False
    """Master switch. When ``False``, :func:`mgf.common.bootstrap` skips
    the OTel-wiring step and :func:`operation_span` yields without
    opening a real span. The default is ``False``; legacy v0.1
    behaviour (auto-enable when ``OTEL_EXPORTER_OTLP_ENDPOINT`` is
    set) is restored under ``preset="auto"``."""

    endpoint: str | None = None
    """OTLP endpoint URL. ``None`` → use the OpenTelemetry-standard
    ``OTEL_EXPORTER_OTLP_ENDPOINT`` env var if set."""

    service_namespace: str | None = None
    """Optional ``service.namespace`` resource attribute. Useful for
    per-team / per-tenant routing in OTel backends. ``None`` →
    omit the attribute."""

    sample_rate: float = Field(default=1.0, ge=0.0, le=1.0)
    """Span sampling rate (0.0 = drop everything; 1.0 = sample
    every span). Production deployments typically set 0.01-0.1 to
    cap volume."""

    resource_attributes: dict[str, str] = Field(default_factory=dict)
    """Additional resource attributes. Merged with the standard
    ``service.name`` (= ``app_name``), ``service.version`` (=
    ``app_version``), and any ``OTEL_RESOURCE_ATTRIBUTES`` env
    var (env wins per OTel spec)."""

    httpx_instrumented: bool = True
    """Auto-instrument ``httpx`` clients when the SDK is available."""

    subprocess_instrumented: bool = False
    """Wrap subprocess invocations in spans. Off by default (the
    overhead matters for tight CLI loops)."""

    metrics_enabled: bool = False
    """Enable the OTel meter alongside tracer. Used by the library's
    self-observation counters (closes FEEDBACK design ref §14.7)
    when set."""


class CrashSettings(BaseModel):
    """Crash-reporter fields."""

    model_config = ConfigDict(extra="forbid")

    sinks: list[str] = Field(default_factory=lambda: ["local"])
    """Active sink names. ``local`` is always available (writes
    JSON to ``state_dir``); registered sinks (``sentry`` / ``otlp``
    / ``http`` / consumer-registered names) join the fan-out when
    listed here. Closes FEEDBACK API-07 partially — the registry
    pattern lands in Phase 3."""

    sentry_dsn: str | None = None
    """Sentry DSN. Required when ``sinks`` includes ``"sentry"``."""

    otlp_endpoint: str | None = None
    """OTLP endpoint for the crash log emitter. Defaults to the
    same endpoint as :attr:`OtelSettings.endpoint` when ``None``."""

    http_url: str | None = None
    """Webhook URL for the ``http`` sink. Required when ``sinks``
    includes ``"http"``."""

    state_dir: Path | None = None
    """Override the local sink's directory. ``None`` →
    ``<state>/crashes/`` derived from ``app_name``."""


class VaultSettings(BaseModel):
    """Vault-backend selection + per-backend configuration.

    The ``backend`` field selects one of the registered vault
    backends (built-in: ``"env"`` / ``"keyring"`` / ``"file"``;
    consumers register additional names via
    :func:`mgf.common.vault.register_vault_backend`). The
    ``config`` field accepts either:

      - A typed config — :class:`~mgf.common.vault.EnvVaultConfig`
        / :class:`~mgf.common.vault.KeyringVaultConfig` /
        :class:`~mgf.common.vault.EncryptedFileVaultConfig` for
        built-ins, or a custom backend's registered
        ``config_model``. Pydantic enforces the shape at
        construction.
      - A ``dict[str, Any]``. The built-in backends register a
        ``config_model``, so an incoming dict is validated and
        stored as the typed instance. Custom backends registered
        without a ``config_model`` keep the dict shape and the
        factory owns its own validation. (Pre-v1.0 the built-in
        factories ALSO accepted a bare dict with a deprecation
        warning; that path was removed.)

    Examples::

        VaultSettings(config=EnvVaultConfig(prefix="MYAPP_VAULT_"))
        VaultSettings(
            backend="file",
            config=EncryptedFileVaultConfig(
                path=Path("/etc/myapp/vault.dat"),
                passphrase="…",
            ),
        )
        # Custom backend with a registered config_model — dict is
        # validated up front into the typed instance:
        VaultSettings(backend="hashicorp", config={"url": "…", "token": "…"})

        # Custom backend without a config_model — dict passes through:
        VaultSettings(backend="legacy-store", config={"opaque": "blob"})
    """

    model_config = ConfigDict(extra="forbid")

    backend: str = "env"
    """Active vault backend name. Must match a registered
    backend (built-in: ``env`` / ``keyring`` / ``file``;
    consumer-registered names also accepted). The default is
    ``"env"`` because it is the only backend that works without
    the ``[vault]`` extra installed."""

    config: Any = Field(default_factory=dict)
    """Backend-specific configuration. Accepts a typed config
    instance (recommended) OR a ``dict[str, Any]``.

    When the active ``backend`` was registered with a
    ``config_model``, an incoming dict is validated against the
    model at construction; the field on the constructed instance
    holds the typed model, not the original dict. Mismatched
    typed instances (e.g., ``KeyringVaultConfig`` for
    ``backend="env"``) raise loudly. When the backend has no
    ``config_model``, the dict passes through and the factory
    is responsible for its own validation."""

    @model_validator(mode="after")
    def _coerce_config_against_registered_model(self) -> VaultSettings:
        """Validate ``config`` against the active backend's
        registered ``config_model`` (when one exists).

        Three cases:

        1. Backend has no ``config_model`` (or backend is
           unregistered) — leave ``config`` untouched. Factories
           registered without a model own their own validation.
        2. ``config`` is already an instance of the registered
           model (or a subclass) — leave it.
        3. ``config`` is a dict — validate via
           ``model_cls.model_validate(config)`` and replace.
        4. ``config`` is a different ``BaseModel`` subclass —
           raise ``ValueError`` naming the expected vs. given.
        """
        # Lazy import to avoid a startup cycle: vault → settings → vault.
        from mgf.common.vault._registry import _get_config_model

        model_cls = _get_config_model(self.backend)
        if model_cls is None:
            return self
        if isinstance(self.config, model_cls):
            return self
        if isinstance(self.config, BaseModel):
            raise ValueError(
                f"VaultSettings.config: backend={self.backend!r} expects "
                f"a {model_cls.__name__} (or dict to validate against it); "
                f"got {type(self.config).__name__}."
            )
        if isinstance(self.config, dict):
            self.config = model_cls.model_validate(self.config)
            return self
        raise ValueError(
            f"VaultSettings.config: expected {model_cls.__name__} or "
            f"dict; got {type(self.config).__name__}."
        )


class RedactionSettings(BaseModel):
    """Redactor configuration. Closes FEEDBACK design ref §14.12
    (strict_mode for GDPR/PII)."""

    model_config = ConfigDict(extra="forbid")

    extra_keys: frozenset[str] = Field(default_factory=frozenset)
    """Project-specific secret-bearing key names to scrub in
    addition to the built-in set (``DEFAULT_REDACTION_KEYS``)."""

    extra_value_patterns: list[str] = Field(default_factory=list)
    """Per-project regex patterns to scrub in string values.
    Concatenated with the built-in patterns (Bearer / JWT / PEM /
    OpenAI / GitHub / AWS / Slack / Google API keys)."""

    enabled_pattern_groups: list[str] = Field(default_factory=lambda: ["builtin"])
    """Active value-pattern groups. ``builtin`` is the always-on
    set. Consumers register additional groups via
    ``register_redaction_pattern_group(name, patterns)`` (Phase 3)
    and enable them by listing the name here."""

    strict_mode: bool = False
    """Adds extra value-pattern redaction for common PII shapes
    (email / IPv4 / IPv6 / US phone / SSN / IBAN / Luhn-validated
    CC). HIGH FALSE-POSITIVE rate — enable only when
    legal/compliance requires. The patterns over-match (e.g., a
    UUID may match the IPv6 pattern). Closes FEEDBACK design ref
    §14.12. Phase 1 wires the field; Phase 6 wires the patterns."""


# ---------------------------------------------------------------------------
# Preset table — Phase 6a wires the §6 preset semantics
# ---------------------------------------------------------------------------
#
# Each preset entry maps ``{sub_model_name: {field: value}}``. Applied
# by :meth:`MgfSettings._apply_preset` AFTER pydantic-settings collates
# defaults + env + init kwargs but BEFORE the model is returned to the
# caller. Per the design (CLOSED_BOX_REDESIGN.md §6) explicit
# overrides win — preset values are only applied to fields the user
# (or env / file source) did NOT explicitly set.
#
# ``"explicit"`` ships an empty entry — every field stays at its
# declared default. The new-project default; surfaces every knob.
# ``"auto"`` is special-cased in :func:`_resolve_preset_overrides`
# because its values come from runtime detection (TTY / JOURNAL_STREAM
# / OTEL_EXPORTER_OTLP_ENDPOINT) at construction time.

_STATIC_PRESETS: dict[PresetName, dict[str, dict[str, Any]]] = {
    "explicit": {},
    "dev": {
        "logging": {
            "level": "DEBUG",
            "format": "text",
            "journald": False,
        },
        "otel": {
            "enabled": False,
        },
    },
    "systemd": {
        "logging": {
            "format": "json",
            "journald": True,
        },
    },
    "docker": {
        "logging": {
            "format": "json",
            "journald": False,
            # No file sink — the container runtime captures stdout.
            # The actual file=None default already does this; the
            # entry is a documentation marker.
            "file": None,
        },
    },
    # "auto" handled in _resolve_preset_overrides — runtime detection.
}


def _resolve_single_preset(
    preset: PresetName,
) -> dict[str, dict[str, Any]]:
    """Return the ``{sub_model: {field: value}}`` overrides for a
    single named preset.

    ``"auto"`` consults the live process state (``sys.stdout.isatty``,
    ``JOURNAL_STREAM``, ``OTEL_EXPORTER_OTLP_ENDPOINT``) so it
    preserves the v0.1 auto-detection behaviour. Every other preset
    is a static lookup.

    Called by :func:`_resolve_preset_overrides` at
    :class:`MgfSettings` construction time. The detection is
    intentionally NOT cached at module import — that would freeze
    the values to whatever the env looked like at library load time,
    which is the wrong semantics when callers mutate ``os.environ``
    between import and ``bootstrap()``.
    """
    if preset != "auto":
        return _STATIC_PRESETS[preset]

    # The legacy v0.1 detection path. None of these reads raise.
    is_tty = sys.stdout.isatty()
    has_journal = bool(os.environ.get("JOURNAL_STREAM"))
    has_otel = bool(os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"))

    return {
        "logging": {
            "format": "text" if is_tty else "json",
            "journald": has_journal,
        },
        "otel": {
            "enabled": has_otel,
        },
    }


def _resolve_preset_overrides(
    preset: PresetName | list[PresetName],
) -> dict[str, dict[str, Any]]:
    """Return the resolved ``{sub_model: {field: value}}`` overrides
    for either a single preset OR a list applied left-to-right.

    v0.4 Phase C: ``preset`` accepts a list. Each preset's overrides
    are merged into the result in order — later presets win on
    field conflicts. Use case: stack a deployment-shape preset
    (e.g. ``"docker"``) with an environment-detection preset (e.g.
    ``"auto"``) to get docker's JSON-format defaults plus
    auto-detected OTel endpoint.

    Examples::

        _resolve_preset_overrides("docker")
        # → {"logging": {"format": "json", ...}}

        _resolve_preset_overrides(["docker", "auto"])
        # → docker's overrides PLUS auto's overrides; auto's
        #   otel.enabled=True wins if OTEL_EXPORTER_OTLP_ENDPOINT
        #   is set in the environment.
    """
    if isinstance(preset, str):
        return _resolve_single_preset(preset)

    # Composable list — merge per sub-model, later wins.
    result: dict[str, dict[str, Any]] = {}
    for name in preset:
        layer = _resolve_single_preset(name)
        for sub_name, sub_overrides in layer.items():
            if sub_name not in result:
                result[sub_name] = {}
            result[sub_name].update(sub_overrides)
    return result


# ---------------------------------------------------------------------------
# Top-level settings root
# ---------------------------------------------------------------------------


class MgfSettings(BaseAppSettings):
    """Single typed root for every ``mgf-common`` configuration knob.

    Sub-models composed via ``Field(default_factory=...)`` so each
    instance gets a fresh sub-model (not the class default).

    Construct directly for explicit configuration::

        from mgf.common import bootstrap
        from mgf.common.settings import MgfSettings, LoggingSettings

        with bootstrap(
            app_name="myapp",
            app_version="1.0",
            settings=MgfSettings(
                logging=LoggingSettings(level="DEBUG", format="json"),
            ),
        ):
            ...

    Or rely on env-var aliases (``MGF_LOGGING__LEVEL=DEBUG``)::

        with bootstrap(app_name="myapp", app_version="1.0"):
            ...
        # → MgfSettings reads MGF_* env vars, applies on top of
        #   the ``preset="auto"`` defaults

    The library reads ``MgfSettings`` only — never ``os.environ``
    directly outside this module. ``mgf-common explain`` (future)
    prints the active state with each field's source (default /
    env / file / override)."""

    # The library's own settings prefix — closes FEEDBACK design
    # ref §14.16 (``MGF_*`` is the reserved namespace).
    # ``env_nested_delimiter="__"`` enables ``MGF_LOGGING__LEVEL=DEBUG``
    # style env-var aliases for nested sub-models (the standard
    # pydantic-settings pattern).
    model_config = settings_config(
        env_prefix="MGF_",
        env_nested_delimiter="__",
    )

    # Settings-version pinning — pydantic-settings ignores this
    # field name; we use the BaseAppSettings::CURRENT_VERSION
    # mechanism (rule CF-03) for actual schema evolution.
    CURRENT_VERSION: ClassVar[int] = 1

    logging: LoggingSettings = Field(default_factory=LoggingSettings)
    otel: OtelSettings = Field(default_factory=OtelSettings)
    crash: CrashSettings = Field(default_factory=CrashSettings)
    redaction: RedactionSettings = Field(default_factory=RedactionSettings)
    vault: VaultSettings = Field(default_factory=VaultSettings)

    preset: PresetName | list[PresetName] = "explicit"
    """Bundle of behavioural defaults applied before per-field
    overrides. See :data:`PresetName` for the available presets.

    Phase 1 shipped the field as a typed seam; Phase 6a wires the
    semantics via :meth:`_apply_preset` (an ``after`` model
    validator). Each preset is a ``{sub_model: {field: value}}``
    map applied to fields the caller did NOT explicitly set.

    v0.4 Phase C: accepts a LIST of presets applied left-to-right.
    Later entries win on field conflicts — useful for stacking a
    deployment shape with an environment-detection preset::

        MgfSettings(preset=["docker", "auto"])
        # docker's static overrides PLUS auto's runtime detection

    Via env var, pass JSON: ``MGF_PRESET='["docker","auto"]'``.

    The ``"auto"`` preset preserves v0.1 auto-detection (TTY /
    JOURNAL_STREAM / OTEL endpoint) and is the default when
    ``bootstrap()`` is called WITHOUT a ``settings=`` argument
    (closes FEEDBACK design ref §14.15 — eliminates the
    silent-break-on-upgrade failure mode)."""

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: type,
        init_settings: Any,
        env_settings: Any,
        dotenv_settings: Any,
        file_secret_settings: Any,
    ) -> tuple[Any, ...]:
        """Wire :class:`SmartEnvSource` and :class:`PyprojectSource`.

        v0.4 Phase C wired the pyproject source between env and
        dotenv. v0.4.2 (V04-04) replaces the default
        :class:`pydantic_settings.EnvSettingsSource` with
        :class:`mgf.common._env_source.SmartEnvSource` — supports
        single-underscore nesting (``MGF_LOGGING_LEVEL=DEBUG``)
        AND double-underscore (legacy
        ``MGF_LOGGING__LEVEL=DEBUG``). Longest-field-name-first
        matching disambiguates fields whose names contain
        underscores (``service_namespace``, ``level_overrides``,
        etc.).

        Final precedence chain (highest first):

          1. Explicit constructor kwargs.
          2. ``MGF_*`` environment variables (smart parser).
          3. ``.env`` file (if configured).
          4. ``[tool.mgf]`` in pyproject.toml (on-disk config — below `.env` per CF-02).
          5. File-secrets directory (if configured — not used).
          6. Field defaults.

        The preset semantics (the ``model_validator(mode="after")``)
        layer on top of all of these.
        """
        from mgf.common._env_source import SmartEnvSource
        from mgf.common.config._pyproject_source import PyprojectSource

        return (
            init_settings,
            SmartEnvSource(settings_cls),
            dotenv_settings,
            PyprojectSource(settings_cls),
            file_secret_settings,
        )

    @model_validator(mode="after")
    def _apply_preset(self) -> Self:
        """Apply preset defaults to sub-model fields the caller did not set.

        Phase 6a wiring of the §6 design: ``preset`` is a typed
        seam declared in Phase 1; this validator gives it
        behaviour. The semantics:

          1. Look up the preset's overrides via
             :func:`_resolve_preset_overrides`. (``"auto"`` reads
             runtime state; every other preset is a static table.)
          2. For each sub-model named in the overrides, walk its
             ``(field, value)`` pairs.
          3. If the field is NOT in the sub-model's
             ``model_fields_set`` (the user did not pass it
             directly, no env var supplied it, no config-file
             entry mentioned it), set the field to the preset's
             value.
          4. Mark the field as set in the sub-model so subsequent
             introspection (``mgf-common explain``, Phase 6b)
             reports the correct provenance.

        The ``"explicit"`` preset has an empty override table —
        every field stays at its declared default. That is the
        new-project default, surfaces every knob.

        Idempotency: re-validating an already-preset-applied
        instance is a no-op because the preset's fields are now
        in ``model_fields_set``.
        """
        overrides = _resolve_preset_overrides(self.preset)
        for sub_name, sub_overrides in overrides.items():
            sub_model = getattr(self, sub_name)
            for field, value in sub_overrides.items():
                if field in sub_model.model_fields_set:
                    continue
                setattr(sub_model, field, value)
                # Pydantic's public model_fields_set is read-only; the
                # underlying set is __pydantic_fields_set__. Mutating
                # it tells future introspection "this field came from
                # the preset, not from the declared default".
                sub_model.__pydantic_fields_set__.add(field)
        return self

    def model_dump_for_yaml(self) -> dict[str, Any]:
        """Recursive-safe dict for YAML serialisation.

        Works around the shallow ``BaseAppSettings.to_yaml_dict``
        (CB-06 — documented in CONFIGURATION.md §8.4). Use this
        for ``yaml.safe_dump`` until ``to_yaml_dict`` is fixed
        upstream in a future release."""
        return self.model_dump(mode="json")

    # ------------------------------------------------------------------
    # v0.4 Phase C — intent-driven factory classmethods
    # ------------------------------------------------------------------
    #
    # The ``preset=`` field is the low-level seam (a Literal); the
    # factories below are the ergonomic layer ABOVE that. They bundle
    # named intents ("development", "production", "testing") with the
    # minimal default-overrides each implies. Callers who want a
    # one-liner construction with intent-tag use these; callers who
    # want full control still pass kwargs to ``MgfSettings(...)``.
    #
    # Each factory accepts the same ``**kwargs`` shape as the
    # constructor — pass typed sub-models (e.g.
    # ``logging=LoggingSettings(level="DEBUG")``) to override fields.
    # User-supplied kwargs ALWAYS win over the factory's defaults.

    @classmethod
    def dev(cls, **kwargs: Any) -> Self:
        """Construct an :class:`MgfSettings` tuned for local development.

        Equivalent to ``MgfSettings(preset="dev", **kwargs)``. The
        ``"dev"`` preset sets logging to DEBUG + text format with no
        journald sink; OTel stays off (no sampling overhead in dev).

        Usage::

            with mgf.common.bootstrap(settings=MgfSettings.dev()):
                ...

            # Override individual sub-models:
            MgfSettings.dev(logging=LoggingSettings(level="WARNING"))
        """
        kwargs.setdefault("preset", "dev")
        return cls(**kwargs)

    @classmethod
    def production(cls, **kwargs: Any) -> Self:
        """Construct an :class:`MgfSettings` tuned for production.

        Defaults: ``preset="explicit"`` (no auto-detection — production
        behaviour MUST be deterministic), JSON logging (every
        mainstream aggregator parses it), low OTel sample rate (0.05)
        to cap volume in high-traffic services.

        Override any of these via kwargs::

            MgfSettings.production(otel=OtelSettings(sample_rate=0.01))
            MgfSettings.production(logging=LoggingSettings(format="text"))
        """
        kwargs.setdefault("preset", "explicit")
        if "logging" not in kwargs:
            kwargs["logging"] = LoggingSettings(format="json")
        if "otel" not in kwargs:
            kwargs["otel"] = OtelSettings(sample_rate=0.05)
        return cls(**kwargs)

    @classmethod
    def testing(cls, **kwargs: Any) -> Self:
        """Construct an :class:`MgfSettings` tuned for unit tests.

        Defaults: ``preset="explicit"`` (deterministic — no env-driven
        surprises), logging at WARNING (cuts test-suite noise), OTel
        off (no real spans / no exporter overhead). Crash sinks stay
        at the default ``["local"]`` — no remote shipping during tests.

        Pair with ``filterwarnings=["error::mgf.common._warnings.MgfDeprecationWarning"]``
        in your pytest config so any v0.1 surface that creeps back in
        becomes a hard test failure (per the v0.3 migration guide §4).
        """
        kwargs.setdefault("preset", "explicit")
        if "logging" not in kwargs:
            kwargs["logging"] = LoggingSettings(level="WARNING")
        if "otel" not in kwargs:
            kwargs["otel"] = OtelSettings(enabled=False)
        return cls(**kwargs)


__all__ = [
    "CrashSettings",
    "LogFormat",
    "LogLevel",
    "LoggingSettings",
    "MgfSettings",
    "OtelSettings",
    "PresetName",
    "RedactionSettings",
    "VaultSettings",
]
