"""Canonical execute-async acceptance contract (job_id, ws_id, delivery).

Defines Pydantic models and validation for JSON-RPC/REST acceptance payloads.
Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

from copy import deepcopy
from typing import Any, Literal, Mapping

from pydantic import BaseModel, ConfigDict, Field

# Stable ValueError messages for matrix violations (tests assert on these).
ERR_POLL_WS = ValueError("ws_id must be null when delivery.mode is poll")
ERR_WS_NEED_ID = ValueError("ws_id required when delivery.mode is ws")
ERR_WS_TERMINAL = ValueError(
    "delivery.terminal_only must be true when delivery.mode is ws"
)
ERR_JOBLESS_NON_WS = ValueError(
    "in-process async requires delivery.mode ws when job_id is absent"
)
ERR_ACCEPTED_IDS = ValueError("accepted response requires job_id or ws_id")


class DeliveryContract(BaseModel):
    """Nested delivery contract: mode and terminal_only."""

    model_config = ConfigDict(extra="forbid")

    mode: Literal["ws", "poll"] = Field(..., description="Delivery mode")
    terminal_only: bool | None = Field(
        default=None, description="When mode is ws, must be True"
    )


class ExecuteAsyncAcceptance(BaseModel):
    """Canonical acceptance payload for async-capable execution responses."""

    model_config = ConfigDict(extra="forbid")

    accepted: bool = Field(..., description="Acceptance flag")
    job_id: str | None = Field(default=None, description="Execution identity")
    ws_id: str | None = Field(default=None, description="WebSocket delivery identity")
    delivery: DeliveryContract = Field(..., description="Effective delivery contract")
    message: str | None = Field(
        default=None, description="Optional human-readable text"
    )


def _is_blank(value: str | None) -> bool:
    """Return True if value is None or a blank/whitespace-only string."""
    if value is None:
        return True
    return isinstance(value, str) and value.strip() == ""


def _normalized_job_id(job_id: str | None) -> str | None:
    """Return None if job_id is blank, else stripped string."""
    if _is_blank(job_id):
        return None
    return str(job_id).strip()


def _normalized_ws_id(ws_id: str | None) -> str | None:
    """Return None if ws_id is blank, else stripped string."""
    if _is_blank(ws_id):
        return None
    return str(ws_id).strip()


def validate_identity_delivery_matrix(
    *,
    job_id: str | None,
    ws_id: str | None,
    delivery: DeliveryContract,
) -> None:
    """Enforce legal (job_id, ws_id, delivery) combinations; raise on violation."""
    j = _normalized_job_id(job_id)
    w = _normalized_ws_id(ws_id)

    if delivery.mode == "poll" and w is not None:
        raise ERR_POLL_WS
    if delivery.mode == "ws" and w is None:
        raise ERR_WS_NEED_ID
    if delivery.mode == "ws" and delivery.terminal_only is not True:
        raise ERR_WS_TERMINAL
    if j is None and delivery.mode != "ws":
        raise ERR_JOBLESS_NON_WS
    if j is not None and delivery.mode == "poll" and w is not None:
        raise ERR_POLL_WS
    if j is not None and delivery.mode == "ws" and w is None:
        raise ERR_WS_NEED_ID


def parse_acceptance_payload(
    data: Mapping[str, Any],
    *,
    allow_legacy_deliver_id: bool = False,
) -> ExecuteAsyncAcceptance:
    """Parse acceptance payload; optionally map legacy deliver_id to ws_id."""
    payload: dict[str, Any] = deepcopy(dict(data))

    if allow_legacy_deliver_id and "ws_id" not in payload and "deliver_id" in payload:
        raw = payload.get("deliver_id")
        if raw is not None:
            stripped = str(raw).strip()
            if stripped:
                payload["ws_id"] = stripped

    if "deliver_id" in payload:
        payload.pop("deliver_id")

    model = ExecuteAsyncAcceptance.model_validate(payload)
    j = _normalized_job_id(model.job_id)
    w = _normalized_ws_id(model.ws_id)

    if model.accepted:
        if j is None and w is None:
            raise ERR_ACCEPTED_IDS
        validate_identity_delivery_matrix(job_id=j, ws_id=w, delivery=model.delivery)

    return model
