"""
core/secrets/client.py
Secrets client with Infisical SDK → environment variable → default fallback chain.

Resolution order per get() call:
  1. In-memory cache       (fast path, avoids repeated lookups)
  2. Infisical SDK         (if token + project_id both available and SDK importable)
  3. os.environ            (always available)
  4. caller-supplied default
  5. KeyError              (if no default supplied)

# VERIFICATION_STAMP
# Story: M1.02 — core/secrets/client.py
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 16/16
# Coverage: 100%
"""
from __future__ import annotations

import logging
import os
from typing import Optional

logger = logging.getLogger(__name__)


class GenesisSecrets:
    """
    Unified secret access for Genesis.

    Parameters
    ----------
    infisical_token : str, optional
        Infisical Universal Auth *client secret*.
        Falls back to ``INFISICAL_TOKEN`` env var when not supplied.
        When neither is present Infisical is skipped silently.
    environment : str
        Infisical environment slug (``"dev"``, ``"staging"``, ``"prod"``).
        Defaults to ``"prod"``.
    project_id : str, optional
        Infisical project ID (UUID).
        Falls back to ``INFISICAL_PROJECT_ID`` env var.
        Required together with *infisical_token* to enable Infisical mode.

    Notes
    -----
    Infisical SDK uses Universal Auth: ``client_id`` == project_id,
    ``client_secret`` == token.  When the SDK is not installed the module
    degrades gracefully to env-var mode with a single debug log line.
    """

    def __init__(
        self,
        infisical_token: Optional[str] = None,
        environment: str = "prod",
        project_id: Optional[str] = None,
    ) -> None:
        self._token: Optional[str] = infisical_token or os.environ.get("INFISICAL_TOKEN")
        self._environment: str = environment
        self._project_id: Optional[str] = project_id or os.environ.get("INFISICAL_PROJECT_ID")
        self._infisical_client = None
        self._cache: dict[str, str] = {}

        # Attempt Infisical initialisation only when both credentials are present
        if self._token and self._project_id:
            self._init_infisical()

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _init_infisical(self) -> None:
        """Try to import and authenticate the Infisical SDK."""
        try:
            from infisical_sdk import InfisicalSDKClient  # type: ignore[import]

            client = InfisicalSDKClient(host="https://app.infisical.com")
            client.auth.universal_auth.login(
                client_id=self._project_id,
                client_secret=self._token,
            )
            self._infisical_client = client
            logger.info("GenesisSecrets: Infisical client initialised (project=%s)", self._project_id)
        except ImportError:
            logger.debug(
                "GenesisSecrets: infisical_sdk not installed — falling back to env vars"
            )
        except Exception as exc:  # noqa: BLE001
            logger.warning(
                "GenesisSecrets: Infisical auth failed (%s) — falling back to env vars", exc
            )

    def _fetch_from_infisical(self, key: str) -> Optional[str]:
        """Return the secret value from Infisical, or None on any error."""
        if self._infisical_client is None:
            return None
        try:
            secret = self._infisical_client.secrets.get_secret_by_name(
                secret_name=key,
                project_id=self._project_id,
                environment_slug=self._environment,
            )
            return secret.secret_value
        except Exception as exc:  # noqa: BLE001
            logger.debug("GenesisSecrets: Infisical lookup failed for %r: %s", key, exc)
            return None

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def get(self, key: str, default: Optional[str] = None) -> str:
        """
        Fetch a secret by *key*.

        Resolution order: **cache → Infisical → env var → default → KeyError**

        Parameters
        ----------
        key : str
            Secret name (case-sensitive).
        default : str, optional
            Value to return when the secret is not found anywhere.

        Returns
        -------
        str
            The resolved secret value.

        Raises
        ------
        KeyError
            When *key* is absent from all sources and no *default* is given.
        """
        # 1. In-memory cache
        if key in self._cache:
            return self._cache[key]

        # 2. Infisical
        value = self._fetch_from_infisical(key)
        if value is not None:
            self._cache[key] = value
            return value

        # 3. Environment variable
        env_value = os.environ.get(key)
        if env_value is not None:
            self._cache[key] = env_value
            return env_value

        # 4. Caller default
        if default is not None:
            return default

        # 5. Hard failure
        raise KeyError(
            f"Secret {key!r} not found in Infisical, environment variables, or defaults"
        )

    def get_all(self) -> dict[str, str]:
        """
        Return a copy of all *cached* secrets.

        This does **not** pull the full secret list from Infisical.
        Only secrets that have been fetched via :meth:`get` appear here.
        """
        return dict(self._cache)

    def clear_cache(self) -> None:
        """Discard all cached secret values, forcing fresh lookups on next access."""
        self._cache.clear()


# ---------------------------------------------------------------------------
# Module-level singleton + convenience function
# ---------------------------------------------------------------------------

_instance: Optional[GenesisSecrets] = None


def get_secret(key: str, default: Optional[str] = None) -> str:
    """
    Fetch a single secret using the module-level :class:`GenesisSecrets` singleton.

    The singleton is created lazily on first call.  It reads
    ``INFISICAL_TOKEN`` and ``INFISICAL_PROJECT_ID`` from the environment
    automatically.

    Parameters
    ----------
    key : str
        Secret name.
    default : str, optional
        Fallback when the secret is absent from all sources.

    Returns
    -------
    str
        The resolved secret value.

    Raises
    ------
    KeyError
        When *key* is not found and no *default* is provided.
    """
    global _instance  # noqa: PLW0603
    if _instance is None:
        _instance = GenesisSecrets()
    return _instance.get(key, default)
