#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "httpx>=0.27.0",
#     "python-dotenv>=1.0.0",
# ]
# ///
"""
Generate plan images via OpenRouter (Gemini 3 image-capable models).

Drop-in alternative to generate_gpt_image.py — same CLI signature so the
planf3 workflows need no changes. Uses your existing OPENROUTER_API_KEY
instead of a paid OpenAI key.

Usage:
    python generate_or_image.py "prompt" output.png [options]

Examples:
    python generate_or_image.py "A sunset over mountains" sunset.png
    python generate_or_image.py "Wide architecture diagram" wide.png --size 1536x1024 --quality high

Environment:
    OPENROUTER_API_KEY - Required (get from https://openrouter.ai/settings/keys)
"""

import argparse
import base64
import json
import os
import re
import sys
from pathlib import Path

import httpx
from dotenv import load_dotenv

load_dotenv(Path.cwd() / ".env")

# OpenRouter image-capable models. Gemini 3 Flash Image is cheap + fast;
# Pro Image is higher quality. Pick via --model or OPENROUTER_IMAGE_MODEL env.
DEFAULT_MODEL = os.environ.get("OPENROUTER_IMAGE_MODEL", "google/gemini-3.1-flash-image")
API_BASE = "https://openrouter.ai/api/v1/chat/completions"

# OpenRouter uses this to attribute usage in their dashboard.
HTTP_REFERER = os.environ.get("OPENROUTER_REFERER", "https://github.com/disler/planf3")
APP_TITLE = "planf3"


def parse_size(size: str) -> str:
    """Validate and normalize the size argument. We pass it through to the model
    via the prompt; OpenRouter image models derive dimensions from the prompt
    context, so we keep the explicit size in the request text as a hint."""
    if size == "auto":
        return "auto"
    if not re.match(r"^\d+x\d+$", size):
        raise ValueError(f"invalid size '{size}' — expected WxH (e.g. 1536x1024) or 'auto'")
    return size


def generate(prompt: str, output_path: str, size: str, quality: str, model: str) -> str:
    api_key = os.environ.get("OPENROUTER_API_KEY")
    if not api_key:
        raise EnvironmentError(
            "OPENROUTER_API_KEY environment variable not set. "
            "Get one from https://openrouter.ai/settings/keys and add to .env"
        )

    # Compose the image request. Image-capable Gemini models on OpenRouter
    # return an inline base64 image in the message content when asked.
    size_hint = f" Image dimensions: {size}." if size != "auto" else ""
    quality_hint = f" Quality: {quality}." if quality != "auto" else ""
    user_content = f"Generate a single professional, minimal image:{size_hint}{quality_hint} {prompt}"

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "HTTP-Referer": HTTP_REFERER,
        "X-Title": APP_TITLE,
    }
    payload = {
        "model": model,
        "messages": [{"role": "user", "content": user_content}],
        # request an image back
        "modalities": ["image", "text"],
        # cap output tokens to control cost — image bytes count against this.
        # Gemini image output is ~4 tokens/px so keep this modest.
        "max_tokens": 4096,
    }

    print(f"[generate_or_image] model={model} size={size} quality={quality}", file=sys.stderr)
    print(f"[generate_or_image] prompt: {prompt[:120]}{'...' if len(prompt)>120 else ''}", file=sys.stderr)

    with httpx.Client(timeout=180.0) as client:
        resp = client.post(API_BASE, headers=headers, json=payload)

    if resp.status_code != 200:
        raise RuntimeError(f"OpenRouter API error {resp.status_code}: {resp.text[:400]}")

    data = resp.json()
    message = data.get("choices", [{}])[0].get("message", {})
    content = message.get("content", "")

    # OpenRouter returns image-capable model output in two possible shapes:
    #  1. A list of content parts with type "image_url" (data URI)
    #  2. A markdown string like ![...](data:image/png;base64,...)
    b64_data = None
    mime = "image/png"

    if isinstance(content, list):
        for part in content:
            if isinstance(part, dict):
                if part.get("type") == "image_url":
                    url = part.get("image_url", {}).get("url", "")
                    b64_data, mime = _extract_data_uri(url)
                    break
    elif isinstance(content, str):
        b64_data, mime = _extract_data_uri(content)

    if not b64_data:
        raise RuntimeError(
            "No image returned by model. Response message content:\n"
            + (json.dumps(content)[:500] if content else "(empty)")
        )

    # Write the decoded bytes
    out = Path(output_path)
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_bytes(base64.b64decode(b64_data))
    print(f"[generate_or_image] wrote {out} ({out.stat().st_size} bytes, {mime})", file=sys.stderr)
    return str(out)


def _extract_data_uri(text: str):
    """Pull base64 image data out of a data: URI or a markdown image with a data URI."""
    if not text:
        return None, "image/png"
    m = re.search(r"data:(image/[a-zA-Z+]+);base64,([A-Za-z0-9+/=\s]+)", text)
    if m:
        mime = m.group(1)
        # strip any whitespace the API may have injected
        b64 = re.sub(r"\s+", "", m.group(2))
        return b64, mime
    return None, "image/png"


def main():
    ap = argparse.ArgumentParser(description="Generate a plan image via OpenRouter.")
    ap.add_argument("prompt", help="Image prompt")
    ap.add_argument("output_path", help="Where to save the PNG")
    ap.add_argument("--size", default="1536x1024", help="WxH or 'auto' (default 1536x1024)")
    ap.add_argument("--quality", default="high", choices=["auto", "low", "medium", "high"], help="Quality hint")
    ap.add_argument("--model", default=DEFAULT_MODEL, help=f"OpenRouter model id (default {DEFAULT_MODEL})")
    args = ap.parse_args()

    try:
        size = parse_size(args.size)
        generate(args.prompt, args.output_path, size, args.quality, args.model)
    except Exception as e:
        print(f"ERROR: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
