"""Protocol envelope types for server <-> plugin communication."""

from __future__ import annotations

from typing import Any
from uuid import uuid4

from pydantic import BaseModel, Field


class CommandRequest(BaseModel):
    """A command sent from the Python server to the Godot plugin."""

    request_id: str = Field(default_factory=lambda: uuid4().hex)
    command: str
    params: dict[str, Any] = Field(default_factory=dict)


class CommandResponse(BaseModel):
    """A response sent from the Godot plugin back to the Python server."""

    request_id: str
    status: str  # "ok" or "error"
    data: dict[str, Any] = Field(default_factory=dict)
    error: ErrorDetail | None = None
    ## Live readiness snapshot stamped by the plugin's dispatcher onto every
    ## response envelope. The server uses it to keep `Session.readiness` in
    ## lockstep with editor state, so a stale "playing" / "importing" cache
    ## (e.g. a `readiness_changed` event lost in transit, or a one-frame race
    ## around `pause_processing`) self-heals on the very next tool call —
    ## without the agent having to call `editor_state` first. Older plugins
    ## omit the field; the server falls through to the existing event-driven
    ## path. See connection.gd::get_readiness for the producer.
    readiness: str | None = None


class ErrorDetail(BaseModel):
    """Structured error information from the plugin."""

    code: str
    message: str
    data: dict[str, Any] = Field(default_factory=dict)


class HandshakeMessage(BaseModel):
    """Initial handshake sent by the Godot plugin on connection."""

    type: str = "handshake"
    session_id: str
    godot_version: str
    project_path: str
    plugin_version: str
    protocol_version: int = 1
    readiness: str = "ready"
    ## Optional because older plugins won't send it; server falls back to 0.
    editor_pid: int = 0
    ## Which launcher tier the plugin resolved for the Python server. Older
    ## plugins omit the field entirely, which lands as "unknown" on the
    ## server — distinguishable from a live detection that returned
    ## "unknown" only by plugin_version.
    server_launch_mode: str = "unknown"


## State events emitted by the plugin's _check_state_changes() poller. Each
## carries one typed string field. Validating them on receive prevents a
## malformed event (or a hijacked WS) from setting non-string values on the
## Session, which then ship to MCP clients verbatim via Session.to_dict().
## See audit-v2 finding #7 (issue #351).


class SceneChangedEvent(BaseModel):
    current_scene: str = ""


class PlayStateChangedEvent(BaseModel):
    play_state: str = "stopped"


class ReadinessChangedEvent(BaseModel):
    readiness: str = "ready"


## Plugin-emitted telemetry event. The plugin relays its own events
## (self-update outcome, plugin reload, dock startup) through this
## envelope so opt-out / endpoint / customer_uuid stay in one place
## (Python). The dispatcher in transport/websocket.py validates and
## forwards to ``telemetry.record_telemetry``.
class PluginTelemetryEvent(BaseModel):
    name: str = ""
    data: dict[str, Any] = Field(default_factory=dict)
