from __future__ import annotations

from typing import Iterable, List, Optional

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.services.formatting.history_formatter import (
    QuestionHistoryFormatter,
)
from analogai.deepthink.messaging.question_answering.services.background.prompt_background_info_builder import (
    QuestionPromptBackgroundInfoBuilder,
)
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.assembly.answer_fact_assembler import (
    AnswerFactAssembler,
)
from analogai.deepthink.messaging.fact.value_objects.source import FactSource
from analogai.deepthink.messaging.fact.services.fact_response_decorator import FactResponseDecorator
from analogai.deepthink.messaging.fact.services.fact_context_assembler import FactContextBuilder
from analogai.deepthink.messaging.fact.services.fact_context_service import FactContextWindowService
from analogai.deepthink.messaging.question_answering.question_answering_llm_client import (
    QuestionAnsweringLlmClient,
)
from analogai.deepthink.messaging.question_answering.qa_synthesizer_prompt_composer import (
    QASynthesizerPromptComposer,
)
from analogai.deepthink.infrastructure.gateways.llm.llm_gateway_provider_factory import (
    LlmGatewayProviderFactory,
)
from analogai.deepthink.infrastructure.gateways.llm.enums.llm_provider import LlmProvider
from analogai.deepthink.infrastructure.gateways.llm.utils.llm_logging_util import (
    format_json_for_log,
    format_llm_output_value,
    trim_for_log,
)
from analogai.foundation.common.logging.app_logger import app_logger
from analogai.foundation.settings import config as app_config
from analogai.deepthink.messaging.question_answering.enums.synthesized_text_type import (
    SynthesizedTextType,
)
from analogai.deepthink.messaging.question_answering.strategies.synthesis_strategy import (
    SYNTHESIS_STRATEGIES,
)
from analogai.deepthink.messaging.question_answering.relaxed_search_notice import (
    prepend_relaxed_notice,
)
from analogai.deepthink.presentation.preprocessing.previous_messages_stack import PreviousMessagesStack
from analogai.deepthink.application.value_objects.search_result import SearchResult
from analogai.deepthink.messaging.question_answering.utils.raw_output_formatter import (
    format_raw_output,
)


def _resolve_default_synthesized_text_type(value: str | None) -> SynthesizedTextType:
    if value is None:
        raise ValueError(
            "DEFAULT_SYNTHESIZED_TEXT_TYPE must be set to one of: exact_match, detailed, hybrid"
        )
    normalized = str(value).strip().lower()
    try:
        return SynthesizedTextType(normalized)
    except ValueError as exc:
        raise ValueError(
            "DEFAULT_SYNTHESIZED_TEXT_TYPE must be set to one of: exact_match, detailed, hybrid"
        ) from exc


_DEFAULT_SYNTHESIZED_TEXT_TYPE = _resolve_default_synthesized_text_type(
    app_config.get("qa.default_synthesized_text_type", "detailed")
)


