"""
file_watcher.py — Overvåger musikbiblioteker og holder SQLite opdateret.

Bruger watchdog til at reagere på fil-ændringer i realtid.
Kører fuld scan ved opstart for at fange ændringer lavet mens appen var lukket.
"""

import threading
import time
import logging
from pathlib import Path
from typing import Callable

try:
    from watchdog.observers import Observer
    from watchdog.events import (
        FileSystemEventHandler,
        FileCreatedEvent,
        FileModifiedEvent,
        FileDeletedEvent,
        FileMovedEvent,
    )
    WATCHDOG_AVAILABLE = True
except ImportError:
    WATCHDOG_AVAILABLE = False
    print("Advarsel: watchdog ikke installeret — fil-overvågning deaktiveret")

from local.tag_reader import is_supported, read_tags, get_file_modified_at
from local.local_db import (
    get_libraries, add_library, remove_library,
    upsert_song, mark_song_missing,
    get_all_song_paths_for_library, update_library_scan_time,
)

logger = logging.getLogger(__name__)


class MusicLibraryHandler(FileSystemEventHandler):
    """
    Reagerer på ændringer i et musikbibliotek.
    Kører i watchdog's baggrundstråd — DB-operationer er thread-safe via WAL.
    """

    def __init__(self, library_id: int, on_change: Callable | None = None):
        self.library_id = library_id
        self.on_change = on_change   # valgfrit callback til GUI-opdatering
        self._debounce: dict[str, float] = {}
        self._debounce_lock = threading.Lock()

    def _debounced(self, path: str) -> bool:
        """
        Forhindrer at samme fil behandles flere gange på kort tid.
        Nogle programmer gemmer filer i flere trin (temp-fil → rename).
        """
        now = time.time()
        with self._debounce_lock:
            last = self._debounce.get(path, 0)
            if now - last < 1.5:   # 1.5 sekunder cooldown
                return False
            self._debounce[path] = now
        return True

    def on_created(self, event):
        if event.is_directory or not is_supported(event.src_path):
            return
        if self._debounced(event.src_path):
            self._process_file(event.src_path)

    def on_modified(self, event):
        if event.is_directory or not is_supported(event.src_path):
            return
        if self._debounced(event.src_path):
            self._process_file(event.src_path)

    def on_deleted(self, event):
        if event.is_directory or not is_supported(event.src_path):
            return
        logger.info(f"Fil slettet: {event.src_path}")
        mark_song_missing(event.src_path)
        if self.on_change:
            self.on_change("deleted", event.src_path, None)

    def on_moved(self, event):
        if event.is_directory:
            return
        # Behandl som slet + opret
        if is_supported(event.src_path):
            mark_song_missing(event.src_path)
        if is_supported(event.dest_path):
            if self._debounced(event.dest_path):
                self._process_file(event.dest_path)

    def _process_file(self, path: str):
        """Læs tags og gem i SQLite."""
        try:
            logger.debug(f"Høster tags fra: {path}")
            tags = read_tags(path)
            tags["library_id"] = self.library_id
            song_id = upsert_song(tags)
            logger.info(f"Opdateret: {Path(path).name} ({len(tags.get('dances', []))} danse)")
            if self.on_change:
                self.on_change("upserted", path, song_id)
        except Exception as e:
            logger.error(f"Fejl ved behandling af {path}: {e}")


