from __future__ import annotations

from typing import List, Optional
from concurrent.futures import ThreadPoolExecutor

from analogai.deepthink.application.value_objects.user_agent import UserAgent
from analogai.deepthink.messaging.question_answering.question_entity_service import (
    EntityExtractionService,
)
from analogai.deepthink.messaging.question_answering.services.formatting.fact_formatter import (
    QuestionFactFormatter,
)
from analogai.deepthink.messaging.question_answering.adapters.belief_search_adapter import (
    BeliefSearchAdapter,
)
from analogai.deepthink.messaging.question_answering.adapters.notion_search_adapter import (
    NotionSearchAdapter,
)
from analogai.foundation.modules.fact.services.fact_service import FactServicePort
from analogai.deepthink.messaging.question_answering.services.background.belief_background_info_builder import (
    BeliefBackgroundInfoBuilder,
)
from analogai.deepthink.messaging.question_answering.services.background.fact_background_info_builder import (
    GraphRagBackgroundInfoBuilder,
)
from analogai.foundation.common.logging.app_logger import app_logger  # type: ignore[import-not-found]
from analogai.foundation.modules.fact.enums.fact_search_mode import FactSearchMode
from analogai.foundation.modules.fact.fact_source import FactSource, SourceType
from analogai.foundation.settings import config as app_config
from analogai.deepthink.messaging.fact.utils.fact_utils import (
    collect_fact_window,
    get_belief_id,
    get_fact_text,
    get_statement_id,
)


