"""Cross-reference validation operator."""

import logging
from dataclasses import dataclass
from functools import lru_cache
from typing import Optional

try:
    from sentence_transformers import SentenceTransformer, util
except ImportError:  # pragma: no cover - handled via graceful fallback
    SentenceTransformer = None  # type: ignore
    util = None  # type: ignore

from .semantic_gate import semantic_gate

logger = logging.getLogger(__name__)


@dataclass
class CrossReferenceResult:
    """Result of cross-reference validation."""

    match_score: float  # 0.0-1.0 similarity between descriptions
    is_match: bool  # True if match_score above threshold
    target_description: str
    candidate_description: str
    target_industry: Optional[str] = None  # Detected industry
    candidate_industry: Optional[str] = None  # Detected industry
    mismatch_reason: Optional[str] = None


class CrossReferenceOperator:
    """Cross-reference target and candidate business descriptions."""

    def __init__(
        self,
        match_threshold: float = 0.7,
        embedding_model: str = "all-MiniLM-L6-v2",
        enable_embeddings: bool = True,
    ):
        self.match_threshold = match_threshold
        self.embedding_model_name = embedding_model
        self.enable_embeddings = enable_embeddings and SentenceTransformer is not None

        self._embedding_model: Optional[SentenceTransformer] = None
        if self.enable_embeddings and SentenceTransformer is not None:
            try:
                self._embedding_model = SentenceTransformer(embedding_model)
                logger.info(
                    f"Loaded embedding model '{embedding_model}' for cross-reference."
                )
            except Exception as exc:  # pragma: no cover - defensive
                logger.warning(
                    "Failed to load embedding model '%s' (%s). Falling back to token overlap.",
                    embedding_model,
                    exc,
                )
                self._embedding_model = None
                self.enable_embeddings = False

    def validate(
        self,
        target_company_name: str,
        target_description: str,
        candidate_company_name: str,
        candidate_description: str,
    ) -> CrossReferenceResult:
        """
        Validate that target and candidate descriptions align.

        Args:
            target_company_name: Name from Fortune 1000 list
            target_description: Business description inferred from name
            candidate_company_name: Name from Wikidata/other source
            candidate_description: Business description from website/data

        Returns:
            CrossReferenceResult with match assessment
        """
        # Compute semantic similarity
        match_score = self._semantic_similarity(
            target_description, candidate_description
        )

        # Apply semantic gating with industry conflict detection and company name matching
        gate_result = semantic_gate(
            target_description,
            candidate_description,
            match_score,
            target_name=target_company_name,
            candidate_name=candidate_company_name,
        )

        return CrossReferenceResult(
            match_score=match_score,
            is_match=gate_result.gated_ok,
            target_description=target_description,
            candidate_description=candidate_description,
            target_industry=gate_result.target_industry,
            candidate_industry=gate_result.candidate_industry,
            mismatch_reason=gate_result.reason if not gate_result.gated_ok else None,
        )

    def _semantic_similarity(self, text1: str, text2: str) -> float:
        """
        Calculate semantic similarity between two descriptions.

        Attempts to use embeddings for semantic similarity; falls back to token overlap if
        embeddings are unavailable or inputs are too short.
        """
        if not text1 or not text2:
            return 0.0

        if self.enable_embeddings and self._embedding_model and util:
            try:
                embedding1 = self._get_embedding(text1)
                embedding2 = self._get_embedding(text2)
                similarity = float(util.cos_sim(embedding1, embedding2))
                # Normalize cosine similarity to [0, 1] and clamp.
                normalized = (similarity + 1) / 2
                return max(0.0, min(1.0, normalized))
            except Exception as exc:  # pragma: no cover - defensive fallback
                logger.warning(
                    "Embedding similarity failed (%s). Falling back to token overlap.",
                    exc,
                )

        # Normalize and tokenize
        return self._token_overlap_similarity(text1, text2)

    @lru_cache(maxsize=512)
    def _get_embedding(self, text: str):
        """Get or compute an embedding for the provided text."""
        if not self._embedding_model:
            raise RuntimeError("Embedding model not available.")
        return self._embedding_model.encode(
            text,
            convert_to_tensor=True,
            normalize_embeddings=True,
        )

    def _token_overlap_similarity(self, text1: str, text2: str) -> float:
        """Fallback token-overlap similarity (Jaccard) to retain deterministic behavior."""
        tokens1 = set(text1.lower().split())
        tokens2 = set(text2.lower().split())

        # Remove common stopwords
        stopwords = {
            "the",
            "a",
            "an",
            "and",
            "or",
            "but",
            "in",
            "on",
            "at",
            "to",
            "for",
            "of",
            "with",
            "by",
            "from",
            "is",
            "are",
            "was",
            "were",
        }
        tokens1 -= stopwords
        tokens2 -= stopwords

        if not tokens1 or not tokens2:
            return 0.0

        intersection = len(tokens1 & tokens2)
        union = len(tokens1 | tokens2)

        return intersection / union if union > 0 else 0.0

    def _identify_mismatch(self, target_desc: str, candidate_desc: str) -> str:
        """Identify why descriptions don't match."""
        # Check for industry keywords
        industries = [
            "technology",
            "healthcare",
            "finance",
            "retail",
            "manufacturing",
            "energy",
            "telecommunications",
            "pharmaceutical",
        ]

        target_lower = target_desc.lower()
        candidate_lower = candidate_desc.lower()

        target_industry = None
        candidate_industry = None

        for industry in industries:
            if industry in target_lower:
                target_industry = industry
            if industry in candidate_lower:
                candidate_industry = industry

        if (
            target_industry
            and candidate_industry
            and target_industry != candidate_industry
        ):
            return f"industry_mismatch: {target_industry} vs {candidate_industry}"

        return "low_similarity"
