"""
Core Engine - Unified Search and Indexing Orchestrator.

ARCHITECTURAL FIXES IMPLEMENTED:
--------------------------------
Issue #2 (Tight Coupling): Applied Dependency Injection pattern
- LEANNVectorBackend, ZoektStrategy, and LegacyBackend are now injected via constructor
- CoreEngine depends on abstractions (interfaces) rather than concrete implementations
- Enables testing with mocks and swapping implementations

Issue #4 (Inconsistent Error Handling): Established consistent error handling
- CoreEngineError base exception for all engine errors
- BackendUnavailableError for when backends are not available
- ValidationError for input validation failures
- All methods raise exceptions instead of returning False/None
- Consistent logging throughout

MIGRATION COMPLETE: LEANN is the ONLY vector backend
---------------------------------------------------
The Core Engine uses LEANNVectorBackend exclusively:

- LEANNVectorBackend: High-performance vector search
- No cloud API keys required
- Supports BAAI/bge-small-en-v1.5, microsoft/codebert-base, all-MiniLM-L6-v2
- Web search feature removed (was Mixedbread-specific)

PRODUCT.MD ALIGNMENT - Dual-Mode Operation:
-------------------------------------------
The Core Engine implements PRODUCT.MD's dual-mode operation requirement:

1. STANDALONE POWER MODE (Primary):
   - LEANNVectorBackend provides semantic search
   - Zoekt Strategy provides fast code-aware search via regex/symbolic matching
   - Full functionality without external dependencies
   - This is the DEFAULT and PRIMARY mode of operation

2. AUGMENTED INTELLIGENCE MODE (Enhancement):
   - Legacy Backend (SQLite/DuckDB) provides augmentation when:
     * Metadata queries are needed (file history, versions)
     * Massive scale historical lookups are required
     * Complex joins across metadata are needed
   - The new engine ALWAYS drives; legacy is ONLY for augmentation
   - If legacy backend is unavailable, core functionality is unaffected

MODE SELECTION STRATEGY:
-------------------------
- Search: Zoekt -> LEANN Vector (primary) -> Legacy (fallback/augmentation only)
- Index: LEANN Vector (primary) -> Legacy (dual-write for metadata)
- Ask (RAG): LEANN Vector only
- File History: Legacy only (metadata feature)

Usage:
    # With dependency injection
    vector_backend = LEANNVectorBackend()  # No API key needed
    zoekt_strategy = ZoektStrategy()
    legacy_backend = get_dal_instance()  # Optional - for augmentation only

    engine = CoreEngine(
        vector_backend=vector_backend,
        zoekt_backend=zoekt_strategy,
        legacy_backend=legacy_backend  # Can be None for pure standalone mode
    )
"""

import os
import logging
from typing import Optional, List, Union, Any, Dict

from .types import SearchOptions, SearchResponse, UploadFileOptions, FileMetadata, AskResponse, StoreInfo, ChunkType
from ..search.zoekt import ZoektStrategy
from .interfaces import IVectorBackend, ISearchStrategy, ILegacyBackend

logger = logging.getLogger(__name__)

# CRITICAL FIX: Enforce result size limits to prevent unbounded memory growth
DEFAULT_MAX_RESULTS = 1000
MAX_RESULTS_LIMIT = 10000


# ============================================================================
# CONSISTENT ERROR HANDLING - Issue #4 Fix
# ============================================================================

class CoreEngineError(Exception):
    """
    Base exception for all Core Engine errors.

    ARCHITECTURAL FIX (Issue #4):
    ----------------------------
    Consistent exception hierarchy for proper error handling.
    All CoreEngine methods raise exceptions instead of returning False/None.
    """

    def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
        super().__init__(message)
        self.details = details or {}

    def __str__(self) -> str:
        if self.details:
            return f"{super().__str__()} | Details: {self.details}"
        return super().__str__()


class BackendUnavailableError(CoreEngineError):
    """
    Raised when a required backend is not available or not configured.

    This allows calling code to handle unavailability gracefully
    rather than checking return values.
    """

    def __init__(self, backend_name: str, reason: str = ""):
        message = f"Backend '{backend_name}' is not available"
        if reason:
            message += f": {reason}"
        super().__init__(message, {"backend": backend_name, "reason": reason})
        self.backend_name = backend_name


class ValidationError(CoreEngineError):
    """
    Raised when input validation fails.
    """

    def __init__(self, field: str, value: Any, reason: str):
        message = f"Validation failed for field '{field}': {reason}"
        super().__init__(message, {"field": field, "value": value})
        self.field = field
        self.value = value


