#!/usr/bin/env python3
"""Generate 1000 matched demo faces and update the Modgle spreadsheet."""
from __future__ import annotations

import json
import random
import time
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

import openpyxl
from openpyxl.styles import Alignment, Font
from openpyxl.utils import get_column_letter

ROOT = Path(r"C:\Users\c5717\Downloads\modgle-demo-faces")
SRC_XLSX = Path(r"C:\Users\c5717\Downloads\modgle-demo-data-v2.xlsx")
OUT_XLSX = Path(r"C:\Users\c5717\Downloads\modgle-demo-data-v3.xlsx")
KEEP = {1, 10, 19}  # approved sample portraits
PROGRESS = ROOT / "progress.json"
MANIFEST = ROOT / "manifest.json"
TEST_GLOBS = ("_pollination_test.jpg", "_t_*.jpg")

COUNTRY_LOOK = {
    "Nigeria": "West African Nigerian, dark brown skin",
    "Kenya": "East African Kenyan, dark brown skin",
    "Egypt": "Egyptian, olive to medium-brown skin, North African features",
    "India": "Indian South Asian",
    "Japan": "Japanese East Asian",
    "South Korea": "Korean East Asian",
    "Vietnam": "Vietnamese Southeast Asian",
    "Indonesia": "Indonesian Southeast Asian",
    "Philippines": "Filipino",
    "Mexico": "Mexican Latino, medium tan skin",
    "Turkey": "Turkish, olive skin",
    "Poland": "Polish European, light skin",
    "Germany": "German European, light skin",
    "Spain": "Spanish European, olive to light skin",
}

MIXED_LOOKS = {
    "United States": [
        "White American, light skin",
        "Black American, dark brown skin",
        "Latino American, medium tan skin",
        "East Asian American",
        "South Asian American",
        "mixed-race American",
    ],
    "United Kingdom": [
        "White British, light skin",
        "Black British, dark brown skin",
        "South Asian British",
        "mixed-race British",
        "East Asian British",
    ],
    "Canada": [
        "White Canadian, light skin",
        "Black Canadian, dark brown skin",
        "East Asian Canadian",
        "South Asian Canadian",
        "mixed-race Canadian",
    ],
    "France": [
        "White French, light skin",
        "North African French, olive to medium-brown skin",
        "Black French, dark brown skin",
        "East Asian French",
    ],
    "Brazil": [
        "White Brazilian, light skin",
        "Black Brazilian, dark brown skin",
        "Pardo mixed Brazilian, medium-brown skin",
        "East Asian Brazilian",
    ],
    "South Africa": [
        "Black South African, dark brown skin",
        "White South African, light skin",
        "Coloured South African, medium-brown skin",
        "Indian South African",
    ],
}

NAME_HINTS = [
    (("sandoval", "garcia", "hernandez", "lopez", "martinez", "rodriguez", "gonzalez", "perez", "ramirez", "torres", "delgado", "guzman", "castellanos", "villarreal", "cervantes", "nascimento", "silva"), "Latino, medium tan skin"),
    (("patel", "malhotra", "singh", "sharma", "kumar", "reddy", "iyer", "nair", "banerjee", "khan"), "South Asian"),
    (("emenike", "okonkwo", "okafor", "chukwu", "mwangi", "otieno", "nkrumah", "balogun", "adeyemi"), "Black African, dark brown skin"),
    (("nguyen", "tran", "pham", "hoang"), "Vietnamese Southeast Asian"),
    (("kim", "park", "choi", "jung", "kang"), "Korean East Asian"),
    (("tanaka", "kuroda", "sato", "suzuki", "watanabe", "takahashi"), "Japanese East Asian"),
    (("dlamini", "ndlovu", "khumalo", "thandiwe"), "Black South African, dark brown skin"),
    (("du plessis", "botha"), "White South African, light skin"),
]

