#!/usr/bin/env python3
"""Social carousel: up to 10 slides (PNG) in one consistent style from a JSON spec, or one wide image split into a
seamless panorama. Also writes a multi-page PDF (LinkedIn document posts) and a preview strip.

Spec (slides.json):
  {"slides": [{"title": "5 ways to brew better coffee", "body": "Swipe ->", "image": "https://.../cover.png"},
              {"title": "1. Grind fresh", "body": "..."}, ...]}

Examples
  python carousel.py --spec slides.json --theme dark --brand-color "#FF5A1F" --handle "@brewandbloom"
  python carousel.py --panorama https://.../wide.png --count 3 --size 1080x1350
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import kmedia as K  # noqa: E402

THEMES = {"light": ((250, 250, 247), (17, 17, 17), (90, 90, 90)), "dark": ((16, 16, 20), (255, 255, 255), (190, 190, 200)),
          "brand": (None, None, None)}


def slide(spec: dict, i: int, n: int, W: int, H: int, theme: str, brand, handle: str, font, tmp: Path, st):
    from PIL import Image, ImageDraw
    bg, fg, muted = THEMES[theme]
    if theme == "brand":
        bg = brand
        fg, _ = K.best_text_color(brand)
        muted = fg
    img = Image.new("RGBA", (W, H), tuple(bg) + (255,))
    pad = int(W * 0.08)
    top = pad
    if spec.get("image"):
        ph = K.open_image(K.fetch(spec["image"], tmp, f"slide{i:02d}", st)).convert("RGB")
        full = i == 1 and not spec.get("body")
        if i == 1:  # cover slide: picture fills the top 62%, or all of it with a scrim
            h = H if full else int(H * 0.62)
            img.paste(K.cover(ph, W, h), (0, 0))
            if full:
                sc = K.gradient(W, H // 2, (0, 0, 0), (0, 0, 0)).convert("RGBA")
                a = Image.linear_gradient("L").resize((W, H // 2))
                sc.putalpha(a.point(lambda v: int(v * 0.8)))
                img.alpha_composite(sc, (0, H - H // 2))
                fg, muted = (255, 255, 255), (230, 230, 230)
            top = h + pad // 2 if not full else int(H * 0.6)
        else:
            h = int(H * 0.42)
            card = K.cover(ph, W - pad * 2, h)
            mask = Image.new("L", card.size, 0)
            ImageDraw.Draw(mask).rounded_rectangle((0, 0, card.width - 1, card.height - 1), radius=int(W * 0.03), fill=255)
            img.paste(card, (pad, pad), mask)
            top = pad + h + pad // 2
    bottom = H - int(pad * 1.6)
    title = spec.get("title", "")
    body = spec.get("body", "")
    th = int((bottom - top) * (0.45 if body else 0.9))
    r = K.draw_block(img, title, (pad, top, W - pad, top + th), fill=fg, align="left", valign="bottom" if body else "center",
                     max_size=int(W * (0.11 if i == 1 else 0.085)), min_size=20, kind="bold", font_override=font, max_lines=5)
    if body:
        y = r["bbox"][3] + int(W * 0.035)
        K.draw_block(img, body, (pad, y, W - pad, bottom), fill=muted, align="left", valign="top",
                     max_size=int(W * 0.05), min_size=16, kind="regular", max_lines=8)
    d = ImageDraw.Draw(img)
    f = K.load_font(int(W * 0.028), "regular")
    acc = brand if theme != "brand" else fg
    # progress dots + counter + handle + swipe arrow
    yb = H - int(pad * 0.9)
    dot = max(6, W // 120)
    for k in range(n):
        x = pad + k * dot * 2.6
        d.ellipse((x, yb - dot / 2, x + dot, yb + dot / 2), fill=tuple(acc) + (255,) if k == i - 1 else tuple(muted) + (110,))
    label = f"{i}/{n}" + (f"  {handle}" if handle else "")
    tw = K.text_w(d, label, f)
    d.text((W - pad - tw - (int(W * 0.06) if i < n else 0), yb - int(W * 0.018)), label, font=f, fill=tuple(muted) + (255,))
    if i < n:
        ax = W - pad
        d.polygon([(ax, yb), (ax - int(W * 0.03), yb - int(W * 0.02)), (ax - int(W * 0.03), yb + int(W * 0.02))], fill=tuple(acc) + (255,))
    return img.convert("RGB")


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--spec", help="slides.json (see above)")
    ap.add_argument("--panorama", help="one wide image to split into seamless slides")
    ap.add_argument("--count", type=int, default=3, help="panorama: number of slides (2-10)")
    ap.add_argument("--size", default="1080x1350", help="slide size (default 1080x1350 = 4:5)")
    ap.add_argument("--theme", choices=list(THEMES), default="light")
    ap.add_argument("--brand-color", default="#FF5A1F")
    ap.add_argument("--handle", default="")
    K.add_font_args(ap)
    ap.add_argument("--no-pdf", action="store_true")
    ap.add_argument("--name", default="carousel")
    ap.add_argument("--out-dir", default="kenerate-out/social-carousel")
    a = K.parse_args(ap)
    if not a.spec and not a.panorama:
        K.fail("Give --spec slides.json or --panorama IMAGE.")
    st = K.Status(3, "social-carousel")
    K.require(ffmpeg=False, status=st)
    W, H = K.size_for(a.size, 1080)
    tmp = K.workdir()
    out = Path(a.out_dir)
    out.mkdir(parents=True, exist_ok=True)
    brand = K.hex_rgb(a.brand_color)
    slides = []

    if a.panorama:
        n = max(2, min(10, a.count))
        st.step(f"Splitting the panorama into {n} slides")
        pano = K.open_image(K.fetch(a.panorama, tmp, "panorama", st)).convert("RGB")
        need = n * W / H
        if abs(pano.width / pano.height - need) > 0.15 * need:
            st.warn(f"panorama ratio {pano.width / pano.height:.2f} vs {need:.2f} needed: cropping to fit "
                    f"(make it with expand_image or generate_image at a wide ratio)")
        pano = K.cover(pano, W * n, H)
        for k in range(n):
            slides.append(pano.crop((k * W, 0, (k + 1) * W, H)))
        st.progress(100)
        st.step("(no text slides)")
    else:
        spec = json.loads(Path(a.spec).read_text(encoding="utf-8"))
        items = spec.get("slides", spec if isinstance(spec, list) else [])[:10]
        if not items:
            K.fail("The spec has no slides.")
        st.step(f"Fetching slide images ({sum(1 for s in items if s.get('image'))})")
        st.step(f"Rendering {len(items)} slides ({a.theme})")
        for i, s in enumerate(items, 1):
            slides.append(slide(s, i, len(items), W, H, a.theme, brand, a.handle, a.font, tmp, st))
            st.progress(i * 100 / len(items), f"slide {i}/{len(items)}")

    st.step("Saving PNGs, PDF and preview")
    files = []
    for i, im in enumerate(slides, 1):
        p = out / f"{K.safe_name(a.name)}_{i:02d}.png"
        im.save(p)
        files.append(p)
    pdf = None
    if not a.no_pdf:
        pdf = out / f"{K.safe_name(a.name)}.pdf"
        slides[0].save(pdf, save_all=True, append_images=slides[1:], resolution=150)
    from PIL import Image
    th = 400
    tw = int(th * W / H)
    strip = Image.new("RGB", (tw * len(slides) + 8 * (len(slides) + 1), th + 16), (30, 30, 30))
    for i, im in enumerate(slides):
        strip.paste(im.resize((tw, th), Image.LANCZOS), (8 + i * (tw + 8), 8))
    sp = out / f"{K.safe_name(a.name)}_preview.jpg"
    strip.save(sp, quality=88)
    K.finish({"slides": [str(p.resolve()) for p in files], "pdf": str(pdf.resolve()) if pdf else None,
              "preview": str(sp.resolve()), "size": f"{W}x{H}", "elapsed_s": st.elapsed()})


if __name__ == "__main__":
    K.main_guard(main)
