from __future__ import annotations

import re
from collections import OrderedDict

from django.db.models import Q
from django.utils.timezone import localtime
from django.utils.translation import gettext as _
from wagtail.models import Page

from ..constants import MODE_ADMIN, SKILL_SCOPE_ADMIN, SKILL_SEO_ANALYSIS
from ..page_resolver import resolve_page_target
from .base import AiAssistantSkill, SkillContextResult, SkillMatch


class SeoAnalysisSkill(AiAssistantSkill):
    key = SKILL_SEO_ANALYSIS
    label = "SEO analysis"
    allowed_modes = (MODE_ADMIN,)
    public_safe = False
    skill_scope = SKILL_SCOPE_ADMIN

    _INTENT_PATTERNS = (
        r"\bseo\b",
        r"\bmetadata\b",
        r"\bmeta data\b",
        r"\bsearch description\b",
        r"\bseo title\b",
        r"\bmissing\b",
        r"\bweak\b",
        r"\bhygiene\b",
        r"\bduplicate\b",
        r"\bimprovement\b",
        r"\bneeds seo\b",
        r"\bpages? need seo\b",
    )
    _SUGGESTION_PATTERNS = (
        r"\bsuggest\b",
        r"\brewrite\b",
        r"\bimprove\b",
        r"\bwhat should i write\b",
        r"\bmeta description\b",
        r"\bmetadata suggestion\b",
        r"\bseo suggestion\b",
    )

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

    def match(
        self, request_text: str, request, site, mode: str, settings_obj
    ) -> SkillMatch:
        if mode != MODE_ADMIN or not site:
            return SkillMatch(score=0, reason="admin_only")
        text = (request_text or "").lower()
        if self._is_suggestion_intent(text):
            return SkillMatch(score=55, reason="seo_suggestion_intent")
        if any(re.search(pattern, text) for pattern in self._INTENT_PATTERNS):
            return SkillMatch(score=45, reason="seo_analysis_intent")
        return SkillMatch(score=0, reason="no_seo_analysis_intent")

    def build_context(
        self, request_text: str, request, site, mode: str, settings_obj
    ) -> SkillContextResult:
        if mode != MODE_ADMIN or not site:
            return SkillContextResult()
        suggestion_mode = self._is_suggestion_intent(request_text)

        resolved = resolve_page_target(request_text, site, request=request)
        if resolved is not None:
            return self._targeted_context(
                request_text=request_text,
                site=site,
                resolved=resolved,
                suggestion_mode=suggestion_mode,
            )

        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()

        grouped = OrderedDict(
            [
                ("missing_search_description", []),
                ("missing_seo_title", []),
                ("weak_metadata", []),
            ]
        )
        for item in results:
            grouped[item["issue_type"]].append(item)

        sections: list[str] = []
        labels = {
            "missing_search_description": _("Pages missing search descriptions:"),
            "missing_seo_title": _("Pages missing SEO titles:"),
            "weak_metadata": _("Pages with weak metadata:"),
        }
        for issue_type, items in grouped.items():
            if not items:
                continue
            lines = [labels[issue_type]]
            for index, item in enumerate(items, start=1):
                if suggestion_mode:
                    lines.extend(
                        [
                            _("%(index)s. Title: %(title)s")
                            % {"index": index, "title": item["title"]},
                            _("   Status: %(status)s") % {"status": item["status"]},
                            _("   Path: %(path)s") % {"path": item["path"]},
                            _("   Type: %(page_type)s")
                            % {"page_type": item["page_type"]},
                            _("   Current SEO title: %(seo_title_value)s")
                            % {"seo_title_value": item["seo_title_value"]},
                            _(
                                "   Current search description: %(search_description_value)s"
                            )
                            % {
                                "search_description_value": item[
                                    "search_description_value"
                                ]
                            },
                            _("   Last updated: %(updated)s")
                            % {"updated": item["updated"]},
                        ]
                    )
                    continue
                lines.extend(
                    [
                        _("%(index)s. Title: %(title)s")
                        % {"index": index, "title": item["title"]},
                        _("   Status: %(status)s") % {"status": item["status"]},
                        _("   Path: %(path)s") % {"path": item["path"]},
                        _("   Type: %(page_type)s") % {"page_type": item["page_type"]},
                        _("   SEO title: %(seo_title)s")
                        % {"seo_title": item["seo_title"]},
                        _("   Search description: %(search_description)s")
                        % {"search_description": item["search_description"]},
                        _("   Last updated: %(updated)s")
                        % {"updated": item["updated"]},
                    ]
                )
            sections.append("\n".join(lines))

        if not sections:
            return SkillContextResult()

        instruction = self._instruction(suggestion_mode=suggestion_mode)
        return SkillContextResult.from_items(
            sections,
            instruction=instruction,
            metadata={
                "skill": self.key,
                "result_count": len(results),
                "suggestion_mode": suggestion_mode,
            },
        )

    def _targeted_context(
        self, *, request_text: str, site, resolved, suggestion_mode: bool
    ) -> SkillContextResult:
        metadata = self._page_metadata(resolved.page, site)
        if metadata is None:
            return SkillContextResult()

        if suggestion_mode:
            title = _("Target page SEO suggestion context:")
        else:
            title = _("Target page SEO analysis:")
        lines = [
            title,
            _("1. Title: %(title)s") % {"title": metadata["title"]},
            _("   Status: %(status)s") % {"status": metadata["status"]},
            _("   Path: %(path)s") % {"path": metadata["path"]},
            _("   Type: %(page_type)s") % {"page_type": metadata["page_type"]},
            _("   Current SEO title: %(seo_title_value)s")
            % {"seo_title_value": metadata["seo_title_value"]},
            _("   Current search description: %(search_description_value)s")
            % {"search_description_value": metadata["search_description_value"]},
            _("   Last updated: %(updated)s") % {"updated": metadata["updated"]},
        ]
        instruction = self._instruction(suggestion_mode=suggestion_mode)
        return SkillContextResult.from_items(
            ["\n".join(lines)],
            instruction=instruction,
            metadata={
                "skill": self.key,
                "result_count": 1,
                "suggestion_mode": suggestion_mode,
                "targeted_page_id": resolved.page.id,
                "resolution_reason": resolved.reason,
            },
        )

    def _instruction(self, *, suggestion_mode: bool) -> str:
        if suggestion_mode:
            return _(
                "SEO suggestion guidance may be provided below. Use it to help staff draft metadata suggestions for manual review only. Generate suggestions only. Do not claim changes were saved, published, or applied. Provide concise draft SEO titles and meta descriptions. Keep SEO titles around 50-60 characters when practical. Keep meta descriptions around 140-160 characters when practical. Label all output as suggestions for manual review and tell staff to apply changes manually in Wagtail."
            )
        return _(
            "SEO analysis context may be provided below. Use it to help staff understand metadata hygiene for the current site. This context is read-only. Do not claim to edit pages or apply SEO changes. If you offer improvement ideas, label them as suggestions only and direct staff to update the page manually in Wagtail."
        )

    def _search_pages(
        self, *, site, request, request_text: str, max_items: int
    ) -> list[dict]:
        queryset = self._base_queryset(site=site, request=request)
        if queryset is None:
            return []

        pages = self._select_pages(queryset, request_text)[: max_items * 4]
        ranked = []
        for page in pages:
            metadata = self._page_metadata(page, site)
            if metadata is None:
                continue
            score = self._score_page(page, request_text)
            ranked.append((score, metadata))

        ranked.sort(
            key=lambda item: (
                -item[0],
                item[1]["title"].lower(),
                item[1]["path"].lower(),
            )
        )
        return [metadata for _, metadata in ranked[:max_items]]

    def _base_queryset(self, *, site, request):
        queryset = Page.objects.descendant_of(site.root_page, inclusive=True).specific()
        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 _select_pages(self, queryset, request_text: str):
        request_text_lower = (request_text or "").lower()
        if "missing seo title" in request_text_lower or (
            "seo title" in request_text_lower and "missing" in request_text_lower
        ):
            return list(
                queryset.filter(Q(seo_title__isnull=True) | Q(seo_title__exact=""))
            )

        if "missing search description" in request_text_lower or (
            "search description" in request_text_lower
            and "missing" in request_text_lower
        ):
            return list(
                queryset.filter(
                    Q(search_description__isnull=True) | Q(search_description__exact="")
                )
            )

        if "live" in request_text_lower and "missing" in request_text_lower:
            return list(
                queryset.filter(live=True).filter(
                    Q(search_description__isnull=True)
                    | Q(search_description__exact="")
                    | Q(seo_title__isnull=True)
                    | Q(seo_title__exact="")
                )
            )

        return list(
            queryset.filter(
                Q(search_description__isnull=True)
                | Q(search_description__exact="")
                | Q(seo_title__isnull=True)
                | Q(seo_title__exact="")
            )
        )

    def _score_page(self, page, request_text: str) -> int:
        request_text_lower = (request_text or "").lower()
        score = 0
        search_description = (getattr(page, "search_description", "") or "").strip()
        seo_title = (getattr(page, "seo_title", "") or "").strip()
        if not search_description:
            score += 5
            if "search description" in request_text_lower:
                score += 3
        if not seo_title:
            score += 5
            if "seo title" in request_text_lower:
                score += 3
        if "weak" in request_text_lower or "hygiene" in request_text_lower:
            if len(search_description) < 50:
                score += 2
            if len(seo_title) < 20:
                score += 2
        if self._is_suggestion_intent(request_text_lower):
            if not search_description:
                score += 3
            if not seo_title:
                score += 3
            if len(search_description) < 80:
                score += 1
            if len(seo_title) < 30:
                score += 1
        if "live" in request_text_lower and page.live:
            score += 2
        if not page.live:
            score += 1
        return score

    def _page_metadata(self, page, site) -> dict | None:
        title = (page.title or "").strip()
        if not title:
            return None

        search_description = (getattr(page, "search_description", "") or "").strip()
        seo_title = (getattr(page, "seo_title", "") or "").strip()
        issue_type = self._issue_type(
            seo_title=seo_title, search_description=search_description
        )
        return {
            "title": title,
            "status": "live" if page.live else "draft",
            "path": self._relative_path(page, site),
            "page_type": self._page_type(page),
            "seo_title": "present" if seo_title else "missing",
            "search_description": "present" if search_description else "missing",
            "seo_title_value": seo_title or _("missing"),
            "search_description_value": search_description or _("missing"),
            "updated": self._updated_label(page),
            "issue_type": issue_type,
        }

    def _issue_type(self, *, seo_title: str, search_description: str) -> str:
        if not search_description:
            return "missing_search_description"
        if not seo_title:
            return "missing_seo_title"
        return "weak_metadata"

    def _page_type(self, page) -> str:
        specific_class = getattr(page, "specific_class", None)
        if specific_class is not None:
            return specific_class.__name__
        content_type = getattr(page, "content_type", None)
        if content_type is not None:
            return content_type.model
        return page.__class__.__name__

    def _relative_path(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}"

    def _updated_label(self, page) -> str:
        updated = getattr(page, "latest_revision_created_at", None) or getattr(
            page, "latest_revision_created_at", None
        )
        if updated is None:
            return _("unknown")
        return localtime(updated).strftime("%Y-%m-%d %H:%M")

    def _is_suggestion_intent(self, request_text: str) -> bool:
        text = (request_text or "").lower()
        return any(re.search(pattern, text) for pattern in self._SUGGESTION_PATTERNS)
