# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Folder watcher for Smart Intake — poll-based auto-intake from watched directories.

No inotify dependency — uses polling with file hash tracking.
Config stored in scribe_watch.json.
"""

import hashlib
import json
import logging
import os
import secrets
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

_lock = threading.Lock()

MAX_WATCHES = 10
MAX_FILES_PER_CYCLE = 100


def _data_dir() -> Path:
    try:
        from familiar.core.paths import DATA_DIR

        return DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


def _file_hash(filepath: Path) -> str:
    """Compute SHA-256 hash of a file for change detection."""
    h = hashlib.sha256()
    try:
        with open(filepath, "rb") as f:
            for chunk in iter(lambda: f.read(8192), b""):
                h.update(chunk)
    except OSError:
        return ""
    return h.hexdigest()


class FolderWatcher:
    """Poll-based folder watcher with hash tracking."""

    def __init__(self, config_file: Optional[Path] = None):
        self._file = config_file or (_data_dir() / "scribe_watch.json")
        self._ensure_file()

    def _ensure_file(self):
        if not self._file.exists():
            self._file.parent.mkdir(parents=True, exist_ok=True)
            self._write({"watches": [], "seen_hashes": {}, "version": 1})

    def _read(self) -> dict:
        try:
            return json.loads(self._file.read_text(encoding="utf-8"))
        except Exception:
            return {"watches": [], "seen_hashes": {}, "version": 1}

    def _write(self, data: dict):
        tmp = self._file.with_suffix(".tmp")
        tmp.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
        os.replace(str(tmp), str(self._file))
        try:
            os.chmod(str(self._file), 0o600)
        except OSError:
            pass

    def add_watch(
        self,
        directory: str,
        recursive: bool = False,
        file_types: Optional[list[str]] = None,
        label: str = "",
    ) -> str:
        """Add a directory to watch. Returns watch ID."""
        path = Path(directory).expanduser().resolve()
        if not path.is_dir():
            raise ValueError(f"Not a directory: {directory}")

        with _lock:
            data = self._read()
            watches = data.get("watches", [])

            # Check cap
            if len(watches) >= MAX_WATCHES:
                raise ValueError(f"Maximum {MAX_WATCHES} watch folders allowed.")

            # Check for duplicate
            for w in watches:
                if w.get("directory") == str(path):
                    return w["id"]

            watch_id = f"watch_{secrets.token_hex(4)}"
            watch = {
                "id": watch_id,
                "directory": str(path),
                "recursive": recursive,
                "file_types": file_types,
                "label": label or path.name,
                "created_at": datetime.now(timezone.utc).isoformat(),
                "last_checked": None,
                "files_processed": 0,
            }
            watches.append(watch)
            data["watches"] = watches
            self._write(data)

        return watch_id

    def remove_watch(self, watch_id: str) -> bool:
        """Remove a watch by ID. Returns True if found and removed."""
        with _lock:
            data = self._read()
            watches = data.get("watches", [])
            original_len = len(watches)
            data["watches"] = [w for w in watches if w.get("id") != watch_id]
            if len(data["watches"]) < original_len:
                # Clean up seen hashes for this watch
                seen = data.get("seen_hashes", {})
                seen.pop(watch_id, None)
                data["seen_hashes"] = seen
                self._write(data)
                return True
        return False

    def list_watches(self) -> list[dict]:
        """List all active watches."""
        data = self._read()
        return data.get("watches", [])

    def get_watch(self, watch_id: str) -> Optional[dict]:
        """Get a specific watch by ID."""
        data = self._read()
        for w in data.get("watches", []):
            if w.get("id") == watch_id:
                return w
        return None

    def check_watches(self) -> list[dict]:
        """Check all watches for new files. Returns list of new file paths with metadata."""
        from familiar.skills.intake.skill import _ALL_SUPPORTED

        new_files = []

        with _lock:
            data = self._read()
            seen = data.get("seen_hashes", {})
            watches = data.get("watches", [])
            files_found = 0

            for watch in watches:
                watch_id = watch["id"]
                directory = Path(watch["directory"])
                if not directory.is_dir():
                    logger.debug("Watch directory missing: %s", directory)
                    continue

                recursive = watch.get("recursive", False)
                allowed_types = watch.get("file_types")
                watch_seen = set(seen.get(watch_id, []))

                # Collect files
                if recursive:
                    candidates = [f for f in directory.rglob("*") if f.is_file()]
                else:
                    candidates = [f for f in directory.iterdir() if f.is_file()]

                for filepath in sorted(candidates):
                    if files_found >= MAX_FILES_PER_CYCLE:
                        break

                    ext = filepath.suffix.lower()
                    if ext not in _ALL_SUPPORTED:
                        continue
                    if allowed_types and ext not in allowed_types:
                        continue

                    fhash = _file_hash(filepath)
                    if not fhash or fhash in watch_seen:
                        continue

                    new_files.append(
                        {
                            "filepath": str(filepath),
                            "watch_id": watch_id,
                            "watch_label": watch.get("label", ""),
                        }
                    )
                    watch_seen.add(fhash)
                    files_found += 1

                # Update seen hashes and stats
                seen[watch_id] = list(watch_seen)
                watch["last_checked"] = datetime.now(timezone.utc).isoformat()
                watch["files_processed"] = watch.get("files_processed", 0) + len(
                    [f for f in new_files if f.get("watch_id") == watch_id]
                )

            data["seen_hashes"] = seen
            data["watches"] = watches
            self._write(data)

        return new_files


_instance: Optional[FolderWatcher] = None


def get_folder_watcher() -> FolderWatcher:
    """Singleton accessor."""
    global _instance
    if _instance is None:
        _instance = FolderWatcher()
    return _instance
