from typing import Optional, List
import json
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.neo4j.neo4j_gateway import Neo4jGateway
from analogai.foundation.infrastructure.mappers import NotionStorageMapper
from analogai.foundation.settings import config
from analogai.foundation.common.logging.app_logger import app_logger


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

class Neo4jNotionRepositoryImpl(NotionRepository):
    VECTOR_INDEX_NAME = "notion_caption_embedding"
    DEFAULT_TOP_K = 10
    
    def __init__(self):
        self.neo4j_gateway = Neo4jGateway()
        self._embedding_service: Optional[object] = None
        if _use_vector_search():
            try:
                from analogai.foundation.infrastructure.services.embedding_service import EmbeddingService
                self._embedding_service = EmbeddingService()
            except Exception:
                self._embedding_service = None
        self._ensure_vector_index()
    
    def _get_author_id(self) -> Optional[str]:
        return None

    def _serialize_attributes(self, attributes: Optional[list]) -> Optional[str]:
        if attributes is None:
            return None
        try:
            return json.dumps(attributes)
        except (TypeError, ValueError):
            return None

    def _deserialize_attributes(self, raw: Optional[object]) -> Optional[list]:
        if raw is None:
            return None
        if isinstance(raw, list):
            return raw
        if isinstance(raw, str):
            try:
                return json.loads(raw)
            except (TypeError, ValueError):
                return None
        return None
    
    def _ensure_vector_index(self):
        if not _use_vector_search() or not self._embedding_service:
            return
        
        try:
            with self.neo4j_gateway.session() as session:
                result = session.run("SHOW INDEXES YIELD name WHERE name = $name", name=self.VECTOR_INDEX_NAME)
                if not result.single():
                    session.run(f"""
                        CREATE VECTOR INDEX {self.VECTOR_INDEX_NAME} IF NOT EXISTS
                        FOR (n:Notion)
                        ON n.embedding
                        OPTIONS {{
                            indexConfig: {{
                                `vector.dimensions`: {self._embedding_service.dimension},
                                `vector.similarity_function`: 'cosine'
                            }}
                        }}
                    """)
        except Exception as e:
            pass
    
    def create(self, notion: Notion) -> Optional[Notion]:
        try:
            data = NotionStorageMapper.to_dict(notion)
            attributes_json = self._serialize_attributes(data.get("attributes"))
            
            if _use_vector_search() and self._embedding_service:
                embedding = self._embedding_service.encode(notion.caption)
                query = """
                    MERGE (n:Notion {
                        caption: $caption,
                        user_id: $user_id,
                        agent_id: $agent_id
                    })
                    ON CREATE SET 
                        n.id = $id,
                        n.embedding = $embedding,
                        n.attributes_json = $attributes_json
                    ON MATCH SET 
                        n.embedding = $embedding,
                        n.attributes_json = $attributes_json
                    RETURN n.id as notion_id
                """
                params = {
                    "id": data["id"], 
                    "caption": data["caption"], 
                    "user_id": data["user_id"], 
                    "agent_id": data["agent_id"],
                    "embedding": embedding,
                    "attributes_json": attributes_json,
                }
            else:
                query = """
                    MERGE (n:Notion {
                        caption: $caption,
                        user_id: $user_id,
                        agent_id: $agent_id
                    })
                    ON CREATE SET 
                        n.id = $id,
                        n.attributes_json = $attributes_json
                    ON MATCH SET
                        n.attributes_json = $attributes_json
                    RETURN n.id as notion_id
                """
                params = {
                    "id": data["id"], 
                    "caption": data["caption"], 
                    "user_id": data["user_id"], 
                    "agent_id": data["agent_id"],
                    "attributes_json": attributes_json,
                }
            
            with self.neo4j_gateway.session() as session:
                result = session.run(query, **params)
                record = result.single()
                if record:
                    actual_id = record["notion_id"]
                    if actual_id != data["id"]:
                        notion.id = actual_id
                result.consume()
            return notion
        except Exception as e:
            app_logger.exception("Failed to create notion in Neo4j", exc_info=e)
            return None
    
    def delete(self, notion: Notion) -> None:
        author_id = self._get_author_id()
        if not author_id:
            return
        try:
            with self.neo4j_gateway.session() as session:
                session.run(
                    "MATCH (n:Notion {id: $id}) "
                    "DETACH DELETE n",
                    id=notion.id
                )
        except Exception as e:
            pass
    
    def find(self, agent_id: str, expression: NotionExpression, context_ids: Optional[List[str]] = None, similarity_threshold: float = _similarity_cutoff()) -> Optional[Notion]:
        if not _use_vector_search() or not self._embedding_service:
            return self._find_exact_match(agent_id, expression)
        
        query_embedding = self._embedding_service.encode(expression.caption)
        
        query = f"""
            CALL db.index.vector.queryNodes('{self.VECTOR_INDEX_NAME}', $top_k, $query_embedding)
            YIELD node, score
            WHERE node.agent_id = $agent_id AND score >= $threshold
            RETURN node, score
            ORDER BY score DESC
            LIMIT 1
        """
        
        params = {
            "query_embedding": query_embedding,
            "agent_id": agent_id,
            "top_k": self.DEFAULT_TOP_K,
            "threshold": similarity_threshold
        }
        
        try:
            with self.neo4j_gateway.session() as session:
                result = session.run(query, **params)
                record = result.single()
                if record:
                    notion_data = dict(record["node"])
                    attributes = self._deserialize_attributes(notion_data.get("attributes_json") or notion_data.get("attributes"))
                    return NotionStorageMapper.from_dict({
                        "id": notion_data.get("id"),
                        "user_id": notion_data.get("user_id", ""),
                        "agent_id": notion_data.get("agent_id", ""),
                        "caption": notion_data.get("caption", ""),
                        "attributes": attributes,
                    })
                return None
        except Exception as e:
            return self._find_exact_match(agent_id, expression)
    
    def _find_exact_match(self, agent_id: str, expression: NotionExpression) -> Optional[Notion]:
        query = (
            "MATCH (n:Notion) "
            "WHERE toLower(n.caption) = toLower($caption) "
            "AND n.agent_id = $agent_id "
            "RETURN n LIMIT 1"
        )
        params = {"caption": expression.caption, "agent_id": agent_id}
        
        try:
            with self.neo4j_gateway.session() as session:
                result = session.run(query, **params)
                for record in result:
                    notion_data = dict(record["n"])
                    attributes = self._deserialize_attributes(notion_data.get("attributes_json") or notion_data.get("attributes"))
                    return NotionStorageMapper.from_dict({
                        "id": notion_data.get("id"),
                        "user_id": notion_data.get("user_id", ""),
                        "agent_id": notion_data.get("agent_id", ""),
                        "caption": notion_data.get("caption", ""),
                        "attributes": attributes,
                    })
                return None
        except Exception as e:
            return None
    
    def find_all_by_agent(self, agent_id: str) -> List[Notion]:
        try:
            with self.neo4j_gateway.session() as session:
                result = session.run(
                    "MATCH (n:Notion) WHERE n.agent_id = $agent_id RETURN n",
                    agent_id=agent_id
                )
                notions = []
                for record in result:
                    notion_data = dict(record["n"])
                    attributes = self._deserialize_attributes(notion_data.get("attributes_json") or notion_data.get("attributes"))
                    notions.append(NotionStorageMapper.from_dict({
                        "id": notion_data.get("id"),
                        "user_id": notion_data.get("user_id", ""),
                        "agent_id": notion_data.get("agent_id", ""),
                        "caption": notion_data.get("caption", ""),
                        "attributes": attributes,
                    }))
                return notions
        except Exception as e:
            return []

    def find_by_id(self, notion_id: str) -> Optional[Notion]:
        author_id = self._get_author_id()
        try:
            with self.neo4j_gateway.session() as session:
                result = session.run(
                    "MATCH (n:Notion {id: $id}) "
                    "RETURN n",
                    id=notion_id
                )
                record = result.single()
                if record:
                    notion_data = dict(record["n"])
                    attributes = self._deserialize_attributes(notion_data.get("attributes_json") or notion_data.get("attributes"))
                    return NotionStorageMapper.from_dict({
                        "id": notion_data.get("id"),
                        "user_id": notion_data.get("user_id", ""),
                        "agent_id": notion_data.get("agent_id", ""),
                        "caption": notion_data.get("caption", ""),
                        "attributes": attributes,
                    })
                return None
        except Exception as e:
            return None
    
    def exists(self, agent_id: str, expression: NotionExpression) -> bool:
        return self.find(agent_id, expression, None) is not None
    
    def search(self, expression: NotionExpression, similarity_threshold: float = _similarity_cutoff(), limit: int = 10) -> List[Notion]:
        if not _use_vector_search() or not self._embedding_service:
            return self._search_exact_match(expression)
        
        query_embedding = self._embedding_service.encode(expression.caption)
        
        query = f"""
            CALL db.index.vector.queryNodes('{self.VECTOR_INDEX_NAME}', $top_k, $query_embedding)
            YIELD node, score
            WHERE score >= $threshold
            RETURN node, score
            ORDER BY score DESC
            LIMIT $limit
        """
        
        params = {
            "query_embedding": query_embedding,
            "top_k": max(self.DEFAULT_TOP_K, limit),
            "threshold": similarity_threshold,
            "limit": limit
        }
        
        try:
            with self.neo4j_gateway.session() as session:
                result = session.run(query, **params)
                notions = []
                for record in result:
                    notion_data = dict(record["node"])
                    attributes = self._deserialize_attributes(notion_data.get("attributes_json") or notion_data.get("attributes"))
                    notions.append(NotionStorageMapper.from_dict({
                        "id": notion_data.get("id"),
                        "user_id": notion_data.get("user_id", ""),
                        "agent_id": notion_data.get("agent_id", ""),
                        "caption": notion_data.get("caption", ""),
                        "attributes": attributes,
                    }))
                return notions
        except Exception as e:
            return self._search_exact_match(expression)
    
    def _search_exact_match(self, expression: NotionExpression) -> List[Notion]:
        query = (
            "MATCH (n:Notion) "
            "WHERE toLower(n.caption) = toLower($caption) "
            "RETURN n"
        )
        params = {"caption": expression.caption}
        
        try:
            with self.neo4j_gateway.session() as session:
                result = session.run(query, **params)
                notions = []
                for record in result:
                    notion_data = dict(record["n"])
                    attributes = self._deserialize_attributes(notion_data.get("attributes_json") or notion_data.get("attributes"))
                    notions.append(NotionStorageMapper.from_dict({
                        "id": notion_data.get("id"),
                        "user_id": notion_data.get("user_id", ""),
                        "agent_id": notion_data.get("agent_id", ""),
                        "caption": notion_data.get("caption", ""),
                        "attributes": attributes,
                    }))
                return notions
        except Exception as e:
            return []