#!/usr/bin/env python3
"""YouTube切り抜きメーカー（配布版・顧問生の各自Macで実行）。

本編YouTube URL（公開/限定公開）またはローカル動画ファイルから、確定デザインの
縦型ショート切り抜きを10〜30本自動生成する。アップロードはしない（各自でアップ）。

  ① 取り込み(yt-dlp DL or --file) → ② mlx_whisper 文字起こし →
  ③ AI採点で区間選定（多ジャンル汎用）→ ④ 確定デザインで量産 → ⑤ 出力フォルダ + zip

使い方:
  python3 make_clips.py --url "https://youtu.be/XXXX"
  python3 make_clips.py --url "https://youtu.be/XXXX" --channel-desc "料理研究家 時短レシピ"
  python3 make_clips.py --file 本編.mp4 --name 7月号
出力: ~/Desktop/切り抜き結果/<name>/clip_01.mp4 ...  と まとめzip
"""
import sys, os, subprocess, argparse, glob, json, zipfile, shutil, time, traceback
import concurrent.futures as cf

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import clip_render as R
import clip_select as S

DEFAULT_OUT = os.path.expanduser("~/Desktop/切り抜き結果")


def log(m): print(f"[{time.strftime('%H:%M:%S')}] {m}", flush=True)


def which(x): return shutil.which(x) is not None


def preflight():
    missing = [x for x in ("yt-dlp", "ffmpeg", "ffprobe", "mlx_whisper", "claude") if not which(x)]
    try:
        import PIL  # noqa
    except Exception:
        missing.append("python:pillow")
    if missing:
        print("✗ 必要な道具が見つかりません: " + ", ".join(missing))
        print("  → 先に setup.sh（初回セットアップ）を実行してください。")
        print("     cd " + os.path.dirname(HERE) + " && zsh setup.sh")
        sys.exit(2)


def ingest(url, file, src):
    if os.path.exists(src) or os.path.islink(src):
        log("① 取り込み: 既存 → スキップ"); return
    if file:
        log(f"① 動画を取り込み: {os.path.basename(file)}")
        os.symlink(os.path.abspath(file), src)
    else:
        log("① 本編をダウンロード中…（yt-dlp）")
        r = subprocess.run(["yt-dlp", "--no-warnings", "-f", "bv*+ba/b", "-o", src, url])
        if r.returncode != 0 or not os.path.exists(src):
            sys.exit("✗ ダウンロード失敗。元動画が【完全非公開】だと取り込めません。"
                     "公開/限定公開にするか、動画ファイルを --file で渡してください。")


def transcribe(src, workdir, reuse_audio=None):
    audio = os.path.join(workdir, "audio.json")
    if os.path.exists(audio):
        log("② 文字起こし: 既存 → スキップ"); return audio
    if reuse_audio and os.path.exists(reuse_audio):
        shutil.copy(reuse_audio, audio); log("② 文字起こし: 既存流用"); return audio
    wav = os.path.join(workdir, "audio.wav")
    log("② 文字起こし中…（mlx_whisper・数分）")
    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", src, "-vn",
                    "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", wav], check=True)
    subprocess.run(["mlx_whisper", wav, "--model", "mlx-community/whisper-large-v3-turbo",
                    "--language", "ja", "--word-timestamps", "True",
                    "--output-format", "json", "--output-dir", workdir, "--output-name", "audio"], check=True)
    return audio


def build(src, clips, outdir, workers, max_build):
    os.makedirs(outdir, exist_ok=True)
    if max_build:
        clips = clips[:max_build]
    src_wh = R.probe_wh(src)
    log(f"④ 量産: {len(clips)}本 / 並列{workers}")

    def one(c):
        out = os.path.join(outdir, f"clip_{c['idx']:02d}.mp4")
        tmp = os.path.join(outdir, f"_tmp_{c['idx']:02d}")
        R.render_one(src, c["start"], c["end"], c["kicker"], c["title"], out, tmp, src_wh=src_wh)
        shutil.rmtree(tmp, ignore_errors=True)
        return c["idx"], out

    ok, err = [], []
    with cf.ThreadPoolExecutor(max_workers=workers) as ex:
        futs = {ex.submit(one, c): c for c in clips}
        for f in cf.as_completed(futs):
            c = futs[f]
            try:
                idx, out = f.result(); ok.append((idx, out))
                log(f"   ✓ #{idx:02d} 「{c['kicker']}／{c['title']}」")
            except Exception as e:
                err.append((c["idx"], str(e))); log(f"   ✗ #{c['idx']:02d} {str(e)[:100]}")
    return [o for _, o in sorted(ok)], err


