from __future__ import annotations

import html
import re
from collections.abc import Iterable
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 GoogleCustomSearchClient:
    endpoint = "https://www.googleapis.com/customsearch/v1"

    def __init__(
        self,
        api_key: str,
        cx: str,
        transport: Any | None = None,
        image_num: int = 3,
        web_num: int = 3,
        image_pages: int = 1,
        web_pages: int = 1,
        timeout: int = 10,
    ) -> None:
        self.api_key = api_key
        self.cx = cx
        self.transport = transport or UrllibJsonTransport()
        self.image_num = min(10, max(1, image_num))
        self.web_num = min(10, max(1, web_num))
        self.image_pages = max(1, image_pages)
        self.web_pages = max(1, web_pages)
        self.timeout = timeout

    def search_image(self, query: str) -> dict[str, Any]:
        return self._search_pages(
            query,
            num=self.image_num,
            pages=self.image_pages,
            extra_params={"searchType": "image"},
        )

    def search_web(self, query: str) -> dict[str, Any]:
        return self._search_pages(
            query,
            num=self.web_num,
            pages=self.web_pages,
            extra_params={},
        )

    def _search_pages(
        self,
        query: str,
        num: 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 = {
                "key": self.api_key,
                "cx": self.cx,
                "q": query,
                "num": num,
                "start": 1 + (page_index * num),
                **extra_params,
            }
            response = self.transport.request_json(
                "GET",
                f"{self.endpoint}?{urlencode(params)}",
                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 GoogleCustomSearchAdapter:
    provider = "google_custom_search"

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

    def search_images(
        self, submission_id: str, query: str, policy: SearchApiPolicy
    ) -> list[Evidence]:
        if not isinstance(query, str):
            raise ValueError("Google custom 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,
                        "search_type": "image",
                        "query": query,
                        "query_signature": _image_query_signature(query),
                    },
                )
            ]

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

        policy.record_call(requested_calls)
        return _map_image_response(submission_id, query, response)

    def search_web_pages(
        self, submission_id: str, query: str, policy: SearchApiPolicy
    ) -> list[Evidence]:
        if not isinstance(query, str):
            raise ValueError("Google custom 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)
        except Exception as exc:  # pragma: no cover - defensive adapter boundary
            return [
                Evidence(
                    source=EvidenceSource.ENRICHMENT_FAILURE,
                    reason=f"Google custom 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),
                    },
                )
            ]

        policy.record_call(requested_calls)
        return _map_web_response(submission_id, query, response)


def _map_image_response(
    submission_id: str, query: str, response: dict[str, Any]
) -> list[Evidence]:
    items = response.get("items", [])
    if not items:
        return [
            Evidence(
                source=EvidenceSource.WEB_DETECTION,
                reason="Google custom image search returned no results",
                confidence=0.0,
                data={
                    "submission_id": submission_id,
                    "provider": "google_custom_search",
                    "search_type": "image",
                    "query": query,
                    "query_signature": _image_query_signature(query),
                    "retrieved_at": _now_iso(),
                },
            )
        ]

    evidence: list[Evidence] = []
    for rank, item in enumerate(items, start=1):
        image = item.get("image", {}) or {}
        image_url = _normalize_image_url(str(item.get("link", "")), "")
        thumbnail_url = _normalize_image_url(str(image.get("thumbnailLink", "")), "")
        result_url = _normalize_result_url(image.get("contextLink", "")) or image_url
        payload = {
            "submission_id": submission_id,
            "provider": "google_custom_search",
            "search_type": "image",
            "query": query,
            "query_signature": _image_query_signature(query),
            "rank": rank,
            "title": item.get("title", ""),
            "description": item.get("snippet", ""),
            "image_url": image_url,
            "thumbnail_url": thumbnail_url,
            "result_url": result_url,
            "domain": item.get("displayLink", ""),
            "page_image_urls": _page_image_urls_from_pagemap(
                item.get("pagemap", {}) or {},
                str(result_url),
            ),
            "height": image.get("height"),
            "width": image.get("width"),
            "match": "search_image",
            "retrieved_at": _now_iso(),
        }
        assert_operator_evidence_payload_allowed(DataClass.SEARCH_EVIDENCE, payload)
        evidence.append(
            Evidence(
                source=EvidenceSource.WEB_DETECTION,
                reason="Google custom image search result found",
                confidence=0.45,
                data=payload,
            )
        )
    return evidence


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

    evidence: list[Evidence] = []
    for rank, item in enumerate(items, start=1):
        result_url = _normalize_result_url(item.get("link", ""))
        payload = {
            "submission_id": submission_id,
            "provider": "google_custom_search",
            "search_type": "web",
            "query": query,
            "query_signature": _web_query_signature(query),
            "rank": rank,
            "title": item.get("title", ""),
            "description": item.get("snippet", ""),
            "result_url": result_url,
            "domain": item.get("displayLink", ""),
            "page_image_urls": _page_image_urls_from_pagemap(
                item.get("pagemap", {}) or {},
                result_url,
            ),
            "match": "page",
            "retrieved_at": _now_iso(),
        }
        assert_operator_evidence_payload_allowed(DataClass.SEARCH_EVIDENCE, payload)
        evidence.append(
            Evidence(
                source=EvidenceSource.WEB_DETECTION,
                reason="Google custom web search result found",
                confidence=0.35,
                data=payload,
            )
        )
    return evidence


