# search/services.py

import re
import unicodedata
from difflib import SequenceMatcher
from django.db.models import (
    Q,
    Case,
    When,
    Value,
    IntegerField,
    FloatField,
    F,
)
from django.db.models.functions import Lower

from admin_panel.models import Product, ProductCategory


# ============================================================
# QUERY NORMALIZATION
# ============================================================

def normalize_query(query: str) -> str:
    """
    Normalize the user's search query.

    Examples:
        "  Samsung   TV  " -> "samsung tv"
        "GREEN-CHILI"      -> "green chili"
        "Chilli!!!"        -> "chilli"
    """

    if not query:
        return ""

    query = str(query).strip().lower()

    # Unicode normalization
    query = unicodedata.normalize("NFKC", query)

    # Replace hyphens/underscores with spaces
    query = re.sub(r"[-_]+", " ", query)

    # Remove unnecessary punctuation
    query = re.sub(r"[^\w\s\u0980-\u09FF]", " ", query)

    # Remove duplicate spaces
    query = re.sub(r"\s+", " ", query)

    return query.strip()


# ============================================================
# TOKENIZATION
# ============================================================

def get_query_tokens(query: str):
    """
    Convert query into useful individual tokens.

    Example:
        "Samsung 55 inch TV"
        ->
        ["samsung", "55", "inch", "tv"]
    """

    normalized = normalize_query(query)

    if not normalized:
        return []

    return [
        token
        for token in normalized.split()
        if len(token) >= 2
    ]


# ============================================================
# CATEGORY HIERARCHY
# ============================================================

def get_category_descendants(category):
    """
    Get the supplied category and all its descendants.

    Example:

        Electronics
            ├── Television
            ├── Mobile
            └── Laptop

    If Electronics is supplied, all three levels can be returned.
    """

    category_ids = [category.id]

    children = ProductCategory.objects.filter(
        parent=category,
        is_active=True
    )

    for child in children:
        category_ids.extend(
            get_category_descendants(child)
        )

    return category_ids


def get_category_path(category):
    """
    Return complete category path.

    Example:

        Electronics > Television > Smart TV
    """

    path = []

    current = category

    while current:
        path.insert(0, current.name)
        current = current.parent

    return path


# ============================================================
# CATEGORY SEARCH
# ============================================================

def find_matching_categories(query: str):
    """
    Find categories matching the user's query.

    Searches:
        - category name
        - parent category
    """

    normalized = normalize_query(query)

    if not normalized:
        return ProductCategory.objects.none()

    tokens = get_query_tokens(normalized)

    query_filter = Q(name__icontains=normalized)

    for token in tokens:
        query_filter |= Q(name__icontains=token)

    return (
        ProductCategory.objects
        .filter(
            query_filter,
            is_active=True
        )
        .select_related("parent")
        .distinct()
    )


# ============================================================
# PRODUCT SEARCH QUERY
# ============================================================

def build_product_search_query(query: str):

    normalized = normalize_query(query)

    if not normalized:
        return Q()

    tokens = get_query_tokens(normalized)

    search_query = Q(
        product_name__icontains=normalized
    ) | Q(
        category__name__icontains=normalized
    ) | Q(
        category__parent__name__icontains=normalized
    ) | Q(
        description__icontains=normalized
    )

    # Only add token searches when there is
    # more than one meaningful token.
    if len(tokens) > 1:

        for token in tokens:

            search_query |= (
                Q(product_name__icontains=token)
                |
                Q(category__name__icontains=token)
                |
                Q(category__parent__name__icontains=token)
                |
                Q(description__icontains=token)
            )

    return search_query

# ============================================================
# FUZZY / TYPO SEARCH
# ============================================================

def similarity_score(value: str, query: str) -> float:
    """
    Compare two strings and return similarity between 0 and 1.

    Examples:
        bottle / bottle -> 1.0
        botle / bottle   -> high score
        bottel / bottle  -> high score
    """

    if not value or not query:
        return 0.0

    value = normalize_query(value)
    query = normalize_query(query)

    if not value or not query:
        return 0.0

    if value == query:
        return 1.0

    if query in value:
        return 0.90

    return SequenceMatcher(
        None,
        value,
        query
    ).ratio()