JOB_SCENE = {
    "Line cook": "cooking at a restaurant stove, face clearly visible",
    "Chef de partie": "plating food in a kitchen, face clearly visible",
    "Barista": "making coffee behind a cafe counter, face clearly visible",
    "Teacher": "standing in a classroom, face clearly visible",
    "Carpenter": "in a workshop with wood, face clearly visible",
    "Florist": "arranging flowers, face clearly visible",
    "Welder": "in a workshop holding a helmet at their side, no sparks in face, face clearly visible",
    "Bus driver": "sitting in a bus driver's seat, face clearly visible",
    "Sound engineer": "at a mixing desk, face clearly visible",
    "Data analyst": "at a laptop in an office, face clearly visible",
    "Bookkeeper": "at a desk with a laptop, face clearly visible",
    "Vet technician": "gently holding a dog in a clinic, face clearly visible",
    "Physiotherapist": "in a clinic, face clearly visible",
    "Dental hygienist": "in a bright clinic, not wearing a mask, face clearly visible",
    "Optician": "in an optical shop, face clearly visible",
    "Logistics coordinator": "in a warehouse office, face clearly visible",
    "Warehouse supervisor": "on a warehouse floor, face clearly visible",
}

ACT_SCENE = {
    "walking the dog": "walking a dog on a city sidewalk, face clearly visible",
    "cycling": "on a bicycle paused at a path, helmet optional, face clearly visible",
    "five-a-side": "on a small soccer pitch, holding a football, face clearly visible",
    "running": "jogging outdoors, face clearly visible",
    "swimming": "at a pool deck in a swim cap or after a swim, dry face clearly visible",
    "climbing": "at an indoor climbing wall, face clearly visible",
    "yoga": "on a yoga mat in a studio, seated, face clearly visible",
}

INT_SCENE = {
    "gardening": "in a garden with plants, face clearly visible",
    "cooking": "cooking at home, face clearly visible",
    "cycling": "with a bicycle outdoors, face clearly visible",
    "running": "outdoors after a run, face clearly visible",
    "fishing": "by a river holding a fishing rod, face clearly visible",
    "jazz": "in a small music venue, face clearly visible",
    "books": "in a bookstore, face clearly visible",
    "birdwatching": "outdoors with binoculars, face clearly visible",
}


def n_from_id(demo_id: str) -> int:
    return int(str(demo_id).replace("DEMO", ""))


def _name_hit(hay: str, token: str) -> bool:
    hay = f" {hay.lower().strip()} "
    token = token.lower().strip()
    return f" {token} " in hay or hay.strip() == token or hay.strip().startswith(token + " ") or hay.strip().endswith(" " + token)


def appearance_for(country: str, last_name: str, seed: int, first_name: str = "") -> str:
    hay = f"{first_name or ''} {last_name or ''}".lower().strip()
    mixed = country in MIXED_LOOKS
    for names, look in NAME_HINTS:
        if any(_name_hit(hay, n) for n in names):
            if mixed or country in ("South Africa", "Brazil"):
                return look
            if "African" in look or "Latino" in look or "South Asian" in look:
                return look
    if mixed:
        return MIXED_LOOKS[country][seed % len(MIXED_LOOKS[country])]
    return COUNTRY_LOOK.get(country, f"person from {country}")


def age_phrase(age: int) -> str:
    age = int(age)
    if age < 25:
        return f"{age} years old, young adult, looks {age}"
    if age < 35:
        return f"{age} years old, late-20s to early-30s, looks {age}"
    if age < 45:
        return f"{age} years old, late-30s to early-40s, looks {age}, subtle age lines"
    if age < 55:
        return f"{age} years old, late-40s to early-50s, looks {age}, some gray hair, natural aging"
    return f"{age} years old, mid-50s, looks {age}, gray in hair, visible age, not young"


def scene_for(n: int, act, job, interests) -> tuple[str, str]:
    slot = n % 5
    act = (act or "").strip()
    job = (job or "").strip()
    interests = (interests or "").lower()

    if slot in (0, 1):
        return "portrait", (
            "square social-app profile portrait, head and shoulders, looking at camera, "
            "plain soft background, natural indoor light, casual clothes"
        )
    if slot == 2:
        return "outdoor portrait", (
            "outdoor candid portrait, face clearly visible looking toward camera, "
            "soft daylight, city or park background slightly blurred"
        )
    if slot == 3:
        if act in ACT_SCENE:
            return "action", "candid action photo, " + ACT_SCENE[act]
        if job in JOB_SCENE:
            return "action", "candid action photo, " + JOB_SCENE[job]
        return "action", "candid action photo, walking on a city street, face clearly visible"
    # lifestyle
    for key, scene in INT_SCENE.items():
        if key in interests:
            return "lifestyle", "lifestyle photo, " + scene
    if job in JOB_SCENE:
        return "lifestyle", "lifestyle photo, " + JOB_SCENE[job]
    return "lifestyle", (
        "lifestyle photo at a cafe table with a drink, face clearly visible looking at camera"
    )


