"""GroverContext — shared state and helpers for GroverAsync mixins."""

from __future__ import annotations

import logging
from contextlib import asynccontextmanager
from dataclasses import dataclass
from typing import TYPE_CHECKING, TypeVar

from grover.exceptions import MountNotFoundError
from grover.permissions import Permission
from grover.worker import IndexingMode

if TYPE_CHECKING:
    from collections.abc import AsyncGenerator

    from sqlalchemy.ext.asyncio import AsyncSession

    from grover.analyzers import AnalyzerRegistry
    from grover.models.internal.results import FileOperationResult
    from grover.mount import Mount, MountRegistry
    from grover.providers.graph.protocol import GraphProvider
    from grover.worker import BackgroundWorker

logger = logging.getLogger(__name__)

T = TypeVar("T")


@dataclass
class GroverContext:
    """Shared state for GroverAsync operations."""

    worker: BackgroundWorker
    registry: MountRegistry
    analyzer_registry: AnalyzerRegistry
    indexing_mode: IndexingMode = IndexingMode.BACKGROUND
    initialized: bool = False
    closed: bool = False

    # ------------------------------------------------------------------
    # Session management & helpers
    # ------------------------------------------------------------------

    @asynccontextmanager
    async def session_for(self, mount: Mount) -> AsyncGenerator[AsyncSession]:
        """Yield a session for the given mount."""
        if mount.session_factory is None:
            raise RuntimeError(f"No session factory on mount {mount.path}")

        session = mount.session_factory()
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

    async def drain(self, *, timeout: float | None = None) -> None:
        """Drain all pending background work."""
        await self.worker.drain(timeout=timeout)

    def check_writable(self, virtual_path: str) -> str | None:
        """Return an error message if *virtual_path* is read-only, else ``None``.

        Replaces the previous raise-based pattern to avoid unnecessary
        exception overhead in the common (writable) case.
        """
        try:
            perm = self.registry.get_permission(virtual_path)
        except MountNotFoundError as e:
            return str(e)
        if perm == Permission.READ_ONLY:
            return f"Cannot write to read-only path: {virtual_path}"
        return None

    @staticmethod
    def get_capability(backend: object, protocol: type[T]) -> T | None:
        """Return *backend* if it satisfies *protocol*, else ``None``."""
        if isinstance(backend, protocol):
            return backend
        return None

    def prefix_path(self, path: str | None, mount_path: str) -> str | None:
        if path is None:
            return None
        if path == "/":
            return mount_path
        return mount_path + path

    def prefix_file_info(self, info: FileOperationResult, mount: Mount) -> FileOperationResult:
        prefixed_path = self.prefix_path(info.file.path, mount.path) or info.file.path
        info.file.path = prefixed_path
        return info

    # ------------------------------------------------------------------
    # Per-mount graph resolution
    # ------------------------------------------------------------------

    def resolve_graph_with_mount(self, path: str) -> tuple[GraphProvider, Mount]:
        """Return ``(graph_provider, mount)`` for the mount owning *path*."""
        try:
            mount, _rel = self.registry.resolve(path)
        except MountNotFoundError:
            msg = f"No mount found for path: {path!r}"
            raise RuntimeError(msg) from None
        gp = getattr(mount.filesystem, "graph_provider", None)
        if gp is None:
            msg = f"No graph on mount at {mount.path}"
            raise RuntimeError(msg)
        return gp, mount

    def resolve_graph(self, path: str) -> GraphProvider:
        """Return the graph provider for the mount owning *path*."""
        gp, _mount = self.resolve_graph_with_mount(path)
        return gp

    def resolve_graph_any_with_mount(self, path: str | None = None) -> tuple[GraphProvider, Mount]:
        """Get ``(graph_provider, mount)`` for a path, or first available."""
        if path is not None:
            return self.resolve_graph_with_mount(path)
        for mount in self.registry.list_visible_mounts():
            gp = getattr(mount.filesystem, "graph_provider", None)
            if gp is not None:
                return gp, mount
        msg = "No graph available on any mount"
        raise RuntimeError(msg)

    def resolve_graph_any(self, path: str | None = None) -> GraphProvider:
        """Get graph for a specific path, or first available mount's graph."""
        gp, _mount = self.resolve_graph_any_with_mount(path)
        return gp
