#!/usr/bin/env python3
"""Собрать локальную музыкальную библиотеку с измерениями, а не просто папку.

Папка с сорока пятью минусовками — это не библиотека, а сорок пять файлов.
Библиотека появляется, когда у каждого трека есть числа, по которым его можно
выбрать не слушая: сколько энергии в речевой полосе, сколько резких атак сверху,
какой темп, где кончается интро.

Скрипт прогоняет каждый файл через тот же анализ, что `audition_music.py`,
складывает лучшие в папку скилла и пишет `index.json`, из которого пайплайн
берёт трек по задаче автоматически.

    python scripts/build_music_library.py "C:/.../minus" --top 30

Аудиофайлы намеренно исключены из `MANIFEST.json` — ровно как исходники шрифтов.
Манифест описывает **распространяемый** пакет, а музыка здесь чужая и лежит
локально для личного монтажа. Индекс с измерениями остаётся в пакете и не
содержит ничего, кроме чисел и имён.
"""
from __future__ import annotations

import argparse
import json
import shutil
import subprocess
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from audition_music import BRIEFS, analyse, score  # noqa: E402
from skill_config import ASSETS, emit, require_binary, utf8_stdout  # noqa: E402

LIBRARY = ASSETS / "music"


def main() -> None:
    utf8_stdout()
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("source", help="Папка с аудиофайлами")
    parser.add_argument("--name", default="nfs_minus", help="Имя коллекции внутри библиотеки")
    parser.add_argument("--top", type=int, default=30)
    parser.add_argument("--brief", default="calm_talk", choices=list(BRIEFS))
    parser.add_argument("--under-duration", type=float, default=110.0)
    parser.add_argument("--no-copy", action="store_true",
                        help="Только индекс, файлы оставить на месте")
    parser.add_argument("--bitrate", default="192k",
                        help="Битрейт копий в библиотеке. 'copy' — не пережимать. "
                             "192k достаточно: подкладка давится на 15 LU под голосом "
                             "и всё равно пересжимается в AAC на выдаче. Исходники "
                             "саундтреков часто лежат по 100-170 МБ, и такой файл "
                             "не примет ни один git-хостинг.")
    args = parser.parse_args()

    require_binary("ffmpeg")
    source = Path(args.source).resolve()
    if not source.is_dir():
        raise SystemExit(f"Нет папки: {source}")
    files = sorted(p for p in source.rglob("*")
                   if p.suffix.lower() in (".mp3", ".wav", ".m4a", ".flac", ".ogg"))
    if not files:
        raise SystemExit(f"В {source} нет аудиофайлов")

    brief = dict(BRIEFS[args.brief])
    brief["min_duration"] = args.under_duration

    measured: list[dict] = []
    failed: list[dict] = []
    for path in files:
        try:
            facts = analyse(path)
        except SystemExit as error:
            failed.append({"file": path.name, "why": str(error)[:200]})
            continue
        loud_at = facts.pop("_loud_at", 0.0)
        verdict = score(facts, brief)
        measured.append({**facts, **verdict, "loud_at": round(loud_at, 2),
                         "origin": str(path)})

    measured.sort(key=lambda item: item["score"], reverse=True)
    chosen = measured[:args.top]

    collection = LIBRARY / args.name
    collection.mkdir(parents=True, exist_ok=True)
    entries: list[dict] = []
    vanished: list[str] = []
    for rank, item in enumerate(chosen, start=1):
        origin = Path(item["origin"])
        target = collection / origin.name
        if not args.no_copy and not target.is_file():
            # Папка-источник может меняться, пока идёт анализ: у пользователя её
            # переименовывал собственный скрипт прямо во время прогона. Файл,
            # исчезнувший между измерением и копированием, — это пропуск с
            # отметкой, а не падение на двадцать девятом треке из тридцати.
            if not origin.is_file():
                vanished.append(origin.name)
                continue
            if args.bitrate == "copy":
                shutil.copy2(origin, target)
            else:
                done = subprocess.run(
                    ["ffmpeg", "-y", "-v", "error", "-nostdin", "-i", str(origin),
                     "-vn", "-c:a", "libmp3lame", "-b:a", args.bitrate,
                     "-ar", "48000", str(target)],
                    capture_output=True, text=True, errors="replace")
                if done.returncode:
                    vanished.append(f"{origin.name} (перекодирование не удалось)")
                    continue
        entries.append({
            "rank": rank,
            "file": origin.name,
            "path": str(target if not args.no_copy else origin),
            "score": item["score"],
            "duration": item["duration"],
            "bpm": item["bpm"],
            "mid_occupancy": item["mid_occupancy"],
            "hf_transients_per_second": item["hf_transients_per_second"],
            "syllabic_ratio": item["syllabic_ratio"],
            "intro_seconds": item["intro_seconds"],
            "dynamic_spread_db": item["dynamic_spread_db"],
            "peak_dbfs": item["peak_dbfs"],
            "pulse_strength": item["pulse_strength"],
            # Готовая подсказка, с какого места начинать: если интро длиннее
            # четырёх секунд, начинать с нуля значит отдать первые секунды ролика
            # под тишину.
            "suggested_start": (round(item["intro_seconds"] + 2, 1)
                                if item["intro_seconds"] > 4 else 0.0),
            "notes": item["notes"],
        })

    index = {
        "version": "1.0",
        "collection": args.name,
        "brief": args.brief,
        "brief_use": BRIEFS[args.brief]["use"],
        "measured_files": len(measured),
        "kept": len(entries),
        "tracks": entries,
        "how_to_pick": {
            "под_речь": "Бери верхний с mid_occupancy ниже 0.20 и "
                        "hf_transients_per_second ниже 2.0. Это трек, который будет "
                        "слышен телом, а не щелчками, и не займёт речевую полосу.",
            "под_клип": "bpm 110-150 и pulse_strength выше 0.3 — по такому можно резать.",
            "начало": "Бери suggested_start как --music-start: у трека с долгим интро "
                      "первые секунды почти пустые.",
        },
        "licence_note": (
            "Коммерческие записи. Лежат локально для личного монтажа и исключены "
            "из MANIFEST.json, как и исходники шрифтов: манифест описывает "
            "распространяемый пакет. На площадках сработает Content ID."),
    }
    index_path = LIBRARY / f"{args.name}.index.json"
    index_path.write_text(json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8")

    emit({
        "status": "ok",
        "measured": len(measured),
        "failed": failed,
        "vanished_between_measure_and_copy": vanished,
        "kept": len(entries),
        "collection": str(collection),
        "index": str(index_path),
        "top5": [{"rank": e["rank"], "file": e["file"], "score": e["score"],
                  "mid": e["mid_occupancy"], "hf": e["hf_transients_per_second"],
                  "bpm": e["bpm"], "start": e["suggested_start"]} for e in entries[:5]],
    })


if __name__ == "__main__":
    main()