def build_prompt(person: dict) -> tuple[str, str]:
    n = n_from_id(person["demo_id"])
    gender = "woman" if person["gender"] == "female" else "man"
    look = appearance_for(person["country"], person.get("last_name") or "", n, person.get("first_name") or "")
    kind, scene = scene_for(n, person.get("act"), person.get("job"), person.get("interests"))
    prompt = (
        f"photorealistic {scene} of a {gender}, {age_phrase(person['age'])}, {look}, "
        "friendly natural expression, realistic skin, not a model, not a celebrity, "
        "no text, no watermark, no logo, square photo"
    )
    return prompt, kind


def pollinations_url(prompt: str, seed: int) -> str:
    qs = urllib.parse.urlencode(
        {
            "width": 768,
            "height": 768,
            "model": "flux",
            "nologo": "true",
            "seed": seed,
            "enhance": "true",
        }
    )
    return "https://image.pollinations.ai/prompt/" + urllib.parse.quote(prompt) + "?" + qs


def load_people() -> list[dict]:
    wb = openpyxl.load_workbook(SRC_XLSX, read_only=True, data_only=True)
    people_ws = wb["people"]
    rows = list(people_ws.iter_rows(values_only=True))
    ph = {h: i for i, h in enumerate(rows[0])}
    people = {}
    for r in rows[1:]:
        if not r or not r[0]:
            continue
        people[r[ph["demo_id"]]] = {
            "demo_id": r[ph["demo_id"]],
            "display_name": r[ph["display_name"]],
            "first_name": r[ph["first_name"]],
            "last_name": r[ph["last_name"]],
            "gender": r[ph["gender"]],
            "age": r[ph["age"]],
            "country": r[ph["country"]],
            "face_url": r[ph["face_url"]],
        }
    prof_ws = wb["people_profile"]
    prows = list(prof_ws.iter_rows(values_only=True))
    pp = {h: i for i, h in enumerate(prows[0])}
    for r in prows[1:]:
        if not r or not r[0]:
            continue
        rec = people.get(r[pp["demo_id"]])
        if rec:
            rec["act"] = r[pp.get("f1_s1_act")] if "f1_s1_act" in pp else None
            rec["job"] = r[pp.get("f1_s1_job1")] if "f1_s1_job1" in pp else None
            rec["interests"] = r[pp.get("f1_s1_int")] if "f1_s1_int" in pp else None
    wb.close()
    return [people[k] for k in sorted(people, key=n_from_id)]


def download_one(person: dict) -> dict:
    n = n_from_id(person["demo_id"])
    dest = ROOT / f"{person['demo_id']}.jpg"
    prompt, kind = build_prompt(person)
    url = pollinations_url(prompt, seed=1000 + n)
    result = {
        "demo_id": person["demo_id"],
        "kind": kind,
        "prompt": prompt,
        "url": url,
        "path": str(dest),
        "ok": False,
        "bytes": dest.stat().st_size if dest.exists() else 0,
        "skipped": False,
        "error": None,
    }
    if n in KEEP and dest.exists() and dest.stat().st_size > 50_000:
        result["ok"] = True
        result["skipped"] = True
        result["bytes"] = dest.stat().st_size
        return result
    if dest.exists() and dest.stat().st_size > 8_000 and n not in KEEP:
        result["ok"] = True
        result["skipped"] = True
        result["bytes"] = dest.stat().st_size
        return result

    last_err = None
    for attempt in range(12):
        try:
            req = urllib.request.Request(
                url,
                headers={
                    "User-Agent": "Mozilla/5.0",
                    "Accept": "image/jpeg,image/png,image/*,*/*",
                    "Referer": "https://pollinations.ai/",
                },
            )
            with urllib.request.urlopen(req, timeout=180) as resp:
                data = resp.read()
            if len(data) < 4000 or data[:2] != b"\xff\xd8":
                last_err = f"bad image {len(data)} bytes"
                time.sleep(8 + attempt * 2)
                continue
            dest.write_bytes(data)
            result["ok"] = True
            result["bytes"] = len(data)
            return result
        except Exception as e:
            last_err = str(e)
            wait = 45 if "429" in last_err else (8 + attempt * 3)
            time.sleep(wait + random.random() * 3)
    result["error"] = last_err
    return result


