# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: AGPL-3.0
"""Base client interface for OpenViking.

Defines the abstract base class that both LocalClient and AsyncHTTPClient implement.
"""

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Union

from openviking.telemetry import TelemetryRequest


class BaseClient(ABC):
    """Abstract base class for OpenViking clients.

    Both LocalClient (embedded mode) and AsyncHTTPClient (HTTP mode) implement this interface.
    """

    # ============= Lifecycle =============

    @abstractmethod
    async def initialize(self) -> None:
        """Initialize the client."""
        ...

    @abstractmethod
    async def close(self) -> None:
        """Close the client and release resources."""
        ...

    # ============= Resource Management =============

    @abstractmethod
    async def add_resource(
        self,
        path: str,
        to: Optional[str] = None,
        parent: Optional[str] = None,
        reason: str = "",
        instruction: str = "",
        wait: bool = False,
        timeout: Optional[float] = None,
        watch_interval: float = 0,
        telemetry: TelemetryRequest = False,
    ) -> Dict[str, Any]:
        """Add resource to OpenViking."""
        ...

    @abstractmethod
    async def add_skill(
        self,
        data: Any,
        wait: bool = False,
        timeout: Optional[float] = None,
        telemetry: TelemetryRequest = False,
    ) -> Dict[str, Any]:
        """Add skill to OpenViking."""
        ...

    @abstractmethod
    async def wait_processed(self, timeout: Optional[float] = None) -> Dict[str, Any]:
        """Wait for all processing to complete."""
        ...

    @abstractmethod
    async def reindex(
        self,
        uri: str,
        mode: str = "vectors_only",
        wait: bool = True,
    ) -> Dict[str, Any]:
        """Reindex semantic/vector artifacts for a URI."""
        ...

    # ============= File System =============

    @abstractmethod
    async def ls(
        self,
        uri: str,
        simple: bool = False,
        recursive: bool = False,
        output: str = "original",
        abs_limit: int = 256,
        show_all_hidden: bool = False,
        node_limit: int = 1000,
    ) -> List[Any]:
        """List directory contents."""
        ...

    @abstractmethod
    async def tree(
        self,
        uri: str,
        output: str = "original",
        abs_limit: int = 128,
        show_all_hidden: bool = False,
        node_limit: int = 1000,
    ) -> List[Dict[str, Any]]:
        """Get directory tree."""
        ...

    @abstractmethod
    async def stat(self, uri: str) -> Dict[str, Any]:
        """Get resource status."""
        ...

    @abstractmethod
    async def mkdir(self, uri: str, description: Optional[str] = None) -> None:
        """Create directory."""
        ...

    @abstractmethod
    async def rm(self, uri: str, recursive: bool = False) -> None:
        """Remove resource."""
        ...

    @abstractmethod
    async def mv(self, from_uri: str, to_uri: str) -> None:
        """Move resource."""
        ...

    # ============= Content Reading =============

    @abstractmethod
    async def read(self, uri: str, offset: int = 0, limit: int = -1) -> str:
        """Read file content (L2).

        Args:
            uri: Viking URI
            offset: Starting line number (0-indexed). Default 0.
            limit: Number of lines to read. -1 means read to end. Default -1.
        """
        ...

    @abstractmethod
    async def abstract(self, uri: str) -> str:
        """Read L0 abstract (.abstract.md)."""
        ...

    @abstractmethod
    async def overview(self, uri: str) -> str:
        """Read L1 overview (.overview.md)."""
        ...

    @abstractmethod
    async def write(
        self,
        uri: str,
        content: str,
        mode: str = "replace",
        wait: bool = False,
        timeout: Optional[float] = None,
        telemetry: TelemetryRequest = False,
    ) -> Dict[str, Any]:
        """Write text content to an existing file and refresh semantics/vectors."""
        ...

    # ============= Search =============

    @abstractmethod
    async def find(
        self,
        query: str,
        target_uri: Union[str, List[str]] = "",
        limit: int = 10,
        score_threshold: Optional[float] = None,
        filter: Optional[Dict] = None,
        telemetry: TelemetryRequest = False,
    ) -> Any:
        """Semantic search without session context."""
        ...

    @abstractmethod
    async def search(
        self,
        query: str,
        target_uri: Union[str, List[str]] = "",
        session_id: Optional[str] = None,
        limit: int = 10,
        score_threshold: Optional[float] = None,
        filter: Optional[Dict] = None,
        telemetry: TelemetryRequest = False,
    ) -> Any:
        """Semantic search with optional session context."""
        ...

    @abstractmethod
    async def grep(
        self,
        uri: str,
        pattern: str,
        case_insensitive: bool = False,
        exclude_uri: Optional[str] = None,
        node_limit: Optional[int] = None,
    ) -> Dict[str, Any]:
        """Content search with pattern."""
        ...

    @abstractmethod
    async def glob(self, pattern: str, uri: str = "viking://") -> Dict[str, Any]:
        """File pattern matching."""
        ...

    # ============= Relations =============

    @abstractmethod
    async def relations(self, uri: str) -> List[Dict[str, Any]]:
        """Get relations for a resource."""
        ...

    @abstractmethod
    async def link(self, from_uri: str, to_uris: Union[str, List[str]], reason: str = "") -> None:
        """Create link between resources."""
        ...

    @abstractmethod
    async def unlink(self, from_uri: str, to_uri: str) -> None:
        """Remove link between resources."""
        ...

    # ============= Sessions =============

    @abstractmethod
    async def create_session(
        self, session_id: Optional[str] = None, telemetry: TelemetryRequest = False
    ) -> Dict[str, Any]:
        """Create a new session.

        Args:
            session_id: Optional session ID. If provided, creates a session with the given ID.
                       If None, creates a new session with auto-generated ID.
            telemetry: Whether to attach operation telemetry data to the result.
        """
        ...

    @abstractmethod
    async def list_sessions(self) -> List[Dict[str, Any]]:
        """List all sessions."""
        ...

    @abstractmethod
    async def get_session(self, session_id: str, *, auto_create: bool = False) -> Dict[str, Any]:
        """Get session details."""
        ...

    @abstractmethod
    async def get_session_context(
        self, session_id: str, token_budget: int = 128_000
    ) -> Dict[str, Any]:
        """Get assembled session context for a session."""
        ...

    @abstractmethod
    async def get_session_archive(self, session_id: str, archive_id: str) -> Dict[str, Any]:
        """Get one completed archive for a session."""
        ...

    @abstractmethod
    async def delete_session(self, session_id: str) -> None:
        """Delete a session."""
        ...

    @abstractmethod
    async def commit_session(
        self, session_id: str, telemetry: TelemetryRequest = False
    ) -> Dict[str, Any]:
        """Commit a session (archive and extract memories)."""
        ...

    @abstractmethod
    async def add_message(
        self,
        session_id: str,
        role: str,
        content: str | None = None,
        parts: list[dict] | None = None,
        created_at: str | None = None,
        role_id: str | None = None,
        telemetry: TelemetryRequest = False,
    ) -> Dict[str, Any]:
        """Add a message to a session.

        Args:
            session_id: Session ID
            role: Message role ("user" or "assistant")
            content: Text content (simple mode)
            parts: Parts array (full Part support: TextPart, ContextPart, ToolPart)
            created_at: Message creation time (ISO format string)
            role_id: Optional explicit actor identity. Omit to let the server derive it.
            telemetry: Whether to attach operation telemetry data to the result.

        If both content and parts are provided, parts takes precedence.
        """
        ...

    # ============= Pack =============

    @abstractmethod
    async def export_ovpack(self, uri: str, to: str, include_vectors: bool = False) -> str:
        """Export as .ovpack file."""
        ...

    @abstractmethod
    async def backup_ovpack(self, to: str, include_vectors: bool = False) -> str:
        """Back up public scopes as a restore-only .ovpack file."""
        ...

    @abstractmethod
    async def import_ovpack(
        self,
        file_path: str,
        parent: str,
        on_conflict: Optional[str] = None,
        vector_mode: Optional[str] = None,
    ) -> str:
        """Import .ovpack file."""
        ...

    @abstractmethod
    async def restore_ovpack(
        self,
        file_path: str,
        on_conflict: Optional[str] = None,
        vector_mode: Optional[str] = None,
    ) -> str:
        """Restore backup .ovpack file."""
        ...

    # ============= Debug =============

    @abstractmethod
    async def check_consistency(self, uri: str) -> Dict[str, Any]:
        """Check filesystem/vector-index consistency for a URI subtree."""
        ...

    @abstractmethod
    async def health(self) -> bool:
        """Quick health check."""
        ...

    @abstractmethod
    def session(self, session_id: Optional[str] = None, must_exist: bool = False) -> Any:
        """Create a new session or load an existing one.

        Args:
            session_id: Session ID, creates a new session if None
            must_exist: If True and session_id is provided, raises NotFoundError
                        when the session does not exist instead of silently
                        returning a fresh empty session.
                        If session_id is None, must_exist is ignored.

        Returns:
            Session object

        Raises:
            NotFoundError: If must_exist=True and the session does not exist.
        """
        ...

    @abstractmethod
    async def session_exists(self, session_id: str) -> bool:
        """Check whether a session exists in storage.

        Args:
            session_id: Session ID to check

        Returns:
            True if the session exists, False otherwise
        """
        ...

    @abstractmethod
    def get_status(self) -> Any:
        """Get system status.

        Returns:
            SystemStatus or Dict containing health status of all components.
        """
        ...

    @abstractmethod
    def is_healthy(self) -> bool:
        """Quick health check (synchronous).

        Returns:
            True if all components are healthy, False otherwise.
        """
        ...

    @property
    @abstractmethod
    def observer(self) -> Any:
        """Get observer service for component status."""
        ...
