"""Async HTTP client for the Sibilance API.

Wraps httpx with auth token injection, silent refresh, and typed responses.
"""

from __future__ import annotations

import types
from typing import Any, TypeVar, Union, get_type_hints

import httpx

from sibilance.auth.credentials import (
    Credentials,
    read_credentials,
    write_credentials,
)
from sibilance.api.types import (
    AgentListItem,
    AuthRequirements,
    BondImpact,
    BondWithLevel,
    CareHistoryEntry,
    CheckoutResponse,
    CheckoutStatus,
    CreatureCountResponse,
    CreatureLog,
    CreatureStatus,
    Den,
    DenResponse,
    DeviceAuthStatus,
    DeviceCodeResponse,
    Emotion,
    EmotionBrief,
    EmotionalStateBrief,
    FavorBalance,
    FeedResponse,
    Friend,
    FriendsResponse,
    GiftResponse,
    GuidanceResponse,
    HatchResponse,
    JournalEntry,
    JournalResponse,
    LookResponse,
    NameResponse,
    Nature,
    NatureBrief,
    ObserverProfile,
    PersonalityBrief,
    PortalResponse,
    RecentEvent,
    SleepSchedule,
    SleepScheduleUpdate,
    SubscriptionStatus,
    TalkResponse,
    WorldState,
)


T = TypeVar("T")

def _from_dict(cls: type[T], data: dict[str, Any] | None) -> T | None:
    """Recursively deserialize a camelCase JSON dict into a frozen dataclass."""
    if data is None:
        return None

    hints = get_type_hints(cls)
    kwargs: dict[str, Any] = {}

    for field_name, field_type in hints.items():
        # Try snake_case field name as camelCase key first, then exact
        camel_key = _to_camel(field_name)
        raw = data.get(camel_key, data.get(field_name))

        if raw is None:
            # Use default if available
            continue

        kwargs[field_name] = _coerce(field_type, raw)

    return cls(**kwargs)


def _to_camel(name: str) -> str:
    parts = name.split("_")
    return parts[0] + "".join(p.capitalize() for p in parts[1:])


def _is_union(tp: Any) -> bool:
    """Check if a type is a Union (including X | None syntax)."""
    origin = getattr(tp, "__origin__", None)
    # typing.Union (e.g., Optional[X])
    if origin is Union:
        return True
    # Python 3.10+ union syntax (X | None) produces types.UnionType
    if isinstance(tp, types.UnionType):
        return True
    return False


def _coerce(field_type: Any, value: Any) -> Any:
    """Coerce a JSON value to the target type, handling nested dataclasses and lists."""
    origin = getattr(field_type, "__origin__", None)

    # list[X]
    if origin is list:
        item_type = field_type.__args__[0]
        if isinstance(value, list):
            return [_coerce(item_type, v) for v in value]
        return value

    # X | None (Optional) — handles both typing.Optional and PEP 604
    if _is_union(field_type):
        args = [a for a in field_type.__args__ if a is not type(None)]
        if len(args) == 1:
            if value is None:
                return None
            return _coerce(args[0], value)
        return value

    # Frozen dataclass
    if hasattr(field_type, "__dataclass_fields__") and isinstance(value, dict):
        return _from_dict(field_type, value)

    return value


class AuthError(Exception):
    """Raised when authentication is required but missing or invalid."""

    pass


def _build_ssl_context() -> object | None:
    """Try to use the OS trust store for TLS verification.

    On macOS this uses Keychain, on Windows the system store, on Linux
    the distro CA bundle — matching what browsers use.  Falls back to
    httpx defaults (certifi) if truststore is not installed.
    """
    try:
        import ssl

        import truststore

        ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        return ctx
    except ImportError:
        return None


