#!/usr/bin/env python3
"""
Gemini comparison test — Generate 10 multi-angle images for side-by-side
evaluation against Qwen Image Edit 2511 + Multi-Angle LoRA.

Uses GeminiEngine from batch_generate_refs.py for consistency with the
existing pipeline.

Usage:
    GOOGLE_API_KEY=xxx python3 test_gemini_comparison.py

Output:
    - 10 angle images in gemini_test/
    - gemini_test_review.html contact sheet
    - comparison.html (side-by-side with Qwen v2 results)
"""

import os
import sys
import json
import time
from pathlib import Path
from datetime import datetime
from dataclasses import field

# Add parent dirs to path so we can import from batch_generate_refs
SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPT_DIR))

from batch_generate_refs import GeminiEngine, GenerationJob

# ── Config ──────────────────────────────────────────────────────────

HERO_IMAGE = SCRIPT_DIR.parent.parent / \
    "leviathan/visual/refs/characters/heroes/Jinx_Hero.jpeg"

OUTPUT_DIR = SCRIPT_DIR.parent.parent / \
    "leviathan/visual/lora_candidates/JINX/gemini_test"

QWEN_V2_DIR = SCRIPT_DIR.parent.parent / \
    "leviathan/visual/lora_candidates/JINX/qwen_test_v2"

# Matching the same 10 angles from the Qwen test.
# Gemini uses text prompts for angle control (no numeric params).
TEST_ANGLES = [
    {
        "name": "01_front_eye_medium",
        "desc": "Front, eye level, medium",
        "prompt_prefix": (
            "Single photorealistic full-body photograph, front-facing view. "
            "Subject standing upright, arms relaxed at sides, neutral pose. "
            "Camera at eye level. Full body visible from head to feet."
        ),
    },
    {
        "name": "02_three_quarter_right_eye_medium",
        "desc": "3/4 right, eye level, medium",
        "prompt_prefix": (
            "Single photorealistic full-body photograph, three-quarter view from the right. "
            "Subject's body rotated 45 degrees — the left shoulder partially hidden behind the torso. "
            "Arms relaxed at sides, neutral standing pose. Full body visible from head to feet."
        ),
    },
    {
        "name": "03_three_quarter_left_eye_medium",
        "desc": "3/4 left, eye level, medium",
        "prompt_prefix": (
            "Single photorealistic full-body photograph, three-quarter view from the left. "
            "Subject's body rotated 45 degrees — the right shoulder partially hidden behind the torso. "
            "Arms relaxed at sides, neutral standing pose. Full body visible from head to feet."
        ),
    },
    {
        "name": "04_profile_right_eye_medium",
        "desc": "Profile right, eye level, medium",
        "prompt_prefix": (
            "Single photorealistic full-body photograph, right profile 90-degree side view. "
            "Subject's body turned so the right shoulder faces the camera. "
            "Arms relaxed at sides, neutral standing pose. Full body visible from head to feet."
        ),
    },
    {
        "name": "05_profile_left_eye_medium",
        "desc": "Profile left, eye level, medium",
        "prompt_prefix": (
            "Single photorealistic full-body photograph, left profile 90-degree side view. "
            "Subject's body turned so the left shoulder faces the camera. "
            "Arms relaxed at sides, neutral standing pose. Full body visible from head to feet."
        ),
    },
    {
        "name": "06_back_eye_medium",
        "desc": "Back view, eye level, medium",
        "prompt_prefix": (
            "Single photorealistic full-body photograph, rear view. "
            "Subject facing DIRECTLY away from camera — back of head visible, face NOT visible. "
            "Subject must NOT turn or look over shoulder. Head facing forward, away from camera. "
            "Arms relaxed at sides, neutral standing pose. Full body visible from head to feet."
        ),
    },
    {
        "name": "07_front_eye_closeup",
        "desc": "Front, eye level, close-up",
        "prompt_prefix": (
            "Tight headshot photograph framed from mid-chest to top of head. "
            "ONLY the face, neck, and upper shoulders are visible in frame. "
            "NO full body. NO legs. NO waist. NO hands. NO belt. NO tools. "
            "The face fills most of the frame. Shot on 85mm f/1.4, shallow depth of field."
        ),
    },
    {
        "name": "08_front_low_medium",
        "desc": "Front, low angle, medium",
        "prompt_prefix": (
            "Single photorealistic full-body photograph, front-facing view from a low camera angle. "
            "Camera positioned below eye level, looking upward at the subject. "
            "Subject appears powerful and imposing. Full body visible from head to feet."
        ),
    },
    {
        "name": "09_front_elevated_wide",
        "desc": "Front, elevated, wide (full body)",
        "prompt_prefix": (
            "Single photorealistic full-body photograph, front-facing view from an elevated camera angle. "
            "Camera positioned above eye level, looking slightly downward at the subject. "
            "Wide framing shows complete figure with space around the subject. "
            "Full body visible from head to feet."
        ),
    },
    {
        "name": "10_three_quarter_right_eye_closeup",
        "desc": "3/4 right, eye level, close-up",
        "prompt_prefix": (
            "Tight headshot photograph from three-quarter right angle, framed from mid-chest to top of head. "
            "Subject's face rotated 45 degrees — the far cheek partially hidden. "
            "ONLY the face, neck, and upper shoulders are visible in frame. "
            "The face fills most of the frame. Shot on 85mm f/1.4, shallow depth of field."
        ),
    },
]

