"""Collection class for VibeDB - manages documents within a collection."""

from typing import Any, Dict, List, Optional, TYPE_CHECKING

from vibedb.core.document import Document
from vibedb.core import schema
from vibedb.errors import DocumentNotFoundError, QueryError
from vibedb.utils import (
    generate_id,
    current_timestamp,
    serialize_document,
    deserialize_document,
    validate_document,
    merge_documents,
)

if TYPE_CHECKING:
    from vibedb.core.database import VibeDB
    from vibedb.query.cursor import Cursor


class Collection:
    """
    Represents a collection of documents (like MongoDB collection).

    Provides CRUD operations, querying, and index management for documents
    within a named collection.

    Example:
        >>> db = VibeDB("app.db")
        >>> users = db.collection("users")
        >>>
        >>> # Insert
        >>> user_id = users.insert({"name": "Alice", "age": 28})
        >>>
        >>> # Get
        >>> user = users.get(user_id)
        >>> print(user.name)
        'Alice'
        >>>
        >>> # Update
        >>> users.update(user_id, {"age": 29})
        >>>
        >>> # Find
        >>> results = users.find({"age": {"$gte": 25}})
        >>> for user in results:
        ...     print(user.name)
        >>>
        >>> # Delete
        >>> users.delete(user_id)
    """

    def __init__(self, db: "VibeDB", name: str):
        """
        Initialize Collection.

        Args:
            db: Parent VibeDB instance
            name: Collection name
        """
        self._db = db
        self._name = name
        self._table_name = f"{name}_docs"

    @property
    def name(self) -> str:
        """Get collection name."""
        return self._name

    @property
    def database(self) -> "VibeDB":
        """Get parent database."""
        return self._db

    def insert(self, doc: dict) -> str:
        """
        Insert a document into the collection.

        Automatically adds _id, _createdAt, and _updatedAt fields.

        Args:
            doc: Document to insert (must be a dict)

        Returns:
            str: Generated document ID

        Raises:
            TypeError: If doc is not a dict
            DatabaseError: If insertion fails

        Example:
            >>> users = db.collection("users")
            >>> user_id = users.insert({"name": "Alice", "email": "alice@example.com"})
            >>> print(user_id)
            'a1b2c3d4...'
        """
        # Validate document structure
        validate_document(doc)

        # Validate against schema if present
        current_schema = self.get_schema()
        if current_schema:
            from vibedb.schema_validation.validator import SchemaValidator
            validator = SchemaValidator(current_schema, self)
            doc = validator.validate(doc, partial=False)

        # Generate ID if not provided
        if '_id' not in doc:
            doc['_id'] = generate_id()

        # Add timestamps
        timestamp = current_timestamp()
        if '_createdAt' not in doc:
            doc['_createdAt'] = timestamp
        doc['_updatedAt'] = timestamp

        # Serialize document
        doc_json = serialize_document(doc)

        # Insert into database
        safe_table = self._name.replace('"', '""')
        sql = f'''
            INSERT INTO "{safe_table}_docs" (_id, _createdAt, _updatedAt, _doc)
            VALUES (?, ?, ?, ?)
        '''

        self._db.engine.execute(
            sql,
            (doc['_id'], doc['_createdAt'], doc['_updatedAt'], doc_json)
        )
        self._db.engine.commit()

        # Update document count
        schema.update_document_count(self._db.engine, self._name, 1)
        self._db.engine.commit()

        return doc['_id']

    def insert_many(self, docs: List[dict]) -> List[str]:
        """
        Insert multiple documents.

        Args:
            docs: List of documents to insert

        Returns:
            list[str]: List of generated document IDs

        Example:
            >>> users = db.collection("users")
            >>> ids = users.insert_many([
            ...     {"name": "Alice"},
            ...     {"name": "Bob"},
            ...     {"name": "Charlie"}
            ... ])
            >>> print(len(ids))
            3
        """
        ids = []
        for doc in docs:
            doc_id = self.insert(doc)
            ids.append(doc_id)
        return ids

    def get(self, doc_id: str) -> Optional[Document]:
        """
        Get a document by ID.

        Args:
            doc_id: Document ID

        Returns:
            Document: Document instance or None if not found

        Example:
            >>> users = db.collection("users")
            >>> user = users.get("abc123")
            >>> if user:
            ...     print(user.name)
        """
        safe_table = self._name.replace('"', '""')
        sql = f'SELECT _doc FROM "{safe_table}_docs" WHERE _id = ?'

        result = self._db.engine.execute(sql, (doc_id,), fetch='one')

        if not result:
            return None

        # Deserialize document
        doc_data = deserialize_document(result['_doc'])

        return Document(doc_data, self)

    def update(self, doc_id: str, updates: dict) -> bool:
        """
        Update a document by ID.

        Args:
            doc_id: Document ID
            updates: Fields to update (merged with existing document)

        Returns:
            bool: True if document was updated, False if not found

        Example:
            >>> users = db.collection("users")
            >>> users.update("abc123", {"age": 29, "city": "NYC"})
            True
        """
        # Get existing document
        existing = self.get(doc_id)
        if not existing:
            return False

        # Merge updates
        merged = merge_documents(existing.to_dict(), updates)

        # Validate against schema if present (partial validation)
        current_schema = self.get_schema()
        if current_schema:
            from vibedb.schema_validation.validator import SchemaValidator
            validator = SchemaValidator(current_schema, self)
            # Validate only the updated fields (partial=True)
            merged = validator.validate(merged, partial=True)

        # Update timestamp
        merged['_updatedAt'] = current_timestamp()

        # Serialize
        doc_json = serialize_document(merged)

        # Update in database
        safe_table = self._name.replace('"', '""')
        sql = f'''
            UPDATE "{safe_table}_docs"
            SET _doc = ?, _updatedAt = ?
            WHERE _id = ?
        '''

        self._db.engine.execute(
            sql,
            (doc_json, merged['_updatedAt'], doc_id)
        )
        self._db.engine.commit()

        return True

    def update_many(self, query: dict, updates: dict) -> int:
        """
        Update multiple documents matching query.

        Args:
            query: Query to match documents
            updates: Fields to update

        Returns:
            int: Number of documents updated

        Example:
            >>> users = db.collection("users")
            >>> count = users.update_many(
            ...     {"age": {"$lt": 25}},
            ...     {"status": "junior"}
            ... )
            >>> print(f"Updated {count} documents")
        """
        # For now, use simple implementation
        # Will be improved in Phase 3 with query parser
        count = 0
        for doc in self.find(query):
            self.update(doc.id, updates)
            count += 1
        return count

    def upsert(self, query: dict, doc: dict) -> str:
        """
        Update if exists, insert if not.

        Args:
            query: Query to find existing document
            doc: Document data to insert/update

        Returns:
            str: Document ID (existing or newly created)

        Example:
            >>> users = db.collection("users")
            >>> user_id = users.upsert(
            ...     {"email": "alice@example.com"},
            ...     {"email": "alice@example.com", "name": "Alice", "age": 28}
            ... )
        """
        # Try to find existing document
        existing = self.find_one(query)

        if existing:
            # Update existing
            self.update(existing.id, doc)
            return existing.id
        else:
            # Insert new
            return self.insert(doc)

    def delete(self, doc_id: str) -> bool:
        """
        Delete a document by ID.

        Args:
            doc_id: Document ID

        Returns:
            bool: True if document was deleted, False if not found

        Example:
            >>> users = db.collection("users")
            >>> users.delete("abc123")
            True
        """
        safe_table = self._name.replace('"', '""')
        sql = f'DELETE FROM "{safe_table}_docs" WHERE _id = ?'

        cursor = self._db.engine.execute(sql, (doc_id,))
        self._db.engine.commit()

        deleted = cursor.rowcount > 0

        if deleted:
            # Update document count
            schema.update_document_count(self._db.engine, self._name, -1)
            self._db.engine.commit()

        return deleted

    def delete_many(self, query: dict) -> int:
        """
        Delete multiple documents matching query.

        Args:
            query: Query to match documents

        Returns:
            int: Number of documents deleted

        Example:
            >>> users = db.collection("users")
            >>> count = users.delete_many({"age": {"$gt": 60}})
            >>> print(f"Deleted {count} documents")
        """
        # For now, use simple implementation
        # Will be improved in Phase 3
        count = 0
        for doc in list(self.find(query)):  # Convert to list to avoid iteration issues
            if self.delete(doc.id):
                count += 1
        return count

    def find(self, query: dict = None) -> "Cursor":
        """
        Find documents matching query.

        This will be fully implemented in Phase 3 with query parser.
        For now, returns a simple cursor over all documents.

        Args:
            query: Query filter (MongoDB-style dict or Q object)

        Returns:
            Cursor: Cursor over matching documents

        Example:
            >>> users = db.collection("users")
            >>>
            >>> # Find all
            >>> for user in users.find():
            ...     print(user.name)
            >>>
            >>> # Find with query (Phase 3)
            >>> for user in users.find({"age": {"$gte": 25}}):
            ...     print(user.name)
        """
        # Phase 3 will implement full query support
        # For now, return all documents if no query, or use simple filter
        from vibedb.query.cursor import Cursor
        return Cursor(self, query or {})

    def find_one(self, query: dict = None) -> Optional[Document]:
        """
        Find a single document matching query.

        Args:
            query: Query filter

        Returns:
            Document: First matching document or None

        Example:
            >>> users = db.collection("users")
            >>> user = users.find_one({"email": "alice@example.com"})
            >>> if user:
            ...     print(user.name)
        """
        cursor = self.find(query)
        return cursor.first()

    def count(self, query: dict = None) -> int:
        """
        Count documents matching query.

        Args:
            query: Query filter (None = count all)

        Returns:
            int: Number of matching documents

        Example:
            >>> users = db.collection("users")
            >>> total = users.count()
            >>> adults = users.count({"age": {"$gte": 18}})
        """
        if query is None or not query:
            # Fast path: use collection metadata
            info = schema.get_collection_info(self._db.engine, self._name)
            return info.get('document_count', 0) if info else 0
        else:
            # Use find to count filtered results
            return self.find(query).count()

    def create_index(self, fields: List[str], unique: bool = False, name: str = None) -> str:
        """
        Create an index on specified fields.

        Creates a SQLite expression index using json_extract() for JSON fields.

        Args:
            fields: List of field names to index (can be nested like "address.city")
            unique: Whether index should enforce uniqueness
            name: Optional custom index name

        Returns:
            str: Index ID

        Example:
            >>> users = db.collection("users")
            >>> users.create_index(["email"], unique=True)
            >>> users.create_index(["age", "status"])  # Composite index
            >>> users.create_index(["address.city"])  # Nested field
        """
        from vibedb.indexes.manager import IndexManager
        manager = IndexManager(self)
        return manager.create_index(fields, unique=unique, name=name)

    def list_indexes(self) -> List[dict]:
        """
        List all indexes for this collection.

        Returns:
            list[dict]: List of index metadata dicts containing:
                - id: Index ID
                - name: Index name
                - fields: List of indexed fields
                - unique: Whether index is unique
                - created_at: Creation timestamp

        Example:
            >>> users = db.collection("users")
            >>> indexes = users.list_indexes()
            >>> for idx in indexes:
            ...     print(f"{idx['name']}: {idx['fields']}")
        """
        from vibedb.indexes.manager import IndexManager
        manager = IndexManager(self)
        return manager.list_indexes()

    def drop_index(self, name: str) -> bool:
        """
        Drop an index by name.

        Args:
            name: Index name

        Returns:
            bool: True if index was dropped, False if not found

        Example:
            >>> users = db.collection("users")
            >>> users.drop_index("users_idx_email")
            True
        """
        from vibedb.indexes.manager import IndexManager
        manager = IndexManager(self)
        return manager.drop_index(name)

    def index_suggestions(self) -> List[dict]:
        """
        Get index suggestions based on slow query analysis.

        Analyzes query patterns to suggest indexes that would improve performance.

        Returns:
            list[dict]: List of index suggestions containing:
                - fields: Fields to index
                - reason: Why this index is suggested
                - query_count: Number of times fields were queried
                - avg_execution_time_ms: Average execution time
                - estimated_speedup: Estimated performance improvement
                - priority: Priority score (higher = more important)

        Example:
            >>> users = db.collection("users")
            >>> suggestions = users.index_suggestions()
            >>> for s in suggestions:
            ...     print(f"Create index on {s['fields']}: {s['reason']}")
            ...     print(f"Expected speedup: {s['estimated_speedup']}")
            ...     # Apply suggestion
            ...     users.create_index(s['fields'])
        """
        from vibedb.indexes.suggestions import IndexSuggestionEngine
        engine = IndexSuggestionEngine(self)
        return engine.generate_suggestions()

    def set_schema(self, schema_def: dict) -> None:
        """
        Set validation schema for this collection.

        After setting a schema, all inserts and updates will be validated.

        Args:
            schema_def: Schema definition dict

        Raises:
            ValueError: If schema definition is invalid

        Example:
            >>> users = db.collection("users")
            >>> users.set_schema({
            ...     "name": {"type": "string", "required": True, "min": 1},
            ...     "age": {"type": "integer", "min": 0, "max": 150},
            ...     "email": {"type": "string", "required": True, "unique": True},
            ...     "tags": {"type": "array", "items": {"type": "string"}},
            ...     "status": {"type": "string", "enum": ["active", "inactive"], "default": "active"}
            ... })
        """
        from vibedb.schema_validation.validator import validate_schema_definition

        # Validate schema definition
        validate_schema_definition(schema_def)

        # Store schema in metadata
        import json
        schema_json = json.dumps(schema_def)
        schema.set_collection_schema(self._db.engine, self._name, schema_json)

    def get_schema(self) -> Optional[dict]:
        """
        Get current validation schema.

        Returns:
            dict: Schema definition or None if no schema is set

        Example:
            >>> schema = users.get_schema()
            >>> if schema:
            ...     print(schema.keys())
        """
        schema_json = schema.get_collection_schema(self._db.engine, self._name)
        if schema_json:
            import json
            return json.loads(schema_json)
        return None

    def drop_schema(self) -> None:
        """
        Remove schema validation.

        After dropping the schema, documents can be inserted without validation.

        Example:
            >>> users.drop_schema()
            >>> # Now can insert any document structure
        """
        schema.set_collection_schema(self._db.engine, self._name, None)

    def __repr__(self) -> str:
        """String representation."""
        count = self.count()
        return f"Collection(name='{self._name}', documents={count})"
