#!/usr/bin/env python3
"""
Gemini Image Generator — Primary: gemini-3.1-flash-image-preview @ 2K
Fallback: DALL-E 3 via OpenAI API

Usage:
  python3 gen.py --prompt "USMC eagle globe anchor" --count 2
  python3 gen.py --prompt-json prompt.json
  python3 gen.py --template usmc --count 4
  python3 gen.py --fallback-only   # force DALL-E

Golden Formula (Gemini):
  [Subject+Details] + [Action] in [Setting+Environment],
  [Composition+Camera], [Lighting+Mood], [Style+Quality]
"""

import argparse
import base64
import json
import os
import sys
import time
import urllib.request
import urllib.error
from datetime import datetime
from pathlib import Path


def json_to_prompt(data: dict) -> str:
    """Convert structured JSON image request into a natural language prompt."""
    req = data.get("image_request", data)  # support both wrapped and flat
    parts = []

    # Subject
    subj = req.get("subject", {})
    if isinstance(subj, dict):
        entity = subj.get("main_entity", "")
        details = subj.get("appearance_details", "")
        action = subj.get("action", "")
        if entity:
            s = entity
            if details:
                s += f", {details}"
            if action:
                s += f", {action}"
            parts.append(s)
    elif isinstance(subj, str):
        parts.append(subj)

    # Environment
    env = req.get("environment", {})
    if isinstance(env, dict):
        loc = env.get("location", "")
        atm = env.get("atmosphere", "")
        tod = env.get("time_of_day", "")
        env_parts = [x for x in [loc, atm, tod] if x]
        if env_parts:
            parts.append("in " + ", ".join(env_parts))
    elif isinstance(env, str):
        parts.append(f"in {env}")

    # Cinematography / Composition
    cam = req.get("cinematography", req.get("composition", {}))
    if isinstance(cam, dict):
        angle = cam.get("camera_angle", "")
        shot = cam.get("shot_type", "")
        lens = cam.get("lens", "")
        dof = cam.get("depth_of_field", "")
        cam_parts = [x for x in [angle, shot, lens, dof] if x]
        if cam_parts:
            parts.append(", ".join(cam_parts))
    elif isinstance(cam, str):
        parts.append(cam)

    # Lighting & mood
    light = req.get("lighting_and_color", req.get("lighting", {}))
    if isinstance(light, dict):
        style = light.get("lighting_style", "")
        palette = light.get("color_palette", "")
        mood = light.get("mood", "")
        l_parts = [x for x in [style, palette, mood] if x]
        if l_parts:
            parts.append(", ".join(l_parts))
    elif isinstance(light, str):
        parts.append(light)

    # Technical style
    tech = req.get("technical_style", req.get("style", {}))
    if isinstance(tech, dict):
        medium = tech.get("medium", "")
        quality = tech.get("quality", "")
        t_parts = [x for x in [medium, quality] if x]
        if t_parts:
            parts.append(", ".join(t_parts))
    elif isinstance(tech, str):
        parts.append(tech)

    return ". ".join(parts) if parts else str(data)


