from __future__ import annotations

import html
from datetime import UTC, datetime
from typing import Any
from urllib.parse import parse_qsl, unquote, urlencode, urljoin, urlparse

from rights_filter.domain.records import DataClass, Evidence, EvidenceSource
from rights_filter.governance.policies import assert_operator_evidence_payload_allowed
from rights_filter.integrations.http_json import UrllibJsonTransport
from rights_filter.integrations.search_policy import SearchApiPolicy


class FakeNaverSearchClient:
    def __init__(
        self,
        response: dict[str, Any] | None = None,
        blog_response: dict[str, Any] | None = None,
        web_response: dict[str, Any] | None = None,
    ) -> None:
        self.response = response or {"items": []}
        self.blog_response = blog_response or {"items": []}
        self.web_response = web_response or {"items": []}
        self.calls: list[str] = []
        self.blog_calls: list[str] = []
        self.web_calls: list[str] = []

    def search_image(self, query: str) -> dict[str, Any]:
        self.calls.append(query)
        return self.response

    def search_blog(self, query: str) -> dict[str, Any]:
        self.blog_calls.append(query)
        return self.blog_response

    def search_web(self, query: str) -> dict[str, Any]:
        self.web_calls.append(query)
        return self.web_response


class NaverOpenApiSearchClient:
    image_endpoint = "https://openapi.naver.com/v1/search/image"
    blog_endpoint = "https://openapi.naver.com/v1/search/blog"
    web_endpoint = "https://openapi.naver.com/v1/search/webkr"

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        transport: Any | None = None,
        display: int = 10,
        start: int = 1,
        sort: str = "sim",
        image_pages: int = 1,
        blog_display: int = 3,
        blog_start: int = 1,
        blog_sort: str = "sim",
        blog_pages: int = 1,
        web_display: int = 3,
        web_start: int = 1,
        web_pages: int = 1,
        timeout: int = 10,
    ) -> None:
        self.client_id = client_id
        self.client_secret = client_secret
        self.transport = transport or UrllibJsonTransport()
        self.display = display
        self.start = start
        self.sort = sort
        self.image_pages = max(1, image_pages)
        self.blog_display = blog_display
        self.blog_start = blog_start
        self.blog_sort = blog_sort
        self.blog_pages = max(1, blog_pages)
        self.web_display = web_display
        self.web_start = web_start
        self.web_pages = max(1, web_pages)
        self.timeout = timeout

    def search_image(self, query: str) -> dict[str, Any]:
        return self._search_pages(
            self.image_endpoint,
            query,
            display=self.display,
            start=self.start,
            pages=self.image_pages,
            extra_params={"sort": self.sort},
        )

    def search_blog(self, query: str) -> dict[str, Any]:
        return self._search_pages(
            self.blog_endpoint,
            query,
            display=self.blog_display,
            start=self.blog_start,
            pages=self.blog_pages,
            extra_params={"sort": self.blog_sort},
        )

    def search_web(self, query: str) -> dict[str, Any]:
        return self._search_pages(
            self.web_endpoint,
            query,
            display=self.web_display,
            start=self.web_start,
            pages=self.web_pages,
            extra_params={},
        )

    def _headers(self) -> dict[str, str]:
        return {
            "X-Naver-Client-Id": self.client_id,
            "X-Naver-Client-Secret": self.client_secret,
        }

    def _search_pages(
        self,
        endpoint: str,
        query: str,
        display: int,
        start: int,
        pages: int,
        extra_params: dict[str, Any],
    ) -> dict[str, Any]:
        merged: dict[str, Any] = {}
        items: list[Any] = []
        for page_index in range(max(1, pages)):
            params = urlencode(
                {
                    "query": query,
                    "display": display,
                    "start": start + (page_index * display),
                    **extra_params,
                }
            )
            response = self.transport.request_json(
                "GET",
                f"{endpoint}?{params}",
                headers=self._headers(),
                timeout=self.timeout,
            )
            if not merged:
                merged = dict(response)
            page_items = response.get("items", [])
            if not page_items:
                break
            items.extend(page_items)
        merged["items"] = items
        return merged