def _page_image_urls_from_pagemap(pagemap: dict[str, Any], base_url: str = "") -> list[str]:
    candidates: list[str] = []
    for item in pagemap.get("cse_image", []) or []:
        if item.get("src"):
            candidates.append(str(item["src"]))
    for item in pagemap.get("cse_thumbnail", []) or []:
        candidates.extend(_image_values(item, ("src", "url")))
    for item in pagemap.get("imageobject", []) or []:
        candidates.extend(
            _image_values(
                item,
                (
                    "url",
                    "contenturl",
                    "contentUrl",
                    "thumbnailurl",
                    "thumbnailUrl",
                    "src",
                ),
            )
        )
    for item in pagemap.get("thumbnail", []) or []:
        candidates.extend(_image_values(item, ("src", "url")))
    for item in pagemap.get("metatags", []) or []:
        for key in (
            "og:image",
            "og:image:url",
            "og:image:secure_url",
            "twitter:image",
            "twitter:image:src",
            "twitter:image:url",
        ):
            if item.get(key):
                candidates.append(str(item[key]))
    candidates.extend(_generic_pagemap_image_values(pagemap))
    return _unique_texts(
        _normalize_image_url(candidate, base_url)
        for candidate in _expanded_image_candidates(candidates)
    )


def _image_values(item: Any, keys: tuple[str, ...]) -> list[str]:
    if not isinstance(item, dict):
        return []
    return [str(item[key]) for key in keys if item.get(key)]


def _generic_pagemap_image_values(pagemap: dict[str, Any]) -> list[str]:
    candidates: list[str] = []
    image_context_keys = {
        "associatedmedia",
        "contenturl",
        "image",
        "logo",
        "mediaurl",
        "photo",
        "picture",
        "primaryimageofpage",
        "thumbnail",
        "thumbnailurl",
    }
    image_value_keys = {
        "content_url",
        "contenturl",
        "content_src",
        "contentsrc",
        "download_url",
        "downloadurl",
        "file_url",
        "fileurl",
        "href",
        "image_url",
        "imageurl",
        "media_url",
        "mediaurl",
        "original_url",
        "originalurl",
        "public_url",
        "publicurl",
        "secure_url",
        "secureurl",
        "src_set",
        "srcset",
        "src",
        "thumbnail_url",
        "thumbnailurl",
        "url",
    }
    direct_image_value_keys = image_context_keys | image_value_keys | {
        "og_image",
        "og_image_url",
        "og_image_secure_url",
        "twitter_image",
        "twitter_image_src",
        "twitter_image_url",
    }

    def normalized_key(value: Any) -> str:
        return str(value).lower().replace("-", "_").replace(":", "_")

    def is_image_context(key: Any) -> bool:
        normalized = normalized_key(key)
        return normalized in image_context_keys or any(
            token in normalized
            for token in ("image", "thumbnail", "photo", "picture", "poster")
        )

    def is_text_or_text_list(value: Any) -> bool:
        if isinstance(value, str):
            return True
        return isinstance(value, list) and all(isinstance(item, str) for item in value)

    def is_direct_image_value_key(key: Any) -> bool:
        normalized = normalized_key(key)
        if normalized in direct_image_value_keys:
            return True
        return any(
            normalized.endswith(f"_{suffix}")
            for suffix in ("image", "thumbnail", "photo", "picture", "poster", "logo")
        )

    def visit(value: Any, in_image_context: bool = False, collect_strings: bool = False) -> None:
        if isinstance(value, str):
            if collect_strings:
                candidates.append(value)
            return
        if isinstance(value, list):
            for item in value:
                visit(item, in_image_context, collect_strings)
            return
        if not isinstance(value, dict):
            return
        for key, child in value.items():
            key_text = normalized_key(key)
            next_context = in_image_context or is_image_context(key)
            if next_context and (
                key_text in image_value_keys
                or (
                    is_image_context(key)
                    and is_direct_image_value_key(key)
                    and is_text_or_text_list(child)
                )
            ):
                visit(child, True, True)
            elif next_context:
                visit(child, True, False)
            else:
                visit(child, False, False)

    visit(pagemap)
    return candidates


