"""
[v0.8.0] Fusion policy for hybrid intent routing.
[AC-AISVC-115~AC-AISVC-117] Fusion decision logic for three-way matching.
"""

import logging
from collections.abc import Callable
from typing import TYPE_CHECKING

from app.services.intent.models import (
    FusionConfig,
    FusionResult,
    LlmJudgeResult,
    RouteTrace,
    RuleMatchResult,
    SemanticMatchResult,
)

if TYPE_CHECKING:
    from app.models.entities import IntentRule

logger = logging.getLogger(__name__)

DecisionCondition = Callable[[RuleMatchResult, SemanticMatchResult, LlmJudgeResult], bool]


class FusionPolicy:
    """
    [AC-AISVC-115] Fusion decision policy for hybrid routing.

    Decision priority:
    1. rule_high_confidence: RuleMatcher hit with score=1.0
    2. llm_judge: LlmJudge triggered and returned valid intent
    3. semantic_override: RuleMatcher missed but SemanticMatcher high confidence
    4. rule_semantic_agree: Both match same intent
    5. semantic_fallback: SemanticMatcher medium confidence
    6. rule_fallback: Only rule matched
    7. no_match: All low confidence
    """

    DECISION_PRIORITY: list[tuple[str, DecisionCondition]] = [
        ("rule_high_confidence", lambda r, s, llm: r.score == 1.0 and r.rule is not None),
        ("llm_judge", lambda r, s, llm: llm.triggered and llm.intent_id is not None),
        (
            "semantic_override",
            lambda r, s, llm: r.score == 0
            and s.top_score > 0.7
            and not s.skipped
            and len(s.candidates) > 0,
        ),
        (
            "rule_semantic_agree",
            lambda r, s, llm: r.score > 0
            and s.top_score > 0.5
            and not s.skipped
            and len(s.candidates) > 0
            and r.rule_id == s.candidates[0].rule.id,
        ),
        (
            "semantic_fallback",
            lambda r, s, llm: s.top_score > 0.5 and not s.skipped and len(s.candidates) > 0,
        ),
        ("rule_fallback", lambda r, s, llm: r.score > 0),
        ("no_match", lambda r, s, llm: True),
    ]

    def __init__(self, config: FusionConfig | None = None):
        """
        Initialize fusion policy with configuration.

        Args:
            config: Fusion configuration, uses default if not provided
        """
        self._config = config or FusionConfig()

    def fuse(
        self,
        rule_result: RuleMatchResult,
        semantic_result: SemanticMatchResult,
        llm_result: LlmJudgeResult | None,
    ) -> FusionResult:
        """
        [AC-AISVC-115] Execute fusion decision.

        Args:
            rule_result: Rule matching result
            semantic_result: Semantic matching result
            llm_result: LLM judge result (may be None)

        Returns:
            FusionResult with final intent, confidence, and trace
        """
        trace = self._build_trace(rule_result, semantic_result, llm_result)

        final_intent = None
        final_confidence = 0.0
        decision_reason = "no_match"

        effective_llm_result = llm_result or LlmJudgeResult.empty()

        for reason, condition in self.DECISION_PRIORITY:
            if condition(rule_result, semantic_result, effective_llm_result):
                decision_reason = reason
                break

        if decision_reason == "rule_high_confidence":
            final_intent = rule_result.rule
            final_confidence = 1.0
        elif decision_reason == "llm_judge" and llm_result:
            final_intent = self._find_rule_by_id(
                llm_result.intent_id, rule_result, semantic_result
            )
            final_confidence = llm_result.score
        elif decision_reason == "semantic_override":
            final_intent = semantic_result.candidates[0].rule
            final_confidence = semantic_result.top_score
        elif decision_reason == "rule_semantic_agree":
            final_intent = rule_result.rule
            final_confidence = self._calculate_weighted_confidence(
                rule_result, semantic_result, llm_result
            )
        elif decision_reason == "semantic_fallback":
            final_intent = semantic_result.candidates[0].rule
            final_confidence = semantic_result.top_score
        elif decision_reason == "rule_fallback":
            final_intent = rule_result.rule
            final_confidence = rule_result.score

        need_clarify = final_confidence < self._config.clarify_threshold
        clarify_candidates = None
        if need_clarify and len(semantic_result.candidates) > 1:
            clarify_candidates = [c.rule for c in semantic_result.candidates[:3]]

        trace.fusion = {
            "weights": {
                "w_rule": self._config.w_rule,
                "w_semantic": self._config.w_semantic,
                "w_llm": self._config.w_llm,
            },
            "final_confidence": final_confidence,
            "decision_reason": decision_reason,
            "need_clarify": need_clarify,
        }

        logger.info(
            f"[AC-AISVC-115] Fusion decision: reason={decision_reason}, "
            f"confidence={final_confidence:.3f}, need_clarify={need_clarify}"
        )

        return FusionResult(
            final_intent=final_intent,
            final_confidence=final_confidence,
            decision_reason=decision_reason,
            need_clarify=need_clarify,
            clarify_candidates=clarify_candidates,
            trace=trace,
        )

    def _build_trace(
        self,
        rule_result: RuleMatchResult,
        semantic_result: SemanticMatchResult,
        llm_result: LlmJudgeResult | None,
    ) -> RouteTrace:
        """
        [AC-AISVC-122] Build route trace log.
        """
        return RouteTrace(
            rule_match={
                "rule_id": str(rule_result.rule_id) if rule_result.rule_id else None,
                "rule_name": rule_result.rule.name if rule_result.rule else None,
                "match_type": rule_result.match_type,
                "matched_text": rule_result.matched_text,
                "score": rule_result.score,
                "duration_ms": rule_result.duration_ms,
            },
            semantic_match={
                "top_candidates": [
                    {
                        "rule_id": str(c.rule.id),
                        "rule_name": c.rule.name,
                        "score": c.score,
                    }
                    for c in semantic_result.candidates
                ],
                "top_score": semantic_result.top_score,
                "duration_ms": semantic_result.duration_ms,
                "skipped": semantic_result.skipped,
                "skip_reason": semantic_result.skip_reason,
            },
            llm_judge={
                "triggered": llm_result.triggered if llm_result else False,
                "intent_id": llm_result.intent_id if llm_result else None,
                "intent_name": llm_result.intent_name if llm_result else None,
                "score": llm_result.score if llm_result else 0.0,
                "reasoning": llm_result.reasoning if llm_result else None,
                "duration_ms": llm_result.duration_ms if llm_result else 0,
                "tokens_used": llm_result.tokens_used if llm_result else 0,
            },
            fusion={},
        )

    def _calculate_weighted_confidence(
        self,
        rule_result: RuleMatchResult,
        semantic_result: SemanticMatchResult,
        llm_result: LlmJudgeResult | None,
    ) -> float:
        """
        [AC-AISVC-116] Calculate weighted confidence.

        Formula:
        final_confidence = (w_rule * rule_score + w_semantic * semantic_score + w_llm * llm_score) / total_weight

        Returns:
            Weighted confidence in [0.0, 1.0]
        """
        rule_score = rule_result.score
        semantic_score = semantic_result.top_score if not semantic_result.skipped else 0.0
        llm_score = llm_result.score if llm_result and llm_result.triggered else 0.0

        total_weight = self._config.w_rule + self._config.w_semantic
        if llm_result and llm_result.triggered:
            total_weight += self._config.w_llm

        if total_weight == 0:
            return 0.0

        confidence = (
            self._config.w_rule * rule_score
            + self._config.w_semantic * semantic_score
            + self._config.w_llm * llm_score
        ) / total_weight

        return min(1.0, max(0.0, confidence))

    def _find_rule_by_id(
        self,
        intent_id: str | None,
        rule_result: RuleMatchResult,
        semantic_result: SemanticMatchResult,
    ) -> "IntentRule | None":
        """Find rule by ID from rule or semantic results."""
        if not intent_id:
            return None

        if rule_result.rule_id and str(rule_result.rule_id) == intent_id:
            return rule_result.rule

        for candidate in semantic_result.candidates:
            if str(candidate.rule.id) == intent_id:
                return candidate.rule

        return None
