#!/usr/bin/env python3
"""Compose la typo (accents nets) par-dessus une scène 3D générée.

Usage : compose_septembre_visual.py [2026-09-07 ...]   (sans argument : tout)
Les scènes sources vivent dans assets/scenes-septembre/scene-<date>.jpg
"""
import sys
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont

ROOT = Path(__file__).resolve().parent.parent
SCENES = ROOT / "assets" / "scenes-septembre"
OUT = ROOT / "www" / "propositions" / "planning-social-septembre-2026" / "visuels"
FONTS = Path.home() / "Library" / "Fonts"

W, H = 1920, 1080
ORANGE = (233, 128, 48)
WHITE = (255, 255, 255)
INK = (18, 16, 14)
SITE = "arnaud-merigeau.fr"

# theme : "dark" (scène nuit) ou "light" (scène studio crème)
# side  : moitié laissée libre par la scène, donc côté du texte
SPECS = {
    "2026-09-04": {
        "badge": "CONVERSION",
        "title": ["Aucun transporteur", "disponible"],
        "subtitle": ["Le checkout s'arrête là.", "Souvent un poids ou une zone."],
        "side": "left", "theme": "dark",
    },
    "2026-09-07": {
        "badge": "SÉCURITÉ",
        "title": ["Les bots", "reprennent"],
        "subtitle": ["reCAPTCHA au bon endroit.", "Pas un module à 80 €."],
        "side": "left", "theme": "dark",
    },
    "2026-09-08": {
        "badge": "PRESTASHOP 9.2",
        "title": ["Le checkout", "natif arrive"],
        "subtitle": ["Testez-le en staging.", "Pas en pleine saison."],
        "side": "left", "theme": "light",
    },
    "2026-09-09": {
        "badge": "CAS CLIENT",
        "title": ["300 000", "références"],
        "subtitle": ["L'ERP ne suivait plus.", "Le site, si."],
        "side": "left", "theme": "dark",
    },
    "2026-09-10": {
        "layout": "poster",
        "title": ["Commandez avant", "la rupture"],
        "module": "Module stock et prévisions d'achats",
    },
    "2026-09-11": {
        "badge": "COMPTABILITÉ",
        "title": ["Le CSV", "suffit souvent"],
        "subtitle": ["Un export propre et daté,", "avant de payer un connecteur."],
        "side": "left", "theme": "dark",
    },
    "2026-09-14": {
        "badge": "CONNECTEUR",
        "title": ["Les commandes", "en cron"],
        "subtitle": ["Marketplace vers PrestaShop.", "Sans copier-coller."],
        "side": "left", "theme": "dark",
    },
    "2026-09-15": {
        "badge": "MOBILE",
        "title": ["79 % du paid", "sur mobile"],
        "subtitle": ["Deux fois moins d'achats.", "Le problème est là."],
        "side": "left", "theme": "light",
    },
    "2026-09-16": {
        "layout": "poster",
        "title": ["Commande payée.", "Facture Pennylane."],
        "module": "Module PrestaShop × Pennylane",
    },
    "2026-09-17": {
        "badge": "FREELANCE",
        "title": ["Un ticket,", "pas un devis"],
        "subtitle": ["Intervention cadrée.", "Réponse la semaine même."],
        "side": "left", "theme": "dark",
    },
    "2026-09-18": {
        "badge": "IA",
        "title": ["Utile en staging,", "pas en prod"],
        "subtitle": ["L'IA lit le code.", "Elle ne le déploie pas."],
        "side": "left", "theme": "dark",
    },
    "2026-09-21": {
        "layout": "poster",
        "title": ["Quelqu'un vient", "d'acheter"],
        "module": "Module preuve sociale PrestaShop",
    },
    "2026-09-22": {
        "badge": "DONNÉES",
        "title": ["56 dans l'ERP,", "17 sur le site"],
        "subtitle": ["Une référence tronquée,", "et le stock ment."],
        "side": "left", "theme": "dark",
    },
    "2026-09-23": {
        "badge": "MODULES",
        "title": ["5 modules,", "pas 40"],
        "subtitle": ["Chaque module en plus", "est une dette."],
        "side": "left", "theme": "light",
    },
    "2026-09-24": {
        "badge": "BUSINESS",
        "title": ["Le TCO,", "pas le slogan"],
        "subtitle": ["PrestaShop ou Shopify", "en 2026."],
        "side": "left", "theme": "dark",
    },
    "2026-09-25": {
        "badge": "ANKORSTORE",
        "title": ["Un module,", "pas un Zapier"],
        "subtitle": ["Catalogue et commandes", "dans PrestaShop."],
        "side": "left", "theme": "dark",
    },
    "2026-09-28": {
        "badge": "MIGRATION",
        "title": ["Encore du 1.7", "en production"],
        "subtitle": ["PrestaShop 9 est là.", "Le plan se fait maintenant."],
        "side": "left", "theme": "dark",
    },
    "2026-09-29": {
        "badge": "API",
        "title": ["Le vrai chantier", "n'est pas le thème"],
        "subtitle": ["Relier ERP, caisse et site.", "Puis parler design."],
        "side": "left", "theme": "dark",
    },
    "2026-09-30": {
        "badge": "Q4",
        "title": ["4 choses", "que je gèle"],
        "subtitle": ["Avant novembre,", "on ne touche plus à ça."],
        "side": "left", "theme": "light",
    },
}


def font(name: str, size: int) -> ImageFont.FreeTypeFont:
    return ImageFont.truetype(str(FONTS / name), size)


