"""Helm-аддоны для выбранного кластера: ingress-nginx, kube-prometheus-stack, metrics-server, Istio+Kiali.

Операции синхронные (долгие таймауты — см. ``KIND_K8S_HELM_TIMEOUT_SEC``).
После каждой операции в ``clusters/<имя>/helm_addon_log.json`` **добавляется** новая запись в массив ``entries``
(история сохраняется, лимит ``KIND_K8S_HELM_ADDON_LOG_MAX_ENTRIES``).

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

from __future__ import annotations

import asyncio
import logging
import uuid
from collections.abc import Callable
from typing import Any

import yaml
from fastapi import APIRouter, Body, HTTPException

from core.cluster_lifecycle import validate_cluster_name
from core.helm_addons import (
    HelmAddonError,
    collect_installed_addons_values,
    helm_addon_snapshot,
    require_helm_binary,
    install_ingress_nginx,
    install_istio_and_kiali,
    install_kube_prometheus_stack,
    install_metrics_server,
    uninstall_ingress_nginx,
    uninstall_istio_and_kiali,
    uninstall_kube_prometheus_stack,
    uninstall_metrics_server,
)
from core.kind_guard import kind_cluster_lock
from core.provision_log import write_cluster_helm_addon_log
from models.schemas import (
    ClusterAddonsInstalledValuesResponse,
    ClusterAddonsStatusResponse,
    HelmAddonApplyResponse,
    InstalledAddonValuesBlob,
    IstioKialiInstalledValuesBlob,
    IngressNginxInstallRequest,
    IstioKialiInstallRequest,
    KubePrometheusStackInstallRequest,
    MetricsServerInstallRequest,
)

logger = logging.getLogger("kind_k8s.api.cluster_addons")

router = APIRouter(tags=["cluster-addons"])

_LOG_MAX = 24000


def _trim_log(s: str) -> str:
    if len(s) <= _LOG_MAX:
        return s
    return s[: _LOG_MAX - 20] + "\n… (обрезано)"


def _map_helm_error(e: HelmAddonError) -> HTTPException:
    msg = str(e)
    if e.exit_code == 127:
        return HTTPException(status_code=503, detail=msg)
    return HTTPException(status_code=500, detail=msg)


def _parse_optional_values_yaml(raw: str | None) -> dict[str, Any] | None:
    """
    Разбор опционального пользовательского YAML для Helm values.

    Пустая строка и ``null`` → ``None``. Корень документа должен быть mapping (объект YAML),
    иначе 400. Используется :func:`yaml.safe_load`.
    """
    if raw is None:
        return None
    s = raw.strip()
    if not s:
        return None
    try:
        data = yaml.safe_load(s)
    except yaml.YAMLError as e:
        raise HTTPException(
            status_code=400,
            detail=f"Некорректный YAML в values_yaml: {e}",
        ) from e
    if data is None:
        return None
    if not isinstance(data, dict):
        raise HTTPException(
            status_code=400,
            detail="values_yaml: в корне ожидается объект YAML (mapping), не список и не скаляр.",
        )
    return data


# Идентификатор аддона в API/UI → ключ :func:`helm_addon_snapshot` и суффикс ``job_kind`` (snake_case).
_HELM_ADDON_SNAPSHOT_KEY: dict[str, str] = {
    "ingress-nginx": "ingress_nginx",
    "kube-prometheus-stack": "kube_prometheus_stack",
    "metrics-server": "metrics_server",
    "istio-kiali": "istio_mesh_ready",
}
_HELM_ADDON_JOB_SLUG: dict[str, str] = {
    "ingress-nginx": "ingress_nginx",
    "kube-prometheus-stack": "kube_prometheus_stack",
    "metrics-server": "metrics_server",
    "istio-kiali": "istio_kiali",
}


def _helm_journal_message_ru(addon_id: str, action: str) -> str:
    """Краткая подпись операции для колонки «Сообщение» в журнале Helm и для UI."""
    name = {
        "ingress-nginx": "ingress-nginx",
        "kube-prometheus-stack": "kube-prometheus-stack",
        "metrics-server": "metrics-server",
        "istio-kiali": "Istio + Kiali",
    }.get(addon_id, addon_id)
    verb = {
        "install": "Установка",
        "upgrade": "Обновление",
        "delete": "Удаление",
    }.get(action, action)
    return f"{verb}: {name}"


async def _helm_install_action_and_job_kind(cluster_name: str, addon_id: str) -> tuple[str, str]:
    """
    Перед ``helm upgrade --install``: если релиз уже есть — операция **upgrade**, иначе **install**.

    Возвращает ``(action, job_kind)``, например ``("upgrade", "helm_addon_upgrade_ingress_nginx")``.
    """
    snap_key = _HELM_ADDON_SNAPSHOT_KEY.get(addon_id)
    slug = _HELM_ADDON_JOB_SLUG.get(addon_id, addon_id.replace("-", "_"))
    if not snap_key:
        return "install", f"helm_addon_install_{slug}"
    try:
        st = await asyncio.to_thread(helm_addon_snapshot, cluster_name)
        installed = bool(st.get(snap_key))
    except HelmAddonError:
        installed = False
    action = "upgrade" if installed else "install"
    return action, f"helm_addon_{action}_{slug}"


async def _run_helm_with_journal(
    cluster_name: str,
    job_kind: str,
    result_meta: dict[str, Any],
    sync_call: Callable[[], tuple[bool, str]],
    *,
    journal_message: str | None = None,
) -> str:
    """
    Выполнить операцию под блокировкой kind, добавить запись в историю ``helm_addon_log.json``.

    При ошибке запись всё равно добавляется со статусом ``failed``.
    """
    log = ""
    try:
        async with kind_cluster_lock:
            _, log = await asyncio.to_thread(sync_call)
        await asyncio.to_thread(
            write_cluster_helm_addon_log,
            cluster_name=cluster_name,
            job_id=str(uuid.uuid4()),
            job_kind=job_kind,
            status="success",
            message=journal_message,
            lines=log.splitlines() if log else [],
            result=result_meta,
        )
        return log
    except HelmAddonError as e:
        msg = str(e)
        lines = log.splitlines() if log else []
        if msg and msg not in lines:
            lines = [msg] + lines
        await asyncio.to_thread(
            write_cluster_helm_addon_log,
            cluster_name=cluster_name,
            job_id=str(uuid.uuid4()),
            job_kind=job_kind,
            status="failed",
            message=msg,
            lines=lines,
            result=result_meta,
        )
        raise


@router.get(
    "/clusters/{name}/addons/status",
    response_model=ClusterAddonsStatusResponse,
    summary="Статус Helm-аддонов в кластере",
)
async def get_cluster_addons_status(name: str) -> ClusterAddonsStatusResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    try:
        require_helm_binary()
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    try:
        st = await asyncio.to_thread(helm_addon_snapshot, n)
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    mesh = bool(st.get("istio_mesh_ready"))
    return ClusterAddonsStatusResponse(
        cluster_name=n,
        ingress_nginx=bool(st.get("ingress_nginx")),
        kube_prometheus_stack=bool(st.get("kube_prometheus_stack")),
        metrics_server=bool(st.get("metrics_server")),
        istio_base=bool(st.get("istio_base")),
        istiod=bool(st.get("istiod")),
        kiali_server=bool(st.get("kiali_server")),
        istio_mesh_ready=mesh,
        ingress_nginx_chart_version=st.get("ingress_nginx_chart_version"),
        kube_prometheus_stack_chart_version=st.get("kube_prometheus_stack_chart_version"),
        metrics_server_chart_version=st.get("metrics_server_chart_version"),
        istiod_chart_version=st.get("istiod_chart_version"),
        istio_base_chart_version=st.get("istio_base_chart_version"),
        kiali_server_chart_version=st.get("kiali_server_chart_version"),
    )


@router.get(
    "/clusters/{name}/addons/installed-values",
    response_model=ClusterAddonsInstalledValuesResponse,
    summary="Values установленных аддонов из кластера (helm get values --all)",
)
async def get_cluster_addons_installed_values(name: str) -> ClusterAddonsInstalledValuesResponse:
    """
    Для каждого установленного логического аддона возвращает версию чарта (как в ``helm list``)
    и полный YAML эффективных values (``helm get values --all -o yaml``) для правки перед upgrade.
    """
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    try:
        require_helm_binary()
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    try:
        raw = await asyncio.to_thread(collect_installed_addons_values, n)
    except HelmAddonError as e:
        raise _map_helm_error(e) from e

    def blob(key: str) -> InstalledAddonValuesBlob | None:
        b = raw.get(key)
        if not isinstance(b, dict):
            return None
        return InstalledAddonValuesBlob(
            chart_version=b.get("chart_version"),
            values_yaml=str(b.get("values_yaml") or ""),
        )

    istio = raw.get("istio_kiali")
    istio_model: IstioKialiInstalledValuesBlob | None = None
    if isinstance(istio, dict):
        istio_model = IstioKialiInstalledValuesBlob(
            istio_chart_version=istio.get("istio_chart_version"),
            kiali_chart_version=istio.get("kiali_chart_version"),
            istio_base_values_yaml=str(istio.get("istio_base_values_yaml") or ""),
            istiod_values_yaml=str(istio.get("istiod_values_yaml") or ""),
            kiali_values_yaml=str(istio.get("kiali_values_yaml") or ""),
        )

    return ClusterAddonsInstalledValuesResponse(
        cluster_name=n,
        ingress_nginx=blob("ingress_nginx"),
        kube_prometheus_stack=blob("kube_prometheus_stack"),
        metrics_server=blob("metrics_server"),
        istio_kiali=istio_model,
    )


@router.post(
    "/clusters/{name}/addons/ingress-nginx",
    response_model=HelmAddonApplyResponse,
    summary="Установить ingress-nginx (Helm)",
)
async def post_install_ingress_nginx(
    name: str,
    body: IngressNginxInstallRequest | None = Body(default=None),
) -> HelmAddonApplyResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    ver: str | None = None
    if body is not None and body.chart_version and body.chart_version.strip():
        ver = body.chart_version.strip()
    user_v = _parse_optional_values_yaml(body.values_yaml if body else None)
    action, job_kind = await _helm_install_action_and_job_kind(n, "ingress-nginx")
    meta = {"addon": "ingress-nginx", "action": action, "chart_version": ver}
    try:
        log = await _run_helm_with_journal(
            n,
            job_kind,
            meta,
            lambda: install_ingress_nginx(n, ver, user_v),
            journal_message=_helm_journal_message_ru("ingress-nginx", action),
        )
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    logger.info("Установлен ingress-nginx для кластера «%s»", n)
    return HelmAddonApplyResponse(ok=True, message="ingress-nginx установлен.", log=_trim_log(log))


@router.delete(
    "/clusters/{name}/addons/ingress-nginx",
    response_model=HelmAddonApplyResponse,
    summary="Удалить ingress-nginx",
)
async def delete_ingress_nginx(name: str) -> HelmAddonApplyResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    meta = {"addon": "ingress-nginx", "action": "delete"}
    try:
        log = await _run_helm_with_journal(
            n,
            "helm_addon_delete_ingress_nginx",
            meta,
            lambda: uninstall_ingress_nginx(n),
            journal_message=_helm_journal_message_ru("ingress-nginx", "delete"),
        )
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    return HelmAddonApplyResponse(ok=True, message="ingress-nginx удалён.", log=_trim_log(log))


@router.post(
    "/clusters/{name}/addons/kube-prometheus-stack",
    response_model=HelmAddonApplyResponse,
    summary="Установить kube-prometheus-stack (Prometheus Operator + Grafana)",
)
async def post_install_kube_prometheus_stack(
    name: str,
    body: KubePrometheusStackInstallRequest,
) -> HelmAddonApplyResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    prom_ver: str | None = None
    if body.chart_version and body.chart_version.strip():
        prom_ver = body.chart_version.strip()
    u = body.grafana_admin_user.strip()
    user_v = _parse_optional_values_yaml(body.values_yaml)
    action, job_kind = await _helm_install_action_and_job_kind(n, "kube-prometheus-stack")
    meta = {
        "addon": "kube-prometheus-stack",
        "action": action,
        "chart_version": prom_ver,
        "grafana_admin_user": u,
    }
    try:
        log = await _run_helm_with_journal(
            n,
            job_kind,
            meta,
            lambda: install_kube_prometheus_stack(
                n,
                grafana_admin_user=u,
                grafana_admin_password=body.grafana_admin_password,
                chart_version=prom_ver,
                user_values=user_v,
            ),
            journal_message=_helm_journal_message_ru("kube-prometheus-stack", action),
        )
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    logger.info("Установлен kube-prometheus-stack для кластера «%s»", n)
    return HelmAddonApplyResponse(ok=True, message="kube-prometheus-stack установлен.", log=_trim_log(log))


@router.delete(
    "/clusters/{name}/addons/kube-prometheus-stack",
    response_model=HelmAddonApplyResponse,
    summary="Удалить kube-prometheus-stack",
)
async def delete_kube_prometheus_stack(name: str) -> HelmAddonApplyResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    meta = {"addon": "kube-prometheus-stack", "action": "delete"}
    try:
        log = await _run_helm_with_journal(
            n,
            "helm_addon_delete_kube_prometheus_stack",
            meta,
            lambda: uninstall_kube_prometheus_stack(n),
            journal_message=_helm_journal_message_ru("kube-prometheus-stack", "delete"),
        )
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    return HelmAddonApplyResponse(ok=True, message="kube-prometheus-stack удалён.", log=_trim_log(log))


@router.post(
    "/clusters/{name}/addons/metrics-server",
    response_model=HelmAddonApplyResponse,
    summary="Установить metrics-server",
)
async def post_install_metrics_server(
    name: str,
    body: MetricsServerInstallRequest | None = Body(default=None),
) -> HelmAddonApplyResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    ms_ver: str | None = None
    if body is not None and body.chart_version and body.chart_version.strip():
        ms_ver = body.chart_version.strip()
    user_v = _parse_optional_values_yaml(body.values_yaml if body else None)
    action, job_kind = await _helm_install_action_and_job_kind(n, "metrics-server")
    meta = {"addon": "metrics-server", "action": action, "chart_version": ms_ver}
    try:
        log = await _run_helm_with_journal(
            n,
            job_kind,
            meta,
            lambda: install_metrics_server(n, ms_ver, user_v),
            journal_message=_helm_journal_message_ru("metrics-server", action),
        )
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    logger.info("Установлен metrics-server для кластера «%s»", n)
    return HelmAddonApplyResponse(ok=True, message="metrics-server установлен.", log=_trim_log(log))


@router.delete(
    "/clusters/{name}/addons/metrics-server",
    response_model=HelmAddonApplyResponse,
    summary="Удалить metrics-server",
)
async def delete_metrics_server(name: str) -> HelmAddonApplyResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    meta = {"addon": "metrics-server", "action": "delete"}
    try:
        log = await _run_helm_with_journal(
            n,
            "helm_addon_delete_metrics_server",
            meta,
            lambda: uninstall_metrics_server(n),
            journal_message=_helm_journal_message_ru("metrics-server", "delete"),
        )
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    return HelmAddonApplyResponse(ok=True, message="metrics-server удалён.", log=_trim_log(log))


@router.post(
    "/clusters/{name}/addons/istio-kiali",
    response_model=HelmAddonApplyResponse,
    summary="Установить Istio (base+istiod) и Kiali (по умолчанию auth anonymous)",
)
async def post_install_istio_kiali(
    name: str,
    body: IstioKialiInstallRequest,
) -> HelmAddonApplyResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    istio_ver: str | None = None
    if body.istio_chart_version and body.istio_chart_version.strip():
        istio_ver = body.istio_chart_version.strip()
    kiali_ver: str | None = None
    if body.kiali_chart_version and body.kiali_chart_version.strip():
        kiali_ver = body.kiali_chart_version.strip()
    kiali_uv = _parse_optional_values_yaml(body.values_yaml)
    istio_base_v = _parse_optional_values_yaml(body.istio_base_values_yaml)
    istiod_v = _parse_optional_values_yaml(body.istio_istiod_values_yaml)
    action, job_kind = await _helm_install_action_and_job_kind(n, "istio-kiali")
    meta = {
        "addon": "istio-kiali",
        "action": action,
        "istio_chart_version": istio_ver,
        "kiali_chart_version": kiali_ver,
    }
    try:
        log = await _run_helm_with_journal(
            n,
            job_kind,
            meta,
            lambda: install_istio_and_kiali(
                n,
                istio_chart_version=istio_ver,
                kiali_chart_version=kiali_ver,
                istio_base_values=istio_base_v,
                istiod_values=istiod_v,
                kiali_user_values=kiali_uv,
            ),
            journal_message=_helm_journal_message_ru("istio-kiali", action),
        )
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    logger.info("Установлены Istio+Kiali для кластера «%s»", n)
    return HelmAddonApplyResponse(ok=True, message="Istio и Kiali установлены.", log=_trim_log(log))


@router.delete(
    "/clusters/{name}/addons/istio-kiali",
    response_model=HelmAddonApplyResponse,
    summary="Удалить Kiali, istiod и istio-base",
)
async def delete_istio_kiali(name: str) -> HelmAddonApplyResponse:
    n = name.strip()
    if not validate_cluster_name(n):
        raise HTTPException(status_code=400, detail="Некорректное имя кластера")
    meta = {"addon": "istio-kiali", "action": "delete"}
    try:
        log = await _run_helm_with_journal(
            n,
            "helm_addon_delete_istio_kiali",
            meta,
            lambda: uninstall_istio_and_kiali(n),
            journal_message=_helm_journal_message_ru("istio-kiali", "delete"),
        )
    except HelmAddonError as e:
        raise _map_helm_error(e) from e
    return HelmAddonApplyResponse(ok=True, message="Istio/Kiali удалены.", log=_trim_log(log))
