"""Nano Banana 2.1 prompts: what the most-liked ones have in common.

Reproduces https://agentcraft.dev/data/nano-banana-2-1-prompts-likes-2026-11.json from the public prompt collection
https://github.com/ai-boost/awesome-image-video-prompts (one YAML file per prompt) and each post's like count, read
from X's public embed endpoint (cdn.syndication.twimg.com), one request per second.

  git clone https://github.com/ai-boost/awesome-image-video-prompts
  git -C awesome-image-video-prompts checkout 590b532
  pip install pyyaml
  python3 nano-banana-2-1-prompts-likes-2026-11.py awesome-image-video-prompts likes-cache.json > out.json

Posts are ranked by likes against posts from the same month. With 81 prompts the quarters are small, so the
script also compares the top and bottom halves and keeps one prompt per author as a check. Prompt text is not
copied into the output; each row links to its YAML file.
"""
import datetime
import json
import math
import re
import statistics
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

import yaml

REPO, CACHE = Path(sys.argv[1]), Path(sys.argv[2])
MODEL = "Nano Banana 2.1"

LABEL = re.compile(r"^\s*[\[【]?[\w一-鿿぀-ヿ /&()-]{2,30}[\]】]?\s*[:：]")
LIST = re.compile(r"^\s*([-*•]|\d+[.)、])\s")
RATIO = re.compile(r"(?i)\b\d{1,2}\s*[:x×]\s*\d{1,2}\b|aspect ratio|vertical|horizontal|portrait orientation")
RESOLUTION = re.compile(r"(?i)\b[248]k\b|\b(?:1080|2160|4096)p?\b|ultra[- ]?hd|uhd\b")
CAMERA = re.compile(r"(?i)\b\d{2,3}\s?mm\b|\bf/\d|\blens\b|bokeh|depth of field|\biso\s?\d|shutter|wide-angle|close-up|macro")
NEGATIVE = re.compile(r"(?i)\b(no|don't|do not|avoid|without|never)\b")
KEEP = re.compile(r"(?i)\bkeep\b|\bpreserv|\bunchanged\b|\bmaintain\b|\bretain\b|\bidentical\b|\bsame (?:face|person|identity|pose|features)\b")
FACE_LOCK = re.compile(r"(?i)\b(?:face|facial|identity|likeness)\b[^.\n]{0,60}\b(?:keep|preserv|unchanged|maintain|retain|exact|same|identical)"
                       r"|\b(?:keep|preserv|maintain|retain|exact|same|identical)\w*\b[^.\n]{0,60}\b(?:face|facial|identity|likeness)\b")
JSON_START = re.compile(r'\s*(?:\{|"[A-Za-z_][\w ]{0,30}"\s*:)')
LIGHT = re.compile(r"(?i)\blight(?:ing)?\b|\bsunlight\b|\bgolden hour\b|\bbacklit\b|\brim light\b|\bshadow")
SKIN = re.compile(r"(?i)\bskin (?:texture|pores|detail)|\bpores\b|\bimperfection|\bfreckles\b|\bnatural skin\b|\bunretouched\b")
REALISM = re.compile(r"(?i)\b(?:candid|amateur|iphone|smartphone|phone (?:photo|camera|snapshot)|snapshot|film grain|flash photo|disposable camera|unposed)\b")
STYLE = {
    "photorealistic / hyper-realistic": r"(?i)\bphoto-?realistic\b|\bhyper-?realistic\b|\bultra-?realistic\b",
    "cinematic": r"(?i)\bcinematic\b",
    "8K / 4K": r"(?i)\b[48]k\b",
    "editorial": r"(?i)\beditorial\b",
}


def git(*args):
    return subprocess.run(["git", "-C", str(REPO), *args], capture_output=True, text=True, check=True).stdout


def posted(url):
    m = re.search(r"/status/(\d+)", url or "")
    if not m:
        return None, None
    ms = (int(m.group(1)) >> 22) + 1288834974657
    return m.group(1), datetime.datetime.fromtimestamp(ms / 1000, datetime.timezone.utc).date()


