"""Internal provider-capacity types. Not a generated-app capability."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


class ProviderCapacityUnavailableError(Exception):
    """Bounded capacity wait expired. Distinct from provider 429, timeout, and abuse admission."""

    code = "PROVIDER_CAPACITY_UNAVAILABLE"

    def __init__(self, message: str = "Provider capacity is unavailable. Please retry shortly."):
        self.message = message
        super().__init__(message)


@dataclass(frozen=True)
class CapacityBucket:
    provider: str
    account_scope: str
    model: str
    dimension: str

    @property
    def id(self) -> str:
        return f"{self.provider}:{self.account_scope}:{self.model}:{self.dimension}"


@dataclass(frozen=True)
class CapacityPolicy:
    """Admission policy knobs. Values come from configuration, not provider evidence."""

    safety_fraction: float
    wait_seconds: float
    unknown_max_in_flight: int
    stale_observation_seconds: float
    poll_seconds: float = 0.1


@dataclass(frozen=True)
class CapacityObservation:
    dimension: str
    limit: int | None = None
    remaining: int | None = None
    reset_in_seconds: float | None = None
    observed_at_ms: int = 0
    source: str = "provider"


@dataclass
class BucketSnapshot:
    reserved: int = 0
    in_flight: int = 0
    observed_limit: int | None = None
    observed_remaining: int | None = None
    reset_at_ms: int | None = None
    observed_at_ms: int | None = None
    waiters: list[tuple[str, int, int]] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "reserved": self.reserved,
            "in_flight": self.in_flight,
            "observed_limit": self.observed_limit,
            "observed_remaining": self.observed_remaining,
            "reset_at_ms": self.reset_at_ms,
            "observed_at_ms": self.observed_at_ms,
            "waiters": [list(item) for item in self.waiters],
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any] | None) -> BucketSnapshot:
        if not data:
            return cls()
        waiters = []
        for item in data.get("waiters") or []:
            waiters.append((str(item[0]), int(item[1]), int(item[2])))
        return cls(
            reserved=int(data.get("reserved") or 0),
            in_flight=int(data.get("in_flight") or 0),
            observed_limit=_optional_int(data.get("observed_limit")),
            observed_remaining=_optional_int(data.get("observed_remaining")),
            reset_at_ms=_optional_int(data.get("reset_at_ms")),
            observed_at_ms=_optional_int(data.get("observed_at_ms")),
            waiters=waiters,
        )


@dataclass(frozen=True)
class ReserveDecision:
    granted: bool
    queued: bool
    available: int
    safe_capacity: int | None
    mode: str
    queue_depth: int
    observed_limit: int | None
    observed_remaining: int | None


@dataclass(frozen=True)
class CapacityReservation:
    bucket: CapacityBucket
    waiter_id: str
    demand: int
    granted_at_monotonic: float
    wait_ms: int
    decision: ReserveDecision
    used_fallback: bool = False


def _optional_int(value: Any) -> int | None:
    if value is None or value == "":
        return None
    return int(value)