def token_similarity_score(text: str, query: str) -> float:
    """
    Compare individual words.

    Example:

        Product:
            Milton Water Bottle

        Query:
            milton wter botle

    The function compares:

        milton -> milton
        wter   -> water
        botle  -> bottle
    """

    text_tokens = get_query_tokens(text)
    query_tokens = get_query_tokens(query)

    if not text_tokens or not query_tokens:
        return 0.0

    total_score = 0.0
    matched_tokens = 0

    for query_token in query_tokens:

        best_match = 0.0

        for text_token in text_tokens:

            score = similarity_score(
                text_token,
                query_token
            )

            if score > best_match:
                best_match = score

        if best_match >= 0.50:
            matched_tokens += 1
            total_score += best_match

    if matched_tokens == 0:
        return 0.0

    return total_score / len(query_tokens)


def fuzzy_product_search(query: str, limit=50):
    """
    SQLite-compatible fuzzy product search.

    This function loads active products and calculates
    fuzzy similarity in Python.

    No PostgreSQL-specific functionality is used.
    """

    normalized = normalize_query(query)

    if not normalized:
        return []

    products = list(
        Product.objects
        .filter(
            is_active=True,
            is_draft=False,
        )
        .select_related(
            "category",
            "category__parent",
        )
    )

    scored_products = []

    for product in products:

        product_name = product.product_name or ""

        category_name = ""

        if product.category:
            category_name = product.category.name or ""

        parent_category_name = ""

        if (
            product.category
            and product.category.parent
        ):
            parent_category_name = (
                product.category.parent.name or ""
            )

        description = product.description or ""

        # Product name gets highest priority
        product_score = token_similarity_score(
            product_name,
            normalized
        )

        # Category
        category_score = token_similarity_score(
            category_name,
            normalized
        )

        # Parent category
        parent_score = token_similarity_score(
            parent_category_name,
            normalized
        )

        # Description
        description_score = token_similarity_score(
            description,
            normalized
        )

        final_score = max(
            product_score,
            category_score * 0.80,
            parent_score * 0.70,
            description_score * 0.50,
        )

        # Minimum threshold
        if final_score >= 0.50:

            scored_products.append(
                {
                    "product": product,
                    "score": final_score,
                }
            )

    # Highest relevance first
    scored_products.sort(
        key=lambda item: (
            item["score"],
            item["product"].rating or 0,
        ),
        reverse=True,
    )

    return [
        item["product"]
        for item in scored_products[:limit]
    ]
    
# ============================================================
# CATEGORY PRODUCT SEARCH
# ============================================================

def get_products_from_categories(categories):
    """
    Return products belonging to the supplied categories,
    including descendants.
    """

    category_ids = set()

    for category in categories:
        category_ids.update(
            get_category_descendants(category)
        )

    if not category_ids:
        return Product.objects.none()

    return Product.objects.filter(
        category_id__in=category_ids,
        is_active=True,
        is_draft=False,
    )


# ============================================================
# RELEVANCE SCORING
# ============================================================

def apply_relevance_score(products, query):
    """
    Apply a basic relevance score.

    Higher score = more relevant.

    Priority:

        Exact product name
        Product name starts with query
        Product name contains query
        Category exact match
        Category contains query
        Parent category match
        Description match
    """

    normalized = normalize_query(query)

    if not normalized:
        return products.annotate(
            relevance_score=Value(
                0,
                output_field=IntegerField()
            )
        )

    return products.annotate(
        relevance_score=Case(

            # Exact product name
            When(
                product_name__iexact=normalized,
                then=Value(100)
            ),

            # Product name starts with query
            When(
                product_name__istartswith=normalized,
                then=Value(90)
            ),

            # Category exact match
            When(
                category__name__iexact=normalized,
                then=Value(85)
            ),

            # Product name contains query
            When(
                product_name__icontains=normalized,
                then=Value(80)
            ),

            # Category starts with query
            When(
                category__name__istartswith=normalized,
                then=Value(75)
            ),

            # Category contains query
            When(
                category__name__icontains=normalized,
                then=Value(70)
            ),

            # Parent category match
            When(
                category__parent__name__icontains=normalized,
                then=Value(65)
            ),

            # Description match
            When(
                description__icontains=normalized,
                then=Value(40)
            ),

            default=Value(10),

            output_field=IntegerField(),
        )
    )


# ============================================================
# MAIN SEARCH FUNCTION
# ============================================================

# ============================================================
# MAIN SEARCH FUNCTION
# ============================================================

