#!/usr/bin/env python3
"""Google search helper for brt -browse."""

from __future__ import annotations

import base64
import html
import json
import os
import re
import shutil
import subprocess
import sys
import urllib.parse

USER_AGENT = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
SCRAPE_USER_AGENT = "Mozilla/5.0"
MAX_RESULTS = 25
GOOGLE_CSE_URL = "https://www.googleapis.com/customsearch/v1"
SERPER_URL = "https://google.serper.dev/search"


def _fetch(
    url: str,
    headers: dict | None = None,
    data: bytes | None = None,
    *,
    user_agent: str | None = None,
) -> str:
    curl = shutil.which("curl")
    if not curl:
        raise RuntimeError("curl is required for brt -browse")

    agent = user_agent or (headers or {}).get("User-Agent") or USER_AGENT
    cmd = [
        curl,
        "-fsSL",
        "--max-time",
        "15",
        "-A",
        agent,
    ]
    for key, value in (headers or {}).items():
        if key.lower() in ("user-agent", "cookie", "accept-language", "content-type", "x-api-key"):
            continue
        cmd.extend(["-H", f"{key}: {value}"])
    for key, value in (headers or {}).items():
        if key.lower() == "cookie":
            cmd.extend(["-H", f"Cookie: {value}"])
        elif key.lower() == "accept-language":
            cmd.extend(["-H", f"Accept-Language: {value}"])
        elif key.lower() == "content-type":
            cmd.extend(["-H", f"Content-Type: {value}"])
        elif key.lower() == "x-api-key":
            cmd.extend(["-H", f"X-API-KEY: {value}"])

    if data is not None:
        cmd.extend(["-X", "POST", "--data-binary", "@-"])
        proc = subprocess.run(
            cmd + [url],
            input=data,
            capture_output=True,
            check=False,
        )
    else:
        proc = subprocess.run(cmd + [url], capture_output=True, check=False)

    if proc.returncode != 0:
        err = proc.stderr.decode("utf-8", errors="replace").strip()
        raise RuntimeError(err or f"curl failed ({proc.returncode})")
    return proc.stdout.decode("utf-8", errors="replace")


def _clean_title(raw: str) -> str:
    text = re.sub(r"<[^>]+>", "", raw)
    return html.unescape(text).strip()


def _normalize_url(raw: str) -> str | None:
    url = html.unescape(raw).strip()
    if url.startswith("//"):
        url = "https:" + url
    if url.startswith("/url?"):
        parsed = urllib.parse.urlparse(url)
        qs = urllib.parse.parse_qs(parsed.query)
        url = qs.get("q", qs.get("url", [""]))[0]
    url = urllib.parse.unquote(url)
    if not url.startswith(("http://", "https://")):
        return None
    if any(
        skip in url
        for skip in (
            "google.com/search",
            "google.com/url",
            "webcache.googleusercontent.com",
            "accounts.google.com",
            "support.google.com",
            "policies.google.com",
        )
    ):
        return None
    return url


def _dedupe_results(items: list[dict]) -> list[dict]:
    seen: set[str] = set()
    out: list[dict] = []
    for item in items:
        url = item.get("url", "")
        if not url or url in seen:
            continue
        seen.add(url)
        title = item.get("title", "").strip() or url
        out.append({"title": title, "url": url})
    return out


def search_google_cse(query: str, offset: int, limit: int) -> list[dict]:
    api_key = os.environ.get("BRT_GOOGLE_API_KEY", "").strip()
    cx = os.environ.get("BRT_GOOGLE_CX", "").strip()
    if not api_key or not cx:
        return []

    want = min(MAX_RESULTS, offset + limit)
    results: list[dict] = []
    start = 1  # Google CSE is 1-indexed
    while len(results) < want:
        batch = min(10, want - len(results))
        params = urllib.parse.urlencode(
            {
                "key": api_key,
                "cx": cx,
                "q": query,
                "num": batch,
                "start": start,
            }
        )
        payload = json.loads(_fetch(f"{GOOGLE_CSE_URL}?{params}"))
        items = payload.get("items") or []
        if not items:
            break
        for item in items:
            results.append({"title": item.get("title", ""), "url": item.get("link", "")})
        start += len(items)
        if len(items) < batch:
            break

    return results[offset : offset + limit]