# Character description for Jinx (simplified for this test)
CHARACTER_DESC = (
    "Young woman with sharp angular features, intense dark eyes, and choppy dark hair. "
    "Athletic lean build. Determined expression. "
    "Pure white #FFFFFF studio background. Studio lighting only."
)


def build_gemini_contact_sheet(output_dir: Path, results: list):
    """Build HTML contact sheet for Gemini results."""
    html = """<!DOCTYPE html>
<html><head>
<title>Gemini 2.5 Flash Comparison Test — JINX</title>
<style>
body { background: #1a1a2e; color: #eee; font-family: system-ui; margin: 20px; }
h1 { color: #4ecca3; }
.info { color: #aaa; margin-bottom: 20px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.card { background: #16213e; border-radius: 8px; overflow: hidden; }
.card img { width: 100%; display: block; cursor: pointer; }
.card img:hover { opacity: 0.9; }
.card .meta { padding: 10px; }
.card .name { font-weight: bold; color: #4ecca3; }
.card .desc { color: #aaa; font-size: 0.85em; margin-top: 4px; }
.card .timing { color: #0f3460; font-size: 0.75em; margin-top: 4px; }
.hero-ref { max-width: 250px; border-radius: 8px; margin-bottom: 20px; }
.summary { background: #16213e; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
.lightbox { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.95); z-index: 1000; cursor: pointer;
  justify-content: center; align-items: center; }
.lightbox.active { display: flex; }
.lightbox img { max-width: 90%; max-height: 90%; object-fit: contain; }
</style>
</head><body>
"""
    html += "<h1>Gemini 2.5 Flash — JINX Comparison Test</h1>\n"
    html += f'<p class="info">Generated: {datetime.now().strftime("%Y-%m-%d %H:%M")}'
    html += f' | Model: gemini-2.5-flash-image | Aspect: 1:1</p>\n'
    html += '<p class="info"><b>Reference image:</b></p>\n'
    html += '<img class="hero-ref" src="../../../refs/characters/heroes/Jinx_Hero.jpeg">\n'

    total_time = sum(r.get("time_s", 0) for r in results)
    html += f'<div class="summary">'
    html += f'<b>Total:</b> {len(results)} images | '
    html += f'Time: {total_time:.1f}s ({total_time/len(results):.1f}s avg)'
    html += '</div>\n'

    html += '<div class="grid">\n'
    for r in results:
        img_name = r["filename"]
        html += f'<div class="card">\n'
        html += f'  <img src="{img_name}" onclick="openLightbox(this.src)">\n'
        html += f'  <div class="meta">\n'
        html += f'    <div class="name">{r["name"]}</div>\n'
        html += f'    <div class="desc">{r["desc"]}</div>\n'
        html += f'    <div class="timing">{r.get("time_s", 0):.1f}s</div>\n'
        html += f'  </div>\n'
        html += f'</div>\n'
    html += '</div>\n'

    html += """
<div class="lightbox" id="lb" onclick="this.classList.remove('active')">
  <img id="lb-img">
</div>
<script>
function openLightbox(src) {
  document.getElementById('lb-img').src = src;
  document.getElementById('lb').classList.add('active');
}
document.addEventListener('keydown', e => {
  if (e.key === 'Escape') document.getElementById('lb').classList.remove('active');
});
</script>
</body></html>"""

    sheet_path = output_dir / "gemini_test_review.html"
    sheet_path.write_text(html)
    return sheet_path


