"""Version Service - Extracted from NexusFSVersionsMixin.

This service handles all file version management operations:
- Get specific file versions
- List version history
- Rollback to previous versions
- Compare (diff) between versions

Phase 2: Core Refactoring (Issue #988, Task 2.5)
Extracted from: nexus_fs_versions.py (300 lines)
"""

import asyncio
import builtins
import difflib
import logging
from typing import TYPE_CHECKING, Any

from nexus.contracts.constants import ROOT_ZONE_ID
from nexus.lib.rpc_decorator import rpc_expose

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from nexus.contracts.protocols.rebac import ReBACBrickProtocol
    from nexus.contracts.types import OperationContext
    from nexus.storage.record_store import RecordStoreABC


class VersionService:
    """Independent version service extracted from NexusFS.

    Handles all file version management operations:
    - Retrieve specific versions from CAS
    - List version history with metadata
    - Rollback files to previous versions
    - Compare versions (metadata and content diffs)

    Architecture:
        - Works with metadata store for version tracking
        - Uses CAS (Content-Addressable Storage) for version content
        - Permission checking via PermissionEnforcer
        - Clean dependency injection

    Example:
        ```python
        version_service = VersionService(
            metadata_store=metadata,
            cas_store=cas,
            permission_enforcer=permissions,
            kernel=kernel,
            dlc=dlc,
        )

        # Get a specific version
        content = version_service.get_version(
            path="/workspace/file.txt",
            version=3,
            context=context
        )

        # List all versions
        versions = version_service.list_versions(
            path="/workspace/file.txt",
            context=context
        )

        # Rollback to previous version
        version_service.rollback(
            path="/workspace/file.txt",
            version=2,
            context=context
        )

        # Compare versions
        diff = version_service.diff_versions(
            path="/workspace/file.txt",
            v1=2,
            v2=3,
            mode="unified",
            context=context
        )
        ```
    """

    def __init__(
        self,
        metadata_store: Any,
        cas_store: Any,  # Backend with read_content method
        permission_enforcer: Any = None,
        dlc: Any = None,
        rebac_manager: "ReBACBrickProtocol | None" = None,
        enforce_permissions: bool = True,
        record_store: "RecordStoreABC | None" = None,
    ):
        """Initialize version service.

        Args:
            metadata_store: Metadata store for version tracking
            cas_store: Backend with read_content method (typically CAS-enabled backend)
            permission_enforcer: Async permission enforcer for access control
            dlc: DriverLifecycleCoordinator for routing + backend refs
            rebac_manager: ReBAC manager for permission checks
            enforce_permissions: Whether to enforce permission checks
            record_store: RecordStoreABC instance providing session_factory for version history queries
        """
        self.metadata = metadata_store
        # Accept either a bare ``PyKernel`` or a legacy proxy shim.
        self._kernel = metadata_store
        self.cas = cas_store
        self._permission_enforcer = permission_enforcer
        self._dlc = dlc
        self._rebac_manager = rebac_manager
        self._enforce_permissions = enforce_permissions
        self._session_factory = record_store.session_factory if record_store else None

        logger.info("[VersionService] Initialized")

    # =========================================================================
    # Public API: Version Retrieval
    # =========================================================================

    @rpc_expose(description="Get specific file version")
    async def get_version(
        self,
        path: str,
        version: int,
        context: "OperationContext | None" = None,
    ) -> bytes:
        """Get a specific version of a file.

        Retrieves the content for a specific version from CAS using the
        version's content hash.

        Args:
            path: Virtual file path
            version: Version number to retrieve (1-indexed)
            context: Operation context for permission checks

        Returns:
            File content as bytes for the specified version

        Raises:
            NexusFileNotFoundError: If file or version doesn't exist
            PermissionDeniedError: If user lacks READ permission
            ValueError: If version number is invalid

        Examples:
            # Get a specific version of a file
            content_v2 = service.get_version("/workspace/data.txt", version=2)

            # Get version with specific context
            content = service.get_version(
                "/workspace/file.txt",
                version=5,
                context=context
            )
        """
        from nexus.contracts.exceptions import NexusFileNotFoundError

        # Validate and normalize path
        path = self._validate_path(path)

        # Validate version number
        if version < 1:
            raise ValueError(f"Version must be positive integer, got: {version}")

        # Check READ permission
        await self._check_read_permission(path, context)

        # Get version metadata via VersionManager + session_factory
        # (RaftMetadataStore lacks get_version(); use SQL version history instead)
        if self._session_factory:
            from nexus.storage.version_manager import VersionManager

            factory = self._session_factory

            def _get_version_meta() -> Any:
                with factory() as session:
                    return VersionManager.get_version(session, f"{path}@{version}")

            version_meta = await asyncio.to_thread(_get_version_meta)
        else:
            version_meta = None

        if version_meta is None:
            raise NexusFileNotFoundError(f"{path} (version {version})")

        # Ensure version has content hash
        if version_meta.content_id is None:
            raise NexusFileNotFoundError(f"{path} (version {version}) has no content")

        # Read content from backend using DLC
        if self._dlc is None:
            raise RuntimeError("DLC not configured for VersionService")

        zone_id = (context.zone_id if context else None) or ROOT_ZONE_ID
        # Read content via kernel syscall — sys_read_raw raises on missing path.
        _kernel = getattr(self, "_kernel", None) or getattr(self._dlc, "_kernel", None)
        if _kernel is None:
            raise RuntimeError("No kernel available for version read")

        return await asyncio.to_thread(_kernel.sys_read_raw, path, zone_id)

    @rpc_expose(description="List file versions")
    async def list_versions(
        self,
        path: str,
        context: "OperationContext | None" = None,
    ) -> builtins.list[dict[str, Any]]:
        """List all versions of a file.

        Returns version history with metadata for each version in reverse
        chronological order (newest first).

        Args:
            path: Virtual file path
            context: Operation context for permission checks

        Returns:
            List of version info dictionaries, each containing:
                - version: Version number (int)
                - created_at: Timestamp when version was created (str)
                - created_by: User who created the version (str)
                - size: File size in bytes (int)
                - content_id: Content hash (str)
                - is_rollback: Whether this is a rollback version (bool)
                - rollback_from: Original version number if rollback (int|None)

        Raises:
            NexusFileNotFoundError: If file doesn't exist
            PermissionDeniedError: If user lacks READ permission

        Examples:
            # List all versions
            versions = service.list_versions("/workspace/data.txt", context=context)

            for v in versions:
                print(f"Version {v['version']}: {v['size']} bytes, {v['created_at']}")

            # Get latest version number
            latest = versions[0]['version'] if versions else 0
        """
        # Validate and normalize path
        path = self._validate_path(path)

        # Check READ permission
        await self._check_read_permission(path, context)

        # Task #45: Query version history from RecordStore (VersionHistoryModel)
        # instead of metadata store (sled doesn't track version history).
        if self._session_factory:
            from nexus.storage.version_manager import VersionManager

            factory = self._session_factory  # bind for closure

            def _query_versions() -> builtins.list[dict[str, Any]]:
                with factory() as session:
                    return VersionManager.list_versions(session, path)

            return await asyncio.to_thread(_query_versions)
        return []

    # =========================================================================
    # Public API: Version Operations
    # =========================================================================

    @rpc_expose(description="Rollback file to previous version")
    async def rollback(
        self,
        path: str,
        version: int,
        context: "OperationContext | None" = None,
    ) -> None:
        """Rollback file to a previous version.

        Updates the file to point to an older version's content from CAS.
        Creates a new version entry marking this as a rollback operation.

        Args:
            path: Virtual file path
            version: Version number to rollback to
            context: Operation context for permission checks

        Raises:
            NexusFileNotFoundError: If file or version doesn't exist
            PermissionDeniedError: If user lacks WRITE permission
            ValueError: If trying to rollback to current version

        Examples:
            # Rollback to previous version
            service.rollback("/workspace/file.txt", version=2, context=context)

            # After rollback, a new version is created
            versions = service.list_versions("/workspace/file.txt", context=context)
            # Latest version will have is_rollback=True, rollback_from=2

        Note:
            Rollback creates a new version rather than deleting history.
            This preserves the audit trail and allows rolling forward again.
        """
        logger.info(f"[ROLLBACK] path={path}, version={version}")

        # Validate and normalize path
        path = self._validate_path(path)

        # Check WRITE permission
        await self._check_write_permission(path, context)

        # DLC presence is a precondition for rollback (derives from mount context).
        if self._dlc is None:
            raise RuntimeError("DLC not configured for VersionService")

        # Perform rollback via VersionManager + session_factory
        # (RaftMetadataStore lacks rollback(); use SQL version history instead)
        created_by = context.user_id if context else None

        if not self._session_factory:
            raise RuntimeError("session_factory required for rollback operation")

        from nexus.storage.version_manager import VersionManager

        factory = self._session_factory

        def _do_rollback() -> tuple[str, int] | None:
            with factory() as session:
                VersionManager.rollback(session, path, version, created_by=created_by)
                session.commit()

                # Return the target version's content_id so we can
                # update the in-memory metadata store for consistent reads.
                from sqlalchemy import select

                from nexus.storage.models import FilePathModel

                stmt = select(FilePathModel.content_id, FilePathModel.size_bytes).where(
                    FilePathModel.virtual_path == path,
                    FilePathModel.deleted_at.is_(None),
                )
                row = session.execute(stmt).one_or_none()
                return (row[0], row[1]) if row else None

        result = await asyncio.to_thread(_do_rollback)

        # Update in-memory metadata store so subsequent reads return rolled-back content
        if result is not None:
            new_hash, new_size = result
            stat = self._kernel.sys_stat(path, "root")
            if stat is not None:
                self._kernel.sys_setattr(
                    path,
                    0,  # UPDATE existing entry
                    content_id=new_hash,
                    size=new_size,
                )

        # Cache invalidation is now handled inside the Rust ``LocalMetaStore``
        # impl on every ``put`` / ``delete`` (commit U); the Python-side
        # ``_cache_enabled`` / ``_cache`` flags are gone with the proxy.

        logger.info(f"[ROLLBACK] Completed for path={path}")

    @rpc_expose(description="Compare file versions")
    async def diff_versions(
        self,
        path: str,
        v1: int,
        v2: int,
        mode: str = "metadata",
        context: "OperationContext | None" = None,
    ) -> dict[str, Any] | str:
        """Compare two versions of a file.

        Supports multiple comparison modes:
        - "metadata": Compare version metadata only (fast)
        - "content": Unified diff format (like git diff)

        Args:
            path: Virtual file path
            v1: First version number
            v2: Second version number
            mode: Diff mode ("metadata" or "content")
            context: Operation context for permission checks

        Returns:
            If mode="metadata": Dictionary with:
                - v1: Version 1 metadata dict
                - v2: Version 2 metadata dict
                - size_delta: Size difference in bytes (int)
                - time_delta: Time between versions in seconds (float)
                - same_content: Whether content is identical (bool)

            If mode="content":
                String containing the unified diff

        Raises:
            NexusFileNotFoundError: If file or either version doesn't exist
            PermissionDeniedError: If user lacks READ permission
            ValueError: If mode is invalid or versions are not text files

        Examples:
            # Quick metadata comparison
            diff = service.diff_versions(
                "/workspace/file.txt",
                v1=2,
                v2=3,
                mode="metadata",
                context=context
            )
            print(f"Size changed by {diff['size_delta']} bytes")

            # Get unified diff (like git diff)
            diff_text = service.diff_versions(
                "/workspace/file.txt",
                v1=2,
                v2=3,
                mode="content",
                context=context
            )
            print(diff_text)

        Note:
            Content diffs require loading both versions into memory and only
            work for text files. Binary files will raise ValueError.
        """
        # Validate and normalize path
        path = self._validate_path(path)

        # Check READ permission
        await self._check_read_permission(path, context)

        # Validate mode
        if mode not in ("metadata", "content"):
            raise ValueError(f"Invalid mode: {mode}. Must be 'metadata' or 'content'")

        # Get metadata diff via VersionManager + session_factory
        # (RaftMetadataStore lacks get_version_diff(); use SQL version history instead)
        if self._session_factory:
            from nexus.storage.version_manager import VersionManager

            factory = self._session_factory

            def _get_diff() -> dict[str, Any]:
                with factory() as session:
                    return VersionManager.get_version_diff(session, path, v1, v2)

            meta_diff = await asyncio.to_thread(_get_diff)
        else:
            raise RuntimeError("session_factory required for diff_versions")

        if mode == "metadata":
            return meta_diff

        # Content diff mode
        if not meta_diff["content_changed"]:
            return "(no content changes)"

        # Retrieve both versions' content
        content1 = (await self.get_version(path, v1, context=context)).decode(
            "utf-8", errors="replace"
        )
        content2 = (await self.get_version(path, v2, context=context)).decode(
            "utf-8", errors="replace"
        )

        # Generate unified diff
        lines1 = content1.splitlines(keepends=True)
        lines2 = content2.splitlines(keepends=True)

        diff_lines = list(
            difflib.unified_diff(
                lines1,
                lines2,
                fromfile=f"{path} (v{v1})",
                tofile=f"{path} (v{v2})",
                lineterm="",
            )
        )

        return "\n".join(diff_lines)

    # =========================================================================
    # Helper Methods
    # =========================================================================

    async def _check_read_permission(self, path: str, context: "OperationContext | None") -> None:
        """Check if user has read permission for path.

        Args:
            path: File path
            context: Operation context

        Raises:
            PermissionError: If permission denied
        """
        from nexus.contracts.types import Permission

        # Skip permission checks if not enforcing or no enforcer configured
        if not self._enforce_permissions or self._permission_enforcer is None:
            return

        # Skip permission checks if no context provided
        if context is None:
            return

        # Skip permission checks for system/admin contexts
        if context.is_system or context.is_admin:
            return

        # Check READ permission via permission enforcer
        has_permission = await self._permission_enforcer.check_permission(
            path, Permission.READ, context
        )

        if not has_permission:
            raise PermissionError(f"User '{context.user_id}' lacks READ permission for: {path}")

    async def _check_write_permission(self, path: str, context: "OperationContext | None") -> None:
        """Check if user has write permission for path.

        Args:
            path: File path
            context: Operation context

        Raises:
            PermissionError: If permission denied
        """
        from nexus.contracts.types import Permission

        # Skip permission checks if not enforcing or no enforcer configured
        if not self._enforce_permissions or self._permission_enforcer is None:
            return

        # Skip permission checks if no context provided
        if context is None:
            return

        # Skip permission checks for system/admin contexts
        if context.is_system or context.is_admin:
            return

        # Check WRITE permission via permission enforcer
        has_permission = await self._permission_enforcer.check_permission(
            path, Permission.WRITE, context
        )

        if not has_permission:
            raise PermissionError(f"User '{context.user_id}' lacks WRITE permission for: {path}")

    def _validate_path(self, path: str) -> str:
        """Validate and normalize path.

        Delegates to shared path validation utility for security checks.

        Args:
            path: Path to validate

        Returns:
            Normalized path

        Raises:
            InvalidPathError: If path is invalid
        """
        from nexus.core.path_utils import validate_path

        return validate_path(path, allow_root=True)


# =============================================================================
# Phase 2 Extraction: Complete ✅
# =============================================================================
#
# All 4 version methods extracted with async support.
# Bug fix: get_version/diff_versions use VersionManager + session_factory
# (RaftMetadataStore lacks get_version/get_version_diff methods).
# Unit tests: tests/unit/services/test_version_service.py
#
# Lines extracted: ~300 / 300 (100%)
# Files affected: 1 created, 0 modified
#
# This is a phased extraction to maintain working code at each step.
#
