"""IMA Stage 2 — Duplicate Check (Local Embedding Similarity).

Finds near-duplicate memories and suggests merge/skip.
Uses local embeddings — no API cost.
"""

import json
import logging
from dataclasses import dataclass

from soagila.db.sqlite_init import get_db
from soagila.core.tcmg.embedding_service import generate_embedding, cosine_similarity

logger = logging.getLogger("soagila.ima.dedup")

DUPLICATE_THRESHOLD = 0.92   # Almost identical
SIMILAR_THRESHOLD = 0.78     # Related but different
MAX_CANDIDATES = 30          # Max memories to compare against


@dataclass
class DedupResult:
    """Result of Stage 2 deduplication check."""
    action: str              # "store" | "merge" | "skip"
    duplicate_id: str | None  # ID of duplicate/similar memory
    similarity: float         # Similarity score
    reason: str

    def to_dict(self) -> dict:
        return {
            "action": self.action,
            "duplicate_id": self.duplicate_id,
            "similarity": round(self.similarity, 4),
            "reason": self.reason,
        }


async def check_duplicate(content: str, user_id: str = "local") -> DedupResult:
    """Check if content is a near-duplicate of existing memories.

    Returns:
        DedupResult recommending store/merge/skip.
    """
    # Generate embedding for new content
    new_embedding = generate_embedding(content)

    if new_embedding is None:
        # Embedding model not available — fall back to content hash
        return await _hash_based_dedup(content, user_id)

    # Find similar existing memories
    db = await get_db()
    cursor = await db.execute(
        """
        SELECT id, embedding, encrypted_content
        FROM episodic_memories
        WHERE user_id = ? AND is_decayed = 0 AND embedding IS NOT NULL
        ORDER BY created_at DESC
        LIMIT ?
        """,
        (user_id, MAX_CANDIDATES),
    )
    rows = await cursor.fetchall()

    best_similarity = 0.0
    best_id = None
    best_content = None

    for row in rows:
        mem_id = str(row[0])
        mem_embedding = json.loads(row[1]) if isinstance(row[1], str) else row[1]

        if not mem_embedding:
            continue

        sim = cosine_similarity(new_embedding, mem_embedding)

        if sim > best_similarity:
            best_similarity = sim
            best_id = mem_id
            raw = row[2]
            best_content = raw.decode("utf-8") if isinstance(raw, (bytes, memoryview)) else str(raw)

    # Decision
    if best_similarity >= DUPLICATE_THRESHOLD:
        logger.info(
            f"IMA dedup: SKIP — {best_similarity:.3f} similarity with {best_id}"
        )
        return DedupResult(
            action="skip",
            duplicate_id=best_id,
            similarity=best_similarity,
            reason=f"near_duplicate (>{DUPLICATE_THRESHOLD})",
        )

    if best_similarity >= SIMILAR_THRESHOLD:
        logger.info(
            f"IMA dedup: MERGE — {best_similarity:.3f} similarity with {best_id}"
        )
        return DedupResult(
            action="merge",
            duplicate_id=best_id,
            similarity=best_similarity,
            reason=f"similar_content (>{SIMILAR_THRESHOLD})",
        )

    logger.debug(
        f"IMA dedup: STORE — best similarity {best_similarity:.3f} (below threshold)"
    )
    return DedupResult(
        action="store",
        duplicate_id=None,
        similarity=best_similarity,
        reason="no_duplicate_found",
    )


async def _hash_based_dedup(content: str, user_id: str) -> DedupResult:
    """Fallback: check for exact duplicates using content hash."""
    import hashlib

    content_hash = hashlib.sha256(content.encode()).hexdigest()

    db = await get_db()
    cursor = await db.execute(
        "SELECT id FROM episodic_memories WHERE content_hash = ? AND user_id = ?",
        (content_hash, user_id),
    )
    row = await cursor.fetchone()

    if row:
        return DedupResult(
            action="skip",
            duplicate_id=str(row[0]),
            similarity=1.0,
            reason="exact_hash_match",
        )

    return DedupResult(
        action="store",
        duplicate_id=None,
        similarity=0.0,
        reason="no_hash_match (embedding unavailable)",
    )


async def merge_memories(existing_id: str, new_content: str) -> str:
    """Merge new content into an existing memory by appending.

    Returns the updated memory ID.
    """
    db = await get_db()
    cursor = await db.execute(
        "SELECT encrypted_content FROM episodic_memories WHERE id = ?",
        (existing_id,),
    )
    row = await cursor.fetchone()

    if not row:
        return existing_id

    existing_raw = row[0]
    existing_content = existing_raw.decode("utf-8") if isinstance(existing_raw, (bytes, memoryview)) else str(existing_raw)

    # Merge: append new info with separator
    merged = f"{existing_content}\n\n[Updated] {new_content}"

    # Regenerate embedding for merged content
    new_embedding = generate_embedding(merged)

    import hashlib
    from datetime import datetime, timezone

    now = datetime.now(timezone.utc).isoformat()
    content_hash = hashlib.sha256(merged.encode()).hexdigest()

    await db.execute(
        """
        UPDATE episodic_memories
        SET encrypted_content = ?,
            embedding = ?,
            content_hash = ?,
            stability = MIN(stability + 0.2, 3.0),
            access_count = access_count + 1,
            updated_at = ?
        WHERE id = ?
        """,
        (
            merged.encode("utf-8"),
            json.dumps(new_embedding) if new_embedding else None,
            content_hash,
            now,
            existing_id,
        ),
    )
    await db.commit()

    logger.info(f"IMA dedup: merged new content into memory {existing_id}")
    return existing_id
