# retoor <retoor@molodetz.nl>
import argparse
import asyncio
import html
import importlib.util
import json
import re
from pathlib import Path

COMMONS_API = "https://commons.wikimedia.org/w/api.php"
OPENVERSE_API = "https://api.openverse.org/v1/images/"
TAG_PATTERN = re.compile(r"<[^>]+>")
TIMEOUT = 15.0
HEADERS = {"User-Agent": "molodetz-research/1.0 (retoor@molodetz.nl)"}


def commons_params(query, limit=10):
    return {
        "action": "query",
        "generator": "search",
        "gsrsearch": query,
        "gsrnamespace": 6,
        "gsrlimit": limit,
        "prop": "imageinfo",
        "iiprop": "url|size|mime|extmetadata",
        "format": "json",
    }


def clean_text(value):
    return html.unescape(TAG_PATTERN.sub("", value or "")).strip()


def parse_commons(payload):
    pages = (payload.get("query") or {}).get("pages") or {}
    found = []
    for page in pages.values():
        infos = page.get("imageinfo") or []
        if not infos:
            continue
        info = infos[0]
        meta = info.get("extmetadata") or {}
        license_name = clean_text((meta.get("LicenseShortName") or {}).get("value", ""))
        if not license_name or license_name.lower() == "unknown":
            continue
        found.append({
            "title": clean_text(page.get("title", "")),
            "file_url": info.get("url", ""),
            "source_url": info.get("descriptionurl", ""),
            "creator": clean_text((meta.get("Artist") or {}).get("value", "")) or "unknown artist",
            "license": license_name,
            "license_url": clean_text((meta.get("LicenseUrl") or {}).get("value", "")),
            "mime": info.get("mime", ""),
            "width": info.get("width", 0),
            "height": info.get("height", 0),
        })
    return found


def credit(item):
    return f"{item['title']} by {item['creator']}, {item['license']} ({item['license_url'] or item['source_url']})"


def search_commons(query, limit=10):
    import httpx

    response = httpx.get(COMMONS_API, params=commons_params(query, limit), headers=HEADERS, timeout=TIMEOUT)
    response.raise_for_status()
    return parse_commons(response.json())


def search_openverse(query, limit=10):
    import httpx

    response = httpx.get(OPENVERSE_API, params={"q": query, "page_size": limit}, headers=HEADERS, timeout=TIMEOUT)
    response.raise_for_status()
    found = []
    for row in (response.json().get("results") or []):
        if not row.get("license"):
            continue
        found.append({
            "title": (row.get("title") or "").strip(),
            "file_url": row.get("url", ""),
            "source_url": row.get("foreign_landing_url", ""),
            "creator": (row.get("creator") or "unknown artist").strip(),
            "license": row.get("license", ""),
            "license_url": row.get("license_url", ""),
            "mime": "",
            "width": row.get("width", 0),
            "height": row.get("height", 0),
        })
    return found


def load_fetch():
    path = Path(__file__).with_name("fetch.py")
    spec = importlib.util.spec_from_file_location("images_fetch", path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


def download(url, dest, timeout=TIMEOUT, referer="", session=None):
    from stealthii import Stealthii

    headers = {"Referer": referer} if referer else None

    async def once():
        stealth = Stealthii()
        await stealth.start()
        try:
            return await stealth.download(url, headers=headers, timeout=timeout, session=session)
        finally:
            await stealth.shutdown()

    body = asyncio.run(once())
    if not body:
        raise RuntimeError(f"empty download from {url}")
    out = Path(dest)
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_bytes(body)
    return str(out)


def fetch_many(records, out_dir, jobs=10, timeout=TIMEOUT, budget=60.0, fetch_bytes=None):
    fetch = load_fetch()
    items = [{
        "url": row.get("file_url", ""),
        "kind": "file",
        "name": row.get("file", ""),
        "referer": row.get("source_url") or row.get("page_url") or "",
    } for row in records]
    if fetch_bytes is None:
        manifest = asyncio.run(fetch.run_live(items, out_dir, jobs, timeout, budget))
    else:
        manifest = asyncio.run(fetch.gather_batch(items, out_dir, jobs, timeout, fetch_bytes))
    by_url = {row["url"]: row for row in manifest}
    out = []
    for row in records:
        hit = by_url.get(row.get("file_url", ""), {})
        out.append(dict(row, path=hit.get("path", ""), bytes=hit.get("bytes", 0),
                        download_ok=hit.get("ok", False), download_error=hit.get("error", ""),
                        credit=credit(row)))
    return out


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Search free image APIs and format credit lines.")
    modes = parser.add_subparsers(dest="mode", required=True)
    parse_mode = modes.add_parser("parse", help="Parse a saved Commons API response into credits")
    parse_mode.add_argument("fixture", help="JSON file holding a Commons API response")
    search_mode = modes.add_parser("search", help="Query Wikimedia Commons live")
    search_mode.add_argument("query", help="Image search query")
    search_mode.add_argument("--limit", type=int, default=10, help="Maximum images to return")
    fetch_mode = modes.add_parser("fetch", help="Download one image URL to a local file")
    fetch_mode.add_argument("url", help="Direct image URL")
    fetch_mode.add_argument("dest", help="Local destination path")
    fetch_mode.add_argument("--referer", default="", help="Referer header for hotlink-guarded hosts")
    fetch_mode.add_argument("--timeout", type=float, default=TIMEOUT, help="Seconds for the download")
    many_mode = modes.add_parser("fetch-many", help="Download many images concurrently from a JSON records file")
    many_mode.add_argument("records", help="JSON file holding [{file_url, file?, title, creator, license, ...}]")
    many_mode.add_argument("--out-dir", default="images", help="Directory for downloaded bytes")
    many_mode.add_argument("--jobs", type=int, default=10, help="Concurrent downloads, capped at 20; per host capped at 6")
    many_mode.add_argument("--budget", type=float, default=60.0, help="Seconds for the whole batch")
    args = parser.parse_args()
    if args.mode == "parse":
        with open(args.fixture, encoding="utf-8") as handle:
            items = parse_commons(json.load(handle))
    elif args.mode == "search":
        items = search_commons(args.query, args.limit)
    elif args.mode == "fetch":
        print(json.dumps({"path": download(args.url, args.dest, timeout=args.timeout, referer=args.referer)}))
        raise SystemExit(0)
    else:
        with open(args.records, encoding="utf-8") as handle:
            records = json.load(handle)
        print(json.dumps(fetch_many(records, args.out_dir, jobs=args.jobs, budget=args.budget)))
        raise SystemExit(0)
    print(json.dumps([dict(item, credit=credit(item)) for item in items]))
