"""Hosted Group Chat rooms (``groups.*``), cross-connection bot relay (``bot_relay.*``) and the
dashboard browser controller (``browser.controller.*``).

Handlers: ``tui_gateway/methods_groups.py``, ``tui_gateway/methods_bot_relay.py``,
``tui_gateway/methods_browser_control.py``. Room / event / page shapes are produced by
``gateway/hosted_rooms.py`` (``_room_from_row`` / ``_event_from_row`` / ``read_events``) and
``gateway/hosted_room_replicas.py``; the RoomLink catalog by ``gateway/hosted_room_peer.py``.
"""

from __future__ import annotations

from .base import JsonValue, Params, Result, WireEnum
from .common import OkResult, OpenModel, ProfileParams
from .registry import method
from .server_requests import ApprovalChoice

# ── shared room shapes ────────────────────────────────────────────────────────────────────────


class RoomMember(OpenModel):
    """One roster row (``hosted_room_discussion.validate_roster``); legacy rooms may carry
    pre-normalisation rows, so the set stays open."""

    member_id: str | None = None
    profile: str | None = None
    handle: str | None = None
    display_name: str | None = None
    target: dict[str, JsonValue] | None = None


class RoomActor(Result):
    kind: str
    id: str


class RoomEvent(Result):
    """``gateway/hosted_rooms.py::_event_from_row``."""

    room_id: str
    seq: int
    event_id: str
    kind: str
    actor: RoomActor
    authority_epoch: int | None = None
    payload: dict[str, JsonValue]
    created_at: float
    idempotent: bool = False


class Room(Result):
    """``gateway/hosted_rooms.py::_room_from_row`` plus the branch-only keys ``create`` (legacy
    adoption), ``state`` (``authority_claim``) and ``rename`` (``event``) add."""

    room_id: str
    name: str
    members: list[RoomMember]
    authority_gateway_id: str
    authority_epoch: int
    revision: int
    created_at: float
    updated_at: float
    idempotent: bool = False
    disbanded_at: float | None = None
    latest_seq: int | None = None
    adopted: bool | None = None
    claim_event: RoomEvent | None = None
    authority_claim: RoomEvent | None = None
    event: RoomEvent | None = None


class RoomAuthority(Result):
    gateway_id: str
    epoch: int


class RoomMemberInput(Params):
    """A roster row as the client proposes it; ``validate_roster`` owns the exact rules."""

    member_id: str | None = None
    profile: str | None = None
    handle: str | None = None
    display_name: str | None = None
    target: dict[str, JsonValue] | None = None
    model_config = Params.model_config | {"extra": "allow"}


class RoomParams(ProfileParams):
    """Any method addressed at one hosted room."""

    room_id: str


# ── RoomLink catalog ──────────────────────────────────────────────────────────────────────────


class RoomExecutionPolicy(Result):
    """``gateway/hosted_room_execution_policy.py::execution_policy_mapping``."""

    version: int
    target_profile: str
    enabled_toolsets: list[str]
    approval_mode: str
    max_iterations: int
    policy_digest: str


class RoomLinkEndpoint(Result):
    """``GatewayRoomCatalog.endpoint_mapping``: ``url``/``transport_security`` when available,
    ``reason`` when not."""

    available: bool
    url: str | None = None
    transport_security: str | None = None
    reason: str | None = None


class RoomLinkCatalog(Result):
    """``gateway/hosted_room_peer.py::GatewayRoomCatalog.as_mapping``."""

    installation_id: str
    protocol_versions: list[int]
    link_modes: list[str]
    persistent_process: bool
    text: bool
    attachments: bool
    execution_policy: RoomExecutionPolicy
    catalog_digest: str
    endpoint: RoomLinkEndpoint | None = None


class RoomLinkStatus(Result):
    """``enabled`` with ``profile``/``catalog``/``endpoint``, or disabled with a ``reason``."""

    enabled: bool
    profile: str | None = None
    catalog: RoomLinkCatalog | None = None
    endpoint: RoomLinkEndpoint | None = None
    reason: str | None = None


# ── groups.capabilities ───────────────────────────────────────────────────────────────────────


class GroupsCapabilitiesParams(ProfileParams):
    pass


class GroupsCapabilitiesResult(Result):
    protocol_version: int
    driver: bool
    persistent_process: bool
    authority_gateway_id: str
    room_link: RoomLinkStatus
    features: list[str]
    methods: list[str]
    max_log_limit: int


