"""watchdog 文件监听 — 自动增量重索引

监听文件 create/modify/delete/rename 事件，500ms debounce 后触发索引。
"""

from __future__ import annotations

import logging
import threading
import time
from pathlib import Path

from watchdog.events import FileSystemEventHandler, FileSystemEvent
from watchdog.observers import Observer

from .indexer import INDEXABLE_EXTENSIONS, SKIP_DIRS

logger = logging.getLogger(__name__)

DEBOUNCE_SECONDS = 0.5


class _DebouncedHandler(FileSystemEventHandler):
    """去抖文件事件处理器"""

    def __init__(self, reindex_callback, delete_callback, base_dir: Path):
        super().__init__()
        self._reindex = reindex_callback
        self._delete = delete_callback
        self._base_dir = base_dir
        self._pending: dict[str, tuple[str, float]] = {}  # path → (action, ts)
        self._lock = threading.Lock()
        self._timer: threading.Timer | None = None

    def _should_handle(self, path: str) -> bool:
        p = Path(path)
        if p.suffix.lower() not in INDEXABLE_EXTENSIONS:
            return False
        try:
            rel = p.relative_to(self._base_dir)
            if any(part in SKIP_DIRS for part in rel.parts[:-1]):
                return False
        except ValueError:
            pass
        return True

    def on_created(self, event: FileSystemEvent) -> None:
        if not event.is_directory and self._should_handle(event.src_path):
            self._schedule(event.src_path, "reindex")

    def on_modified(self, event: FileSystemEvent) -> None:
        if not event.is_directory and self._should_handle(event.src_path):
            self._schedule(event.src_path, "reindex")

    def on_deleted(self, event: FileSystemEvent) -> None:
        if not event.is_directory and self._should_handle(event.src_path):
            self._schedule(event.src_path, "delete")

    def on_moved(self, event: FileSystemEvent) -> None:
        if event.is_directory:
            return
        if self._should_handle(event.src_path):
            self._schedule(event.src_path, "delete")
        if hasattr(event, "dest_path") and self._should_handle(event.dest_path):
            self._schedule(event.dest_path, "reindex")

    def _schedule(self, path: str, action: str) -> None:
        with self._lock:
            self._pending[path] = (action, time.monotonic())
            if self._timer is not None:
                self._timer.cancel()
            self._timer = threading.Timer(DEBOUNCE_SECONDS, self._flush)
            self._timer.daemon = True
            self._timer.start()

    # 单次 flush 最多处理的文件数，防止 git checkout 等大批量事件阻塞
    _MAX_FLUSH_BATCH = 50

    def _flush(self) -> None:
        with self._lock:
            batch = dict(self._pending)
            self._pending.clear()
            self._timer = None

        items = list(batch.items())
        total = len(items)

        if total > self._MAX_FLUSH_BATCH:
            n_batches = (total + self._MAX_FLUSH_BATCH - 1) // self._MAX_FLUSH_BATCH
            logger.warning(
                "检测到大批量文件变更（%d 个），将分 %d 批处理，预计耗时较长，请耐心等待",
                total, n_batches,
            )

        # 分批处理，每批之间让出 CPU 防止阻塞
        for i in range(0, total, self._MAX_FLUSH_BATCH):
            chunk = items[i : i + self._MAX_FLUSH_BATCH]
            if i > 0:
                logger.info("处理第 %d/%d 批...", i // self._MAX_FLUSH_BATCH + 1,
                            (total + self._MAX_FLUSH_BATCH - 1) // self._MAX_FLUSH_BATCH)
            for path, (action, _ts) in chunk:
                try:
                    if action == "delete":
                        try:
                            rel_path = str(Path(path).relative_to(self._base_dir))
                        except ValueError:
                            logger.debug("文件不在监听目录内: %s", path)
                            rel_path = path
                        self._delete(rel_path)
                        logger.info("已删除索引: %s", rel_path)
                    elif action == "reindex":
                        self._reindex(path)
                        logger.info("已重索引: %s", path)
                except Exception as e:
                    logger.warning("处理文件事件失败 %s: %s", path, e)


class FileWatcher:
    """文件监听器，后台线程运行，带崩溃自动重启"""

    _HEALTH_CHECK_INTERVAL = 30  # 健康检查间隔（秒）
    _MAX_RESTART_BACKOFF = 300   # 最大重启退避（秒）

    def __init__(
        self,
        paths: list[str | Path],
        reindex_callback,
        delete_callback,
        base_dir: Path | None = None,
    ):
        self._paths = [Path(p).resolve() for p in paths]
        self._base_dir = base_dir or (self._paths[0] if self._paths else Path.cwd())
        self._reindex_callback = reindex_callback
        self._delete_callback = delete_callback
        self._handler = _DebouncedHandler(
            reindex_callback=reindex_callback,
            delete_callback=delete_callback,
            base_dir=self._base_dir,
        )
        self._observer = Observer()
        self._stopped = False
        self._restart_count = 0
        self._health_timer: threading.Timer | None = None

    def _schedule_watches(self, observer: Observer) -> None:
        """给 observer 注册监听路径"""
        for path in self._paths:
            try:
                if path.is_dir():
                    observer.schedule(self._handler, str(path), recursive=True)
                elif path.is_file():
                    observer.schedule(self._handler, str(path.parent), recursive=False)
            except OSError as e:
                logger.warning("无法监听 %s: %s", path, e)

    def start(self) -> None:
        """启动文件监听（后台线程）+ 健康检查"""
        self._stopped = False
        self._schedule_watches(self._observer)
        self._observer.daemon = True
        self._observer.start()
        logger.info("文件监听已启动: %s", ", ".join(str(p) for p in self._paths))
        self._start_health_check()

    def _start_health_check(self) -> None:
        """启动定期健康检查"""
        if self._stopped:
            return
        self._health_timer = threading.Timer(
            self._HEALTH_CHECK_INTERVAL, self._check_health,
        )
        self._health_timer.daemon = True
        self._health_timer.start()

    def _check_health(self) -> None:
        """检查 Observer 是否存活，崩溃则自动重启"""
        if self._stopped:
            return
        if not self._observer.is_alive():
            backoff = min(
                2 ** min(self._restart_count, 10),  # 限制指数上限防溢出
                self._MAX_RESTART_BACKOFF,
            )
            self._restart_count += 1
            logger.warning(
                "文件监听已崩溃，%.0fs 后尝试第 %d 次重启...",
                backoff, self._restart_count,
            )
            # 可中断的 sleep：每秒检查一次 _stopped 标志
            for _ in range(int(backoff)):
                if self._stopped:
                    return
                time.sleep(1)
            if self._stopped:
                return
            try:
                self._observer = Observer()
                self._schedule_watches(self._observer)
                self._observer.daemon = True
                self._observer.start()
                if self._observer.is_alive():
                    logger.info("文件监听已重启（第 %d 次）", self._restart_count)
                    self._restart_count = 0  # 确认存活后才重置
                else:
                    logger.error("Observer.start() 未报错但未运行")
            except Exception as e:
                logger.error("文件监听重启失败: %s", e)
        self._start_health_check()

    def stop(self) -> None:
        """停止监听"""
        self._stopped = True
        if self._health_timer:
            self._health_timer.cancel()
            self._health_timer = None
        self._observer.stop()
        self._observer.join(timeout=5)
        logger.info("文件监听已停止")

    @property
    def is_alive(self) -> bool:
        return self._observer.is_alive()