def _expanded_image_candidates(candidates: list[str]) -> list[str]:
    expanded: list[str] = []
    for candidate in candidates:
        text = str(candidate).strip()
        if _looks_like_srcset(text):
            expanded.extend(_srcset_image_urls(text))
        else:
            expanded.append(text)
    return expanded


def _looks_like_srcset(value: str) -> bool:
    if "," not in value:
        return False
    return bool(re.search(r"\s+\d+(?:\.\d+)?[wx](?:\s*,|$)", value, flags=re.IGNORECASE))


def _srcset_image_urls(value: str) -> list[str]:
    ranked: list[tuple[float, int, str]] = []
    for order, candidate in enumerate(_split_srcset_candidates(value)):
        parts = candidate.split()
        if not parts:
            continue
        ranked.append((_srcset_descriptor_score(parts[1] if len(parts) > 1 else ""), order, parts[0]))
    ranked.sort(key=lambda item: (-item[0], item[1]))
    return [url for _score, _order, url in ranked]


def _split_srcset_candidates(value: str) -> list[str]:
    candidates: list[str] = []
    start = 0
    text = str(value)
    for index, character in enumerate(text):
        if character != ",":
            continue
        remainder = text[index + 1 :].lstrip()
        if not _starts_srcset_candidate(remainder):
            continue
        candidates.append(text[start:index])
        start = index + 1
    candidates.append(text[start:])
    return candidates


def _starts_srcset_candidate(value: str) -> bool:
    text = str(value).strip()
    if not text:
        return False
    first_token = text.split(None, 1)[0]
    return _is_urlish_reference(first_token) or _is_scheme_less_remote_image_url(first_token)


def _is_urlish_reference(value: str) -> bool:
    text = str(value).strip()
    return text.startswith(("http://", "https://", "//", "/", "./", "../")) or _url_looks_like_image(text)


def _srcset_descriptor_score(value: str) -> float:
    descriptor = str(value).strip().lower()
    if descriptor.endswith("w"):
        try:
            return float(descriptor[:-1])
        except ValueError:
            return 0.0
    if descriptor.endswith("x"):
        try:
            density = float(descriptor[:-1])
        except ValueError:
            return 0.0
        if density <= 0.0:
            return 0.0
        # Map density descriptors into a strictly-below-1 band so any real pixel
        # width ('w', >= 1) outranks them in a malformed mixed-unit srcset, while
        # preserving x-vs-x ordering (monotonic in density).
        return density / (density + 1.0)
    return 0.0


def _normalize_image_url(url: str, base_url: str) -> str:
    text = _decoded_url_reference(_clean_url(url))
    if not text or text.lower().startswith("data:"):
        return ""
    if _is_scheme_less_remote_image_url(text):
        text = f"https://{text.lstrip('/')}"
    normalized = urljoin(base_url, text) if base_url else text
    normalized = _unwrapped_image_url(normalized) or normalized
    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 _normalize_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 "google" not in parsed.netloc.lower():
        return ""

    redirect_keys = {
        "url",
        "u",
        "q",
        "target",
        "redirect",
        "redirect_url",
        "imgrefurl",
        "page_url",
    }
    for key, value in parse_qsl(parsed.query, keep_blank_values=False):
        if key.lower().replace("-", "_") not in redirect_keys:
            continue
        candidate = _decoded_nested_url(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, value in parse_qsl(parsed.query, keep_blank_values=False):
        key_text = key.lower().replace("-", "_")
        candidate = _decoded_nested_url(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 and _is_http_url(candidate):
            return candidate
        if key_text in weak_keys and _is_http_url(candidate) 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:
    path = urlparse(value).path.lower()
    return path.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 _image_query_signature(query: str) -> str:
    return "google-custom-image:" + " ".join(query.lower().split())


def _web_query_signature(query: str) -> str:
    return "google-custom-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 _unique_texts(values: Iterable[str]) -> list[str]:
    seen: set[str] = set()
    result: list[str] = []
    for value in values:
        text = str(value).strip()
        if not text or text in seen:
            continue
        seen.add(text)
        result.append(text)
    return result


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