"""Share Link Service - Extracted from NexusFSShareLinksMixin.

This service handles all share link operations:
- Create share links with configurable access (viewer/editor/owner)
- Password protection, expiration, and download limits
- Access validation and logging
- Revocation support

Implements W3C TAG Capability URL pattern for secure file sharing.

Phase 2: Core Refactoring (Issue #1287)
Extracted from: nexus_fs_share_links.py (678 lines)
"""

import asyncio
import hashlib
import logging
import secrets
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Any

from nexus.contracts.constants import ROOT_ZONE_ID
from nexus.contracts.exceptions import (
    AccessDeniedError,
    NexusFileNotFoundError,
    ServiceUnavailableError,
    ValidationError,
)
from nexus.core.path_utils import validate_path
from nexus.lib.rpc_decorator import rpc_expose

logger = logging.getLogger(__name__)

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


class ShareLinkService:
    """Independent share link service extracted from NexusFS.

    Implements W3C TAG Capability URL pattern:
    - Share links are unguessable tokens (UUID v4, 122 bits entropy)
    - Token IS the credential - no session/ReBAC for anonymous access
    - Server validates token against DB on each request

    Architecture:
        - Uses NexusFS directly for filesystem operations
        - Uses session_factory for ShareLinkModel database access
        - Permission checking via NexusFS service("rebac") operations
        - Clean dependency injection
    """

    def __init__(
        self,
        nexus_fs: Any,
        record_store: "RecordStoreABC | None" = None,
        enforce_permissions: bool = True,
    ):
        """Initialize share link service.

        Args:
            nexus_fs: NexusFS instance for filesystem and DB access
            record_store: Optional RecordStoreABC for direct DB access (preferred over nexus_fs)
            enforce_permissions: Whether to enforce permission checks
        """
        self._nexus_fs = nexus_fs
        self._record_store = record_store
        self._enforce_permissions = enforce_permissions
        logger.info("[ShareLinkService] Initialized")

    # =========================================================================
    # Password Hashing
    # =========================================================================

    @staticmethod
    def _hash_password(password: str) -> str:
        """Hash password for share link protection.

        Uses SHA-256 with random salt.

        Args:
            password: Plain text password

        Returns:
            Hashed password string in format "salt:hash"
        """
        salt = secrets.token_hex(16)
        hash_input = f"{salt}:{password}".encode()
        password_hash = hashlib.sha256(hash_input).hexdigest()
        return f"{salt}:{password_hash}"

    @staticmethod
    def _verify_password(password: str, password_hash: str) -> bool:
        """Verify password against stored hash.

        Args:
            password: Plain text password to verify
            password_hash: Stored hash in format "salt:hash"

        Returns:
            True if password matches
        """
        try:
            salt, stored_hash = password_hash.split(":", 1)
            hash_input = f"{salt}:{password}".encode()
            computed_hash = hashlib.sha256(hash_input).hexdigest()
            return secrets.compare_digest(computed_hash, stored_hash)
        except (ValueError, AttributeError):
            return False

    # =========================================================================
    # Context Extraction Helper
    # =========================================================================

    @staticmethod
    def _extract_context_info(
        context: "OperationContext | None",
    ) -> tuple[str, str, bool]:
        """Extract zone_id, user_id, is_admin from context.

        Args:
            context: Operation context

        Returns:
            Tuple of (zone_id, user_id, is_admin)
        """
        zone_id = ROOT_ZONE_ID
        user_id = "anonymous"
        is_admin = False
        if context:
            zone_id = getattr(context, "zone_id", None) or ROOT_ZONE_ID
            user_id = (
                getattr(context, "user_id", None)
                or getattr(context, "subject_id", None)
                or "anonymous"
            )
            is_admin = getattr(context, "is_admin", False)
        return zone_id, user_id, is_admin

    # =========================================================================
    # Public API
    # =========================================================================

    @rpc_expose(description="Create a share link for a file or directory")
    async def create_share_link(
        self,
        path: str,
        permission_level: str = "viewer",
        expires_in_hours: int | None = None,
        max_access_count: int | None = None,
        password: str | None = None,
        context: "OperationContext | None" = None,
    ) -> dict[str, Any]:
        """Create a shareable link for a file or directory.

        Args:
            path: Virtual path to share
            permission_level: Access level - 'viewer', 'editor', or 'owner'
            expires_in_hours: Optional hours until link expires
            max_access_count: Optional max number of accesses
            password: Optional password protection
            context: Operation context with user/zone info

        Returns:
            Dict with link_id, path, permission_level, etc.

        Raises:
            ValidationError: If permission_level or path is invalid.
            AccessDeniedError: If user lacks write permission.
            ServiceUnavailableError: If database not configured.
        """

        async def _impl() -> dict[str, Any]:
            from nexus.storage.models import ShareLinkModel

            valid_levels = {"viewer", "editor", "owner"}
            if permission_level not in valid_levels:
                raise ValidationError(
                    f"Invalid permission_level '{permission_level}'. Must be one of: {valid_levels}",
                )

            try:
                normalized_path = validate_path(path, allow_root=True)
            except Exception as e:
                raise ValidationError(f"Invalid path: {e}") from e

            zone_id, created_by, _ = self._extract_context_info(context)

            # Check write permission to create share link
            if self._enforce_permissions:
                if not context:
                    raise AccessDeniedError(
                        "Permission denied: authentication required to create share links",
                    )
                has_perm = self._nexus_fs.service("rebac").rebac_check_sync(
                    subject=("user", created_by),
                    permission="write",
                    object=("file", normalized_path),
                    zone_id=zone_id,
                )
                if not has_perm:
                    raise AccessDeniedError(
                        f"Permission denied: cannot create share link for '{path}'",
                    )

            # Determine resource type
            resource_type = "file"
            if self._nexus_fs.access(normalized_path):
                stat = self._nexus_fs._kernel.sys_stat(normalized_path, ROOT_ZONE_ID)
                if stat and stat.get("is_directory", False):
                    resource_type = "directory"

            # Calculate expiration
            expires_at = None
            if expires_in_hours is not None and expires_in_hours > 0:
                expires_at = datetime.now(UTC) + timedelta(hours=expires_in_hours)

            # Hash password if provided
            password_hash = None
            if password:
                password_hash = self._hash_password(password)

            # Create share link record
            session_factory = (
                self._record_store.session_factory
                if self._record_store
                else getattr(self._nexus_fs, "SessionLocal", None)
            )
            if session_factory is None:
                raise ServiceUnavailableError("Database not configured for share links")

            with session_factory() as session:
                share_link = ShareLinkModel(
                    resource_type=resource_type,
                    resource_id=normalized_path,
                    permission_level=permission_level,
                    zone_id=zone_id,
                    created_by=created_by,
                    password_hash=password_hash,
                    expires_at=expires_at,
                    max_access_count=max_access_count,
                    access_count=0,
                )
                session.add(share_link)
                session.commit()

                return {
                    "link_id": share_link.link_id,
                    "path": normalized_path,
                    "permission_level": permission_level,
                    "resource_type": resource_type,
                    "expires_at": expires_at.isoformat() if expires_at else None,
                    "max_access_count": max_access_count,
                    "has_password": password_hash is not None,
                    "created_at": share_link.created_at.isoformat(),
                }

        return await _impl()

    @rpc_expose(description="Get details of a share link")
    async def get_share_link(
        self,
        link_id: str,
        context: "OperationContext | None" = None,
    ) -> dict[str, Any]:
        """Get details of a share link.

        Args:
            link_id: The share link ID/token
            context: Operation context (used for authorization)

        Returns:
            Dict with link details.

        Raises:
            NexusFileNotFoundError: If link not found.
            ServiceUnavailableError: If database not configured.
        """

        def _impl() -> dict[str, Any]:
            from sqlalchemy import select

            from nexus.storage.models import ShareLinkModel

            session_factory = (
                self._record_store.session_factory
                if self._record_store
                else getattr(self._nexus_fs, "SessionLocal", None)
            )
            if session_factory is None:
                raise ServiceUnavailableError("Database not configured")

            with session_factory() as session:
                link = (
                    session.execute(select(ShareLinkModel).filter_by(link_id=link_id))
                    .scalars()
                    .first()
                )
                if not link:
                    raise NexusFileNotFoundError(
                        link_id, message=f"Share link not found: {link_id}"
                    )

                zone_id, user_id, is_admin = self._extract_context_info(context)
                is_owner = link.created_by == user_id and link.zone_id == zone_id

                if not is_owner and not is_admin:
                    return {
                        "link_id": link.link_id,
                        "resource_type": link.resource_type,
                        "permission_level": link.permission_level,
                        "is_valid": link.is_valid(),
                        "has_password": link.password_hash is not None,
                    }

                return {
                    "link_id": link.link_id,
                    "path": link.resource_id,
                    "resource_type": link.resource_type,
                    "permission_level": link.permission_level,
                    "zone_id": link.zone_id,
                    "created_by": link.created_by,
                    "created_at": link.created_at.isoformat(),
                    "expires_at": link.expires_at.isoformat() if link.expires_at else None,
                    "max_access_count": link.max_access_count,
                    "access_count": link.access_count,
                    "last_accessed_at": (
                        link.last_accessed_at.isoformat() if link.last_accessed_at else None
                    ),
                    "revoked_at": link.revoked_at.isoformat() if link.revoked_at else None,
                    "revoked_by": link.revoked_by,
                    "has_password": link.password_hash is not None,
                    "is_valid": link.is_valid(),
                }

        return await asyncio.to_thread(_impl)

    @rpc_expose(description="List share links created by the current user")
    async def list_share_links(
        self,
        path: str | None = None,
        include_revoked: bool = False,
        include_expired: bool = False,
        context: "OperationContext | None" = None,
    ) -> dict[str, Any]:
        """List share links created by the current user.

        Args:
            path: Optional - filter by resource path
            include_revoked: Include revoked links
            include_expired: Include expired links
            context: Operation context with user/zone info

        Returns:
            Dict with links list and count.

        Raises:
            ServiceUnavailableError: If database not configured.
        """

        def _impl() -> dict[str, Any]:
            from sqlalchemy import select

            from nexus.storage.models import ShareLinkModel

            session_factory = (
                self._record_store.session_factory
                if self._record_store
                else getattr(self._nexus_fs, "SessionLocal", None)
            )
            if session_factory is None:
                raise ServiceUnavailableError("Database not configured")

            zone_id, user_id, is_admin = self._extract_context_info(context)

            with session_factory() as session:
                stmt = select(ShareLinkModel).filter_by(zone_id=zone_id)

                if not is_admin:
                    stmt = stmt.filter_by(created_by=user_id)

                if path:
                    normalized_path = validate_path(path, allow_root=True)
                    stmt = stmt.filter_by(resource_id=normalized_path)

                if not include_revoked:
                    stmt = stmt.filter(ShareLinkModel.revoked_at.is_(None))

                if not include_expired:
                    now = datetime.now(UTC)
                    stmt = stmt.filter(
                        (ShareLinkModel.expires_at.is_(None)) | (ShareLinkModel.expires_at >= now)
                    )

                stmt = stmt.order_by(ShareLinkModel.created_at.desc())
                links = session.execute(stmt).scalars().all()

                result = [
                    {
                        "link_id": link.link_id,
                        "path": link.resource_id,
                        "resource_type": link.resource_type,
                        "permission_level": link.permission_level,
                        "created_at": link.created_at.isoformat(),
                        "expires_at": link.expires_at.isoformat() if link.expires_at else None,
                        "max_access_count": link.max_access_count,
                        "access_count": link.access_count,
                        "has_password": link.password_hash is not None,
                        "is_valid": link.is_valid(),
                        "revoked_at": link.revoked_at.isoformat() if link.revoked_at else None,
                    }
                    for link in links
                ]

                return {"links": result, "count": len(result)}

        return await asyncio.to_thread(_impl)

    @rpc_expose(description="Revoke a share link")
    async def revoke_share_link(
        self,
        link_id: str,
        context: "OperationContext | None" = None,
    ) -> dict[str, Any]:
        """Revoke a share link, immediately disabling access.

        Args:
            link_id: The share link ID to revoke
            context: Operation context with user/zone info

        Returns:
            Dict with link_id, revoked status, timestamps.

        Raises:
            NexusFileNotFoundError: If link not found.
            AccessDeniedError: If user is not owner or admin.
            ValidationError: If link is already revoked.
            ServiceUnavailableError: If database not configured.
        """

        def _impl() -> dict[str, Any]:
            from sqlalchemy import select

            from nexus.storage.models import ShareLinkModel

            session_factory = (
                self._record_store.session_factory
                if self._record_store
                else getattr(self._nexus_fs, "SessionLocal", None)
            )
            if session_factory is None:
                raise ServiceUnavailableError("Database not configured")

            zone_id, user_id, is_admin = self._extract_context_info(context)

            with session_factory() as session:
                link = (
                    session.execute(select(ShareLinkModel).filter_by(link_id=link_id))
                    .scalars()
                    .first()
                )
                if not link:
                    raise NexusFileNotFoundError(
                        link_id, message=f"Share link not found: {link_id}"
                    )

                is_owner = link.created_by == user_id and link.zone_id == zone_id
                if not is_owner and not is_admin:
                    raise AccessDeniedError(
                        "Permission denied: only link creator or admin can revoke",
                    )

                if link.revoked_at is not None:
                    raise ValidationError("Share link is already revoked")

                link.revoked_at = datetime.now(UTC)
                link.revoked_by = user_id
                session.commit()

                return {
                    "link_id": link_id,
                    "revoked": True,
                    "revoked_at": link.revoked_at.isoformat(),
                    "revoked_by": link.revoked_by,
                }

        return await asyncio.to_thread(_impl)

    @rpc_expose(description="Access a shared resource via share link")
    async def access_share_link(
        self,
        link_id: str,
        password: str | None = None,
        ip_address: str | None = None,
        user_agent: str | None = None,
        context: "OperationContext | None" = None,
    ) -> dict[str, Any]:
        """Validate and access a shared resource via share link.

        This method:
        1. Validates the share link (exists, not expired, not revoked, not over limit)
        2. Verifies password if required
        3. Logs the access attempt
        4. Returns resource access info if valid

        Args:
            link_id: The share link token
            password: Password if link is password-protected
            ip_address: Client IP for logging
            user_agent: Client user agent for logging
            context: Optional operation context (for authenticated access)

        Returns:
            Dict with link_id, path, permission_level, access_granted, etc.

        Raises:
            NexusFileNotFoundError: If link not found.
            AccessDeniedError: If link revoked, expired, or wrong password.
            ValidationError: If access limit exceeded or password required.
            ServiceUnavailableError: If database not configured.
        """

        def _impl() -> dict[str, Any]:
            from sqlalchemy import select

            from nexus.storage.models import ShareLinkAccessLogModel, ShareLinkModel

            session_factory = (
                self._record_store.session_factory
                if self._record_store
                else getattr(self._nexus_fs, "SessionLocal", None)
            )
            if session_factory is None:
                raise ServiceUnavailableError("Database not configured")

            accessed_by_user_id = None
            accessed_by_zone_id = None
            if context:
                accessed_by_user_id = getattr(context, "user_id", None) or getattr(
                    context, "subject_id", None
                )
                accessed_by_zone_id = getattr(context, "zone_id", None)

            with session_factory() as session:
                link = (
                    session.execute(select(ShareLinkModel).filter_by(link_id=link_id))
                    .scalars()
                    .first()
                )

                def log_access(success: bool, failure_reason: str | None = None) -> None:
                    log_entry = ShareLinkAccessLogModel(
                        link_id=link_id,
                        ip_address=ip_address,
                        user_agent=user_agent,
                        success=1 if success else 0,
                        failure_reason=failure_reason,
                        accessed_by_user_id=accessed_by_user_id,
                        accessed_by_zone_id=accessed_by_zone_id,
                    )
                    session.add(log_entry)

                if not link:
                    raise NexusFileNotFoundError(link_id, message="Share link not found or invalid")

                if link.revoked_at is not None:
                    log_access(False, "revoked")
                    session.commit()
                    raise AccessDeniedError("Share link has been revoked")

                now = datetime.now(UTC)
                if link.expires_at is not None and link.expires_at < now:
                    log_access(False, "expired")
                    session.commit()
                    raise AccessDeniedError("Share link has expired")

                if link.password_hash is not None:
                    if not password:
                        log_access(False, "password_required")
                        session.commit()
                        raise ValidationError("Password required for this share link")
                    if not self._verify_password(password, link.password_hash):
                        log_access(False, "wrong_password")
                        session.commit()
                        raise AccessDeniedError("Incorrect password")

                # Atomic access-count increment with row-level guard
                # to prevent races from exceeding max_access_count.
                if link.max_access_count is not None:
                    from sqlalchemy import update as sa_update

                    from nexus.storage.models import ShareLinkModel as SLM

                    result = session.execute(
                        sa_update(SLM)
                        .where(SLM.link_id == link_id)
                        .where(
                            (SLM.max_access_count.is_(None))
                            | (SLM.access_count < SLM.max_access_count)
                        )
                        .values(
                            access_count=SLM.access_count + 1,
                            last_accessed_at=now,
                        )
                    )
                    if getattr(result, "rowcount", 0) == 0:
                        log_access(False, "limit_exceeded")
                        session.commit()
                        raise ValidationError("Share link access limit exceeded")
                    log_access(True)
                    session.commit()
                    # Refresh to get updated values
                    session.refresh(link)
                else:
                    link.access_count += 1
                    link.last_accessed_at = now
                    log_access(True)
                    session.commit()

                return {
                    "link_id": link.link_id,
                    "path": link.resource_id,
                    "resource_type": link.resource_type,
                    "permission_level": link.permission_level,
                    "zone_id": link.zone_id,
                    "access_granted": True,
                    "remaining_accesses": (
                        link.max_access_count - link.access_count if link.max_access_count else None
                    ),
                    "expires_at": link.expires_at.isoformat() if link.expires_at else None,
                }

        return await asyncio.to_thread(_impl)

    @rpc_expose(description="Get access logs for a share link")
    async def get_share_link_access_logs(
        self,
        link_id: str,
        limit: int = 100,
        context: "OperationContext | None" = None,
    ) -> dict[str, Any]:
        """Get access logs for a share link.

        Args:
            link_id: The share link ID
            limit: Maximum number of log entries to return
            context: Operation context (must be owner or admin)

        Returns:
            Dict with logs list and count.

        Raises:
            NexusFileNotFoundError: If link not found.
            AccessDeniedError: If user is not owner or admin.
            ServiceUnavailableError: If database not configured.
        """

        def _impl() -> dict[str, Any]:
            from sqlalchemy import select

            from nexus.storage.models import ShareLinkAccessLogModel, ShareLinkModel

            session_factory = (
                self._record_store.session_factory
                if self._record_store
                else getattr(self._nexus_fs, "SessionLocal", None)
            )
            if session_factory is None:
                raise ServiceUnavailableError("Database not configured")

            zone_id, user_id, is_admin = self._extract_context_info(context)

            with session_factory() as session:
                link = (
                    session.execute(select(ShareLinkModel).filter_by(link_id=link_id))
                    .scalars()
                    .first()
                )
                if not link:
                    raise NexusFileNotFoundError(
                        link_id, message=f"Share link not found: {link_id}"
                    )

                is_owner = link.created_by == user_id and link.zone_id == zone_id
                if not is_owner and not is_admin:
                    raise AccessDeniedError(
                        "Permission denied: only link creator or admin can view logs",
                    )

                logs = (
                    session.execute(
                        select(ShareLinkAccessLogModel)
                        .filter_by(link_id=link_id)
                        .order_by(ShareLinkAccessLogModel.accessed_at.desc())
                        .limit(limit)
                    )
                    .scalars()
                    .all()
                )

                result = [
                    {
                        "log_id": log.log_id,
                        "accessed_at": log.accessed_at.isoformat(),
                        "ip_address": log.ip_address,
                        "user_agent": log.user_agent,
                        "success": bool(log.success),
                        "failure_reason": log.failure_reason,
                        "accessed_by_user_id": log.accessed_by_user_id,
                        "accessed_by_zone_id": log.accessed_by_zone_id,
                    }
                    for log in logs
                ]

                return {"logs": result, "count": len(result)}

        return await asyncio.to_thread(_impl)
