from typing import Optional, List
import math

from analogai.foundation.settings import config
from analogai.foundation.common.logging.app_logger import app_logger
from analogai.foundation.modules.notion.notion_repository import NotionRepository
from analogai.foundation.modules.notion.notion import Notion
from analogai.foundation.common.dtos.notion_expression import NotionExpression
from analogai.foundation.infrastructure.storage.in_memory.in_memory_storage_adapter_impl import InMemoryStorageAdapterImpl
from analogai.foundation.infrastructure.mappers.notion_storage_mapper import NotionStorageMapper


def _environment() -> str:
    value = config.get("ENVIRONMENT")
    return str(value).strip().lower()


def _use_vector_search() -> bool:
    if _environment() in {"test", "testing"}:
        return False
    return bool(config.get("vector.enabled", True))


def _similarity_cutoff() -> float:
    value = config.get("vector.cutoff", 0.4)
    try:
        return float(value)
    except (TypeError, ValueError):
        return 0.4


def _max_vector_candidates() -> int:
    value = config.get("vector.max_candidates", 200)
    try:
        parsed = int(value)
    except (TypeError, ValueError):
        return 200
    return max(1, parsed)


class InMemoryNotionRepositoryImpl(NotionRepository):
    COLLECTION_NAME = "notions"

    def __init__(self, storage_adapter=None):
        if storage_adapter is None:
            self.storage_adapter = InMemoryStorageAdapterImpl()
        else:
            self.storage_adapter = storage_adapter
        self._embedding_service = None
        if _use_vector_search():
            from analogai.foundation.infrastructure.services.embedding_service import EmbeddingService
            self._embedding_service = EmbeddingService()

    def _cosine_similarity(self, vector_a: List[float], vector_b: List[float]) -> float:
        if not vector_a or not vector_b or len(vector_a) != len(vector_b):
            return 0.0
        dot = sum(a * b for a, b in zip(vector_a, vector_b))
        norm_a = math.sqrt(sum(a * a for a in vector_a))
        norm_b = math.sqrt(sum(b * b for b in vector_b))
        if norm_a == 0 or norm_b == 0:
            return 0.0
        return dot / (norm_a * norm_b)

    def create(self, notion: Notion) -> Notion:
        data = NotionStorageMapper.to_dict(notion)
        if _use_vector_search() and self._embedding_service:
            data["embedding"] = self._embedding_service.encode(notion.caption)
        stored_data = self.storage_adapter.store(self.COLLECTION_NAME, data)
        if not stored_data:
            raise ValueError(f"Failed to store notion {notion.id}")
        return notion

    def find(
        self,
        agent_id: str,
        expression: NotionExpression,
        context_ids: Optional[List[str]] = None,
        similarity_threshold: Optional[float] = None,
    ) -> Optional[Notion]:
        if not _use_vector_search() or not self._embedding_service:
            query = {"caption": expression.caption, "agent_id": agent_id}
            all_notions = self.storage_adapter.retrieve_by_attributes(self.COLLECTION_NAME, query)

            if not all_notions:
                return None

            notion_data = all_notions[0]
            notion = NotionStorageMapper.from_dict(notion_data)
            return notion

        candidates = self.storage_adapter.retrieve_by_attributes(
            self.COLLECTION_NAME,
            {"agent_id": agent_id},
        )
        if not candidates:
            return None

        target_caption = (expression.caption or "").strip().lower()
        if target_caption:
            for notion_data in candidates:
                caption = (notion_data.get("caption") or "").strip().lower()
                if caption == target_caption:
                    return NotionStorageMapper.from_dict(notion_data)

        max_candidates = _max_vector_candidates()
        if max_candidates and len(candidates) > max_candidates:
            candidates = candidates[:max_candidates]

        threshold = (
            similarity_threshold
            if similarity_threshold is not None
            else _similarity_cutoff()
        )
        query_embedding = self._embedding_service.encode(expression.caption)
        best_match = None
        best_score = 0.0
        for notion_data in candidates:
            embedding = notion_data.get("embedding")
            if embedding is None:
                embedding = self._embedding_service.encode(notion_data.get("caption", ""))
            score = self._cosine_similarity(query_embedding, embedding)
            app_logger.debug(
                "Vector similarity notion match: query='%s' candidate='%s' score=%.4f threshold=%.4f",
                expression.caption,
                notion_data.get("caption", ""),
                score,
                threshold,
            )
            if score >= threshold and score > best_score:
                best_score = score
                best_match = notion_data

        if not best_match:
            return None

        app_logger.debug(
            "Vector notion match selected: query='%s' candidate='%s' score=%.4f",
            expression.caption,
            best_match.get("caption", ""),
            best_score,
        )
        return NotionStorageMapper.from_dict(best_match)

    def find_by_id(self, notion_id: str) -> Optional[Notion]:
        result_data = self.storage_adapter.retrieve_by_id(self.COLLECTION_NAME, notion_id)
        if result_data:
            notion = NotionStorageMapper.from_dict(result_data)
            return notion
        return None

    def find_all_by_agent(self, agent_id: str) -> List[Notion]:
        results = self.storage_adapter.retrieve_by_attributes(
            self.COLLECTION_NAME,
            {"agent_id": agent_id},
        )
        return [NotionStorageMapper.from_dict(d) for d in results]

    def delete(self, notion: Notion) -> None:
        if notion is None or not getattr(notion, "id", None):
            return
        self.storage_adapter.delete(self.COLLECTION_NAME, str(notion.id))

