"""Deduplication engine for the knowledge plane.

Provides multi-level deduplication:
1. Content-hash deduplication (SHA-256 exact match)
2. Semantic deduplication (cosine similarity > threshold)
3. URL deduplication (same URL + same content hash)
"""

from __future__ import annotations

import hashlib
import logging
from dataclasses import dataclass, field

from cyberbrain26.knowledge_plane.contracts import KnowledgeDocument
from cyberbrain26.knowledge_plane.embedding_service import EmbeddingService
from cyberbrain26.knowledge_plane.vector_store import KnowledgeVectorStore

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class DuplicateCandidate:
    """A potential duplicate found during deduplication."""

    document_id: str
    source_uri: str
    similarity: float
    match_type: str  # "exact_hash", "semantic", "url"


@dataclass
class DeduplicationResult:
    """Result of a deduplication check."""

    is_duplicate: bool
    candidates: list[DuplicateCandidate] = field(default_factory=list)
    recommended_action: str = "register"  # "register", "skip", "merge"


class DeduplicationEngine:
    """Multi-level deduplication for knowledge documents."""

    def __init__(
        self,
        *,
        embedding_service: EmbeddingService | None = None,
        vector_store: KnowledgeVectorStore | None = None,
        semantic_threshold: float = 0.95,
    ) -> None:
        self.embedding_service = embedding_service
        self.vector_store = vector_store
        self.semantic_threshold = semantic_threshold

    def check_duplicate(
        self,
        content: bytes,
        source_uri: str,
        existing_documents: list[KnowledgeDocument],
    ) -> DeduplicationResult:
        """Check if content is a duplicate of existing documents."""
        content_hash = hashlib.sha256(content).hexdigest()
        candidates: list[DuplicateCandidate] = []

        # Level 1: Exact content hash
        for doc in existing_documents:
            if doc.content_sha256 == content_hash:
                candidates.append(
                    DuplicateCandidate(
                        document_id=doc.document_id,
                        source_uri=doc.source_uri,
                        similarity=1.0,
                        match_type="exact_hash",
                    )
                )

        if candidates:
            return DeduplicationResult(
                is_duplicate=True,
                candidates=candidates,
                recommended_action="skip",
            )

        # Level 2: URL deduplication
        for doc in existing_documents:
            if doc.source_uri == source_uri and doc.content_sha256 == content_hash:
                candidates.append(
                    DuplicateCandidate(
                        document_id=doc.document_id,
                        source_uri=doc.source_uri,
                        similarity=1.0,
                        match_type="url",
                    )
                )

        if candidates:
            return DeduplicationResult(
                is_duplicate=True,
                candidates=candidates,
                recommended_action="skip",
            )

        # Level 3: Semantic similarity (if embedding service and vector store available)
        if self.embedding_service is not None and self.vector_store is not None:
            try:
                text = content.decode("utf-8", errors="replace")
                vectors = self.embedding_service.embed_texts([text])
                if vectors:
                    similar = self.vector_store.search(vectors[0], limit=5)
                    for result in similar:
                        # LanceDB returns _distance; convert to similarity
                        distance = result.get("_distance", 1.0)
                        similarity = 1.0 - distance
                        if similarity >= self.semantic_threshold:
                            candidates.append(
                                DuplicateCandidate(
                                    document_id=result.get("id", ""),
                                    source_uri=result.get("path", ""),
                                    similarity=similarity,
                                    match_type="semantic",
                                )
                            )
            except Exception as exc:  # noqa: BLE001 - fail closed: skip semantic match on any error
                logger.debug("Semantic similarity check skipped: %s", exc)

        if candidates:
            return DeduplicationResult(
                is_duplicate=True,
                candidates=candidates,
                recommended_action="merge",
            )

        return DeduplicationResult(is_duplicate=False, recommended_action="register")

    def deduplicate_batch(
        self,
        items: list[tuple[bytes, str]],
        existing_documents: list[KnowledgeDocument],
    ) -> list[tuple[int, DeduplicationResult]]:
        """Check a batch of items for duplicates."""
        results = []
        seen_hashes: set[str] = {doc.content_sha256 for doc in existing_documents}
        for idx, (content, source_uri) in enumerate(items):
            content_hash = hashlib.sha256(content).hexdigest()
            if content_hash in seen_hashes:
                results.append(
                    (
                        idx,
                        DeduplicationResult(
                            is_duplicate=True,
                            candidates=[
                                DuplicateCandidate(
                                    document_id="",
                                    source_uri=source_uri,
                                    similarity=1.0,
                                    match_type="exact_hash",
                                )
                            ],
                            recommended_action="skip",
                        ),
                    )
                )
                continue
            result = self.check_duplicate(content, source_uri, existing_documents)
            if not result.is_duplicate:
                seen_hashes.add(content_hash)
            results.append((idx, result))
        return results


__all__ = ["DeduplicationEngine", "DeduplicationResult", "DuplicateCandidate"]
