from __future__ import annotations

from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import Optional, List, TYPE_CHECKING
from uuid import uuid4

from analogai.foundation.common.logging.app_logger import app_logger
from analogai.foundation.infrastructure.services.embedding_service import EmbeddingService
from analogai.foundation.modules.fact.basefact import BaseFact
from analogai.foundation.modules.fact.fact_deletion_utils import (
    delete_notion,
    delete_statement,
)
from analogai.foundation.modules.fact.notion_fact import NotionFact
from analogai.foundation.modules.fact.direct_statement_fact import DirectStatementFact
from analogai.foundation.modules.fact.hypothetical_statement_fact import (
    HypotheticalStatementFact,
)
from analogai.foundation.modules.fact.source.fact_source import FactSource
from analogai.foundation.modules.fact.repositories.fact_repository import FactRepository
from analogai.foundation.modules.fact.source.repositories.fact_source_repository import FactSourceRepository
from analogai.foundation.modules.fact.enums.fact_search_mode import FactSearchMode
from analogai.foundation.modules.fact.enums.fact_embedding_mode import FactEmbeddingMode

if TYPE_CHECKING:
    from analogai.foundation.modules.fact.source.services.fact_source_service import FactSourceService


class FactServicePort(ABC):
    @abstractmethod
    def record_direct_statement_fact(
        self,
        user_id: str,
        agent_id: str,
        statement_id: str,
        text: str,
        role: str,
        *,
        source: FactSource,
        user_name: Optional[str] = None,
        timestamp: Optional[datetime] = None,
    ) -> str:
        raise NotImplementedError

    @abstractmethod
    def record_notion_fact(
        self,
        user_id: str,
        agent_id: str,
        notion_id: str,
        text: str,
        role: str,
        *,
        source: FactSource,
        user_name: Optional[str] = None,
        timestamp: Optional[datetime] = None,
    ) -> str:
        raise NotImplementedError

    @abstractmethod
    def record_hypothetical_statement_fact(
        self,
        user_id: str,
        agent_id: str,
        statement_id: str,
        text: str,
        role: str,
        *,
        source: FactSource,
        user_name: Optional[str] = None,
        timestamp: Optional[datetime] = None,
    ) -> str:
        raise NotImplementedError

    @abstractmethod
    def link_belief(self, belief_id: str, turn_id: str) -> None:
        raise NotImplementedError

    @abstractmethod
    def link_notion(self, notion_id: str, turn_id: str) -> None:
        raise NotImplementedError

    @abstractmethod
    def link_hypothetical_statement(self, statement_id: str, turn_id: str) -> None:
        raise NotImplementedError

    @abstractmethod
    def get_facts_for_turn(self, turn_id: str) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def get_facts_for_statement(self, statement_id: str) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def get_facts_for_belief(self, belief_id: str) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def get_facts_for_notion(self, notion_id: str) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def get_facts_for_hypothetical_statement(self, statement_id: str) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def get_nearby_turn_facts(
        self,
        turn_id: str,
        user_id: str,
        agent_id: str,
        window: int = 2,
    ) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def search_facts(
        self,
        user_id: str,
        agent_id: str,
        query: str,
        top_k: int = 5,
        similarity_cutoff: Optional[float] = None,
        mode: FactSearchMode = FactSearchMode.VECTOR_THEN_TEXT,
    ) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def get_facts_by_source(self, source_id: str) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def delete_facts_by_source(self, source_id: str) -> List[BaseFact]:
        raise NotImplementedError

    @abstractmethod
    def get_statement_ids_for_turn(self, turn_id: str) -> List[str]:
        raise NotImplementedError

    @abstractmethod
    def get_belief_ids_for_turn(self, turn_id: str) -> List[str]:
        raise NotImplementedError


