#!/usr/bin/env python3
"""FireRed LoRA API — Image edit service.

Architecture:
  Client → This API (auth) → Baseten (GPU) → Supabase (tracking)

Endpoints:
  POST /api/edit          - Edit image (sync, ~5s)
  GET  /api/status/{id}   - Job details + output
  GET  /api/history       - Paginated job history
  GET  /api/queue         - Stats
  POST /api/gpu/scale     - Scale GPUs
"""

import base64
import hashlib
import os
import uuid
from datetime import datetime, timezone
from typing import Optional

import httpx
from fastapi import FastAPI, HTTPException, Request, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import boto3
from supabase import create_client

# ══════════════════════════════════════════════════════════════
# CONFIG
# ══════════════════════════════════════════════════════════════

API_KEY = os.environ.get("FIRERED_API_KEY", "fr-live-k8x9mP2qR7vN4wJ6tB3nF5")
DASHBOARD_PASSWORD_HASH = os.environ.get(
    "DASHBOARD_PASSWORD_HASH",
    hashlib.sha256("firered2026".encode()).hexdigest()
)

BASETEN_API_KEY = os.environ.get("BASETEN_API_KEY", "OppO9O1J.5KZDmCEAze6OmKat23pdV3cBji2EEdu9")
BASETEN_MODEL_ID = os.environ.get("BASETEN_MODEL_ID", "q04vdp23")
BASETEN_URL = f"https://model-{BASETEN_MODEL_ID}.api.baseten.co/production"

SUPABASE_URL = os.environ.get("SUPABASE_URL", "https://odqahsqyxuvuxxkptndy.supabase.co")
SUPABASE_KEY = os.environ.get("SUPABASE_SERVICE_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9kcWFoc3F5eHV2dXh4a3B0bmR5Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3NDcyODQ5OCwiZXhwIjoyMDkwMzA0NDk4fQ.gJ6EzdWrAwKGrMFh6nIAwCnJhygK2wY60i932EVs8u8")

R2_ENDPOINT = os.environ.get("R2_ENDPOINT", "https://cb908ed13329eb7b186e06ab51bda190.r2.cloudflarestorage.com")
R2_ACCESS_KEY = os.environ.get("R2_ACCESS_KEY", "853b656f58ae29510c69dd857f27a247")
R2_SECRET_KEY = os.environ.get("R2_SECRET_KEY", "923a7dc8dbb619d4ee5778f545931af8fbd0c3b303602504949f117d30ed81cc")
R2_BUCKET = os.environ.get("R2_BUCKET", "cc-storage")
R2_PUBLIC_URL = os.environ.get("R2_PUBLIC_URL", "https://pub-cc-storage.r2.dev")

DEFAULT_STEPS = 6
DEFAULT_CFG = 1.0
ESTIMATED_TIME_S = 3.5

# ══════════════════════════════════════════════════════════════
# CLIENTS
# ══════════════════════════════════════════════════════════════

app = FastAPI(title="FireRed LoRA API", version="1.0.0")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

supabase = None
s3_client = None
http_client = None


@app.on_event("startup")
async def startup():
    global supabase, s3_client, http_client
    if SUPABASE_URL and SUPABASE_KEY:
        supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
    s3_client = boto3.client(
        "s3",
        endpoint_url=R2_ENDPOINT,
        aws_access_key_id=R2_ACCESS_KEY,
        aws_secret_access_key=R2_SECRET_KEY,
        region_name="auto",
    ) if R2_ACCESS_KEY else None
    http_client = httpx.AsyncClient(timeout=120)


@app.on_event("shutdown")
async def shutdown():
    if http_client:
        await http_client.aclose()


# ══════════════════════════════════════════════════════════════
# AUTH
# ══════════════════════════════════════════════════════════════

async def verify_api_key(request: Request):
    """Check X-API-Key header."""
    key = request.headers.get("X-API-Key", "")
    if key != API_KEY:
        raise HTTPException(401, "Invalid or missing API key. Include X-API-Key header.")
    return key


# ══════════════════════════════════════════════════════════════
# HELPERS
# ══════════════════════════════════════════════════════════════

