import logging
import os
from abc import ABC, abstractmethod
from typing import Protocol

from mcard.model.card import MCard
from mcard.model.pagination import Page

logger = logging.getLogger(__name__)


def resolve_db_path(db_path: str) -> str:
    """Resolve a database path to an absolute path, creating parent dirs.

    Shared by SQLiteConnection and DuckDBConnection to avoid duplicating
    path resolution logic.

    Args:
        db_path: Path string, or ':memory:' for in-memory database.

    Returns:
        The resolved absolute path, or ':memory:' unchanged.

    Raises:
        PermissionError: If the parent directory cannot be created.
        OSError: On other filesystem errors.
    """
    if db_path == ":memory:":
        return ":memory:"

    abs_path = os.path.abspath(db_path)

    if not os.path.isabs(db_path):
        base_dir = os.path.dirname(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        )
        abs_path = os.path.normpath(os.path.join(base_dir, db_path))

    try:
        os.makedirs(os.path.dirname(abs_path), exist_ok=True)
    except PermissionError as e:
        logger.error(f"Permission error: {e}", exc_info=True)
        logger.error(f"Unable to access or create database at {abs_path}")
        raise
    except OSError as e:
        logger.error(f"Unexpected error setting up database: {e}", exc_info=True)
        raise

    return abs_path


class HandleHistoryEntry:
    """Represents a single entry in handle version history."""

    __slots__ = ("previous_hash", "changed_at")

    def __init__(self, previous_hash: str, changed_at: str) -> None:
        self.previous_hash = previous_hash
        self.changed_at = changed_at

    def to_dict(self) -> dict:
        return {"previous_hash": self.previous_hash, "changed_at": self.changed_at}


class DatabaseConnection(Protocol):
    """Protocol for database connections"""

    def connect(self) -> None: ...
    def disconnect(self) -> None: ...
    def commit(self) -> None: ...
    def rollback(self) -> None: ...


class StorageEngine(ABC):
    """Abstract base class for storage engines.

    Mirrors the TypeScript StorageEngine interface in mcard-js to ensure
    cross-language parity.  All card *and* handle operations are declared
    here so that every concrete engine is guaranteed to support them.
    """

    # ========== Card Operations ==========

    @abstractmethod
    def add(self, card: MCard) -> str:
        """Add a card and return its hash."""
        pass

    @abstractmethod
    def get(self, hash_value: str) -> MCard | None:
        """Retrieve a card by its hash."""
        pass

    @abstractmethod
    def delete(self, hash_value: str) -> bool:
        """Delete a card by its hash."""
        pass

    @abstractmethod
    def get_page(self, page_number: int, page_size: int) -> Page:
        """Get a page of cards."""
        pass

    @abstractmethod
    def get_all_cards(self) -> Page:
        """Get all cards in a single page."""
        pass

    @abstractmethod
    def get_cards_by_page(self, page_number: int, page_size: int) -> Page:
        """Get a page of cards (canonical pagination method)."""
        pass

    @abstractmethod
    def search_by_string(
        self, search_string: str, page_number: int, page_size: int
    ) -> Page:
        """Search cards by string in content, hash, or g_time."""
        pass

    @abstractmethod
    def search_by_content(
        self, search_string: str, page_number: int, page_size: int
    ) -> Page:
        """Search cards by content pattern."""
        pass

    @abstractmethod
    def clear(self) -> None:
        """Remove all cards."""
        pass

    @abstractmethod
    def count(self) -> int:
        """Return total number of cards."""
        pass

    # ========== Handle Operations ==========

    @abstractmethod
    def ensure_handle_tables(self) -> None:
        """Ensure handle-related tables exist."""
        pass

    @abstractmethod
    def register_handle(self, handle: str, hash_value: str) -> bool:
        """Register a new handle pointing to a hash."""
        pass

    @abstractmethod
    def resolve_handle(self, handle: str) -> str | None:
        """Resolve a handle to its current hash."""
        pass

    @abstractmethod
    def get_by_handle(self, handle: str) -> MCard | None:
        """Get the MCard currently pointed to by a handle."""
        pass

    @abstractmethod
    def update_handle(self, handle: str, new_hash: str) -> str:
        """Update handle to point to new_hash; return previous hash."""
        pass

    @abstractmethod
    def get_handle_history(self, handle: str) -> list:
        """Get version history for a handle."""
        pass

    @abstractmethod
    def prune_handle_history(
        self, handle: str, older_than: str | None = None, delete_all: bool = False
    ) -> int:
        """Prune version history for a handle."""
        pass

    # ========== Handle Management Operations ==========

    @abstractmethod
    def get_all_handles(self) -> list:
        """List all registered handles with their current hashes."""
        pass

    @abstractmethod
    def remove_handle(self, handle: str) -> None:
        """Remove a handle and all its history. Does NOT delete the card."""
        pass

    @abstractmethod
    def rename_handle(self, old_handle: str, new_handle: str) -> None:
        """Rename a handle atomically."""
        pass