class RepositoryBackedFactService(FactServicePort):
    def __init__(
        self,
        repository: FactRepository,
        source_repository: Optional[FactSourceRepository] = None,
        source_service: Optional["FactSourceService"] = None,
        statement_service: Optional[object] = None,
        notion_service: Optional[object] = None,
        embedding_service: Optional[EmbeddingService] = None,
        embedding_mode: FactEmbeddingMode = FactEmbeddingMode.ENABLED,
        max_chunk_chars: int = 800,
        overlap_sentences: int = 1,
        max_paragraphs_per_fact: Optional[int] = None,
        max_sentences_per_fact: Optional[int] = None,
    ) -> None:
        if statement_service is None or notion_service is None:
            raise ValueError("FactService requires statement_service and notion_service wiring")
        self._repository = repository
        self._source_repository = source_repository
        self._source_service = source_service
        self._statement_service = statement_service
        self._notion_service = notion_service
        self._embedding_service = embedding_service
        self._embedding_mode = embedding_mode
        self._max_chunk_chars = max(200, int(max_chunk_chars))
        self._overlap_sentences = max(0, int(overlap_sentences))
        resolved_paragraphs_per_fact = (
            max_paragraphs_per_fact
            if max_paragraphs_per_fact is not None
            else max_sentences_per_fact
        )
        self._max_paragraphs_per_fact = (
            int(resolved_paragraphs_per_fact)
            if resolved_paragraphs_per_fact is not None
            else None
        )

    def record_direct_statement_fact(
        self,
        user_id: str,
        agent_id: str,
        statement_id: str,
        text: str,
        role: str,
        *,
        source: FactSource,
        user_name: Optional[str] = None,
        timestamp: Optional[datetime] = None,
    ) -> str:
        if not text or not text.strip():
            raise ValueError("Fact text must be provided")
        if not statement_id:
            raise ValueError("Concrete fact requires reference id")
        fact_id = str(uuid4())
        if not isinstance(source, FactSource):
            raise TypeError("Fact source must be a FactSource instance")
        self._register_source(source)
        resolved_timestamp = timestamp or datetime.now(timezone.utc)
        embedding_vector = None
        if self._embedding_mode == FactEmbeddingMode.ENABLED:
            if self._embedding_service is None:
                raise RuntimeError("Fact embedding service unavailable")
            embedding_vector = self._embedding_service.encode(text)
        fact = DirectStatementFact(
            id=fact_id,
            statement_id=statement_id,
            turn_id=None,
            user_id=user_id,
            agent_id=agent_id,
            embedding=embedding_vector,
            source_id=source.id,
            prev_id=None,
            next_id=None,
            created_at=resolved_timestamp,
        )
        setattr(fact, "text", text)
        self._repository.save_fact(fact)
        return fact_id

    def record_notion_fact(
        self,
        user_id: str,
        agent_id: str,
        notion_id: str,
        text: str,
        role: str,
        *,
        source: FactSource,
        user_name: Optional[str] = None,
        timestamp: Optional[datetime] = None,
    ) -> str:
        if not text or not text.strip():
            raise ValueError("Fact text must be provided")
        if not notion_id:
            raise ValueError("Concrete fact requires reference id")
        fact_id = str(uuid4())
        if not isinstance(source, FactSource):
            raise TypeError("Fact source must be a FactSource instance")
        self._register_source(source)
        resolved_timestamp = timestamp or datetime.now(timezone.utc)
        embedding_vector = None
        if self._embedding_mode == FactEmbeddingMode.ENABLED:
            if self._embedding_service is None:
                raise RuntimeError("Fact embedding service unavailable")
            embedding_vector = self._embedding_service.encode(text)
        fact = NotionFact(
            id=fact_id,
            notion_id=notion_id,
            turn_id=None,
            user_id=user_id,
            agent_id=agent_id,
            embedding=embedding_vector,
            source_id=source.id,
            prev_id=None,
            next_id=None,
            created_at=resolved_timestamp,
        )
        setattr(fact, "text", text)
        self._repository.save_fact(fact)
        return fact_id

    def record_hypothetical_statement_fact(
        self,
        user_id: str,
        agent_id: str,
        statement_id: str,
        text: str,
        role: str,
        *,
        source: FactSource,
        user_name: Optional[str] = None,
        timestamp: Optional[datetime] = None,
    ) -> str:
        if not text or not text.strip():
            raise ValueError("Fact text must be provided")
        if not statement_id:
            raise ValueError("Concrete fact requires reference id")
        fact_id = str(uuid4())
        if not isinstance(source, FactSource):
            raise TypeError("Fact source must be a FactSource instance")
        self._register_source(source)
        resolved_timestamp = timestamp or datetime.now(timezone.utc)
        embedding_vector = None
        if self._embedding_mode == FactEmbeddingMode.ENABLED:
            if self._embedding_service is None:
                raise RuntimeError("Fact embedding service unavailable")
            embedding_vector = self._embedding_service.encode(text)
        fact = HypotheticalStatementFact(
            id=fact_id,
            statement_id=statement_id,
            turn_id=None,
            user_id=user_id,
            agent_id=agent_id,
            embedding=embedding_vector,
            source_id=source.id,
            prev_id=None,
            next_id=None,
            created_at=resolved_timestamp,
        )
        setattr(fact, "text", text)
        self._repository.save_fact(fact)
        return fact_id

    def set_source_service(self, source_service: Optional["FactSourceService"]) -> None:
        self._source_service = source_service

    def set_statement_service(self, statement_service: Optional[object]) -> None:
        self._statement_service = statement_service

    def set_notion_service(self, notion_service: Optional[object]) -> None:
        self._notion_service = notion_service

    def _register_source(self, source: FactSource) -> None:
        if source is None:
            return
        if self._source_service is not None:
            try:
                self._source_service.store_source(source)
            except Exception as exc:
                app_logger.warning(
                    "[FactService] Failed to register fact source via service source_id=%s error=%s",
                    source.id,
                    exc,
                )
            return
        if self._source_repository is None:
            return
        try:
            self._source_repository.register_source(source)
        except Exception as exc:
            app_logger.warning(
                "[FactService] Failed to register fact source source_id=%s error=%s",
                source.id,
                exc,
            )

    def link_statement(self, statement_id: str, turn_id: str) -> None:
        self._repository.link_statement(statement_id, turn_id)
        self._repository.update_facts_statement_id(turn_id, statement_id)

    def link_belief(self, belief_id: str, turn_id: str) -> None:
        self._repository.link_belief(belief_id, turn_id)

    def link_notion(self, notion_id: str, turn_id: str) -> None:
        self._repository.link_notion(notion_id, turn_id)
        self._repository.update_facts_notion_id(turn_id, notion_id)

    def link_hypothetical_statement(self, statement_id: str, turn_id: str) -> None:
        self._repository.link_hypothetical_statement(statement_id, turn_id)
        self._repository.update_facts_hypothetical_statement_id(turn_id, statement_id)

    def get_facts_for_turn(self, turn_id: str) -> List[BaseFact]:
        return self._attach_fact_services(self._repository.get_facts_for_turn(turn_id))

    def get_facts_for_statement(self, statement_id: str) -> List[BaseFact]:
        turn_id = self._repository.get_turn_id_for_statement(statement_id)
        return (
            self._attach_fact_services(self._repository.get_facts_for_turn(turn_id))
            if turn_id
            else []
        )

    def get_facts_for_belief(self, belief_id: str) -> List[BaseFact]:
        turn_id = self._repository.get_turn_id_for_belief(belief_id)
        return (
            self._attach_fact_services(self._repository.get_facts_for_turn(turn_id))
            if turn_id
            else []
        )

    def get_facts_for_notion(self, notion_id: str) -> List[BaseFact]:
        turn_id = self._repository.get_turn_id_for_notion(notion_id)
        return (
            self._attach_fact_services(self._repository.get_facts_for_turn(turn_id))
            if turn_id
            else []
        )

    def get_facts_for_hypothetical_statement(self, statement_id: str) -> List[BaseFact]:
        turn_id = self._repository.get_turn_id_for_hypothetical_statement(statement_id)
        return (
            self._attach_fact_services(self._repository.get_facts_for_turn(turn_id))
            if turn_id
            else []
        )

    def get_nearby_turn_facts(
        self,
        turn_id: str,
        user_id: str,
        agent_id: str,
        window: int = 2,
    ) -> List[BaseFact]:
        index = self._repository.get_turn_ids(user_id, agent_id)
        if not index or turn_id not in index:
            return []
        position = index.index(turn_id)
        start = max(0, position - max(0, int(window)))
        end = min(len(index), position + max(0, int(window)) + 1)
        nearby_ids = index[start:end]
        results: List[BaseFact] = []
        for tid in nearby_ids:
            results.extend(self._repository.get_facts_for_turn(tid))
        return self._attach_fact_services(results)

    def search_facts(
        self,
        user_id: str,
        agent_id: str,
        query: str,
        top_k: int = 5,
        similarity_cutoff: Optional[float] = None,
        mode: FactSearchMode = FactSearchMode.VECTOR_THEN_TEXT,
    ) -> List[BaseFact]:
        if not query or not query.strip():
            return []
        if mode == FactSearchMode.TEXT_ONLY:
            return self._attach_fact_services(
                self._repository.search_text(user_id, agent_id, query, top_k=top_k)
            )
        if mode == FactSearchMode.VECTOR_ONLY:
            return self._attach_fact_services(
                self._search_vector_only(user_id, agent_id, query, top_k, similarity_cutoff)
            )
        if mode == FactSearchMode.VECTOR_THEN_TEXT:
            results = self._search_vector_only(user_id, agent_id, query, top_k, similarity_cutoff)
            if results:
                return self._attach_fact_services(results)
            return self._attach_fact_services(
                self._repository.search_text(user_id, agent_id, query, top_k=top_k)
            )
        return []

    def get_facts_by_source(self, source_id: str) -> List[BaseFact]:
        return self._attach_fact_services(self._repository.get_facts_by_source(source_id))

    def delete_facts_by_source(self, source_id: str) -> List[BaseFact]:
        deleted = self._repository.delete_facts_by_source(source_id)
        if deleted:
            deleted = self._attach_fact_services(deleted)
            self._delete_attached_entities(deleted)
        return deleted

    def get_statement_ids_for_turn(self, turn_id: str) -> List[str]:
        return self._repository.get_statement_ids_for_turn(turn_id)

    def get_belief_ids_for_turn(self, turn_id: str) -> List[str]:
        return self._repository.get_belief_ids_for_turn(turn_id)


    def _search_vector_only(
        self,
        user_id: str,
        agent_id: str,
        query: str,
        top_k: int,
        similarity_cutoff: Optional[float],
    ) -> List[BaseFact]:
        if self._embedding_mode != FactEmbeddingMode.ENABLED:
            return []
        if self._embedding_service is None:
            raise RuntimeError("Fact embedding service unavailable")
        query_embedding = self._embedding_service.encode(query)
        if not query_embedding:
            app_logger.warning(
                "[FactService] encode returned empty query embedding query='%s'",
                query.strip(),
            )
            return []
        return self._repository.search_similar(
            user_id,
            agent_id,
            query_embedding,
            top_k=top_k,
            similarity_cutoff=similarity_cutoff,
        )


    def _delete_attached_entities(self, facts: List[BaseFact]) -> None:
        for fact in facts:
            try:
                if isinstance(fact, DirectStatementFact):
                    delete_statement(fact.statement_id, self._statement_service)
                elif isinstance(fact, NotionFact):
                    delete_notion(fact.notion_id, self._notion_service)
            except Exception as exc:
                app_logger.warning(
                    "[FactService] Failed to delete attached entity for fact_id=%s error=%s",
                    getattr(fact, "id", None),
                    exc,
                )

    def _attach_fact_services(self, facts: List[BaseFact]) -> List[BaseFact]:
        if not facts:
            return []
        for fact in facts:
            fact._statement_service = self._statement_service
            fact._notion_service = self._notion_service
            if fact._deleter is None:
                fact._deleter = self._repository.delete_fact
        return facts