def upload_to_r2(data: bytes, key: str, content_type: str = "image/png") -> str:
    if not s3_client:
        return ""
    try:
        s3_client.put_object(Bucket=R2_BUCKET, Key=key, Body=data, ContentType=content_type)
        return f"{R2_PUBLIC_URL}/{key}"
    except Exception as e:
        print(f"R2 upload failed: {e}")
        return ""


# ══════════════════════════════════════════════════════════════
# ENDPOINTS
# ══════════════════════════════════════════════════════════════

class EditRequest(BaseModel):
    image: str
    prompt: str
    steps: int = 6
    cfg: float = 1.0
    seed: int = 42
    priority: int = 1
    client_id: Optional[str] = None


@app.post("/api/edit", dependencies=[Depends(verify_api_key)])
async def submit_edit(req: EditRequest):
    """Edit an image. Sends to Baseten GPU, returns result synchronously (~5-8s)."""
    job_id = str(uuid.uuid4())

    input_url = ""
    if s3_client:
        try:
            input_bytes = base64.b64decode(req.image)
            r2_key = f"firered-inputs/{job_id}.jpg"
            input_url = upload_to_r2(input_bytes, r2_key, "image/jpeg")
        except Exception:
            pass

    job_data = {
        "id": job_id,
        "prompt": req.prompt,
        "input_image_url": input_url or "inline",
        "steps": req.steps,
        "cfg": req.cfg,
        "seed": req.seed,
        "status": "processing",
        "priority": req.priority,
        "client_id": req.client_id,
        "estimated_time_s": ESTIMATED_TIME_S,
        "processing_started_at": datetime.now(timezone.utc).isoformat(),
    }

    if supabase:
        supabase.table("edit_jobs").insert(job_data).execute()

    if not BASETEN_MODEL_ID:
        raise HTTPException(503, "No GPU backend configured")

    try:
        resp = await http_client.post(
            f"{BASETEN_URL}/predict",
            headers={"Authorization": f"Api-Key {BASETEN_API_KEY}"},
            json={
                "image": req.image,
                "prompt": req.prompt,
                "steps": req.steps,
                "cfg": req.cfg,
                "seed": req.seed,
            },
        )
        resp.raise_for_status()
        result = resp.json()

        if result.get("status") == "success":
            output_b64 = result.get("image", "")
            output_url = ""
            if output_b64 and s3_client:
                output_bytes = base64.b64decode(output_b64)
                r2_key = f"firered-edits/{job_id}.png"
                output_url = upload_to_r2(output_bytes, r2_key)

            update_data = {
                "status": "completed",
                "completed_at": datetime.now(timezone.utc).isoformat(),
                "actual_time_s": result.get("time_s"),
            }
            if output_url:
                update_data["output_image_url"] = output_url
            else:
                update_data["output_b64"] = output_b64
            if supabase:
                supabase.table("edit_jobs").update(update_data).eq("id", job_id).execute()

            return {
                "job_id": job_id,
                "status": "completed",
                "actual_time_s": result.get("time_s"),
                "output_image_url": output_url,
                "output_b64": output_b64 if not output_url else "",
                "config": result.get("config", {}),
                "input_size": result.get("input_size", ""),
                "output_size": result.get("output_size", ""),
            }
        else:
            error_msg = result.get("message", "Unknown error")
            if supabase:
                supabase.table("edit_jobs").update({
                    "status": "failed",
                    "error_message": error_msg,
                }).eq("id", job_id).execute()
            raise HTTPException(500, f"Edit failed: {error_msg}")

    except httpx.HTTPError as e:
        if supabase:
            supabase.table("edit_jobs").update({
                "status": "failed",
                "error_message": str(e),
            }).eq("id", job_id).execute()
        raise HTTPException(502, f"Baseten error: {e}")


@app.get("/api/status/{job_id}", dependencies=[Depends(verify_api_key)])
async def get_status(job_id: str):
    """Get job details including output image."""
    if not supabase:
        raise HTTPException(500, "Database not configured")

    result = supabase.table("edit_jobs").select("*").eq("id", job_id).single().execute()
    if not result.data:
        raise HTTPException(404, "Job not found")

    job = result.data
    response = {
        "job_id": job["id"],
        "status": job["status"],
        "created_at": job["created_at"],
        "prompt": job["prompt"],
        "steps": job["steps"],
        "cfg": job["cfg"],
        "seed": job["seed"],
        "client_id": job.get("client_id"),
    }

    if job["status"] == "completed":
        response["output_image_url"] = job.get("output_image_url") or ""
        response["output_b64"] = job.get("output_b64") or ""
        response["actual_time_s"] = job["actual_time_s"]
        response["completed_at"] = job["completed_at"]
    elif job["status"] == "failed":
        response["error"] = job["error_message"]
        response["retry_count"] = job["retry_count"]

    return response


