"""Client operation state and acceptance contract parsing.

Unified runtime state for one client operation; parse server acceptance and
apply delivery/execution updates with domain isolation. Per tech_spec §14.7, §15.1.
Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from mcp_proxy_adapter.core.delivery.contracts import (
    DeliveryContract,
    DeliveryMode,
)
from mcp_proxy_adapter.core.delivery.exceptions import (
    ExecutionDeliveryIdentityCollisionError,
    InvalidDeliveryContractError,
)

# Terminal delivery event names (canonical delivery_* family)
WS_DELIVERY_TERMINAL_EVENTS = frozenset(
    {"delivery_completed", "delivery_failed", "delivery_cancelled"}
)


def _is_blank(value: str | None) -> bool:
    """
    Check if a string value is blank (None, empty, or whitespace only).

    Args:
        value: String value to check.

    Returns:
        True if value is None, empty, or contains only whitespace; False otherwise.
    """
    if value is None:
        return True
    return isinstance(value, str) and value.strip() == ""


def _normalize_mode(raw: Any) -> DeliveryMode:
    """Coerce string to DeliveryMode; raise InvalidDeliveryContractError if invalid."""
    if isinstance(raw, DeliveryMode):
        return raw
    s = (raw or "").strip().lower()
    try:
        return DeliveryMode(s)
    except ValueError:
        raise InvalidDeliveryContractError(
            f"delivery.mode must be one of immediate, poll, ws, ws_with_status; got {raw!r}"
        ) from None


def _parse_delivery(raw: Any) -> DeliveryContract:
    """Parse nested delivery dict into DeliveryContract."""
    if not isinstance(raw, dict):
        raise InvalidDeliveryContractError("delivery must be an object")
    mode = _normalize_mode(raw.get("mode"))
    terminal_only = raw.get("terminal_only")
    if terminal_only is None and mode in (DeliveryMode.WS, DeliveryMode.WS_WITH_STATUS):
        terminal_only = True
    elif terminal_only is None:
        terminal_only = False
    status_observable = bool(raw.get("status_observable", False))
    return DeliveryContract(
        mode=mode,
        terminal_only=bool(terminal_only),
        status_observable=status_observable,
    )


def parse_execution_acceptance(raw: dict[str, Any]) -> dict[str, Any]:
    """
    Validate and normalize server acceptance dict.

    Returns a dict with keys: accepted, job_id, ws_id, delivery (DeliveryContract),
    message. Raises InvalidDeliveryContractError on missing required keys, wrong types,
    or illegal job_id/ws_id combination per §15.1.
    """
    if not isinstance(raw, dict):
        raise InvalidDeliveryContractError("acceptance payload must be an object")
    if "delivery" not in raw:
        raise InvalidDeliveryContractError("acceptance missing delivery")
    if "accepted" not in raw:
        raise InvalidDeliveryContractError("acceptance missing accepted")

    delivery = _parse_delivery(raw["delivery"])
    job_id = raw.get("job_id")
    ws_id = raw.get("ws_id")
    if job_id is not None and not isinstance(job_id, str):
        job_id = str(job_id).strip() or None
    elif isinstance(job_id, str):
        job_id = job_id.strip() or None
    if ws_id is not None and not isinstance(ws_id, str):
        ws_id = str(ws_id).strip() or None
    elif isinstance(ws_id, str):
        ws_id = ws_id.strip() or None

    # §15.1 / matrix: poll => ws_id must be null
    if delivery.mode == DeliveryMode.POLL and ws_id is not None:
        raise InvalidDeliveryContractError("delivery.mode is poll; ws_id must be null")
    # ws or ws_with_status => ws_id required
    if delivery.mode in (DeliveryMode.WS, DeliveryMode.WS_WITH_STATUS):
        if _is_blank(ws_id):
            raise InvalidDeliveryContractError(
                "delivery.mode is ws or ws_with_status; ws_id is required"
            )
        if delivery.terminal_only is not True:
            raise InvalidDeliveryContractError(
                "delivery.terminal_only must be true when mode is ws or ws_with_status"
            )
    # in-process async: job_id None => mode must be ws
    if job_id is None and delivery.mode not in (
        DeliveryMode.WS,
        DeliveryMode.WS_WITH_STATUS,
    ):
        raise InvalidDeliveryContractError(
            "accepted response with no job_id requires delivery.mode ws or ws_with_status"
        )
    if raw.get("accepted") is True and job_id is None and _is_blank(ws_id):
        raise InvalidDeliveryContractError("accepted response requires job_id or ws_id")

    return {
        "accepted": bool(raw.get("accepted")),
        "job_id": job_id,
        "ws_id": ws_id,
        "delivery": delivery,
        "message": raw.get("message"),
    }


def build_operation_state_from_acceptance(
    payload: dict[str, Any],
) -> ClientOperationState:
    """Build initial ClientOperationState from parsed acceptance payload."""
    delivery = payload["delivery"]
    if isinstance(delivery, DeliveryContract):
        contract = delivery
    else:
        contract = _parse_delivery(delivery)
    return ClientOperationState(
        job_id=payload.get("job_id"),
        ws_id=payload.get("ws_id"),
        delivery_mode=contract.mode,
        execution_status=None,
        delivery_status="pending",
        result=None,
        error=None,
    )


@dataclass
class ClientOperationState:
    """
    Unified runtime state for one client operation.

    Holds execution (job_id, execution_status) and delivery (ws_id, delivery_status,
    result, error) fields separately; apply_delivery_event and apply_execution_status
    must not cross domains.
    """

    job_id: str | None
    ws_id: str | None
    delivery_mode: DeliveryMode
    execution_status: str | None
    delivery_status: str
    result: dict[str, Any] | None
    error: str | None

    def apply_delivery_event(self, payload: dict[str, Any]) -> None:
        """
        Update from WS delivery terminal payload.

        Sets delivery_status from event, stores result/error; validates ws_id
        consistency. Raises ExecutionDeliveryIdentityCollisionError if payload
        ws_id differs from stored ws_id when state already has ws_id.
        """
        payload_ws_id = payload.get("ws_id")
        if payload_ws_id is not None:
            payload_ws_id = str(payload_ws_id).strip() or None
        if (
            self.ws_id is not None
            and payload_ws_id is not None
            and payload_ws_id != self.ws_id
        ):
            raise ExecutionDeliveryIdentityCollisionError(
                f"delivery event ws_id {payload_ws_id!r} does not match state ws_id {self.ws_id!r}"
            )
        event = (payload.get("event") or payload.get("type") or "").strip()
        if event:
            self.delivery_status = event
        if "result" in payload:
            self.result = (
                payload["result"] if isinstance(payload["result"], dict) else {}
            )
        if "error" in payload:
            self.error = (
                payload["error"] if payload["error"] is None else str(payload["error"])
            )

    def apply_execution_status(self, payload: dict[str, Any]) -> None:
        """
        Update execution_status and progress-related fields from queue/status API.

        Must not read or set ws_id. Ignores delivery-domain keys.
        """
        # Execution-domain keys only; ignore ws_id, deliver_id, subscription_type
        status = payload.get("status")
        if status is not None:
            self.execution_status = str(status).strip() or self.execution_status

    def mark_timeout(self) -> None:
        """Set timeout marker for coordinated shutdown."""
        self.delivery_status = "delivery_timeout"

    def mark_cancelled(self) -> None:
        """Set cancellation marker for coordinated shutdown."""
        self.delivery_status = "delivery_cancelled"