class NaverSearchAdapter:
    provider = "naver"

    def __init__(self, client: Any) -> None:
        self.client = client

    def search(
        self, submission_id: str, query: str, policy: SearchApiPolicy
    ) -> list[Evidence]:
        if not isinstance(query, str):
            raise ValueError("Naver search requires a text query")

        requested_calls = _requested_calls(self.client, "image_pages")
        allowed, reason = policy.can_call(self.provider, requested_calls=requested_calls)
        if not allowed:
            return [
                Evidence(
                    source=EvidenceSource.SEARCH_SKIPPED,
                    reason=reason or "search API skipped",
                    confidence=1.0,
                    data={
                        "submission_id": submission_id,
                        "provider": self.provider,
                        "query": query,
                        "query_signature": _query_signature(query),
                    },
                )
            ]

        try:
            response = self.client.search_image(query)
            policy.record_call(requested_calls)
            return _map_response(submission_id, query, response)
        except Exception as exc:  # pragma: no cover - defensive adapter boundary
            return [
                Evidence(
                    source=EvidenceSource.ENRICHMENT_FAILURE,
                    reason=f"Naver search failed: {exc}",
                    confidence=1.0,
                    data={
                        "submission_id": submission_id,
                        "provider": self.provider,
                        "query": query,
                        "query_signature": _query_signature(query),
                    },
                )
            ]

    def search_pages(
        self, submission_id: str, query: str, policy: SearchApiPolicy
    ) -> list[Evidence]:
        if not isinstance(query, str):
            raise ValueError("Naver search requires a text query")

        requested_calls = _requested_calls(self.client, "blog_pages")
        allowed, reason = policy.can_call(self.provider, requested_calls=requested_calls)
        if not allowed:
            return [
                Evidence(
                    source=EvidenceSource.SEARCH_SKIPPED,
                    reason=reason or "search API skipped",
                    confidence=1.0,
                    data={
                        "submission_id": submission_id,
                        "provider": self.provider,
                        "search_type": "blog",
                        "query": query,
                        "query_signature": _blog_query_signature(query),
                    },
                )
            ]

        try:
            response = self.client.search_blog(query)
            policy.record_call(requested_calls)
            return _map_blog_response(submission_id, query, response)
        except Exception as exc:  # pragma: no cover - defensive adapter boundary
            return [
                Evidence(
                    source=EvidenceSource.ENRICHMENT_FAILURE,
                    reason=f"Naver blog search failed: {exc}",
                    confidence=1.0,
                    data={
                        "submission_id": submission_id,
                        "provider": self.provider,
                        "search_type": "blog",
                        "query": query,
                        "query_signature": _blog_query_signature(query),
                    },
                )
            ]

    def search_web_pages(
        self, submission_id: str, query: str, policy: SearchApiPolicy
    ) -> list[Evidence]:
        if not isinstance(query, str):
            raise ValueError("Naver search requires a text query")

        requested_calls = _requested_calls(self.client, "web_pages")
        allowed, reason = policy.can_call(self.provider, requested_calls=requested_calls)
        if not allowed:
            return [
                Evidence(
                    source=EvidenceSource.SEARCH_SKIPPED,
                    reason=reason or "search API skipped",
                    confidence=1.0,
                    data={
                        "submission_id": submission_id,
                        "provider": self.provider,
                        "search_type": "web",
                        "query": query,
                        "query_signature": _web_query_signature(query),
                    },
                )
            ]

        try:
            response = self.client.search_web(query)
            policy.record_call(requested_calls)
            return _map_web_response(submission_id, query, response)
        except Exception as exc:  # pragma: no cover - defensive adapter boundary
            return [
                Evidence(
                    source=EvidenceSource.ENRICHMENT_FAILURE,
                    reason=f"Naver web search failed: {exc}",
                    confidence=1.0,
                    data={
                        "submission_id": submission_id,
                        "provider": self.provider,
                        "search_type": "web",
                        "query": query,
                        "query_signature": _web_query_signature(query),
                    },
                )
            ]