@app.get("/api/history", dependencies=[Depends(verify_api_key)])
async def get_history(page: int = 1, limit: int = 20, status: Optional[str] = None):
    """Paginated job history. Returns metadata only (no base64 images)."""
    if not supabase:
        raise HTTPException(500, "Database not configured")

    offset = (page - 1) * limit
    query = supabase.table("edit_jobs").select(
        "id, created_at, prompt, status, steps, cfg, seed, actual_time_s, "
        "completed_at, error_message, client_id, input_image_url, output_image_url, retry_count",
        count="exact"
    )
    if status:
        query = query.eq("status", status)
    result = query.order("created_at", desc=True).range(offset, offset + limit - 1).execute()

    return {
        "jobs": result.data or [],
        "total": result.count or 0,
        "page": page,
        "limit": limit,
        "pages": ((result.count or 0) + limit - 1) // limit,
    }


@app.get("/api/queue", dependencies=[Depends(verify_api_key)])
async def queue_stats():
    """Queue statistics and GPU info."""
    if not supabase:
        return {"error": "Database not configured"}

    stats = {}
    for s in ["queued", "processing", "completed", "failed", "cancelled"]:
        result = supabase.table("edit_jobs").select("id", count="exact").eq("status", s).execute()
        stats[s] = result.count or 0

    recent = supabase.table("edit_jobs").select("actual_time_s").eq("status", "completed").order("completed_at", desc=True).limit(50).execute()
    times = [r["actual_time_s"] for r in (recent.data or []) if r.get("actual_time_s")]
    stats["avg_time_s"] = round(sum(times) / len(times), 2) if times else ESTIMATED_TIME_S
    stats["throughput_per_min"] = round(60 / stats["avg_time_s"], 1) if stats["avg_time_s"] > 0 else 0
    stats["total_jobs"] = sum(stats.get(s, 0) for s in ["queued", "processing", "completed", "failed", "cancelled"])

    # Get Baseten deployment info
    try:
        async with httpx.AsyncClient(timeout=10) as client:
            resp = await client.get(
                f"https://api.baseten.co/v1/models/{BASETEN_MODEL_ID}/deployments/q04ev42",
                headers={"Authorization": f"Api-Key {BASETEN_API_KEY}"},
            )
            if resp.status_code == 200:
                dep = resp.json()
                stats["gpu"] = {
                    "status": dep.get("status", "unknown"),
                    "active_replicas": dep.get("active_replica_count", 0),
                    "min_replicas": dep.get("autoscaling_settings", {}).get("min_replica", 0),
                    "max_replicas": dep.get("autoscaling_settings", {}).get("max_replica", 1),
                    "instance_type": dep.get("instance_type_name", "H100"),
                }
    except Exception:
        stats["gpu"] = {"status": "unknown"}

    return stats


@app.post("/api/gpu/scale", dependencies=[Depends(verify_api_key)])
async def scale_gpus(replicas: int = 1):
    """Scale Baseten model replicas."""
    if not BASETEN_MODEL_ID:
        return {"error": "No Baseten model configured"}

    async with httpx.AsyncClient() as client:
        resp = await client.patch(
            f"https://api.baseten.co/v1/models/{BASETEN_MODEL_ID}/deployments/q04ev42",
            headers={"Authorization": f"Api-Key {BASETEN_API_KEY}"},
            json={"min_replica": replicas, "max_replica": max(replicas, replicas * 2)},
        )
        resp.raise_for_status()

    return {"status": "scaled", "min_replicas": replicas, "max_replicas": max(replicas, replicas * 2)}