class QuestionPromptBackgroundInfoBuilder:
    def __init__(
        self,
        keyword_extractor: EntityExtractionService,
        belief_retriever: BeliefSearchAdapter,
        notion_retriever: NotionSearchAdapter,
        belief_formatter: QuestionFactFormatter,
        fact_retriever: FactServicePort | None = None,
    ) -> None:
        self._keyword_extractor = keyword_extractor
        self._belief_retriever = belief_retriever
        self._notion_retriever = notion_retriever
        self._belief_formatter = belief_formatter
        self._fact_retriever = fact_retriever
        self._belief_background_builder = BeliefBackgroundInfoBuilder(
            belief_retriever=self._belief_retriever,
            notion_retriever=self._notion_retriever,
            belief_formatter=self._belief_formatter,
        )
        self._fact_background_builder = GraphRagBackgroundInfoBuilder(self._fact_retriever)

    def build_prompt_background_info(
        self,
        question_text: str,
        user_agent: UserAgent,
    ) -> tuple[
        Optional[tuple[str, List[str], str]],
        int,
        int,
        List,
        List,
        List[str],
        dict[str, str],
        dict[str, FactSource],
        dict[str, list[str]],
        bool,
    ]:
        fact_lookup: dict[str, str] = {}
        source_lookup: dict[str, FactSource] = {}
        context_lookup: dict[str, list[str]] = {}
        parallel_enabled = bool(app_config.get("qa.belief_fact_parallel_enabled", True))
        # Step 1: extract keywords (entities + relations)
        entities, relations = self._keyword_extractor.extract_entities_and_relations(question_text)
        keywords_for_prompt = self._combine_terms(entities, relations)
        try:
            app_logger.info(
                "[QuestionPromptBackgroundInfoBuilder] entities=%s relations=%s question='%s'",
                entities,
                relations,
                question_text.strip(),
            )
        except Exception:
            pass
        try:
            app_logger.debug(
                "[QuestionPromptBackgroundInfoBuilder] entities_extracted=%s relations_extracted=%s",
                entities,
                relations,
            )
        except Exception:
            pass
        if not entities:
            try:
                app_logger.info("[QuestionPromptBackgroundInfoBuilder] no entities extracted")
            except Exception:
                pass
        # Step 2: graph search (entity -> statement -> fact) + RAG (question)
        beliefs, notions, notion_lines, belief_block, belief_count = self._build_belief_background(
            user_agent,
            entities,
            parallel_enabled=parallel_enabled,
        )
        if self._fact_retriever:
            fact_lookup = self._fact_background_builder.build_background(
                user_agent=user_agent,
                question_text=question_text,
                entities=entities,
                notions=notions,
                search_mode=FactSearchMode.VECTOR_ONLY,
                fact_lookup=fact_lookup,
                source_lookup=source_lookup,
                context_lookup=context_lookup,
            )
            self._build_linked_facts(
                beliefs=beliefs,
                notions=notions,
                fact_lookup=fact_lookup,
                source_lookup=source_lookup,
                context_lookup=context_lookup,
            )

        relaxed_used = self._fact_background_builder.was_relaxed()

        # Step 5: construct background
        if not beliefs and not notions and not fact_lookup:
            try:
                app_logger.info(
                    "[QuestionPromptBackgroundInfoBuilder] empty beliefs/notions/turns entities=%s relations=%s",
                    entities,
                    relations,
                )
            except Exception:
                pass
            return None, 0, 0, [], [], [], {}, source_lookup, context_lookup, relaxed_used
        if not fact_lookup and context_lookup:
            context_lookup = {}
        fact_list = list(fact_lookup.values())
        if not belief_block and notion_lines:
            belief_block = "\n".join(notion_lines)
        if not belief_block:
            belief_block = ""
        if fact_list:
            fact_block = "\n".join(f"CONVERSATION FACT: {line}" for line in fact_list)
            belief_block = f"{belief_block}\n{fact_block}" if belief_block else fact_block
        # Allow belief block to grow without truncation.
        if not belief_block.strip():
            try:
                app_logger.info(
                    "[QuestionPromptBackgroundInfoBuilder] empty belief block keywords=%s beliefs=%d notions=%d turns=%d",
                    keywords_for_prompt,
                    len(beliefs or []),
                    len(notions or []),
                    len(fact_list),
                )
            except Exception:
                pass
            return None, 0, 0, [], notions, notion_lines, fact_lookup, source_lookup, context_lookup, relaxed_used

        line_count = self._count_block_lines(belief_block)
        try:
            app_logger.info(
                "[QuestionPromptBackgroundInfoBuilder] background ready beliefs=%d lines=%d notions=%d turns=%d block_chars=%d",
                belief_count,
                line_count,
                len(notion_lines),
                len(fact_list),
                len(belief_block),
            )
        except Exception:
            pass

        return (
            (question_text.strip(), keywords_for_prompt, belief_block),
            belief_count,
            line_count,
            beliefs,
            notions,
            notion_lines,
            fact_lookup,
            source_lookup,
            context_lookup,
            relaxed_used,
        )

    def _build_belief_background(
        self,
        user_agent: UserAgent,
        entities: List[str],
        parallel_enabled: bool,
    ) -> tuple[List, List, List[str], str, int]:
        if parallel_enabled and self._fact_retriever:
            with ThreadPoolExecutor(max_workers=2) as executor:
                belief_future = executor.submit(
                    self._belief_background_builder.build_background,
                    user_agent,
                    entities,
                )
                return belief_future.result()
        return self._belief_background_builder.build_background(
            user_agent,
            entities,
        )

    def _build_statement_facts(
        self,
        user_agent: UserAgent,
        entities: List[str],
        notions: List | None,
        fact_lookup: dict[str, str],
        source_lookup: dict[str, FactSource],
        search_mode: FactSearchMode,
        parallel_workers: int | None = None,
        search_timeout_seconds: float | None = None,
    ) -> dict[str, str]:
        if not self._fact_retriever:
            return fact_lookup
        return self._fact_background_builder.build_statement_facts(
            user_agent=user_agent,
            entities=entities,
            notions=notions,
            search_mode=search_mode,
            fact_lookup=fact_lookup,
            source_lookup=source_lookup,
            parallel_workers=parallel_workers,
            search_timeout_seconds=search_timeout_seconds,
        )

    def _build_direct_facts(
        self,
        user_agent: UserAgent,
        question_text: str,
        fact_lookup: dict[str, str],
        source_lookup: dict[str, FactSource],
        search_mode: FactSearchMode,
    ) -> dict[str, str]:
        if not self._fact_retriever:
            return fact_lookup
        return self._fact_background_builder.build_direct_facts(
            user_agent=user_agent,
            question_text=question_text,
            search_mode=search_mode,
            fact_lookup=fact_lookup,
            source_lookup=source_lookup,
        )

    def _build_linked_facts(
        self,
        beliefs: List,
        notions: List | None,
        fact_lookup: dict[str, str],
        source_lookup: dict[str, FactSource],
        context_lookup: dict[str, list[str]],
    ) -> None:
        if not self._fact_retriever:
            return
        text_lookup = {_normalize_fact_text(value) for value in fact_lookup.values() if value}
        context_window = max(
            0,
            _coerce_int(
                app_config.get(
                    "qa.neighbouring_background_fact_max_step",
                    app_config.get("qa.fact_context_window", 2),
                ),
                2,
            ),
        )
        time_window_seconds = _resolve_context_time_window_seconds()

        for notion in notions or []:
            notion_id = getattr(notion, "id", None)
            if not notion_id:
                continue
            facts = self._fact_retriever.get_facts_for_notion(str(notion_id))
            self._append_facts(
                facts,
                fact_lookup,
                source_lookup,
                context_lookup,
                context_window,
                time_window_seconds,
                text_lookup,
            )

        for belief in beliefs or []:
            belief_id = get_belief_id(belief)
            if belief_id:
                facts = self._fact_retriever.get_facts_for_belief(belief_id)
                self._append_facts(
                    facts,
                    fact_lookup,
                    source_lookup,
                    context_lookup,
                    context_window,
                    time_window_seconds,
                    text_lookup,
                )
            statement_id = get_statement_id(belief)
            if statement_id:
                facts = self._fact_retriever.get_facts_for_statement(statement_id)
                self._append_facts(
                    facts,
                    fact_lookup,
                    source_lookup,
                    context_lookup,
                    context_window,
                    time_window_seconds,
                    text_lookup,
                )
                if _is_hypothetical_belief(belief):
                    facts = self._fact_retriever.get_facts_for_hypothetical_statement(statement_id)
                    self._append_facts(
                        facts,
                        fact_lookup,
                        source_lookup,
                        context_lookup,
                        context_window,
                        time_window_seconds,
                        text_lookup,
                    )

    @staticmethod
    def _count_block_lines(text: str) -> int:
        return sum(1 for line in text.splitlines() if line.strip())

    def _build_with_keywords(
        self,
        user_agent: UserAgent,
        question_text: str,
        keywords: List[str],
        parallel_enabled: bool,
        fact_lookup: dict[str, str],
        source_lookup: dict[str, FactSource],
        context_lookup: dict[str, list[str]] | None,
        search_mode: FactSearchMode,
    ) -> tuple[List, List, List[str], str, int, dict[str, str]]:
        if parallel_enabled and self._fact_retriever:
            with ThreadPoolExecutor(max_workers=2) as executor:
                belief_future = executor.submit(
                    self._belief_background_builder.build_background,
                    user_agent,
                    keywords,
                )
                fact_future = executor.submit(
                    self._fact_background_builder.build_background,
                    user_agent,
                    question_text,
                    keywords,
                    None,
                    search_mode,
                    fact_lookup,
                    source_lookup,
                    context_lookup,
                )
                beliefs, notions, notion_lines, belief_block, belief_count = belief_future.result()
                fact_lookup = fact_future.result()
        else:
            beliefs, notions, notion_lines, belief_block, belief_count = (
                self._belief_background_builder.build_background(
                    user_agent,
                    keywords,
                )
            )
            if self._fact_retriever:
                fact_lookup = self._fact_background_builder.build_background(
                    user_agent,
                    question_text,
                    keywords,
                    None,
                    search_mode=search_mode,
                    fact_lookup=fact_lookup,
                    source_lookup=source_lookup,
                    context_lookup=context_lookup,
                )
        return beliefs, notions, notion_lines, belief_block, belief_count, fact_lookup

    @staticmethod
    def _combine_terms(entities: List[str], relations: List[str]) -> List[str]:
        combined = []
        seen = set()
        for item in list(entities) + list(relations):
            if not item:
                continue
            key = str(item).strip().lower()
            if not key or key in seen:
                continue
            seen.add(key)
            combined.append(str(item).strip())
        return combined

    @staticmethod
    def _append_facts(
        facts: List,
        lookup: dict[str, str],
        source_lookup: dict[str, FactSource],
        context_lookup: dict[str, list[str]],
        context_window: int,
        context_time_window_seconds: int | None,
        text_lookup: set[str],
    ) -> None:
        for fact in facts or []:
            text = get_fact_text(fact)
            if not text:
                continue
            cleaned = str(text).strip()
            if not cleaned:
                continue
            if _looks_like_question(cleaned):
                continue
            normalized = _normalize_fact_text(cleaned)
            if normalized in text_lookup:
                continue
            fact_key = getattr(fact, "id", None)
            if fact_key is None:
                continue
            key = str(fact_key)
            if key in lookup:
                continue
            lookup[key] = cleaned
            text_lookup.add(normalized)
            resolved_source = _resolve_fact_source(fact)
            if resolved_source is not None:
                source_lookup[key] = resolved_source
            if key not in context_lookup:
                context_lookup[key] = _collect_fact_context(
                    fact,
                    window=context_window,
                    time_window_seconds=context_time_window_seconds,
                )


