from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Literal
from urllib.parse import urlparse
from uuid import uuid4

from pathlib import Path

from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator


PROTOCOL_VERSION = "1.0"
RouteName = Literal["dialogue", "hermes", "video", "image", "cancel", "clarify"]
RiskLevel = Literal["low", "medium", "high"]
ModelToolName = Literal[
    "reasoning.delegate",
    "internet.search",
    "image.generate",
    "image.edit",
    "video.generate",
    "audio.generate",
    "voice.speak",
]


def utc_now() -> datetime:
    return datetime.now(timezone.utc)


def new_id(prefix: str) -> str:
    return f"{prefix}_{uuid4().hex}"


class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid")


class EventEnvelope(StrictModel):
    protocol_version: str = PROTOCOL_VERSION
    type: str = Field(min_length=1, max_length=128)
    event_id: str = Field(default_factory=lambda: new_id("evt"))
    seq: int = Field(ge=1)
    timestamp: datetime = Field(default_factory=utc_now)
    session_id: str = Field(min_length=1, max_length=160)
    turn_id: str = Field(min_length=1, max_length=160)
    operation_id: str = Field(min_length=1, max_length=160)
    trace_id: str = Field(min_length=1, max_length=160)
    causation_id: str | None = None
    source: str = Field(min_length=1, max_length=80)
    payload: dict[str, Any] = Field(default_factory=dict)

    @field_validator("protocol_version")
    @classmethod
    def validate_protocol(cls, value: str) -> str:
        if value != PROTOCOL_VERSION:
            raise ValueError(f"Unsupported protocol version: {value}")
        return value


class TranscriptInput(StrictModel):
    session_id: str = Field(min_length=1, max_length=160)
    turn_id: str = Field(min_length=1, max_length=160)
    text: str = Field(min_length=1, max_length=32_000)
    final: bool
    trace_id: str | None = None
    language: str | None = Field(default=None, max_length=24)
    thinking_oz: Literal[2, 4, 8] | None = None
    confidence: float | None = Field(default=None, ge=0, le=1)
    needs_vision: bool = False
    target_node: str | None = Field(default=None, max_length=240)
    required_capabilities: set[str] = Field(default_factory=set)
    requested_permissions: set[str] = Field(default_factory=set)
    minicpm_session_id: str | None = Field(default=None, max_length=160)


class TextInput(StrictModel):
    session_id: str = Field(min_length=1, max_length=160)
    turn_id: str = Field(min_length=1, max_length=160)
    text: str = Field(min_length=1, max_length=32_000)
    trace_id: str | None = None
    language: str | None = Field(default=None, max_length=24)
    thinking_oz: Literal[2, 4, 8] | None = None
    needs_vision: bool = False
    target_node: str | None = Field(default=None, max_length=240)
    required_capabilities: set[str] = Field(default_factory=set)
    requested_permissions: set[str] = Field(default_factory=set)


class AgentFocusRequest(StrictModel):
    turn_id: str = Field(default="control", min_length=1, max_length=160)
    trace_id: str | None = None
    needs_vision: bool = False
    cancel_if_active: bool = True
    armed: bool = True
    target_node: str | None = Field(default=None, max_length=240)
    required_capabilities: set[str] = Field(default_factory=set)
    requested_permissions: set[str] = Field(default_factory=set)


class AgentFocusResponse(StrictModel):
    mode: Literal["armed", "cancelled", "cleared"]
    cancelled_operation_ids: list[str] = Field(default_factory=list)
    next_route: Literal["hermes"] | None = None
    target_node: str | None = None


class SessionEndRequest(StrictModel):
    turn_id: str = Field(default="end", min_length=1, max_length=160)
    trace_id: str | None = None


class SessionEndResponse(StrictModel):
    ended: bool = True
    cancelled_operation_ids: list[str] = Field(default_factory=list)


class RouteRequest(StrictModel):
    text: str = Field(min_length=1, max_length=32_000)
    needs_vision: bool = False
    language: str | None = Field(default=None, max_length=24)


class RouteDecision(StrictModel):
    route: RouteName
    confidence: float = Field(ge=0, le=1)
    needs_vision: bool = False
    risk: RiskLevel = "low"
    reason_code: str = Field(min_length=1, max_length=80)
    ack: str = Field(min_length=1, max_length=240)
    thinking_oz: Literal[2, 4, 8] = 4


