"""Atlas sub-client — app registration, token exchange, env provisioning, frontier resolution.

Provides two entry points:

1. **AtlasClient** — sub-client following the standard ``GecClient`` pattern.
   Used via ``client.atlas.resolve(...)`` when you already have an ``SbnClient``.

2. **Atlas** — standalone convenience class with auto-config.
   Used via ``Atlas.from_credentials(...)`` for zero-setup SDK usage.

Example::

    from sbn import Atlas

    # Option A: explicit credentials
    atlas = Atlas.from_credentials(
        client_id="atl_a1b2c3d4e5f6g7h8",
        client_secret="atl_sec_...",
        base_url="https://api.smartblocks.network",
    )

    # Option B: from environment variables
    atlas = Atlas.from_env()

    # Frontier info is immediately available
    print(atlas.scoring_frontier)   # "ai.assistant"
    print(atlas.regime)             # "unknown"
    print(atlas.env_vars)           # {"ATLAS_CODE_FRONTIER": "ai.assistant", ...}

    # Resolve at runtime
    result = atlas.resolve(workflow="code", event_type="frontend_generation")
"""
from __future__ import annotations

import os
import time
from dataclasses import dataclass, field
from typing import Any, Mapping

from sbn._http import AuthState, HttpTransport, RetryConfig, SbnError


PREFIX = "/api/atlas"


# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------

@dataclass(slots=True)
class AtlasBinding:
    """A single workflow → frontier binding with CSK diagnostics."""

    workflow: str
    frontier: str
    c_max: float | None = None
    regime: str = "unknown"
    metallic_label: str | None = None
    lambda_score: float | None = None
    auto_provisioned: bool = True
    env_prefix: str = ""

    @classmethod
    def from_dict(cls, data: Mapping[str, Any]) -> AtlasBinding:
        return cls(
            workflow=str(data.get("workflow", "default")),
            frontier=str(data.get("frontier", "")),
            c_max=data.get("c_max"),
            regime=str(data.get("regime", "unknown")),
            metallic_label=data.get("metallic_label"),
            lambda_score=data.get("lambda_score"),
            auto_provisioned=bool(data.get("auto_provisioned", True)),
            env_prefix=str(data.get("env_prefix", "")),
        )


@dataclass(slots=True)
class AtlasConfig:
    """Resolved Atlas configuration for a registered app."""

    app_id: str
    client_id: str
    app_name: str = ""
    bindings: list[AtlasBinding] = field(default_factory=list)
    env_vars: dict[str, str] = field(default_factory=dict)

    @classmethod
    def from_dict(cls, data: Mapping[str, Any]) -> AtlasConfig:
        bindings = [
            AtlasBinding.from_dict(b) for b in data.get("bindings", [])
        ]
        return cls(
            app_id=str(data.get("app_id", "")),
            client_id=str(data.get("client_id", "")),
            app_name=str(data.get("app_name", "")),
            bindings=bindings,
            env_vars=dict(data.get("env_vars", {})),
        )


# ---------------------------------------------------------------------------
# AtlasClient — sub-client (used via SbnClient.atlas)
# ---------------------------------------------------------------------------

