"""Человекочитаемые отчёты по версиям модели."""

from __future__ import annotations

import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)

HOW_TO_READ = [
    "«Точность с первого раза» — доля заказов, где первый рекомендованный размер совпал с купленным (по сетке артикула, как в API).",
    "«Размер в паре» — купленный размер попал в 1–2 размера ответа API.",
    "«Уверенность» — как часто модель отдаёт один размер с большим отрывом (разница ≥30 п.п.). Не равна точности.",
    "Сравнивайте версии с похожим объёмом данных; скачок числа записей тоже влияет на цифры.",
]


def _hit_pct(value: Optional[float]) -> Optional[float]:
    if value is None:
        return None
    return round(float(value) * 100.0, 1)


def _grade(hit_first_pct: Optional[float]) -> Tuple[str, str]:
    if hit_first_pct is None:
        return "?", "Недостаточно данных для оценки"
    if hit_first_pct >= 70:
        return "A", "Высокая точность с первого раза"
    if hit_first_pct >= 55:
        return "B", "Хорошая точность для широкой сетки размеров"
    if hit_first_pct >= 40:
        return "C", "Средняя точность; для каталога с большой сеткой размеров ожидаемо"
    return "D", "Низкая точность — стоит проверить данные и откат"


def _extract_holdout(metrics: Dict[str, Any]) -> Dict[str, Any]:
    holdout = metrics.get("holdout") or {}
    return {
        "accuracy_top1": holdout.get("hit_first_api", holdout.get("accuracy_top1")),
        "accuracy_top2": holdout.get("hit_top2_api", holdout.get("accuracy_top2")),
        "accuracy_top3": holdout.get("accuracy_top3"),
        "hit_first_api": holdout.get("hit_first_api"),
        "hit_top2_api": holdout.get("hit_top2_api"),
        "f1_weighted": holdout.get("f1_weighted"),
        "log_loss": holdout.get("log_loss"),
        "confident_share": holdout.get("confident_share"),
    }


def load_metrics(model_dir: Path, version: int) -> Optional[Dict[str, Any]]:
    path = model_dir / f"v{version}" / "metrics.json"
    if not path.exists():
        return None
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except Exception as e:
        logger.warning("Cannot read metrics for v%s: %s", version, e)
        return None