def likes(ids):
    """Current like count per X post id, cached in CACHE so reruns only fetch what is missing."""
    cache = json.loads(CACHE.read_text()) if CACHE.exists() else {}
    for i in [i for i in ids if i not in cache]:
        url = f"https://cdn.syndication.twimg.com/tweet-result?id={i}&token=a"
        for wait in (0, 5, 20):
            time.sleep(wait)
            try:
                d = json.loads(urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}), timeout=20).read())
                cache[i] = {"likes": d.get("favorite_count")}
                break
            except urllib.error.HTTPError as err:
                if err.code == 404:
                    cache[i] = {"likes": None}
                    break
            except Exception:
                pass
        time.sleep(1)
    CACHE.write_text(json.dumps(cache))
    return {i: v.get("likes") for i, v in cache.items()}


def row(path, e):
    p = e["prompt"].strip()
    lines = [l for l in p.split("\n") if l.strip()]
    pid, day = posted(e["source"].get("url"))
    is_json = bool(JSON_START.match(p))
    labeled = sum(1 for l in lines if LABEL.match(l)) >= 2
    structure = "json" if is_json else "labeled blocks" if labeled else "list" if any(LIST.match(l) for l in lines) else "prose"
    return {
        "id": e["id"], "postId": pid, "posted": str(day) if day else None, "month": str(day)[:7] if day else None,
        "author": e["source"].get("credit") or e["source"].get("name"), "mode": e.get("mode"), "category": e.get("category"),
        "chars": len(p), "words": len(p.split()), "lines": len(lines), "structure": structure,
        "aspectRatio": bool(RATIO.search(p)), "resolution": bool(RESOLUTION.search(p)), "cameraTerms": bool(CAMERA.search(p)),
        "negative": bool(NEGATIVE.search(p)), "keep": bool(KEEP.search(p)), "faceLock": bool(FACE_LOCK.search(p)),
        "lighting": bool(LIGHT.search(p)), "skinDetail": bool(SKIN.search(p)), "candidRealism": bool(REALISM.search(p)),
        "style": sorted(k for k, v in STYLE.items() if re.search(v, p)),
        "file": f"https://github.com/ai-boost/awesome-image-video-prompts/blob/main/{path}",
    }


FLAGS = ["aspectRatio", "resolution", "cameraTerms", "negative", "keep", "faceLock", "lighting", "skinDetail", "candidRealism"]


def summary(rows):
    n = len(rows)
    if not n:
        return {"n": 0}
    pct = lambda f: round(100 * sum(1 for r in rows if f(r)) / n)
    med = lambda k: statistics.median([r[k] for r in rows])
    out = {"n": n, "authors": len({r["author"] for r in rows}), "medianLikes": med("likes"), "medianChars": med("chars"),
           "medianWords": med("words"), "medianLines": med("lines"),
           "pctEdit": pct(lambda r: r["mode"] == "image-edit"),
           "structure": {s: pct(lambda r, s=s: r["structure"] == s) for s in ["prose", "labeled blocks", "list", "json"]},
           "categories": {c: sum(1 for r in rows if r["category"] == c) for c in sorted({r["category"] for r in rows})}}
    for k in FLAGS:
        out["pct" + k[0].upper() + k[1:]] = pct(lambda r, k=k: r[k])
    out["style"] = {s: pct(lambda r, s=s: s in r["style"]) for s in STYLE}
    return out


def ztest(a, b, f):
    """Two-proportion z for feature f between groups a and b."""
    x, y = sum(1 for r in a if f(r)), sum(1 for r in b if f(r))
    p = (x + y) / (len(a) + len(b))
    se = math.sqrt(p * (1 - p) * (1 / len(a) + 1 / len(b))) or 1
    return round((x / len(a) - y / len(b)) / se, 2)


