"""
Semantic matcher for intent recognition.
[AC-AISVC-113, AC-AISVC-114] Vector-based semantic matching.
"""

import asyncio
import logging
import time
from typing import TYPE_CHECKING

import numpy as np

from app.services.intent.models import (
    FusionConfig,
    SemanticCandidate,
    SemanticMatchResult,
)

if TYPE_CHECKING:
    from app.models.entities import IntentRule
    from app.services.embedding.base import EmbeddingProvider

logger = logging.getLogger(__name__)


class SemanticMatcher:
    """
    [AC-AISVC-113] Semantic matcher using vector similarity.

    Supports two matching modes:
    - Mode A: Use pre-computed intent_vector for direct similarity calculation
    - Mode B: Use semantic_examples for dynamic vector computation
    """

    def __init__(
        self,
        embedding_provider: "EmbeddingProvider",
        config: FusionConfig,
    ):
        """
        Initialize semantic matcher.

        Args:
            embedding_provider: Provider for generating embeddings
            config: Fusion configuration
        """
        self._embedding_provider = embedding_provider
        self._config = config

    async def match(
        self,
        message: str,
        rules: list["IntentRule"],
        tenant_id: str,
        top_k: int | None = None,
    ) -> SemanticMatchResult:
        """
        [AC-AISVC-113] Perform vector semantic matching.

        Args:
            message: User message
            rules: List of intent rules
            tenant_id: Tenant ID for isolation
            top_k: Number of top candidates to return

        Returns:
            SemanticMatchResult with candidates and scores
        """
        start_time = time.time()
        effective_top_k = top_k or self._config.semantic_top_k

        if not self._config.semantic_matcher_enabled:
            return SemanticMatchResult(
                candidates=[],
                top_score=0.0,
                duration_ms=0,
                skipped=True,
                skip_reason="disabled",
            )

        rules_with_semantic = [r for r in rules if self._has_semantic_config(r)]
        if not rules_with_semantic:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.debug(
                f"[AC-AISVC-113] No rules with semantic config for tenant={tenant_id}"
            )
            return SemanticMatchResult(
                candidates=[],
                top_score=0.0,
                duration_ms=duration_ms,
                skipped=True,
                skip_reason="no_semantic_config",
            )

        try:
            message_vector = await asyncio.wait_for(
                self._embedding_provider.embed(message),
                timeout=self._config.semantic_matcher_timeout_ms / 1000,
            )
        except asyncio.TimeoutError:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.warning(
                f"[AC-AISVC-113] Embedding timeout for tenant={tenant_id}, "
                f"timeout={self._config.semantic_matcher_timeout_ms}ms"
            )
            return SemanticMatchResult(
                candidates=[],
                top_score=0.0,
                duration_ms=duration_ms,
                skipped=True,
                skip_reason="embedding_timeout",
            )
        except Exception as e:
            duration_ms = int((time.time() - start_time) * 1000)
            logger.error(
                f"[AC-AISVC-113] Embedding error for tenant={tenant_id}: {e}"
            )
            return SemanticMatchResult(
                candidates=[],
                top_score=0.0,
                duration_ms=duration_ms,
                skipped=True,
                skip_reason=f"embedding_error: {str(e)}",
            )

        candidates = []
        for rule in rules_with_semantic:
            try:
                score = await self._calculate_similarity(message_vector, rule)
                if score > 0:
                    candidates.append(SemanticCandidate(rule=rule, score=score))
            except Exception as e:
                logger.warning(
                    f"[AC-AISVC-114] Similarity calculation failed for rule={rule.id}: {e}"
                )
                continue

        candidates.sort(key=lambda x: x.score, reverse=True)
        candidates = candidates[:effective_top_k]

        duration_ms = int((time.time() - start_time) * 1000)
        logger.info(
            f"[AC-AISVC-113] Semantic match completed for tenant={tenant_id}, "
            f"candidates={len(candidates)}, top_score={candidates[0].score if candidates else 0:.3f}, "
            f"duration={duration_ms}ms"
        )

        return SemanticMatchResult(
            candidates=candidates,
            top_score=candidates[0].score if candidates else 0.0,
            duration_ms=duration_ms,
            skipped=False,
            skip_reason=None,
        )

    def _has_semantic_config(self, rule: "IntentRule") -> bool:
        """
        Check if rule has semantic configuration.

        Args:
            rule: Intent rule to check

        Returns:
            True if rule has intent_vector or semantic_examples
        """
        return bool(rule.intent_vector) or bool(rule.semantic_examples)

    async def _calculate_similarity(
        self,
        message_vector: list[float],
        rule: "IntentRule",
    ) -> float:
        """
        [AC-AISVC-114] Calculate similarity between message and rule.

        Mode A: Use pre-computed intent_vector
        Mode B: Use semantic_examples for dynamic computation

        Args:
            message_vector: Message embedding vector
            rule: Intent rule with semantic config

        Returns:
            Similarity score (0.0 ~ 1.0)
        """
        if rule.intent_vector:
            return self._cosine_similarity(message_vector, rule.intent_vector)
        elif rule.semantic_examples:
            try:
                example_vectors = await self._embedding_provider.embed_batch(
                    rule.semantic_examples
                )
                similarities = [
                    self._cosine_similarity(message_vector, v)
                    for v in example_vectors
                ]
                return max(similarities) if similarities else 0.0
            except Exception as e:
                logger.warning(
                    f"[AC-AISVC-114] Failed to compute example vectors for rule={rule.id}: {e}"
                )
                return 0.0
        return 0.0

    def _cosine_similarity(
        self,
        v1: list[float],
        v2: list[float],
    ) -> float:
        """
        Calculate cosine similarity between two vectors.

        Args:
            v1: First vector
            v2: Second vector

        Returns:
            Cosine similarity (0.0 ~ 1.0)
        """
        if not v1 or not v2:
            return 0.0

        v1_arr = np.array(v1)
        v2_arr = np.array(v2)

        norm1 = np.linalg.norm(v1_arr)
        norm2 = np.linalg.norm(v2_arr)

        if norm1 == 0 or norm2 == 0:
            return 0.0

        similarity = float(np.dot(v1_arr, v2_arr) / (norm1 * norm2))
        return max(0.0, min(1.0, similarity))