def write_preview(people: list[dict], results: dict[str, dict]) -> None:
    cards = []
    for p in people:
        r = results.get(p["demo_id"], {})
        fn = f"{p['demo_id']}.jpg"
        kind = r.get("kind", "")
        cards.append(
            f'<figure><img src="{fn}" alt="{p["demo_id"]}" loading="lazy">'
            f"<figcaption>{p['demo_id']}<br>{p['display_name']}<br>"
            f"{p['gender']}, {p['age']}, {p['country']}<br>{kind}</figcaption></figure>"
        )
    html = f"""<!doctype html>
<html><head><meta charset="utf-8"><title>Modgle demo faces</title>
<style>
body{{font-family:sans-serif;margin:24px;background:#111;color:#eee}}
h1{{font-weight:600}}
.grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:16px}}
figure{{margin:0;background:#1c1c1c;border-radius:12px;overflow:hidden}}
img{{width:100%;aspect-ratio:1;object-fit:cover;display:block}}
figcaption{{padding:8px 10px;font-size:12px;line-height:1.35}}
</style></head>
<body>
<h1>Modgle demo faces ({len(people)})</h1>
<p>Open this file from the folder so the pictures load. Approved samples: DEMO0001, DEMO0010, DEMO0019.</p>
<div class="grid">
{''.join(cards)}
</div>
</body></html>
"""
    (ROOT / "preview.html").write_text(html, encoding="utf-8")


def update_workbook(people: list[dict], results: dict[str, dict]) -> None:
    wb = openpyxl.load_workbook(SRC_XLSX)
    ws = wb["people"]
    headers = [c.value for c in next(ws.iter_rows(min_row=1, max_row=1))]
    if "face_file" not in headers:
        ws.cell(1, len(headers) + 1, "face_file")
        ws.cell(1, len(headers) + 2, "face_kind")
        ws.cell(1, len(headers) + 3, "face_web")
        ws.cell(1, len(headers) + 4, "face")
        headers.extend(["face_file", "face_kind", "face_web", "face"])
    col = {h: i + 1 for i, h in enumerate(headers)}
    ws.column_dimensions[get_column_letter(col["face"])].width = 18
    for row in range(2, ws.max_row + 1):
        demo_id = ws.cell(row, col["demo_id"]).value
        if not demo_id:
            continue
        r = results.get(demo_id, {})
        filename = f"{demo_id}.jpg"
        web = r.get("url") or ""
        ws.cell(row, col["face_file"], filename)
        ws.cell(row, col["face_kind"], r.get("kind") or "")
        ws.cell(row, col["face_web"], web)
        formula = f'=IMAGE("{web}",1)' if web else f'=IMAGE("{ws.cell(row, col["face_url"]).value}",1)'
        cell = ws.cell(row, col["face"], formula)
        cell.alignment = Alignment(horizontal="center", vertical="center")
        ws.row_dimensions[row].height = 80

    if "faces" in wb.sheetnames:
        del wb["faces"]
    faces = wb.create_sheet("faces", 2)
    faces.append(["demo_id", "display_name", "gender", "age", "country", "kind", "photo", "face_file", "face_url"])
    for col_i in range(1, 10):
        faces.cell(1, col_i).font = Font(bold=True)
    faces.column_dimensions["A"].width = 12
    faces.column_dimensions["B"].width = 22
    faces.column_dimensions["G"].width = 18
    faces.column_dimensions["H"].width = 16
    faces.column_dimensions["I"].width = 55
    for i, p in enumerate(people, start=2):
        r = results.get(p["demo_id"], {})
        web = r.get("url") or ""
        faces.cell(i, 1, p["demo_id"])
        faces.cell(i, 2, p["display_name"])
        faces.cell(i, 3, p["gender"])
        faces.cell(i, 4, p["age"])
        faces.cell(i, 5, p["country"])
        faces.cell(i, 6, r.get("kind") or "")
        faces.cell(i, 7, f'=IMAGE("{web}",1)' if web else None)
        faces.cell(i, 8, f"{p['demo_id']}.jpg")
        faces.cell(i, 9, p.get("face_url"))
        faces.row_dimensions[i].height = 90
    wb.save(OUT_XLSX)
    wb.close()


