
import os
import time
import threading
from collections import deque
from dataclasses import dataclass
from typing import Callable, Dict, List, Optional

try:
    from watchdog.observers import Observer
    from watchdog.events import FileSystemEventHandler
    # Guard against mocked watchdog modules that return non-types.
    if not isinstance(FileSystemEventHandler, type) or not isinstance(Observer, type):
        raise ImportError("watchdog mocked")
    HAS_WATCHDOG = True
except Exception:
    HAS_WATCHDOG = False
    # Dummy classes for safe definition
    class FileSystemEventHandler: pass
    class Observer: pass

try:
    from .queue_pipeline import FsEvent, FsEventKind
except Exception:
    from queue_pipeline import FsEvent, FsEventKind
try:
    from .workspace import WorkspaceManager
except Exception:
    from workspace import WorkspaceManager
try:
    from .constants import (
        WATCHER_DEBOUNCE_MIN_SECONDS,
        WATCHER_DEBOUNCE_MAX_SECONDS,
        WATCHER_TARGET_RPS,
        WATCHER_RATE_WINDOW_SECONDS,
        WATCHER_BUCKET_CAPACITY,
        WATCHER_BUCKET_RATE,
        WATCHER_BUCKET_FLUSH_SECONDS,
        WATCHER_GIT_DEBOUNCE_SECONDS,
        WATCHER_MONITOR_INTERVAL_SECONDS,
        WATCHER_OBSERVER_TIMEOUT_SECONDS,
        WATCHER_EVENT_QUEUE_MAXLEN,
        WATCHER_RESTART_MAX_RETRIES,
        WATCHER_RESTART_RETRY_DELAY_SECONDS,
        WATCHER_OBSERVER_JOIN_TIMEOUT_SECONDS,
        ENV_SARI_GIT_CHECKOUT_DEBOUNCE,
        ENV_SARI_WATCHER_MONITOR_SECONDS,
    )
except Exception:
    # Fallback for tests or when constants module is not available
    WATCHER_DEBOUNCE_MIN_SECONDS = 0.1
    WATCHER_DEBOUNCE_MAX_SECONDS = 1.0
    WATCHER_TARGET_RPS = 50.0
    WATCHER_RATE_WINDOW_SECONDS = 1.0
    WATCHER_BUCKET_CAPACITY = 100.0
    WATCHER_BUCKET_RATE = 50.0
    WATCHER_BUCKET_FLUSH_SECONDS = 0.1
    WATCHER_GIT_DEBOUNCE_SECONDS = 3.0
    WATCHER_MONITOR_INTERVAL_SECONDS = 10.0
    WATCHER_OBSERVER_TIMEOUT_SECONDS = 0.1
    WATCHER_EVENT_QUEUE_MAXLEN = 200
    WATCHER_RESTART_MAX_RETRIES = 3
    WATCHER_RESTART_RETRY_DELAY_SECONDS = 1.0
    WATCHER_OBSERVER_JOIN_TIMEOUT_SECONDS = 5.0
    ENV_SARI_GIT_CHECKOUT_DEBOUNCE = "SARI_GIT_CHECKOUT_DEBOUNCE"
    ENV_SARI_WATCHER_MONITOR_SECONDS = "SARI_WATCHER_MONITOR_SECONDS"

try:
    from .logging_utils import safe_log
except Exception:
    # Fallback for tests
    def safe_log(logger, level: str, message: str) -> None:
        if not logger:
            return
        method = getattr(logger, f"log_{level}", None)
        if callable(method):
            method(message)
            return
        method = getattr(logger, level, None)
        if callable(method):
            method(message)
            return
        method = getattr(logger, "log", None)
        if callable(method):
            method(message)

def _is_git_event(path: str) -> bool:
    if not path:
        return False
    if not isinstance(path, str):
        return False
    norm = path.replace("\\", "/")
    if "/.git/" in norm or norm.endswith("/.git"):
        return True
    base = os.path.basename(norm)
    return base in {"HEAD", "index", "packed-refs", "ORIG_HEAD", "FETCH_HEAD"}


