"""Multi-repo registry and connection pool.

Manages a registry of multiple repositories at ``~/.code-review-graph/registry.json``
and provides a connection pool for concurrent access to multiple graph databases.
"""

from __future__ import annotations

import json
import logging
import sqlite3
import threading
from collections import OrderedDict
from pathlib import Path

logger = logging.getLogger(__name__)

# Default registry path
_REGISTRY_DIR = Path.home() / ".code-review-graph"
_REGISTRY_PATH = _REGISTRY_DIR / "registry.json"


class Registry:
    """Manages a JSON-based registry of code-review-graph repositories.

    Each entry stores the repo path and an optional alias.
    The registry lives at ``~/.code-review-graph/registry.json``.
    """

    def __init__(self, path: Path | None = None) -> None:
        self._path = path or _REGISTRY_PATH
        self._path.parent.mkdir(parents=True, exist_ok=True)
        self._lock = threading.Lock()
        self._repos: list[dict[str, str]] = []
        self._load()

    def _load(self) -> None:
        """Load registry from disk."""
        if self._path.exists():
            try:
                data = json.loads(self._path.read_text(encoding="utf-8", errors="replace"))
                self._repos = data.get("repos", [])
            except (json.JSONDecodeError, KeyError, TypeError):
                logger.warning("Invalid registry file, starting fresh: %s", self._path)
                self._repos = []
        else:
            self._repos = []

    def _save(self) -> None:
        """Write registry to disk."""
        self._path.parent.mkdir(parents=True, exist_ok=True)
        data = {"repos": self._repos}
        self._path.write_text(
            json.dumps(data, indent=2) + "\n", encoding="utf-8"
        )

    def register(
        self, path: str, alias: str | None = None, data_dir: str | None = None,
    ) -> dict[str, str]:
        """Register a repository path.

        Validates that the path contains a ``.git`` or ``.code-review-graph``
        directory.

        Args:
            path: Absolute or relative path to the repository root.
            alias: Optional short alias for the repository.
            data_dir: Optional external directory for graph database.

        Returns:
            The registered entry dict.

        Raises:
            ValueError: If the path is not a valid repository.
        """
        resolved = Path(path).resolve()
        if not resolved.is_dir():
            raise ValueError(f"Path is not a directory: {resolved}")
        has_repo_marker = (
            (resolved / ".git").exists()
            or (resolved / ".svn").exists()
            or (resolved / ".code-review-graph").exists()
        )
        if not has_repo_marker:
            raise ValueError(
                f"Path does not look like a repository "
                f"(no .git, .svn, or .code-review-graph): {resolved}"
            )

        with self._lock:
            # Check for duplicate path
            str_path = str(resolved)
            for entry in self._repos:
                if entry["path"] == str_path:
                    # Update alias and/or data_dir if provided
                    if alias:
                        entry["alias"] = alias
                    if data_dir:
                        entry["data_dir"] = str(Path(data_dir).resolve())
                    self._save()
                    return entry

            new_entry: dict[str, str] = {"path": str_path}
            if alias:
                new_entry["alias"] = alias
            if data_dir:
                new_entry["data_dir"] = str(Path(data_dir).resolve())
            self._repos.append(new_entry)
            self._save()
            return new_entry

    def unregister(self, path_or_alias: str) -> bool:
        """Remove a repository by path or alias.

        Args:
            path_or_alias: Either the absolute path or the alias.

        Returns:
            True if an entry was removed, False otherwise.
        """
        with self._lock:
            resolved = str(Path(path_or_alias).resolve())
            original_len = len(self._repos)
            self._repos = [
                entry for entry in self._repos
                if entry["path"] != resolved
                and entry.get("alias") != path_or_alias
            ]
            if len(self._repos) < original_len:
                self._save()
                return True
            return False

    def list_repos(self) -> list[dict[str, str]]:
        """Return list of all registered repositories.

        Returns:
            List of dicts with 'path' and optional 'alias' keys.
        """
        with self._lock:
            return list(self._repos)

    def find_by_alias(self, alias: str) -> dict[str, str] | None:
        """Look up a repository by its alias.

        Args:
            alias: The alias to search for.

        Returns:
            The matching entry, or None.
        """
        with self._lock:
            for entry in self._repos:
                if entry.get("alias") == alias:
                    return dict(entry)
            return None

    def find_by_path(self, path: str) -> dict[str, str] | None:
        """Look up a repository by its path.

        Args:
            path: The path to search for.

        Returns:
            The matching entry, or None.
        """
        resolved = str(Path(path).resolve())
        with self._lock:
            for entry in self._repos:
                if entry["path"] == resolved:
                    return dict(entry)
            return None

    def set_data_dir(self, path: str, data_dir: str) -> dict[str, str]:
        """Set the external data directory for a repository.

        Args:
            path: Repository path (absolute or relative).
            data_dir: External directory path to store graph database.

        Returns:
            The updated or created registry entry.
        """
        resolved = str(Path(path).resolve())
        data_resolved = str(Path(data_dir).resolve())

        with self._lock:
            # Check for existing entry
            for entry in self._repos:
                if entry["path"] == resolved:
                    entry["data_dir"] = data_resolved
                    self._save()
                    return dict(entry)

            # Create new entry if not found
            new_entry = {
                "path": resolved,
                "data_dir": data_resolved
            }
            self._repos.append(new_entry)
            self._save()
            return new_entry

    def get_data_dir_for_repo(self, path: str) -> str | None:
        """Get the stored data directory for a repository.

        Args:
            path: Repository path (absolute or relative).

        Returns:
            The stored data_dir path, or None if not set.
        """
        resolved = str(Path(path).resolve())
        with self._lock:
            for entry in self._repos:
                if entry["path"] == resolved:
                    return entry.get("data_dir")
            return None


