from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import logging
import threading
import time

from .sync_engine import SyncEngine

logger = logging.getLogger(__name__)

try:
    from watchdog.events import FileSystemEvent, FileSystemEventHandler
    from watchdog.observers import Observer
except ImportError:  # pragma: no cover
    FileSystemEvent = object  # type: ignore[assignment]
    FileSystemEventHandler = object  # type: ignore[assignment]
    Observer = None


@dataclass(slots=True)
class _DirtyState:
    lock: threading.Lock
    dirty: bool = False
    last_event_at: float = 0.0


class _SyncTriggerHandler(FileSystemEventHandler):
    def __init__(self, engine: SyncEngine, dirty_state: _DirtyState) -> None:
        self.engine = engine
        self.dirty_state = dirty_state

    def on_any_event(self, event: FileSystemEvent) -> None:
        if getattr(event, "is_directory", False):
            return

        paths: list[str] = []
        src_path = getattr(event, "src_path", None)
        if src_path:
            paths.append(src_path)
        dest_path = getattr(event, "dest_path", None)
        if dest_path:
            paths.append(dest_path)

        if not paths:
            return

        for raw in paths:
            if self.engine.should_ignore_absolute(Path(raw)):
                return

        with self.dirty_state.lock:
            self.dirty_state.dirty = True
            self.dirty_state.last_event_at = time.monotonic()


def run_watch_loop(engine: SyncEngine, *, dry_run: bool = False) -> None:
    run_watch_loop_until(engine, dry_run=dry_run, stop_event=None)


def run_watch_loop_until(
    engine: SyncEngine,
    *,
    dry_run: bool = False,
    stop_event: threading.Event | None = None,
) -> None:
    if Observer is None:
        logger.warning(
            "watchdog is not installed; using polling-only watch mode "
            "(install requirements for filesystem event support)."
        )
        _run_polling_loop(engine, dry_run=dry_run, stop_event=stop_event)
        return

    dirty_state = _DirtyState(lock=threading.Lock())
    handler = _SyncTriggerHandler(engine, dirty_state)
    observer = Observer()
    observer.schedule(handler, str(engine.source_root), recursive=True)
    observer.schedule(handler, str(engine.target_root), recursive=True)
    observer.start()

    logger.info("Watch mode started. Press Ctrl+C to stop.")
    last_sync_at = 0.0
    last_audit_at = 0.0
    debounce = max(engine.config.sync.debounce_seconds, 0.1)
    # Allow sub-5s polling for lower cross-device latency, while keeping a sane floor.
    poll_interval = max(engine.config.sync.poll_interval_seconds, 1.0)
    audit_interval = max(engine.config.sync.integrity_audit_interval_seconds, 30.0)

    try:
        while True:
            if stop_event and stop_event.is_set():
                logger.info("Watch mode stopping by stop event.")
                break

            now = time.monotonic()
            should_sync = False

            with dirty_state.lock:
                if dirty_state.dirty and (now - dirty_state.last_event_at) >= debounce:
                    should_sync = True
                    dirty_state.dirty = False

            if should_sync or (now - last_sync_at) >= poll_interval:
                try:
                    stats = engine.sync_once(dry_run=dry_run)
                except Exception as exc:  # noqa: BLE001
                    logger.exception("Sync cycle failed: %s", exc)
                else:
                    logger.info(
                        "Sync complete | to_source=%s to_target=%s deleted_source=%s deleted_target=%s conflicts=%s errors=%s",
                        stats.copied_to_source,
                        stats.copied_to_target,
                        stats.deleted_from_source,
                        stats.deleted_from_target,
                        stats.conflicts,
                        stats.errors,
                    )
                finally:
                    # Avoid tight retry loops if the previous cycle failed.
                    last_sync_at = now

            if (now - last_audit_at) >= audit_interval:
                issues = engine.audit_integrity(
                    repair=engine.config.sync.integrity_auto_repair,
                    dry_run=dry_run,
                )
                if issues:
                    logger.warning("Integrity audit found %s issue(s).", len(issues))
                last_audit_at = now

            time.sleep(0.2)
    except KeyboardInterrupt:
        logger.info("Watch mode stopped by user.")
    finally:
        observer.stop()
        observer.join(timeout=5)


def _run_polling_loop(
    engine: SyncEngine,
    *,
    dry_run: bool = False,
    stop_event: threading.Event | None = None,
) -> None:
    poll_interval = max(engine.config.sync.poll_interval_seconds, 1.0)
    audit_interval = max(engine.config.sync.integrity_audit_interval_seconds, 30.0)
    last_audit_at = 0.0
    logger.info("Polling watch mode started. Press Ctrl+C to stop.")
    try:
        while True:
            if stop_event and stop_event.is_set():
                logger.info("Polling watch mode stopping by stop event.")
                break

            try:
                stats = engine.sync_once(dry_run=dry_run)
            except Exception as exc:  # noqa: BLE001
                logger.exception("Polling sync cycle failed: %s", exc)
            else:
                logger.info(
                    "Sync complete | to_source=%s to_target=%s deleted_source=%s deleted_target=%s conflicts=%s errors=%s",
                    stats.copied_to_source,
                    stats.copied_to_target,
                    stats.deleted_from_source,
                    stats.deleted_from_target,
                    stats.conflicts,
                    stats.errors,
                )

            now = time.monotonic()
            if (now - last_audit_at) >= audit_interval:
                issues = engine.audit_integrity(
                    repair=engine.config.sync.integrity_auto_repair,
                    dry_run=dry_run,
                )
                if issues:
                    logger.warning("Integrity audit found %s issue(s).", len(issues))
                last_audit_at = now

            time.sleep(poll_interval)
    except KeyboardInterrupt:
        logger.info("Polling watch mode stopped by user.")