def search_products(query: str, limit=50):
    """
    Main product search function.

    Search order:

        1. Normal database search
        2. Category search
        3. Fuzzy/typo search fallback
        4. Relevance ranking
    """

    original_query = query or ""

    normalized_query = normalize_query(
        original_query
    )

    # --------------------------------------------------------
    # EMPTY QUERY
    # --------------------------------------------------------

    if not normalized_query:
        return {
            "query": original_query,
            "normalized_query": "",
            "intent": "empty",
            "category": None,
            "total": 0,
            "products": Product.objects.none(),
        }

    # --------------------------------------------------------
    # STEP 1: FIND MATCHING CATEGORIES
    # --------------------------------------------------------

    categories = list(
        find_matching_categories(
            normalized_query
        )
    )

    # --------------------------------------------------------
    # STEP 2: BUILD NORMAL PRODUCT QUERY
    # --------------------------------------------------------

    product_query = build_product_search_query(
        normalized_query
    )

    # --------------------------------------------------------
    # STEP 3: CATEGORY IDS
    # --------------------------------------------------------

    category_ids = set()

    for category in categories:

        category_ids.update(
            get_category_descendants(
                category
            )
        )

    # --------------------------------------------------------
    # STEP 4: COMBINE SEARCH CONDITIONS
    # --------------------------------------------------------

    combined_query = product_query

    if category_ids:

        combined_query |= Q(
            category_id__in=category_ids
        )

    # --------------------------------------------------------
    # STEP 5: NORMAL SEARCH
    # --------------------------------------------------------

    products = (
        Product.objects
        .filter(
            combined_query,
            is_active=True,
            is_draft=False,
        )
        .select_related(
            "category",
            "category__parent",
        )
        .distinct()
    )

    # --------------------------------------------------------
    # STEP 6: FUZZY FALLBACK
    # --------------------------------------------------------

    if not products.exists():

        fuzzy_products = fuzzy_product_search(
            normalized_query,
            limit=limit,
        )

        if fuzzy_products:

            return {
                "query": original_query,
                "normalized_query": normalized_query,
                "intent": "fuzzy",
                "category": None,
                "total": len(fuzzy_products),
                "products": fuzzy_products,
            }

    # --------------------------------------------------------
    # STEP 7: DETERMINE INTENT
    # --------------------------------------------------------

    if categories:

        intent = "mixed"
        resolved_category = categories[0]

    else:

        intent = "product"
        resolved_category = None

    # --------------------------------------------------------
    # STEP 8: APPLY RELEVANCE SCORE
    # --------------------------------------------------------

    products = apply_relevance_score(
        products,
        normalized_query
    )

    # --------------------------------------------------------
    # STEP 9: ORDER RESULTS
    # --------------------------------------------------------

    products = products.order_by(
        "-relevance_score",
        "-rating",
        "-created_at",
    )

    # --------------------------------------------------------
    # STEP 10: TOTAL
    # --------------------------------------------------------

    total = products.count()

    # --------------------------------------------------------
    # STEP 11: LIMIT
    # --------------------------------------------------------

    products = products[:limit]

    # --------------------------------------------------------
    # RETURN
    # --------------------------------------------------------

    return {
        "query": original_query,
        "normalized_query": normalized_query,
        "intent": intent,
        "category": resolved_category,
        "total": total,
        "products": products,
    }

# ============================================================
# SEARCH SUGGESTIONS
# ============================================================

def get_search_suggestions(query: str, limit=10):
    """
    Return autocomplete suggestions.

    Suggestions can come from:

        Product names
        Categories
    """

    normalized = normalize_query(query)

    if not normalized:
        return []

    suggestions = []

    # --------------------------------------------------------
    # Product suggestions
    # --------------------------------------------------------

    products = (
        Product.objects
        .filter(
            product_name__icontains=normalized,
            is_active=True,
            is_draft=False,
        )
        .values(
            "id",
            "product_name",
        )
        .order_by("product_name")[:limit]
    )

    for product in products:
        suggestions.append({
            "type": "product",
            "id": product["id"],
            "text": product["product_name"],
        })

    # --------------------------------------------------------
    # Category suggestions
    # --------------------------------------------------------

    categories = (
        ProductCategory.objects
        .filter(
            name__icontains=normalized,
            is_active=True,
        )
        .values(
            "id",
            "name",
        )
        .order_by("name")[:limit]
    )

    for category in categories:
        suggestions.append({
            "type": "category",
            "id": category["id"],
            "text": category["name"],
        })

    return suggestions[:limit]


# ============================================================
# SEARCH QUERY LOGGING
# ============================================================

def log_search_query(
    query: str,
    normalized_query: str,
    result_count: int,
):
    """
    Store search analytics in SearchQuery.

    SearchQuery is currently located in admin_panel.models.
    """

    from admin_panel.models import SearchQuery

    return SearchQuery.objects.create(
        query=query,
        normalized_query=normalized_query,
        result_count=result_count,
    )