"""Card collection module for MCard."""

import json
import logging
import os
import sqlite3
import sys
from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, Any, TypeVar

if TYPE_CHECKING:
    from mcard.ptr.core.clm_template import Maybe

from mcard.config.settings import settings
from mcard.engine.sqlite_engine import SQLiteConnection, SQLiteEngine
from mcard.model.card import MCard
from mcard.model.event_producer import generate_collision_event
from mcard.model.pagination import Page

logger = logging.getLogger(__name__)

# Initialize environment parameters (Removed - using settings singleton)

# Database paths are imported from config_constants

# Type variable for generic type hints
T = TypeVar("T")

# Type variable for MCard instances
MCardT = TypeVar("MCardT", bound="MCard")


class CardCollection:
    """High-level interface for card collection operations.

    DOTS Vocabulary Role: Carrier Category Car(S)
    =============================================
    CardCollection represents the **Carrier Category** in the Double Operadic Theory of Systems.
    - Objects: Individual MCards (data artifacts).
    - Morphisms: Hash references between MCards (content-addressable links).
    - Composition: Morphisms compose via hash chains (Merkle-DAG structure).

    The Action: Loose(I) ⊛ Car(S) → Car(S)
    --------------------------------------
    The **Action** is how interactions act on systems to produce new systems:
    - CLM/PCard (Chart/Lens) defines the interaction pattern.
    - CardCollection.add() produces a new MCard in the Carrier.
    - The result is a new Object in Car(S).

    CRDT Semantics (G-Set)
    ---------------------
    - Commutative: merge(A, B) = merge(B, A)
    - Associative: merge(A, merge(B, C)) = merge(merge(A, B), C)
    - Idempotent: merge(A, A) = A
    - Strong Eventual Consistency: Replicas converge without coordination.

    CRD-Only Operations
    -------------------
    - Create: add(), add_with_handle()
    - Read: get(), get_by_handle(), get_page(), search_*()
    - Delete: delete(), clear()
    - NO UPDATE: MCard content is immutable. Updates create new versions.

    This class provides a simple interface for working with MCard collections.
    It handles database initialization automatically if no engine is provided.
    """

    @staticmethod
    def print_all_cards(db_path: str | None = None, page_size: int = 10) -> None:
        """Print all cards in the database in a formatted table.

        Args:
            db_path: Path to the database file. If None, uses the default from environment.
            page_size: Number of items to process per page (default: 10).
        """

        try:
            # Use the default collection which is already initialized with the correct path
            collection = (
                CardCollection() if not db_path else CardCollection(db_path=db_path)
            )

            logger.info("Fetching MCards from the database...")

            # Get database path for display
            db_path_str = "default"
            if hasattr(collection, "engine") and hasattr(collection.engine, "db_path"):
                db_path_str = collection.engine.db_path

            # Print the header with database path
            logger.info(f"\nDATABASE: {db_path_str}")
            logger.info("=" * 100)
            logger.info(
                f"{'Hash':<10} | {'Content':<40} | {'Created':<17} | {'Type':<15}"
            )
            logger.info("-" * 100)

            # Get all cards directly without pagination
            all_cards = collection.get_all_mcards_raw()
            total_cards = len(all_cards)

            # Print all cards
            for card in all_cards:
                # Process each card for display using MCard.to_display_dict
                processed = card.to_display_dict()
                if processed:
                    # Shorten hash to 7 characters with ellipsis
                    short_hash = (
                        f"{processed['hash'][:7]}..."
                        if len(processed["hash"]) > 7
                        else processed["hash"]
                    )
                    # Shorten content preview to fit in 40 chars
                    preview = (
                        str(processed["content_preview"])[:37] + "..."
                        if len(str(processed["content_preview"])) > 40
                        else str(processed["content_preview"])
                    )
                    # Shorten created_at and content_type if needed
                    created = str(processed["created_at"])[:14]
                    ctype = processed["content_type"].split("/")[-1][
                        :12
                    ]  # Just take the subtype and limit length
                    logger.info(
                        f"{short_hash:<10} | {preview:<40} | {created:<17} | {ctype:<15}"
                    )

            # Print summary
            logger.info(f"\n{'=' * 100}")
            logger.info(f"Displaying all {total_cards} MCards in database")
            logger.info(f"Database path: {db_path_str}")

            if total_cards == 0:
                logger.info("No MCards found in the database.")

        except Exception as e:
            logger.error(f"Error accessing the database: {e}", exc_info=True)
            sys.exit(1)

    def __init__(self, engine=None, engine_type: str = "sqlite", db_path: str = None):
        """Initialize the card collection.

        Args:
            engine: Optional pre-configured database engine. If None, a new one will be created.
            engine_type: Type of database engine to use ('sqlite' is the only supported option).
            db_path: Path to the database file. If None, defaults to 'data/mcard.db'.

        Raises:
            ValueError: If the provided engine is not a valid database engine instance.
            ValueError: If db_path is not a string when engine is None.
        """
        # Input validation for engine
        if engine is not None:
            if not hasattr(engine, "add") or not callable(engine.add):
                raise ValueError(
                    "Invalid engine provided. Engine must implement the StorageEngine interface "
                    "with required methods (add, get, delete, etc.)"
                )
            self.engine = engine
            logger.debug(
                f"Using provided engine, db path: {getattr(engine, 'db_path', 'unknown')}"
            )
            return

        # If no engine provided, initialize a new one
        if db_path is not None and not isinstance(db_path, (str, Path)):
            raise ValueError(
                f"db_path must be a string or Path, got {type(db_path).__name__}"
            )

        # Set up database path using environment variable or default if none provided
        if db_path is None:
            logger.debug("No db_path provided, getting from settings")
            db_path = settings.database.path
        else:
            logger.debug(f"Using provided db_path: {db_path}")

        # Handle special :memory: database
        if str(db_path) == ":memory:":
            logger.debug("Using in-memory database")
            abs_db_path = ":memory:"
        else:
            # Ensure the directory exists for file-based databases
            abs_db_path = os.path.abspath(str(db_path))
            db_dir = os.path.dirname(abs_db_path) or "."
            logger.debug(f"Ensuring directory exists: {db_dir}")
            try:
                os.makedirs(db_dir, exist_ok=True)
            except OSError as e:
                logger.error(f"Failed to create directory {db_dir}: {e}", exc_info=True)
                raise ValueError(f"Failed to create directory {db_dir}: {e}") from e

        try:
            if engine_type == "duckdb":
                try:
                    from mcard.engine.duckdb_engine import (
                        DuckDBConnection,
                        DuckDBEngine,
                    )
                except ImportError as e:
                    raise ValueError(
                        "DuckDB is not installed. Install with: pip install mcard[duckdb]"
                    ) from e
                logger.debug(f"Initializing DuckDB connection to: {abs_db_path}")
                connection = DuckDBConnection(abs_db_path)
                self.engine = DuckDBEngine(connection)
                logger.info(f"Initialized DuckDB database at {abs_db_path}")
            else:
                logger.debug(f"Initializing SQLite connection to: {abs_db_path}")
                connection = SQLiteConnection(abs_db_path)
                self.engine = SQLiteEngine(connection)
                logger.info(f"Initialized SQLite database at {abs_db_path}")
            logger.debug(
                f"Engine initialized with db_path: {getattr(self.engine, 'db_path', 'unknown')}"
            )

        except (OSError, sqlite3.Error) as e:
            logger.error(
                f"Failed to initialize database at {abs_db_path}: {e}", exc_info=True
            )
            raise ValueError(
                f"Failed to initialize database at {abs_db_path}: {e}"
            ) from e

    def add(self, card: MCard) -> str:
        """Add a card to the collection.

        In a content-addressable scheme, we first check if there's an existing card
        with the same hash. If found, we compare the content to determine if it's
        a duplicate (same content) or a collision (different content).

        Args:
            card: The MCard to add

        Returns:
            str: The hash of the card

        Raises:
            ValueError: If card is None
        """
        logger.debug(f"Attempting to add card with content: {card.content}")
        if card is None:
            raise ValueError("Card cannot be None")

        # Get the hash of the incoming card
        hash_value = card.hash

        # Check if a card with this hash already exists
        existing_card = self.get(hash_value)
        if existing_card:
            logger.debug(f"Card with hash {hash_value} already exists")
            # Compare content to determine if it's a duplicate or collision
            if existing_card.content == card.content:
                logger.debug(f"Duplicate card found with content: {card.content}")
                # Duplicate detected - create duplicate event and add directly via engine
                logger.info(
                    f"Duplicate detected for hash {hash_value}; creating duplicate event."
                )
                from mcard.model.event_producer import generate_duplication_event

                duplicate_event_content_str = generate_duplication_event(card)
                duplicate_event_card = MCard(duplicate_event_content_str)
                try:
                    self.engine.add(duplicate_event_card)
                    return hash_value
                except (ValueError, sqlite3.Error):
                    # If event creation fails, return existing hash
                    logger.warning(
                        f"Failed to create duplicate event for {hash_value}; returning existing hash."
                    )
                    return hash_value
            else:
                logger.debug(
                    f"Collision detected for card with content: {card.content}"
                )
                # Create collision event card and store the new card with new hash function
                collision_event_content_str = generate_collision_event(card)
                contentDict = json.loads(collision_event_content_str)
                # Different content = collision, upgrade hash function to stronger level
                collision_content_card = MCard(
                    card.content, contentDict["upgraded_function"]
                )  # Use the new hash function (upgraded_function)
                collision_event_card = MCard(collision_event_content_str)
                logger.debug(f"Collision event: {collision_event_content_str}")
                # Add via engine directly to avoid recursive duplicate handling
                try:
                    self.engine.add(collision_event_card)
                except (ValueError, sqlite3.Error) as e:
                    logger.debug(
                        f"Collision event already exists or failed to add: {e}"
                    )
                try:
                    self.engine.add(collision_content_card)
                except (ValueError, sqlite3.Error) as e:
                    logger.debug(
                        f"Collision content already exists or failed to add: {e}"
                    )
                logger.debug(
                    f"Added collision event card with hash: {collision_event_card.hash}"
                )
                return collision_event_card.hash

        # No existing card with this hash or content, add the new card
        self.engine.add(card)
        logger.debug(f"Successfully added card with hash {hash_value}")
        return hash_value

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

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

    def get_page(self, page_number: int = 1, page_size: int = None) -> Page:
        """Get a page of cards"""
        if page_size is None:
            page_size = settings.pagination.default_page_size
        return self.engine.get_cards_by_page(
            page_number=page_number, page_size=page_size
        )

    def search_by_string(
        self, search_string: str, page_number: int = 1, page_size: int = None
    ) -> Page:
        """Search for cards containing the given string.

        Args:
            search_string: The string to search for in card content
            page_number: The page number to return (1-based)
            page_size: Number of items per page (defaults to env setting)

        Returns:
            Page: A page of matching cards
        """
        if page_size is None:
            page_size = settings.pagination.default_page_size
        # Delegate the search to the engine's search_by_string method
        return self.engine.search_by_string(search_string, page_number, page_size)

    def search_by_hash(
        self, hash_value: str, page_number: int = 1, page_size: int = None
    ) -> Page:
        """Search for cards by exact hash value.

        Uses a direct O(1) primary-key lookup instead of loading all cards.

        Args:
            hash_value: The hash value to search for
            page_number: The page number to return (1-based)
            page_size: Number of items per page (defaults to env setting)

        Returns:
            Page: A page of matching cards
        """
        if page_size is None:
            page_size = settings.pagination.default_page_size
        if page_number < 1:
            raise ValueError("Page number must be greater than 0")
        if page_size < 1:
            raise ValueError("Page size must be greater than 0")

        card = self.engine.get(hash_value)
        items = [card] if card and page_number == 1 else []
        total_items = 1 if card else 0

        return Page.create(items, total_items, page_number, page_size)

    def search_by_content(
        self, search_string: str, page_number: int = 1, page_size: int = None
    ) -> Page:
        """Search for cards by content string

        Args:
            search_string: The string to search for in card content
            page_number: The page number to return (1-based)
            page_size: Number of items per page (defaults to env setting)

        Returns:
            Page: A page of matching cards
        """
        if page_size is None:
            page_size = settings.pagination.default_page_size
        return self.engine.search_by_content(search_string, page_number, page_size)

    def clear(self) -> None:
        """Remove all cards"""
        self.engine.clear()

    def count(self) -> int:
        """Return total number of cards"""
        return self.engine.count()

    def get_all_mcards_raw(self) -> list[MCard]:
        """Retrieve all MCard objects directly from the database without pagination or callbacks.

        This method is more efficient than get_all_cards when you need all cards and don't need
        pagination or processing callbacks.

        Returns:
            list: A list of MCard objects
        """
        page = self.engine.get_all_cards()
        return page.items if page and hasattr(page, "items") else []

    def get_all_cards(
        self,
        page_size: int = 10,
        process_callback: Callable[[MCard], Any] | None = None,
    ) -> tuple[list[MCard], int]:
        """Retrieve all cards from the database with pagination support.

        Args:
            page_size: Number of items per page. Defaults to 10.
            process_callback: Optional callback function to process each card.
                           Signature: (card) -> processed_card

        Returns:
            tuple: (list of all cards, total count of cards)
        """
        all_cards = []
        page_number = 1
        total_cards = 0

        while True:
            # Get a page of cards
            page = self.get_mcards_at_page(page_number=page_number, page_size=page_size)

            # If no items on this page, we're done
            if not page or not page.items:
                break

            # Process cards if callback is provided
            processed_cards = [
                (
                    process_callback(card)
                    if process_callback and callable(process_callback)
                    else card
                )
                for card in page.items
                if card is not None
            ]
            all_cards.extend(processed_cards)

            # Update total cards count
            total_cards += len(page.items)

            # If there are no more pages, we're done
            if not page.has_next:
                break

            # Move to the next page
            page_number += 1

        return all_cards, total_cards

    def get_mcards_at_page(self, page_number: int = 1, page_size: int = 10) -> Page:
        """Get a single page of MCards.

        Args:
            page_number: Page number to retrieve (1-based)
            page_size: Number of items per page

        Returns:
            Page: A Page object containing the MCards for the requested page

        Raises:
            ValueError: If page_number < 1 or page_size < 1
        """
        return self.engine.get_cards_by_page(
            page_number=page_number, page_size=page_size
        )

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

    def add_with_handle(self, card: MCard, handle: str) -> str:
        """Add a card and register a handle for it.

        Args:
            card: The MCard to add.
            handle: The handle string to register.

        Returns:
            The hash of the added card.

        Raises:
            HandleValidationError: If the handle string is invalid.
            ValueError: If the handle already exists.
        """
        hash_value = self.add(card)
        self.engine.register_handle(handle, hash_value)
        return hash_value

    def update_handle(self, handle: str, new_card: MCard) -> str:
        """Add a new card and update the handle to point to it.

        Args:
            handle: The handle string to update.
            new_card: The new MCard to add.

        Returns:
            The hash of the new card.

        Raises:
            HandleValidationError: If the handle string is invalid.
            ValueError: If the handle does not exist.
        """
        hash_value = self.add(new_card)
        self.engine.update_handle(handle, hash_value)
        return hash_value

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

        Args:
            handle: The handle string to resolve.

        Returns:
            The MCard, or None if the handle does not exist.
        """
        return self.engine.get_by_handle(handle)

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

        Args:
            handle: The handle string to resolve.

        Returns:
            The current hash, or None if the handle does not exist.
        """
        return self.engine.resolve_handle(handle)

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

        Args:
            handle: The handle string.

        Returns:
            A list of dicts with 'previous_hash' and 'changed_at' keys.
        """
        return self.engine.get_handle_history(handle)

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

        Args:
            handle: The handle string.
            older_than: Optional timestamp string. Delete older entries.
            delete_all: If True, delete all history.

        Returns:
            Number of deleted entries.
        """
        return self.engine.prune_handle_history(
            handle, older_than=older_than, delete_all=delete_all
        )

    # =========================================================================
    # Monadic Handle Methods (using Maybe monad from clm_template.py)
    # =========================================================================

    def get_by_handle_m(self, handle: str) -> "Maybe[MCard]":
        """Monadic version of get_by_handle.

        Returns Maybe.just(card) on success, Maybe.nothing() on failure.
        Enables functional composition with .bind() chaining.

        Example:
            collection.get_by_handle_m("my_doc").bind(
                lambda card: Maybe.just(card.get_content(as_text=True))
            )

        Args:
            handle: The handle string to resolve.

        Returns:
            Maybe[MCard]: Just(card) if found, Nothing otherwise.
        """
        from mcard.ptr.core.clm_template import Maybe

        card = self.get_by_handle(handle)
        return Maybe.just(card) if card else Maybe.nothing()

    def resolve_handle_m(self, handle: str) -> "Maybe[str]":
        """Monadic version of resolve_handle.

        Returns Maybe.just(hash) on success, Maybe.nothing() on failure.

        Args:
            handle: The handle string to resolve.

        Returns:
            Maybe[str]: Just(hash) if found, Nothing otherwise.
        """
        from mcard.ptr.core.clm_template import Maybe

        hash_val = self.resolve_handle(handle)
        return Maybe.just(hash_val) if hash_val else Maybe.nothing()

    def get_m(self, hash_value: str) -> "Maybe[MCard]":
        """Monadic version of get.

        Returns Maybe.just(card) on success, Maybe.nothing() on failure.

        Args:
            hash_value: The hash of the card to retrieve.

        Returns:
            Maybe[MCard]: Just(card) if found, Nothing otherwise.
        """
        from mcard.ptr.core.clm_template import Maybe

        card = self.get(hash_value)
        return Maybe.just(card) if card else Maybe.nothing()

    def resolve_and_get_m(self, handle: str) -> "Maybe[MCard]":
        """Resolve a handle and get the card in one monadic operation.

        This is equivalent to:
            resolve_handle_m(handle).bind(lambda h: get_m(h))

        Args:
            handle: The handle string to resolve.

        Returns:
            Maybe[MCard]: Just(card) if resolved and found, Nothing otherwise.
        """
        return self.resolve_handle_m(handle).bind(lambda h: self.get_m(h))
