"""
Orchestration polling lifecycle (TZ 8.1, 8.3): start polling after base init,
initial poll before ready, graceful stop on shutdown.

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com

Purpose: Provides lifespan hooks for discovery polling: on startup (after base
networking init) run initial poll (block until done or timeout), then start
background thread that runs polling_loop with configured interval; on shutdown
signal stop and wait for thread to exit (no partial swap). If discovery is
disabled, no polling is started. Integration: register startup/shutdown
callables from this module inside the framework lifespan (e.g. in
lifespan_manager.create_lifespan after heartbeat/queue init, before yield;
and during shutdown after registration stop).
"""

from __future__ import annotations

import asyncio
import logging
import threading
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Sequence
from urllib.parse import urlparse

from mcp_proxy_adapter.core.orchestration.polling_loop import (
    run_one_cycle,
    run_polling_loop,
)
from mcp_proxy_adapter.core.orchestration.snapshot_holder import SnapshotHolder

logger = logging.getLogger(__name__)


@dataclass
class _PollingConfigAdapter:
    """
    Adapter for polling_loop _PollingConfig protocol.

    Built from raw config dict to avoid importing orchestration_section
    (circular import with simple_config).
    """

    discovery_enabled: bool
    polling_interval: float
    initial_poll_timeout: float
    static_servers: Optional[Sequence[Any]]


# Module-level state for the polling thread and stop event; set by startup,
# cleared by shutdown. Used so shutdown can signal stop and join the thread.
_polling_thread: Optional[threading.Thread] = None
_stop_event: Optional[threading.Event] = None


def _proxy_base_url_from_config(config: Dict[str, Any]) -> Optional[str]:
    """
    Derive proxy base URL from config (registration section).

    Uses registration.register_url if present; strips path to get base
    (e.g. http://localhost:3005/register -> http://localhost:3005).
    Returns None if no usable URL.
    """
    reg = config.get("registration") or {}
    url = reg.get("register_url") or reg.get("proxy_url")
    if not url or not isinstance(url, str):
        return None
    url = url.strip()
    if not url:
        return None
    parsed = urlparse(url)
    if not parsed.scheme or not parsed.netloc:
        return None
    return f"{parsed.scheme}://{parsed.netloc}"


def _get_orchestration_config(
    config: Optional[Dict[str, Any]],
) -> _PollingConfigAdapter:
    """
    Parse orchestration section from config for use by polling_loop.

    Returns an object compatible with polling_loop _PollingConfig protocol
    (discovery_enabled, polling_interval, initial_poll_timeout, static_servers).
    """
    raw = (config or {}).get("orchestration")
    if not isinstance(raw, dict):
        raw = {}
    discovery_enabled = bool(raw.get("discovery_enabled", False))
    polling_interval = float(raw.get("polling_interval", 30.0))
    initial_poll_timeout = float(raw.get("initial_poll_timeout", 60.0))
    static_raw = raw.get("static_servers")
    static_servers: Optional[List[Any]] = (
        list(static_raw) if isinstance(static_raw, list) else None
    )
    return _PollingConfigAdapter(
        discovery_enabled=discovery_enabled,
        polling_interval=polling_interval,
        initial_poll_timeout=initial_poll_timeout,
        static_servers=static_servers,
    )


async def startup_orchestration_polling(
    current_config: Optional[Dict[str, Any]],
    snapshot_holder: SnapshotHolder,
) -> None:
    """
    Start orchestration discovery polling (TZ 8.1).

    If discovery is disabled in config, no-op. Otherwise: (1) run one initial
    poll (block until done or initial_poll_timeout); (2) start background
    thread that runs polling_loop with polling_interval until stop is requested.
    Uses discovery_enabled, polling_interval, initial_poll_timeout and
    proxy URL from config; no hardcoded timeouts.

    Args:
        current_config: Application config dict (orchestration and registration).
        snapshot_holder: Holder for catalog and tool context snapshots.
    """
    global _polling_thread, _stop_event
    if _polling_thread is not None:
        logger.warning("Orchestration polling already started; skipping")
        return

    orch_config = _get_orchestration_config(current_config)
    if not orch_config.discovery_enabled:
        logger.info("Orchestration discovery disabled; not starting polling")
        return

    proxy_url = _proxy_base_url_from_config(current_config or {})
    if not proxy_url:
        logger.warning(
            "Orchestration discovery enabled but no proxy URL in config; skipping polling"
        )
        return

    initial_poll_timeout = float(orch_config.initial_poll_timeout)
    logger.info(
        "Orchestration polling: running initial poll (timeout=%s s)",
        initial_poll_timeout,
    )
    try:
        await asyncio.wait_for(
            run_one_cycle(proxy_url, orch_config, snapshot_holder),
            timeout=initial_poll_timeout,
        )
    except asyncio.TimeoutError:
        logger.warning(
            "Initial orchestration poll timed out after %s s; continuing (snapshot may be empty)",
            initial_poll_timeout,
        )
    except Exception as exc:
        logger.warning("Initial orchestration poll failed: %s; continuing", exc)

    _stop_event = threading.Event()
    _polling_thread = threading.Thread(
        target=run_polling_loop,
        name="orchestration-polling",
        args=(proxy_url, orch_config, snapshot_holder, _stop_event),
        daemon=False,
    )
    _polling_thread.start()
    logger.info(
        "Orchestration polling thread started (interval=%s s)",
        orch_config.polling_interval,
    )


async def shutdown_orchestration_polling() -> None:
    """
    Stop orchestration polling thread gracefully (TZ 8.3).

    Sets stop event and waits for the polling thread to exit. No partial
    swap: the loop checks stop flag and exits without swapping after stop.
    """
    global _polling_thread, _stop_event
    if _polling_thread is None or _stop_event is None:
        return
    logger.info("Stopping orchestration polling thread...")
    _stop_event.set()
    _polling_thread.join(timeout=30.0)
    if _polling_thread.is_alive():
        logger.warning("Orchestration polling thread did not exit within 30s")
    _polling_thread = None
    _stop_event = None
    logger.info("Orchestration polling stopped")


# Integration with framework lifespan (lifespan_manager):
# 1. Create or obtain a SnapshotHolder (e.g. app-state or module singleton)
#    that the orchestration API layer will use for reads.
# 2. In create_lifespan(), inside the lifespan async generator, after base
#    networking init (e.g. after heartbeat/queue init), call:
#    await startup_orchestration_polling(current_config, snapshot_holder)
# 3. Before yield, ensure initial poll has completed (startup does it).
# 4. In shutdown section, call: await shutdown_orchestration_polling()
#    (e.g. after registration_manager.stop(), before queue shutdown).