method("groups.capabilities", params=GroupsCapabilitiesParams, result=GroupsCapabilitiesResult,
       doc="Describe the hosted-room protocol implemented by this gateway.")


# ── groups.list / create / state ──────────────────────────────────────────────────────────────


class GroupsListParams(ProfileParams):
    include_disbanded: bool | None = None
    limit: int | None = None
    offset: int | None = None


class GroupsListResult(Result):
    rooms: list[Room]
    next_offset: int | None = None


method("groups.list", params=GroupsListParams, result=GroupsListResult,
       doc="List rooms hosted by this gateway, most recently changed first.")


class GroupsCreateParams(ProfileParams):
    room_id: str
    name: str
    members: list[RoomMemberInput]
    # Ignored: authority is always this gateway's install identity (a client cannot spoof it).
    authority_gateway_id: str | None = None


class GroupsCreateResult(Result):
    room: Room


method("groups.create", params=GroupsCreateParams, result=GroupsCreateResult,
       doc="Create a hosted room idempotently; authority is this gateway's stable install identity.")


class GroupsStateParams(RoomParams):
    include_disbanded: bool | None = None


class PeerRouteStatus(Result):
    room_id: str
    member_id: str
    status: str


class RoomDriverStatus(Result):
    """``HostedRoomService.status(room_id)``; ``pending_actions`` rows are ``{kind: retry, task_id}``
    or the driver's approval action (``kind: approval`` + run/session/approval context)."""

    running: bool
    working: bool
    blocked: bool
    counts: dict[str, int]
    pending_actions: list[dict[str, JsonValue]]
    peer_routes: list[PeerRouteStatus]


class GroupsStateResult(Result):
    room: Room
    driver_status: RoomDriverStatus | None = None


method("groups.state", params=GroupsStateParams, result=GroupsStateResult,
       doc="One hosted room's replay cursor and fenced authority state, plus live driver status.")


# ── groups.send / rename / log ────────────────────────────────────────────────────────────────


class GroupsSendParams(RoomParams):
    event_id: str | None = None
    payload: dict[str, JsonValue]


class GroupsSendResult(Result):
    event: RoomEvent
    client_event_id: str | None = None
    accepted: bool = True
    driver_started: bool = True


method("groups.send", params=GroupsSendParams, result=GroupsSendResult,
       doc="Append one inert message.user event idempotently; the actor is server-owned.")


class GroupsRenameParams(RoomParams):
    event_id: str
    name: str


class GroupsRenameResult(Result):
    room: Room


method("groups.rename", params=GroupsRenameParams, result=GroupsRenameResult,
       doc="Rename one hosted room atomically with its replay event.")


class GroupsLogParams(RoomParams):
    since_seq: int | None = None
    limit: int | None = None
    include_disbanded: bool | None = None


class GroupsLogResult(Result):
    """``gateway/hosted_rooms.py::read_events`` page — also the ``page`` ``groups.replicate`` ingests."""

    events: list[RoomEvent]
    cursor: int
    latest_seq: int
    has_more: bool
    authority: RoomAuthority


method("groups.log", params=GroupsLogParams, result=GroupsLogResult,
       doc="A monotonic room-log delta after since_seq, bounded by count and page bytes.")


# ── groups.disband / stop / approve / retry ───────────────────────────────────────────────────


class GroupsDisbandParams(RoomParams):
    cancel_id: str | None = None


class RoomTombstone(Result):
    room_id: str
    disbanded_at: float
    idempotent: bool
    history_expired: bool | None = None
    event: RoomEvent | None = None


class GroupsDisbandResult(Result):
    tombstone: RoomTombstone


method("groups.disband", params=GroupsDisbandParams, result=GroupsDisbandResult,
       doc="Permanently tombstone a hosted room id after stopping its work and revoking peer routes.")


class GroupsStopParams(RoomParams):
    cancel_id: str | None = None


class GroupsStopResult(Result):
    cancelled: int


method("groups.stop", params=GroupsStopParams, result=GroupsStopResult,
       doc="Durably cancel queued or running work for one hosted room.")


class GroupsApproveParams(RoomParams):
    member_id: str
    task_id: str
    execution_generation: int
    choice: ApprovalChoice
    request_id: str


