"""Server managers for search caching and file synchronization.

This module provides typed data structures and managers for:
- SearchManager: Caches search results with aliases (A, B, C...)
- SyncManager: Tracks links between local files and Google Drive
"""

import json
import os
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any

# ============================================================================
# Data Classes
# ============================================================================


@dataclass
class CachedFile:
    """A file cached from search results.

    Attributes:
        id: The Google Drive file ID.
        name: The file name.
        alias: Single-letter alias (A-Z) for quick reference.
        mime_type: The MIME type of the file.
        snippet: Preview text from the file content.
        score: Relevance score (0-100).
    """

    id: str
    name: str
    alias: str
    mime_type: str = ""
    snippet: str = ""
    score: int = 0

    def __repr__(self) -> str:
        return f"CachedFile(alias={self.alias!r}, name={self.name!r}, id={self.id!r})"


@dataclass
class SyncLink:
    """Link between a local file and a Google Drive file.

    Attributes:
        file_id: The Google Drive file ID.
        last_synced_version: The version number at last sync.
    """

    file_id: str
    last_synced_version: int = 0

    def __repr__(self) -> str:
        return f"SyncLink(file_id={self.file_id!r}, version={self.last_synced_version})"

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {"id": self.file_id, "last_synced_version": self.last_synced_version}

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "SyncLink":
        """Create from dictionary (loaded from JSON)."""
        return cls(file_id=data.get("id", ""), last_synced_version=data.get("last_synced_version", 0))


# ============================================================================
# Search Manager
# ============================================================================


class SearchManager:
    """Manages search result caching and alias resolution.

    Assigns single-letter aliases (A-Z) to search results for quick reference
    in subsequent commands. Thread-safe for concurrent access.
    """

    def __init__(self) -> None:
        self._cache: dict[str, CachedFile] = {}
        self.search_cache: dict[str, str] = {}
        self._lock = threading.RLock()

    def cache_results(self, files: list[dict]) -> list[dict]:
        """Cache search results and assign aliases (A, B, C...)."""
        with self._lock:
            self._cache.clear()
            self.search_cache.clear()
            letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            ranked_results = []

            for i, file in enumerate(files):
                if i < len(letters):
                    alias = letters[i]
                    cached = CachedFile(
                        id=file["id"],
                        name=file.get("name", "Untitled"),
                        alias=alias,
                        mime_type=file.get("mimeType", ""),
                        snippet=file.get("snippet", ""),
                        score=file.get("score", 0),
                    )
                    self._cache[alias] = cached
                    self.search_cache[alias] = file["id"]
                    file["alias"] = alias
                    ranked_results.append(file)

            return ranked_results

    def resolve_alias(self, query: str) -> str:
        """Resolve a single-letter alias to a file_id."""
        with self._lock:
            if len(query) == 1 and query.upper() in self.search_cache:
                return self.search_cache[query.upper()]
            return query

    def get_cached_file(self, alias: str) -> CachedFile | None:
        """Get the full cached file info by alias."""
        with self._lock:
            return self._cache.get(alias.upper())

    def clear(self) -> None:
        """Clear all cached results."""
        with self._lock:
            self._cache.clear()
            self.search_cache.clear()


# ============================================================================
# Sync Manager
# ============================================================================


def _default_sync_map_path() -> Path:
    """Return the default path for the sync map file.

    This is a fallback used when no explicit path is provided via constructor
    injection. The composition root (core/server.py or core/container.py)
    should inject the actual path from auth.config.get_sync_map_path().
    """
    return Path.home() / ".config" / "gws-mcp-advanced" / "sync_map.json"


class SyncManager:
    """Manages local file to Google Drive ID mappings for synchronization.

    Persists links to gdrive_map.json for cross-session persistence.
    Thread-safe for concurrent access.

    Args:
        sync_map_path: Path to the sync map file. If None, uses default
            (~/.config/gws-mcp-advanced/sync_map.json). Composition roots
            should inject this via auth.config.get_sync_map_path().
    """

    def __init__(self, sync_map_path: Path | str | None = None) -> None:
        self._sync_map_path = Path(sync_map_path) if sync_map_path else None
        self._links: dict[str, SyncLink] = {}
        self._lock = threading.RLock()
        self.file_map: dict[str, dict] = {}
        self._load_map()

    @property
    def sync_map_path(self) -> Path:
        if self._sync_map_path is not None:
            return self._sync_map_path
        return _default_sync_map_path()

    def _load_map(self) -> None:
        with self._lock:
            if self.sync_map_path.exists():
                with open(self.sync_map_path) as f:
                    data = json.load(f)
                    for path, info in data.items():
                        self._links[path] = SyncLink.from_dict(info)
                        self.file_map[path] = info
            else:
                self._links = {}
                self.file_map = {}

    def _save_map(self) -> None:
        with self._lock:
            map_path = self.sync_map_path
            map_path.parent.mkdir(parents=True, exist_ok=True)
            self.file_map = {path: link.to_dict() for path, link in self._links.items()}
            with open(map_path, "w") as f:
                json.dump(self.file_map, f, indent=2)

    def link_file(self, local_path: str, file_id: str, version: int = 0) -> str:
        """Link a local file to a Google Drive file ID.

        Args:
            local_path: Path to the local file.
            file_id: Google Drive file ID.
            version: Initial synced version (default 0).

        Returns:
            Success message.
        """
        abs_path = os.path.abspath(local_path)
        link = SyncLink(file_id=file_id, last_synced_version=version)
        with self._lock:
            self._links[abs_path] = link
            self.file_map[abs_path] = link.to_dict()
        self._save_map()
        return f"Linked {local_path} -> {file_id}"

    def get_link(self, local_path: str) -> dict | None:
        """Get the Drive link info for a local file.

        Args:
            local_path: Path to the local file.

        Returns:
            Link info dict or None (backward compatible format).
        """
        abs_path = os.path.abspath(local_path)
        with self._lock:
            link = self._links.get(abs_path)
            if link:
                return link.to_dict()
            return self.file_map.get(abs_path)

    def get_sync_link(self, local_path: str) -> SyncLink | None:
        """Get the typed SyncLink for a local file.

        Args:
            local_path: Path to the local file.

        Returns:
            SyncLink or None.
        """
        abs_path = os.path.abspath(local_path)
        with self._lock:
            return self._links.get(abs_path)

    def update_version(self, local_path: str, version: int) -> None:
        """Update the last synced version for a file.

        Args:
            local_path: Path to the local file.
            version: New version number.
        """
        abs_path = os.path.abspath(local_path)
        with self._lock:
            if abs_path in self._links:
                self._links[abs_path].last_synced_version = version
            if abs_path in self.file_map:
                self.file_map[abs_path]["last_synced_version"] = version
        self._save_map()

    def unlink_file(self, local_path: str) -> bool:
        """Remove a sync link.

        Args:
            local_path: Path to the local file.

        Returns:
            True if link was removed, False if not found.
        """
        abs_path = os.path.abspath(local_path)
        with self._lock:
            if abs_path in self._links:
                del self._links[abs_path]
                if abs_path in self.file_map:
                    del self.file_map[abs_path]
                self._save_map()
                return True
            return False


# ============================================================================
# Global manager instances
# ============================================================================

search_manager = SearchManager()
sync_manager = SyncManager()