class DebouncedEventHandler(FileSystemEventHandler):
    """Handles events with debounce to prevent duplicate indexing on save."""
    def __init__(
        self,
        callback: Callable[[str], None],
        debounce_seconds: float = WATCHER_DEBOUNCE_MIN_SECONDS,
        logger=None,
        git_callback: Optional[Callable[[str], None]] = None,
        git_debounce_seconds: float = WATCHER_GIT_DEBOUNCE_SECONDS,
    ):
        self.callback = callback
        self.debounce_seconds = debounce_seconds
        self.logger = logger
        self.git_callback = git_callback
        self.git_debounce_seconds = git_debounce_seconds
        self._timers = {}
        self._lock = threading.Lock()
        self._pending_events: Dict[str, FsEvent] = {}
        self._git_timer: Optional[threading.Timer] = None
        self._git_last_path: str = ""
        self._event_times = deque(maxlen=WATCHER_EVENT_QUEUE_MAXLEN)

        # Adaptive debounce (ms) and rate window (sec)
        # LLM Freshness: 100ms minimum debounce is now standard.
        self._debounce_min = WATCHER_DEBOUNCE_MIN_SECONDS
        self._debounce_max = WATCHER_DEBOUNCE_MAX_SECONDS
        self._debounce_target_rps = WATCHER_TARGET_RPS
        self._rate_window = WATCHER_RATE_WINDOW_SECONDS

        # Token bucket for burst control
        self._bucket_capacity = WATCHER_BUCKET_CAPACITY
        self._bucket_rate = WATCHER_BUCKET_RATE
        self._bucket_tokens = self._bucket_capacity
        self._bucket_last_ts = time.time()
        self._bucket_flush_seconds = WATCHER_BUCKET_FLUSH_SECONDS
        self._bucket_timer: Optional[threading.Timer] = None

    def on_any_event(self, event):
        if event.is_directory:
            return

        # We care about Created, Modified, Moved, Deleted
        # watchdog event types: 'created', 'deleted', 'modified', 'moved'

        evt_kind = None
        if event.event_type == 'created':
            evt_kind = FsEventKind.CREATED
        elif event.event_type == 'modified':
            evt_kind = FsEventKind.MODIFIED
        elif event.event_type == 'deleted':
            evt_kind = FsEventKind.DELETED
        elif event.event_type == 'moved':
            evt_kind = FsEventKind.MOVED

        if not evt_kind:
            return

        src_path = getattr(event, "src_path", "")
        if not isinstance(src_path, str):
            return
        dest_path = getattr(event, "dest_path", "")
        if not isinstance(dest_path, str):
            dest_path = ""

        key = src_path
        if _is_git_event(src_path) or _is_git_event(dest_path):
            if self.git_callback:
                with self._lock:
                    if self._git_timer:
                        self._git_timer.cancel()
                    self._git_last_path = src_path
                    self._git_timer = threading.Timer(self.git_debounce_seconds, self._trigger_git)
                    self._git_timer.start()
            if self.logger:
                safe_log(self.logger, "info", "Git activity detected; deferring to rescan.")
            return
        fs_event = FsEvent(kind=evt_kind, path=src_path,
                           root="", # Will be inferred in dispatch
                           dest_path=dest_path or None,
                           ts=time.time())

        with self._lock:
            # Adaptive debounce update
            self._update_debounce(fs_event.ts)

            # Burst control: token bucket
            if not self._try_consume_token(fs_event.ts):
                if key in self._timers:
                    self._timers[key].cancel()
                    del self._timers[key]
                self._pending_events[key] = fs_event
                self._schedule_bucket_flush()
                return

            if key in self._timers:
                self._timers[key].cancel()
            self._pending_events[key] = fs_event
            t = threading.Timer(self.debounce_seconds, self._trigger, args=[key])
            self._timers[key] = t
            t.start()

    def _trigger(self, path: str):
        with self._lock:
            if path in self._timers:
                del self._timers[path]
            fs_event = self._pending_events.pop(path, None)
        if not fs_event:
            return
        try:
           self.callback(fs_event)
        except Exception as e:
            if self.logger:
                safe_log(self.logger, "error", f"Watcher callback failed for {path}: {e}")

    def _update_debounce(self, now_ts: float) -> None:
        self._event_times.append(now_ts)
        # Drop old events outside window
        while self._event_times and now_ts - self._event_times[0] > self._rate_window:
            self._event_times.popleft()
        rate = len(self._event_times) / max(0.5, self._rate_window)
        scale = max(1.0, rate / max(1.0, self._debounce_target_rps))
        self.debounce_seconds = min(self._debounce_max, max(self._debounce_min, self._debounce_min * scale))

    def _refill_tokens(self, now_ts: float) -> None:
        elapsed = max(0.0, now_ts - self._bucket_last_ts)
        if elapsed > 0:
            self._bucket_tokens = min(self._bucket_capacity, self._bucket_tokens + elapsed * self._bucket_rate)
            self._bucket_last_ts = now_ts

    def _try_consume_token(self, now_ts: float) -> bool:
        self._refill_tokens(now_ts)
        if self._bucket_tokens >= 1.0:
            self._bucket_tokens -= 1.0
            return True
        return False

    def _schedule_bucket_flush(self) -> None:
        if self._bucket_timer and self._bucket_timer.is_alive():
            return
        self._bucket_timer = threading.Timer(max(0.1, self._bucket_flush_seconds), self._flush_bucket)
        self._bucket_timer.start()

    def _flush_bucket(self) -> None:
        to_dispatch: List[FsEvent] = []
        with self._lock:
            now = time.time()
            self._refill_tokens(now)
            items = sorted(self._pending_events.items(), key=lambda kv: kv[1].ts)
            dispatched_keys = []
            for key, evt in items:
                if self._bucket_tokens >= 1.0:
                    self._bucket_tokens -= 1.0
                    to_dispatch.append(evt)
                    dispatched_keys.append(key)
            
            # 처리된 이벤트만 제거 (원자성 보장)
            for key in dispatched_keys:
                self._pending_events.pop(key, None)
            
            if self._pending_events:
                self._bucket_timer = threading.Timer(max(0.1, self._bucket_flush_seconds), self._flush_bucket)
                self._bucket_timer.start()
            else:
                self._bucket_timer = None
        
        # Lock 외부에서 callback 호출 (데드락 방지)
        for evt in to_dispatch:
            try:
                self.callback(evt)
            except Exception as e:
                if self.logger:
                    safe_log(self.logger, "error", f"Watcher callback failed for {evt.path}: {e}")

    def _trigger_git(self):
        path = ""
        with self._lock:
            path = self._git_last_path
            self._git_timer = None
        try:
            if self.git_callback:
                self.git_callback(path or ".git")
        except Exception as e:
            if self.logger:
                safe_log(self.logger, "error", f"Watcher git callback failed for {path}: {e}")