class GroupsApproveResult(Result):
    """``result`` is the local ``approval.respond`` answer or the peer's run-action receipt."""

    approved: bool = True
    result: dict[str, JsonValue]


method("groups.approve", params=GroupsApproveParams, result=GroupsApproveResult,
       doc="Resolve one exact pending approval raised by a local or peer room member.")


class GroupsRetryParams(RoomParams):
    task_id: str


class RoomTaskReceipt(Result):
    room_id: str
    task_id: str
    thread_id: str
    turn_id: str
    status: str
    execution_generation: int
    cancel_generation: int


class GroupsRetryResult(Result):
    retried: bool = True
    task: RoomTaskReceipt


method("groups.retry", params=GroupsRetryParams, result=GroupsRetryResult,
       doc="Retry one indeterminate room task after explicit user confirmation.")


# ── replication / authority takeover ──────────────────────────────────────────────────────────


class GroupsReplicateParams(RoomParams):
    room_name: str
    members: list[RoomMemberInput]
    page: dict[str, JsonValue]  # a verbatim ``groups.log`` result


class GroupsReplicateResult(Result):
    room_id: str
    stored_seq: int
    ingested: int
    authority: RoomAuthority
    caught_up: bool


method("groups.replicate", params=GroupsReplicateParams, result=GroupsReplicateResult,
       doc="Persist one authority-stamped replay page into the local replica store; idempotent.")


class GroupsReplicaStateParams(RoomParams):
    pass


class GroupsReplicaStateResult(Result):
    room_id: str
    name: str
    members: list[RoomMember]
    authority: RoomAuthority
    last_seq: int
    latest_seq: int
    event_bytes: int
    created_at: float
    updated_at: float


method("groups.replica_state", params=GroupsReplicaStateParams, result=GroupsReplicaStateResult,
       doc="The local replica's coverage and authority lineage for one room.")


class GroupsPromoteParams(RoomParams):
    confirm: bool | None = None
    reason: str | None = None


class GroupsPromoteResult(Result):
    room_id: str
    authority_gateway_id: str
    authority_epoch: int
    previous_gateway_id: str
    previous_epoch: int
    claim_seq: int
    latest_seq: int


method("groups.promote", params=GroupsPromoteParams, result=GroupsPromoteResult,
       doc="Continue a replicated room on this gateway at epoch + 1; requires confirm=true.")


class GroupsDemoteParams(RoomParams):
    observed_gateway_id: str
    observed_epoch: int


class GroupsDemoteResult(Result):
    room_id: str
    authority_gateway_id: str
    authority_epoch: int
    idempotent: bool


method("groups.demote", params=GroupsDemoteParams, result=GroupsDemoteResult,
       doc="Fence this gateway's stale room authority against a proven newer epoch.")


# ── peer routes (RoomLink) ────────────────────────────────────────────────────────────────────


class GroupsPeerInviteParams(ProfileParams):
    room_id: str | None = None
    home_install_id: str | None = None
    authority_gateway_id: str | None = None
    authority_epoch: int | None = None
    member_id: str | None = None
    grant_id: str | None = None
    ttl_seconds: float | None = None


class GroupsPeerInviteResult(Result):
    grant: str
    target_profile: str
    catalog: RoomLinkCatalog
    endpoint: RoomLinkEndpoint


method("groups.peer.invite", params=GroupsPeerInviteParams, result=GroupsPeerInviteResult,
       doc="Mint one target-issued room/profile grant for a prospective room home.")


class GroupsPeerRevokeParams(ProfileParams):
    grant: str


class GroupsPeerRevokeResult(Result):
    revoked: bool = True


method("groups.peer.revoke", params=GroupsPeerRevokeParams, result=GroupsPeerRevokeResult,
       doc="Revoke one target-issued grant using its exact profile scope.")


class GroupsPeerRegisterParams(RoomParams):
    member_id: str
    target_url: str
    target_profile: str
    grant: str
    catalog: dict[str, JsonValue]  # a RoomLinkCatalog mapping; ``GatewayRoomCatalog.from_mapping`` is exact
    cancellation_scope_id: str | None = None
    trace_id: str | None = None


class GroupsPeerRegisterResult(Result):
    registered: bool = True
    mode: str
    transport_security: str
    target_install_id: str
    target_profile: str


method("groups.peer.register", params=GroupsPeerRegisterParams, result=GroupsPeerRegisterResult,
       doc="Register and probe one scoped peer route on the room home.")


