"""
Lifespan wiring for job push: registry, ws_delivery_manager, notifier listener, poll loop.

Called from api/core/lifespan_manager.py when queue_manager is enabled and
job_push is enabled. Creates app.state.job_push_registry, app.state.ws_delivery_manager,
and starts a task that polls queue subscriptions only.
Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

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

from fastapi import FastAPI

from mcp_proxy_adapter.core.constants import (
    JOB_PUSH_POLL_INTERVAL,
    TRANSFER_SESSION_EXPIRY_POLL_INTERVAL,
)
from mcp_proxy_adapter.core.delivery.terminal_replay_store import TerminalReplayStore
from mcp_proxy_adapter.core.delivery.ws_delivery_manager import WsDeliveryManager
from mcp_proxy_adapter.core.job_push.events import (
    build_delivery_push_message,
    build_push_message,
)
from mcp_proxy_adapter.core.job_push.hooks import run_job_push_handlers
from mcp_proxy_adapter.core.job_push.notifier import (
    NotificationDomain,
    configure_notifier_job_push_policy,
    register_notifier_listener,
)
from mcp_proxy_adapter.core.job_push.notifier_policy import (
    read_legacy_unified_terminal_dedup,
)
from mcp_proxy_adapter.core.job_push.subscriptions import (
    SubscriberRegistry,
    SubscriptionCategory,
)
from mcp_proxy_adapter.core.job_push.transfer_push import set_transfer_push_app
from mcp_proxy_adapter.core.logging import get_global_logger


def _make_listener(app: FastAPI) -> Callable[..., Any]:
    """Build the notifier listener that sends to registry subscribers."""

    async def listener(
        domain: NotificationDomain,
        identifier: str,
        status: str,
        result: Optional[Dict[str, Any]],
        error: Optional[str],
        progress: int,
        description: str,
    ) -> None:
        registry = getattr(app.state, "job_push_registry", None)
        if registry is None:
            return
        lookup_key = identifier.strip()
        if domain == NotificationDomain.WS_DELIVERY:
            payload = build_delivery_push_message(
                ws_id=lookup_key,
                status=status,
                result=result,
                error=error,
                progress=progress,
                description=description,
            )
            subscription_category = SubscriptionCategory.WS_DELIVERY
        elif domain == NotificationDomain.QUEUE_JOB:
            payload = build_push_message(
                job_id=lookup_key,
                status=status,
                result=result,
                error=error,
                progress=progress,
                description=description,
            )
            subscription_category = SubscriptionCategory.QUEUE_JOB
        else:
            # Defensive programming: handle unknown domains
            # (mypy knows this is unreachable for current enum, but keep for future-proofing)
            get_global_logger().warning(  # type: ignore[unreachable]
                "Job push: unknown notification domain %s", domain
            )
            return
        final = await run_job_push_handlers(lookup_key, payload)
        if final is None:
            return
        subscribers = await registry.get_subscribers(lookup_key, subscription_category)
        logger = get_global_logger()
        for sub in subscribers:
            try:
                await sub.send(final)
            except Exception as e:
                logger.warning(
                    "Job push send failed for identifier=%s: %s", lookup_key, e
                )

    return listener


async def _job_push_poll_loop(app: FastAPI) -> None:
    """Poll subscribed job_ids; on terminal state call notify_job_state_changed."""
    from mcp_proxy_adapter.integrations.queuemgr_integration import (
        get_global_queue_manager,
        QueueJobStatus,
    )

    logger = get_global_logger()
    last_status: Dict[str, str] = {}
    last_progress_sig: Dict[str, tuple[Any, ...]] = {}
    while True:
        await asyncio.sleep(JOB_PUSH_POLL_INTERVAL)
        registry = getattr(app.state, "job_push_registry", None)
        if registry is None:
            break
        try:
            queue_manager = await get_global_queue_manager()
        except Exception:
            continue
        job_ids = await registry.get_subscribed_keys_for_queue_poll()
        if not job_ids:
            continue
        for job_id in job_ids:
            try:
                job_result = await queue_manager.get_job_status(job_id)
            except Exception:
                continue
            status = job_result.status
            if status in (
                QueueJobStatus.COMPLETED,
                QueueJobStatus.FAILED,
                QueueJobStatus.STOPPED,
            ):
                prev = last_status.get(job_id)
                if prev == status:
                    continue
                last_status[job_id] = status
                from mcp_proxy_adapter.core.job_push.notifier import (
                    notify_job_state_changed,
                )

                await notify_job_state_changed(
                    NotificationDomain.QUEUE_JOB,
                    job_id,
                    status=status,
                    result=job_result.result,
                    error=job_result.error,
                    progress=job_result.progress,
                    description=job_result.description or "",
                )
                manager = getattr(app.state, "ws_delivery_manager", None)
                if manager is not None:
                    resolved_ws_id = await manager.resolve_ws_id_for_job(job_id)
                    if resolved_ws_id is not None:
                        delivery_payload = build_push_message(
                            job_id=job_id,
                            status=status,
                            result=job_result.result,
                            error=job_result.error,
                            progress=job_result.progress,
                            description=job_result.description or "",
                        )
                        await manager.notify_terminal_delivery(
                            resolved_ws_id, delivery_payload
                        )
                        await manager.clear_queue_job_mapping(job_id)
            else:
                step = getattr(job_result, "step", "") or ""
                desc = job_result.description or ""
                sig = (job_result.progress, desc, step)
                if last_progress_sig.get(job_id) != sig:
                    last_progress_sig[job_id] = sig
                    from mcp_proxy_adapter.core.job_push.events import (
                        EVENT_JOB_PROCESSING_STEP,
                    )
                    from mcp_proxy_adapter.core.job_push.queue_custom_push import (
                        job_processing_step_should_emit,
                        push_queue_job_ws_payload,
                    )

                    if job_processing_step_should_emit(job_id) and (
                        job_result.progress > 0 or desc.strip() or step
                    ):
                        await push_queue_job_ws_payload(
                            {
                                "event": EVENT_JOB_PROCESSING_STEP,
                                "job_id": job_id,
                                "step": step or "progress",
                                "message": desc,
                                "progress_percent": job_result.progress,
                            }
                        )
                last_status[job_id] = status
        await asyncio.sleep(0)

    logger.debug("Job push poll loop stopped")


async def _transfer_session_expiry_loop(app: FastAPI) -> None:
    """Periodically expire TTL-eligible transfer sessions and push ``transfer_expired``."""
    logger = get_global_logger()
    while True:
        await asyncio.sleep(TRANSFER_SESSION_EXPIRY_POLL_INTERVAL)
        registry = getattr(app.state, "job_push_registry", None)
        if registry is None:
            break
        try:
            from mcp_proxy_adapter.api.handlers import (
                expire_transfer_sessions_and_notify,
            )

            await expire_transfer_sessions_and_notify()
        except Exception as e:
            logger.warning("Transfer session expiry tick failed: %s", e)


async def startup_job_push(
    app: FastAPI, current_config: Optional[Dict[str, Any]] = None
) -> None:
    """
    Create SubscriberRegistry, set app.state.job_push_registry, register listener, start poll loop.

    WS is always on; no config. Call from lifespan startup after queue_manager is initialized.
    """
    logger = get_global_logger()
    # Configure notifier policy from config
    raw_jp = current_config.get("job_push") if current_config else None
    jp_dict = raw_jp if isinstance(raw_jp, dict) else None
    legacy = read_legacy_unified_terminal_dedup(jp_dict)
    configure_notifier_job_push_policy(legacy_unified_terminal_dedup=legacy)
    registry = SubscriberRegistry()
    app.state.job_push_registry = registry
    set_transfer_push_app(app)
    ttl: Optional[float] = None
    if current_config is not None and isinstance(
        current_config.get("delivery_replay_ttl_seconds"), (int, float)
    ):
        ttl = float(current_config["delivery_replay_ttl_seconds"])
    replay = (
        TerminalReplayStore() if ttl is None else TerminalReplayStore(ttl_seconds=ttl)
    )
    manager = WsDeliveryManager(registry, replay)
    app.state.ws_delivery_manager = manager
    listener = _make_listener(app)
    register_notifier_listener(listener)
    task = asyncio.create_task(_job_push_poll_loop(app))
    app.state._job_push_poll_task = task
    expiry_task = asyncio.create_task(_transfer_session_expiry_loop(app))
    app.state._transfer_session_expiry_task = expiry_task
    logger.info("Job push: registry and poll loop started")


async def shutdown_job_push(app: FastAPI) -> None:
    """Cancel poll loop task. Call from lifespan shutdown."""
    expiry_task = getattr(app.state, "_transfer_session_expiry_task", None)
    if expiry_task is not None and not expiry_task.done():
        expiry_task.cancel()
        try:
            await expiry_task
        except asyncio.CancelledError:
            pass
        get_global_logger().info("Transfer session expiry loop stopped")
    task = getattr(app.state, "_job_push_poll_task", None)
    if task is not None and not task.done():
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            pass
        get_global_logger().info("Job push poll loop stopped")
    set_transfer_push_app(None)
