"""Evidence-based pagination detection.

Order of precedence:
1. <link/a rel="next">
2. Adapter-provided explicit next selectors, then conservative generic next UI
3. Adapter-specific URL generation (site-approved pattern)
4. Numbered navigation where the current page and its next sibling are visible

Query parameters are never guessed from URLs inside generic pages.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import TYPE_CHECKING
from urllib.parse import urldefrag, urljoin, urlsplit

from bs4 import BeautifulSoup, Tag

if TYPE_CHECKING:
    from app.adapters.base import SiteAdapter

_NEXT_TEXTS = {
    "next",
    "next page",
    "next »",
    "»",
    "›",
    "older",
    "older posts",
    "بعدی",
    "صفحه بعد",
    "صفحهٔ بعد",
    "بعد",
    "التالي",
    "suivant",
    "weiter",
}
_DISABLED_CLASS_RE = re.compile(r"(?:^|\s)(?:disabled|is-disabled|deactive|inactive)(?:\s|$)", re.I)
_PAGINATION_CONTAINER_SELECTORS = (
    "nav[aria-label*='pag' i]",
    "nav[role='navigation']",
    "[class*='pagination' i]",
    "[class*='pager' i]",
    "[role='navigation']",
)
_NUMBER_RE = re.compile(r"^\s*(\d{1,4})\s*$")


@dataclass(frozen=True, slots=True)
class PaginationDecision:
    url: str | None
    strategy: str
    evidence: str = ""


def _is_disabled(tag: Tag) -> bool:
    if tag.has_attr("disabled"):
        return True
    if str(tag.get("aria-disabled", "")).lower() == "true":
        return True
    classes = " ".join(tag.get("class", []))
    if _DISABLED_CLASS_RE.search(classes):
        return True
    parent = tag.parent
    return isinstance(parent, Tag) and _DISABLED_CLASS_RE.search(
        " ".join(parent.get("class", []))
    ) is not None


def _href(tag: Tag | None, base_url: str) -> str | None:
    if not isinstance(tag, Tag) or tag.name != "a" or _is_disabled(tag):
        return None
    href = str(tag.get("href") or "").strip()
    if not href or href.startswith(("#", "javascript:", "mailto:", "tel:", "data:")):
        return None
    absolute = urldefrag(urljoin(base_url, href))[0]
    scheme = urlsplit(absolute).scheme.lower()
    if scheme not in {"http", "https"}:
        return None
    return absolute


def _same_url(left: str, right: str) -> bool:
    return urldefrag(left)[0] == urldefrag(right)[0]


def _text(tag: Tag) -> str:
    return " ".join(tag.get_text(" ", strip=True).split()).casefold()


class PaginationEngine:
    def detect(
        self,
        soup: BeautifulSoup,
        current_url: str,
        adapter: SiteAdapter | None = None,
    ) -> PaginationDecision:
        # 1) Explicit standards-based rel=next.
        for selector in ('link[rel~="next"]', 'a[rel~="next"]'):
            tag = soup.select_one(selector)
            if selector.startswith("link"):
                href = str(tag.get("href") or "").strip() if isinstance(tag, Tag) else ""
                if href:
                    url = urldefrag(urljoin(current_url, href))[0]
                    if urlsplit(url).scheme in {"http", "https"} and not _same_url(
                        url, current_url
                    ):
                        return PaginationDecision(url, "rel_next", selector)
            else:
                url = _href(tag, current_url)
                if url and not _same_url(url, current_url):
                    return PaginationDecision(url, "rel_next", selector)

        # 2) Adapter's explicit next-link selector is stronger than generic UI.
        if adapter is not None:
            for selector in getattr(adapter, "next_page_selectors", ()):
                try:
                    tag = soup.select_one(selector)
                except Exception:
                    continue
                url = _href(tag, current_url)
                if url and not _same_url(url, current_url):
                    return PaginationDecision(url, "adapter_next", selector)

        # 3) Conservative generic "next" UI inside navigation/pagination regions.
        containers: list[Tag] = []
        for selector in _PAGINATION_CONTAINER_SELECTORS:
            containers.extend(tag for tag in soup.select(selector) if isinstance(tag, Tag))
        seen_containers: set[int] = set()
        for container in containers:
            if id(container) in seen_containers:
                continue
            seen_containers.add(id(container))
            for anchor in container.find_all("a", href=True):
                label = _text(anchor)
                rel = " ".join(str(item).lower() for item in anchor.get("rel", []))
                aria = str(anchor.get("aria-label") or "").casefold()
                classes = " ".join(anchor.get("class", [])).casefold()
                title = str(anchor.get("title") or "").casefold()
                if (
                    "next" in rel
                    or label in _NEXT_TEXTS
                    or aria in _NEXT_TEXTS
                    or title in _NEXT_TEXTS
                    or re.search(r"(?:^|\s)(?:next|next-page)(?:\s|$)", classes)
                ):
                    url = _href(anchor, current_url)
                    if url and not _same_url(url, current_url):
                        return PaginationDecision(url, "next_ui", label or aria or classes)
        # aria-label next outside a pagination container is still explicit.
        for anchor in soup.find_all("a", href=True):
            aria = str(anchor.get("aria-label") or "").casefold()
            if aria in _NEXT_TEXTS:
                url = _href(anchor, current_url)
                if url and not _same_url(url, current_url):
                    return PaginationDecision(url, "next_aria", aria)

        # 4) Adapter-controlled URL pattern (never guessed by the engine).
        if adapter is not None:
            try:
                custom = adapter.get_next_page_url(soup, current_url)
            except Exception:
                custom = None
            if custom:
                absolute = urldefrag(urljoin(current_url, custom))[0]
                if urlsplit(absolute).scheme in {"http", "https"} and not _same_url(
                    absolute, current_url
                ):
                    return PaginationDecision(absolute, "adapter_pattern", "configured adapter")

        # 5) Numbered pagination with visible current-page evidence.
        numbered = self._numbered_next(soup, current_url)
        if numbered:
            return numbered
        return PaginationDecision(None, "none")

    @staticmethod
    def _numbered_next(soup: BeautifulSoup, current_url: str) -> PaginationDecision | None:
        for selector in _PAGINATION_CONTAINER_SELECTORS:
            for container in soup.select(selector):
                if not isinstance(container, Tag):
                    continue
                current_tag: Tag | None = None
                current_selector = (
                    "[aria-current='page']",
                    ".current",
                    ".active",
                    ".page-item.active",
                    ".is-active",
                )
                for marker_selector in current_selector:
                    candidate = container.select_one(marker_selector)
                    if isinstance(candidate, Tag):
                        current_tag = candidate
                        break
                if current_tag is None:
                    continue
                number_text = _text(current_tag)
                match = _NUMBER_RE.match(number_text)
                if not match and isinstance(current_tag.parent, Tag):
                    match = _NUMBER_RE.match(_text(current_tag.parent))
                if not match:
                    continue
                current_number = int(match.group(1))
                next_links: list[tuple[int, Tag]] = []
                for anchor in container.find_all("a", href=True):
                    number_match = _NUMBER_RE.match(_text(anchor))
                    if not number_match:
                        continue
                    number = int(number_match.group(1))
                    if number == current_number + 1:
                        next_links.append((number, anchor))
                if next_links:
                    _, anchor = sorted(next_links, key=lambda item: item[0])[0]
                    url = _href(anchor, current_url)
                    if url and not _same_url(url, current_url):
                        return PaginationDecision(url, "numbered", str(current_number + 1))
                # Also support a numbered "next" sibling (not merely a text link).
                sibling = current_tag.find_next_sibling()
                while isinstance(sibling, Tag):
                    if sibling.name == "a":
                        number_match = _NUMBER_RE.match(_text(sibling))
                        if number_match:
                            url = _href(sibling, current_url)
                            if url and not _same_url(url, current_url):
                                return PaginationDecision(url, "numbered", number_match.group(1))
                    sibling = sibling.find_next_sibling()
        return None
