"""Редактирование ``kind-config.yaml`` и ``meta.json`` в каталоге кластера.

Kind **не** меняет число нод и образ узлов у уже созданного кластера: сохранённые файлы
используются при следующем ``kind create`` (например после ``kind delete`` и кнопки «Старт»
или полного пересоздания).

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

from __future__ import annotations

import json
import logging
import re
from typing import Any

import yaml

from core.cluster_lifecycle import build_kind_config_yaml, read_meta_json, validate_cluster_name
from kindest_node_tags import normalize_tag_v_prefix
from kind_k8s_paths import clusters_dir, data_root

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

# Пояснение для UI: до следующего «Старт» kind продолжает использовать старые узлы; после сохранения
# конфига выставляется флаг — при «Старт» выполняется kind delete + create по новому kind-config.yaml.
NOTE_REGISTERED_IN_KIND = (
    "Конфиг сохранён. При следующем «Старт» кластер будет пересоздан в kind по обновлённому "
    "kind-config.yaml (запись в kind удаляется и создаётся заново; каталог clusters/<имя>/ сохраняется). "
    "Пока вы не нажали «Старт», работают прежние узлы."
)

NOTE_NOT_IN_KIND = (
    "Конфигурация сохранена. При «Старт» или создании кластера будет использован этот файл."
)


def validate_kind_cluster_yaml_root(data: Any) -> str | None:
    """
    Проверить распарсенный YAML корня конфига kind.

    Возвращает текст ошибки для пользователя или ``None``, если структура допустима.
    """
    if not isinstance(data, dict):
        return "Корень YAML должен быть объектом (mapping)."
    if data.get("kind") != "Cluster":
        return "Ожидается поле kind: Cluster."
    api = str(data.get("apiVersion") or "")
    if "kind.x-k8s.io" not in api:
        return "Ожидается apiVersion с группой kind.x-k8s.io (например kind.x-k8s.io/v1alpha4)."
    nodes = data.get("nodes")
    if not isinstance(nodes, list) or len(nodes) < 1:
        return "Нужен непустой список nodes."
    has_cp = False
    for i, n in enumerate(nodes):
        if not isinstance(n, dict):
            return f"Элемент nodes[{i}] должен быть объектом."
        role = n.get("role")
        if role == "control-plane":
            has_cp = True
        elif role != "worker":
            return f"Узел nodes[{i}]: role должен быть control-plane или worker."
    if not has_cp:
        return "В nodes должна быть хотя бы одна нода с role: control-plane."
    return None


def _count_workers(nodes: list[Any]) -> int:
    return sum(1 for n in nodes if isinstance(n, dict) and n.get("role") == "worker")


def _first_image_from_nodes(nodes: list[Any]) -> str | None:
    for n in nodes:
        if isinstance(n, dict):
            img = n.get("image")
            if isinstance(img, str) and img.strip():
                return img.strip()
    return None


def _tag_from_node_image(image: str) -> str | None:
    m = re.search(r":([^:]+)$", image.strip())
    return m.group(1) if m else None


class ClusterConfigEditError(Exception):
    """Ошибка валидации или отсутствия каталога (сообщение для HTTP 400/404)."""

    def __init__(self, message: str, *, not_found: bool = False) -> None:
        super().__init__(message)
        self.not_found = not_found


def apply_cluster_config_update(
    cluster_name: str,
    *,
    kubernetes_version: str | None,
    workers: int | None,
    kind_config_yaml: str | None,
    description: str | None,
) -> tuple[bool, str]:
    """
    Обновить ``kind-config.yaml`` и/или поля ``meta.json``.

    Возвращает (обновлён ли YAML, краткое сообщение для ответа API).
    """
    if not validate_cluster_name(cluster_name):
        raise ClusterConfigEditError("Некорректное имя кластера")

    out_dir = clusters_dir() / cluster_name
    if not out_dir.is_dir():
        raise ClusterConfigEditError("Каталог кластера не найден", not_found=True)

    meta: dict[str, Any] = dict(read_meta_json(cluster_name) or {})
    cfg_path = out_dir / "kind-config.yaml"
    root = data_root()
    updated_yaml = False

    yaml_raw = (kind_config_yaml or "").strip()
    if yaml_raw:
        try:
            parsed = yaml.safe_load(yaml_raw)
        except yaml.YAMLError as e:
            raise ClusterConfigEditError(f"Ошибка разбора YAML: {e}") from e
        err = validate_kind_cluster_yaml_root(parsed)
        if err:
            raise ClusterConfigEditError(err)
        if not yaml_raw.endswith("\n"):
            yaml_raw += "\n"
        cfg_path.write_text(yaml_raw, encoding="utf-8")
        updated_yaml = True
        nodes = parsed.get("nodes") or []
        if isinstance(nodes, list):
            meta["worker_nodes"] = _count_workers(nodes)
            img = _first_image_from_nodes(nodes)
            if img:
                meta["node_image"] = img
                tag = _tag_from_node_image(img)
                if tag:
                    meta["kubernetes_version_tag"] = tag
        logger.info("Обновлён kind-config.yaml (расширенный режим) для кластера «%s»", cluster_name)
    elif kubernetes_version is not None or workers is not None:
        ver_src = (kubernetes_version or "").strip() or str(meta.get("kubernetes_version_tag") or "v1.29.4")
        ver_tag = normalize_tag_v_prefix(ver_src)
        w_val = meta.get("worker_nodes")
        try:
            w_default = int(w_val) if w_val is not None else 0
        except (TypeError, ValueError):
            w_default = 0
        w = workers if workers is not None else w_default
        if w < 0 or w > 20:
            raise ClusterConfigEditError("Число worker-нод должно быть от 0 до 20.")
        node_image = f"kindest/node:{ver_tag}"
        text = build_kind_config_yaml(node_image=node_image, workers=w)
        cfg_path.write_text(text, encoding="utf-8")
        updated_yaml = True
        meta["kubernetes_version_tag"] = ver_tag
        meta["node_image"] = node_image
        meta["worker_nodes"] = w
        logger.info(
            "Пересобран kind-config.yaml для «%s»: workers=%s, образ %s",
            cluster_name,
            w,
            node_image,
        )
    elif description is None:
        raise ClusterConfigEditError("Укажите версию/workers, полный YAML kind-config или описание.")

    # После смены kind-config.yaml при «Старт» (кластер в kind) — пересоздание, а не docker start.
    if updated_yaml:
        meta["apply_kind_config_on_next_start"] = True

    if description is not None:
        d = description.strip()
        if d:
            meta["description"] = d[:2000]
        else:
            meta.pop("description", None)

    meta["cluster_name"] = cluster_name
    if cfg_path.is_file():
        try:
            meta["kind_config_path"] = str(cfg_path.relative_to(root))
        except ValueError:
            meta["kind_config_path"] = str(cfg_path)

    meta_path = out_dir / "meta.json"
    meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
    logger.info("Обновлён meta.json для кластера «%s»", cluster_name)

    return updated_yaml, "Сохранено."


def read_cluster_config_bundle(cluster_name: str) -> dict[str, Any]:
    """Собрать данные для GET /clusters/{name}/config (без флагов kind — их добавляет эндпоинт)."""
    if not validate_cluster_name(cluster_name):
        raise ClusterConfigEditError("Некорректное имя кластера")

    out_dir = clusters_dir() / cluster_name
    if not out_dir.is_dir():
        raise ClusterConfigEditError("Каталог кластера не найден", not_found=True)

    cfg_path = out_dir / "kind-config.yaml"
    yaml_text: str | None = None
    if cfg_path.is_file():
        yaml_text = cfg_path.read_text(encoding="utf-8")

    meta = read_meta_json(cluster_name)
    return {
        "cluster_name": cluster_name,
        "meta": meta if isinstance(meta, dict) else {},
        "kind_config_yaml": yaml_text,
        "has_kind_config": cfg_path.is_file(),
    }
