"""Connection-profile selection — Python reference implementation.

Single source of truth for "magnetic API snapping" on the Python side
(Erlich / Solvulator runtime). The Babashka proxy mirrors this logic in
``select_profile.bb``; the two MUST stay semantically identical.

Core rule
---------
A profile is *snappable* iff it is **tested**, not merely **available**:

    eligible(p)  <=>  p.state.available AND p.state.tested

Ranking of eligible profiles is deterministic:

    sort key = (priority ASC, latency_ms ASC, id ASC)

with ``latency_ms is None`` sorting last (it cannot rank ahead of a
measured latency). If nothing is eligible we never fail silently: we
surface the best *candidate* with an honest, machine-readable reason.

This module is dependency-free (stdlib only) and importable from any
surface. Validate inputs against ``connection-profiles.schema.json``
upstream; here we defensively coerce the ``state`` block so a missing or
partial record degrades to "offline" rather than raising.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Iterable, Optional

__all__ = [
    "OfflineReason",
    "ProfileState",
    "Profile",
    "Selection",
    "load_profiles",
    "select_best",
    "rank_eligible",
]

# A latency sentinel that always sorts last without using float('inf')
# (keeps the key orderable against real ints under a uniform tuple shape).
_LATENCY_LAST = (1, 0)


class OfflineReason(str, Enum):
    """Why a profile is not snappable. Stable strings safe for UI/telemetry."""

    READY = "ready"                       # eligible; not an offline reason
    UNREACHABLE = "unreachable"           # probe failed: available is False
    REACHABLE_UNVERIFIED = "unverified"   # available but no inference round-trip
    LEGACY_NOT_WIRED = "legacy"           # kind == 'legacy'; never auto-eligible
    NO_PROFILES = "no_profiles"           # empty input

    @property
    def display(self) -> str:
        return {
            OfflineReason.READY: "Ready",
            OfflineReason.UNREACHABLE: "Unreachable",
            OfflineReason.REACHABLE_UNVERIFIED: "Reachable, unverified",
            OfflineReason.LEGACY_NOT_WIRED: "Legacy — not wired",
            OfflineReason.NO_PROFILES: "No profiles configured",
        }[self]


@dataclass(frozen=True)
class ProfileState:
    """Runtime-written health. Owned by the probe/smoke-test loop."""

    available: bool = False
    tested: bool = False
    last_ok: Optional[str] = None
    latency_ms: Optional[int] = None

    @classmethod
    def from_dict(cls, raw: Optional[dict[str, Any]]) -> "ProfileState":
        raw = raw or {}
        latency = raw.get("latency_ms")
        if latency is not None:
            try:
                latency = int(latency)
            except (TypeError, ValueError):
                latency = None
        return cls(
            available=bool(raw.get("available", False)),
            tested=bool(raw.get("tested", False)),
            last_ok=raw.get("last_ok"),
            latency_ms=latency,
        )


@dataclass(frozen=True)
class Profile:
    """One connection target. Mirrors connection-profiles.schema.json."""

    id: str
    label: str
    kind: str  # "endpoint" | "gateway" | "legacy"
    target: str = ""
    route: str = ""
    models: Any = "auto"
    tags: tuple[str, ...] = field(default_factory=tuple)
    priority: int = 100
    summary: str = ""
    note: str = ""
    state: ProfileState = field(default_factory=ProfileState)

    @classmethod
    def from_dict(cls, raw: dict[str, Any]) -> "Profile":
        try:
            pid = raw["id"]
            label = raw["label"]
            kind = raw["kind"]
        except KeyError as exc:  # fail loud on structural errors, not state ones
            raise ValueError(f"profile missing required field: {exc.args[0]}") from exc
        return cls(
            id=pid,
            label=label,
            kind=kind,
            target=raw.get("target", ""),
            route=raw.get("route", ""),
            models=raw.get("models", "auto"),
            tags=tuple(raw.get("tags", []) or ()),
            priority=int(raw.get("priority", 100)),
            summary=raw.get("summary", ""),
            note=raw.get("note", ""),
            state=ProfileState.from_dict(raw.get("state")),
        )

    # --- predicates -------------------------------------------------------

    def is_eligible(self) -> bool:
        """Snappable iff reachable AND a real inference round-trip succeeded."""
        return self.kind != "legacy" and self.state.available and self.state.tested

    def offline_reason(self) -> OfflineReason:
        """Machine-readable explanation; READY when eligible."""
        if self.kind == "legacy":
            return OfflineReason.LEGACY_NOT_WIRED
        if not self.state.available:
            return OfflineReason.UNREACHABLE
        if not self.state.tested:
            return OfflineReason.REACHABLE_UNVERIFIED
        return OfflineReason.READY

    # --- ordering ---------------------------------------------------------

    def _sort_key(self) -> tuple[int, tuple[int, int], str]:
        if self.state.latency_ms is None:
            latency_key = _LATENCY_LAST
        else:
            latency_key = (0, self.state.latency_ms)
        return (self.priority, latency_key, self.id)


@dataclass(frozen=True)
class Selection:
    """Result of a snap attempt.

    ``profile`` is the chosen target. ``ready`` distinguishes a true snap
    (an eligible profile) from a best-effort fallback surfaced for honesty.
    ``reason`` explains the state regardless of outcome.
    """

    profile: Optional[Profile]
    ready: bool
    reason: OfflineReason

    @property
    def display(self) -> str:
        if self.profile is None:
            return self.reason.display
        return f"{self.profile.label} — {self.reason.display}"


def load_profiles(doc: dict[str, Any]) -> list[Profile]:
    """Parse a schema-shaped document into Profile objects."""
    return [Profile.from_dict(p) for p in doc.get("profiles", [])]


def rank_eligible(profiles: Iterable[Profile]) -> list[Profile]:
    """Return eligible profiles in snap order (best first)."""
    return sorted((p for p in profiles if p.is_eligible()), key=Profile._sort_key)


def select_best(profiles: Iterable[Profile]) -> Selection:
    """Snap to the best ready path, or surface an honest fallback.

    1. If any profile is eligible, return the top-ranked one (ready=True).
    2. Otherwise return the highest-priority candidate with the reason it
       is not ready, so the UI can show it disabled-with-explanation.
    3. With no profiles at all, return an empty NO_PROFILES selection.
    """
    profiles = list(profiles)
    if not profiles:
        return Selection(profile=None, ready=False, reason=OfflineReason.NO_PROFILES)

    eligible = rank_eligible(profiles)
    if eligible:
        best = eligible[0]
        return Selection(profile=best, ready=True, reason=OfflineReason.READY)

    # Honest fallback: surface the most-preferred profile and why it's blocked.
    fallback = sorted(profiles, key=Profile._sort_key)[0]
    return Selection(profile=fallback, ready=False, reason=fallback.offline_reason())


if __name__ == "__main__":
    import json
    import pathlib
    import sys

    path = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else pathlib.Path("profiles.example.json")
    doc = json.loads(path.read_text())
    profs = load_profiles(doc)

    print("rank (eligible, best first):")
    for p in rank_eligible(profs):
        print(f"  {p.priority:>4}  {p.state.latency_ms!s:>5}ms  {p.id}")

    sel = select_best(profs)
    print("\nselected:", sel.display, f"(ready={sel.ready})")

    print("\nall profiles with reasons:")
    for p in profs:
        print(f"  {p.id:<22} {p.offline_reason().value}")