class ConnectionPool:
    """LRU connection pool for SQLite graph databases.

    Caches open connections keyed by database path, evicting the least
    recently used connection when the pool is full.
    """

    def __init__(self, max_size: int = 10) -> None:
        self._max_size = max_size
        self._pool: OrderedDict[str, sqlite3.Connection] = OrderedDict()
        self._lock = threading.Lock()

    def get(self, db_path: str) -> sqlite3.Connection:
        """Get or create a connection for the given database path.

        Args:
            db_path: Path to the SQLite database file.

        Returns:
            An open SQLite connection.
        """
        key = str(Path(db_path).resolve())
        with self._lock:
            if key in self._pool:
                self._pool.move_to_end(key)
                return self._pool[key]

            # Evict LRU if full
            while len(self._pool) >= self._max_size:
                evict_key, evict_conn = self._pool.popitem(last=False)
                try:
                    evict_conn.close()
                except sqlite3.Error:
                    logger.debug("Failed to close evicted connection: %s", evict_key)
                logger.debug("Evicted connection: %s", evict_key)

            conn = sqlite3.connect(
                key, timeout=30, check_same_thread=False,
                isolation_level=None,
            )
            conn.row_factory = sqlite3.Row
            conn.execute("PRAGMA journal_mode=WAL")
            conn.execute("PRAGMA busy_timeout=5000")
            self._pool[key] = conn
            return conn

    def close_all(self) -> None:
        """Close all connections in the pool."""
        with self._lock:
            for key, conn in self._pool.items():
                try:
                    conn.close()
                except sqlite3.Error:
                    logger.debug("Failed to close connection: %s", key)
            self._pool.clear()

    @property
    def size(self) -> int:
        """Current number of open connections."""
        with self._lock:
            return len(self._pool)


def resolve_repo(
    registry: Registry,
    repo: str | None,
    cwd: str | None = None,
) -> str | None:
    """Resolve a repo parameter to an absolute path.

    Resolution order:
    1. If repo is given, try as alias first.
    2. If repo is given and not an alias, try as a direct path.
    3. If repo is None, use cwd.

    Args:
        registry: The Registry instance.
        repo: Alias or path string, or None.
        cwd: Current working directory fallback.

    Returns:
        Resolved absolute path string, or None if unresolvable.
    """
    if repo:
        # Try alias first
        entry = registry.find_by_alias(repo)
        if entry:
            return entry["path"]

        # Try as direct path
        path = Path(repo).resolve()
        if path.is_dir():
            return str(path)

    # Fall back to CWD
    if cwd:
        return str(Path(cwd).resolve())

    return None
