import logging

from altk_evolve.backend.base import BaseEntityBackend
from altk_evolve.config.evolve import EvolveConfig
from altk_evolve.schema.conflict_resolution import EntityUpdate
from altk_evolve.schema.core import Entity, Namespace, RecordedEntity
from altk_evolve.schema.exceptions import NamespaceAlreadyExistsException, NamespaceNotFoundException
from altk_evolve.schema.guidelines import ConsolidationResult

logger = logging.getLogger(__name__)


class EvolveClient:
    """Wrapper client around evolve entity backends."""

    def __init__(self, config: EvolveConfig | None = None):
        """Initialize the Evolve client."""
        self.config = config or EvolveConfig()
        self.backend: BaseEntityBackend

        if self.config.backend == "milvus":
            from altk_evolve.backend.milvus import MilvusEntityBackend

            self.backend = MilvusEntityBackend(self.config.settings)
        elif self.config.backend == "filesystem":
            from altk_evolve.backend.filesystem import FilesystemEntityBackend, FilesystemSettings

            if not isinstance(self.config.settings, (FilesystemSettings, type(None))):
                raise TypeError(
                    f"Type of `config` should be `{FilesystemSettings.__name__}` or `None`, got `{type(self.config.settings).__name__}`"
                )
            self.backend = FilesystemEntityBackend(self.config.settings)
        elif self.config.backend == "postgres":
            from altk_evolve.backend.postgres import PostgresEntityBackend
            from altk_evolve.config.postgres import PostgresDBSettings

            if not isinstance(self.config.settings, (PostgresDBSettings, type(None))):
                raise TypeError(
                    f"Type of `config` should be `{PostgresDBSettings.__name__}` or `None`, got `{type(self.config.settings).__name__}`"
                )
            self.backend = PostgresEntityBackend(self.config.settings)
        else:
            raise NotImplementedError(f"Entity backend not implemented: {self.config.backend}")

    def ready(self) -> bool:
        """Check if the backend is healthy."""
        return self.backend.ready()

    def create_namespace(self, namespace_id: str | None = None) -> Namespace:
        """Create a new namespace for entities to exist in."""
        return self.backend.create_namespace(namespace_id)

    def all_namespaces(self, limit: int = 10) -> list[Namespace]:
        """Get details about a specific namespace."""
        return self.backend.search_namespaces(limit)

    def get_namespace_details(self, namespace_id: str) -> Namespace:
        """Get details about a specific namespace."""
        return self.backend.get_namespace_details(namespace_id)

    def search_namespaces(self, limit: int = 10) -> list[Namespace]:
        """Search namespace with filters."""
        return self.backend.search_namespaces(limit)

    def delete_namespace(self, namespace_id: str) -> None:
        """Delete a namespace that entities exist in."""
        self.backend.delete_namespace(namespace_id)

    def update_entities(self, namespace_id: str, entities: list[Entity], enable_conflict_resolution: bool = True) -> list[EntityUpdate]:
        """Add multiple entities to a namespace."""
        return self.backend.update_entities(namespace_id, entities, enable_conflict_resolution)

    def search_entities(
        self, namespace_id: str, query: str | None = None, filters: dict | None = None, limit: int = 10
    ) -> list[RecordedEntity]:
        """Search for entities in a namespace."""
        return self.backend.search_entities(namespace_id, query, filters, limit)

    def get_all_entities(self, namespace_id: str, filters: dict | None = None, limit: int = 100) -> list[RecordedEntity]:
        """Get all entities from a namespace."""
        return self.search_entities(namespace_id, query=None, filters=filters, limit=limit)

    def delete_entity_by_id(self, namespace_id: str, entity_id: str) -> None:
        """Delete a specific entity by its ID."""
        self.backend.delete_entity_by_id(namespace_id, entity_id)

    def get_entity_by_id(self, namespace_id: str, entity_id: str) -> RecordedEntity | None:
        """Fetch a single entity by its ID. Returns None if not found."""
        results = self.search_entities(namespace_id, filters={"id": entity_id}, limit=1)
        return results[0] if results else None

    def patch_entity_metadata(self, namespace_id: str, entity_id: str, metadata_updates: dict) -> RecordedEntity:
        """Merge metadata_updates into an entity without touching content or ID."""
        return self.backend.update_entity_metadata(namespace_id, entity_id, metadata_updates)

    def get_public_entities(
        self,
        query: str | None = None,
        entity_type: str | None = None,
        limit: int = 100,
        exclude_namespace_ids: list[str] | None = None,
    ) -> list[RecordedEntity]:
        """Search for public entities across all namespaces.

        Args:
            query: Optional semantic search query.
            entity_type: Optional type filter (e.g. 'guideline').
            limit: Maximum total results to return.
            exclude_namespace_ids: Namespace IDs to skip (e.g. the caller's own
                namespace whose entities are already returned via a private search).
        """
        if limit <= 0:
            return []

        excluded = set(exclude_namespace_ids or [])
        all_results: list[RecordedEntity] = []
        for ns in self.search_namespaces(limit=1000):
            if ns.id in excluded:
                continue
            remaining = limit - len(all_results)
            if remaining <= 0:
                break
            filters: dict = {"metadata.visibility": "public"}
            if entity_type:
                filters["type"] = entity_type
            all_results.extend(self.search_entities(ns.id, query=query, filters=filters, limit=remaining))
        return all_results

    def cluster_guidelines(self, namespace_id: str, threshold: float | None = None, limit: int = 10000) -> list[list[RecordedEntity]]:
        """Cluster guideline entities by task description similarity.

        Args:
            namespace_id: Namespace to fetch entities from.
            threshold: Cosine similarity threshold (0-1). Defaults to config value.
            limit: Maximum number of guideline entities to fetch for clustering.

        Returns:
            List of clusters, each containing related RecordedEntity objects.
        """
        from altk_evolve.llm.guidelines.clustering import cluster_entities

        if threshold is None:
            threshold = self.config.clustering_threshold

        entities = self.get_all_entities(namespace_id, filters={"type": "guideline"}, limit=limit)
        if len(entities) >= limit:
            logger.warning(
                "Fetched %d entities (hit limit=%d); clustering results may be incomplete. Consider increasing the limit.",
                len(entities),
                limit,
            )
        return cluster_entities(entities, threshold=threshold)

    def consolidate_guidelines(self, namespace_id: str, threshold: float | None = None) -> ConsolidationResult:
        """Cluster similar guidelines and combine each cluster into consolidated guidelines.

        Args:
            namespace_id: Namespace to consolidate entities in.
            threshold: Cosine similarity threshold (0-1). Defaults to config value.

        Returns:
            ConsolidationResult with counts of clusters, guidelines before, and guidelines after.
        """
        from altk_evolve.llm.guidelines.clustering import combine_cluster

        clusters = self.cluster_guidelines(namespace_id, threshold=threshold)
        clusters_found = 0
        guidelines_before = 0
        guidelines_after = 0

        for cluster in clusters:
            # Phase 1: combine + insert (skip cluster on failure)
            try:
                consolidated_guidelines = combine_cluster(cluster)

                task_description = (cluster[0].metadata or {}).get("task_description", "")
                new_entities = [
                    Entity(
                        content=guideline.content,
                        type="guideline",
                        metadata={
                            "task_description": task_description,
                            "rationale": guideline.rationale,
                            "category": guideline.category,
                            "trigger": guideline.trigger,
                            "implementation_steps": guideline.implementation_steps,
                        },
                    )
                    for guideline in consolidated_guidelines
                ]
                if not new_entities:
                    logger.warning(
                        "LLM returned no consolidated guidelines for cluster (IDs: %s); skipping deletion.",
                        [e.id for e in cluster],
                    )
                    continue
                self.update_entities(namespace_id, new_entities, enable_conflict_resolution=False)
            except Exception:
                logger.warning(
                    "Failed to consolidate cluster of %d entities (IDs: %s); skipping.",
                    len(cluster),
                    [e.id for e in cluster],
                    exc_info=True,
                )
                continue

            clusters_found += 1
            guidelines_before += len(cluster)
            guidelines_after += len(consolidated_guidelines)

            # Phase 2: delete originals (log errors but don't roll back insert)
            for entity in cluster:
                try:
                    self.delete_entity_by_id(namespace_id, entity.id)
                except Exception:
                    logger.warning(
                        "Failed to delete original entity %s after successful insert; skipping.",
                        entity.id,
                        exc_info=True,
                    )

        return ConsolidationResult(
            clusters_found=clusters_found,
            guidelines_before=guidelines_before,
            guidelines_after=guidelines_after,
        )

    # Convenience methods for common patterns
    def namespace_exists(self, namespace_id: str) -> bool:
        """Check if a namespace exists."""
        try:
            self.backend.get_namespace_details(namespace_id)
            return True
        except NamespaceNotFoundException:
            return False

    def ensure_namespace(self, namespace_id: str) -> Namespace:
        """Get an existing namespace or create it if missing."""
        try:
            return self.get_namespace_details(namespace_id)
        except NamespaceNotFoundException:
            try:
                return self.create_namespace(namespace_id)
            except NamespaceAlreadyExistsException:
                return self.get_namespace_details(namespace_id)