class QuestionAnsweringService:
    def __init__(
        self,
        belief_service,
        notion_service,
        llm_gateway,
        synthesizer_gateway=None,
        fact_context_assembler: Optional[FactContextBuilder] = None,
        fact_context_service: Optional[FactContextWindowService] = None,
        keyword_extractor: Optional[EntityExtractionService] = None,
        prompt_builder: Optional[QASynthesizerPromptComposer] = None,
        llm_caller: Optional[QuestionAnsweringLlmClient] = None,
        synthesizer_caller: Optional[QuestionAnsweringLlmClient] = None,
        belief_retriever: Optional[BeliefSearchAdapter] = None,
        notion_retriever: Optional[NotionSearchAdapter] = None,
        fact_retriever: Optional[FactServicePort] = None,
        belief_formatter: Optional[QuestionFactFormatter] = None,
        history_formatter: Optional[QuestionHistoryFormatter] = None,
        facts_builder: Optional[AnswerFactAssembler] = None,
        background_info_builder: Optional[QuestionPromptBackgroundInfoBuilder] = None,
    ):
        self._keyword_extractor = keyword_extractor or EntityExtractionService()
        self._prompt_builder = prompt_builder or QASynthesizerPromptComposer()
        self._llm_caller = llm_caller or QuestionAnsweringLlmClient(llm_gateway)
        self._provenance_decorator = (
            FactResponseDecorator(fact_context_assembler)
            if fact_context_assembler is not None
            else None
        )
        self._fact_context_service = fact_context_service
        self._synthesizer_gateway = synthesizer_gateway
        self._synthesizer_caller = synthesizer_caller
        self._synthesizer_callers: dict[SynthesizedTextType, QuestionAnsweringLlmClient] = {}
        qa_max_entity_depth = app_config.get(
            "qa.fact_building.max_entity_depth",
            app_config.get("qa.max_entity_depth", 3),
        )
        qa_max_beliefs = app_config.get(
            "qa.fact_building.max_beliefs",
            app_config.get("qa.max_beliefs", 12),
        )
        self._belief_retriever = belief_retriever or BeliefSearchAdapter(
            belief_service,
            max_entity_depth=qa_max_entity_depth,
            max_beliefs=qa_max_beliefs,
        )
        self._notion_retriever = notion_retriever or NotionSearchAdapter(notion_service)
        self._fact_retriever = fact_retriever
        self._belief_formatter = belief_formatter or QuestionFactFormatter(
            provenance_decorator=self._provenance_decorator,
            fact_context_service=self._fact_context_service,
        )
        self._history_formatter = history_formatter or QuestionHistoryFormatter()
        self._facts_builder = facts_builder or AnswerFactAssembler(self._fact_context_service)
        self._background_info_builder = background_info_builder or QuestionPromptBackgroundInfoBuilder(
            keyword_extractor=self._keyword_extractor,
            belief_retriever=self._belief_retriever,
            notion_retriever=self._notion_retriever,
            belief_formatter=self._belief_formatter,
            fact_retriever=self._fact_retriever,
        )

    def answer_question(
        self,
        question_text: str,
        user_agent: UserAgent,
        synthesized_text_type: SynthesizedTextType | None = None,
        previous_messages_stack: Optional[PreviousMessagesStack] = None,
    ) -> Optional[SearchResult]:
        if not question_text or not question_text.strip():
            return None

        (
            prompt_background_info,
            belief_count,
            line_count,
            beliefs,
            _,
            notion_lines,
            fact_lookup,
            source_lookup,
            context_lookup,
            relaxed_used,
        ) = (
            self._background_info_builder.build_prompt_background_info(
                question_text=question_text,
                user_agent=user_agent,
            )
        )
        if prompt_background_info is None:
            app_logger.info(
                "[QuestionAnsweringService] no prompt background info; returning I don't know question='%s'",
                question_text.strip(),
            )
            return SearchResult.from_sources("I don't know")
        question_for_prompt, keywords_for_prompt, belief_block_for_prompt = prompt_background_info
        if not beliefs and not notion_lines and not fact_lookup:
            app_logger.info(
                "[QuestionAnsweringService] no prompt background info question='%s'",
                question_text.strip(),
            )
            return SearchResult.from_sources("I don't know")
        if belief_count + line_count == 0:
            app_logger.info(
                "[QuestionAnsweringService] empty background info counts beliefs=%d lines=%d question='%s'",
                belief_count,
                line_count,
                question_text.strip(),
            )
            return SearchResult.from_sources("I don't know")
        if not beliefs and not notion_lines and not fact_lookup:
            app_logger.info(
                "[QuestionAnsweringService] no background facts; returning I don't know question='%s'",
                question_text.strip(),
            )
            return SearchResult.from_sources("I don't know", source_lookup.values())

        user_message = self._prompt_builder.build_user_message(
            question=question_for_prompt,
            keywords=keywords_for_prompt,
            belief_block=belief_block_for_prompt,
        )
        raw_answer = ""

        qa_history = self._history_formatter.format_qa_history(previous_messages_stack)
        user_history_block = self._history_formatter.format_user_history(previous_messages_stack)

        synthesized = self._synthesize_answer(
            question_text=question_text.strip(),
            belief_block=belief_block_for_prompt,
            raw_answer=raw_answer,
            beliefs=beliefs,
            notion_lines=notion_lines,
            fact_lookup=fact_lookup,
            source_lookup=source_lookup,
            context_lookup=context_lookup,
            synthesized_text_type=synthesized_text_type,
            qa_history=qa_history,
            user_history_block=user_history_block,
        )
        detailed_sources = self._build_fact_sources(
            fact_lookup,
            source_lookup,
            user_name=user_agent.user_id,
        )
        if not synthesized or not synthesized.strip() or synthesized.strip().lower() == "i don't know":
            app_logger.info(
                "[QuestionAnsweringService] empty synthesized answer question='%s' raw_answer_len=%d beliefs=%d",
                question_text.strip(),
                len(raw_answer or ""),
                len(list(beliefs or [])),
            )
            if raw_answer and raw_answer.strip():
                final_answer = raw_answer.strip()
                if relaxed_used:
                    final_answer = prepend_relaxed_notice(final_answer)
                return SearchResult.from_sources(final_answer, detailed_sources)
            return SearchResult.from_sources("I don't know", detailed_sources)
        app_logger.info(
            "[QuestionAnsweringService] answer ready question='%s' answer_len=%d sources=%d",
            question_text.strip(),
            len(synthesized),
            len(source_lookup),
        )
        final_synthesized = synthesized
        if relaxed_used and final_synthesized:
            final_synthesized = prepend_relaxed_notice(final_synthesized)
        return SearchResult.from_sources(final_synthesized, detailed_sources)

    def build_question_background_info(
        self,
        question_text: str,
        user_agent: UserAgent,
    ) -> str:
        if not question_text or not question_text.strip():
            return ""

        prompt_background_info, _, _, _, _, _, _, _, _ = self._background_info_builder.build_prompt_background_info(
            question_text=question_text,
            user_agent=user_agent,
        )
        if prompt_background_info is None:
            return ""
        return prompt_background_info[2]

    def get_background_info(
        self,
        question_text: str,
        user_agent: UserAgent,
    ) -> str:
        return self.build_question_background_info(
            question_text=question_text,
            user_agent=user_agent,
        )

    def search(
        self,
        question_text: str,
        user_agent: UserAgent,
        synthesized_text_type: SynthesizedTextType | None = None,
        previous_messages_stack: Optional[PreviousMessagesStack] = None,
    ) -> Optional[SearchResult]:
        return self.answer_question(
            question_text=question_text,
            user_agent=user_agent,
            synthesized_text_type=synthesized_text_type,
            previous_messages_stack=previous_messages_stack,
        )

    def generate_background_info(
        self,
        question_text: str,
        user_agent: UserAgent,
    ) -> str:
        return self.build_question_background_info(
            question_text=question_text,
            user_agent=user_agent,
        )

    def _synthesize_answer(
        self,
        question_text: str,
        belief_block: str,
        raw_answer: str,
        beliefs: Iterable,
        notion_lines: Iterable[str],
        fact_lookup: dict[str, str],
        source_lookup: dict[str, FactSource],
        context_lookup: dict[str, list[str]],
        synthesized_text_type: SynthesizedTextType | None = None,
        qa_history: str = "",
        user_history_block: str = "",
    ) -> str | None:
        default_type = _resolve_default_synthesized_text_type(
            app_config.get("qa.default_synthesized_text_type", "detailed")
        )
        text_type = synthesized_text_type or default_type
        strategy = SYNTHESIS_STRATEGIES[text_type]
        include_raw = strategy.include_raw_answer
        belief_lines = [self._belief_formatter.format_fact(belief) for belief in beliefs or []]
        facts, additional_background = self._facts_builder.build_fact_sections(
            beliefs=beliefs,
            belief_lines=belief_lines,
            notion_lines=notion_lines,
            fact_lookup=fact_lookup,
            source_lookup=source_lookup,
            context_lookup=context_lookup,
            raw_answer=raw_answer,
            qa_history=qa_history,
            user_history_block=user_history_block,
            include_raw_answer=include_raw,
        )
        app_logger.info(
            "[QuestionAnsweringService] Fact sources derived belief_lines=%d notion_lines=%d rag_facts=%d context_facts=%d",
            len(belief_lines),
            len(list(notion_lines or [])),
            len(fact_lookup or {}),
            len(context_lookup or {}),
        )
        if text_type == SynthesizedTextType.HYBRID:
            try:
                threshold = int(
                    app_config.get("qa.hybrid_synthesizer_background_threshold_chars", 200)
                )
            except (TypeError, ValueError):
                threshold = 200
            background_len = len((additional_background or "").strip())
            text_type = (
                SynthesizedTextType.DETAILED
                if background_len > threshold
                else SynthesizedTextType.EXACT_MATCH
            )
            strategy = SYNTHESIS_STRATEGIES[text_type]
            if include_raw != strategy.include_raw_answer:
                include_raw = strategy.include_raw_answer
                facts, additional_background = self._facts_builder.build_fact_sections(
                    beliefs=beliefs,
                    belief_lines=belief_lines,
                    notion_lines=notion_lines,
                    fact_lookup=fact_lookup,
                    source_lookup=source_lookup,
                    context_lookup=context_lookup,
                    raw_answer=raw_answer,
                    qa_history=qa_history,
                    user_history_block=user_history_block,
                    include_raw_answer=include_raw,
                )
        if not facts:
            return "I don't know"
        if not any(line.strip() for line in facts.splitlines()):
            return "I don't know"
        synthesizer_enabled = app_config.get("qa.synthesizer_enabled", True)
        if not synthesizer_enabled:
            background_text = additional_background.strip() if additional_background else ""
            return format_raw_output(facts, background_text)
        system_prompt = self._prompt_builder.build_synthesis_system_prompt(
            synthesized_text_type=strategy.text_type,
        )
        user_message = self._prompt_builder.build_synthesis_user_message(
            question=question_text,
            facts=facts,
            qa_history=qa_history,
            additional_background=additional_background,
        )
        app_logger.info(
            "[QuestionAnsweringService] Synthesizer LLM call\n%s",
            format_json_for_log({
                "additional_background": trim_for_log(additional_background),
            }),
        )
        app_logger.info(
            "Synthesizer prompt prepared: facts=%d, prompt_chars=%d",
            len([line for line in facts.splitlines() if line.strip()]),
            len(system_prompt or ""),
        )
        caller = self._get_synthesizer_caller(text_type)
        response = caller.generate_answer(user_message, system_prompt=system_prompt)
        app_logger.info(
            "[QuestionAnsweringService] Synthesizer LLM output\n%s",
            format_json_for_log({
                "output": format_llm_output_value(response),
            }),
        )
        return response

    def _get_synthesizer_caller(
        self,
        synthesized_text_type: SynthesizedTextType,
    ) -> QuestionAnsweringLlmClient:
        if synthesized_text_type in self._synthesizer_callers:
            return self._synthesizer_callers[synthesized_text_type]
        if self._synthesizer_caller is not None:
            self._synthesizer_callers[synthesized_text_type] = self._synthesizer_caller
            return self._synthesizer_caller
        provider = self._resolve_synthesizer_provider(synthesized_text_type)
        model = self._resolve_synthesizer_model(synthesized_text_type)
        if self._synthesizer_gateway is None or provider or model:
            self._synthesizer_gateway = LlmGatewayProviderFactory.create_gateway(
                gateway_type=provider,
                model=model,
                system_prompt="",
                max_tokens=200,
            )
        caller = QuestionAnsweringLlmClient(self._synthesizer_gateway)
        self._synthesizer_callers[synthesized_text_type] = caller
        return caller

    @staticmethod
    def _resolve_synthesizer_provider(
        synthesized_text_type: SynthesizedTextType,
    ) -> LlmProvider | None:
        if synthesized_text_type == SynthesizedTextType.EXACT_MATCH:
            value = app_config.get("llm.qa_synthesizer.exact_match_provider")
        else:
            value = app_config.get("llm.qa_synthesizer.detailed_provider")
        if value is None:
            return None
        if isinstance(value, LlmProvider):
            return value
        normalized = str(value).strip().lower().replace("-", "_")
        if not normalized:
            return None
        legacy_map = {
            "claude": LlmProvider.ANTHROPIC,
            "azureopenai": LlmProvider.AZURE_OPENAI,
            "xai": LlmProvider.XAI,
        }
        if normalized in legacy_map:
            return legacy_map[normalized]
        return LlmProvider(normalized)

    @staticmethod
    def _resolve_synthesizer_model(
        synthesized_text_type: SynthesizedTextType,
    ) -> str | None:
        if synthesized_text_type == SynthesizedTextType.EXACT_MATCH:
            value = app_config.get("llm.qa_synthesizer.exact_match_model")
        else:
            value = app_config.get("llm.qa_synthesizer.detailed_model")
        return str(value).strip() if value else None

    @staticmethod
    def _build_fact_sources(
        fact_lookup: dict[str, str],
        source_lookup: dict[str, FactSource],
        user_name: str | None = None,
    ) -> list[dict[str, str]]:
        if not fact_lookup:
            return []
        items: list[dict[str, str]] = []
        seen = set()
        for fact_id, fact_text in fact_lookup.items():
            source = source_lookup.get(fact_id)
            source_value = ""
            source_type = "OTHER"
            if source is not None:
                if hasattr(source, "resolve_value"):
                    source_value = source.resolve_value()
                if not source_value:
                    source_value = str(source)
                source_type = str(getattr(source, "type", "OTHER"))
            else:
                source_type = "user input"
                source_value = (user_name or "").strip()
            fact_text = (fact_text or "").strip()
            source_value = (source_value or "").strip()
            if not fact_text:
                continue
            entry_key = (fact_text, source_value)
            if entry_key in seen:
                continue
            seen.add(entry_key)
            entry = {
                "type": source_type,
                "value": source_value,
                "excerpt": fact_text,
            }
            items.append(entry)
        return items