class SearchError(CoreEngineError):
    """
    Raised when a search operation fails.
    """

    def __init__(self, query: str, backend: str, reason: str):
        message = f"Search failed for query '{query}' on {backend} backend: {reason}"
        super().__init__(message, {"query": query, "backend": backend})
        self.query = query
        self.backend = backend


class IndexingError(CoreEngineError):
    """
    Raised when an indexing operation fails.
    """

    def __init__(self, file_path: str, backend: str, reason: str):
        message = f"Indexing failed for '{file_path}' on {backend} backend: {reason}"
        super().__init__(message, {"file_path": file_path, "backend": backend})
        self.file_path = file_path
        self.backend = backend


class ResultLimitError(CoreEngineError):
    """
    Raised when results exceed the configured limit.
    """

    def __init__(self, requested: int, maximum: int):
        message = f"Requested result limit {requested} exceeds maximum {maximum}"
        super().__init__(message, {"requested": requested, "maximum": maximum})
        self.requested = requested
        self.maximum = maximum


# ============================================================================
# CORE ENGINE WITH DEPENDENCY INJECTION - Issue #2 Fix
# ============================================================================

class CoreEngine:
    """
    The Unified Core Engine with Dependency Injection.

    ARCHITECTURAL FIX (Issue #2 - Tight Coupling):
    -----------------------------------------------
    All backends are now injected via constructor parameters.
    The engine depends on abstractions (IVectorBackend, ISearchStrategy)
    rather than concrete implementations.

    Benefits:
    - Testable: Can inject mocks for unit testing
    - Flexible: Can swap implementations without changing engine code
    - Loosely coupled: Engine doesn't know about concrete backend classes
    - Clear dependencies: All dependencies are explicit in constructor

    ARCHITECTURAL FIX (Issue #4 - Inconsistent Error Handling):
    ----------------------------------------------------------
    All methods raise exceptions on errors instead of returning False/None.
    Consistent error types enable proper error handling by calling code.
    """

    def __init__(
        self,
        vector_backend: Optional[IVectorBackend] = None,
        zoekt_backend: Optional[ISearchStrategy] = None,
        legacy_backend: Optional[ILegacyBackend] = None,
        max_results: int = DEFAULT_MAX_RESULTS
    ):
        """
        Initialize the Core Engine with dependency injection.

        ARCHITECTURAL FIX (Issue #2): All backends are injected, not instantiated.

        Args:
            vector_backend: Optional vector backend implementation (LEANNVectorBackend recommended)
                           If None, creates a default LEANNVectorBackend
            zoekt_backend: Optional Zoekt search strategy implementation
                          If None, creates a default ZoektStrategy
            legacy_backend: Optional legacy DAL backend
                           If None, legacy features are disabled
            max_results: Maximum number of results to return from search operations

        Raises:
            ValidationError: If max_results is invalid
        """
        # Validate and enforce max_results limit
        if max_results < 1:
            raise ValidationError("max_results", max_results, "Must be at least 1")
        if max_results > MAX_RESULTS_LIMIT:
            raise ValidationError("max_results", max_results,
                               f"Must not exceed {MAX_RESULTS_LIMIT}")

        self.max_results = max_results
        logger.info(f"CoreEngine initialized with max_results={self.max_results}")

        # ARCHITECTURAL FIX (Issue #2): Inject backends instead of creating them
        # Use provided backends or create defaults for backward compatibility

        # LEANNVectorBackend is the ONLY supported vector backend
        if vector_backend is None:
            # Create default LEANN backend
            from .leann_backend import LEANNVectorBackend
            self.vector_backend: IVectorBackend = LEANNVectorBackend()
            logger.info("Using LEANNVectorBackend (high-performance vector store)")
        else:
            self.vector_backend = vector_backend

        self.zoekt_backend: ISearchStrategy = zoekt_backend or ZoektStrategy()
        self.legacy_backend: Optional[ILegacyBackend] = legacy_backend

        # Log availability of each backend
        if self.vector_backend:
            logger.info(f"Vector backend: {type(self.vector_backend).__name__}")
        if self.zoekt_backend:
            logger.info(f"Zoekt backend: {type(self.zoekt_backend).__name__}")
        if self.legacy_backend:
            logger.info(f"Legacy backend: {type(self.legacy_backend).__name__}")

    async def initialize(self) -> None:
        """
        Initialize connections.

        ARCHITECTURAL FIX (Issue #4): Raises exceptions instead of silent failure.

        Raises:
            BackendUnavailableError: If a required backend cannot be initialized
        """
        # Initialize LEANNVectorBackend if it's the active backend
        if self.vector_backend is not None:
            backend_type = type(self.vector_backend).__name__
            logger.info(f"Initializing vector backend: {backend_type}")

            # LEANNVectorBackend requires explicit initialization
            if hasattr(self.vector_backend, "initialize"):
                try:
                    await self.vector_backend.initialize()
                    logger.info(f"{backend_type} initialized successfully")
                except Exception as e:
                    logger.error(f"Failed to initialize {backend_type}: {e}")
                    raise BackendUnavailableError(backend_type, str(e))

        logger.info("CoreEngine initialization complete")

    def _validate_search_options(self, options: SearchOptions) -> SearchOptions:
        """
        Validate and normalize search options.

        ARCHITECTURAL FIX (Issue #4): Consistent validation with exceptions.

        Args:
            options: Search options to validate

        Returns:
            Normalized search options

        Raises:
            ValidationError: If options are invalid
            ResultLimitError: If result limit exceeds maximum
        """
        if options is None:
            options = SearchOptions()

        # Validate top_k (number of results to return)
        if options.top_k is not None and options.top_k > self.max_results:
            raise ResultLimitError(options.top_k, self.max_results)

        # Apply default limit if needed
        if options.top_k is None or options.top_k > self.max_results:
            effective_top_k = min(options.top_k or self.max_results, self.max_results)
            if options.top_k != effective_top_k:
                logger.warning(f"Search top_k {options.top_k} exceeds max_results {self.max_results}, using {effective_top_k}")
            options = SearchOptions(
                top_k=effective_top_k,
                rerank=options.rerank,
                include_web=options.include_web,
                content=options.content,
                use_zoekt=options.use_zoekt
            )

        return options

    def _enforce_result_limit(self, response: SearchResponse, limit: int) -> SearchResponse:
        """
        Enforce result size limit on SearchResponse.

        Args:
            response: The search response to limit
            limit: Maximum number of results to return

        Returns:
            SearchResponse with limited results
        """
        if len(response.data) > limit:
            logger.warning(f"Truncating search results from {len(response.data)} to {limit}")
            return SearchResponse(data=response.data[:limit])
        return response

    async def search(
        self,
        store_ids: List[str],
        query: str,
        options: Optional[SearchOptions] = None
    ) -> SearchResponse:
        """
        Unified search method implementing PRODUCT.MD's dual-mode operation strategy.

        DUAL-MODE OPERATION (PRODUCT.MD ALIGNMENT):
        -------------------------------------------
        STANDALONE POWER MODE (Primary - Default):
            - Uses Core Vector Backend (semantic search with reranking)
            - Falls back to Zoekt for fast symbolic/regex search
            - Full functionality without external dependencies

        AUGMENTED INTELLIGENCE MODE (When legacy is available):
            - Core engine ALWAYS drives the search
            - Legacy backend augments for:
              * Metadata queries (file history, versions)
              * Historical lookups at scale
            - Legacy is NEVER the primary; only fallback/augmentation

        SEARCH ROUTING STRATEGY:
        ------------------------
        Priority 1: Web Search (if requested) -> VectorBackend
        Priority 2: Zoekt (if requested) -> Zoekt Strategy
        Priority 3: Core Vector Search -> VectorBackend (PRIMARY)
        Priority 4: Legacy Backend -> Fallback/Augmentation ONLY

        Args:
            store_ids: List of store identifiers
            query: Search query
            options: Search options

        Returns:
            SearchResponse with results

        Raises:
            ValidationError: If query or options are invalid
            SearchError: If all search backends fail
            BackendUnavailableError: If a required backend is unavailable
        """
        # Validate inputs
        if not query or not query.strip():
            raise ValidationError("query", query, "Query cannot be empty")

        if not store_ids:
            raise ValidationError("store_ids", store_ids, "Store IDs cannot be empty")

        # Validate and normalize options
        options = self._validate_search_options(options)

        # Note: Web search was a Mixedbread cloud feature and is not available
        # with LEANNVectorBackend. If include_web is requested, log a warning and continue.

        # 1. Zoekt Strategy (fast code-aware search)
        if options.use_zoekt and self.zoekt_backend and self.zoekt_backend.is_available():
            try:
                base_path = store_ids[0] if store_ids else "."
                if not os.path.exists(base_path):
                    base_path = "."

                zoekt_results = self.zoekt_backend.search(query, base_path)
                data = []
                result_count = 0
                for file_path, matches in zoekt_results.items():
                    for line_num, content in matches:
                        if result_count >= self.max_results:
                            logger.warning(f"Zoekt search results truncated at {result_count}")
                            break
                        data.append(ChunkType(
                            type="text",
                            text=content,
                            score=1.0,
                            metadata=FileMetadata(path=file_path, hash=""),
                            generated_metadata={"line_number": line_num}
                        ))
                        result_count += 1
                    if result_count >= self.max_results:
                        break
                return SearchResponse(data=data)
            except Exception as e:
                logger.error(f"Zoekt search failed: {e}")
                # Continue to other backends

        # 2. Core Vector Search (Primary)
        try:
            if self.vector_backend and self.vector_backend.is_available():
                result = await self.vector_backend.search(store_ids, query, options)
                return self._enforce_result_limit(result, self.max_results)
        except Exception as e:
            logger.error(f"Core Vector search failed: {e}")
            # Continue to fallback

        # 3. Fallback/Augmentation: Legacy Backend
        if self.legacy_backend and self.legacy_backend.search:
            try:
                legacy_results = self.legacy_backend.search.search_files(query)
                data = []
                for i, res in enumerate(legacy_results[:self.max_results]):
                    data.append(ChunkType(
                        type="text",
                        text=res.get('content', ''),
                        score=res.get('score', 0.5),
                        metadata=FileMetadata(
                            path=res.get('file_path', ''),
                            hash=""
                        ),
                        generated_metadata=res
                    ))
                if len(legacy_results) > self.max_results:
                    logger.warning(f"Legacy search results truncated from {len(legacy_results)} to {self.max_results}")
                return SearchResponse(data=data)
            except Exception as e:
                logger.error(f"Legacy backend search failed: {e}")

        # All backends failed - raise exception (Issue #4 fix)
        raise SearchError(query, "all", "All search backends failed or are unavailable")

    async def ask(
        self,
        store_ids: List[str],
        question: str,
        options: Optional[SearchOptions] = None
    ) -> AskResponse:
        """
        Unified Q&A (RAG) method with dependency-injected backends.

        ARCHITECTURAL FIX (Issue #2): Uses injected vector_backend
        ARCHITECTURAL FIX (Issue #4): Raises exceptions on errors

        Args:
            store_ids: List of store identifiers
            question: Question to ask
            options: Search options

        Returns:
            AskResponse with answer and sources

        Raises:
            ValidationError: If question is invalid
            BackendUnavailableError: If vector backend is unavailable
            CoreEngineError: If the ask operation fails
        """
        if not question or not question.strip():
            raise ValidationError("question", question, "Question cannot be empty")

        if not store_ids:
            raise ValidationError("store_ids", store_ids, "Store IDs cannot be empty")

        if options is None:
            options = SearchOptions()

        if not self.vector_backend or not self.vector_backend.is_available():
            raise BackendUnavailableError("LEANNVectorBackend", "Required for ask operation")

        try:
            return await self.vector_backend.ask(store_ids, question, options)
        except Exception as e:
            logger.error(f"Ask operation failed: {e}")
            raise CoreEngineError(f"Ask operation failed: {e}")

    async def index_file(
        self,
        store_id: str,
        file_path: str,
        content: Union[str, bytes],
        metadata: Optional[FileMetadata] = None
    ) -> None:
        """
        Index a file implementing PRODUCT.MD's dual-mode operation strategy.

        DUAL-MODE OPERATION (PRODUCT.MD ALIGNMENT):
        -------------------------------------------
        STANDALONE POWER MODE (Primary):
            - Writes to Core Vector Backend for semantic search
            - No external dependencies required
            - Full searchability immediately available

        AUGMENTED INTELLIGENCE MODE (Dual-Write for Metadata):
            - Core Vector Backend: Primary search index
            - Legacy Backend: Metadata preservation (versions, history)
            - If legacy fails, core functionality is unaffected

        Args:
            store_id: Store identifier
            file_path: Path of the file to index
            content: File content
            metadata: Optional file metadata

        Raises:
            ValidationError: If parameters are invalid
            IndexingError: If indexing fails on all backends
        """
        if not store_id:
            raise ValidationError("store_id", store_id, "Store ID cannot be empty")
        if not file_path:
            raise ValidationError("file_path", file_path, "File path cannot be empty")
        if content is None:
            raise ValidationError("content", content, "Content cannot be None")

        errors = []

        # 1. Write to Core Vector Backend
        if self.vector_backend and self.vector_backend.is_available():
            try:
                upload_options = UploadFileOptions(
                    external_id=file_path,
                    metadata=metadata or FileMetadata(path=file_path, hash="")
                )
                await self.vector_backend.upload_file(store_id, file_path, content, upload_options)
                logger.debug(f"Successfully indexed {file_path} in LEANNVectorBackend")
                return  # Success - don't try other backends
            except Exception as e:
                error_msg = f"Core Vector indexing failed for {file_path}: {e}"
                logger.error(error_msg)
                errors.append(error_msg)

        # 2. Write to Legacy Backend (Augmentation)
        if self.legacy_backend:
            try:
                str_content = content
                if isinstance(content, bytes):
                    str_content = content.decode('utf-8', errors='ignore')

                self.legacy_backend.storage.save_file_content(file_path, str_content)
                self.legacy_backend.search.index_file(file_path, str_content)
                if metadata:
                    meta_dict = {
                        "path": metadata.path,
                        "hash": metadata.hash,
                        "last_modified": metadata.last_modified,
                        "size": metadata.size
                    }
                    self.legacy_backend.metadata.save_file_metadata(file_path, meta_dict)
                logger.debug(f"Successfully indexed {file_path} in LegacyBackend")
                return  # Success
            except Exception as e:
                error_msg = f"Legacy indexing failed for {file_path}: {e}"
                logger.error(error_msg)
                errors.append(error_msg)

        # All backends failed
        raise IndexingError(file_path, "all", "; ".join(errors))

    async def delete_file(self, store_id: str, file_path: str) -> None:
        """
        Delete a file from index with dependency-injected backends.

        ARCHITECTURAL FIX (Issue #2): Uses injected backends
        ARCHITECTURAL FIX (Issue #4): Raises exceptions on errors

        Args:
            store_id: Store identifier
            file_path: Path of the file to delete

        Raises:
            ValidationError: If parameters are invalid
            CoreEngineError: If deletion fails on all backends
        """
        if not store_id:
            raise ValidationError("store_id", store_id, "Store ID cannot be empty")
        if not file_path:
            raise ValidationError("file_path", file_path, "File path cannot be empty")

        errors = []

        # 1. Delete from Core Vector Backend
        if self.vector_backend and self.vector_backend.is_available():
            try:
                await self.vector_backend.delete_file(store_id, file_path)
                logger.debug(f"Successfully deleted {file_path} from LEANNVectorBackend")
            except Exception as e:
                error_msg = f"Core Vector delete failed for {file_path}: {e}"
                logger.error(error_msg)
                errors.append(error_msg)

        # 2. Delete from Legacy Backend
        if self.legacy_backend:
            try:
                self.legacy_backend.storage.delete_file_content(file_path)
                self.legacy_backend.search.delete_indexed_file(file_path)
                self.legacy_backend.metadata.delete_file_metadata(file_path)
                logger.debug(f"Successfully deleted {file_path} from LegacyBackend")
            except Exception as e:
                error_msg = f"Legacy delete failed for {file_path}: {e}"
                logger.error(error_msg)
                errors.append(error_msg)

        # Check if at least one backend succeeded
        if not errors:
            return  # Success

        # All backends failed
        raise CoreEngineError(f"Failed to delete {file_path} on all backends: {'; '.join(errors)}")

    async def get_store_info(self, store_id: str) -> StoreInfo:
        """
        Get store information using dependency-injected backend.

        ARCHITECTURAL FIX (Issue #2): Uses injected vector_backend
        ARCHITECTURAL FIX (Issue #4): Raises exceptions on errors

        Args:
            store_id: Store identifier

        Returns:
            StoreInfo with store details

        Raises:
            ValidationError: If store_id is invalid
            BackendUnavailableError: If vector backend is unavailable
        """
        if not store_id:
            raise ValidationError("store_id", store_id, "Store ID cannot be empty")

        if not self.vector_backend or not self.vector_backend.is_available():
            raise BackendUnavailableError("LEANNVectorBackend", "Required for get_store_info")

        try:
            return await self.vector_backend.get_info(store_id)
        except Exception as e:
            logger.error(f"Failed to get store info for {store_id}: {e}")
            raise CoreEngineError(f"Failed to get store info: {e}")
