"""
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, 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."""

# ---------------------------------------------------------------------------
# Client -> server messages (subscribe / unsubscribe)
# ---------------------------------------------------------------------------
# Expected client messages (documented contract):
#   {"action": "subscribe", "job_id": "<job_id>"}
#   {"action": "unsubscribe", "job_id": "<job_id>"}
# Server does not send a confirmation for successful subscribe.
# On rejection (auth/authorization), server MUST send:
#   {"action": "subscribe_error", "job_id": "<job_id>", "error": "<reason>"}

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

# ---------------------------------------------------------------------------
# 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


JobPushMessage = Union[
    JobCompletedPayload,
    JobFailedPayload,
    JobStoppedPayload,
    JobProgressPayload,
    JobPushMessageBase,
]


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.

    Maps status (e.g. QueueJobStatus) to event name and fills payload.
    Single source of truth for the contract; used by notifier (step 06).

    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
