#!/usr/bin/env python3
"""確定デザインで「1区間」を縦型ショートに焼く描画モジュール（配布版・ポータブル）。

朝倉様確定デザインをそのまま踏襲（白背景 / ネイビー#0E2E41丸ピル+特大タイトル /
中央に横型16:9フル表示・自前字幕OFF / 動画直下に大きいネイビーCTA）。
配布版はフォントを無料の Noto Sans JP Black に差し替え、パッケージ同梱フォントを使う
（凸版見出しゴシックEBは商用ライセンスのため同梱不可）。パスは __file__ 相対で完全ポータブル。
"""
import os, subprocess, shutil, sys
from PIL import Image, ImageFont, ImageDraw

FPS, OUTW, OUTH = 30, 1080, 1920
FONT = "NotoSansJP-Black.otf"
_HERE = os.path.dirname(os.path.abspath(__file__))          # .../youtube-clip-maker/bin
_PKG = os.path.dirname(_HERE)                                # .../youtube-clip-maker
FONT_DURABLE = os.path.join(_PKG, "assets", "fonts", FONT)   # 同梱フォント（恒久）
FONT_TMP = f"/tmp/fonts/{FONT}"

# 確定デザイン定数（原則不変。Noto Black向けにtitle_strokeのみ微調整=白背景で視認影響なし）
DCFG = dict(
    bg_color="white", video_y="center", video_margin_x=40,
    src_crop_top=0, src_crop_bottom=0,
    kicker_fs=46, kicker_color="#0E2E41",
    title_fs=156, title_max_w=1000, title_scale_x=1.0, title_stroke=6, title_color="#0E2E41",
    cta_text="▼本編はチャンネルへ▼", cta_fs=72, cta_color="#0E2E41", cta_gap=34,
)


def ensure_font():
    if not os.path.exists(FONT_TMP):
        os.makedirs("/tmp/fonts", exist_ok=True)
        if os.path.exists(FONT_DURABLE):
            shutil.copy(FONT_DURABLE, FONT_TMP)
        else:
            sys.exit(f"FONT MISSING: {FONT_DURABLE} も {FONT_TMP} も無し。setup を実行してください。")
    return FONT_TMP


_fit_draw = ImageDraw.Draw(Image.new("RGBA", (8, 8)))


def probe_wh(src):
    r = subprocess.run(
        ["ffprobe", "-v", "error", "-select_streams", "v:0",
         "-show_entries", "stream=width,height", "-of", "csv=p=0:s=x", src],
        capture_output=True, text=True)
    w, h = r.stdout.strip().split("x")
    return int(w), int(h)


def _tight_text(text, fs, fill, font_path, stroke_w=0, stroke_fill=(255, 255, 255, 255)):
    font = ImageFont.truetype(font_path, fs)
    bb = _fit_draw.textbbox((0, 0), text, font=font, stroke_width=stroke_w)
    pad = stroke_w + 10
    lay = Image.new("RGBA", (bb[2] - bb[0] + pad * 2, bb[3] - bb[1] + pad * 2), (0, 0, 0, 0))
    ImageDraw.Draw(lay).text((pad - bb[0], pad - bb[1]), text, font=font,
                             fill=fill, stroke_width=stroke_w, stroke_fill=stroke_fill)
    return lay


def _fit_font(text, fs, max_w, font_path, stroke_w=0, step=4, lo=24):
    while fs > lo:
        f = ImageFont.truetype(font_path, fs)
        bb = _fit_draw.textbbox((0, 0), text, font=f, stroke_width=stroke_w)
        if bb[2] - bb[0] <= max_w:
            break
        fs -= step
    return fs


