"""Pipeline stage event publisher (DE → UPA via Redis pub/sub).

DE publishes stage-transition events to a per-tenant+node Redis channel so UPA
can relay them to browsers on the Socket.IO ``/pipeline`` namespace. DE itself
runs no Socket.IO server.

Messages are ephemeral: Redis pub/sub does not persist. If no subscriber is
connected, the message is dropped. The source of truth for document lifecycle
remains ``documents.status`` in Postgres; events are a live UX signal only.
"""

from __future__ import annotations

import json
from datetime import datetime, timezone
from typing import Literal, Optional

import redis
import structlog

from src.core.config import settings

logger = structlog.get_logger(__name__)

Stage = Literal[
    "pending",
    "processing",
    "parsing",
    "chunking",
    "indexing",
    "completed",
    "failed",
]
Status = Literal["started", "succeeded", "failed"]


def _build_channel(tenant_id: str, node_id: str) -> str:
    return f"{settings.pipeline_event_channel_prefix}:{tenant_id}:{node_id}"


def _build_payload(
    *,
    stage: Stage,
    status: Status,
    document_id: str,
    tenant_id: str,
    node_id: str,
    correlation_id: str,
    error: Optional[str] = None,
) -> dict:
    payload = {
        "correlation_id": correlation_id,
        "document_id": document_id,
        "tenant_id": tenant_id,
        "node_id": node_id,
        "stage": stage,
        "status": status,
        "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
    }
    if error is not None:
        payload["error"] = error
    return payload


def publish_stage_event(
    stage: Stage,
    status: Status,
    *,
    document_id: str,
    tenant_id: str,
    node_id: str,
    correlation_id: str,
    error: Optional[str] = None,
) -> None:
    """Publish a pipeline stage event to Redis pub/sub.

    Fire-and-forget: never raises. Disabled unless
    ``settings.pipeline_events_enabled`` is true. Redis errors are logged at
    WARNING and swallowed so emit failures never break a DAG task.
    """
    if not settings.pipeline_events_enabled:
        return

    if not tenant_id or not node_id:
        logger.error(
            "pipeline_event.missing_routing_id",
            stage=stage,
            document_id=document_id,
            tenant_id=tenant_id,
            node_id=node_id,
        )
        return

    channel = _build_channel(tenant_id, node_id)
    payload = _build_payload(
        stage=stage,
        status=status,
        document_id=document_id,
        tenant_id=tenant_id,
        node_id=node_id,
        correlation_id=correlation_id,
        error=error,
    )

    try:
        client = redis.Redis.from_url(settings.redis_url)
        client.publish(channel, json.dumps(payload))
    except redis.RedisError as exc:
        logger.warning(
            "pipeline_event.publish_failed",
            channel=channel,
            stage=stage,
            status=status,
            document_id=document_id,
            error=str(exc),
        )
