"""Сравнение ПОКРЫТИЯ captions двух наборов (v1 vs v2) — отдельно от обучения.

Отвечает на один вопрос: одинаковый ли набор картинок покрыт текстом в v1 и v2?
Если да — сравнение R@1 между версиями чистое. Если нет — разница R@1 может
объясняться покрытием, а не качеством текста (см. протокол §5.4, §0.4).

Использует ТЕ ЖЕ функции чтения, что и обучение (load_text_descriptions,
combine_text_levels), поэтому «есть текст» здесь означает ровно то же, что
видит модель: непустой текст для выбранных text_levels, а не заглушку
"No description available.".

Запуск:
    python3 check_coverage.py \
    --data_root "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/GTA-UAV-LR" \
    --captions_v1 "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/GTA-UAV-LR-captions_ v1" \
    --captions_v2 "/media/servml/SSD_2_2TB/datasets/cvgl_datasets/GTA-UAV-LR-captions_v2" \
    --text_levels level1 level2 level3

"""
from __future__ import annotations

import argparse
import json
from pathlib import Path

from src.data.gta_uav import combine_text_levels, load_text_descriptions
from src.data.gta_uav_eval import collect_gallery_names


def _has_real_text(
    name: str,
    descriptions: dict[str, dict],
    text_levels: list[str],
) -> bool:
    """True, если для картинки есть непустой текст на выбранных уровнях.

    Повторяет семантику обучения: пустой результат combine_text_levels →
    модель получит заглушку, т.е. текста фактически нет.
    """
    desc = descriptions.get(name)
    if not desc:
        return False
    # combine_text_levels возвращает заглушку, когда все уровни пустые →
    # это и есть «текста фактически нет».
    combined = combine_text_levels(desc, text_levels)
    return combined != "No description available."


def _covered_set(
    names: list[str],
    descriptions: dict[str, dict],
    text_levels: list[str],
) -> set[str]:
    """Подмножество имён, у которых есть реальный текст."""
    return {n for n in names if _has_real_text(n, descriptions, text_levels)}


def _report_side(
    title: str,
    names: list[str],
    desc_v1: dict[str, dict],
    desc_v2: dict[str, dict],
    text_levels: list[str],
) -> None:
    """Напечатать покрытие v1/v2 и пересечение для одной стороны."""
    total = len(names)
    cov_v1 = _covered_set(names, desc_v1, text_levels)
    cov_v2 = _covered_set(names, desc_v2, text_levels)
    both = cov_v1 & cov_v2
    only_v1 = cov_v1 - cov_v2
    only_v2 = cov_v2 - cov_v1

    def pct(n: int) -> str:
        return f"{n:>6d} / {total} ({100.0 * n / max(total, 1):5.1f}%)"

    print(f"\n── {title} (всего уникальных: {total}) " + "─" * 20)
    print(f"  покрытие v1:            {pct(len(cov_v1))}")
    print(f"  покрытие v2:            {pct(len(cov_v2))}")
    print(f"  есть в ОБОИХ (пересеч): {pct(len(both))}")
    print(f"  только v1:              {len(only_v1)}")
    print(f"  только v2:              {len(only_v2)}")

    if len(cov_v1) == len(cov_v2) == len(both):
        print("  ✅ наборы совпадают → покрытие не влияет на сравнение")
    else:
        diff = abs(len(cov_v1) - len(cov_v2))
        print(
            f"  ⚠️  расхождение: |v1−v2| = {diff} картинок; "
            f"для честного сравнения оценивайте на пересечении ({len(both)})"
        )


def _report_split(
    data_root: Path,
    meta_name: str,
    desc_by_view: dict[str, tuple[dict, dict]],
    text_levels: list[str],
) -> None:
    """Покрытие v1/v2 для одного 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)):
        desc_v1, desc_v2 = desc_by_view[view]
        _report_side(
            f"{view.upper()} {'queries' if view == 'drone' else 'gallery'}",
            names, desc_v1, desc_v2, text_levels,
        )


def main(args: argparse.Namespace) -> None:
    data_root = Path(args.data_root)
    text_levels = list(args.text_levels)

    print("=" * 64)
    print(f"COVERAGE CHECK  |  text_levels={text_levels}")
    print("=" * 64)

    # Описания не зависят от split — грузим один раз на каждую сторону.
    desc_by_view = {
        view: (
            load_text_descriptions(args.captions_v1, view_type=view),
            load_text_descriptions(args.captions_v2, view_type=view),
        )
        for view in ("drone", "satellite")
    }

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


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Сравнить покрытие captions v1 vs v2")
    p.add_argument("--data_root", type=str, required=True)
    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")
    p.add_argument("--captions_v1", type=str, required=True)
    p.add_argument("--captions_v2", type=str, required=True)
    p.add_argument("--text_levels", nargs="+", default=["level1"])
    return p.parse_args()


if __name__ == "__main__":
    main(parse_args())