def _pill(text, fs, bg, font_path, tx=(255, 255, 255, 255), pad_x=42, pad_y=20):
    t = _tight_text(text, fs, tx, font_path)
    pw, ph = t.width + pad_x * 2, t.height + pad_y * 2
    pill = Image.new("RGBA", (pw, ph), (0, 0, 0, 0))
    ImageDraw.Draw(pill).rounded_rectangle([0, 0, pw - 1, ph - 1], radius=ph // 2, fill=bg)
    pill.alpha_composite(t, (pad_x, pad_y))
    return pill


def render_one(src, start, end, kicker, title, out_path, tmp_dir, cfg=None, src_wh=None):
    cfg = {**DCFG, **(cfg or {})}
    font_path = ensure_font()
    os.makedirs(tmp_dir, exist_ok=True)
    W, H = src_wh or probe_wh(src)
    dur = round(end - start, 3)

    crop_h = H - cfg["src_crop_top"] - cfg["src_crop_bottom"]
    video_w = (OUTW - 2 * cfg["video_margin_x"]) & ~1
    video_x = (OUTW - video_w) // 2
    video_h = int(round(video_w * crop_h / W)) & ~1
    video_top = ((OUTH - video_h) // 2 if cfg["video_y"] == "center" else int(cfg["video_y"])) & ~1
    video_bottom = video_top + video_h

    main_fs = _fit_font(title, cfg["title_fs"], cfg["title_max_w"], font_path, stroke_w=cfg["title_stroke"])
    main = _tight_text(title, main_fs, cfg["title_color"], font_path,
                       stroke_w=cfg["title_stroke"], stroke_fill=(255, 255, 255, 255))
    if cfg["title_scale_x"] != 1.0:
        main = main.resize((int(main.width * cfg["title_scale_x"]), main.height), Image.LANCZOS)
    pill = _pill(kicker, cfg["kicker_fs"], cfg["kicker_color"], font_path)
    tc = Image.new("RGBA", (OUTW, OUTH), (0, 0, 0, 0))
    main_y = video_top - 26 - main.height
    pill_y = max(36, main_y - 16 - pill.height)
    tc.alpha_composite(pill, ((OUTW - pill.width) // 2, pill_y))
    tc.alpha_composite(main, ((OUTW - main.width) // 2, main_y))
    title_png = os.path.join(tmp_dir, "title.png"); tc.save(title_png)

    cta_fs = _fit_font(cfg["cta_text"], cfg["cta_fs"], cfg["title_max_w"], font_path)
    cta = _tight_text(cfg["cta_text"], cta_fs, cfg["cta_color"], font_path)
    cc = Image.new("RGBA", (OUTW, OUTH), (0, 0, 0, 0))
    cc.alpha_composite(cta, ((OUTW - cta.width) // 2, video_bottom + cfg["cta_gap"]))
    cta_png = os.path.join(tmp_dir, "cta.png"); cc.save(cta_png)

    fc = [
        f"color=c={cfg['bg_color']}:s={OUTW}x{OUTH}:r={FPS}:d={dur:.3f},format=rgba[bg]",
        (f"[0:v]fps={FPS},crop={W}:{crop_h}:0:{cfg['src_crop_top']},"
         f"scale={video_w}:{video_h}:flags=lanczos,setsar=1,format=rgba[vid]"),
        f"[bg][vid]overlay={video_x}:{video_top}[base]",
        f"[base][1:v]overlay=0:0[v1]",
        f"[v1][2:v]overlay=0:0[v2]",
        f"[v2]format=yuv420p[vout]",
    ]
    cmd = ["ffmpeg", "-y", "-loglevel", "error",
           "-ss", f"{start:.3f}", "-i", src, "-i", title_png, "-i", cta_png, "-t", f"{dur:.3f}",
           "-filter_complex", ";".join(fc), "-map", "[vout]", "-map", "0:a:0",
           "-c:v", "libx264", "-crf", "20", "-preset", "medium", "-pix_fmt", "yuv420p", "-r", str(FPS),
           "-c:a", "aac", "-b:a", "192k", "-ar", "48000", "-movflags", "+faststart", out_path]
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode != 0:
        raise RuntimeError(f"ffmpeg失敗: {r.stderr[-600:]}")
    return out_path
