"""Теги образа ``kindest/node`` с Docker Hub для интерактивного выбора версии Kubernetes.

Фильтр: только стабильные семверы ``vX.Y.Z`` (без ``-rc`` и т.п.), версия **>= 1.19.0**.
Сортировка: от новых к старым.

Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""

from __future__ import annotations

import json
import logging
import os
import re
import urllib.error
import urllib.request
from typing import Final

logger = logging.getLogger("kind_k8s.kindest_node_tags")

# Минимальная версия Kubernetes (включительно) для списка выбора
MIN_K8S: Final[tuple[int, int, int]] = (1, 19, 0)

# Первая страница API Docker Hub для репозитория kindest/node
_HUB_FIRST_PAGE: Final[str] = (
    "https://hub.docker.com/v2/repositories/kindest/node/tags?page_size=100"
)

_USER_AGENT: Final[str] = "kind-k8s-tools/1.0 (+https://devops.org.ru)"

# Семвер без префикса v, только три числа (стабильные релизы)
_TAG_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)$")


def parse_semver_tag(name: str) -> tuple[int, int, int] | None:
    """Разобрать тег вида ``v1.29.4`` / ``1.29.4`` в ``(major, minor, patch)`` или ``None``."""
    m = _TAG_RE.match(name.strip())
    if not m:
        return None
    return int(m.group(1)), int(m.group(2)), int(m.group(3))


def normalize_tag_v_prefix(tag: str) -> str:
    """Единый вид тега: ``v1.29.4`` или ``latest`` (плавающий тег Docker Hub)."""
    s = tag.strip().lower()
    if s in ("latest", "vlatest"):
        return "latest"
    s = s.removeprefix("v")
    return f"v{s}"


def _version_ok(tup: tuple[int, int, int], minimum: tuple[int, int, int]) -> bool:
    return tup >= minimum


def tags_from_hub_json_results(
    results: list[dict],
    *,
    minimum: tuple[int, int, int] = MIN_K8S,
) -> list[tuple[tuple[int, int, int], str]]:
    """Из элементов ``results`` API Hub извлечь подходящие ``(tuple, канонический_тег)``."""
    out: list[tuple[tuple[int, int, int], str]] = []
    for item in results:
        name = (item.get("name") or "").strip()
        if not name or name == "latest":
            continue
        tup = parse_semver_tag(name)
        if tup is None:
            continue
        if not _version_ok(tup, minimum):
            continue
        out.append((tup, normalize_tag_v_prefix(name)))
    return out


def merge_sort_unique_tags(
    collected: list[tuple[tuple[int, int, int], str]],
) -> list[str]:
    """Убрать дубликаты по ``(major, minor, patch)``, отсортировать от новых к старым."""
    by_key: dict[tuple[int, int, int], str] = {}
    for tup, canon in collected:
        by_key[tup] = canon
    ordered = sorted(by_key.keys(), reverse=True)
    return [by_key[k] for k in ordered]


def _default_max_hub_pages() -> int:
    """Верхняя граница числа запросов к API Hub (защита от бесконечного цикла)."""
    raw = (os.environ.get("KIND_K8S_HUB_TAGS_MAX_PAGES") or "").strip()
    if raw.isdigit():
        return max(1, min(int(raw), 500))
    # Старые семверы (1.19.x …) часто только на поздних страницах Hub; при необходимости задайте KIND_K8S_HUB_TAGS_MAX_PAGES (до 500).
    return 120


def fetch_kindest_node_tags(
    *,
    minimum: tuple[int, int, int] = MIN_K8S,
    max_pages: int | None = None,
    timeout: float = 45.0,
) -> list[str]:
    """
    Загрузить теги с Docker Hub (постранично).

    Возвращает список: первым элементом всегда ``latest``, далее стабильные ``vX.Y.Z`` от новых к старым (>= minimum).

    При ошибке сети или HTTP возвращает пустой список (в лог — предупреждение).
    """
    pages_cap = max_pages if max_pages is not None else _default_max_hub_pages()

    collected: list[tuple[tuple[int, int, int], str]] = []
    url: str | None = _HUB_FIRST_PAGE
    page_idx = 0

    while url and page_idx < pages_cap:
        page_idx += 1
        req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                payload = json.load(resp)
        except urllib.error.HTTPError as e:
            logger.warning(
                "Docker Hub HTTP %s при загрузке тегов kindest/node: %s",
                e.code,
                e.reason,
            )
            break
        except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as e:
            logger.warning("Не удалось загрузить теги kindest/node: %s", e)
            break

        batch = tags_from_hub_json_results(payload.get("results") or [], minimum=minimum)
        collected.extend(batch)
        url = payload.get("next") or None
        logger.debug(
            "kindest/node Docker Hub: страница %s/%s, подходящих тегов на странице %s",
            page_idx,
            pages_cap,
            len(batch),
        )

    tags = merge_sort_unique_tags(collected)
    out = ["latest"] + tags
    logger.info(
        "kindest/node: в списке %s тегов (включая latest), стабильных семверов %s >= %s.%s.%s (страниц API: %s)",
        len(out),
        len(tags),
        minimum[0],
        minimum[1],
        minimum[2],
        page_idx,
    )
    return out
