#!/usr/bin/env python3
"""Ad resizer: one ad (image or video) -> 9:16, 1:1, 4:5, 16:9 (or any W:H) with smart crop or blurred pad, plus a
safe-zone preview per format and a report that says which formats need AI outpainting (Kenerate expand_image).

Examples
  python resize.py --input https://.../ad.png --formats 9:16,1:1,4:5,16:9
  python resize.py --input ad.mp4 --formats 9:16,1:1 --mode blur --focus 0.5,0.35
  python resize.py --input ad.png --formats 9:16 --expanded 9:16=https://.../expanded.png   # use an outpainted version

Modes: auto (crop when it loses <= --max-crop of the picture, else blurred pad and flag for expand_image),
crop, blur, pad (solid --pad-color).
"""
from __future__ import annotations

import argparse
import sys
from pathlib import Path

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

# Approximate UI-safe areas (fractions of the frame kept clear of platform UI): top, bottom, left, right.
SAFE = {
    "9:16": {"top": 0.14, "bottom": 0.22, "left": 0.06, "right": 0.12, "note": "Reels / TikTok / Shorts: caption, buttons and profile UI"},
    "4:5": {"top": 0.05, "bottom": 0.08, "left": 0.05, "right": 0.05, "note": "Feed: keep a 5% margin, text away from the bottom"},
    "1:1": {"top": 0.05, "bottom": 0.05, "left": 0.05, "right": 0.05, "note": "Feed: 5% margin"},
    "16:9": {"top": 0.07, "bottom": 0.1, "left": 0.05, "right": 0.05, "note": "YouTube / web: title-safe 90%, player bar at the bottom"},
}
DEFAULT_SAFE = {"top": 0.05, "bottom": 0.05, "left": 0.05, "right": 0.05, "note": "generic 5% margin"}


def crop_loss(sw: int, sh: int, W: int, H: int) -> float:
    """Fraction of the source picture a cover-crop to W:H throws away."""
    src, dst = sw / sh, W / H
    return round(1 - (dst / src if src > dst else src / dst), 3)