# ── bot relay ─────────────────────────────────────────────────────────────────────────────────


class RelayAgentRow(Params):
    """A roster row the Desktop pushes (``tools/bot_relay.py::_normalize_roster_row``); invalid
    rows are dropped server-side, so the shape stays open."""

    profile: str | None = None
    handle: str | None = None
    connection_id: str | None = None
    connection_label: str | None = None
    title: str | None = None
    description: str | None = None
    online: bool | None = None
    model_config = Params.model_config | {"extra": "allow"}


class BotRelayRosterSyncParams(ProfileParams):
    agents: list[RelayAgentRow] | None = None


class BotRelayRosterSyncResult(Result):
    count: int


method("bot_relay.roster.sync", params=BotRelayRosterSyncParams, result=BotRelayRosterSyncResult,
       doc="Replace this gateway's view of agents on other connections; answers the accepted row count.")


class BotRelayOutboxDrainParams(ProfileParams):
    pass


class RelayEnvelope(OpenModel):
    """``tools/bot_relay.py::enqueue_envelope``."""

    id: str
    created_at: int | float
    from_profile: str
    from_handle: str
    target_connection: str
    target_profile: str
    target_handle: str
    message: str


class BotRelayOutboxDrainResult(Result):
    envelopes: list[RelayEnvelope]


method("bot_relay.outbox.drain", params=BotRelayOutboxDrainParams, result=BotRelayOutboxDrainResult,
       doc="Atomically claim every pending cross-connection envelope queued on this gateway.")


class BotRelayDeliverParams(Params):
    """``profile`` here is the TARGET profile on this gateway (also what the desktop route wrapper adds)."""

    profile: str
    message: str
    from_profile: str | None = None
    from_handle: str | None = None
    from_connection: str | None = None


class BotRelayDeliverResult(Result):
    reply: str


method("bot_relay.deliver", params=BotRelayDeliverParams, result=BotRelayDeliverResult,
       doc="Deliver a relayed DM into a Bot Chat on this gateway and return the one-turn reply (blocking).")


class BotRelayReplyParams(ProfileParams):
    id: str
    reply: str | None = None
    error: str | None = None
    reason: str | None = None


method("bot_relay.reply", params=BotRelayReplyParams, result=OkResult,
       doc="Write a relayed reply and/or typed error for an envelope so the sender-side waiter resolves.")


# ── browser controller ────────────────────────────────────────────────────────────────────────


class BrowserControllerParams(Params):
    """Every controller call names the session the controller is attached to."""

    session_id: str


class BrowserControllerRegisterParams(BrowserControllerParams):
    controller_id: str
    browser_profile_id: str
    capabilities: list[str] | None = None
    protocol_version: JsonValue | None = None  # checked exactly by the handler (an int today)
    # Ignored: the principal is derived from the server-minted identity, never client-supplied.
    principal_id: str | None = None


class ControllerScope(Result):
    principal_id: str
    profile_id: str
    session_id: str
    controller_id: str
    browser_profile_id: str
    transport_family: str
    capabilities: list[str]


class BrowserControllerRegisterResult(Result):
    scope: ControllerScope


method("browser.controller.register", params=BrowserControllerRegisterParams,
       result=BrowserControllerRegisterResult,
       doc="Attach this connection as the browser controller for one session; fails closed (4403).")


class BrowserControllerResultParams(BrowserControllerParams):
    command_id: str
    ok: JsonValue | None = None  # only the exact ``true`` counts as success
    result: JsonValue | None = None
    error: JsonValue | None = None


class BrowserControllerResultResult(Result):
    accepted: bool


method("browser.controller.result", params=BrowserControllerResultParams,
       result=BrowserControllerResultResult,
       doc="Deliver one command result to the broker; accepted is false for unknown or settled command ids.")


method("browser.controller.heartbeat", params=BrowserControllerParams, result=OkResult,
       doc="Acknowledge a heartbeat only for this transport's own attached controller.")


class BrowserControllerDetachResult(Result):
    detached: bool = True


method("browser.controller.detach", params=BrowserControllerParams, result=BrowserControllerDetachResult,
       doc="Hard-detach only the controller owned by this authenticated transport.")


__all__ = [
    "GroupsLogResult", "RelayEnvelope", "Room", "RoomAuthority", "RoomEvent", "RoomLinkCatalog",
    "RoomMember", "RoomMemberInput",
]
