"""VP 贴纸联网查找 — search provider + og:image 候选 + URL 导入。"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Any, Callable
from urllib.parse import urlparse

from niannian.soul.search_providers import SearchProvider, filter_hits_by_allowlist, url_allowed
from niannian.virtual_persona.enrich import (
    _allowed_image_url,
    _extract_og_image_url,
    _fetch_url_bytes,
)
from niannian.virtual_persona.pack import read_pack

if TYPE_CHECKING:
    from niannian.virtual_persona.store import VirtualPersonaStore

_PROFILE_MEME_RE = re.compile(r"^## 梗图关键词\s*$", re.MULTILINE)
_IMAGE_URL_RE = re.compile(r"\.(?:png|jpe?g|webp|gif)(?:\?|$)", re.IGNORECASE)
_DEFAULT_MEME_HINTS = ("Q版 表情包", "表情包 梗图")
_DEFAULT_MAX_RESULTS = 8
_DEFAULT_MAX_IMAGE_BYTES = 512 * 1024
_PAGE_MAX_BYTES = 256_000


class StickerSearchError(Exception):
    def __init__(self, code: str, message: str) -> None:
        super().__init__(message)
        self.code = code
        self.message = message


def sticker_search_config(config: dict[str, Any] | None) -> dict[str, Any]:
    vp = (config or {}).get("virtual_persona") or {}
    sticker = vp.get("sticker") if isinstance(vp, dict) else {}
    if not isinstance(sticker, dict):
        sticker = {}
    hints = sticker.get("search_meme_hints") or list(_DEFAULT_MEME_HINTS)
    if not isinstance(hints, list):
        hints = list(_DEFAULT_MEME_HINTS)
    return {
        "search_meme_hints": [str(h).strip() for h in hints if str(h).strip()],
        "max_stickers_per_run": int(sticker.get("max_stickers_per_run") or _DEFAULT_MAX_RESULTS),
        "size_px": int(sticker.get("size_px") or 128),
        "max_image_bytes": int(sticker.get("max_image_bytes") or _DEFAULT_MAX_IMAGE_BYTES),
    }


def extract_profile_meme_keywords(profile_md: str) -> list[str]:
    """Parse bullet lines under optional ``## 梗图关键词``."""
    match = _PROFILE_MEME_RE.search(str(profile_md or ""))
    if not match:
        return []
    tail = profile_md[match.end() :].lstrip("\n")
    keywords: list[str] = []
    for line in tail.splitlines():
        stripped = line.strip()
        if stripped.startswith("## "):
            break
        if stripped.startswith("- "):
            kw = stripped[2:].strip()
            if kw:
                keywords.append(kw)
    return keywords


def read_pack_meme_keywords(store: VirtualPersonaStore, vp_id: str) -> list[str]:
    try:
        pack = read_pack(store, vp_id)
    except FileNotFoundError:
        return []
    raw = pack.get("meme_keywords") or []
    if not isinstance(raw, list):
        return []
    return [str(k).strip() for k in raw if str(k).strip()]


def resolve_meme_keywords(
    store: VirtualPersonaStore,
    vp_id: str,
    *,
    profile_md: str = "",
    override: list[str] | None = None,
) -> list[str]:
    """Body override > pack.yaml > profile ``## 梗图关键词``."""
    if override:
        return [str(k).strip() for k in override if str(k).strip()]
    pack_kw = read_pack_meme_keywords(store, vp_id)
    if pack_kw:
        return pack_kw
    return extract_profile_meme_keywords(profile_md)


def build_sticker_search_query(
    label: str,
    *,
    query: str | None = None,
    meme_keywords: list[str] | None = None,
    meme_hints: list[str] | None = None,
) -> str:
    """Merge label · user query · meme keywords · default hints into one search string."""
    name = str(label or "角色").strip() or "角色"
    parts: list[str] = []
    user_q = str(query or "").strip()
    if user_q:
        parts.append(user_q)
    else:
        parts.append(name)
        hints = meme_hints or list(_DEFAULT_MEME_HINTS)
        if hints:
            parts.append(str(hints[0]))
    for kw in meme_keywords or []:
        clean = str(kw).strip()
        if clean and clean not in parts:
            parts.append(clean)
    return " ".join(parts)


