"""Configuration hot-reload with watchfiles."""

from __future__ import annotations

import asyncio
import contextlib
from collections.abc import Callable, Coroutine
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import structlog
from watchfiles import awatch

from curadise_agent.config.loader import load_config
from curadise_agent.config.schema import AgentConfig
from curadise_agent.errors import ConfigurationError

log = structlog.get_logger(__name__)

ConfigChangeCallback = Callable[[AgentConfig, AgentConfig], Coroutine[Any, Any, None]]


@dataclass
class ConfigWatcher:
    """
    Watches configuration file for changes and triggers reloads.

    Uses watchfiles for efficient file system monitoring.
    """

    config_path: Path
    on_change: ConfigChangeCallback | None = None
    debounce_ms: int = 500
    _current_config: AgentConfig | None = field(default=None, init=False)
    _running: bool = field(default=False, init=False)
    _task: asyncio.Task[None] | None = field(default=None, init=False)

    @property
    def is_running(self) -> bool:
        """Check if watcher is running."""
        return self._running

    @property
    def current_config(self) -> AgentConfig | None:
        """Get current loaded configuration."""
        return self._current_config

    async def start(self, initial_config: AgentConfig) -> None:
        """
        Start watching for configuration changes.

        Args:
            initial_config: The initial configuration
        """
        if self._running:
            return

        self._current_config = initial_config
        self._running = True

        self._task = asyncio.create_task(
            self._watch_loop(),
            name="config-watcher",
        )

        log.info("config_watcher_started", path=str(self.config_path))

    async def stop(self) -> None:
        """Stop watching for changes."""
        if not self._running:
            return

        self._running = False

        if self._task:
            self._task.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await self._task
            self._task = None

        log.info("config_watcher_stopped")

    async def _watch_loop(self) -> None:
        """Main file watching loop."""
        try:
            async for changes in awatch(
                self.config_path,
                debounce=self.debounce_ms,
                stop_event=asyncio.Event(),  # Will be set when task is cancelled
            ):
                if not self._running:
                    break

                for change_type, path in changes:
                    if Path(path) == self.config_path:
                        log.info("config_file_changed", change_type=change_type.name)
                        await self._reload_config()

        except asyncio.CancelledError:
            pass
        except Exception as e:
            log.error("config_watcher_error", error=str(e))

    async def _reload_config(self) -> None:
        """Reload configuration from file."""
        try:
            new_config = load_config(config_path=self.config_path)

            if self._current_config and self.on_change:
                # Check if config actually changed
                if new_config != self._current_config:
                    old_config = self._current_config
                    self._current_config = new_config

                    log.info("config_reloaded")

                    try:
                        await self.on_change(old_config, new_config)
                    except Exception as e:
                        log.error("config_change_callback_failed", error=str(e))
                        # Rollback
                        self._current_config = old_config
                else:
                    log.debug("config_unchanged")
            else:
                self._current_config = new_config
                log.info("config_loaded")

        except ConfigurationError as e:
            log.error("config_reload_failed", error=e.message)
            # Keep using old config
        except Exception as e:
            log.error("config_reload_error", error=str(e))

    async def reload(self) -> AgentConfig | None:
        """
        Manually trigger a configuration reload.

        Returns:
            New configuration if successful, None otherwise
        """
        await self._reload_config()
        return self._current_config


def get_config_diff(
    old_config: AgentConfig,
    new_config: AgentConfig,
) -> dict[str, Any]:
    """
    Get the differences between two configurations.

    Args:
        old_config: Previous configuration
        new_config: New configuration

    Returns:
        Dictionary of changed sections and their new values
    """
    old_dict = old_config.model_dump()
    new_dict = new_config.model_dump()

    diff: dict[str, Any] = {}

    for key in new_dict:
        if key not in old_dict:
            diff[key] = {"added": new_dict[key]}
        elif old_dict[key] != new_dict[key]:
            diff[key] = {
                "old": old_dict[key],
                "new": new_dict[key],
            }

    for key in old_dict:
        if key not in new_dict:
            diff[key] = {"removed": old_dict[key]}

    return diff


def can_hot_reload(
    old_config: AgentConfig,
    new_config: AgentConfig,
) -> tuple[bool, list[str]]:
    """
    Check if configuration change can be applied without restart.

    Some changes require a full agent restart.

    Args:
        old_config: Previous configuration
        new_config: New configuration

    Returns:
        Tuple of (can_reload, list of reasons if cannot)
    """
    reasons: list[str] = []

    # Server URL change requires restart
    if old_config.server.url != new_config.server.url:
        reasons.append("Server URL changed")

    # mTLS changes require restart
    if old_config.auth.mtls_cert != new_config.auth.mtls_cert:
        reasons.append("mTLS certificate changed")
    if old_config.auth.mtls_key != new_config.auth.mtls_key:
        reasons.append("mTLS key changed")

    # State directory change requires restart
    if old_config.state.directory != new_config.state.directory:
        reasons.append("State directory changed")

    return len(reasons) == 0, reasons