class ApiClient:
    """Async HTTP client for the Sibilance server API."""

    def __init__(self, server_url: str) -> None:
        self._server_url = server_url.rstrip("/")
        ssl_ctx = _build_ssl_context()
        transport = httpx.AsyncHTTPTransport(retries=1)
        kwargs: dict[str, object] = {
            "base_url": self._server_url,
            "timeout": httpx.Timeout(30.0, connect=10.0),
            "transport": transport,
        }
        if ssl_ctx is not None:
            kwargs["verify"] = ssl_ctx
        self._http = httpx.AsyncClient(**kwargs)  # type: ignore[arg-type]

    async def close(self) -> None:
        await self._http.aclose()

    # ── Core request method ──────────────────────────────────────────────

    async def _request(
        self,
        method: str,
        path: str,
        *,
        json_body: dict[str, Any] | None = None,
        authenticated: bool = True,
    ) -> httpx.Response:
        """Make an HTTP request with optional auth token."""
        headers: dict[str, str] = {}

        if authenticated:
            creds = read_credentials()
            if creds and creds.token:
                headers["Authorization"] = f"Bearer {creds.token}"

        response = await self._http.request(
            method,
            path,
            json=json_body,
            headers=headers,
        )

        if response.status_code == 401 and authenticated:
            raise AuthError(
                "Session expired. Run `creature login` to authenticate again."
            )

        return response

    async def _get(self, path: str, *, authenticated: bool = True) -> httpx.Response:
        return await self._request("GET", path, authenticated=authenticated)

    async def _post(
        self,
        path: str,
        *,
        json_body: dict[str, Any] | None = None,
        authenticated: bool = True,
    ) -> httpx.Response:
        return await self._request(
            "POST", path, json_body=json_body, authenticated=authenticated
        )

    def _raise_for_error(self, response: httpx.Response) -> None:
        if not response.is_success:
            try:
                body = response.json()
                msg = body.get("error", f"Server returned {response.status_code}")
            except Exception:
                msg = f"Server returned {response.status_code}"
            raise RuntimeError(msg)

    # ── Token Refresh ────────────────────────────────────────────────────

    async def try_refresh(self) -> bool:
        """Attempt silent token refresh. Returns True if token is still valid."""
        creds = read_credentials()
        if creds is None:
            return False

        try:
            response = await self._http.post(
                "/auth/refresh",
                headers={
                    "Authorization": f"Bearer {creds.token}",
                    "Content-Type": "application/json",
                },
            )

            if response.status_code == 204:
                return True

            if response.is_success:
                result = response.json()
                write_credentials(
                    Credentials(
                        token=result["token"],
                        observer_id=creds.observer_id,
                        display_name=creds.display_name,
                        expires_at=result.get("expiresAt", ""),
                    )
                )
                return True

            return False
        except httpx.HTTPError:
            return False

    # ── Auth Check ───────────────────────────────────────────────────────

    async def check_auth_requirements(self) -> AuthRequirements:
        response = await self._get("/auth/check", authenticated=False)
        if not response.is_success:
            return AuthRequirements(requires_access_code=False)
        data = response.json()
        return AuthRequirements(
            requires_access_code=data.get("requiresAccessCode", False)
        )

    # ── Device Code Auth ─────────────────────────────────────────────────

    async def create_device_code(self) -> DeviceCodeResponse:
        response = await self._post("/auth/device", authenticated=False)
        self._raise_for_error(response)
        d = response.json()
        return DeviceCodeResponse(
            code=d["code"],
            device_id=d["deviceId"],
            verify_url=d["verifyUrl"],
            expires_at=d["expiresAt"],
            expires_in=d["expiresIn"],
        )

    async def poll_device_status(self, device_id: str) -> DeviceAuthStatus:
        response = await self._get(
            f"/auth/device/status?deviceId={device_id}", authenticated=False
        )
        self._raise_for_error(response)
        d = response.json()
        return DeviceAuthStatus(
            status=d["status"],
            token=d.get("token"),
            observer_id=d.get("observerId"),
            display_name=d.get("displayName"),
        )

    async def get_profile(self) -> ObserverProfile:
        response = await self._get("/auth/me")
        self._raise_for_error(response)
        d = response.json()
        return _from_dict(ObserverProfile, d)  # type: ignore[return-value]

    async def logout(self) -> None:
        try:
            await self._post("/auth/logout")
        except Exception:
            pass

    # ── Billing ──────────────────────────────────────────────────────────

    async def create_checkout(self) -> CheckoutResponse:
        response = await self._post("/api/billing/checkout")
        self._raise_for_error(response)
        return CheckoutResponse(url=response.json()["url"])

    async def poll_checkout_status(self) -> CheckoutStatus:
        response = await self._get("/api/billing/checkout/status")
        self._raise_for_error(response)
        d = response.json()
        return CheckoutStatus(
            status=d["status"],
            subscription_status=d.get("subscriptionStatus"),
            creature_count=d.get("creatureCount"),
        )

    async def get_subscription_status(self) -> SubscriptionStatus:
        response = await self._get("/api/billing/status")
        self._raise_for_error(response)
        d = response.json()
        return _from_dict(SubscriptionStatus, d)  # type: ignore[return-value]

    async def release_creature(self, agent_id: str) -> CreatureCountResponse:
        response = await self._post(
            "/api/billing/creatures/release",
            json_body={"agentId": agent_id},
        )
        self._raise_for_error(response)
        d = response.json()
        return CreatureCountResponse(
            success=d["success"], creature_count=d["creatureCount"]
        )

    async def add_creature(self, agent_id: str) -> CreatureCountResponse:
        response = await self._post(
            "/api/billing/creatures/add",
            json_body={"agentId": agent_id},
        )
        self._raise_for_error(response)
        d = response.json()
        return CreatureCountResponse(
            success=d["success"], creature_count=d["creatureCount"]
        )

    async def create_portal_session(self) -> PortalResponse:
        response = await self._post("/api/billing/portal")
        self._raise_for_error(response)
        return PortalResponse(url=response.json()["url"])

    async def list_agents(self) -> list[AgentListItem]:
        response = await self._get("/api/agents")
        self._raise_for_error(response)
        return [
            AgentListItem(
                id=a["id"],
                name=a["name"],
                is_dormant=a.get("isDormant"),
            )
            for a in response.json()
        ]

    # ── World ──────────────────────────────────────────────────────────

    async def get_world_state(self) -> WorldState:
        response = await self._get("/api/world")
        self._raise_for_error(response)
        d = response.json()
        return _from_dict(WorldState, d)  # type: ignore[return-value]

    # ── Creature ─────────────────────────────────────────────────────────

    async def hatch_creature(self) -> HatchResponse:
        response = await self._post("/api/creature/hatch")
        self._raise_for_error(response)
        d = response.json()
        return HatchResponse(
            agent_id=d["agentId"],
            temp_name=d["tempName"],
            starting_location=d["startingLocation"],
            nature=_from_dict(Nature, d["nature"]),  # type: ignore[arg-type]
            personality=_from_dict(PersonalityBrief, d.get("personality")),
            emotional_state=_from_dict(EmotionalStateBrief, d.get("emotionalState")),
        )

    async def name_creature(self, agent_id: str, name: str) -> NameResponse:
        response = await self._post(
            "/api/creature/name",
            json_body={"agentId": agent_id, "name": name},
        )
        self._raise_for_error(response)
        d = response.json()
        return NameResponse(
            success=d["success"], name=d["name"], agent_id=d["agentId"]
        )

    async def get_my_creatures(self) -> list[CreatureStatus]:
        response = await self._get("/api/creature/mine")
        self._raise_for_error(response)
        return [_from_dict(CreatureStatus, c) for c in response.json()]  # type: ignore[misc]

    async def guide_creature(self, message: str) -> GuidanceResponse:
        response = await self._post(
            "/api/creature/guide", json_body={"message": message}
        )
        self._raise_for_error(response)
        d = response.json()
        return GuidanceResponse(
            success=d["success"],
            guidance_type=d["guidanceType"],
            response=d["response"],
        )

    async def talk_to_creature(self, message: str) -> TalkResponse:
        response = await self._post(
            "/api/creature/talk", json_body={"message": message}
        )
        self._raise_for_error(response)
        d = response.json()
        return TalkResponse(
            response=d["response"],
            creature_name=d["creatureName"],
            emotion=_from_dict(EmotionBrief, d.get("emotion")),
            bond_impact=_from_dict(BondImpact, d.get("bondImpact")),
            care_quality=d.get("careQuality", 0.0),
        )

    async def feed_creature(self) -> FeedResponse:
        response = await self._post("/api/creature/feed")
        self._raise_for_error(response)
        d = response.json()
        return FeedResponse(
            reaction=d["reaction"],
            creature_name=d["creatureName"],
            emotion=_from_dict(EmotionBrief, d.get("emotion")),
            bond_impact=_from_dict(BondImpact, d.get("bondImpact")),
            care_quality=d.get("careQuality", 0.0),
        )

    async def look_at_creature(self) -> LookResponse:
        response = await self._get("/api/creature/look")
        self._raise_for_error(response)
        d = response.json()
        return LookResponse(
            creature_name=d["creatureName"],
            life_stage=d["lifeStage"],
            location=d.get("location"),
            emotion=_from_dict(Emotion, d.get("emotion")),
            bond=_from_dict(BondWithLevel, d.get("bond")),
            recent_events=[
                _from_dict(RecentEvent, e) for e in d.get("recentEvents", [])  # type: ignore[misc]
            ],
            absence_hours=d.get("absenceHours", 0.0),
            nature=_from_dict(NatureBrief, d.get("nature")),
        )

    async def get_creature_log(self) -> CreatureLog:
        response = await self._get("/api/creature/log")
        self._raise_for_error(response)
        d = response.json()
        return CreatureLog(
            creature_name=d["creatureName"],
            events=[
                _from_dict(RecentEvent, e) for e in d.get("events", [])  # type: ignore[misc]
            ],
            care_history=[
                _from_dict(CareHistoryEntry, e) for e in d.get("careHistory", [])  # type: ignore[misc]
            ],
            bond_level=d.get("bondLevel", ""),
        )

    async def get_creature_friends(self) -> FriendsResponse:
        response = await self._get("/api/creature/friends")
        self._raise_for_error(response)
        d = response.json()
        return FriendsResponse(
            friends=[
                _from_dict(Friend, f) for f in d.get("friends", [])  # type: ignore[misc]
            ],
            favors=[
                _from_dict(FavorBalance, f) for f in d.get("favors", [])  # type: ignore[misc]
            ],
        )

    async def get_creature_journal(self) -> JournalResponse:
        response = await self._get("/api/creature/journal")
        self._raise_for_error(response)
        d = response.json()
        return JournalResponse(
            entries=[
                _from_dict(JournalEntry, e) for e in d.get("entries", [])  # type: ignore[misc]
            ],
            trust_level=d.get("trustLevel", 0.0),
        )

    async def get_creature_den(self) -> DenResponse:
        response = await self._get("/api/creature/den")
        self._raise_for_error(response)
        d = response.json()
        return DenResponse(
            dens=[_from_dict(Den, den) for den in d.get("dens", [])]  # type: ignore[misc]
        )

    async def get_sleep_schedule(self) -> SleepSchedule:
        response = await self._get("/api/creature/sleep")
        self._raise_for_error(response)
        d = response.json()
        return SleepSchedule(
            sleep_hour=d["sleepHour"],
            wake_hour=d["wakeHour"],
            is_asleep=d["isAsleep"],
            current_world_hour=d["currentWorldHour"],
        )

    async def set_sleep_schedule(
        self, sleep_hour: float, wake_hour: float
    ) -> SleepScheduleUpdate:
        response = await self._post(
            "/api/creature/sleep",
            json_body={"sleepHour": sleep_hour, "wakeHour": wake_hour},
        )
        self._raise_for_error(response)
        d = response.json()
        return SleepScheduleUpdate(
            success=d["success"],
            sleep_hour=d["sleepHour"],
            wake_hour=d["wakeHour"],
        )

    async def gift_creature(self, description: str) -> GiftResponse:
        response = await self._post(
            "/api/creature/gift", json_body={"description": description}
        )
        self._raise_for_error(response)
        d = response.json()
        return GiftResponse(
            success=d["success"],
            response=d["response"],
            bond_impact=_from_dict(BondImpact, d.get("bondImpact")),
        )