def build_version_report(
    version: int,
    metrics: Dict[str, Any],
    active_version: int,
    previous_metrics: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """
    Строит карточку версии с plain- и technical-слоями.

    :param version: Номер версии.
    :param metrics: Содержимое metrics.json.
    :param active_version: Активная версия.
    :param previous_metrics: Метрики предыдущей версии для vs_previous.
    """
    holdout = _extract_holdout(metrics)
    hit_first = _hit_pct(holdout.get("accuracy_top1"))
    if holdout.get("hit_top2_api") is not None:
        hit_top3 = _hit_pct(holdout.get("hit_top2_api"))
    else:
        hit_top3 = _hit_pct(holdout.get("accuracy_top3"))
    confident = _hit_pct(holdout.get("confident_share"))
    grade, grade_label = _grade(hit_first)
    records = int(metrics.get("records_used") or 0)
    vs_previous = None
    if previous_metrics:
        prev = _extract_holdout(previous_metrics)
        prev_hit = _hit_pct(prev.get("accuracy_top1"))
        if hit_first is not None and prev_hit is not None:
            delta = round(hit_first - prev_hit, 1)
            sign = "+" if delta > 0 else ""
            vs_previous = f"На {sign}{delta} п.п. к предыдущей версии по «с первого раза»"
    return {
        "version": version,
        "is_active": version == active_version,
        "trained_at": metrics.get("trained_at"),
        "records_used": records,
        "records_label": f"обучена на {records:,} примерах".replace(",", " "),
        "scores": {
            "hit_first_pct": hit_first,
            "hit_top3_pct": hit_top3,
            "confident_share_pct": confident,
            "grade": grade,
            "grade_label": grade_label,
        },
        "plain": {
            "hit_first": (
                f"Примерно {int(round(hit_first))} из 100 — верный размер сразу"
                if hit_first is not None
                else "Нет данных по точности с первого раза"
            ),
            "hit_top3": (
                f"Примерно {int(round(hit_top3))} из 100 — верный размер в 1–2 ответах API"
                if hit_top3 is not None
                else "Нет данных по попаданию в ответ API"
            ),
            "vs_previous": vs_previous,
        },
        "technical": {
            "accuracy_top1": holdout.get("accuracy_top1"),
            "accuracy_top2": holdout.get("accuracy_top2"),
            "accuracy_top3": holdout.get("accuracy_top3"),
            "f1_weighted": holdout.get("f1_weighted"),
            "log_loss": holdout.get("log_loss"),
            "n_classes": metrics.get("n_classes"),
        },
    }


def list_version_numbers(model_dir: Path) -> List[int]:
    versions = []
    for path in model_dir.glob("v*"):
        if path.is_dir() and path.name[1:].isdigit():
            versions.append(int(path.name[1:]))
    return sorted(versions)


def build_models_report(model_dir: Path, active_version: int) -> Dict[str, Any]:
    """
    Сводка по всем версиям с recommendation.

    :param model_dir: Каталог models.
    :param active_version: Активная версия.
    """
    versions = list_version_numbers(model_dir)
    cards: List[Dict[str, Any]] = []
    prev_metrics = None
    for ver in versions:
        metrics = load_metrics(model_dir, ver)
        if not metrics:
            continue
        cards.append(build_version_report(ver, metrics, active_version, prev_metrics))
        prev_metrics = metrics
    best_first = None
    best_top3 = None
    best_first_val = -1.0
    best_top3_val = -1.0
    for card in cards:
        hf = card["scores"].get("hit_first_pct")
        ht = card["scores"].get("hit_top3_pct")
        if hf is not None and hf >= best_first_val:
            best_first_val = hf
            best_first = card["version"]
        if ht is not None and ht >= best_top3_val:
            best_top3_val = ht
            best_top3 = card["version"]
    active_card = next((c for c in cards if c["version"] == active_version), None)
    if active_card and active_card["scores"].get("hit_first_pct") is not None:
        summary = (
            f"Сейчас в работе версия {active_version}. "
            f"Из 100 похожих заказов примерно {int(round(active_card['scores']['hit_first_pct']))} раза "
            f"размер угадан с первого раза"
        )
        if active_card["scores"].get("hit_top3_pct") is not None:
            summary += (
                f", в {int(round(active_card['scores']['hit_top3_pct']))} случаях "
                f"правильный размер попал в тройку рекомендаций."
            )
        else:
            summary += "."
    else:
        summary = f"Активная версия: {active_version}." if active_version else "Модель не обучена."
    recommendation = None
    if best_first is not None:
        if best_first == active_version:
            recommendation = f"Версия {active_version} лучшая по «с первого раза» среди сохранённых — оставлять активной."
        else:
            recommendation = (
                f"По holdout лучше версия {best_first} "
                f"(сейчас активна {active_version}). Рассмотрите активацию через /admin/models/{{v}}/activate."
            )
    return {
        "active_version": active_version,
        "summary": summary,
        "how_to_read": HOW_TO_READ,
        "versions": list(reversed(cards)),
        "best_by_hit_first": best_first,
        "best_by_hit_top3": best_top3,
        "recommendation": recommendation,
    }


def compare_versions(model_dir: Path, a: int, b: int) -> Dict[str, Any]:
    """
    Сравнивает две версии по holdout-метрикам.

    :param model_dir: Каталог models.
    :param a: Первая версия.
    :param b: Вторая версия.
    """
    ma = load_metrics(model_dir, a)
    mb = load_metrics(model_dir, b)
    if not ma:
        raise ValueError(f"No metrics for version v{a}")
    if not mb:
        raise ValueError(f"No metrics for version v{b}")
    ha = _extract_holdout(ma)
    hb = _extract_holdout(mb)
    hit_a = _hit_pct(ha.get("accuracy_top1")) or 0.0
    hit_b = _hit_pct(hb.get("accuracy_top1")) or 0.0
    top3_a = _hit_pct(ha.get("accuracy_top3")) or 0.0
    top3_b = _hit_pct(hb.get("accuracy_top3")) or 0.0
    rec_a = int(ma.get("records_used") or 0)
    rec_b = int(mb.get("records_used") or 0)
    hit_delta = round(hit_b - hit_a, 1)
    top3_delta = round(top3_b - top3_a, 1)
    records_pct = round(((rec_b - rec_a) / rec_a) * 100.0, 1) if rec_a else None
    score_a = hit_a + 0.5 * top3_a
    score_b = hit_b + 0.5 * top3_b
    winner = b if score_b >= score_a else a
    summary = (
        f"Версия {winner} лучше: {hit_delta:+.1f} п.п. «с первого раза» (v{b} vs v{a}), "
        f"{top3_delta:+.1f} п.п. «в тройке»."
    )
    if records_pct is not None:
        summary += f" Данных у v{b} на {records_pct:+.1f}% относительно v{a}."
    return {
        "a": a,
        "b": b,
        "winner": winner,
        "summary": summary,
        "deltas": {
            "hit_first_pp": hit_delta,
            "hit_top3_pp": top3_delta,
            "records_pct": records_pct,
        },
    }
