"""
Entity Fuzzy Matching Service for ThrillWiki

Provides intelligent entity matching when exact lookups fail, with authentication
prompts for suggesting new entity creation.

Features:
- Levenshtein distance for typo correction
- Phonetic matching using Soundex algorithm
- Partial name matching
- Priority-based scoring (parks > rides > companies)
- Authentication state-aware suggestions
"""

import re
from dataclasses import dataclass
from difflib import SequenceMatcher
from enum import Enum
from typing import Any

from django.db.models import Q

from apps.parks.models import Company, Park
from apps.rides.models import Ride


class EntityType(Enum):
    """Supported entity types for fuzzy matching."""

    PARK = "park"
    RIDE = "ride"
    COMPANY = "company"


@dataclass
class FuzzyMatchResult:
    """Result of a fuzzy matching operation."""

    entity_type: EntityType
    entity: Any  # The actual model instance
    name: str
    slug: str
    score: float  # 0.0 to 1.0, higher is better match
    match_reason: str  # Description of why this was matched
    confidence: str  # 'high', 'medium', 'low'
    url: str | None = None

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for API responses."""
        return {
            "entity_type": self.entity_type.value,
            "name": self.name,
            "slug": self.slug,
            "score": round(self.score, 3),
            "match_reason": self.match_reason,
            "confidence": self.confidence,
            "url": self.url,
            "entity_id": getattr(self.entity, "id", None),
        }


@dataclass
class EntitySuggestion:
    """Suggestion for creating a new entity when no matches found."""

    suggested_name: str
    entity_type: EntityType
    requires_authentication: bool
    login_prompt: str
    signup_prompt: str
    creation_hint: str


class FuzzyMatchingAlgorithms:
    """Collection of fuzzy matching algorithms."""

    @staticmethod
    def levenshtein_distance(s1: str, s2: str) -> int:
        """Calculate Levenshtein distance between two strings."""
        if len(s1) < len(s2):
            return FuzzyMatchingAlgorithms.levenshtein_distance(s2, s1)

        if len(s2) == 0:
            return len(s1)

        previous_row = list(range(len(s2) + 1))
        for i, c1 in enumerate(s1):
            current_row = [i + 1]
            for j, c2 in enumerate(s2):
                insertions = previous_row[j + 1] + 1
                deletions = current_row[j] + 1
                substitutions = previous_row[j] + (c1 != c2)
                current_row.append(min(insertions, deletions, substitutions))
            previous_row = current_row

        return previous_row[-1]

    @staticmethod
    def similarity_ratio(s1: str, s2: str) -> float:
        """Calculate similarity ratio (0.0 to 1.0) using SequenceMatcher."""
        return SequenceMatcher(None, s1.lower(), s2.lower()).ratio()

    @staticmethod
    def soundex(name: str) -> str:
        """Generate Soundex code for phonetic matching."""
        name = re.sub(r"[^A-Za-z]", "", name.upper())
        if not name:
            return "0000"

        # Soundex algorithm
        soundex_map = {
            "BFPV": "1",
            "CGJKQSXZ": "2",
            "DT": "3",
            "L": "4",
            "MN": "5",
            "R": "6",
        }

        first_letter = name[0]
        name = name[1:]

        # Replace letters with numbers
        for letters, number in soundex_map.items():
            name = re.sub(f"[{letters}]", number, name)

        # Remove consecutive duplicates
        name = re.sub(r"(\d)\1+", r"\1", name)

        # Remove zeros
        name = re.sub("0", "", name)

        # Pad or truncate to 4 characters
        soundex_code = (first_letter + name + "000")[:4]
        return soundex_code

    @staticmethod
    def partial_match_score(query: str, target: str) -> float:
        """Calculate partial matching score for substring matches."""
        query_lower = query.lower()
        target_lower = target.lower()

        # Exact match
        if query_lower == target_lower:
            return 1.0

        # Starts with
        if target_lower.startswith(query_lower):
            return 0.8 + (len(query) / len(target)) * 0.15

        # Contains
        if query_lower in target_lower:
            return 0.6 + (len(query) / len(target)) * 0.2

        # Words match
        query_words = set(query_lower.split())
        target_words = set(target_lower.split())
        if query_words & target_words:
            intersection = len(query_words & target_words)
            union = len(query_words | target_words)
            return 0.4 + (intersection / union) * 0.3

        return 0.0


class EntityFuzzyMatcher:
    """Main fuzzy matching service for entities."""

    # Matching thresholds
    HIGH_CONFIDENCE_THRESHOLD = 0.8
    MEDIUM_CONFIDENCE_THRESHOLD = 0.6
    LOW_CONFIDENCE_THRESHOLD = 0.4

    # Maximum results to consider
    MAX_CANDIDATES = 50
    MAX_RESULTS = 5

    def __init__(self):
        self.algorithms = FuzzyMatchingAlgorithms()

    def find_entity(
        self, query: str, entity_types: list[EntityType] | None = None, user=None
    ) -> tuple[list[FuzzyMatchResult], EntitySuggestion | None]:
        """
        Find entities matching the query with fuzzy matching.

        Args:
            query: Search query string
            entity_types: Limit search to specific entity types
            user: Current user for authentication context

        Returns:
            Tuple of (matches, suggestion_for_new_entity)
        """
        if not query or len(query.strip()) < 2:
            return [], None

        query = query.strip()
        entity_types = entity_types or [
            EntityType.PARK,
            EntityType.RIDE,
            EntityType.COMPANY,
        ]

        # Collect all potential matches
        candidates = []

        for entity_type in entity_types:
            candidates.extend(self._get_candidates(query, entity_type))

        # Score and rank candidates
        matches = self._score_and_rank_candidates(query, candidates)

        # Generate suggestion if no good matches found
        suggestion = None
        if not matches or matches[0].score < self.LOW_CONFIDENCE_THRESHOLD:
            suggestion = self._generate_entity_suggestion(query, entity_types, user)

        return matches[: self.MAX_RESULTS], suggestion

    def _get_candidates(self, query: str, entity_type: EntityType) -> list[dict[str, Any]]:
        """Get potential matching candidates for an entity type."""
        candidates = []

        if entity_type == EntityType.PARK:
            parks = Park.objects.filter(Q(name__icontains=query) | Q(slug__icontains=query.lower().replace(" ", "-")))[
                : self.MAX_CANDIDATES
            ]

            for park in parks:
                candidates.append(
                    {
                        "entity_type": EntityType.PARK,
                        "entity": park,
                        "name": park.name,
                        "slug": park.slug,
                        "search_names": [park.name],
                        "url": getattr(park, "get_absolute_url", lambda: None)(),
                        "priority_boost": 0.1,  # Parks get priority
                    }
                )

        elif entity_type == EntityType.RIDE:
            rides = Ride.objects.select_related("park").filter(
                Q(name__icontains=query)
                | Q(slug__icontains=query.lower().replace(" ", "-"))
                | Q(park__name__icontains=query)
            )[: self.MAX_CANDIDATES]

            for ride in rides:
                candidates.append(
                    {
                        "entity_type": EntityType.RIDE,
                        "entity": ride,
                        "name": ride.name,
                        "slug": ride.slug,
                        "search_names": [ride.name, f"{ride.park.name} {ride.name}"],
                        "url": getattr(ride, "get_absolute_url", lambda: None)(),
                        "priority_boost": 0.05,  # Rides get some priority
                    }
                )

        elif entity_type == EntityType.COMPANY:
            companies = Company.objects.filter(
                Q(name__icontains=query) | Q(slug__icontains=query.lower().replace(" ", "-"))
            )[: self.MAX_CANDIDATES]

            for company in companies:
                candidates.append(
                    {
                        "entity_type": EntityType.COMPANY,
                        "entity": company,
                        "name": company.name,
                        "slug": company.slug,
                        "search_names": [company.name],
                        "url": getattr(company, "get_absolute_url", lambda: None)(),
                        "priority_boost": 0.0,  # Companies get no priority boost
                    }
                )

        return candidates

    def _score_and_rank_candidates(self, query: str, candidates: list[dict[str, Any]]) -> list[FuzzyMatchResult]:
        """Score and rank all candidates using multiple algorithms."""
        scored_matches = []

        for candidate in candidates:
            best_score = 0.0
            best_reason = ""

            # Test against all search names for this candidate
            for search_name in candidate["search_names"]:
                # Algorithm 1: Sequence similarity
                similarity_score = self.algorithms.similarity_ratio(query, search_name)
                if similarity_score > best_score:
                    best_score = similarity_score
                    best_reason = f"Text similarity with '{search_name}'"

                # Algorithm 2: Partial matching
                partial_score = self.algorithms.partial_match_score(query, search_name)
                if partial_score > best_score:
                    best_score = partial_score
                    best_reason = f"Partial match with '{search_name}'"

                # Algorithm 3: Levenshtein distance
                if len(query) > 3 and len(search_name) > 3:
                    max_len = max(len(query), len(search_name))
                    distance = self.algorithms.levenshtein_distance(query, search_name)
                    lev_score = 1.0 - (distance / max_len)
                    if lev_score > best_score:
                        best_score = lev_score
                        best_reason = f"Similar spelling to '{search_name}'"

                # Algorithm 4: Soundex phonetic matching
                if len(query) > 2 and len(search_name) > 2:
                    query_soundex = self.algorithms.soundex(query)
                    name_soundex = self.algorithms.soundex(search_name)
                    if query_soundex == name_soundex and best_score < 0.7:
                        best_score = max(best_score, 0.7)
                        best_reason = f"Sounds like '{search_name}'"

            # Apply priority boost
            best_score += candidate["priority_boost"]
            best_score = min(1.0, best_score)  # Cap at 1.0

            # Determine confidence level
            if best_score >= self.HIGH_CONFIDENCE_THRESHOLD:
                confidence = "high"
            elif best_score >= self.MEDIUM_CONFIDENCE_THRESHOLD:
                confidence = "medium"
            else:
                confidence = "low"

            # Only include if above minimum threshold
            if best_score >= self.LOW_CONFIDENCE_THRESHOLD:
                match = FuzzyMatchResult(
                    entity_type=candidate["entity_type"],
                    entity=candidate["entity"],
                    name=candidate["name"],
                    slug=candidate["slug"],
                    score=best_score,
                    match_reason=best_reason,
                    confidence=confidence,
                    url=candidate["url"],
                )
                scored_matches.append(match)

        # Sort by score (highest first) and return
        return sorted(scored_matches, key=lambda x: x.score, reverse=True)

    def _generate_entity_suggestion(self, query: str, entity_types: list[EntityType], user) -> EntitySuggestion:
        """Generate suggestion for creating new entity when no matches found."""

        # Determine most likely entity type based on query characteristics
        suggested_type = EntityType.PARK  # Default to park

        # Simple heuristics for entity type detection
        query_lower = query.lower()
        if any(word in query_lower for word in ["roller coaster", "ride", "coaster", "attraction"]):
            suggested_type = EntityType.RIDE
        elif any(word in query_lower for word in ["inc", "corp", "company", "manufacturer"]):
            suggested_type = EntityType.COMPANY
        elif EntityType.PARK in entity_types:
            suggested_type = EntityType.PARK
        elif entity_types:
            suggested_type = entity_types[0]

        # Clean up the suggested name
        suggested_name = " ".join(word.capitalize() for word in query.split())

        # Check if user is authenticated
        is_authenticated = user and hasattr(user, "is_authenticated") and user.is_authenticated

        # Generate appropriate prompts
        entity_name = suggested_type.value
        login_prompt = f"Log in to suggest adding '{suggested_name}' as a new {entity_name}"
        signup_prompt = f"Sign up to contribute and add '{suggested_name}' to ThrillWiki"
        creation_hint = f"Help expand ThrillWiki by adding information about '{suggested_name}'"

        return EntitySuggestion(
            suggested_name=suggested_name,
            entity_type=suggested_type,
            requires_authentication=not is_authenticated,
            login_prompt=login_prompt,
            signup_prompt=signup_prompt,
            creation_hint=creation_hint,
        )


# Global service instance
entity_fuzzy_matcher = EntityFuzzyMatcher()