def main():
    commit = git("rev-parse", "--short", "HEAD").strip()
    rows = []
    for path in sorted(git("ls-tree", "--name-only", "HEAD", "prompts/image/").split()):
        e = yaml.safe_load(git("show", f"HEAD:{path}"))
        if e.get("model") == MODEL and "x.com/" in e.get("source", {}).get("url", ""):
            rows.append(row(path, e))
    got = likes([r["postId"] for r in rows])
    for r in rows:
        r["likes"] = got.get(r["postId"])
    rows = [r for r in rows if r["likes"] is not None]
    for m in {r["month"] for r in rows}:
        group = sorted((r for r in rows if r["month"] == m), key=lambda r: r["likes"])
        for i, r in enumerate(group):
            r["monthPercentile"] = round(100 * i / max(len(group) - 1, 1))
    top_half = [r for r in rows if r["monthPercentile"] >= 50]
    bottom_half = [r for r in rows if r["monthPercentile"] < 50]
    best = {}
    for r in rows:
        if r["author"] not in best or r["monthPercentile"] > best[r["author"]]["monthPercentile"]:
            best[r["author"]] = r
    per_author = list(best.values())
    groups = {
        "all": summary(rows),
        "top half": summary(top_half),
        "bottom half": summary(bottom_half),
        "top quarter": summary([r for r in rows if r["monthPercentile"] >= 75]),
        "bottom quarter": summary([r for r in rows if r["monthPercentile"] <= 25]),
        "one per author, top half": summary([r for r in per_author if r["monthPercentile"] >= 50]),
        "one per author, bottom half": summary([r for r in per_author if r["monthPercentile"] < 50]),
        "text-to-image, top half": summary([r for r in top_half if r["mode"] == "text-to-image"]),
        "text-to-image, bottom half": summary([r for r in bottom_half if r["mode"] == "text-to-image"]),
    }
    tests = {k: ztest(top_half, bottom_half, lambda r, k=k: r[k]) for k in FLAGS}
    tests.update({f"structure {s}": ztest(top_half, bottom_half, lambda r, s=s: r["structure"] == s) for s in ["prose", "labeled blocks", "json"]})
    tests.update({f"style {s}": ztest(top_half, bottom_half, lambda r, s=s: s in r["style"]) for s in STYLE})
    tests["mode image-edit"] = ztest(top_half, bottom_half, lambda r: r["mode"] == "image-edit")
    a, b = [r["chars"] for r in top_half], [r["chars"] for r in bottom_half]
    tests["P(top-half prompt longer than bottom-half)"] = round(sum((x > y) + 0.5 * (x == y) for x in a for y in b) / (len(a) * len(b)), 3)
    json.dump({
        "title": "Nano Banana 2.1 prompts and their likes: per-prompt features",
        "source": f"https://github.com/ai-boost/awesome-image-video-prompts/tree/{commit}",
        "page": "https://agentcraft.dev/en/blog/nano-banana-2-1-prompts-likes-2026-11/",
        "likesFetched": str(datetime.date.today()),
        "definitions": {
            "monthPercentile": "rank by likes among Nano Banana 2.1 posts from the same month, 0 to 100",
            "structure": "json, labeled blocks (two or more Label: lines), list, or prose",
            "faceLock": "ties face, identity or likeness to keep, preserve, exact or same within one sentence",
            "keep": "says what must stay: keep, preserve, unchanged, maintain, retain, identical, same face",
            "skinDetail": "asks for skin texture, pores, freckles, imperfections or unretouched skin",
            "candidRealism": "asks for a candid or amateur look: candid, amateur, iPhone, smartphone, snapshot, film grain, flash photo, disposable camera",
            "tests": "two-proportion z between the top and bottom halves; |z| above about 2 is the usual 5% level",
        },
        "groups": groups,
        "tests": tests,
        "rows": rows,
    }, sys.stdout, ensure_ascii=False, indent=1)


if __name__ == "__main__":
    main()