@app.post("/api/job/{job_id}/cancel", dependencies=[Depends(verify_api_key)])
async def cancel_job(job_id: str):
    """Cancel a queued job."""
    if not supabase:
        raise HTTPException(500, "Database not configured")
    result = supabase.table("edit_jobs").select("status").eq("id", job_id).single().execute()
    if not result.data:
        raise HTTPException(404, "Job not found")
    if result.data["status"] not in ("queued", "processing"):
        raise HTTPException(400, f"Cannot cancel job in '{result.data['status']}' status")
    supabase.table("edit_jobs").update({"status": "cancelled"}).eq("id", job_id).execute()
    return {"status": "cancelled", "job_id": job_id}


@app.post("/api/job/{job_id}/retry", dependencies=[Depends(verify_api_key)])
async def retry_job(job_id: str):
    """Retry a failed job."""
    if not supabase:
        raise HTTPException(500, "Database not configured")
    result = supabase.table("edit_jobs").select("*").eq("id", job_id).single().execute()
    if not result.data:
        raise HTTPException(404, "Job not found")
    job = result.data
    if job["status"] != "failed":
        raise HTTPException(400, f"Can only retry failed jobs, current: {job['status']}")
    if job["retry_count"] >= job["max_retries"]:
        raise HTTPException(400, f"Max retries ({job['max_retries']}) exceeded")
    supabase.table("edit_jobs").update({
        "status": "queued",
        "retry_count": job["retry_count"] + 1,
        "error_message": None,
    }).eq("id", job_id).execute()
    return {"status": "requeued", "job_id": job_id, "retry_count": job["retry_count"] + 1}


# ══════════════════════════════════════════════════════════════
# DASHBOARD AUTH
# ══════════════════════════════════════════════════════════════

@app.post("/api/dashboard/auth")
async def dashboard_auth(request: Request):
    """Verify dashboard password. Returns API key on success."""
    body = await request.json()
    pw = body.get("password", "")
    pw_hash = hashlib.sha256(pw.encode()).hexdigest()
    if pw_hash == DASHBOARD_PASSWORD_HASH:
        return {"authenticated": True, "api_key": API_KEY}
    raise HTTPException(401, "Wrong password")


# ══════════════════════════════════════════════════════════════
# DASHBOARD
# ══════════════════════════════════════════════════════════════

@app.get("/", response_class=HTMLResponse)
async def dashboard():
    """Password-protected admin dashboard."""
    return DASHBOARD_HTML


@app.get("/health")
async def health():
    return {"status": "ok", "model": "firered-lora", "version": "1.0.0"}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)


# ══════════════════════════════════════════════════════════════
# DASHBOARD HTML
# ══════════════════════════════════════════════════════════════

DASHBOARD_HTML = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FireRed LoRA — Admin Dashboard</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0a0f;--card:#12121a;--border:#1e1e2e;--accent:#ef4444;--accent2:#f97316;
--green:#22c55e;--blue:#3b82f6;--yellow:#eab308;--text:#e2e8f0;--muted:#64748b;--white:#fff}
body{font-family:'Inter',system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}

/* LOGIN */
#login-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(135deg,#0a0a0f 0%,#1a0a0a 50%,#0a0a0f 100%)}
.login-box{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:48px;width:400px;text-align:center}
.login-box h1{font-size:28px;margin-bottom:8px;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.login-box p{color:var(--muted);margin-bottom:32px;font-size:14px}
.login-box input{width:100%;padding:14px 16px;background:#1a1a2e;border:1px solid var(--border);border-radius:10px;color:var(--text);font-size:16px;outline:none;margin-bottom:16px}
.login-box input:focus{border-color:var(--accent)}
.login-box button{width:100%;padding:14px;background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;border-radius:10px;color:var(--white);font-size:16px;font-weight:600;cursor:pointer}
.login-box button:hover{opacity:.9}
.login-error{color:var(--accent);font-size:13px;margin-top:8px;display:none}

/* MAIN */
#app{display:none}
header{background:var(--card);border-bottom:1px solid var(--border);padding:16px 32px;display:flex;align-items:center;justify-content:space-between}
header h1{font-size:20px;display:flex;align-items:center;gap:10px}
header h1 span{background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.header-actions{display:flex;gap:12px;align-items:center}
.header-actions button{padding:8px 16px;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);cursor:pointer;font-size:13px}
.header-actions button:hover{border-color:var(--accent)}
.header-actions .logout{color:var(--accent)}
#last-refresh{color:var(--muted);font-size:12px}

.container{max-width:1400px;margin:0 auto;padding:24px}

/* STATS CARDS */
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:24px}
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px}
.stat-card .label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}
.stat-card .value{font-size:32px;font-weight:700}
.stat-card .sub{font-size:12px;color:var(--muted);margin-top:4px}
.stat-card.green .value{color:var(--green)}
.stat-card.blue .value{color:var(--blue)}
.stat-card.yellow .value{color:var(--yellow)}
.stat-card.red .value{color:var(--accent)}
.stat-card.orange .value{color:var(--accent2)}

