from __future__ import annotations

import threading
import time
import traceback
from pathlib import Path
from typing import Any, Callable

from . import error_logging


WatchCallback = Callable[[dict[str, Any]], None]


def wait_until_file_is_stable(path: str | Path, timeout: float = 3.0, interval: float = 0.12) -> bool:
    target = Path(path)
    deadline = time.monotonic() + max(0.1, float(timeout))
    previous: tuple[int, int] | None = None
    stable_count = 0
    while time.monotonic() < deadline:
        try:
            stat = target.stat()
            current = (int(stat.st_size), int(stat.st_mtime_ns))
        except OSError:
            current = None
        if current and current == previous:
            stable_count += 1
            if stable_count >= 2:
                return True
        else:
            stable_count = 0
            previous = current
        time.sleep(max(0.03, float(interval)))
    return False


class PngFileWatcher:
    def __init__(self, debounce_ms: int = 700, poll_interval: float = 0.25) -> None:
        self.debounce_ms = int(debounce_ms)
        self.poll_interval = float(poll_interval)
        self._lock = threading.RLock()
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None
        self._callback: WatchCallback | None = None
        self._path: Path | None = None
        self._allowed_names: set[str] = set()
        self._running = False
        self._change_id = 0
        self._last_signature: tuple[int, int] | None = None
        self._last_signatures: dict[str, tuple[int, int]] = {}
        self._last_event_at = 0.0
        self._last_stable_at = 0.0
        self._last_error = ""
        self._errors: list[str] = []
        self._backend = "polling"
        self._last_paths: list[str] = []

    def start(self, path: str | Path, callback: WatchCallback | None = None, allowed_names: list[str] | tuple[str, ...] | set[str] | None = None) -> dict[str, Any]:
        target = Path(path).expanduser()
        allowed = {str(name).lower() for name in (allowed_names or []) if str(name).lower().endswith(".png")}
        if target.exists() and target.is_dir():
            if not allowed:
                raise ValueError("Folder watching requires at least one allowed .png filename")
        elif target.suffix.lower() != ".png":
            raise ValueError("Only .png files can be watched")
        elif target.exists() and not target.is_file():
            raise FileNotFoundError(f"PNG path is not a file: {target}")

        self.stop()
        with self._lock:
            self._path = target
            self._allowed_names = allowed
            self._callback = callback
            self._stop_event = threading.Event()
            self._running = True
            self._last_error = ""
            self._errors = []
            self._last_paths = []
            self._last_signature = self._signature(target)
            self._last_signatures = self._folder_signatures(target) if target.is_dir() else {}
            self._thread = threading.Thread(target=self._run, name="LiveryLabPngWatcher", daemon=True)
            self._thread.start()
        return self.get_status()

    def stop(self) -> dict[str, Any]:
        thread: threading.Thread | None = None
        with self._lock:
            if self._thread and self._thread.is_alive():
                self._stop_event.set()
                thread = self._thread
            self._running = False
            self._thread = None
            self._callback = None
            self._allowed_names = set()
        if thread:
            thread.join(timeout=1.0)
        return self.get_status()

    def is_running(self) -> bool:
        with self._lock:
            return bool(self._running and self._thread and self._thread.is_alive())

    def get_status(self) -> dict[str, Any]:
        with self._lock:
            path = self._path
            exists = bool(path and path.exists())
            return {
                "running": self.is_running(),
                "backend": self._backend,
                "path": str(path) if path else "",
                "allowed_names": sorted(self._allowed_names),
                "last_paths": list(self._last_paths),
                "exists": exists,
                "change_id": int(self._change_id),
                "last_event_at": float(self._last_event_at),
                "last_stable_at": float(self._last_stable_at),
                "last_error": self._last_error,
                "errors": list(self._errors[-5:]),
                "debounce_ms": int(self.debounce_ms),
            }

    def _signature(self, path: Path) -> tuple[int, int] | None:
        try:
            stat = path.stat()
            return int(stat.st_size), int(stat.st_mtime_ns)
        except OSError:
            return None

    def _watched_files(self, folder: Path) -> list[Path]:
        names = sorted(self._allowed_names)
        return [folder / name for name in names]

    def _folder_signatures(self, folder: Path) -> dict[str, tuple[int, int]]:
        signatures: dict[str, tuple[int, int]] = {}
        if not folder.exists() or not folder.is_dir():
            return signatures
        for path in self._watched_files(folder):
            signature = self._signature(path)
            if signature:
                signatures[str(path)] = signature
        return signatures

    def _record_error(self, exc: BaseException) -> None:
        text = "".join(traceback.format_exception_only(type(exc), exc)).strip()
        error_logging.log_exception(
            exc,
            component="file_watcher",
            operation="watch_png",
            context={"path": self._path or "", "thread": threading.current_thread().name},
        )
        with self._lock:
            self._last_error = text
            self._errors.append(text)
            self._errors = self._errors[-10:]

    def _notify(self, path: Path, signature: tuple[int, int]) -> None:
        event = {
            "path": str(path),
            "paths": [str(path)],
            "change_id": self._change_id,
            "size": signature[0],
            "mtime_ns": signature[1],
            "stable_at": self._last_stable_at,
        }
        callback = self._callback
        if callback:
            try:
                callback(event)
            except Exception as exc:
                self._record_error(exc)

    def _run(self) -> None:
        pending_signature: tuple[int, int] | None = None
        pending_since = 0.0
        while not self._stop_event.is_set():
            try:
                path = self._path
                if not path:
                    break
                if path.exists() and path.is_dir():
                    self._run_folder_tick(path)
                    self._stop_event.wait(self.poll_interval)
                    continue
                signature = self._signature(path)
                now = time.monotonic()
                if signature and signature != self._last_signature and signature != pending_signature:
                    pending_signature = signature
                    pending_since = now
                    with self._lock:
                        self._last_event_at = time.time()

                if pending_signature and (now - pending_since) * 1000 >= self.debounce_ms:
                    if wait_until_file_is_stable(path, timeout=3.0):
                        stable_signature = self._signature(path)
                        if stable_signature and stable_signature != self._last_signature:
                            with self._lock:
                                self._last_signature = stable_signature
                                self._change_id += 1
                                self._last_stable_at = time.time()
                                self._last_error = ""
                            self._notify(path, stable_signature)
                        pending_signature = None
                    elif now - pending_since > 3.5:
                        pending_signature = None
                self._stop_event.wait(self.poll_interval)
            except Exception as exc:
                self._record_error(exc)
                self._stop_event.wait(1.0)
        with self._lock:
            self._running = False

    def _run_folder_tick(self, folder: Path) -> None:
        current = self._folder_signatures(folder)
        now = time.monotonic()
        changed = {path: signature for path, signature in current.items() if self._last_signatures.get(path) != signature}
        if not changed:
            return
        with self._lock:
            self._last_event_at = time.time()
        deadline = now + (self.debounce_ms / 1000.0)
        while time.monotonic() < deadline and not self._stop_event.is_set():
            self._stop_event.wait(min(0.05, max(0.0, deadline - time.monotonic())))
        stable: dict[str, tuple[int, int]] = {}
        for path_text in sorted(changed):
            path = Path(path_text)
            if wait_until_file_is_stable(path, timeout=3.0):
                signature = self._signature(path)
                if signature and self._last_signatures.get(path_text) != signature:
                    stable[path_text] = signature
        if not stable:
            return
        with self._lock:
            self._last_signatures.update(stable)
            self._change_id += 1
            self._last_stable_at = time.time()
            self._last_error = ""
            self._last_paths = sorted(stable)
        callback = self._callback
        if callback:
            try:
                callback({"paths": sorted(stable), "path": sorted(stable)[0], "change_id": self._change_id, "stable_at": self._last_stable_at})
            except Exception as exc:
                self._record_error(exc)