def main() -> None:
    ROOT.mkdir(parents=True, exist_ok=True)
    for pattern in TEST_GLOBS:
        for p in ROOT.glob(pattern.replace("_t_*.jpg", "_t_*.jpg") if False else pattern):
            try:
                p.unlink()
            except OSError:
                pass
    for p in ROOT.glob("_t_*.jpg"):
        p.unlink(missing_ok=True)
    test = ROOT / "_pollination_test.jpg"
    if test.exists():
        test.unlink()

    people = load_people()
    print(f"people={len(people)}", flush=True)
    results: dict[str, dict] = {}
    if MANIFEST.exists():
        try:
            old = json.loads(MANIFEST.read_text(encoding="utf-8"))
            if isinstance(old, list):
                results = {x["demo_id"]: x for x in old if x.get("ok")}
        except Exception:
            pass

    todo = [p for p in people if not (results.get(p["demo_id"], {}).get("ok"))]
    # still download missing files even if manifest thinks ok
    todo = []
    for p in people:
        path = ROOT / f"{p['demo_id']}.jpg"
        n = n_from_id(p["demo_id"])
        if n in KEEP and path.exists() and path.stat().st_size > 50_000:
            prompt, kind = build_prompt(p)
            results[p["demo_id"]] = {
                "demo_id": p["demo_id"],
                "kind": kind,
                "prompt": prompt,
                "url": pollinations_url(prompt, 1000 + n),
                "path": str(path),
                "ok": True,
                "skipped": True,
                "bytes": path.stat().st_size,
            }
            continue
        if path.exists() and path.stat().st_size > 8_000:
            prompt, kind = build_prompt(p)
            results[p["demo_id"]] = {
                "demo_id": p["demo_id"],
                "kind": kind,
                "prompt": prompt,
                "url": pollinations_url(prompt, 1000 + n),
                "path": str(path),
                "ok": True,
                "skipped": True,
                "bytes": path.stat().st_size,
            }
            continue
        todo.append(p)

    print(f"already={len(people)-len(todo)} todo={len(todo)}", flush=True)
    done = len(people) - len(todo)
    t0 = time.time()
    workers = 1
    with ThreadPoolExecutor(max_workers=workers) as ex:
        futs = {ex.submit(download_one, p): p["demo_id"] for p in todo}
        for fut in as_completed(futs):
            rec = fut.result()
            results[rec["demo_id"]] = rec
            done += 1
            if done % 5 == 0 or not rec["ok"] or done <= 20:
                ok = sum(1 for x in results.values() if x.get("ok"))
                elapsed = max(time.time() - t0, 1)
                newly = max(done - (len(people) - len(todo)), 1)
                rate = newly / elapsed
                remain = (len(people) - done) / rate if rate else 0
                print(
                    f"progress {done}/{len(people)} ok={ok} last={rec['demo_id']} "
                    f"err={rec.get('error')} eta_min={remain/60:.1f}",
                    flush=True,
                )
                PROGRESS.write_text(json.dumps({"done": done, "ok": ok}, indent=2), encoding="utf-8")
                MANIFEST.write_text(json.dumps(list(results.values()), indent=2), encoding="utf-8")

    MANIFEST.write_text(json.dumps([results[p["demo_id"]] for p in people if p["demo_id"] in results], indent=2), encoding="utf-8")
    write_preview(people, results)
    update_workbook(people, results)
    ok = sum(1 for p in people if results.get(p["demo_id"], {}).get("ok"))
    fail = [p["demo_id"] for p in people if not results.get(p["demo_id"], {}).get("ok")]
    print(f"DONE ok={ok} fail={len(fail)} xlsx={OUT_XLSX}", flush=True)
    if fail:
        print("FAIL_IDS " + ",".join(fail[:40]), flush=True)


if __name__ == "__main__":
    main()