# ── Etsy Prompt Templates (RoyalStylesCreations) ─────────────────────────────
ETSY_TEMPLATES = {
    "usmc": {
        "image_request": {
            "subject": {
                "main_entity": "Eagle Globe and Anchor emblem",
                "appearance_details": "bold relief carving, aged bronze patina, intricate detail",
                "action": "centered and prominent"
            },
            "environment": {
                "location": "plain off-white textured paper background",
                "atmosphere": "clean, print-ready",
                "time_of_day": "neutral studio lighting"
            },
            "cinematography": {
                "camera_angle": "front-facing, flat lay",
                "shot_type": "medium close-up",
                "lens": "macro lens, sharp edge-to-edge",
                "depth_of_field": "full depth, no bokeh"
            },
            "lighting_and_color": {
                "lighting_style": "soft even studio light, no harsh shadows",
                "color_palette": "black ink and off-white, or aged gold on white",
                "mood": "honorable, strong, traditional"
            },
            "technical_style": {
                "medium": "vintage military insignia illustration, woodcut engraving style",
                "quality": "8K high-resolution, print-ready, crisp lines"
            }
        }
    },
    "christian": {
        "image_request": {
            "subject": {
                "main_entity": "Open leather-bound Bible",
                "appearance_details": "worn pages, gold-edged leaves, ribbon bookmark",
                "action": "lying open, pages slightly turned by a gentle wind"
            },
            "environment": {
                "location": "rustic wooden table near a window",
                "atmosphere": "soft natural morning light streaming in",
                "time_of_day": "early morning"
            },
            "cinematography": {
                "camera_angle": "slight high-angle, three-quarter view",
                "shot_type": "medium shot",
                "lens": "50mm lens, natural perspective",
                "depth_of_field": "sharp Bible, softly blurred background"
            },
            "lighting_and_color": {
                "lighting_style": "warm golden hour window light, soft diffused",
                "color_palette": "warm browns, creams, muted gold",
                "mood": "reverent, peaceful, intimate"
            },
            "technical_style": {
                "medium": "photorealistic fine art photography",
                "quality": "8K resolution, rich texture detail, print-ready"
            }
        }
    },
    "patriotic": {
        "image_request": {
            "subject": {
                "main_entity": "American flag",
                "appearance_details": "weathered and distressed fabric, faded stars and stripes, battle-worn look",
                "action": "waving gently in wind"
            },
            "environment": {
                "location": "open sky background",
                "atmosphere": "dramatic clouds breaking apart",
                "time_of_day": "golden hour sunset"
            },
            "cinematography": {
                "camera_angle": "low-angle, looking up",
                "shot_type": "wide cinematic shot",
                "lens": "24mm wide-angle lens",
                "depth_of_field": "full focus"
            },
            "lighting_and_color": {
                "lighting_style": "dramatic god-rays breaking through clouds, warm backlight",
                "color_palette": "deep reds, navy blues, warm gold",
                "mood": "proud, resilient, timeless"
            },
            "technical_style": {
                "medium": "cinematic photorealism, high dynamic range",
                "quality": "8K resolution, print-ready"
            }
        }
    },
    "nature": {
        "image_request": {
            "subject": {
                "main_entity": "Pacific Northwest old-growth forest",
                "appearance_details": "towering Douglas firs, ferns carpeting the forest floor, moss-covered boulders",
                "action": "rays of light filtering through the canopy"
            },
            "environment": {
                "location": "dense temperate rainforest",
                "atmosphere": "light morning mist, dew on leaves",
                "time_of_day": "early morning golden hour"
            },
            "cinematography": {
                "camera_angle": "eye-level, standing in the forest",
                "shot_type": "wide immersive shot",
                "lens": "35mm wide-angle",
                "depth_of_field": "foreground ferns sharp, trees softly layered into distance"
            },
            "lighting_and_color": {
                "lighting_style": "volumetric god-rays through canopy, dappled light",
                "color_palette": "deep greens, earthy browns, soft gold light",
                "mood": "serene, majestic, alive"
            },
            "technical_style": {
                "medium": "National Geographic nature photography",
                "quality": "8K resolution, hyper-detailed, print-ready"
            }
        }
    },
}

# ── Config ────────────────────────────────────────────────────────────────────
GEMINI_MODEL    = "gemini-3.1-flash-image-preview"
GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
DALLE_MODEL     = "dall-e-3"

# Aspect ratio → Gemini aspectRatio param
ASPECT_MAP = {
    "1:1":   "1:1",
    "square": "1:1",
    "16:9":  "16:9",
    "landscape": "16:9",
    "9:16":  "9:16",
    "portrait": "9:16",
    "4:3":   "4:3",
    "3:4":   "3:4",
}

# Aspect ratio → closest DALL-E 3 size for fallback
DALLE_SIZE_MAP = {
    "1:1":  "1024x1024",
    "16:9": "1792x1024",
    "9:16": "1024x1792",
    "4:3":  "1792x1024",
    "3:4":  "1024x1792",
}


def get_gemini_key():
    key = os.environ.get("GEMINI_API_KEY", "")
    if not key:
        # Try reading from openclaw.json directly
        cfg = Path.home() / ".openclaw" / "openclaw.json"
        if cfg.exists():
            try:
                data = json.loads(cfg.read_text())
                key = data.get("env", {}).get("vars", {}).get("GEMINI_API_KEY", "")
            except Exception:
                pass
    return key


def get_openai_key():
    key = os.environ.get("OPENAI_API_KEY", "") or os.environ.get("OPENAI_DALLE_API_KEY", "")
    if not key:
        cfg = Path.home() / ".openclaw" / "openclaw.json"
        if cfg.exists():
            try:
                data = json.loads(cfg.read_text())
                key = (
                    data.get("env", {}).get("vars", {}).get("OPENAI_DALLE_API_KEY", "")
                    or data.get("skills", {}).get("entries", {}).get("openai-image-gen", {}).get("apiKey", "")
                )
            except Exception:
                pass
    return key


