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

Domain-aware terminal deduplication (queue_job vs ws_delivery) and extended
listener arity (domain, identifier, status, result, error, progress, description).
Listeners are registered at app startup; they build payload from events and
send to subscribers via registry.
Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

import asyncio
from enum import Enum
from threading import RLock
from typing import Any, Callable, Dict, List, Optional


class NotificationDomain(str, Enum):
    """Domain for terminal dedup: queue jobs vs WS delivery."""

    QUEUE_JOB = "queue_job"
    WS_DELIVERY = "ws_delivery"


_Listener = Callable[
    [
        NotificationDomain,
        str,
        str,
        Optional[Dict[str, Any]],
        Optional[str],
        int,
        str,
    ],
    Any,
]

_listeners: List[_Listener] = []
_listeners_lock = RLock()
_sent_terminal_queue: Dict[str, str] = {}
_sent_terminal_delivery: Dict[str, str] = {}
_sent_terminal_unified: Dict[str, str] = {}
_use_legacy_unified_terminal_dedup: bool = False
_sent_lock = RLock()


async def notify_job_state_changed(
    domain: NotificationDomain,
    identifier: str,
    status: str,
    result: Optional[Dict[str, Any]] = None,
    error: Optional[str] = None,
    progress: int = 0,
    description: str = "",
) -> None:
    """Notify registered listeners of a job/delivery state change with domain-scoped terminal deduplication."""
    stripped_id = identifier.strip()
    if stripped_id == "":
        raise ValueError("notify_job_state_changed requires non-empty identifier")
    normalized = (status or "").strip().lower()

    if normalized in ("completed", "failed", "stopped"):
        with _sent_lock:
            if _use_legacy_unified_terminal_dedup:
                D = _sent_terminal_unified
            else:
                D = (
                    _sent_terminal_queue
                    if domain == NotificationDomain.QUEUE_JOB
                    else _sent_terminal_delivery
                )
            if D.get(stripped_id) == normalized:
                return
            D[stripped_id] = normalized

    with _listeners_lock:
        listeners = list(_listeners)
    for listener in listeners:
        try:
            out = listener(
                domain,
                stripped_id,
                status,
                result,
                error,
                progress,
                description,
            )
            if asyncio.iscoroutine(out):
                await out
        except Exception:
            from mcp_proxy_adapter.core.logging import get_global_logger

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


def register_notifier_listener(listener: _Listener) -> None:
    """Register a listener for job push notifications."""
    with _listeners_lock:
        _listeners.append(listener)


def configure_notifier_job_push_policy(*, legacy_unified_terminal_dedup: bool) -> None:
    """
    Configure notifier policy for terminal deduplication.

    Args:
        legacy_unified_terminal_dedup: If True, use unified terminal dedup
            across all domains (legacy behavior). If False, use domain-partitioned
            dedup (default Step 04 behavior).
    """
    global _use_legacy_unified_terminal_dedup
    with _sent_lock:
        _use_legacy_unified_terminal_dedup = legacy_unified_terminal_dedup
        _sent_terminal_unified.clear()
        _sent_terminal_queue.clear()
        _sent_terminal_delivery.clear()


def clear_notifier_listeners() -> None:
    """Remove all listeners and clear domain terminal dedup state; clear execute-async deliver guard ids."""
    with _listeners_lock:
        _listeners.clear()
    with _sent_lock:
        _sent_terminal_unified.clear()
        _sent_terminal_queue.clear()
        _sent_terminal_delivery.clear()
    from mcp_proxy_adapter.core.job_push.execute_async_deliver_guard import (
        clear_execute_async_deliver_ids,
    )

    clear_execute_async_deliver_ids()