def _map_response(
    submission_id: str, query: str, response: dict[str, Any]
) -> list[Evidence]:
    items = response.get("items", [])
    retrieved_at = _now_iso()
    if not items:
        return [
            Evidence(
                source=EvidenceSource.NAVER_SEARCH,
                reason="Naver search returned no results",
                confidence=0.0,
                data={
                    "submission_id": submission_id,
                    "provider": "naver",
                    "query": query,
                    "query_signature": _query_signature(query),
                    "retrieved_at": retrieved_at,
                },
            )
        ]

    evidence: list[Evidence] = []
    for rank, item in enumerate(items, start=1):
        image_url = _normalized_image_url(item.get("link", ""))
        thumbnail_url = _normalized_image_url(item.get("thumbnail", ""))
        result_url = _normalized_result_url(item.get("page_url") or item.get("originallink", "")) or image_url
        payload = {
            "submission_id": submission_id,
            "provider": "naver",
            "query": query,
            "query_signature": _query_signature(query),
            "rank": rank,
            "title": item.get("title", ""),
            "description": item.get("description", ""),
            "image_url": image_url,
            "thumbnail_url": thumbnail_url,
            "result_url": result_url,
            "height": item.get("sizeheight"),
            "width": item.get("sizewidth"),
            "retrieved_at": retrieved_at,
        }
        assert_operator_evidence_payload_allowed(DataClass.SEARCH_EVIDENCE, payload)
        evidence.append(
            Evidence(
                source=EvidenceSource.NAVER_SEARCH,
                reason="Naver search result found",
                confidence=0.5,
                data=payload,
            )
        )
    return evidence


def _map_blog_response(
    submission_id: str, query: str, response: dict[str, Any]
) -> list[Evidence]:
    items = response.get("items", [])
    retrieved_at = _now_iso()
    if not items:
        return [
            Evidence(
                source=EvidenceSource.NAVER_SEARCH,
                reason="Naver blog search returned no results",
                confidence=0.0,
                data={
                    "submission_id": submission_id,
                    "provider": "naver",
                    "search_type": "blog",
                    "query": query,
                    "query_signature": _blog_query_signature(query),
                    "retrieved_at": retrieved_at,
                },
            )
        ]

    evidence: list[Evidence] = []
    for rank, item in enumerate(items, start=1):
        result_url = _normalized_result_url(item.get("link", ""))
        page_image_urls = _page_image_urls_from_item(item, result_url)
        payload = {
            "submission_id": submission_id,
            "provider": "naver",
            "search_type": "blog",
            "query": query,
            "query_signature": _blog_query_signature(query),
            "rank": rank,
            "title": item.get("title", ""),
            "description": item.get("description", ""),
            "result_url": result_url,
            "blogger_name": item.get("bloggername", ""),
            "blogger_link": item.get("bloggerlink", ""),
            "postdate": item.get("postdate", ""),
            "match": "page",
            "retrieved_at": retrieved_at,
        }
        if page_image_urls:
            payload["page_image_urls"] = page_image_urls
        assert_operator_evidence_payload_allowed(DataClass.SEARCH_EVIDENCE, payload)
        evidence.append(
            Evidence(
                source=EvidenceSource.NAVER_SEARCH,
                reason="Naver blog search result found",
                confidence=0.35,
                data=payload,
            )
        )
    return evidence


def _map_web_response(
    submission_id: str, query: str, response: dict[str, Any]
) -> list[Evidence]:
    items = response.get("items", [])
    retrieved_at = _now_iso()
    if not items:
        return [
            Evidence(
                source=EvidenceSource.NAVER_SEARCH,
                reason="Naver web search returned no results",
                confidence=0.0,
                data={
                    "submission_id": submission_id,
                    "provider": "naver",
                    "search_type": "web",
                    "query": query,
                    "query_signature": _web_query_signature(query),
                    "retrieved_at": retrieved_at,
                },
            )
        ]

    evidence: list[Evidence] = []
    for rank, item in enumerate(items, start=1):
        result_url = _normalized_result_url(item.get("link", ""))
        page_image_urls = _page_image_urls_from_item(item, result_url)
        payload = {
            "submission_id": submission_id,
            "provider": "naver",
            "search_type": "web",
            "query": query,
            "query_signature": _web_query_signature(query),
            "rank": rank,
            "title": item.get("title", ""),
            "description": item.get("description", ""),
            "result_url": result_url,
            "match": "page",
            "retrieved_at": retrieved_at,
        }
        if page_image_urls:
            payload["page_image_urls"] = page_image_urls
        assert_operator_evidence_payload_allowed(DataClass.SEARCH_EVIDENCE, payload)
        evidence.append(
            Evidence(
                source=EvidenceSource.NAVER_SEARCH,
                reason="Naver web search result found",
                confidence=0.3,
                data=payload,
            )
        )
    return evidence


