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 wagtail.models import Page

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


class PageSearchSkill(AiAssistantSkill):
    key = SKILL_PAGE_SEARCH
    label = "Page 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 site or not self._keywords(request_text):
            return SkillMatch(score=0, reason="no_keywords")
        return SkillMatch(score=25, reason="keyword_page_lookup")

    def build_context(
        self, request_text: str, request, site, mode: str, settings_obj
    ) -> SkillContextResult:
        keywords = self._keywords(request_text)
        if not site or not keywords:
            return SkillContextResult()

        results = self._search_pages(
            site=site,
            request=request,
            request_text=request_text,
            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):
            items.append(
                _(
                    "%(index)s. Title: %(title)s\n"
                    "   URL: %(url)s\n"
                    "   Summary: %(summary)s"
                )
                % {
                    "index": index,
                    "title": result["title"],
                    "url": result["url"],
                    "summary": result["summary"],
                }
            )

        instruction = _(
            "Relevant public page context may be provided below. Use it when relevant, mention the page links when useful, and say clearly when the available page context is insufficient."
        )
        return SkillContextResult.from_items(
            items,
            instruction=instruction,
            metadata={"skill": self.key, "result_count": len(results)},
        )

    def _search_pages(
        self, *, site, request, request_text: str, max_items: int
    ) -> list[dict]:
        keywords = self._keywords(request_text)
        if not keywords:
            return []

        queryset = self._base_queryset(site=site, request=request)
        query = Q()
        for keyword in keywords:
            query |= (
                Q(title__icontains=keyword)
                | Q(search_description__icontains=keyword)
                | Q(seo_title__icontains=keyword)
                | Q(url_path__icontains=keyword)
            )

        candidates = list(queryset.filter(query).specific()[:25])
        ranked: list[dict] = []
        for page in candidates:
            score = self._score_page(page, keywords)
            if score <= 0:
                continue
            ranked.append(
                {
                    "score": score,
                    "title": page.title,
                    "url": self._relative_url(page, site),
                    "summary": self._page_summary(page),
                }
            )

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

    def _base_queryset(self, *, site, request):
        queryset = (
            Page.objects.live().public().descendant_of(site.root_page, inclusive=True)
        )
        language_code = (getattr(request, "LANGUAGE_CODE", "") or "").strip()
        if language_code and "locale" in {
            field.name for field in Page._meta.get_fields()
        }:
            queryset = queryset.filter(locale__language_code=language_code)
        return queryset

    def _score_page(self, page, keywords: list[str]) -> int:
        title = (page.title or "").lower()
        seo_title = (getattr(page, "seo_title", "") or "").lower()
        search_description = (getattr(page, "search_description", "") or "").lower()
        summary_text = self._summary_source_text(page).lower()
        score = 0
        for keyword in keywords:
            if keyword in title:
                score += 5
            if keyword in seo_title:
                score += 4
            if keyword in search_description:
                score += 3
            if keyword in summary_text:
                score += 1
        return score

    def _page_summary(self, page) -> str:
        search_description = (getattr(page, "search_description", "") or "").strip()
        if search_description:
            return Truncator(strip_tags(search_description)).chars(220)
        return Truncator(self._summary_source_text(page)).chars(220)

    def _summary_source_text(self, page) -> str:
        specific = getattr(page, "specific", page)
        for field_name in ("body", "intro", "description", "summary"):
            value = getattr(specific, field_name, None)
            text = self._extract_text(value)
            if text:
                return text
        return (page.seo_title or page.title or "").strip()

    def _extract_text(self, value) -> str:
        parts: list[str] = []
        self._append_text_parts(value, parts)
        deduped = OrderedDict()
        for part in parts:
            cleaned = Truncator(strip_tags(str(part).strip())).chars(400)
            if cleaned:
                deduped[cleaned] = None
        return " ".join(deduped.keys())

    def _append_text_parts(self, value, parts: list[str]) -> None:
        if value is None:
            return
        if hasattr(value, "stream_data"):
            self._append_text_parts(value.stream_data, parts)
            return
        if isinstance(value, str):
            cleaned = strip_tags(value).strip()
            if cleaned:
                parts.append(cleaned)
            return
        if isinstance(value, dict):
            for key, nested in value.items():
                if key in {"id", "type"}:
                    continue
                self._append_text_parts(nested, parts)
            return
        if isinstance(value, (list, tuple)):
            for nested in value:
                self._append_text_parts(nested, parts)

    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) < 3:
                continue
            seen[token] = None
            if len(seen) >= 8:
                break
        return list(seen.keys())

    def _relative_url(self, page, site) -> str:
        root_path = (site.root_page.url_path or "/").rstrip("/")
        page_path = page.url_path or "/"
        if root_path and page_path.startswith(f"{root_path}/"):
            relative = page_path[len(root_path) :]
            return relative if relative.startswith("/") else f"/{relative}"
        if page_path.startswith("/"):
            return page_path
        return f"/{page_path}"
