"""Hot-reload support for Logos ADK agents."""
from __future__ import annotations

import os
import threading
import time
from typing import Any, Callable

from logos_adk.errors import ConfigError


class ConfigWatcher:
    """Watches YAML config files and reloads agents when they change."""

    def __init__(
        self,
        directory: str,
        *,
        interval: float = 2.0,
        on_reload: Callable[[Any, str], None] | None = None,
        on_error: Callable[[Any, str, Exception], None] | None = None,
    ) -> None:
        self._directory = directory
        self._interval = interval
        self._on_reload = on_reload
        self._on_error = on_error
        self._watched: dict[str, tuple[float, Any]] = {}
        self._watched_lock = threading.Lock()
        self._running = False
        self._thread: threading.Thread | None = None
        self._loop: Any | None = None

    def register(self, agent: Any) -> None:
        """Register an agent for config watching.

        Raises:
            ConfigError: If the agent has no config path.
        """
        path = getattr(agent, "_config_path", None)
        if path is None:
            raise ConfigError(
                "Cannot watch agent: agent has no config path. "
                "Use Agent.from_config() to create the agent."
            )
        mtime = os.stat(path).st_mtime
        with self._watched_lock:
            self._watched[path] = (mtime, agent)

    def start(self, loop: Any | None = None) -> None:
        """Start watching for config changes in a background thread.

        Args:
            loop: Event loop for scheduling reloads via call_soon_threadsafe.
                  If None, reload() is called directly.
        """
        if self._running:
            return
        self._loop = loop
        self._running = True
        self._thread = threading.Thread(target=self._poll_loop, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        """Stop the watcher."""
        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=self._interval * 2)
            self._thread = None

    def _poll_loop(self) -> None:
        """Background polling loop."""
        while self._running:
            time.sleep(self._interval)
            if not self._running:
                break
            with self._watched_lock:
                watched_snapshot = list(self._watched.items())
            for path, (old_mtime, agent) in watched_snapshot:
                try:
                    current_mtime = os.stat(path).st_mtime
                except OSError:
                    continue

                if current_mtime != old_mtime:
                    try:
                        if self._loop is not None:
                            self._loop.call_soon_threadsafe(agent.reload)
                        else:
                            agent.reload()
                        with self._watched_lock:
                            self._watched[path] = (current_mtime, agent)
                        if self._on_reload is not None:
                            self._on_reload(agent, path)
                    except Exception as exc:
                        with self._watched_lock:
                            self._watched[path] = (current_mtime, agent)
                        if self._on_error is not None:
                            self._on_error(agent, path, exc)