class TranscriptReceipt(StrictModel):
    accepted: bool = True
    final: bool
    event_seq: int
    route: RouteDecision | None = None
    operation_id: str | None = None
    replayed: bool = False


class ModelToolRequest(StrictModel):
    source_model: Literal["minicpm", "qwen", "hermes", "conductor", "connector"]
    tool: ModelToolName
    session_id: str = Field(min_length=1, max_length=160)
    turn_id: str = Field(min_length=1, max_length=160)
    prompt: str = Field(min_length=1, max_length=32_000)
    trace_id: str | None = None
    thinking_oz: Literal[2, 4, 8] = 4
    target_node: str | None = Field(default=None, max_length=240)
    task_id: str | None = Field(default=None, max_length=160)
    task_version: int | None = Field(default=None, ge=1)
    options: dict[str, Any] = Field(default_factory=dict)

    @model_validator(mode="after")
    def image_edit_requires_input(self) -> "ModelToolRequest":
        if (self.task_id is None) != (self.task_version is None):
            raise ValueError("task_id and task_version must be provided together")
        if self.tool == "image.edit" and not self.options.get("input_artifacts"):
            raise ValueError("image.edit requires options.input_artifacts")
        connector = self.options.get("connector")
        if self.tool == "voice.speak" and connector not in {None, "local"}:
            raise ValueError(
                "external voice output requires a separately registered connector adapter"
            )
        return self


class ModelToolReceipt(StrictModel):
    accepted: bool = True
    tool: ModelToolName
    target_model: Literal["minicpm", "qwen", "hunyuan", "minimax-h3"]
    lifecycle: Literal["always", "cold"]
    status: Literal["completed", "queued"]
    event_seq: int
    operation_id: str | None = None
    replayed: bool = False


class ModelToolDescriptor(StrictModel):
    tool: ModelToolName
    target_model: Literal["minicpm", "qwen", "hunyuan", "minimax-h3"]
    lifecycle: Literal["always", "cold"]
    external_side_effect: bool = False


class OperationView(StrictModel):
    operation_id: str
    kind: Literal["hermes", "video", "image"]
    session_id: str
    turn_id: str
    trace_id: str
    task_id: str | None = None
    task_version: int | None = None
    status: Literal[
        "created", "queued", "started", "approval_required", "cancelling",
        "completed", "failed", "cancelled"
    ]
    downstream_id: str | None = None
    created_at: datetime
    updated_at: datetime
    terminal_event_id: str | None = None


class HermesRunRequest(StrictModel):
    prompt: str = Field(min_length=1, max_length=32_000)
    session_id: str
    turn_id: str
    trace_id: str
    needs_vision: bool = False
    context: dict[str, Any] = Field(default_factory=dict)
    target_node: str | None = Field(default=None, max_length=240)
    required_capabilities: set[str] = Field(default_factory=set)
    requested_permissions: set[str] = Field(default_factory=set)


class HermesRun(StrictModel):
    run_id: str
    provider_id: str | None = None


class DiscoveredNode(StrictModel):
    node_id: str = Field(min_length=1, max_length=160)
    name: str = Field(min_length=1, max_length=240)
    endpoint: str = Field(pattern=r"^https://", max_length=2048)
    # Discovery is intentionally not a trust channel.  The operator supplies
    # the independently verified fingerprint only in PairNodeRequest.
    certificate_fingerprint_sha256: str | None = Field(
        default=None, pattern=r"^[0-9a-fA-F]{64}$"
    )
    capabilities: set[str] = Field(default_factory=set)
    source: Literal["mdns"] = "mdns"


class PairNodeRequest(StrictModel):
    endpoint: str = Field(pattern=r"^https://", max_length=2048)
    one_time_code: SecretStr
    expected_fingerprint_sha256: str = Field(pattern=r"^[0-9a-fA-F]{64}$")
    ca_cert_path: Path
    client_cert_path: Path
    client_key_path: Path
    requested_permissions: set[str] = Field(default_factory=set)


class PairingReceipt(StrictModel):
    node_id: str
    name: str
    endpoint: str = Field(pattern=r"^https://")
    certificate_fingerprint_sha256: str = Field(pattern=r"^[0-9a-fA-F]{64}$")
    capabilities: set[str] = Field(default_factory=set)
    granted_permissions: set[str] = Field(default_factory=set)