def build_comparison_html(qwen_dir: Path, gemini_dir: Path, output_path: Path):
    """Build side-by-side comparison HTML."""
    html = """<!DOCTYPE html>
<html><head>
<title>Qwen vs Gemini — Side-by-Side Comparison — JINX</title>
<style>
body { background: #1a1a2e; color: #eee; font-family: system-ui; margin: 20px; }
h1 { color: #fff; }
h2 { color: #aaa; margin-top: 40px; border-bottom: 1px solid #333; padding-bottom: 8px; }
.info { color: #aaa; margin-bottom: 30px; }
.comparison-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
  margin-bottom: 30px; align-items: start;
}
.engine-card { background: #16213e; border-radius: 8px; overflow: hidden; }
.engine-card img { width: 100%; display: block; cursor: pointer; }
.engine-card img:hover { opacity: 0.9; }
.engine-card .label {
  padding: 8px 12px; font-weight: bold; font-size: 0.9em;
}
.qwen-label { color: #e94560; }
.gemini-label { color: #4ecca3; }
.engine-card .meta { padding: 4px 12px 10px; color: #aaa; font-size: 0.8em; }
.hero-ref { max-width: 200px; border-radius: 8px; margin-bottom: 20px; }
.legend { display: flex; gap: 30px; margin-bottom: 20px; }
.legend span { font-size: 0.9em; }
.lightbox { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.95); z-index: 1000; cursor: pointer;
  justify-content: center; align-items: center; }
.lightbox.active { display: flex; }
.lightbox img { max-width: 90%; max-height: 90%; object-fit: contain; }
</style>
</head><body>
"""
    html += "<h1>Qwen vs Gemini — LoRA Training Candidate Comparison</h1>\n"
    html += f'<p class="info">Generated: {datetime.now().strftime("%Y-%m-%d %H:%M")} | Character: JINX</p>\n'
    html += '<p class="info"><b>Reference image:</b></p>\n'
    html += '<img class="hero-ref" src="../../../refs/characters/heroes/Jinx_Hero.jpeg">\n'
    html += '<div class="legend">\n'
    html += '  <span class="qwen-label">■ Qwen Image Edit 2511 + Multi-Angle LoRA (fal.ai, ~$0.035/img)</span>\n'
    html += '  <span class="gemini-label">■ Gemini 2.5 Flash (Google, token-based pricing)</span>\n'
    html += '</div>\n'

    for angle in TEST_ANGLES:
        name = angle["name"]
        desc = angle["desc"]
        qwen_img = f"../qwen_test_v2/{name}.png"
        gemini_img = f"../gemini_test/{name}.png"

        html += f'<h2>{desc}</h2>\n'
        html += '<div class="comparison-row">\n'

        # Qwen card
        html += '  <div class="engine-card">\n'
        html += f'    <img src="{qwen_img}" onclick="openLightbox(this.src)">\n'
        html += f'    <div class="label qwen-label">Qwen Multi-Angle</div>\n'
        html += f'    <div class="meta">Numeric: h={angle.get("h", "?")}, v={angle.get("v", "?")}, z={angle.get("z", "?")}</div>\n'
        html += '  </div>\n'

        # Gemini card
        html += '  <div class="engine-card">\n'
        html += f'    <img src="{gemini_img}" onclick="openLightbox(this.src)">\n'
        html += f'    <div class="label gemini-label">Gemini 2.5 Flash</div>\n'
        html += f'    <div class="meta">Text prompt: {desc}</div>\n'
        html += '  </div>\n'

        html += '</div>\n'

    html += """
<div class="lightbox" id="lb" onclick="this.classList.remove('active')">
  <img id="lb-img">
</div>
<script>
function openLightbox(src) {
  document.getElementById('lb-img').src = src;
  document.getElementById('lb').classList.add('active');
}
document.addEventListener('keydown', e => {
  if (e.key === 'Escape') document.getElementById('lb').classList.remove('active');
});
</script>
</body></html>"""

    output_path.write_text(html)
    return output_path


