from __future__ import annotations

import re
from collections import OrderedDict

from django.db.models import Q
from django.utils.html import strip_tags
from django.utils.text import Truncator
from django.utils.translation import gettext as _

from ..constants import MODE_ADMIN, MODE_CUSTOMER, SKILL_PRODUCT_SEARCH
from .base import AiAssistantSkill, SkillContextResult, SkillMatch


class ProductSearchSkill(AiAssistantSkill):
    key = SKILL_PRODUCT_SEARCH
    label = "Product search"
    allowed_modes = (MODE_CUSTOMER, MODE_ADMIN)
    public_safe = True

    def is_enabled(self, site, settings_obj, mode: str) -> bool:
        return self.key in settings_obj.enabled_skill_keys(mode)

    def match(
        self, request_text: str, request, site, mode: str, settings_obj
    ) -> SkillMatch:
        if not self._keywords(request_text):
            return SkillMatch(score=0, reason="no_keywords")
        if self._load_product_model() is None:
            return SkillMatch(score=0, reason="catalogue_unavailable")
        return SkillMatch(score=30, reason="keyword_product_lookup")

    def build_context(
        self, request_text: str, request, site, mode: str, settings_obj
    ) -> SkillContextResult:
        results = self._search_products(
            request_text=request_text,
            request=request,
            max_items=max(int(settings_obj.max_skill_context_items or 1), 1),
        )
        if not results:
            return SkillContextResult()

        items = []
        for index, result in enumerate(results, start=1):
            lines = [
                _("%(index)s. Name: %(name)s")
                % {"index": index, "name": result["name"]},
                _("   URL: %(url)s") % {"url": result["url"]},
            ]
            if result["category"]:
                lines.append(_("   Category: %(category)s") % result)
            if result["description"]:
                lines.append(_("   Description: %(description)s") % result)
            items.append("\n".join(lines))

        instruction = _(
            "Relevant public product context may be provided below. Use it when relevant, recommend only products from that context, mention links when useful, and say clearly when no matching product context is available. Do not invent prices, availability, delivery times, discounts, basket actions, or checkout actions."
        )
        return SkillContextResult.from_items(
            items,
            instruction=instruction,
            metadata={"skill": self.key, "result_count": len(results)},
        )

    def _search_products(
        self, *, request_text: str, request=None, max_items: int
    ) -> list[dict]:
        product_model = self._load_product_model()
        if product_model is None:
            return []

        keywords = self._keywords(request_text)
        if not keywords:
            return []

        queryset = self._base_queryset(product_model=product_model, request=request)
        if queryset is None:
            return []

        query = Q()
        searchable_fields = ("title", "slug", "description", "long_description", "upc")
        model_field_names = {field.name for field in product_model._meta.get_fields()}
        for keyword in keywords:
            for field_name in searchable_fields:
                if field_name in model_field_names:
                    query |= Q(**{f"{field_name}__icontains": keyword})
            if "categories" in model_field_names:
                query |= Q(categories__name__icontains=keyword)

        candidates = list(queryset.filter(query).distinct()[:50])
        ranked: list[dict] = []
        for product in candidates:
            score = self._score_product(product, keywords)
            if score <= 0:
                continue
            ranked.append(
                {
                    "product": product,
                    "score": score,
                    "name": self._title(product),
                    "url": self._product_url(product),
                    "category": self._category_name(product),
                    "description": self._product_summary(product),
                }
            )

        ranked.sort(key=lambda item: (-item["score"], item["name"].lower()))
        return ranked[:max_items]

    def _base_queryset(self, *, product_model, request=None):
        manager = getattr(product_model, "objects", None)
        if manager is None:
            return None

        queryset = manager.all()
        browsable = getattr(queryset, "browsable", None)
        if callable(browsable):
            queryset = browsable()

        model_field_names = {field.name for field in product_model._meta.get_fields()}
        if "is_public" in model_field_names:
            queryset = queryset.filter(is_public=True)
        if "parent" in model_field_names:
            queryset = queryset.filter(parent__isnull=True)

        queryset = self._filter_current_site(queryset, request)
        return queryset

    def _filter_current_site(self, queryset, request):
        if request is None:
            return queryset
        site_category = self._get_site_category(request)
        if site_category is None:
            return queryset

        paths = list(site_category.categories.values_list("path", flat=True))
        if not paths:
            return queryset

        query = Q()
        for path in paths:
            query |= Q(categories__path__startswith=path)
        return queryset.filter(query)

    def _get_site_category(self, request):
        try:
            from ocyan.plugin.oscar_multi_site.utils import get_site_category
        except Exception:
            return None

        try:
            return get_site_category(request)
        except Exception:
            return None

    def _load_product_model(self):
        try:
            from oscar.core.loading import get_model

            return get_model("catalogue", "Product")
        except Exception:
            return None

    def _score_product(self, product, keywords: list[str]) -> int:
        haystacks = {
            "title": (self._title(product) or "").lower(),
            "slug": (getattr(product, "slug", "") or "").lower(),
            "description": (self._summary_source_text(product) or "").lower(),
            "category": (self._category_name(product) or "").lower(),
            "upc": (getattr(product, "upc", "") or "").lower(),
        }
        score = 0
        for keyword in keywords:
            if keyword in haystacks["title"]:
                score += 5
            if keyword in haystacks["slug"]:
                score += 4
            if keyword in haystacks["category"]:
                score += 3
            if keyword in haystacks["description"]:
                score += 2
            if haystacks["upc"] and keyword == haystacks["upc"]:
                score += 2
        return score

    def _title(self, product) -> str:
        return (
            getattr(product, "title", "") or getattr(product, "name", "") or ""
        ).strip()

    def _product_url(self, product) -> str:
        try:
            url = product.get_absolute_url()
        except Exception:
            url = ""
        url = (url or "").strip()
        if url.startswith("/"):
            return url
        if url:
            return f"/{url}"
        slug = (getattr(product, "slug", "") or "").strip("/")
        return f"/{slug}/" if slug else "/"

    def _category_name(self, product) -> str:
        try:
            category = product.primary_category()
        except Exception:
            category = None
        return (getattr(category, "name", "") or "").strip()

    def _product_summary(self, product) -> str:
        return Truncator(self._summary_source_text(product)).chars(220)

    def _product_attributes(self, product) -> list[str]:
        attributes = []
        try:
            attribute_values = getattr(product, "attribute_values", None)
            all_values = attribute_values.all() if attribute_values else []
        except Exception:
            all_values = []

        for value in all_values:
            attribute = getattr(value, "attribute", None)
            name = (getattr(attribute, "name", "") or "").strip()
            rendered_value = (
                getattr(value, "value_text", None)
                or getattr(value, "value", None)
                or getattr(value, "value_option", None)
                or getattr(value, "value_boolean", None)
                or getattr(value, "value_integer", None)
                or getattr(value, "value_float", None)
            )
            rendered_value = str(rendered_value or "").strip()
            if not name or not rendered_value:
                continue
            attributes.append(f"{name}: {rendered_value}")
            if len(attributes) >= 5:
                break
        return attributes

    def _parent_child_summary(self, product) -> str:
        parent = getattr(product, "parent", None)
        if parent is not None:
            parent_title = self._title(parent)
            if parent_title:
                return f"Variant of {parent_title}"

        try:
            children = getattr(product, "children", None)
            child_count = children.count() if children is not None else 0
        except Exception:
            child_count = 0
        if child_count:
            return f"Has {child_count} public variants"
        return ""

    def _summary_source_text(self, product) -> str:
        for field_name in ("description", "long_description"):
            value = getattr(product, field_name, None)
            text = Truncator(strip_tags(str(value or "").strip())).chars(500)
            if text:
                return text
        return self._title(product)

    def _keywords(self, request_text: str) -> list[str]:
        seen = OrderedDict()
        for token in re.findall(r"[A-Za-z0-9]+", (request_text or "").lower()):
            if len(token) < 2:
                continue
            seen[token] = None
            if len(seen) >= 8:
                break
        return list(seen.keys())