class TrustedNode(StrictModel):
    node_id: str
    name: str
    endpoint: str = Field(pattern=r"^https://")
    certificate_fingerprint_sha256: str = Field(pattern=r"^[0-9a-fA-F]{64}$")
    capabilities: set[str] = Field(default_factory=set)
    granted_permissions: set[str] = Field(default_factory=set)
    ca_cert_path: Path
    client_cert_path: Path
    client_key_path: Path
    paired_at: datetime = Field(default_factory=utc_now)
    enabled: bool = True


class NodeView(StrictModel):
    node_id: str
    name: str
    endpoint: str
    capabilities: set[str] = Field(default_factory=set)
    granted_permissions: set[str] = Field(default_factory=set)
    enabled: bool
    trusted: bool = True
    status: Literal["unknown", "ready", "unavailable"] = "unknown"
    latency_ms: float | None = Field(default=None, ge=0)


class NodeKillSwitchRequest(StrictModel):
    engaged: bool


class NodeKillSwitchResponse(StrictModel):
    engaged: bool
    cancelled_run_ids: list[str] = Field(default_factory=list)


class NodeActionContract(StrictModel):
    """Payload contract used inside a paired Hermes run, never a public control API."""

    action_id: str = Field(default_factory=lambda: new_id("action"))
    type: Literal[
        "desktop.screenshot",
        "desktop.control",
        "files.read",
        "files.write",
        "tool.execute",
        "webcam.start",
        "webcam.frame",
        "webcam.stop",
    ]
    permission: Literal[
        "desktop.observe",
        "desktop.control",
        "files.read",
        "files.write",
        "tool.execute",
        "webcam.read",
    ]
    arguments: dict[str, Any] = Field(default_factory=dict)

    @model_validator(mode="after")
    def permission_matches_action(self) -> "NodeActionContract":
        expected = {
            "desktop.screenshot": "desktop.observe",
            "desktop.control": "desktop.control",
            "files.read": "files.read",
            "files.write": "files.write",
            "tool.execute": "tool.execute",
            "webcam.start": "webcam.read",
            "webcam.frame": "webcam.read",
            "webcam.stop": "webcam.read",
        }[self.type]
        if self.permission != expected:
            raise ValueError(f"{self.type} requires permission {expected}")
        return self


class MediaJobRequest(StrictModel):
    kind: Literal["video", "image"]
    prompt: str = Field(min_length=1, max_length=32_000)
    session_id: str
    turn_id: str
    trace_id: str
    options: dict[str, Any] = Field(default_factory=dict)


class MediaArtifact(StrictModel):
    artifact_id: str = Field(min_length=1, max_length=256)
    url: str = Field(min_length=1, max_length=4096)
    mime_type: str = Field(min_length=1, max_length=160)

    @field_validator("url")
    @classmethod
    def reject_embedded_payloads(cls, value: str) -> str:
        parsed = urlparse(value)
        if parsed.scheme.lower() == "data":
            raise ValueError("Media results must use artifact IDs/URLs, never data URLs")
        if not parsed.scheme and not value.startswith("/"):
            raise ValueError("Artifact URL must be absolute or a local absolute path")
        return value


class MediaLease(StrictModel):
    lease_id: str
    endpoint: str | None = None


class MediaJob(StrictModel):
    job_id: str


class ASRStart(StrictModel):
    type: Literal["input.start"] = "input.start"
    protocol_version: str = PROTOCOL_VERSION
    session_id: str
    turn_id: str
    trace_id: str
    sample_rate_hz: int = Field(default=16_000, ge=8_000, le=192_000)
    channels: int = Field(default=1, ge=1, le=8)
    encoding: Literal["pcm_s16le", "pcm_f32le"] = "pcm_s16le"
    language: str | None = None
    needs_vision: bool = False
    target_node: str | None = Field(default=None, max_length=240)
    required_capabilities: set[str] = Field(default_factory=set)
    requested_permissions: set[str] = Field(default_factory=set)
    minicpm_session_id: str | None = Field(default=None, max_length=160)

    @field_validator("protocol_version")
    @classmethod
    def validate_protocol(cls, value: str) -> str:
        if value != PROTOCOL_VERSION:
            raise ValueError(f"Unsupported protocol version: {value}")
        return value


class ASRControl(StrictModel):
    type: Literal["input.commit", "input.cancel"]


class ASRTranscriptEvent(StrictModel):
    type: Literal["input.transcript.partial", "input.transcript.final", "error"]
    text: str = ""
    confidence: float | None = Field(default=None, ge=0, le=1)
    language: str | None = None
    detail: str | None = None