class AtlasClient:
    """Atlas sub-client for frontier resolution, app management, and provisioning.

    Used as ``client.atlas.resolve(...)`` when wired into ``SbnClient``.
    """

    def __init__(self, transport: HttpTransport) -> None:
        self._t = transport

    # ── Frontier Resolution ───────────────────────────────────────────

    def resolve(
        self,
        *,
        workflow: str | None = None,
        event_type: str | None = None,
        frontier_hint: str | None = None,
        context: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Resolve the best frontier for the given workflow context.

        Returns scoring frontier, CSK diagnostics, regime placement,
        metallic map parameters, and steering guidance.
        """
        body: dict[str, Any] = {}
        if workflow:
            body["workflow"] = workflow
        if event_type:
            body["event_type"] = event_type
        if frontier_hint:
            body["frontier_hint"] = frontier_hint
        if context:
            body["context"] = context
        return self._t.post(f"{PREFIX}/resolve", json=body).json()

    def list_frontiers(
        self,
        *,
        kind: str | None = None,
        family: str | None = None,
        regime: str | None = None,
    ) -> dict[str, Any]:
        """List all frontiers in the Atlas registry with optional filters."""
        params: dict[str, str] = {}
        if kind:
            params["kind"] = kind
        if family:
            params["family"] = family
        if regime:
            params["regime"] = regime
        return self._t.get(f"{PREFIX}/frontiers", params=params).json()

    # ── App Registration ──────────────────────────────────────────────

    def register_app(
        self,
        *,
        app_name: str,
        app_family: str,
        declared_workflows: list[str],
        declared_event_types: list[str] | None = None,
        token_ttl_seconds: int = 3600,
    ) -> dict[str, Any]:
        """Register a new Atlas app with auto-provisioned frontier bindings.

        Returns client credentials (``client_secret`` shown once only).
        """
        body: dict[str, Any] = {
            "app_name": app_name,
            "app_family": app_family,
            "declared_workflows": declared_workflows,
            "token_ttl_seconds": token_ttl_seconds,
        }
        if declared_event_types:
            body["declared_event_types"] = declared_event_types
        return self._t.post(f"{PREFIX}/apps/register", json=body).json()

    def exchange_token(
        self,
        *,
        client_id: str,
        client_secret: str,
    ) -> dict[str, Any]:
        """Exchange client credentials for a short-lived Atlas bearer token."""
        body = {"client_id": client_id, "client_secret": client_secret}
        return self._t.post(f"{PREFIX}/token", json=body).json()

    # ── Env Provisioning ──────────────────────────────────────────────

    def get_env(
        self,
        app_id: str,
        *,
        format: str = "json",
    ) -> dict[str, Any] | str:
        """Get provisioned environment variables for a registered app.

        Pass ``format="dotenv"`` for plain-text KEY=VALUE output.
        """
        resp = self._t.get(f"{PREFIX}/apps/{app_id}/env", params={"format": format})
        if format == "dotenv":
            return resp.text
        return resp.json()

    # ── App Management ────────────────────────────────────────────────

    def list_apps(self) -> dict[str, Any]:
        """List all Atlas apps registered by the authenticated user."""
        return self._t.get(f"{PREFIX}/apps").json()

    def revoke_app(self, app_id: str) -> dict[str, Any]:
        """Revoke an Atlas app and invalidate all its tokens."""
        return self._t.delete(f"{PREFIX}/apps/{app_id}").json()


# ---------------------------------------------------------------------------
# Atlas — standalone convenience class with auto-config
# ---------------------------------------------------------------------------

class Atlas:
    """Zero-setup Atlas client that auto-discovers config via token exchange.

    Usage::

        atlas = Atlas.from_credentials(
            client_id="atl_...",
            client_secret="atl_sec_...",
            base_url="https://api.smartblocks.network",
        )
        print(atlas.scoring_frontier)
        print(atlas.regime)
        result = atlas.resolve(workflow="code")

    The client:
    1. Exchanges credentials for a short-lived token
    2. Fetches env vars (frontier bindings, CSK, dynamics)
    3. Caches config and auto-refreshes token on expiry
    """

    def __init__(
        self,
        *,
        client_id: str,
        client_secret: str,
        base_url: str = "https://api.smartblocks.network",
        timeout: float = 10.0,
        retry: RetryConfig | None = None,
    ) -> None:
        self._client_id = client_id
        self._client_secret = client_secret
        self._base_url = base_url.rstrip("/")

        self._auth = AuthState()
        self._transport = HttpTransport(
            base_url=self._base_url,
            auth=self._auth,
            retry=retry,
            timeout=timeout,
            user_agent="sbn-atlas-sdk/0.1",
        )
        self._atlas_client = AtlasClient(self._transport)

        # Token state
        self._token: str | None = None
        self._token_expires_at: float = 0.0
        self._app_id: str | None = None

        # Resolved config (populated on first access)
        self._config: AtlasConfig | None = None

    # ── Factory methods ───────────────────────────────────────────────

    @classmethod
    def from_credentials(
        cls,
        *,
        client_id: str,
        client_secret: str,
        base_url: str = "https://api.smartblocks.network",
        timeout: float = 10.0,
        retry: RetryConfig | None = None,
        eager: bool = True,
    ) -> Atlas:
        """Create an Atlas client from explicit credentials.

        If ``eager=True`` (default), immediately exchanges credentials
        for a token and fetches the full config. Set ``eager=False``
        to defer until first use.
        """
        inst = cls(
            client_id=client_id,
            client_secret=client_secret,
            base_url=base_url,
            timeout=timeout,
            retry=retry,
        )
        if eager:
            inst._ensure_authenticated()
            inst._ensure_config()
        return inst

    @classmethod
    def from_env(
        cls,
        *,
        base_url: str = "https://api.smartblocks.network",
        timeout: float = 10.0,
        retry: RetryConfig | None = None,
        eager: bool = True,
    ) -> Atlas:
        """Create an Atlas client from ``ATLAS_CLIENT_ID`` and ``ATLAS_CLIENT_SECRET`` env vars."""
        client_id = os.environ.get("ATLAS_CLIENT_ID", "")
        client_secret = os.environ.get("ATLAS_CLIENT_SECRET", "")
        if not client_id or not client_secret:
            raise ValueError(
                "ATLAS_CLIENT_ID and ATLAS_CLIENT_SECRET must be set in environment"
            )
        return cls.from_credentials(
            client_id=client_id,
            client_secret=client_secret,
            base_url=base_url,
            timeout=timeout,
            retry=retry,
            eager=eager,
        )

    # ── Token management ──────────────────────────────────────────────

    def _ensure_authenticated(self) -> None:
        """Exchange credentials for a token if expired or missing."""
        now = time.time()
        if self._token and now < self._token_expires_at - 30:
            return  # Still valid (with 30s buffer)

        resp = self._atlas_client.exchange_token(
            client_id=self._client_id,
            client_secret=self._client_secret,
        )
        self._token = resp["access_token"]
        self._app_id = resp.get("app_id")
        expires_in = resp.get("expires_in", 3600)
        self._token_expires_at = now + expires_in

        # Update transport auth so subsequent requests use the token
        self._auth.bearer_token = self._token
        self._auth.api_key = None

    def _ensure_config(self) -> None:
        """Fetch env vars / config if not yet loaded."""
        if self._config is not None:
            return
        self._ensure_authenticated()
        if not self._app_id:
            raise SbnError("no app_id from token exchange", status_code=0)
        data = self._atlas_client.get_env(self._app_id)
        if isinstance(data, dict):
            self._config = AtlasConfig.from_dict(data)

    # ── Properties (auto-resolve on access) ───────────────────────────

    @property
    def config(self) -> AtlasConfig:
        self._ensure_config()
        assert self._config is not None
        return self._config

    @property
    def app_id(self) -> str:
        return self.config.app_id

    @property
    def bindings(self) -> list[AtlasBinding]:
        return self.config.bindings

    @property
    def env_vars(self) -> dict[str, str]:
        return self.config.env_vars

    @property
    def scoring_frontier(self) -> str | None:
        """The canonical_path of the first bound scoring frontier."""
        if self.bindings:
            return self.bindings[0].frontier
        return None

    @property
    def regime(self) -> str:
        """The regime of the first bound frontier."""
        if self.bindings:
            return self.bindings[0].regime
        return "unknown"

    @property
    def c_max(self) -> float | None:
        if self.bindings:
            return self.bindings[0].c_max
        return None

    @property
    def metallic_label(self) -> str | None:
        if self.bindings:
            return self.bindings[0].metallic_label
        return None

    # ── Delegation to AtlasClient ─────────────────────────────────────

    def resolve(
        self,
        *,
        workflow: str | None = None,
        event_type: str | None = None,
        frontier_hint: str | None = None,
        context: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Resolve a frontier at runtime (auto-refreshes token if needed)."""
        self._ensure_authenticated()
        return self._atlas_client.resolve(
            workflow=workflow,
            event_type=event_type,
            frontier_hint=frontier_hint,
            context=context,
        )

    def list_frontiers(self, **kwargs: Any) -> dict[str, Any]:
        self._ensure_authenticated()
        return self._atlas_client.list_frontiers(**kwargs)

    def get_env(self, format: str = "json") -> dict[str, Any] | str:
        """Fetch latest env vars (bypasses cache)."""
        self._ensure_authenticated()
        assert self._app_id
        return self._atlas_client.get_env(self._app_id, format=format)

    def dotenv(self) -> str:
        """Get env vars in .env format (KEY=VALUE lines)."""
        result = self.get_env(format="dotenv")
        return result if isinstance(result, str) else ""

    # ── Context manager ───────────────────────────────────────────────

    def __enter__(self) -> Atlas:
        return self

    def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
        self.close()

    def close(self) -> None:
        self._transport.close()

    def __repr__(self) -> str:
        frontier = self.scoring_frontier if self._config else "?"
        return f"<Atlas client_id={self._client_id!r} frontier={frontier!r}>"
