"""Typed configuration models for the built-in vault backends.

v0.4 Phase D — closes the closed-box leak documented in
``CLOSED_BOX_REDESIGN.md`` §3 and ``FEEDBACK.md`` round 1: the v0.3
``VaultSettings.config: dict[str, Any]`` was stringly-typed, forcing
consumers to read source to know what keys each backend wants. Each
typed config below carries:

  - A ``backend_type`` :class:`typing.Literal` field as the
    discriminator. The same discriminator is used in the union on
    :class:`mgf.common.settings.VaultSettings.config` for type-safe
    construction.
  - The set of fields each backend's factory consumes — defaults
    where the v0.3 dict-shape factories had defaults; required where
    the factory raised on missing keys.
  - ``model_config = ConfigDict(extra="forbid", frozen=True)`` so a
    typo fails loudly at construction AND the config is immutable
    once built (consistent with rule DP-13 — resource ownership).

For consumer-registered backends (HashiCorp Vault, AWS Secrets
Manager), the dict shape on ``VaultSettings.config`` still works —
the typed-config path is the recommended shape for built-ins; the
dict fallback is for everything else. A future minor version can
extend the registry to accept consumer-registered config models;
Phase D ships the foundation.

Usage::

    from mgf.common.vault import EnvVaultConfig
    from mgf.common.settings import VaultSettings

    settings = VaultSettings(
        backend="env",
        config=EnvVaultConfig(prefix="MYAPP_VAULT_"),
    )

The legacy dict shape continues to work::

    settings = VaultSettings(
        backend="env",
        config={"prefix": "MYAPP_VAULT_"},
    )
    # Emits MgfDeprecationWarning on built-in backends; v0.5 may
    # tighten this further.
"""

from __future__ import annotations

from pathlib import Path  # noqa: TC003 — pydantic needs runtime resolution
from typing import Annotated, Literal

from pydantic import BaseModel, ConfigDict, Field, SecretStr


class _BaseVaultConfig(BaseModel):
    """Base class for typed vault-backend configs.

    Subclasses MUST declare a ``backend_type: Literal[...]`` field
    matching their registered backend name; that field is the
    discriminator for the union below.
    """

    model_config = ConfigDict(extra="forbid", frozen=True)


class EnvVaultConfig(_BaseVaultConfig):
    """Typed config for the built-in ``"env"`` vault backend.

    Maps each key to ``<prefix><KEY>`` env var (uppercased). The
    right shape for CI / containerised deployments where the
    orchestrator injects secrets at process-start time. Works
    WITHOUT the optional ``[vault]`` extra installed.
    """

    backend_type: Literal["env"] = "env"
    """Discriminator. Must be ``"env"`` (Literal — pydantic enforces)."""

    prefix: str = "MGF_VAULT_"
    """Env-var prefix prepended to every key. Default
    ``"MGF_VAULT_"``; per-app overrides typically use the app's own
    namespace (e.g. ``"VMANAGER_VAULT_"``)."""


class KeyringVaultConfig(_BaseVaultConfig):
    """Typed config for the built-in ``"keyring"`` vault backend.

    Uses the third-party :mod:`keyring` library to read / write
    against the OS credential store (libsecret on Linux, Keychain
    on macOS, Credential Manager on Windows). Requires
    ``mgf-common[vault]``.
    """

    backend_type: Literal["keyring"] = "keyring"
    """Discriminator. Must be ``"keyring"``."""

    service: str = "mgf-common"
    """Keyring service name. Consumer apps SHOULD override per
    rule SC-12 — the default ``"mgf-common"`` is the library's own
    namespace and would collide if multiple apps share it."""


class EncryptedFileVaultConfig(_BaseVaultConfig):
    """Typed config for the built-in ``"file"`` vault backend.

    Fernet (AES-128 + HMAC-SHA256) encrypted JSON file with
    PBKDF2-HMAC-SHA256 (600 000 iterations, OWASP 2023 baseline
    per rule SC-05). Requires ``mgf-common[vault]``. The fallback
    for headless Linux / embedded CI where no keyring daemon runs.
    """

    backend_type: Literal["file"] = "file"
    """Discriminator. Must be ``"file"``."""

    path: Path
    """Path to the encrypted vault file. Created at first
    :meth:`mgf.common.vault.EncryptedFileVault.set` if missing."""

    passphrase: SecretStr
    """The passphrase used for PBKDF2 key derivation. The consumer
    is responsible for prompting / fetching this — typically from
    an env var, a system keyring entry, or a user prompt at
    startup.

    Typed :class:`pydantic.SecretStr` (rule **CF-05** / **SC-01**): a
    ``model_dump()`` masks it as ``**********``, so it can NEVER be
    serialised in clear into ``config.yaml`` (``save_settings``) or a
    doctor runtime snapshot. Read the real value at the crypto boundary
    via ``.get_secret_value()``. ``"passphrase"`` is also in the default
    redaction key set, so it is scrubbed from any log extra (SC-17)."""


# ---------------------------------------------------------------------------
# Discriminated union — used by :class:`VaultSettings.config`
# ---------------------------------------------------------------------------

BuiltinVaultBackendConfig = Annotated[
    EnvVaultConfig | KeyringVaultConfig | EncryptedFileVaultConfig,
    Field(discriminator="backend_type"),
]
"""Tagged union over the three built-in typed configs.

The ``backend_type`` field is the discriminator — pydantic uses it
to pick the right config class when validating a dict-shaped input.
``VaultSettings.config`` is typed as
``BuiltinVaultBackendConfig | dict[str, Any]`` so consumers can pass
typed configs for built-ins (recommended) OR raw dicts for
consumer-registered custom backends (still works; future minor
version may tighten with a registered-config-model story).
"""


__all__ = [
    "BuiltinVaultBackendConfig",
    "EncryptedFileVaultConfig",
    "EnvVaultConfig",
    "KeyringVaultConfig",
]