def gemini_generate(prompt, size="2K", aspect="1:1", api_key=None):
    """Call Gemini image generation. Returns list of (png_bytes, mime_type)."""
    key = api_key or get_gemini_key()
    if not key:
        raise RuntimeError("GEMINI_API_KEY not found")

    url = f"{GEMINI_API_BASE}/{GEMINI_MODEL}:generateContent?key={key}"
    payload = {
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {
            "responseModalities": ["IMAGE", "TEXT"],
            "imageConfig": {
                "aspectRatio": ASPECT_MAP.get(aspect, "1:1"),
                "imageSize": size,
            }
        }
    }
    body = json.dumps(payload).encode()
    req = urllib.request.Request(url, data=body,
                                  headers={"Content-Type": "application/json"},
                                  method="POST")

    # Retry up to 2 times on 429 with backoff (free tier = ~2 RPM)
    last_err = None
    for attempt in range(3):
        try:
            with urllib.request.urlopen(req, timeout=120) as resp:
                data = json.loads(resp.read())
            break  # success
        except urllib.error.HTTPError as e:
            if e.code == 429 and attempt < 2:
                wait = 30 * (attempt + 1)  # 30s, 60s
                print(f"  ↻ Gemini rate limited, retrying in {wait}s...", flush=True)
                time.sleep(wait)
                last_err = e
            else:
                raise
    else:
        raise last_err

    images = []
    for candidate in data.get("candidates", []):
        for part in candidate.get("content", {}).get("parts", []):
            if "inlineData" in part:
                mime = part["inlineData"].get("mimeType", "image/png")
                raw = base64.b64decode(part["inlineData"]["data"])
                images.append((raw, mime))
    if not images:
        raise RuntimeError(f"Gemini returned no images. Response: {json.dumps(data)[:400]}")
    return images


def dalle_generate(prompt, aspect="1:1", api_key=None):
    """Call DALL-E 3. Returns list of (png_bytes, mime_type)."""
    key = api_key or get_openai_key()
    if not key:
        raise RuntimeError("OpenAI API key not found")

    size = DALLE_SIZE_MAP.get(aspect, "1024x1024")
    url = "https://api.openai.com/v1/images/generations"
    payload = {
        "model": DALLE_MODEL,
        "prompt": prompt,
        "n": 1,
        "size": size,
        "quality": "hd",
        "response_format": "b64_json",
    }
    body = json.dumps(payload).encode()
    req = urllib.request.Request(url, data=body,
                                  headers={
                                      "Content-Type": "application/json",
                                      "Authorization": f"Bearer {key}",
                                  },
                                  method="POST")
    with urllib.request.urlopen(req, timeout=120) as resp:
        data = json.loads(resp.read())

    images = []
    for item in data.get("data", []):
        if "b64_json" in item:
            raw = base64.b64decode(item["b64_json"])
            images.append((raw, "image/png"))
    if not images:
        raise RuntimeError(f"DALL-E returned no images. Response: {json.dumps(data)[:400]}")
    return images