def write_manifest_and_zip(outdir, clips, name):
    mp4s = sorted(glob.glob(os.path.join(outdir, "clip_*.mp4")))
    by = {c["idx"]: c for c in clips}
    lines = [f"YouTube切り抜き 生成結果：{name}", f"本数: {len(mp4s)}本", ""]
    for m in mp4s:
        idx = int(os.path.basename(m).split("_")[1].split(".")[0])
        c = by.get(idx, {})
        lines.append(f"clip_{idx:02d}.mp4  「{c.get('kicker','')}／{c.get('title','')}」  {c.get('dur','?')}s")
    lines += ["", "※各ショート下部「▼本編はチャンネルへ▼」から本編へ送客できます。",
              "※アップロードはご自身のチャンネルへ行ってください。"]
    open(os.path.join(outdir, "一覧.txt"), "w").write("\n".join(lines))
    zip_path = os.path.join(os.path.dirname(outdir), f"{name}_切り抜き{len(mp4s)}本.zip")
    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_STORED) as z:
        for m in mp4s:
            z.write(m, arcname=os.path.basename(m))
        z.write(os.path.join(outdir, "一覧.txt"), arcname="一覧.txt")
    return zip_path, len(mp4s)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--url"); ap.add_argument("--file")
    ap.add_argument("--out", default=DEFAULT_OUT)
    ap.add_argument("--name", default=time.strftime("%m%d_%H%M"))
    ap.add_argument("--n", type=int, default=30)
    ap.add_argument("--channel-desc", default="")
    ap.add_argument("--min-score", type=int, default=55)
    ap.add_argument("--workers", type=int, default=3)
    ap.add_argument("--max-build", type=int)
    ap.add_argument("--reuse-audio")
    ap.add_argument("--no-open", action="store_true")
    args = ap.parse_args()
    if not args.url and not args.file:
        sys.exit("✗ --url か --file が必要です。")

    preflight()
    base = os.path.join(args.out, args.name)
    workdir = os.path.join(base, "_work")
    os.makedirs(workdir, exist_ok=True)
    src = os.path.join(workdir, "source.mp4")
    try:
        ingest(args.url, args.file, src)
        audio = transcribe(src, workdir, reuse_audio=args.reuse_audio)
        log("③ AI採点で区間選定中…（1〜3分）")
        clips = S.select(audio, n=args.n, channel_desc=args.channel_desc, min_score=args.min_score)
        if not clips:
            sys.exit("✗ 採用区間が0本でした。素材を変えるか --min-score を下げてください。")
        log(f"   選定 {len(clips)}本: " + " / ".join(f"{c['kicker']}／{c['title']}" for c in clips[:5]) + " …")
        built, errors = build(src, clips, base, args.workers, args.max_build)
        if not built:
            sys.exit("✗ 生成0本。" + (str(errors[:1]) if errors else ""))
        zip_path, n = write_manifest_and_zip(base, clips, args.name)
        print("\n" + "=" * 46)
        print(f"✅ 完成: {n}本")
        print(f"  フォルダ: {base}")
        print(f"  まとめzip: {zip_path}")
        print("  各ショートをご自身のYouTubeチャンネルに投稿してください。")
        print("=" * 46)
        if not args.no_open and sys.platform == "darwin":
            subprocess.run(["open", base])
    except SystemExit:
        raise
    except Exception as e:
        print("✗ エラー:", e)
        print(traceback.format_exc()[-800:])
        sys.exit(1)


if __name__ == "__main__":
    main()