def _is_image_url(url: str) -> bool:
    return bool(_IMAGE_URL_RE.search(str(url or "")))


def _validate_candidate_url(url: str, allowlist: list[str]) -> None:
    parsed = urlparse(str(url or "").strip())
    if parsed.scheme != "https":
        raise StickerSearchError("invalid_image_url", "only https image URLs are allowed")
    if allowlist and not url_allowed(url, allowlist):
        raise StickerSearchError("url_not_allowed", "image URL not in allowlist")


def search_sticker_candidates(
    *,
    search: SearchProvider,
    query: str,
    allowlist: list[str],
    max_results: int = _DEFAULT_MAX_RESULTS,
    page_fetcher: Callable[[str], bytes] | None = None,
) -> dict[str, Any]:
    """Search web hits and extract image candidate URLs (direct or og:image)."""
    limit = max(1, min(int(max_results), 16))
    raw_hits = search.search(query, limit=limit)
    hits = filter_hits_by_allowlist(raw_hits, allowlist)
    candidates: list[dict[str, Any]] = []
    seen: set[str] = set()

    def fetch_page(url: str) -> bytes:
        if page_fetcher is not None:
            return page_fetcher(url)
        return _fetch_url_bytes(url, max_bytes=_PAGE_MAX_BYTES)

    for hit in hits:
        page_url = str(hit.url or "").strip()
        if not page_url:
            continue
        if _is_image_url(page_url) and _allowed_image_url(page_url):
            if page_url not in seen and (not allowlist or url_allowed(page_url, allowlist)):
                candidates.append(
                    {
                        "url": page_url,
                        "title": hit.title,
                        "source_url": page_url,
                        "kind": "image",
                    }
                )
                seen.add(page_url)
            continue
        try:
            page_bytes = fetch_page(page_url)
            html = page_bytes.decode("utf-8", errors="replace")
            image_url = _extract_og_image_url(html, page_url)
            if not image_url or image_url in seen:
                continue
            if allowlist and not url_allowed(image_url, allowlist):
                continue
            candidates.append(
                {
                    "url": image_url,
                    "title": hit.title,
                    "source_url": page_url,
                    "kind": "og:image",
                }
            )
            seen.add(image_url)
        except Exception:
            continue

    return {
        "query": query,
        "candidates": candidates,
        "raw_hits": len(raw_hits),
        "allowed_hits": len(hits),
    }


def import_vp_sticker_from_url(
    store: VirtualPersonaStore,
    vp_id: str,
    url: str,
    *,
    allowlist: list[str],
    label: str | None = None,
    slug: str | None = None,
    triggers: list[str] | None = None,
    max_bytes: int = _DEFAULT_MAX_IMAGE_BYTES,
    fetch_fn: Callable[[str, int], bytes] | None = None,
) -> dict[str, Any]:
    """Download HTTPS image and append to VP stickers manifest."""
    from niannian.virtual_persona.stickers import add_vp_sticker_from_bytes

    clean_url = str(url or "").strip()
    _validate_candidate_url(clean_url, allowlist)

    def _fetch(target: str, limit: int) -> bytes:
        if fetch_fn is not None:
            return fetch_fn(target, limit)
        return _fetch_url_bytes(target, max_bytes=limit)

    try:
        data = _fetch(clean_url, max_bytes)
    except ValueError as exc:
        if str(exc) == "response_too_large":
            raise StickerSearchError("image_too_large", "image exceeds size limit") from exc
        raise StickerSearchError("fetch_failed", str(exc)) from exc
    except Exception as exc:
        raise StickerSearchError("fetch_failed", str(exc) or "fetch failed") from exc

    clean_label = (label or "联网表情").strip()[:32] or "联网表情"
    row = add_vp_sticker_from_bytes(
        store,
        vp_id,
        data,
        slug=slug,
        label=clean_label,
        source="web_search",
        triggers=triggers or [],
        source_url=clean_url,
    )
    return {"ok": True, "vp_id": vp_id, "sticker": row, "url": clean_url}