def main():
    if not os.environ.get("GOOGLE_API_KEY"):
        print("ERROR: GOOGLE_API_KEY not set", file=sys.stderr)
        print("Get a key at: https://aistudio.google.com/apikey", file=sys.stderr)
        sys.exit(1)

    if not HERO_IMAGE.exists():
        print(f"ERROR: Hero image not found: {HERO_IMAGE}", file=sys.stderr)
        sys.exit(1)

    # Create output dir
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

    print("=" * 60)
    print("GEMINI 2.5 FLASH — COMPARISON TEST")
    print("=" * 60)
    print(f"Hero image:  {HERO_IMAGE.name}")
    print(f"Output dir:  {OUTPUT_DIR}")
    print(f"Model:       gemini-2.5-flash-image")
    print(f"Test images: {len(TEST_ANGLES)}")
    print(f"Rate limit:  4.5s between requests (15 RPM)")
    print("=" * 60)

    engine = GeminiEngine(model="gemini-2.5-flash-image")

    results = []
    for i, angle in enumerate(TEST_ANGLES):
        print(f"\n[{i+1}/{len(TEST_ANGLES)}] {angle['desc']}")

        output_path = OUTPUT_DIR / f"{angle['name']}.png"

        # Build the full prompt
        full_prompt = f"{angle['prompt_prefix']}\n\n{CHARACTER_DESC}"

        job = GenerationJob(
            asset_type="character",
            asset_key="JINX",
            variant=None,
            angle=angle["name"],
            prompt=full_prompt,
            output_path=output_path,
            aspect_ratio="1:1",
            hero_image_path=HERO_IMAGE,
        )

        t0 = time.time()
        result = engine.generate(job)
        elapsed = time.time() - t0

        if result.success:
            print(f"  Saved: {angle['name']}.png ({elapsed:.1f}s)")
            results.append({
                "name": angle["name"],
                "desc": angle["desc"],
                "filename": f"{angle['name']}.png",
                "time_s": elapsed,
            })
        else:
            print(f"  FAILED: {result.error}")
            results.append({
                "name": angle["name"],
                "desc": angle["desc"],
                "filename": "FAILED",
                "time_s": elapsed,
                "error": result.error,
            })

        # Rate limiting (skip after last image)
        if i < len(TEST_ANGLES) - 1:
            wait = max(0, engine.rate_limit_delay - elapsed)
            if wait > 0:
                print(f"  Rate limit wait: {wait:.1f}s")
                time.sleep(wait)

    # Write manifest
    manifest = {
        "test": "gemini_comparison",
        "model": "gemini-2.5-flash-image",
        "hero_image": str(HERO_IMAGE),
        "generated_at": datetime.now().isoformat(),
        "results": results,
    }
    manifest_path = OUTPUT_DIR / "test_manifest.json"
    manifest_path.write_text(json.dumps(manifest, indent=2))

    # Build Gemini contact sheet
    sheet_path = build_gemini_contact_sheet(OUTPUT_DIR, results)

    # Build comparison HTML (only if Qwen v2 results exist)
    comparison_dir = OUTPUT_DIR.parent / "comparison"
    comparison_dir.mkdir(parents=True, exist_ok=True)
    comparison_path = comparison_dir / "comparison.html"

    if QWEN_V2_DIR.exists():
        build_comparison_html(QWEN_V2_DIR, OUTPUT_DIR, comparison_path)
        print(f"\nComparison: {comparison_path}")
    else:
        print(f"\nNote: Qwen v2 results not found at {QWEN_V2_DIR} — skipping comparison HTML")

    # Summary
    success = [r for r in results if r["filename"] != "FAILED"]
    failed = [r for r in results if r["filename"] == "FAILED"]
    total_time = sum(r["time_s"] for r in results)

    print("\n" + "=" * 60)
    print("TEST COMPLETE")
    print("=" * 60)
    print(f"Success: {len(success)}/{len(results)}")
    if failed:
        print(f"Failed:  {len(failed)}")
        for f in failed:
            print(f"  - {f['name']}: {f.get('error', 'unknown')}")
    print(f"Time:    {total_time:.1f}s total ({total_time/len(results):.1f}s avg)")
    print(f"\nGemini review:     {sheet_path}")
    if comparison_path.exists():
        print(f"Side-by-side:      {comparison_path}")
    print(f"Output:            {OUTPUT_DIR}")


if __name__ == "__main__":
    main()
