"""Optional YAML policy file watcher for hot-reload on change.

Watches a directory (or single file) for changes and reloads policies
when YAML files are modified. Uses a polling approach that works everywhere
without external dependencies.

Example:
    rp = PyAuthz()
    watcher = PolicyWatcher(rp, "policies/", interval=2.0)
    watcher.start()   # Starts a background daemon thread
    # ... policies auto-reload on change ...
    watcher.stop()
"""

from __future__ import annotations

import hashlib
import logging
import threading
from pathlib import Path

logger = logging.getLogger("pyauthz.watcher")


class PolicyWatcher:
    """Watches YAML policy files and reloads on change.

    Uses file content hashing to detect changes. Runs a daemon thread that
    polls at a configurable interval.

    Args:
        engine: The PyAuthz engine instance to reload policies into.
        path: Path to a YAML file or directory of YAML files.
        interval: Polling interval in seconds (default 2.0).
    """

    def __init__(self, engine: object, path: str, *, interval: float = 2.0) -> None:
        self._engine = engine
        self._path = Path(path)
        self._interval = interval
        self._thread: threading.Thread | None = None
        self._stop_event = threading.Event()
        self._hashes: dict[str, str] = {}
        # Snapshot initial state
        self._hashes = self._compute_hashes()

    def start(self) -> None:
        """Start watching for file changes in a background daemon thread."""
        if self._thread is not None and self._thread.is_alive():
            return
        self._stop_event.clear()
        self._thread = threading.Thread(
            target=self._poll_loop, daemon=True, name="pyauthz-watcher"
        )
        self._thread.start()
        logger.info("Policy watcher started for '%s' (interval=%.1fs)", self._path, self._interval)

    def stop(self) -> None:
        """Stop watching for file changes."""
        self._stop_event.set()
        if self._thread is not None:
            self._thread.join(timeout=self._interval * 2)
            self._thread = None
        logger.info("Policy watcher stopped")

    @property
    def is_running(self) -> bool:
        """Whether the watcher thread is currently running."""
        return self._thread is not None and self._thread.is_alive()

    def _poll_loop(self) -> None:
        """Main polling loop — runs in background thread."""
        while not self._stop_event.is_set():
            self._stop_event.wait(self._interval)
            if self._stop_event.is_set():
                break
            try:
                self._check_and_reload()
            except Exception:
                logger.exception("Error during policy reload")

    def _check_and_reload(self) -> None:
        """Check for file changes and reload if needed."""
        new_hashes = self._compute_hashes()
        if new_hashes != self._hashes:
            logger.info("Policy file changes detected, reloading...")
            self._hashes = new_hashes
            self._reload()

    def _reload(self) -> None:
        """Reload all policies from the watched path.

        Uses load_policies on the engine, which handles the full YAML
        parsing and validation pipeline. On error, logs the exception
        and keeps the previous policy set.
        """
        from pyauthz.storage.yaml_file import load_yaml_policies

        try:
            roles, policies = load_yaml_policies(str(self._path))
        except Exception:
            logger.exception(
                "Failed to parse policy files during reload, keeping previous policies"
            )
            return

        try:
            self._engine.replace_roles(roles)  # type: ignore[attr-defined]
            self._engine.replace_policies(policies)  # type: ignore[attr-defined]
            logger.info(
                "Policies reloaded successfully (%d roles, %d policies)", len(roles), len(policies)
            )
        except Exception:
            logger.exception("Failed to apply reloaded policies, keeping previous state")

    def _compute_hashes(self) -> dict[str, str]:
        """Compute content hashes for all watched YAML files."""
        hashes: dict[str, str] = {}
        if self._path.is_dir():
            files = sorted(self._path.glob("*.yaml")) + sorted(self._path.glob("*.yml"))
        elif self._path.is_file():
            files = [self._path]
        else:
            return hashes

        for f in files:
            try:
                content = f.read_bytes()
                hashes[str(f)] = hashlib.md5(content).hexdigest()  # noqa: S324
            except OSError:
                continue

        return hashes
