"""Generate one on-brand Swipix image via Gemini image-gen ("nanobanana 2").

This is the SAME approach used to produce the 10 approved posts in
images/approved-posts/ — a direct call to the Gemini generateContent endpoint,
feeding it 1-3 real reference photos (images/brand-style-refs/) plus a single
prose mega-prompt (see ../PROMPT_TEMPLATES.md and the
crm-smm-visual-director skill for how to write that prompt), then center-cropping
the result to a clean square JPEG.

Setup:
  pip install -r requirements.txt
  Put your API key in ../keys/smm_gemini_api_key.txt (one line, no quotes) —
  see ../GOOGLE_AI_SETUP.md for where that key comes from.

Usage:
  python generate_image.py --prompt "full mega-prompt text..." --out out.jpg
  python generate_image.py --prompt "..." --ref ../images/brand-style-refs/fashion-editorial/ref-fashion-03.jpg --out out.jpg
  python generate_image.py --prompt-file my_prompt.txt --out out.jpg --ref a.jpg --ref b.jpg

Reference images: pass up to 3 with repeated --ref flags. More than 3 references
tends to confuse the model rather than help it — pick the 1-3 photos closest to
the scene you want.
"""
import argparse
import base64
import io
import os
import time

import requests
from PIL import Image

API = "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
MODELS = ["gemini-3-pro-image", "gemini-3.1-flash-image"]
MAX_BYTES = 900_000
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_KEY_FILE = os.path.join(HERE, "..", "keys", "smm_gemini_api_key.txt")
SOFTEN = " Wholesome, positive, fully brand-safe advertising image."


def load_key(key_file):
    key_file = key_file or DEFAULT_KEY_FILE
    if os.path.exists(key_file):
        key = open(key_file, encoding="utf-8").readline().strip()
        if key:
            return key
    env = os.environ.get("GEMINI_API_KEY")
    if env:
        return env
    raise SystemExit(
        f"No API key found. Put it in {key_file} (one line) or set the "
        f"GEMINI_API_KEY environment variable. See ../GOOGLE_AI_SETUP.md."
    )


def extract_image(resp_json):
    for cand in resp_json.get("candidates", []):
        for part in cand.get("content", {}).get("parts", []):
            inline = part.get("inlineData") or part.get("inline_data")
            mime = inline.get("mimeType", inline.get("mime_type", "")) if inline else ""
            if inline and mime.startswith("image/"):
                return base64.b64decode(inline["data"])
    return None


def ref_part(path):
    im = Image.open(path).convert("RGB")
    im.thumbnail((900, 900), Image.LANCZOS)
    buf = io.BytesIO()
    im.save(buf, "JPEG", quality=88)
    return {"inlineData": {"mimeType": "image/jpeg", "data": base64.b64encode(buf.getvalue()).decode()}}


def postprocess(raw, out_path, square=True):
    im = Image.open(io.BytesIO(raw)).convert("RGB")
    if square:
        w, h = im.size
        s = min(w, h)
        left, top = (w - s) // 2, (h - s) // 2
        im = im.crop((left, top, left + s, top + s))
        im = im.resize((1536, 1536), Image.LANCZOS)
    q = 90
    buf = io.BytesIO()
    while q >= 55:
        buf = io.BytesIO()
        im.save(buf, "JPEG", quality=q, optimize=True)
        if buf.tell() <= MAX_BYTES:
            break
        q -= 6
    with open(out_path, "wb") as f:
        f.write(buf.getvalue())
    return len(buf.getvalue())


def generate(prompt, refs, out_path, key, aspect="1:1", size="2K"):
    ref_parts = [ref_part(p) for p in (refs or [])[:3]]
    for model in MODELS:
        cfg = {"responseModalities": ["TEXT", "IMAGE"], "imageConfig": {"aspectRatio": aspect, "imageSize": size}}
        p = prompt
        for attempt in range(5):
            body = {"contents": [{"parts": ref_parts + [{"text": p}]}], "generationConfig": cfg}
            try:
                r = requests.post(API.format(model=model), json=body, headers={"x-goog-api-key": key}, timeout=240)
            except requests.RequestException as e:
                print(f"  [warn] {model} attempt {attempt}: {e}")
                time.sleep(10 * (attempt + 1))
                continue
            if r.status_code == 200:
                img = extract_image(r.json())
                if img:
                    n = postprocess(img, out_path, square=(aspect == "1:1"))
                    print(f"  ok via {model} ({n} bytes) -> {out_path}")
                    return True
                print(f"  [warn] {model} returned no image, retrying with a softened prompt")
                p = p + SOFTEN
                continue
            if r.status_code == 400 and "imageSize" in r.text:
                cfg = {"responseModalities": ["TEXT", "IMAGE"], "imageConfig": {"aspectRatio": aspect}}
                continue
            print(f"  [warn] {model} http {r.status_code}: {r.text[:200]}")
            if r.status_code in (429, 500, 503):
                time.sleep([8, 20, 40, 70, 110][attempt])
                continue
            break
    return False


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--prompt", help="Full mega-prompt text")
    ap.add_argument("--prompt-file", help="Path to a text file containing the prompt (alternative to --prompt)")
    ap.add_argument("--out", required=True, help="Output JPEG path")
    ap.add_argument("--ref", action="append", default=[], help="Reference image path (repeat up to 3 times)")
    ap.add_argument("--key-file", help=f"Default: {DEFAULT_KEY_FILE}")
    ap.add_argument("--aspect", default="1:1", help="1:1 (default), 4:5, 16:9, ...")
    ap.add_argument("--size", default="2K")
    args = ap.parse_args()

    if not args.prompt and not args.prompt_file:
        raise SystemExit("Pass --prompt \"...\" or --prompt-file path.txt")
    prompt = args.prompt or open(args.prompt_file, encoding="utf-8").read()

    key = load_key(args.key_file)
    ok = generate(prompt, args.ref, args.out, key, aspect=args.aspect, size=args.size)
    if not ok:
        raise SystemExit("FAILED to generate an image after retries — see warnings above.")


if __name__ == "__main__":
    main()
