"""
Single source of truth for job-push event names and payload schema.

No I/O or queue dependency. Used by server (push) and client (receive).
See docs/plans/bidirectional_job_push/01_contracts_and_event_schema.md.

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from typing import Any, Dict, Final, Literal, Optional, TypedDict, Union

# ---------------------------------------------------------------------------
# Event name constants (server and client contract)
# ---------------------------------------------------------------------------

EVENT_JOB_QUEUED = "job_queued"
"""Job accepted and queued. By default the framework does not send this event."""

EVENT_JOB_PROGRESS = "job_progress"
"""Progress update (optional)."""

EVENT_JOB_COMPLETED = "job_completed"
"""Job finished successfully."""

EVENT_JOB_FAILED = "job_failed"
"""Job finished with error."""

EVENT_JOB_STOPPED = "job_stopped"
"""Job was stopped."""

EVENT_JOB_INPUT_ACCEPTED = "job_input_accepted"
"""Host confirmed input file is accepted for a queue job (correlation)."""

EVENT_JOB_PROCESSING_STEP = "job_processing_step"
"""Fine-grained processing step for long jobs (machine step + optional text)."""

EVENT_ARTIFACT_READY = "artifact_ready"
"""Outbound download session is registered; includes outbound_transfer_id and checksum."""

# ---------------------------------------------------------------------------
# Delivery event name constants (WS delivery domain)
# ---------------------------------------------------------------------------

EVENT_DELIVERY_COMPLETED: Final[str] = "delivery_completed"
"""Delivery finished successfully."""

EVENT_DELIVERY_FAILED: Final[str] = "delivery_failed"
"""Delivery finished with error."""

EVENT_DELIVERY_STOPPED: Final[str] = "delivery_stopped"
"""Delivery was stopped."""

EVENT_DELIVERY_PROGRESS: Final[str] = "delivery_progress"
"""Delivery progress update (optional)."""

# ---------------------------------------------------------------------------
# Client -> server messages (subscribe / unsubscribe)
# ---------------------------------------------------------------------------
# Expected client messages (documented contract):
#   {"action": "subscribe", "job_id": "<job_id>"}
#   {"action": "unsubscribe", "job_id": "<job_id>"}
# Typed WebSocket subscribe (client -> server):
#   {"action": "subscribe", "subscription_type": "queue_job", "job_id": "<job_id>"}
#   {"action": "subscribe", "subscription_type": "ws_delivery", "ws_id": "<ws_id>"}
#   {"action": "subscribe", "subscription_type": "transfer_session", "transfer_id": "<id>"}
# Typed WebSocket unsubscribe (symmetric):
#   {"action": "unsubscribe", "subscription_type": "queue_job", "job_id": "<job_id>"}
#   {"action": "unsubscribe", "subscription_type": "ws_delivery", "ws_id": "<ws_id>"}
#   {"action": "unsubscribe", "subscription_type": "transfer_session", "transfer_id": "<id>"}
# On authorization failure the server sends action "subscribe_error" (see ACTION_SUBSCRIBE_ERROR).
# Server does not send a confirmation for successful subscribe.

ACTION_SUBSCRIBE = "subscribe"
ACTION_UNSUBSCRIBE = "unsubscribe"
ACTION_SUBSCRIBE_ERROR = "subscribe_error"

SUBSCRIPTION_TYPE_QUEUE_JOB = "queue_job"
SUBSCRIPTION_TYPE_WS_DELIVERY = "ws_delivery"
SUBSCRIPTION_TYPE_TRANSFER_SESSION = "transfer_session"

# ---------------------------------------------------------------------------
# Transfer session WebSocket events (server -> client, subscription_type transfer_session)
# ---------------------------------------------------------------------------

EVENT_TRANSFER_ACCEPTED = "transfer_accepted"
EVENT_TRANSFER_PROGRESS = "transfer_progress"
EVENT_TRANSFER_COMPLETED = "transfer_completed"
EVENT_TRANSFER_FAILED = "transfer_failed"
EVENT_TRANSFER_EXPIRED = "transfer_expired"
EVENT_TRANSFER_RELEASED = "transfer_released"
"""Server released staging after verified transfer_ack (or policy); optional follow-up."""

TRANSFER_WS_TERMINAL_EVENTS: frozenset[str] = frozenset(
    {
        EVENT_TRANSFER_COMPLETED,
        EVENT_TRANSFER_FAILED,
        EVENT_TRANSFER_EXPIRED,
    }
)

# ---------------------------------------------------------------------------
# Payload types (server -> client)
# ---------------------------------------------------------------------------


class JobPushMessageBase(TypedDict, total=False):
    """Base fields present in every push message."""

    event: str
    job_id: str


class JobCompletedPayload(JobPushMessageBase):
    """Payload for job_completed."""

    result: Dict[str, Any]


class JobFailedPayload(JobPushMessageBase):
    """Payload for job_failed."""

    error: Optional[str]
    result: Dict[str, Any]


class JobStoppedPayload(JobPushMessageBase):
    """Payload for job_stopped."""

    result: Dict[str, Any]


class JobProgressPayload(JobPushMessageBase):
    """Payload for job_progress."""

    progress: int  # 0-100
    description: str


class JobInputAcceptedPayload(JobPushMessageBase, total=False):
    """Payload for job_input_accepted."""

    inbound_transfer_id: str
    command: str
    filename: str
    correlation_id: str


class JobProcessingStepPayload(JobPushMessageBase, total=False):
    """Payload for job_processing_step."""

    step: str
    message: str
    progress_percent: int


class ArtifactReadyPayload(JobPushMessageBase, total=False):
    """Payload for artifact_ready (queue_job subscription)."""

    outbound_transfer_id: str
    filename: str
    checksum_algorithm: str
    checksum_value: str
    size_bytes: int
    correlation_id: str


JobPushMessage = Union[
    JobCompletedPayload,
    JobFailedPayload,
    JobStoppedPayload,
    JobProgressPayload,
    JobInputAcceptedPayload,
    JobProcessingStepPayload,
    ArtifactReadyPayload,
    JobPushMessageBase,
]

# ---------------------------------------------------------------------------
# Delivery payload types (server -> client, WS delivery domain)
# ---------------------------------------------------------------------------


class DeliveryPushMessageBase(TypedDict, total=False):
    """Base fields present in every delivery push message."""

    event: str
    ws_id: str


class DeliveryCompletedPayload(DeliveryPushMessageBase):
    """Payload for delivery_completed."""

    result: Dict[str, Any]


class DeliveryFailedPayload(DeliveryPushMessageBase):
    """Payload for delivery_failed."""

    error: Optional[str]
    result: Dict[str, Any]


class DeliveryStoppedPayload(DeliveryPushMessageBase):
    """Payload for delivery_stopped."""

    result: Dict[str, Any]


class DeliveryProgressPayload(DeliveryPushMessageBase):
    """Payload for delivery_progress."""

    progress: int  # 0-100
    description: str


DeliveryPushMessage = Union[
    DeliveryCompletedPayload,
    DeliveryFailedPayload,
    DeliveryStoppedPayload,
    DeliveryProgressPayload,
    DeliveryPushMessageBase,
]

# ---------------------------------------------------------------------------
# Transfer payload types (server -> client, transfer_session subscription)
# ---------------------------------------------------------------------------


class TransferPushMessageBase(TypedDict, total=False):
    """Base fields for transfer_session push messages."""

    event: str
    subscription_type: str
    transfer_id: str


class TransferAcceptedPayload(TransferPushMessageBase, total=False):
    """Optional transfer_accepted payload (no extra required fields)."""


class TransferProgressPayload(TransferPushMessageBase, total=False):
    """Payload for transfer_progress."""

    direction: Literal["upload", "download"]
    offset: int
    size_bytes: int
    progress_percent: int
    status: str
    checksum_algorithm: str
    checksum_value: str


class TransferCompletedPayload(TransferPushMessageBase, total=False):
    """Payload for transfer_completed."""

    direction: Literal["upload", "download"]
    status: str
    filename: str
    checksum_algorithm: str
    checksum_value: str
    checksum_verified: bool
    progress_percent: int


class TransferFailedPayload(TransferPushMessageBase, total=False):
    """Payload for transfer_failed."""

    error_type: str
    message: str
    progress_percent: int


class TransferExpiredPayload(TransferPushMessageBase, total=False):
    """Payload for transfer_expired."""

    error_type: str
    message: str
    progress_percent: int


TransferPushMessage = Union[
    TransferAcceptedPayload,
    TransferProgressPayload,
    TransferCompletedPayload,
    TransferFailedPayload,
    TransferExpiredPayload,
    TransferPushMessageBase,
]


def build_push_message(
    job_id: str,
    status: str,
    result: Optional[Dict[str, Any]] = None,
    error: Optional[str] = None,
    progress: int = 0,
    description: str = "",
) -> Dict[str, Any]:
    """
    Build the push message dict from job state (queue job family only).

    Maps status (e.g. QueueJobStatus) to event name and fills payload.
    Single source of truth for the contract; used by notifier (step 06).
    This function is for queue job domain only; use build_delivery_push_message
    for WS delivery domain.

    Args:
        job_id: Job identifier.
        status: One of pending, running, completed, failed, stopped.
        result: Optional result dict (for completed/stopped/failed).
        error: Optional error message (for failed).
        progress: Progress 0-100 (for progress events).
        description: Optional description (for progress).

    Returns:
        Dict with event, job_id, and event-specific fields.
    """
    normalized = (status or "").strip().lower()
    res = result or {}
    payload: Dict[str, Any] = {"job_id": job_id}

    if normalized == "completed":
        payload["event"] = EVENT_JOB_COMPLETED
        payload["result"] = res
    elif normalized == "failed":
        payload["event"] = EVENT_JOB_FAILED
        payload["error"] = error or "Job failed"
        payload["result"] = res
    elif normalized == "stopped":
        payload["event"] = EVENT_JOB_STOPPED
        payload["result"] = res
    elif normalized in ("running", "pending") and (progress > 0 or description):
        payload["event"] = EVENT_JOB_PROGRESS
        payload["progress"] = max(0, min(100, progress))
        payload["description"] = description or ""
    else:
        # Fallback: treat as completed for unknown terminal-like status
        payload["event"] = EVENT_JOB_COMPLETED
        payload["result"] = res

    return payload


def build_delivery_push_message(
    ws_id: str,
    status: str,
    result: Optional[Dict[str, Any]] = None,
    error: Optional[str] = None,
    progress: int = 0,
    description: str = "",
) -> Dict[str, Any]:
    """
    Build the delivery push message dict from delivery state (WS delivery domain).

    Maps status to delivery event name and fills payload with ws_id.
    Single source of truth for delivery domain contract.

    Args:
        ws_id: WebSocket delivery identifier (must be non-empty).
        status: One of pending, running, completed, failed, stopped.
        result: Optional result dict (for completed/stopped/failed).
        error: Optional error message (for failed).
        progress: Progress 0-100 (for progress events).
        description: Optional description (for progress).

    Returns:
        Dict with event, ws_id, and event-specific fields.

    Raises:
        ValueError: If ws_id is empty or whitespace-only.
    """
    if ws_id.strip() == "":
        raise ValueError("build_delivery_push_message requires non-empty ws_id")
    normalized = (status or "").strip().lower()
    res = result or {}
    payload: Dict[str, Any] = {"ws_id": ws_id.strip()}

    if normalized == "completed":
        payload["event"] = EVENT_DELIVERY_COMPLETED
        payload["result"] = res
    elif normalized == "failed":
        payload["event"] = EVENT_DELIVERY_FAILED
        payload["error"] = error or "Job failed"
        payload["result"] = res
    elif normalized == "stopped":
        payload["event"] = EVENT_DELIVERY_STOPPED
        payload["result"] = res
    elif normalized in ("running", "pending") and (progress > 0 or description):
        payload["event"] = EVENT_DELIVERY_PROGRESS
        payload["progress"] = max(0, min(100, progress))
        payload["description"] = description or ""
    else:
        # Fallback: treat as completed for unknown terminal-like status
        payload["event"] = EVENT_DELIVERY_COMPLETED
        payload["result"] = res

    return payload
