"""
Hook point: when job state changes, call notify_job_state_changed.

Listeners are registered at app startup (step 06); they build payload from
events.py and send to subscribers via registry. No WebSocket or registry
dependency in this module's public API.
See docs/plans/bidirectional_job_push/02_job_state_change_source.md and 06.

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

from __future__ import annotations

import asyncio
from typing import Any, Callable, Dict, List, Optional

# Type for listener: (job_id, status, result, error, progress, description) -> None
_Listener = Callable[
    [
        str,
        str,
        Optional[Dict[str, Any]],
        Optional[str],
        int,
        str,
    ],
    Any,
]

_listeners: List[_Listener] = []
_lock = asyncio.Lock()

# Deduplication: last sent (job_id, event) so we do not send same terminal event twice.
_sent_terminal: Dict[str, str] = {}
_sent_lock = asyncio.Lock()


async def notify_job_state_changed(
    job_id: str,
    status: str,
    result: Optional[Dict[str, Any]] = None,
    error: Optional[str] = None,
    progress: int = 0,
    description: str = "",
) -> None:
    """
    Called when job state changes (e.g. from poll loop or queuemgr callback).

    Uses status values compatible with QueueJobStatus and event names from events.py.
    Registered listeners (set in step 06) build the push message and send to
    subscribers. Terminal state is deduplicated so the same event is not sent twice.

    Args:
        job_id: Job identifier.
        status: One of pending, running, completed, failed, stopped.
        result: Optional result dict.
        error: Optional error message.
        progress: Progress 0-100.
        description: Optional description.
    """
    normalized = (status or "").strip().lower()

    # Deduplicate terminal events: only notify once per job_id for terminal state
    if normalized in ("completed", "failed", "stopped"):
        async with _sent_lock:
            if _sent_terminal.get(job_id) == normalized:
                return
            _sent_terminal[job_id] = normalized

    async with _lock:
        listeners = list(_listeners)
    for listener in listeners:
        try:
            out = listener(
                job_id,
                status,
                result,
                error,
                progress,
                description,
            )
            if asyncio.iscoroutine(out):
                await out
        except Exception:
            # Log but do not break other listeners
            from mcp_proxy_adapter.core.logging import get_global_logger

            get_global_logger().exception(
                "Job push listener failed for job_id=%s status=%s", job_id, status
            )


def register_notifier_listener(listener: _Listener) -> None:
    """Register a listener to be called on job state change. Used by step 06."""
    _listeners.append(listener)


def clear_notifier_listeners() -> None:
    """Remove all listeners (e.g. for tests)."""
    _listeners.clear()
    _sent_terminal.clear()