def _page_image_urls_from_item(item: dict[str, Any], base_url: str) -> list[str]:
    image_keys = {
        "content_url",
        "contenturl",
        "image",
        "image_url",
        "imageurl",
        "img",
        "img_url",
        "imgurl",
        "media_url",
        "mediaurl",
        "og_image",
        "og_image_url",
        "photo",
        "photo_url",
        "photourl",
        "picture",
        "picture_url",
        "poster",
        "poster_url",
        "src",
        "thumbnail",
        "thumbnail_url",
        "thumbnailurl",
        "twitter_image",
        "twitter_image_url",
    }
    candidates: list[str] = []
    for key, value in item.items():
        if _image_hint_key(key) in image_keys:
            candidates.extend(_image_hint_values(value))
    return _unique_texts(
        _normalized_page_image_url(candidate, base_url) for candidate in candidates
    )


def _image_hint_key(key: Any) -> str:
    return (
        str(key)
        .strip()
        .lower()
        .replace("-", "_")
        .replace(":", "_")
        .replace(".", "_")
    )


def _image_hint_values(value: Any) -> list[str]:
    if isinstance(value, str):
        return [value]
    if isinstance(value, list):
        values: list[str] = []
        for item in value:
            values.extend(_image_hint_values(item))
        return values
    if isinstance(value, dict):
        values = []
        for key in (
            "contentUrl",
            "content_url",
            "imageUrl",
            "image_url",
            "src",
            "thumbnailUrl",
            "thumbnail_url",
            "url",
        ):
            if value.get(key):
                values.extend(_image_hint_values(value[key]))
        return values
    return []


def _normalized_page_image_url(value: Any, base_url: str) -> str:
    text = _decoded_url_reference(_clean_url(value))
    if not text or text.lower().startswith("data:"):
        return ""
    if text.startswith("//"):
        text = f"https:{text}"
    elif base_url and text.startswith(("/", "./", "../")):
        text = urljoin(base_url, text)
    normalized = _normalized_image_url(text)
    parsed = urlparse(normalized)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        return ""
    return normalized


def _unique_texts(values: Any) -> list[str]:
    seen: set[str] = set()
    result: list[str] = []
    for value in values:
        text = str(value or "").strip()
        if not text or text in seen:
            continue
        seen.add(text)
        result.append(text)
    return result


def _query_signature(query: str) -> str:
    return "naver:" + " ".join(query.lower().split())


def _blog_query_signature(query: str) -> str:
    return "naver-blog:" + " ".join(query.lower().split())


def _web_query_signature(query: str) -> str:
    return "naver-web:" + " ".join(query.lower().split())


def _requested_calls(client: Any, attribute: str) -> int:
    try:
        return max(1, int(getattr(client, attribute, 1) or 1))
    except (TypeError, ValueError):
        return 1


def _normalized_image_url(value: Any) -> str:
    text = _decoded_url_reference(_clean_url(value))
    if not text or text.lower().startswith("data:"):
        return ""
    if _is_scheme_less_remote_image_url(text):
        text = f"https://{text.lstrip('/')}"
    normalized = _unwrapped_image_url(text) or text
    parsed = urlparse(normalized)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        return ""
    return normalized


def _clean_url(value: Any) -> str:
    return html.unescape(str(value or "").strip())


def _normalized_result_url(value: Any) -> str:
    text = _decoded_url_reference(_clean_url(value))
    if not text or text.lower().startswith("data:"):
        return ""
    if text.startswith("//"):
        text = f"https:{text}"
    normalized = _unwrapped_result_url(text) or text
    parsed = urlparse(normalized)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        return ""
    return normalized