def search_serper(query: str, offset: int, limit: int) -> list[dict]:
    api_key = os.environ.get("BRT_SERPER_API_KEY", "").strip()
    if not api_key:
        return []

    page = offset // 10 + 1
    body = json.dumps({"q": query, "num": min(10, limit), "page": page}).encode()
    headers = {
        "X-API-KEY": api_key,
        "Content-Type": "application/json",
        "User-Agent": USER_AGENT,
    }
    payload = json.loads(_fetch(SERPER_URL, headers=headers, data=body))
    organic = payload.get("organic") or []
    results = [{"title": item.get("title", ""), "url": item.get("link", "")} for item in organic]
    inner_offset = offset % 10
    return results[inner_offset : inner_offset + limit]


def search_google_html(query: str, offset: int, limit: int) -> list[dict]:
    want = min(MAX_RESULTS, offset + limit)
    results: list[dict] = []
    start = 0

    while len(results) < want:
        params = urllib.parse.urlencode(
            {
                "q": query,
                "num": 10,
                "hl": "en",
                "pws": "0",
                "start": start,
                "gbv": "1",
                "filter": "0",
            }
        )
        url = f"https://www.google.com/search?{params}"
        headers = {
            "User-Agent": SCRAPE_USER_AGENT,
            "Accept-Language": "en-US,en;q=0.9",
            "Cookie": "CONSENT=YES+; SOCS=CAI",
        }
        try:
            page = _fetch(url, headers=headers)
        except (RuntimeError, OSError):
            break

        batch = _parse_google_html(page)
        if not batch:
            break
        results.extend(batch)
        start += 10
        if len(batch) < 5 or start >= MAX_RESULTS:
            break

    return _dedupe_results(results)[offset : offset + limit]


def _parse_google_html(page: str) -> list[dict]:
    results: list[dict] = []

    for block in re.findall(r"<div[^>]+class=\"[^\"]*\bg\b[^\"]*\"[^>]*>(.*?)</div>\s*</div>", page, re.S | re.I):
        title_match = re.search(r"<h3[^>]*>(.*?)</h3>", block, re.S | re.I)
        if not title_match:
            continue
        link_match = re.search(r"<a[^>]+href=\"([^\"]+)\"[^>]*>", block, re.I)
        if not link_match:
            continue
        url = _normalize_url(link_match.group(1))
        if not url:
            continue
        results.append({"title": _clean_title(title_match.group(1)), "url": url})

    if results:
        return results

    titles = re.findall(r"<h3[^>]*>(.*?)</h3>", page, re.S | re.I)
    links = re.findall(r"<a[^>]+href=\"(/url\?q=[^\"]+|https?://[^\"]+)\"[^>]*>", page, re.I)
    for link in links:
        url = _normalize_url(link)
        if not url:
            continue
        title = _clean_title(titles[len(results)]) if len(results) < len(titles) else url
        results.append({"title": title, "url": url})
        if len(results) >= 10:
            break

    return results


def search_ddg_html(query: str, offset: int, limit: int) -> list[dict]:
    want = min(MAX_RESULTS, offset + limit)
    results: list[dict] = []
    page = offset // 10

    while len(results) < want:
        params = urllib.parse.urlencode({"q": query, "s": page * 10})
        url = f"https://html.duckduckgo.com/html/?{params}"
        headers = {"User-Agent": SCRAPE_USER_AGENT}
        try:
            page_html = _fetch(url, headers=headers)
        except (RuntimeError, OSError):
            break

        batch = _parse_ddg_html(page_html)
        if not batch:
            break
        results.extend(batch)
        page += 1
        if len(batch) < 5 or page * 10 >= MAX_RESULTS:
            break

    return _dedupe_results(results)[offset : offset + limit]


def _decode_bing_redirect(href: str) -> str | None:
    href = html.unescape(href).strip()
    parsed = urllib.parse.urlparse(href)
    encoded = urllib.parse.parse_qs(parsed.query).get("u", [""])[0]
    if not encoded:
        return href if href.startswith(("http://", "https://")) else None
    payload = encoded[2:] if len(encoded) > 2 else encoded
    try:
        pad = "=" * (-len(payload) % 4)
        return base64.b64decode(payload + pad).decode("utf-8", errors="replace")
    except (ValueError, UnicodeDecodeError):
        return None


def search_bing_html(query: str, offset: int, limit: int) -> list[dict]:
    want = min(MAX_RESULTS, offset + limit)
    params = urllib.parse.urlencode({"q": query, "count": 10, "first": 1})
    url = f"https://www.bing.com/search?{params}"
    headers = {"User-Agent": SCRAPE_USER_AGENT}
    try:
        page = _fetch(url, headers=headers)
    except (RuntimeError, OSError):
        return []

    return _dedupe_results(_parse_bing_html(page))[offset : offset + limit]


