"""
SpaceService - Main orchestration service for Information Spaces.

This module provides the primary API for space operations, coordinating
between repositories, event handling, and transclusion context management.
"""

from typing import List, Optional, Dict, Any
from pathlib import Path

from ..models import (
    InformationSpace,
    SpaceDocument,
    SpaceVariable,
    TransclusionReference,
    SpaceStatus,
    SpaceConfig,
    SpaceMetadata,
)
from ..repositories.interfaces import (
    ISpaceRepository,
    IDocumentAssociationRepository,
    IVariableRepository,
    IReferenceRepository,
)
from ..events import (
    EventBus,
    SpaceEvent,
    SpaceEventType,
    space_created_event,
    space_updated_event,
    space_deleted_event,
    document_added_event,
    document_removed_event,
    document_content_changed_event,
)


class SpaceService:
    """
    Main orchestration service for Information Space operations.

    Provides a high-level API for managing spaces, documents, variables,
    and transclusion references. This service coordinates between the
    repository layer and future event/rendering systems.

    Usage:
        service = SpaceService(
            space_repo=SqliteSpaceRepository(db_path),
            document_repo=SqliteDocumentRepository(db_path),
            variable_repo=SqliteVariableRepository(db_path),
            reference_repo=SqliteReferenceRepository(db_path),
        )

        # Create a space
        space = service.create_space(name="my-docs", description="My documentation")

        # Add documents
        service.add_document(space.id, "/intro.md", document_id="doc-1")
    """

    def __init__(
        self,
        space_repo: ISpaceRepository,
        document_repo: IDocumentAssociationRepository,
        variable_repo: IVariableRepository,
        reference_repo: IReferenceRepository,
        event_bus: Optional[EventBus] = None,
    ):
        """
        Initialize the SpaceService.

        Args:
            space_repo: Repository for space CRUD operations
            document_repo: Repository for document associations
            variable_repo: Repository for space variables
            reference_repo: Repository for transclusion references
            event_bus: Optional event bus for emitting events
        """
        self._space_repo = space_repo
        self._document_repo = document_repo
        self._variable_repo = variable_repo
        self._reference_repo = reference_repo
        self._event_bus = event_bus

    def _emit(self, event: SpaceEvent) -> None:
        """Emit an event if event bus is configured."""
        if self._event_bus:
            self._event_bus.emit(event)

    # =========================================================================
    # Space CRUD Operations
    # =========================================================================

    def create_space(
        self,
        name: str,
        description: Optional[str] = None,
        config: Optional[SpaceConfig] = None,
        metadata: Optional[SpaceMetadata] = None,
        parent_space_id: Optional[str] = None,
    ) -> InformationSpace:
        """
        Create a new information space.

        Args:
            name: Unique name for the space
            description: Optional description
            config: Optional configuration (defaults provided if None)
            metadata: Optional metadata (defaults provided if None)
            parent_space_id: Optional parent space for hierarchy

        Returns:
            The created InformationSpace

        Raises:
            ValueError: If name is empty or already exists
        """
        if not name or not name.strip():
            raise ValueError("Space name cannot be empty")

        # Validate parent exists if specified
        if parent_space_id:
            parent = self._space_repo.get_by_id(parent_space_id)
            if not parent:
                raise ValueError(f"Parent space '{parent_space_id}' not found")

        space = InformationSpace(
            name=name.strip(),
            description=description,
            config=config or SpaceConfig(),
            metadata=metadata or SpaceMetadata(),
            parent_space_id=parent_space_id,
        )

        created_space = self._space_repo.create(space)
        self._emit(space_created_event(created_space.id, created_space.name))
        return created_space

    def get_space(self, space_id: str) -> Optional[InformationSpace]:
        """
        Get a space by its ID.

        Args:
            space_id: The space ID

        Returns:
            The space if found, None otherwise
        """
        return self._space_repo.get_by_id(space_id)

    def get_space_by_name(self, name: str) -> Optional[InformationSpace]:
        """
        Get a space by its name.

        Args:
            name: The space name

        Returns:
            The space if found, None otherwise
        """
        return self._space_repo.get_by_name(name)

    def list_spaces(self, include_archived: bool = False) -> List[InformationSpace]:
        """
        List all spaces.

        Args:
            include_archived: Whether to include archived spaces

        Returns:
            List of spaces
        """
        return self._space_repo.list_all(include_archived=include_archived)

    def update_space(
        self,
        space_id: str,
        name: Optional[str] = None,
        description: Optional[str] = None,
        config: Optional[SpaceConfig] = None,
        metadata: Optional[SpaceMetadata] = None,
    ) -> InformationSpace:
        """
        Update a space's properties.

        Args:
            space_id: The space ID to update
            name: New name (optional)
            description: New description (optional)
            config: New config (optional)
            metadata: New metadata (optional)

        Returns:
            The updated space

        Raises:
            ValueError: If space not found or name already taken
        """
        space = self._space_repo.get_by_id(space_id)
        if not space:
            raise ValueError(f"Space '{space_id}' not found")

        if name is not None:
            if not name.strip():
                raise ValueError("Space name cannot be empty")
            # Check if name is taken by another space
            existing = self._space_repo.get_by_name(name.strip())
            if existing and existing.id != space_id:
                raise ValueError(f"Space name '{name}' already exists")
            space.name = name.strip()

        if description is not None:
            space.description = description

        if config is not None:
            space.config = config

        if metadata is not None:
            space.metadata = metadata

        updated_space = self._space_repo.update(space)

        # Build changes dict for event
        changes = {}
        if name is not None:
            changes["name"] = name
        if description is not None:
            changes["description"] = description
        if config is not None:
            changes["config"] = "updated"
        if metadata is not None:
            changes["metadata"] = "updated"

        self._emit(space_updated_event(space_id, changes))
        return updated_space

    def delete_space(self, space_id: str, cascade: bool = True) -> bool:
        """
        Delete a space.

        Args:
            space_id: The space ID to delete
            cascade: If True, delete all child spaces too

        Returns:
            True if deleted, False if not found

        Raises:
            ValueError: If space has children and cascade is False
        """
        space = self._space_repo.get_by_id(space_id)
        if not space:
            return False

        children = self._space_repo.get_children(space_id)
        if children and not cascade:
            raise ValueError(
                f"Space '{space_id}' has {len(children)} child spaces. "
                "Set cascade=True to delete them."
            )

        # Delete children first (if cascade)
        if cascade:
            for child in children:
                self.delete_space(child.id, cascade=True)

        space_name = space.name
        result = self._space_repo.delete(space_id)
        if result:
            self._emit(space_deleted_event(space_id, space_name))
        return result

    def activate_space(self, space_id: str) -> InformationSpace:
        """
        Activate a space (change status from draft to active).

        Args:
            space_id: The space ID

        Returns:
            The updated space

        Raises:
            ValueError: If space not found
        """
        space = self._space_repo.get_by_id(space_id)
        if not space:
            raise ValueError(f"Space '{space_id}' not found")

        space.activate()
        updated_space = self._space_repo.update(space)
        self._emit(SpaceEvent(
            event_type=SpaceEventType.SPACE_ACTIVATED,
            space_id=space_id,
            payload={"status": "active"},
        ))
        return updated_space

    def archive_space(self, space_id: str) -> InformationSpace:
        """
        Archive a space.

        Args:
            space_id: The space ID

        Returns:
            The updated space

        Raises:
            ValueError: If space not found
        """
        space = self._space_repo.get_by_id(space_id)
        if not space:
            raise ValueError(f"Space '{space_id}' not found")

        space.archive()
        updated_space = self._space_repo.update(space)
        self._emit(SpaceEvent(
            event_type=SpaceEventType.SPACE_ARCHIVED,
            space_id=space_id,
            payload={"status": "archived"},
        ))
        return updated_space

    def get_child_spaces(self, parent_space_id: str) -> List[InformationSpace]:
        """
        Get all child spaces of a parent.

        Args:
            parent_space_id: The parent space ID

        Returns:
            List of child spaces
        """
        return self._space_repo.get_children(parent_space_id)

    # =========================================================================
    # Document Operations
    # =========================================================================

    def add_document(
        self,
        space_id: str,
        space_path: str,
        document_id: Optional[str] = None,
        order_index: int = 0,
        metadata: Optional[Dict[str, Any]] = None,
        content_hash: Optional[str] = None,
    ) -> SpaceDocument:
        """
        Add a document to a space.

        Args:
            space_id: The space ID
            space_path: Path within the space (e.g., "/intro.md")
            document_id: External document ID (optional)
            order_index: Position in space ordering
            metadata: Document metadata
            content_hash: Content hash for change detection

        Returns:
            The created document association

        Raises:
            ValueError: If space not found or path already exists
        """
        if not self._space_repo.exists(space_id):
            raise ValueError(f"Space '{space_id}' not found")

        # Normalize path
        if not space_path.startswith("/"):
            space_path = "/" + space_path

        document = SpaceDocument(
            space_id=space_id,
            document_id=document_id or "",
            space_path=space_path,
            order_index=order_index,
            metadata=metadata or {},
            content_hash=content_hash,
        )

        added_doc = self._document_repo.add_document(document)
        self._emit(document_added_event(space_id, added_doc.id, space_path))
        return added_doc

    def get_document(self, document_id: str) -> Optional[SpaceDocument]:
        """
        Get a document by its association ID.

        Args:
            document_id: The document association ID

        Returns:
            The document if found, None otherwise
        """
        return self._document_repo.get_document(document_id)

    def get_document_by_path(
        self, space_id: str, space_path: str
    ) -> Optional[SpaceDocument]:
        """
        Get a document by its path within a space.

        Args:
            space_id: The space ID
            space_path: The path within the space

        Returns:
            The document if found, None otherwise
        """
        # Normalize path
        if not space_path.startswith("/"):
            space_path = "/" + space_path
        return self._document_repo.get_by_space_path(space_id, space_path)

    def list_documents(self, space_id: str) -> List[SpaceDocument]:
        """
        List all documents in a space.

        Args:
            space_id: The space ID

        Returns:
            List of documents ordered by order_index
        """
        return self._document_repo.list_by_space(space_id)

    def remove_document(self, document_id: str) -> bool:
        """
        Remove a document from a space.

        Args:
            document_id: The document association ID

        Returns:
            True if removed, False if not found
        """
        # Get document info before removal for event
        document = self._document_repo.get_document(document_id)
        if not document:
            return False

        # Clear any references from this document first
        self._reference_repo.clear_references_from(document_id, document.space_id)

        result = self._document_repo.remove_document(document_id)
        if result:
            self._emit(document_removed_event(
                document.space_id, document_id, document.space_path
            ))
        return result

    def move_document(self, document_id: str, new_path: str) -> SpaceDocument:
        """
        Move a document to a new path within its space.

        Args:
            document_id: The document association ID
            new_path: The new path

        Returns:
            The updated document

        Raises:
            ValueError: If document not found or new path exists
        """
        # Get old path for event
        old_doc = self._document_repo.get_document(document_id)
        old_path = old_doc.space_path if old_doc else None

        if not new_path.startswith("/"):
            new_path = "/" + new_path

        moved_doc = self._document_repo.move_document(document_id, new_path)

        if old_doc:
            self._emit(SpaceEvent(
                event_type=SpaceEventType.DOCUMENT_MOVED,
                space_id=moved_doc.space_id,
                payload={
                    "document_id": document_id,
                    "old_path": old_path,
                    "new_path": new_path,
                },
            ))
        return moved_doc

    def reorder_documents(self, space_id: str, document_ids: List[str]) -> None:
        """
        Reorder documents within a space.

        Args:
            space_id: The space ID
            document_ids: Ordered list of document IDs
        """
        self._document_repo.reorder_documents(space_id, document_ids)

    def update_document_hash(self, document_id: str, content_hash: str) -> None:
        """
        Update the content hash for a document.

        Args:
            document_id: The document association ID
            content_hash: The new content hash
        """
        # Get old hash for event
        document = self._document_repo.get_document(document_id)
        old_hash = document.content_hash if document else None

        self._document_repo.update_content_hash(document_id, content_hash)

        if document and old_hash != content_hash:
            self._emit(document_content_changed_event(
                document.space_id,
                document_id,
                old_hash,
                content_hash,
            ))

    # =========================================================================
    # Variable Operations
    # =========================================================================

    def set_variable(
        self,
        space_id: str,
        name: str,
        value: Any,
        scope: str = "space",
    ) -> SpaceVariable:
        """
        Set a variable in a space.

        Args:
            space_id: The space ID
            name: Variable name
            value: Variable value (any JSON-serializable value)
            scope: Variable scope ("space" or "document")

        Returns:
            The saved variable

        Raises:
            ValueError: If space not found
        """
        if not self._space_repo.exists(space_id):
            raise ValueError(f"Space '{space_id}' not found")

        variable = SpaceVariable(
            space_id=space_id,
            name=name,
            value=value,
            scope=scope,
        )

        saved_var = self._variable_repo.set_variable(variable)
        self._emit(SpaceEvent(
            event_type=SpaceEventType.VARIABLE_SET,
            space_id=space_id,
            payload={"name": name, "scope": scope},
        ))
        return saved_var

    def get_variable(self, space_id: str, name: str) -> Optional[SpaceVariable]:
        """
        Get a variable by name.

        Args:
            space_id: The space ID
            name: Variable name

        Returns:
            The variable if found, None otherwise
        """
        return self._variable_repo.get_variable(space_id, name)

    def list_variables(
        self, space_id: str, scope: Optional[str] = None
    ) -> List[SpaceVariable]:
        """
        List all variables in a space.

        Args:
            space_id: The space ID
            scope: Optional scope filter

        Returns:
            List of variables
        """
        return self._variable_repo.list_variables(space_id, scope)

    def delete_variable(self, space_id: str, name: str) -> bool:
        """
        Delete a variable.

        Args:
            space_id: The space ID
            name: Variable name

        Returns:
            True if deleted, False if not found
        """
        result = self._variable_repo.delete_variable(space_id, name)
        if result:
            self._emit(SpaceEvent(
                event_type=SpaceEventType.VARIABLE_DELETED,
                space_id=space_id,
                payload={"name": name},
            ))
        return result

    def get_variables_dict(self, space_id: str) -> Dict[str, Any]:
        """
        Get all variables as a dictionary.

        Useful for transclusion context.

        Args:
            space_id: The space ID

        Returns:
            Dictionary of variable names to values
        """
        variables = self._variable_repo.list_variables(space_id)
        return {var.name: var.value for var in variables}

    # =========================================================================
    # Reference Operations
    # =========================================================================

    def add_reference(
        self,
        source_doc_id: str,
        target_doc_id: str,
        space_id: str,
    ) -> TransclusionReference:
        """
        Add a transclusion reference.

        Args:
            source_doc_id: The source document ID
            target_doc_id: The target document ID
            space_id: The space ID

        Returns:
            The created reference
        """
        reference = TransclusionReference(
            source_doc_id=source_doc_id,
            target_doc_id=target_doc_id,
            space_id=space_id,
        )
        return self._reference_repo.add_reference(reference)

    def get_references_from(
        self, source_doc_id: str, space_id: str
    ) -> List[TransclusionReference]:
        """
        Get all references from a source document.

        Args:
            source_doc_id: The source document ID
            space_id: The space ID

        Returns:
            List of references
        """
        return self._reference_repo.get_references_from(source_doc_id, space_id)

    def get_references_to(
        self, target_doc_id: str, space_id: str
    ) -> List[TransclusionReference]:
        """
        Get all references to a target document.

        Args:
            target_doc_id: The target document ID
            space_id: The space ID

        Returns:
            List of references
        """
        return self._reference_repo.get_references_to(target_doc_id, space_id)

    def clear_references_from(self, source_doc_id: str, space_id: str) -> int:
        """
        Clear all references from a source document.

        Args:
            source_doc_id: The source document ID
            space_id: The space ID

        Returns:
            Number of references cleared
        """
        return self._reference_repo.clear_references_from(source_doc_id, space_id)

    def get_dependents(self, document_id: str, space_id: str) -> List[str]:
        """
        Get all documents that depend on a given document.

        Used for cache invalidation - returns documents that need
        to be re-rendered when the target document changes.

        Args:
            document_id: The document ID
            space_id: The space ID

        Returns:
            List of dependent document IDs
        """
        return self._reference_repo.get_dependents(document_id, space_id)

    # =========================================================================
    # Convenience Methods
    # =========================================================================

    def space_exists(self, space_id: str) -> bool:
        """
        Check if a space exists.

        Args:
            space_id: The space ID

        Returns:
            True if exists, False otherwise
        """
        return self._space_repo.exists(space_id)

    def get_space_stats(self, space_id: str) -> Dict[str, Any]:
        """
        Get statistics about a space.

        Args:
            space_id: The space ID

        Returns:
            Dictionary with statistics

        Raises:
            ValueError: If space not found
        """
        space = self._space_repo.get_by_id(space_id)
        if not space:
            raise ValueError(f"Space '{space_id}' not found")

        documents = self._document_repo.list_by_space(space_id)
        variables = self._variable_repo.list_variables(space_id)
        children = self._space_repo.get_children(space_id)

        return {
            "space_id": space_id,
            "name": space.name,
            "status": space.status.value,
            "document_count": len(documents),
            "variable_count": len(variables),
            "child_space_count": len(children),
            "created_at": space.created_at.isoformat(),
            "updated_at": space.updated_at.isoformat(),
        }