class FileWatcher:
    def __init__(
        self,
        paths: List[str],
        on_change_callback: Callable[[FsEvent], None],
        logger=None,
        on_git_checkout: Optional[Callable[[str], None]] = None,
        event_bus=None,
    ):
        self.paths = paths
        self.callback = on_change_callback
        self.logger = logger
        self.git_callback = on_git_checkout
        self.event_bus = event_bus
        self.observer = None
        self._running = False
        self._monitor_thread = None
        self._stop_event = threading.Event()

    def start(self):
        if not HAS_WATCHDOG:
            if self.logger:
                safe_log(self.logger, "info", "Watchdog not installed. Skipping real-time monitoring.")
            return

        if self._running:
            return

        # High-performance observer with low timeout (latency)
        self.observer = Observer(timeout=WATCHER_OBSERVER_TIMEOUT_SECONDS)
        try:
            git_debounce = float(os.environ.get(ENV_SARI_GIT_CHECKOUT_DEBOUNCE, str(WATCHER_GIT_DEBOUNCE_SECONDS)) or WATCHER_GIT_DEBOUNCE_SECONDS)
        except Exception:
            git_debounce = WATCHER_GIT_DEBOUNCE_SECONDS
        handler = DebouncedEventHandler(
            self._dispatch_event,
            logger=self.logger,
            git_callback=self.git_callback,
            git_debounce_seconds=max(1.0, git_debounce),
        )

        started_any = False
        for p in self.paths:
            if os.path.exists(p):
                try:
                    self.observer.schedule(handler, p, recursive=True)
                    started_any = True
                except Exception as e:
                    if self.logger:
                        safe_log(self.logger, "error", f"Failed to watch path {p}: {e}")

        if started_any:
            try:
                self.observer.start()
                self._running = True
                self._start_monitor()
                if self.logger:
                    safe_log(self.logger, "info", f"Watcher started on: {self.paths}")
            except Exception as e:
                if self.logger:
                    safe_log(self.logger, "error", f"Failed to start observer: {e}")

    def stop(self):
        self._stop_event.set()
        if self.observer and self._running:
            self.observer.stop()
            self.observer.join()
            self._running = False
        if self._monitor_thread and self._monitor_thread.is_alive():
            self._monitor_thread.join(timeout=1.0)

    def _start_monitor(self):
        if self._monitor_thread and self._monitor_thread.is_alive():
            return
        self._stop_event.clear()
        self._monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
        self._monitor_thread.start()

    def _restart_observer(self):
        max_retries = WATCHER_RESTART_MAX_RETRIES
        for attempt in range(max_retries):
            try:
                if self.observer:
                    try:
                        self.observer.stop()
                        # 더 긴 타임아웃으로 완전 종료 대기
                        self.observer.join(timeout=WATCHER_OBSERVER_JOIN_TIMEOUT_SECONDS)
                        if self.observer.is_alive():
                            if self.logger:
                                safe_log(self.logger, "warning", 
                                    f"Observer still alive after stop (attempt {attempt+1}/{max_retries})"
                                )
                            if attempt < max_retries - 1:
                                time.sleep(WATCHER_RESTART_RETRY_DELAY_SECONDS)
                                continue
                            else:
                                if self.logger:
                                    safe_log(self.logger, "error", "Observer forced restart after max retries")
                    except Exception as e:
                        if self.logger:
                            safe_log(self.logger, "error", f"Error stopping observer: {e}")
                
                # 새 observer 생성
                self.observer = Observer()
                try:
                    git_debounce = float(os.environ.get(ENV_SARI_GIT_CHECKOUT_DEBOUNCE, str(WATCHER_GIT_DEBOUNCE_SECONDS)) or WATCHER_GIT_DEBOUNCE_SECONDS)
                except Exception:
                    git_debounce = WATCHER_GIT_DEBOUNCE_SECONDS
                handler = DebouncedEventHandler(
                    self._dispatch_event,
                    logger=self.logger,
                    git_callback=self.git_callback,
                    git_debounce_seconds=max(1.0, git_debounce),
                )
                started_any = False
                for p in self.paths:
                    if os.path.exists(p):
                        try:
                            self.observer.schedule(handler, p, recursive=True)
                            started_any = True
                        except Exception as e:
                            if self.logger:
                                safe_log(self.logger, "error", f"Failed to watch path {p}: {e}")
                if started_any:
                    self.observer.start()
                    self._running = True
                    if self.logger:
                        safe_log(self.logger, "info", "Watcher restarted successfully.")
                    break  # 성공
                else:
                    if self.logger:
                        safe_log(self.logger, "error", "Watcher restart failed: no valid paths.")
                    self._running = False
                    break
            except Exception as e:
                if self.logger:
                    safe_log(self.logger, "error", f"Watcher restart attempt {attempt+1}/{max_retries} failed: {e}")
                if attempt < max_retries - 1:
                    time.sleep(WATCHER_RESTART_RETRY_DELAY_SECONDS)
                else:
                    self._running = False
                    raise

    def _monitor_loop(self):
        try:
            interval = float(os.environ.get(ENV_SARI_WATCHER_MONITOR_SECONDS, str(WATCHER_MONITOR_INTERVAL_SECONDS)))
        except Exception:
            interval = WATCHER_MONITOR_INTERVAL_SECONDS
        while not self._stop_event.is_set():
            time.sleep(max(1.0, interval))
            if self._stop_event.is_set():
                break
            if self.observer and not self.observer.is_alive() and self._running:
                if self.logger:
                    safe_log(self.logger, "error", "Watcher observer died; restarting.")
                self._restart_observer()

    def _dispatch_event(self, evt: FsEvent):
        normalized_evt = evt
        try:
            if not getattr(evt, "root", ""):
                inferred_root = self._infer_root_for_path(getattr(evt, "path", ""))
                if inferred_root:
                    normalized_evt = FsEvent(
                        kind=evt.kind,
                        path=evt.path,
                        root=inferred_root,
                        dest_path=evt.dest_path,
                        ts=evt.ts,
                    )
        except Exception:
            normalized_evt = evt

        if self.event_bus:
            try:
                self.event_bus.publish("fs_event", normalized_evt)
                return
            except Exception:
                pass
        try:
            self.callback(normalized_evt)
        except Exception as e:
            if self.logger:
                safe_log(self.logger, "error", f"Watcher callback failed for {getattr(normalized_evt, 'path', '')}: {e}")

    def _infer_root_for_path(self, event_path: str) -> str:
        if not event_path:
            return ""
        norm_event = WorkspaceManager.normalize_path(event_path)
        roots = [WorkspaceManager.normalize_path(p) for p in self.paths if p]
        roots.sort(key=len, reverse=True)
        for root in roots:
            try:
                if os.path.commonpath([norm_event, root]) == root:
                    return root
            except ValueError:
                continue
        return ""