def _collect_fact_context(fact, window: int, time_window_seconds: int | None) -> list[str]:
    if fact is None:
        return []
    window_facts = collect_fact_window(
        fact,
        window=max(0, int(window)),
        time_window_seconds=time_window_seconds,
    )
    context_lines: list[str] = []
    for item in window_facts:
        text = get_fact_text(item)
        cleaned = str(text or "").strip()
        if not cleaned:
            continue
        if _looks_like_question(cleaned):
            continue
        context_lines.append(cleaned)
    return context_lines


def _looks_like_question(text: str) -> bool:
    if "?" in text:
        return True
    lowered = text.strip().lower()
    return lowered.startswith("q:") or lowered.startswith("question:")


def _normalize_fact_text(text: str) -> str:
    return " ".join(text.split()).strip().casefold()


def _resolve_fact_source(fact) -> FactSource | None:
    if fact is None:
        return None
    source = getattr(fact, "source", None)
    if source is not None:
        return source
    source_id = getattr(fact, "source_id", None)
    if not source_id:
        return None
    raw = str(source_id)
    if not raw:
        return None
    if ":" in raw:
        type_text, value = raw.split(":", 1)
    else:
        type_text, value = raw, None
    type_text = type_text.strip()
    value = value.strip() if value else None
    try:
        source_type = SourceType(type_text)
    except Exception:
        try:
            source_type = SourceType[type_text.upper()]
        except Exception:
            source_type = SourceType.OTHER
    return FactSource(source_type, value)


def _resolve_context_time_window_seconds() -> int | None:
    value = _coerce_int(app_config.get("qa.fact_context_time_window_seconds"), 300)
    if value <= 0:
        return None
    return int(value)


def _coerce_int(value: object | None, default: int) -> int:
    if value is None:
        return int(default)
    if isinstance(value, bool):
        return int(default)
    if isinstance(value, int):
        return int(value)
    if isinstance(value, float) and value.is_integer():
        return int(value)
    text = str(value).strip()
    if text.startswith(("+", "-")):
        sign = text[0]
        numeric = text[1:]
    else:
        sign = ""
        numeric = text
    if numeric.isdigit():
        return int(f"{sign}{numeric}")
    return int(default)


def _is_hypothetical_belief(belief) -> bool:
    relation = getattr(belief, "relation", None)
    if relation is None:
        return False
    value = str(getattr(relation, "value", relation)).strip().lower()
    return "cause" in value
