"""SSE consumer for the Worldtree Conversation API.

Implements docs/contracts/issues/1.contract.md.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, NamedTuple


class SseId(NamedTuple):
    """Parsed composite SSE wire `id:` per spec §SSE id format."""

    turn_id: int
    seq: int




@dataclass(frozen=True)
class AdminEvent:
    """One `/admin/events` envelope (INV-046) — an admin-tier lifecycle event.

    Distinct from the turn-stream `Event` union: this is the process-wide admin
    broadcast stream, not a per-turn stream. `id` is a plain monotonic int
    (resets on restart; heartbeats have id=0). `type` is a dotted namespace
    (session.* / turn.* / key.* / system.*). `data` is a type-specific dict —
    most carry `session_id`; per INV-049 it holds IDs + small metadata only.
    """

    id: int
    type: str
    timestamp: str | None
    data: dict[str, Any]


class MalformedSseId(Exception):
    """Raised when an SSE event's `id:` wire field is missing or non-composite."""

    def __init__(self, raw: str) -> None:
        super().__init__(f"malformed SSE id: {raw[:64]!r}")
        self.raw = raw[:64]


class MalformedSseData(Exception):
    """Raised when an SSE event's `data:` payload is non-empty but fails JSON parsing.

    Sibling to MalformedSseId — both are wire-level protocol violations from the
    server. Empty-data frames are skipped silently per issue #7 INV-001; this
    exception is for non-empty payloads that fail json.loads.
    """

    def __init__(self, raw: str) -> None:
        super().__init__(f"malformed SSE data: {raw[:200]!r}")
        self.raw = raw[:200]


class TurnIdFlip(Exception):
    """Raised when an SSE event's turn_id doesn't match the established/expected turn."""

    def __init__(self, *, established: int, got: int) -> None:
        super().__init__(f"turn_id flip: established={established}, got={got}")
        self.established = established
        self.got = got


class SseConnectFailed(Exception):
    """Raised when the SSE endpoint returned a non-2xx status before the stream opened."""

    def __init__(self, *, status: int, body: bytes) -> None:
        body = body[:1024]
        super().__init__(f"SSE connect failed: status={status}, body={body[:128]!r}")
        self.status = status
        self.body = body


class AgentNotAvailable(SseConnectFailed):
    """Eager 409 from the turn POST (Worldtree v1.0.0b1, #331): the session's
    agent is unavailable, so the turn never launched. Pre-b1 this arrived as a
    200 stream + an in-stream `error` event; b1 surfaces it eagerly. Subclass of
    SseConnectFailed so existing `except SseConnectFailed` handlers still catch
    it — this type just adds the parsed `error_code` + `message`."""

    def __init__(self, *, body: bytes, error_code: str, message: str) -> None:
        super().__init__(status=409, body=body)
        self.error_code = error_code
        self.message = message


class TurnLaunchUnavailable(SseConnectFailed):
    """Eager 503 from the turn POST (Worldtree v1.0.0b1, #331): a transient
    turn-launch failure (loop shutdown / resource exhaustion). RETRYABLE.
    Subclass of SseConnectFailed; adds `error_code`, `message`, `retryable`."""

    retryable = True

    def __init__(self, *, body: bytes, error_code: str, message: str) -> None:
        super().__init__(status=503, body=body)
        self.error_code = error_code
        self.message = message




class SseConnectionDropped(Exception):
    """Raised when the HTTP/SSE connection dropped mid-stream.

    `last_seen_sse_id` is the resume cursor of the last frame seen. The
    hand-rolled path carries a parsed `SseId`; the worldtree-sdk cutover carries
    the SDK's raw composite-id `str` (the cutover's target form) — both accepted
    during the migration.
    """

    def __init__(self, *, last_seen_sse_id: SseId | str | None) -> None:
        super().__init__(f"SSE connection dropped; last_seen_sse_id={last_seen_sse_id}")
        self.last_seen_sse_id = last_seen_sse_id


class InvalidLastEventId(Exception):
    """Raised on HTTP 400 from a reconnect request — caller's last_event_id was rejected."""

    def __init__(self, *, raw: str) -> None:
        super().__init__(f"server rejected Last-Event-ID: {raw!r}")
        self.raw = raw


class ResumeTurnFinished(Exception):
    """Raised on HTTP 410 from a reconnect — turn finished while disconnected."""

    def __init__(self, *, turn_id: int) -> None:
        super().__init__(f"resume target finished: turn_id={turn_id}")
        self.turn_id = turn_id


class ResumeBufferExpired(Exception):
    """Raised on HTTP 412 from a reconnect — Last-Event-ID older than replay buffer."""

    def __init__(self, *, turn_id: int, buffered_from_seq: int) -> None:
        super().__init__(
            f"replay buffer expired: turn_id={turn_id}, buffered_from_seq={buffered_from_seq}"
        )
        self.turn_id = turn_id
        self.buffered_from_seq = buffered_from_seq


class CancelTurnNotFound(Exception):
    """Raised on HTTP 404 from cancel — turn finished before cancel arrived (race)."""

    def __init__(self, *, turn_id: int) -> None:
        super().__init__(f"cancel target not found: turn_id={turn_id}")
        self.turn_id = turn_id


class CancelAlreadyCompleted(Exception):
    """Raised on HTTP 409 from cancel — turn already terminated server-side (race)."""

    def __init__(self, *, turn_id: int) -> None:
        super().__init__(f"cancel: turn already completed: turn_id={turn_id}")
        self.turn_id = turn_id


class CancelFailed(Exception):
    """Raised on unexpected cancel response status."""

    def __init__(self, *, status: int, body: bytes) -> None:
        body = body[:1024]
        super().__init__(f"cancel failed: status={status}, body={body[:128]!r}")
        self.status = status
        self.body = body