def safe_preview(img, fmt: str, dst: Path) -> dict:
    from PIL import Image, ImageDraw
    z = SAFE.get(fmt, DEFAULT_SAFE)
    W, H = img.size
    base = img.convert("RGBA")
    ov = Image.new("RGBA", (W, H), (0, 0, 0, 0))
    d = ImageDraw.Draw(ov)
    red = (255, 40, 60, 95)
    t, b, l, r = int(H * z["top"]), int(H * (1 - z["bottom"])), int(W * z["left"]), int(W * (1 - z["right"]))
    d.rectangle((0, 0, W, t), fill=red)
    d.rectangle((0, b, W, H), fill=red)
    d.rectangle((0, t, l, b), fill=red)
    d.rectangle((r, t, W, b), fill=red)
    d.rectangle((l, t, r, b), outline=(255, 255, 255, 230), width=max(2, W // 300))
    base.alpha_composite(ov)
    # busy detail (often text / logos) inside the unsafe bands: edge energy of the whole frame, then per band
    from PIL import ImageFilter, ImageStat
    edges = img.convert("L").filter(ImageFilter.FIND_EDGES)
    m = max(2, W // 200)

    def energy(bx):
        x0, y0, x1, y1 = max(bx[0], m), max(bx[1], m), min(bx[2], W - m), min(bx[3], H - m)
        if x1 - x0 < 4 or y1 - y0 < 4:
            return 0.0
        return round(min(ImageStat.Stat(edges.crop((x0, y0, x1, y1))).mean[0] / 25.0, 1.0), 3)

    def tiles(bx):
        step = max(16, min(W, H) // 7)
        x0, y0, x1, y1 = bx
        for yy in range(y0, max(y1 - 4, y0 + 1), step):
            for xx in range(x0, max(x1 - 4, x0 + 1), step):
                yield (xx, yy, min(xx + step, x1), min(yy + step, y1))

    bands = [(0, 0, W, t), (0, b, W, H), (0, t, l, b), (r, t, W, b)]
    busy = max((energy(tb) for bx in bands for tb in tiles(bx)), default=0.0)
    inner = energy((l, t, r, b))
    lab = K.load_font(max(14, W // 40), "regular")
    ImageDraw.Draw(base).text((l + 8, t + 8), f"{fmt} safe area", font=lab, fill=(255, 255, 255, 255), stroke_width=2,
                              stroke_fill=(0, 0, 0, 255))
    base.convert("RGB").save(dst, quality=85)
    flag = busy > 0.3
    return {"preview": str(dst.resolve()), "unsafe_detail": busy, "safe_detail": inner,
            "warning": "busy detail (text / logo?) sits under platform UI - check the preview" if flag else None,
            "zones": {k: v for k, v in z.items() if k != "note"}, "note": z["note"]}


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--input", required=True, help="ad image or video (URL or file)")
    ap.add_argument("--formats", default="9:16,1:1,4:5,16:9")
    ap.add_argument("--mode", choices=["auto", "crop", "blur", "pad"], default="auto")
    ap.add_argument("--max-crop", type=float, default=0.22, help="auto: crop if it loses at most this share (default 0.22)")
    ap.add_argument("--focus", default="0.5,0.5", help="crop centre x,y in 0..1 (default centre)")
    ap.add_argument("--pad-color", default="#000000")
    ap.add_argument("--res", type=int, default=1080, help="short side px (default 1080)")
    ap.add_argument("--expanded", action="append", default=[], help="FMT=URL_OR_FILE: an expand_image result to use for FMT")
    ap.add_argument("--name", default="", help="output prefix (default: input name)")
    ap.add_argument("--out-dir", default="kenerate-out/ad-resizer")
    ap.add_argument("--keep-temp", action="store_true")
    a = ap.parse_args()

    fmts = [f.strip() for f in a.formats.split(",") if f.strip()]
    st = K.Status(3 + len(fmts), "ad-resizer")
    K.require(status=st)
    tmp = K.workdir(a.keep_temp)
    out = Path(a.out_dir)
    out.mkdir(parents=True, exist_ok=True)
    fx, fy = (float(v) for v in a.focus.split(","))
    expanded = {}
    for e in a.expanded:
        k, _, v = e.partition("=")
        expanded[k.strip()] = v.strip()

    st.step("Fetching the ad")
    src = K.fetch(a.input, tmp, "ad", st)
    kind = K.kind_of(str(src))
    info = K.probe(src) if kind != "image" else None
    is_video = bool(info and info["has_video"] and info["duration"] > 0.05)
    if is_video:
        sw, sh = info["width"], info["height"]
    else:
        img0 = K.open_image(src)
        sw, sh = img0.size
    base = K.safe_name(a.name or Path(src).stem)
    st.info(f"{'video' if is_video else 'image'} {sw}x{sh}" + (f", {info['duration']:.1f} s" if is_video else ""))

    st.step("Planning formats")
    plan = []
    for f in fmts:
        W, H = K.size_for(f, a.res)
        loss = crop_loss(sw, sh, W, H)
        mode = a.mode
        if f in expanded:
            mode = "expanded"
        elif mode == "auto":
            mode = "crop" if loss <= a.max_crop else "blur"
        plan.append((f, W, H, loss, mode))
        st.info(f"{f}: {W}x{H}, crop would lose {loss * 100:.0f}% -> {mode}")

    results = []
    for f, W, H, loss, mode in plan:
        st.step(f"Rendering {f} ({mode})")
        tag = f.replace(":", "x")
        ext = ".mp4" if is_video else (".png" if not is_video and img0.mode == "RGBA" else ".jpg")
        dst = out / f"{base}_{tag}{ext}"
        if is_video:
            if mode == "expanded":
                st.warn("expanded images apply to stills; using blur pad for the video")
                mode = "blur"
            fit = {"crop": "cover", "blur": "blur", "pad": "contain"}[mode]
            K.normalize_clip(src, dst, W, H, 30, fit=fit, focus=(fx, fy), status=st)
            frame = K.open_image(K.extract_frame(dst, min(1.0, info["duration"] / 2), tmp / f"f_{tag}.jpg"))
        else:
            im = img0
            if mode == "expanded":
                im = K.open_image(K.fetch(expanded[f], tmp, f"expanded_{tag}", st))
                frame = K.cover(im, W, H, (0.5, 0.5))
            elif mode == "crop":
                frame = K.cover(im, W, H, (fx, fy))
            elif mode == "blur":
                frame = K.contain(im.convert("RGB"), W, H, blur_bg=True)
            else:
                frame = K.contain(im, W, H, bg=K.hex_rgb(a.pad_color) + (255,))
            if ext == ".jpg":
                frame.convert("RGB").save(dst, quality=92)
            else:
                frame.save(dst)
            st.progress(100)
        sz = safe_preview(frame.convert("RGB"), f, out / f"{base}_{tag}_safezone.jpg")
        row = {"format": f, "file": str(dst.resolve()), "size": f"{W}x{H}", "mode": mode, "crop_loss": loss, **sz}
        if mode in ("blur", "pad") and not is_video:
            row["suggest"] = {"tool": "expand_image", "arguments": {"image_url": "<the ad's Kenerate url>", "aspect_ratio": f}}
            row["why"] = f"a crop would cut {loss * 100:.0f}% of the ad; outpainting fills {f} without cutting anything"
        if sz.get("warning"):
            st.warn(f"{f}: {sz['warning']}")
        results.append(row)

    st.step("Report")
    need = [r["format"] for r in results if r.get("suggest")]
    if need:
        st.info("for a cleaner result, outpaint with Kenerate expand_image: " + ", ".join(need) +
                " (then re-run with --expanded FMT=<result url>)")
    K.finish({"input": {"kind": "video" if is_video else "image", "size": f"{sw}x{sh}"}, "outputs": results,
              "needs_expand_image": need, "elapsed_s": st.elapsed()})


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