class LibraryWatcher:
    """
    Styrer watchdog-observere for alle aktive musikbiblioteker.
    Én instans per applikation.
    """

    def __init__(self, on_change: Callable | None = None):
        self.on_change = on_change
        self._observer: Observer | None = None
        self._running = False

    def start(self):
        """Start overvågning af alle aktive biblioteker + kør fuld scan."""
        if not WATCHDOG_AVAILABLE:
            logger.warning("watchdog ikke tilgængelig — starter kun fuld scan")
            self._full_scan_all()
            return

        self._observer = Observer()
        libraries = get_libraries(active_only=True)

        for lib in libraries:
            path = Path(lib["path"])
            if not path.exists():
                logger.warning(f"Bibliotek findes ikke: {path}")
                continue

            handler = MusicLibraryHandler(lib["id"], self.on_change)
            self._observer.schedule(handler, str(path), recursive=True)
            logger.info(f"Overvåger: {path}")

        self._observer.start()
        self._running = True

        # Fuld scan i baggrundstråd så GUI ikke blokeres
        threading.Thread(target=self._full_scan_all, daemon=True).start()

    def stop(self):
        if self._observer and self._running:
            self._observer.stop()
            self._observer.join()
            self._running = False

    def add_library(self, path: str) -> int:
        """Tilføj et nyt bibliotek og start overvågning af det med det samme."""
        library_id = add_library(path)

        if self._observer and self._running:
            handler = MusicLibraryHandler(library_id, self.on_change)
            self._observer.schedule(handler, path, recursive=True)
            logger.info(f"Tilføjet bibliotek: {path}")

        # Scan det nye bibliotek i baggrunden
        threading.Thread(
            target=self._full_scan_library,
            args=(library_id, path),
            daemon=True,
        ).start()

        return library_id

    def remove_library(self, library_id: int):
        """Deaktiver bibliotek. Watchdog stopper automatisk ved næste restart."""
        remove_library(library_id)
        # Genstart observer for at fjerne watch (watchdog understøtter ikke unschedule by id)
        if self._observer and self._running:
            self._observer.unschedule_all()
            self._reschedule_all()

    def _reschedule_all(self):
        """Genplanlæg alle aktive biblioteker på observeren."""
        for lib in get_libraries(active_only=True):
            path = Path(lib["path"])
            if path.exists():
                handler = MusicLibraryHandler(lib["id"], self.on_change)
                self._observer.schedule(handler, str(path), recursive=True)

    def _full_scan_all(self):
        """Kør fuld scan på alle aktive biblioteker."""
        for lib in get_libraries(active_only=True):
            path = Path(lib["path"])
            if path.exists():
                self._full_scan_library(lib["id"], str(path))

    def _full_scan_library(self, library_id: int, library_path: str):
        """
        Sammenligner filer på disk med SQLite og synkroniserer forskelle.
        
        Tre operationer:
          1. Nye filer     → indsæt i SQLite
          2. Ændrede filer → opdater SQLite (baseret på fil-timestamp)
          3. Forsvundne    → marker som missing i SQLite
        """
        logger.info(f"Fuld scan starter: {library_path}")
        base = Path(library_path)

        # Hvad SQLite kender til
        known = get_all_song_paths_for_library(library_id)

        # Hvad der faktisk er på disk
        found_paths = set()
        processed = 0
        errors = 0

        for file_path in base.rglob("*"):
            if not file_path.is_file() or not is_supported(file_path):
                continue

            path_str = str(file_path)
            found_paths.add(path_str)
            disk_modified = get_file_modified_at(file_path)

            # Ny fil eller ændret siden sidst
            if path_str not in known or known[path_str] != disk_modified:
                try:
                    tags = read_tags(file_path)
                    tags["library_id"] = library_id
                    upsert_song(tags)
                    processed += 1
                    if self.on_change:
                        self.on_change("upserted", path_str, None)
                except Exception as e:
                    logger.error(f"Scan-fejl for {file_path}: {e}")
                    errors += 1

        # Marker forsvundne filer
        missing_count = 0
        for known_path in known:
            if known_path not in found_paths:
                mark_song_missing(known_path)
                missing_count += 1
                if self.on_change:
                    self.on_change("deleted", known_path, None)

        update_library_scan_time(library_id)
        logger.info(
            f"Scan færdig: {library_path} — "
            f"{processed} opdateret, {missing_count} mangler, {errors} fejl"
        )


# ── Singleton til brug i appen ────────────────────────────────────────────────

_watcher: LibraryWatcher | None = None


def get_watcher(on_change: Callable | None = None) -> LibraryWatcher:
    """Returnerer den globale LibraryWatcher-instans."""
    global _watcher
    if _watcher is None:
        _watcher = LibraryWatcher(on_change=on_change)
    return _watcher