def search_brave_html(query: str, offset: int, limit: int) -> list[dict]:
    want = min(MAX_RESULTS, offset + limit)
    params = urllib.parse.urlencode({"q": query})
    url = f"https://search.brave.com/search?{params}"
    headers = {"User-Agent": SCRAPE_USER_AGENT}
    try:
        page = _fetch(url, headers=headers)
    except (RuntimeError, OSError):
        return []

    return _dedupe_results(_parse_brave_html(page))[:want][offset : offset + limit]


def _parse_brave_html(page: str) -> list[dict]:
    results: list[dict] = []
    for part in page.split('data-type="web"')[1:]:
        link_match = re.search(r'<a[^>]+href="(https?://[^"]+)"', part)
        if not link_match:
            continue
        url = link_match.group(1)
        if "search.brave.com" in url:
            continue
        title_match = re.search(r'class="title[^"]*"[^>]*>([\s\S]*?)</', part)
        if title_match:
            title = _clean_title(title_match.group(1))
        else:
            title = url
        if title:
            results.append({"title": title, "url": url})
    return results


def search_web_scrape(query: str, offset: int, limit: int) -> list[dict]:
    want = min(MAX_RESULTS, offset + limit)
    merged: list[dict] = []
    for search_fn in (search_brave_html, search_bing_html):
        try:
            batch = search_fn(query, 0, want)
        except Exception:
            batch = []
        merged.extend(batch)
        merged = _dedupe_results(merged)
        if len(merged) >= want:
            break
    return merged[offset : offset + limit]


def _parse_bing_html(page: str) -> list[dict]:
    results: list[dict] = []
    for match in re.finditer(r"<h2[^>]*>([\s\S]*?)</h2>", page, re.I):
        link_match = re.search(
            r'<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)</a>',
            match.group(1),
            re.I,
        )
        if not link_match:
            continue
        title = _clean_title(link_match.group(2))
        if not title:
            continue
        url = _decode_bing_redirect(link_match.group(1))
        if not url:
            continue
        url = _normalize_url(url) or url
        results.append({"title": title, "url": url})
    return results


def _parse_ddg_html(page: str) -> list[dict]:
    results: list[dict] = []
    for match in re.finditer(
        r'class="result__a"[^>]*href="([^"]+)"[^>]*>(.*?)</a>',
        page,
        re.S | re.I,
    ):
        href = match.group(1)
        title = _clean_title(match.group(2))
        if "duckduckgo.com/y.js" in href or not title:
            continue
        if href.startswith("//"):
            href = "https:" + href
        if "uddg=" in href:
            parsed = urllib.parse.urlparse(href)
            href = urllib.parse.unquote(
                urllib.parse.parse_qs(parsed.query).get("uddg", [""])[0]
            )
        url = _normalize_url(href)
        if url:
            results.append({"title": title, "url": url})
    return results


def search_all(query: str, max_results: int = MAX_RESULTS) -> list[dict]:
    query = query.strip()
    if not query:
        return []

    max_results = max(1, min(MAX_RESULTS, max_results))
    for backend in (search_google_cse, search_serper, search_google_html):
        try:
            batch = backend(query, 0, max_results)
        except Exception:
            batch = []
        batch = _dedupe_results(batch)
        if batch:
            return batch[:max_results]

    for fallback, search_fn in (
        ("web", search_web_scrape),
        ("DuckDuckGo", search_ddg_html),
    ):
        try:
            batch = _dedupe_results(search_fn(query, 0, max_results))
        except Exception:
            batch = []
        if batch:
            label = "open web" if fallback == "web" else fallback
            print(
                f"note: Google unavailable; showing {label} results (set "
                "BRT_GOOGLE_API_KEY + BRT_GOOGLE_CX or BRT_SERPER_API_KEY for "
                "official Google results)",
                file=sys.stderr,
            )
            return batch[:max_results]
    return []


def main() -> int:
    if len(sys.argv) < 3 or sys.argv[1] != "search":
        print(
            "usage: browse.py search <query> [max_results]",
            file=sys.stderr,
        )
        return 1

    query = sys.argv[2]
    max_results = int(sys.argv[3]) if len(sys.argv) > 3 else MAX_RESULTS
    results = search_all(query, max_results)
    if not results:
        print(
            "error: no search results (set keys in ~/.brt/.env or run brt -config)",
            file=sys.stderr,
        )
        return 1

    json.dump(results, sys.stdout, ensure_ascii=False)
    return 0


if __name__ == "__main__":
    sys.exit(main())