def save_images(images, out_dir, prefix="img", ext_override=None):
    """Save images to out_dir, return list of saved paths."""
    out_dir = Path(out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    paths = []
    for i, (raw, mime) in enumerate(images):
        ext = ext_override or ("png" if "png" in mime else "jpg")
        fname = out_dir / f"{prefix}_{i+1:03d}.{ext}"
        fname.write_bytes(raw)
        paths.append(fname)
    return paths


def build_gallery(out_dir, image_paths, prompt, source):
    """Write a simple index.html gallery."""
    items = ""
    for p in image_paths:
        items += f'<div class="item"><img src="{p.name}" alt="generated"><p class="name">{p.name}</p></div>\n'

    html = f"""<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Image Gallery</title>
<style>
body {{ font-family: sans-serif; background: #111; color: #eee; margin: 0; padding: 20px; }}
h1 {{ font-size: 1.2rem; color: #aaa; }}
.prompt {{ background: #222; padding: 10px; border-radius: 6px; margin-bottom: 20px; font-size: 0.9rem; }}
.grid {{ display: flex; flex-wrap: wrap; gap: 12px; }}
.item {{ background: #1a1a1a; border-radius: 8px; overflow: hidden; max-width: 320px; }}
.item img {{ width: 100%; display: block; }}
.name {{ padding: 6px 10px; font-size: 0.75rem; color: #888; }}
.source {{ position: fixed; bottom: 10px; right: 14px; font-size: 0.75rem; color: #555; }}
</style>
</head>
<body>
<h1>Generated Images</h1>
<div class="prompt">🖼️ {prompt}</div>
<div class="grid">{items}</div>
<div class="source">via {source}</div>
</body>
</html>"""
    (Path(out_dir) / "index.html").write_text(html)


def main():
    parser = argparse.ArgumentParser(description="Gemini Image Generator (DALL-E fallback)")
    prompt_group = parser.add_mutually_exclusive_group(required=True)
    prompt_group.add_argument("--prompt", "-p", help="Natural language image prompt")
    prompt_group.add_argument("--prompt-json", metavar="FILE_OR_JSON",
                        help="JSON file path or inline JSON string with structured image_request")
    prompt_group.add_argument("--template", "-t", choices=list(ETSY_TEMPLATES.keys()),
                        help=f"Etsy template shortcut: {', '.join(ETSY_TEMPLATES.keys())}")
    parser.add_argument("--count", "-n", type=int, default=1,
                        help="Number of images to generate")
    parser.add_argument("--size", default="2K",
                        choices=["1K", "2K"], help="Image resolution (default: 2K)")
    parser.add_argument("--aspect", default="1:1",
                        choices=list(ASPECT_MAP.keys()), help="Aspect ratio (default: 1:1)")
    parser.add_argument("--out-dir", default=None,
                        help="Output directory (default: ~/Projects/tmp/gemini-image-TIMESTAMP)")
    parser.add_argument("--fallback-only", action="store_true",
                        help="Skip Gemini, use DALL-E directly")
    parser.add_argument("--no-gallery", action="store_true",
                        help="Skip writing index.html")
    parser.add_argument("--quiet", "-q", action="store_true",
                        help="Suppress progress output")
    parser.add_argument("--show-prompt", action="store_true",
                        help="Print the resolved prompt before generating")
    args = parser.parse_args()

    # Resolve prompt from template or JSON
    if args.template:
        template_data = ETSY_TEMPLATES[args.template]
        prompt = json_to_prompt(template_data)
        if not args.quiet:
            print(f"📋 Template: {args.template}")
    elif args.prompt_json:
        src = args.prompt_json
        if src.strip().startswith("{"):
            data = json.loads(src)
        else:
            data = json.loads(Path(src).read_text())
        prompt = json_to_prompt(data)
    else:
        prompt = args.prompt

    if args.show_prompt or (args.template and not args.quiet):
        print(f"🖊  Prompt: {prompt}\n")

    # Resolve output dir (prompt defined above for template/json paths)
    if not (args.template or args.prompt_json):
        prompt = args.prompt  # already set for --prompt, re-affirm

    if args.out_dir:
        out_dir = Path(args.out_dir)
    else:
        ts = datetime.now().strftime("%Y%m%d-%H%M%S")
        base = Path.home() / "Projects" / "tmp"
        if not base.exists():
            base = Path("/tmp")
        out_dir = base / f"gemini-image-{ts}"

    all_images = []
    source = "unknown"

    for i in range(args.count):
        if not args.quiet:
            print(f"[{i+1}/{args.count}] Generating image...", flush=True)

        images = None
        # Try Gemini first (unless --fallback-only)
        if not args.fallback_only:
            try:
                images = gemini_generate(args.prompt, size=args.size, aspect=args.aspect)
                source = f"Gemini {GEMINI_MODEL} @ {args.size}"
                if not args.quiet:
                    print(f"  ✓ Gemini ({len(images)} image(s))", flush=True)
            except Exception as e:
                print(f"  ⚠ Gemini failed: {e}", file=sys.stderr)
                print(f"  → Falling back to DALL-E 3...", file=sys.stderr)

        # Fallback to DALL-E
        if images is None:
            try:
                images = dalle_generate(args.prompt, aspect=args.aspect)
                source = f"DALL-E 3 (fallback)"
                if not args.quiet:
                    print(f"  ✓ DALL-E fallback ({len(images)} image(s))", flush=True)
            except Exception as e:
                print(f"  ✗ DALL-E also failed: {e}", file=sys.stderr)
                sys.exit(1)

        all_images.extend(images)
        # Small delay between calls
        if i < args.count - 1:
            time.sleep(1)

    # Save
    saved = save_images(all_images, out_dir)
    if not args.quiet:
        print(f"\nSaved {len(saved)} image(s) to: {out_dir}")

    # Gallery
    if not args.no_gallery:
        build_gallery(out_dir, saved, args.prompt, source)
        if not args.quiet:
            print(f"Gallery: {out_dir}/index.html")

    # Print paths for programmatic use
    for p in saved:
        print(str(p))


if __name__ == "__main__":
    main()