/* GPU STATUS */
.gpu-bar{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px 20px;margin-bottom:24px;display:flex;align-items:center;gap:24px;flex-wrap:wrap}
.gpu-bar .gpu-indicator{width:12px;height:12px;border-radius:50%;background:var(--green);animation:pulse 2s infinite}
.gpu-bar .gpu-indicator.offline{background:var(--accent);animation:none}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.gpu-bar .gpu-label{font-weight:600;font-size:14px}
.gpu-bar .gpu-detail{font-size:13px;color:var(--muted)}

/* FILTER BAR */
.filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;align-items:center}
.filter-bar button{padding:6px 14px;border-radius:20px;border:1px solid var(--border);background:transparent;color:var(--muted);cursor:pointer;font-size:13px;transition:all .2s}
.filter-bar button.active{background:var(--accent);border-color:var(--accent);color:var(--white)}
.filter-bar button:hover{border-color:var(--accent);color:var(--text)}
.filter-bar .count{font-size:11px;background:#ffffff15;padding:2px 6px;border-radius:8px;margin-left:4px}

/* TABLE */
.jobs-table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--border);border-radius:12px;overflow:hidden}
.jobs-table th{text-align:left;padding:12px 16px;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid var(--border);background:#0d0d15}
.jobs-table td{padding:12px 16px;font-size:13px;border-bottom:1px solid var(--border);vertical-align:middle}
.jobs-table tr:hover{background:#ffffff05}
.jobs-table tr:last-child td{border-bottom:none}
.status-badge{padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}
.status-badge.completed{background:#22c55e20;color:var(--green)}
.status-badge.processing{background:#3b82f620;color:var(--blue)}
.status-badge.queued{background:#eab30820;color:var(--yellow)}
.status-badge.failed{background:#ef444420;color:var(--accent)}
.status-badge.cancelled{background:#64748b20;color:var(--muted)}
.prompt-cell{max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.time-cell{font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--green)}
.id-cell{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted);cursor:pointer}
.id-cell:hover{color:var(--text)}

/* IMAGE MODAL */
.modal-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.85);z-index:1000;align-items:center;justify-content:center;padding:24px}
.modal-overlay.show{display:flex}
.modal-content{background:var(--card);border:1px solid var(--border);border-radius:16px;max-width:1100px;width:100%;max-height:90vh;overflow-y:auto;padding:32px}
.modal-close{position:absolute;top:16px;right:24px;font-size:24px;color:var(--muted);cursor:pointer;z-index:1001}
.modal-close:hover{color:var(--white)}
.modal-header{margin-bottom:24px}
.modal-header h2{font-size:18px;margin-bottom:8px}
.modal-header .meta{font-size:13px;color:var(--muted)}
.modal-images{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:24px}
.modal-images .img-panel{text-align:center}
.modal-images .img-panel label{display:block;font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}
.modal-images img{max-width:100%;border-radius:8px;border:1px solid var(--border)}
.modal-images .no-image{padding:80px 20px;background:#1a1a2e;border-radius:8px;color:var(--muted);font-size:14px}
.modal-prompt{background:#1a1a2e;padding:16px;border-radius:8px;font-size:14px;line-height:1.6;margin-bottom:16px}
.modal-details{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}
.modal-details .detail{background:#1a1a2e;padding:12px;border-radius:8px}
.modal-details .detail label{font-size:11px;color:var(--muted);display:block;margin-bottom:4px}
.modal-details .detail span{font-size:14px;font-weight:600}

/* LOAD MORE */
.load-more{text-align:center;padding:24px}
.load-more button{padding:12px 32px;background:transparent;border:1px solid var(--border);border-radius:10px;color:var(--text);font-size:14px;cursor:pointer}
.load-more button:hover{border-color:var(--accent);background:#ef444410}
.load-more .info{font-size:12px;color:var(--muted);margin-top:8px}

.loading{text-align:center;padding:40px;color:var(--muted)}
.spinner{display:inline-block;width:24px;height:24px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;margin-right:8px;vertical-align:middle}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>

<!-- LOGIN SCREEN -->
<div id="login-screen">
<div class="login-box">
  <h1>FireRed LoRA</h1>
  <p>Admin Dashboard &mdash; Enter password to continue</p>
  <input type="password" id="pw-input" placeholder="Password" autofocus>
  <button onclick="doLogin()">Unlock Dashboard</button>
  <div class="login-error" id="login-error">Wrong password. Try again.</div>
</div>
</div>

<!-- MAIN APP -->
<div id="app">
<header>
  <h1><span>FireRed LoRA</span> Admin</h1>
  <div class="header-actions">
    <span id="last-refresh"></span>
    <button onclick="refreshAll()">Refresh</button>
    <button class="logout" onclick="doLogout()">Logout</button>
  </div>
</header>

<div class="container">
  <!-- STATS -->
  <div class="stats-grid" id="stats-grid">
    <div class="stat-card green"><div class="label">Completed</div><div class="value" id="s-completed">-</div><div class="sub" id="s-completed-sub"></div></div>
    <div class="stat-card blue"><div class="label">Processing</div><div class="value" id="s-processing">-</div></div>
    <div class="stat-card yellow"><div class="label">Queued</div><div class="value" id="s-queued">-</div></div>
    <div class="stat-card red"><div class="label">Failed</div><div class="value" id="s-failed">-</div></div>
    <div class="stat-card orange"><div class="label">Avg Time</div><div class="value" id="s-avg">-</div><div class="sub">seconds per edit</div></div>
    <div class="stat-card"><div class="label">Throughput</div><div class="value" id="s-throughput">-</div><div class="sub">edits per minute</div></div>
  </div>

  <!-- GPU BAR -->
  <div class="gpu-bar" id="gpu-bar">
    <div class="gpu-indicator" id="gpu-dot"></div>
    <span class="gpu-label" id="gpu-label">GPU: Checking...</span>
    <span class="gpu-detail" id="gpu-detail"></span>
  </div>

  <!-- FILTER -->
  <div class="filter-bar" id="filter-bar">
    <button class="active" data-status="">All</button>
    <button data-status="completed">Completed</button>
    <button data-status="processing">Processing</button>
    <button data-status="queued">Queued</button>
    <button data-status="failed">Failed</button>
    <button data-status="cancelled">Cancelled</button>
  </div>

  <!-- TABLE -->
  <table class="jobs-table">
    <thead>
      <tr>
        <th>ID</th>
        <th>Status</th>
        <th>Prompt</th>
        <th>Time</th>
        <th>Steps</th>
        <th>CFG</th>
        <th>Created</th>
        <th>Client</th>
      </tr>
    </thead>
    <tbody id="jobs-tbody"></tbody>
  </table>

  <div class="load-more" id="load-more" style="display:none">
    <button onclick="loadMore()">Load More</button>
    <div class="info" id="load-info"></div>
  </div>
  <div class="loading" id="loading"><span class="spinner"></span> Loading jobs...</div>
</div>
</div>

<!-- IMAGE MODAL -->
<div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
<span class="modal-close" onclick="closeModal()">&times;</span>
<div class="modal-content">
  <div class="modal-header">
    <h2 id="modal-title">Job Details</h2>
    <div class="meta" id="modal-meta"></div>
  </div>
  <div class="modal-images" id="modal-images"></div>
  <div class="modal-prompt" id="modal-prompt"></div>
  <div class="modal-details" id="modal-details"></div>
</div>
</div>

<script>
const BASE = location.origin;
let API_KEY = '';
let currentPage = 1;
let currentFilter = '';
let allJobs = [];

// ── AUTH ──
async function doLogin() {
  const pw = document.getElementById('pw-input').value;
  try {
    const r = await fetch(`${BASE}/api/dashboard/auth`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({password: pw})
    });
    if (!r.ok) throw new Error();
    const d = await r.json();
    API_KEY = d.api_key;
    sessionStorage.setItem('firered_key', API_KEY);
    document.getElementById('login-screen').style.display = 'none';
    document.getElementById('app').style.display = 'block';
    refreshAll();
  } catch {
    const el = document.getElementById('login-error');
    el.style.display = 'block';
    document.getElementById('pw-input').value = '';
    document.getElementById('pw-input').focus();
  }
}
document.getElementById('pw-input').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });

function doLogout() {
  sessionStorage.removeItem('firered_key');
  API_KEY = '';
  document.getElementById('login-screen').style.display = 'flex';
  document.getElementById('app').style.display = 'none';
  document.getElementById('pw-input').value = '';
}

// Check session
(function checkSession() {
  const k = sessionStorage.getItem('firered_key');
  if (k) {
    API_KEY = k;
    document.getElementById('login-screen').style.display = 'none';
    document.getElementById('app').style.display = 'block';
    refreshAll();
  }
})();

// ── API CALLS ──
async function apiFetch(path) {
  const r = await fetch(`${BASE}${path}`, {headers: {'X-API-Key': API_KEY}});
  if (r.status === 401) { doLogout(); throw new Error('Unauthorized'); }
  return r.json();
}

// ── REFRESH ──
async function refreshAll() {
  document.getElementById('last-refresh').textContent = 'Refreshing...';
  await Promise.all([loadStats(), loadJobs(true)]);
  document.getElementById('last-refresh').textContent = `Updated ${new Date().toLocaleTimeString()}`;
}

async function loadStats() {
  try {
    const s = await apiFetch('/api/queue');
    document.getElementById('s-completed').textContent = s.completed || 0;
    document.getElementById('s-completed-sub').textContent = `${s.total_jobs || 0} total jobs`;
    document.getElementById('s-processing').textContent = s.processing || 0;
    document.getElementById('s-queued').textContent = s.queued || 0;
    document.getElementById('s-failed').textContent = s.failed || 0;
    document.getElementById('s-avg').textContent = s.avg_time_s ? s.avg_time_s + 's' : '-';
    document.getElementById('s-throughput').textContent = s.throughput_per_min ? s.throughput_per_min + '/min' : '-';

    // GPU
    const gpu = s.gpu || {};
    const dot = document.getElementById('gpu-dot');
    const label = document.getElementById('gpu-label');
    const detail = document.getElementById('gpu-detail');
    if (gpu.status === 'ACTIVE') {
      dot.className = 'gpu-indicator';
      label.textContent = `GPU: ${gpu.active_replicas || 0} active replica(s)`;
      detail.textContent = `${gpu.instance_type || 'H100'} | Min: ${gpu.min_replicas} Max: ${gpu.max_replicas}`;
    } else if (gpu.status === 'DEPLOYING') {
      dot.className = 'gpu-indicator';
      dot.style.background = '#eab308';
      label.textContent = 'GPU: Deploying...';
      detail.textContent = 'Model is loading weights';
    } else {
      dot.className = 'gpu-indicator offline';
      label.textContent = `GPU: ${gpu.status || 'Offline'}`;
      detail.textContent = 'Scaled to zero — first request will cold start (~2 min)';
    }
  } catch(e) { console.error('Stats error:', e); }
}

async function loadJobs(reset = false) {
  if (reset) { currentPage = 1; allJobs = []; }
  document.getElementById('loading').style.display = currentPage === 1 ? 'block' : 'none';

  try {
    const params = new URLSearchParams({page: currentPage, limit: 20});
    if (currentFilter) params.set('status', currentFilter);
    const d = await apiFetch(`/api/history?${params}`);
    if (reset) allJobs = d.jobs; else allJobs = allJobs.concat(d.jobs);
    renderJobs();
    // Load more
    const lm = document.getElementById('load-more');
    if (currentPage < d.pages) {
      lm.style.display = 'block';
      document.getElementById('load-info').textContent = `Showing ${allJobs.length} of ${d.total} jobs`;
    } else {
      lm.style.display = 'none';
    }
  } catch(e) { console.error('Jobs error:', e); }
  document.getElementById('loading').style.display = 'none';
}

function loadMore() {
  currentPage++;
  loadJobs(false);
}

function renderJobs() {
  const tbody = document.getElementById('jobs-tbody');
  tbody.innerHTML = allJobs.map(j => `
    <tr onclick="openJob('${j.id}')" style="cursor:pointer">
      <td class="id-cell" title="${j.id}">${j.id.slice(0,8)}...</td>
      <td><span class="status-badge ${j.status}">${j.status}</span></td>
      <td class="prompt-cell" title="${esc(j.prompt)}">${esc(j.prompt)}</td>
      <td class="time-cell">${j.actual_time_s ? j.actual_time_s + 's' : '-'}</td>
      <td>${j.steps || 6}</td>
      <td>${j.cfg || 1.0}</td>
      <td>${timeAgo(j.created_at)}</td>
      <td>${j.client_id || '-'}</td>
    </tr>
  `).join('');
}

function esc(s) { if (!s) return ''; return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;'); }

function timeAgo(iso) {
  if (!iso) return '-';
  const d = new Date(iso);
  const now = new Date();
  const s = Math.floor((now - d) / 1000);
  if (s < 60) return s + 's ago';
  if (s < 3600) return Math.floor(s/60) + 'm ago';
  if (s < 86400) return Math.floor(s/3600) + 'h ago';
  return d.toLocaleDateString();
}

// ── FILTERS ──
document.getElementById('filter-bar').addEventListener('click', e => {
  if (e.target.tagName !== 'BUTTON') return;
  document.querySelectorAll('#filter-bar button').forEach(b => b.classList.remove('active'));
  e.target.classList.add('active');
  currentFilter = e.target.dataset.status;
  loadJobs(true);
});

// ── JOB DETAIL MODAL ──
async function openJob(id) {
  document.getElementById('modal').classList.add('show');
  document.getElementById('modal-title').textContent = 'Loading...';
  document.getElementById('modal-meta').textContent = '';
  document.getElementById('modal-images').innerHTML = '<div class="loading"><span class="spinner"></span></div>';
  document.getElementById('modal-prompt').textContent = '';
  document.getElementById('modal-details').innerHTML = '';

  try {
    const j = await apiFetch(`/api/status/${id}`);
    document.getElementById('modal-title').textContent = `Job ${j.job_id.slice(0,8)}...`;
    document.getElementById('modal-meta').textContent = `Status: ${j.status} | Created: ${new Date(j.created_at).toLocaleString()}`;
    document.getElementById('modal-prompt').innerHTML = `<strong>Prompt:</strong> ${esc(j.prompt)}`;

    // Images
    let imgsHtml = '';
    // We don't have input image base64 in status response, show placeholder
    imgsHtml += `<div class="img-panel"><label>Input</label><div class="no-image">Input image stored on server</div></div>`;
    if (j.output_b64) {
      imgsHtml += `<div class="img-panel"><label>Output</label><img src="data:image/png;base64,${j.output_b64}" alt="Output"></div>`;
    } else if (j.output_image_url) {
      imgsHtml += `<div class="img-panel"><label>Output</label><img src="${j.output_image_url}" alt="Output"></div>`;
    } else {
      imgsHtml += `<div class="img-panel"><label>Output</label><div class="no-image">${j.status === 'completed' ? 'No output stored' : j.status === 'failed' ? 'Job failed: ' + esc(j.error || '') : 'Pending...'}</div></div>`;
    }
    document.getElementById('modal-images').innerHTML = imgsHtml;

    // Details
    const details = [
      {label: 'Time', value: j.actual_time_s ? j.actual_time_s + 's' : '-'},
      {label: 'Steps', value: j.steps || 6},
      {label: 'CFG', value: j.cfg || 1.0},
      {label: 'Seed', value: j.seed || 42},
      {label: 'Client', value: j.client_id || '-'},
      {label: 'Completed', value: j.completed_at ? new Date(j.completed_at).toLocaleString() : '-'},
    ];
    if (j.error) details.push({label: 'Error', value: j.error});
    document.getElementById('modal-details').innerHTML = details.map(d =>
      `<div class="detail"><label>${d.label}</label><span>${esc(String(d.value))}</span></div>`
    ).join('');
  } catch(e) {
    document.getElementById('modal-images').innerHTML = `<div class="no-image">Error loading job details</div>`;
  }
}

function closeModal() { document.getElementById('modal').classList.remove('show'); }
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });

// Auto-refresh every 30s
setInterval(() => { if (API_KEY) refreshAll(); }, 30000);
</script>
</body>
</html>"""