def fit(draw, lines, name, start, min_size, max_w):
    """Plus grande taille de police où toutes les lignes tiennent dans max_w."""
    size = start
    while size > min_size:
        f = font(name, size)
        if max(draw.textlength(l, font=f) for l in lines) <= max_w:
            break
        size -= 2
    return font(name, size), size


def compose_poster(spec: dict, scene: Image.Image) -> Image.Image:
    """Affiche photo : titre au centre, punch orange, rail marque, URL en coin."""
    img = scene.copy()
    band = Image.new("L", (1, H))
    for y in range(H):
        d = 1 - abs(y - H / 2) / (H / 2)
        band.putpixel((0, y), int(175 * (d ** 0.55)))
    veil = Image.new("RGB", (W, H), INK)
    img = Image.composite(veil, img, band.resize((W, H)))
    wash = Image.new("RGBA", (W, H), (*INK, 70))
    img = Image.alpha_composite(img.convert("RGBA"), wash)

    draw = ImageDraw.Draw(img)
    draw.rectangle((0, 0, 18, H), fill=ORANGE)

    titles = spec["title"] if isinstance(spec["title"], list) else [spec["title"]]
    tf, size = fit(draw, titles, "AktivGrotesk-Black.ttf", 118, 72, W - 220)
    mf = font("AktivGrotesk-Medium.ttf", 32)
    uf = font("AktivGrotesk-Bold.ttf", 26)
    gap = 22
    rule_h = 6
    block_h = int(len(titles) * size * 1.02) + gap + rule_h + 18 + 32
    y = H // 2 - block_h // 2

    def shadow_text(xy, text, fnt, fill, anchor="mt"):
        x, yy = xy
        for ox, oy in ((3, 3), (0, 3), (3, 0)):
            draw.text((x + ox, yy + oy), text, font=fnt, fill=(0, 0, 0), anchor=anchor)
        draw.text(xy, text, font=fnt, fill=fill, anchor=anchor)

    for i, line in enumerate(titles):
        fill = ORANGE if i == len(titles) - 1 else WHITE
        shadow_text((W // 2, y), line, tf, fill)
        y += int(size * 1.02)
    y += gap
    rw = min(280, int(draw.textlength(titles[-1], font=tf) * 0.35))
    draw.rectangle((W // 2 - rw // 2, y, W // 2 + rw // 2, y + rule_h), fill=ORANGE)
    y += rule_h + 18
    shadow_text((W // 2, y), spec["module"], mf, (255, 236, 220))

    shadow_text((48, 48), SITE, uf, (255, 236, 220), anchor="lt")
    return img.convert("RGB")


def compose(date: str) -> Path:
    spec = SPECS[date]
    scene = Image.open(SCENES / f"scene-{date}.jpg").convert("RGB").resize((W, H), Image.LANCZOS)
    if spec.get("layout") == "poster":
        img = compose_poster(spec, scene)
        OUT.mkdir(parents=True, exist_ok=True)
        dest = OUT / f"daily-{date}.jpg"
        img.save(dest, quality=92, optimize=True)
        return dest

    light = spec.get("theme") == "light"

    # voile dégradé du côté texte pour garantir le contraste
    veil = Image.new("L", (W, 1))
    for x in range(W):
        t = x / (W - 1)
        if spec["side"] == "right":
            t = 1 - t
        veil.putpixel((x, 0), int((170 if light else 190) * max(0.0, 1 - t * 1.9) ** 1.4))
    flat = Image.new("RGB", (W, H), (247, 241, 233) if light else (4, 10, 26))
    img = Image.composite(flat, scene, veil.resize((W, H)))

    draw = ImageDraw.Draw(img)
    x0 = 96 if spec["side"] == "left" else W // 2 + 48
    max_w = W // 2 - 136
    title_col = INK if light else WHITE
    accent = (188, 86, 14) if light else ORANGE  # orange plus profond, lisible sur crème
    muted = (120, 110, 100) if light else (150, 165, 190)

    # badge pilule
    bf = font("AktivGrotesk-Bold.ttf", 30)
    tw = draw.textlength(spec["badge"], font=bf)
    by = 132
    draw.rounded_rectangle((x0, by, x0 + tw + 72, by + 66), radius=33, fill=ORANGE)
    draw.text((x0 + 36, by + 33), spec["badge"], font=bf, fill=WHITE, anchor="lm")

    # titre (auto-ajusté pour ne jamais déborder sur la scène)
    tf, size = fit(draw, spec["title"], "AktivGrotesk-Black.ttf", 128, 72, max_w)
    y = by + 150
    for line in spec["title"]:
        draw.text((x0, y), line, font=tf, fill=title_col)
        y += int(size * 1.1)

    # sous-titre
    sf, ssize = fit(draw, spec["subtitle"], "AktivGrotesk-Medium.ttf", 40, 28, max_w)
    y += 44
    for line in spec["subtitle"]:
        draw.text((x0, y), line, font=sf, fill=accent)
        y += int(ssize * 1.4)

    # signature site, angle inférieur gauche
    draw.text((x0, H - 72), SITE, font=font("AktivGrotesk-Medium.ttf", 30), fill=muted, anchor="ls")

    OUT.mkdir(parents=True, exist_ok=True)
    dest = OUT / f"daily-{date}.jpg"
    img.save(dest, quality=92, optimize=True)
    return dest


if __name__ == "__main__":
    for d in sys.argv[1:] or SPECS:
        if SPECS[d].get("layout") == "neo" or (SCENES / f"scene-{d}.jpg").exists():
            print(compose(d))
        else:
            print(f"scène manquante : {d}", file=sys.stderr)