def _unwrapped_result_url(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        return ""
    if "naver" not in parsed.netloc.lower():
        return ""

    redirect_keys = {
        "url",
        "u",
        "q",
        "target",
        "redirect",
        "redirect_url",
        "imgrefurl",
        "page_url",
    }
    for key, raw_value in parse_qsl(parsed.query, keep_blank_values=False):
        if key.lower().replace("-", "_") not in redirect_keys:
            continue
        candidate = _decoded_nested_url(raw_value)
        if candidate.startswith("//"):
            candidate = f"https:{candidate}"
        if _is_http_url(candidate):
            return candidate
    return ""


def _unwrapped_image_url(url: str) -> str:
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"} or not parsed.netloc:
        return ""

    strong_keys = {
        "imgurl",
        "imageurl",
        "image_url",
        "mediaurl",
        "media_url",
        "contenturl",
        "content_url",
        "photo",
        "photo_url",
        "src",
        "source",
        "image",
        "img",
    }
    weak_keys = {"url", "u", "target", "redirect", "redirect_url"}
    for key, raw_value in parse_qsl(parsed.query, keep_blank_values=False):
        key_text = key.lower().replace("-", "_")
        candidate = _decoded_nested_url(raw_value)
        if not candidate:
            continue
        if not _is_http_url(candidate):
            if candidate.startswith("//"):
                candidate = f"https:{candidate}"
            elif _is_scheme_less_remote_image_url(candidate):
                candidate = f"https://{candidate.lstrip('/')}"
            elif candidate.startswith("/") or _url_looks_like_image(candidate):
                candidate = urljoin(url, candidate)
            else:
                continue
        if key_text in strong_keys:
            return candidate
        if key_text in weak_keys and _url_looks_like_image(candidate):
            return candidate
    return ""


def _decoded_url_reference(value: str) -> str:
    raw = str(value).strip()
    decoded = _decoded_nested_url(raw)
    if decoded == raw:
        return raw
    if (
        _is_http_url(decoded)
        or decoded.startswith(("/", "//", "./", "../"))
        or _is_scheme_less_remote_image_url(decoded)
        or _url_looks_like_image(decoded)
    ):
        return decoded
    return raw


def _decoded_nested_url(value: str) -> str:
    candidate = str(value).strip()
    for _ in range(3):
        decoded = unquote(candidate).strip()
        if decoded == candidate:
            break
        candidate = decoded
    return candidate


def _is_http_url(value: str) -> bool:
    parsed = urlparse(value)
    return parsed.scheme in {"http", "https"} and bool(parsed.netloc)


def _is_scheme_less_remote_image_url(value: str) -> bool:
    text = str(value).strip().lstrip("/")
    if not _url_looks_like_image(text):
        return False
    first_segment = text.split("/", 1)[0]
    if first_segment in {".", ".."} or first_segment.startswith("."):
        return False
    return "." in first_segment and " " not in first_segment


def _url_path_has_image_suffix(value: str) -> bool:
    return urlparse(value).path.lower().endswith(
        (
            ".jpg",
            ".jpeg",
            ".jfif",
            ".pjp",
            ".pjpeg",
            ".png",
            ".gif",
            ".webp",
            ".avif",
            ".bmp",
        )
    )


def _url_has_image_format_hint(value: str) -> bool:
    image_formats = {
        "avif",
        "bmp",
        "gif",
        "jpeg",
        "jfif",
        "jpg",
        "pjp",
        "pjpeg",
        "png",
        "webp",
    }
    image_format_keys = {"format", "fm", "ext", "extension", "mime", "output", "type"}
    for key, hint in parse_qsl(urlparse(value).query, keep_blank_values=False):
        if key.lower().replace("-", "_") not in image_format_keys:
            continue
        normalized = hint.lower().split(";", 1)[0].strip().lstrip(".")
        if normalized.startswith("image/"):
            normalized = normalized.split("/", 1)[1]
        normalized = normalized.split("+", 1)[0]
        if normalized in image_formats:
            return True
    return False


def _url_looks_like_image(value: str) -> bool:
    return _url_path_has_image_suffix(value) or _url_has_image_format_hint(value)


def _now_iso() -> str:
    return datetime.now(UTC).replace(microsecond=0).isoformat()
