"""Измерение процента обрезаемых описаний по комбинациям уровней.

Обрезка (truncation до 248 токенов) зависит ТОЛЬКО от текста и токенизатора,
не от обучения. Поэтому процент обрезанных сэмплов можно посчитать по готовым
данным — результат идентичен тому, что было во время обучения.

Считается ОТДЕЛЬНО по каждому split (cross-area train/test) и по каждой стороне
(drone queries / satellite gallery), чтобы процент отражал ровно те сэмплы, что
реально идут в обучение и в eval, а не весь датасет целиком (ср. check_coverage.py).

Для каждой из 6 комбинаций уровней считает:
    - сколько сэмплов превышает 248 токенов (обрезается)
    - средняя/максимальная длина в токенах
    - средний «перебор» у обрезанных (на сколько токенов текст длиннее 248)

Запуск:
    python measure_truncation.py \
        --data_root "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/GTA-UAV-LR" \
        --descriptions_path "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/GTA-UAV-LR-captions_ v1" \
        --version v1
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

sys.path.insert(0, ".")

from src.data.gta_uav import load_text_descriptions, combine_text_levels
from src.data.gta_uav_eval import collect_gallery_names
from src.models.dgtrs.model import tokenize_dgtrs

CONTEXT_LENGTH = 248

# те же 6 комбинаций, что в экспериментах
COMBINATIONS = [
    ["level1"],
    ["level2"],
    ["level3"],
    ["level1", "level2"],
    ["level1", "level3"],
    ["level1", "level2", "level3"],
]


def count_tokens(text: str) -> int:
    """Реальное число ненулевых токенов ДО обрезки.

    Токенизируем с большим запасом (context_length=1024), чтобы увидеть полную
    длину без потери токенов на лимите 248.
    """
    if not text.strip():
        return 0
    toks = tokenize_dgtrs(text, context_length=1024, truncate=True)
    return int((toks != 0).sum())


def measure(names: list[str], descriptions: dict, combo: list[str]) -> dict:
    """Посчитать статистику обрезки для одной комбинации уровней.

    Учитываются только имена из `names` (сэмплы конкретного split/стороны),
    у которых есть непустой склеенный текст.
    """
    n_total = 0
    n_truncated = 0
    lengths = []
    overflows = []

    for name in names:
        desc = descriptions.get(name)
        if not desc:
            continue
        text = combine_text_levels(desc, combo)
        if not text.strip():
            continue
        n = count_tokens(text)
        n_total += 1
        lengths.append(n)
        if n > CONTEXT_LENGTH:
            n_truncated += 1
            overflows.append(n - CONTEXT_LENGTH)

    pct = 100.0 * n_truncated / n_total if n_total else 0.0
    avg_len = sum(lengths) / len(lengths) if lengths else 0
    max_len = max(lengths) if lengths else 0
    avg_overflow = sum(overflows) / len(overflows) if overflows else 0

    return {
        "combo": " + ".join(combo),
        "n_total": n_total,
        "n_truncated": n_truncated,
        "pct_truncated": pct,
        "avg_len": avg_len,
        "max_len": max_len,
        "avg_overflow": avg_overflow,
    }


def _report_side(title: str, names: list[str], descriptions: dict) -> None:
    """Напечатать таблицу обрезки по всем комбинациям для одной стороны."""
    print(f"\n── {title} (всего уникальных: {len(names)}) " + "─" * 20)
    print(f"{'Комбинация':<26} {'Всего':>7} {'Обрезано':>9} {'%':>7} "
          f"{'СрДлина':>8} {'МаксДлина':>10} {'СрПеребор':>10}")
    print("-" * 82)
    for combo in COMBINATIONS:
        r = measure(names, descriptions, combo)
        print(f"{r['combo']:<26} {r['n_total']:>7} {r['n_truncated']:>9} "
              f"{r['pct_truncated']:>6.1f}% {r['avg_len']:>8.0f} "
              f"{r['max_len']:>10} {r['avg_overflow']:>10.0f}")
    print("-" * 82)


def _report_split(
    data_root: Path,
    meta_name: str,
    desc_by_view: dict[str, dict],
) -> None:
    """Обрезка v для одного split-файла (train или test), обе стороны."""
    meta_path = data_root / meta_name
    if not meta_path.exists():
        print(f"\n⚠️  Пропущен (нет файла): {meta_path}")
        return

    with open(meta_path) as f:
        pairs_meta = json.load(f)

    # Дрон-запросы: уникальные имена картинок дрона в split.
    drone_names = sorted({e["drone_img_name"] for e in pairs_meta})
    # Спутниковая галерея: те же уникальные тайлы, что и в eval.
    gallery_names, _ = collect_gallery_names(pairs_meta)

    print("\n" + "=" * 64)
    print(f"SPLIT: {meta_name}  ({len(pairs_meta)} пар)")
    print("=" * 64)

    for view, names in (("drone", drone_names), ("satellite", gallery_names)):
        _report_side(
            f"{view.upper()} {'queries' if view == 'drone' else 'gallery'}",
            names, desc_by_view[view],
        )


def main():
    args = parse_args()
    data_root = Path(args.data_root)

    print("=" * 64)
    print(f"TRUNCATION CHECK  |  version={args.version}  |  лимит={CONTEXT_LENGTH} токенов")
    print("=" * 64)

    # Описания не зависят от split — грузим один раз на каждую сторону.
    desc_by_view = {
        view: load_text_descriptions(args.descriptions_path, view_type=view)
        for view in ("drone", "satellite")
    }
    for view, desc in desc_by_view.items():
        print(f"Загружено {len(desc)} описаний ({view}, {args.version})")

    # Обрезка важна и на train (что модель УЧИТ), и на test (что видит eval).
    for meta_name in (args.train_meta, args.test_meta):
        _report_split(data_root, meta_name, desc_by_view)

    print("\nОбрезано = число сэмплов, где склеенный текст длиннее лимита "
          "(хвост, включая level3-якорь, отсекается).")


def parse_args():
    p = argparse.ArgumentParser(description="Измерение обрезки по комбинациям уровней и split")
    p.add_argument("--data_root", type=str, required=True)
    p.add_argument("--descriptions_path", type=str, required=True)
    p.add_argument("--version", type=str, default="v1")
    p.add_argument("--train_meta", type=str, default="cross-area-drone2sate-train.json")
    p.add_argument("--test_meta", type=str, default="cross-area-drone2sate-test.json")
    return p.parse_args()


if __name__ == "__main__":
    main()
