"""Модели запросов/ответов REST API веб-интерфейса.

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

from __future__ import annotations

from typing import Any, Literal

from pydantic import BaseModel, Field, model_validator


class ClusterCreateRequest(BaseModel):
    """Тело POST /api/v1/clusters — создание кластера."""

    name: str = Field(..., min_length=1, max_length=63, description="DNS-имя кластера (a-z0-9-)")
    kubernetes_version: str = Field(
        ...,
        min_length=1,
        description="Тег kindest/node: latest, 1.29.4, v1.29.4 и т.д.",
    )
    workers: int = Field(2, ge=0, le=20, description="Число worker-нод (0–20)")


class ClusterCreateAccepted(BaseModel):
    """Ответ 202 — задание поставлено в очередь."""

    job_id: str
    status: Literal["queued"] = "queued"
    message: str = "Создание кластера выполняется в фоне; опросите GET /api/v1/jobs/{job_id}"


class JobView(BaseModel):
    """Статус фонового задания."""

    job_id: str
    kind: str
    status: Literal["queued", "running", "success", "failed", "cancelled"]
    cluster_name: str | None
    created_at_utc: str
    message: str | None = None
    result: dict[str, Any] | None = None
    progress_stage: str | None = Field(default=None, description="Текущий этап операции (пока задание активно)")
    progress_percent: int | None = Field(default=None, description="Прогресс 0–100 для индикатора в UI")
    progress_log: list[str] = Field(
        default_factory=list,
        description="Хвост журнала (скачивание образа, длительные команды, этапы); при опросе GET /jobs/{id}",
    )


class ClusterSummary(BaseModel):
    """Элемент списка кластеров."""

    name: str
    registered_in_kind: bool
    kind_nodes_running: bool = Field(
        default=False,
        description="Запущены контейнеры узлов kind (имя-control-plane, имя-worker…; docker/podman ps)",
    )
    has_local_kubeconfig: bool
    has_provision_log: bool = Field(
        default=False,
        description="Есть сохранённый полный журнал развёртывания (provision_log.json)",
    )
    meta: dict[str, Any] = Field(default_factory=dict)


class KindNodeResourceStat(BaseModel):
    """Одна нода kind (контейнер): снимок ``docker stats`` / ``podman stats``."""

    container_name: str
    cpu_percent: str | None = Field(default=None, description="Доля CPU, напр. 1.23%")
    memory_usage: str | None = Field(default=None, description="Использование / лимит, напр. 120MiB / 7.7GiB")
    memory_percent: str | None = Field(default=None, description="Процент памяти от лимита контейнера")
    net_io: str | None = Field(default=None, description="Сетевой I/O (накопительно за жизнь контейнера)")
    block_io: str | None = Field(default=None, description="Блочный I/O")
    pids: int | None = Field(default=None, description="Число процессов в контейнере")


class KindClusterResources(BaseModel):
    """Ресурсы узлов одного кластера kind (только запущенные контейнеры)."""

    cluster_name: str
    nodes: list[KindNodeResourceStat] = Field(default_factory=list)
    note: str | None = Field(default=None, description="Пояснение, если узлов нет или ошибка списка")


class AggregateResourcesSummary(BaseModel):
    """Сводка по всем запущенным узлам kind для донат-диаграмм на главной."""

    nodes_count: int = Field(0, description="Число учтённых контейнеров-нод")
    cpu_ring: float = Field(0, ge=0, le=100, description="Заполнение кольца «средний CPU», 0–100")
    cpu_label: str = Field("—", description="Подпись в центре доната")
    memory_percent_ring: float = Field(0, ge=0, le=100)
    memory_percent_label: str = Field("—")
    memory_used_ratio_ring: float = Field(0, ge=0, le=100, description="Доля занятой RAM от лимита по строке MemUsage")
    memory_used_ratio_label: str = Field("—")
    network_ring: float = Field(0, ge=0, le=100, description="Индикатор суммарного сетевого I/O (условная шкала)")
    network_label: str = Field("—")
    disk_ring: float = Field(0, ge=0, le=100, description="Индикатор суммарного блочного I/O")
    disk_label: str = Field("—")


class StatsResponse(BaseModel):
    """Краткая статистика для дашборда."""

    container_cli: str = Field(
        description="CLI движка контейнеров (docker или podman): узлы kind и метрики собираются через него",
    )
    kind_clusters_count: int
    local_cluster_dirs_count: int
    total_workers_from_meta: int = Field(
        0,
        ge=0,
        description="Сумма worker_nodes по meta.json каталогов; 0 если ни в одном meta нет поля или данных",
    )
    jobs_total: int
    jobs_recent_failed: int
    helm_addons_installable_count: int = Field(
        0,
        ge=0,
        description="Число типовых Helm-аддонов в каталоге UI (страница «Аддоны»), доступных для установки",
    )
    cluster_resources: list[KindClusterResources] = Field(
        default_factory=list,
        description="CPU/RAM/I/O узлов kind по данным container CLI",
    )
    cluster_resources_error: str | None = Field(
        default=None,
        description="Глобальная ошибка сбора метрик (например CLI не найден)",
    )
    aggregate_cluster_resources: AggregateResourcesSummary = Field(
        default_factory=AggregateResourcesSummary,
        description="Агрегаты по узлам для донатов на главной странице",
    )


class ClusterWorkloadsResponse(BaseModel):
    """Вывод kubectl по кластеру (узлы и поды)."""

    cluster_name: str
    nodes_rc: int | None = None
    nodes_output: str | None = None
    pods_rc: int | None = None
    pods_output: str | None = None
    error: str | None = None


class K8sListJsonBlock(BaseModel):
    """Фрагмент списка ресурсов Kubernetes (поле ``items`` из JSON List) для таблицы в UI."""

    ok: bool = True
    rc: int | None = Field(default=None, description="Код возврата при обращении к API кластера")
    items: list[dict[str, Any]] = Field(default_factory=list)
    message: str | None = Field(default=None, description="Текст ошибки или разбора JSON")


class PodRestartRequest(BaseModel):
    """Тело POST: перезапуск пода через ``kubectl delete pod`` (воссоздание контроллером)."""

    namespace: str = Field(..., min_length=1, max_length=253, description="Namespace пода")
    pod: str = Field(..., min_length=1, max_length=253, description="Имя пода (metadata.name)")


class PodRestartResponse(BaseModel):
    """Результат запроса на удаление/рестарт пода."""

    cluster_name: str
    namespace: str
    pod: str
    rc: int
    message: str


class ClusterOverviewResponse(BaseModel):
    """Сводка для страницы кластера: мета, ресурсы узлов, таблицы ресурсов Kubernetes."""

    cluster_name: str
    registered_in_kind: bool = False
    kind_nodes_running: bool = False
    has_local_kubeconfig: bool = False
    has_provision_log: bool = False
    meta: dict[str, Any] = Field(default_factory=dict)
    resources_error: str | None = Field(default=None, description="Ошибка сбора docker/podman stats")
    cluster_resources: KindClusterResources = Field(
        default_factory=lambda: KindClusterResources(cluster_name="", nodes=[], note=None),
    )
    aggregate_resources: AggregateResourcesSummary = Field(default_factory=AggregateResourcesSummary)
    kubeconfig_error: str | None = Field(default=None, description="Нет kubeconfig или недоступен API кластера")
    nodes_rc: int | None = None
    nodes_output: str | None = None
    pods_rc: int | None = None
    pods_output: str | None = None
    deployments_rc: int | None = None
    deployments_output: str | None = None
    statefulsets_rc: int | None = None
    statefulsets_output: str | None = None
    daemonsets_rc: int | None = None
    daemonsets_output: str | None = None
    services_rc: int | None = None
    services_output: str | None = None
    ingresses_rc: int | None = None
    ingresses_output: str | None = None
    k8s_nodes: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock, description="Узлы кластера")
    k8s_pods: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock, description="Поды, все namespace")
    k8s_deployments: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
    k8s_statefulsets: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
    k8s_daemonsets: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
    k8s_services: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
    k8s_ingresses: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
    k8s_namespaces: K8sListJsonBlock = Field(
        default_factory=K8sListJsonBlock,
        description="Namespaces",
    )
    k8s_replicasets: K8sListJsonBlock = Field(
        default_factory=K8sListJsonBlock,
        description="ReplicaSets, все namespace",
    )
    k8s_jobs: K8sListJsonBlock = Field(
        default_factory=K8sListJsonBlock,
        description="Jobs, все namespace",
    )
    k8s_cronjobs: K8sListJsonBlock = Field(
        default_factory=K8sListJsonBlock,
        description="CronJobs, все namespace",
    )
    k8s_pvcs: K8sListJsonBlock = Field(
        default_factory=K8sListJsonBlock,
        description="PVC, все namespace",
    )


class ClusterConfigGetResponse(BaseModel):
    """Ответ GET /clusters/{name}/config — текущие meta и kind-config.yaml."""

    cluster_name: str
    meta: dict[str, Any] = Field(default_factory=dict)
    kind_config_yaml: str | None = Field(default=None, description="Содержимое kind-config.yaml или null, если файла нет")
    has_kind_config: bool = False
    registered_in_kind: bool = Field(
        default=False,
        description="Есть ли кластер в списке kind (важно для подсказки про применение конфига)",
    )
    kind_note: str = Field(
        default="",
        description="Пояснение: kind не меняет топологию уже созданного кластера без пересоздания",
    )


class ClusterConfigUpdateRequest(BaseModel):
    """Тело PUT /clusters/{name}/config.

    Если передан непустой ``kind_config_yaml``, он перезаписывает файл целиком; поля ``kubernetes_version``
    и ``workers`` в этом запросе для генерации YAML не используются (можно не отправлять).
    """

    kubernetes_version: str | None = Field(
        default=None,
        description="Тег kindest/node для простого режима (пересборка YAML: control-plane + N worker)",
    )
    workers: int | None = Field(default=None, ge=0, le=20)
    kind_config_yaml: str | None = Field(default=None, description="Полный YAML манифеста kind Cluster")
    description: str | None = Field(
        default=None,
        max_length=2000,
        description="Текст в meta.json (пустая строка сбрасывает поле)",
    )

    @model_validator(mode="after")
    def at_least_one_field(self) -> ClusterConfigUpdateRequest:
        has_ver = self.kubernetes_version is not None
        has_w = self.workers is not None
        yaml_s = (self.kind_config_yaml or "").strip()
        has_yaml = self.kind_config_yaml is not None and yaml_s != ""
        has_desc = self.description is not None
        if not (has_ver or has_w or has_yaml or has_desc):
            raise ValueError("Укажите хотя бы одно поле для обновления.")
        return self


class ClusterConfigUpdateResponse(BaseModel):
    """Результат сохранения конфигурации кластера на диск."""

    cluster_name: str
    updated_kind_config_yaml: bool = Field(description="Был ли перезаписан kind-config.yaml")
    message: str
    registered_in_kind: bool
    kind_note: str
    summary: ClusterSummary = Field(description="Актуальная сводка для строки таблицы / панели")


# --- Helm-аддоны (страница «Дополнения кластера») ---


class ClusterAddonsStatusResponse(BaseModel):
    """Какие релизы Helm обнаружены для кластера (по helm list -A)."""

    cluster_name: str
    ingress_nginx: bool = Field(description="Релиз ingress-nginx в ns ingress-nginx")
    kube_prometheus_stack: bool = Field(description="Релиз kube-prometheus-stack в ns monitoring")
    metrics_server: bool = Field(description="Релиз metrics-server в ns kube-system")
    istio_base: bool
    istiod: bool
    kiali_server: bool
    istio_mesh_ready: bool = Field(
        description="Установлены istio-base, istiod и kiali-server в istio-system",
    )
    ingress_nginx_chart_version: str | None = Field(
        default=None,
        description="Версия чарта из helm list (если релиз установлен)",
    )
    kube_prometheus_stack_chart_version: str | None = Field(default=None)
    metrics_server_chart_version: str | None = Field(default=None)
    istiod_chart_version: str | None = Field(
        default=None,
        description="Версия чарта istiod в кластере (для селекта Istio на UI)",
    )
    istio_base_chart_version: str | None = Field(default=None)
    kiali_server_chart_version: str | None = Field(default=None)


class InstalledAddonValuesBlob(BaseModel):
    """Снимок values одного релиза (helm get values --all)."""

    chart_version: str | None = Field(default=None, description="Версия чарта из helm list")
    values_yaml: str = Field(default="", description="YAML эффективных values")


class IstioKialiInstalledValuesBlob(BaseModel):
    """Три релиза mesh + версии для селектов UI."""

    istio_chart_version: str | None = Field(
        default=None,
        description="Версия istiod (или base), для селекта «Версия чартов Istio»",
    )
    kiali_chart_version: str | None = None
    istio_base_values_yaml: str = ""
    istiod_values_yaml: str = ""
    kiali_values_yaml: str = ""


class ClusterAddonsInstalledValuesResponse(BaseModel):
    """Values из кластера только для установленных аддонов (для редактирования перед upgrade)."""

    cluster_name: str
    ingress_nginx: InstalledAddonValuesBlob | None = None
    kube_prometheus_stack: InstalledAddonValuesBlob | None = None
    metrics_server: InstalledAddonValuesBlob | None = None
    istio_kiali: IstioKialiInstalledValuesBlob | None = None


class HelmAddonApplyResponse(BaseModel):
    """Результат синхронной установки/удаления чарта."""

    ok: bool = True
    message: str
    log: str = Field(default="", description="Сводный вывод helm/kubectl")


_VALUES_YAML_MAX = 131_072


class IngressNginxInstallRequest(BaseModel):
    """Тело POST …/addons/ingress-nginx — установка ingress-nginx."""

    chart_version: str | None = Field(
        default=None,
        max_length=64,
        description="Версия чарта; пусто — последняя из репозитория",
    )
    values_yaml: str | None = Field(
        default=None,
        max_length=_VALUES_YAML_MAX,
        description=(
            "Полный YAML values для чарта (корень — объект), как в редакторе на странице аддонов "
            "(``helm show values`` + сервис для kind). Пусто — собрать автоматически на сервере. "
            "После ``-f`` применяются ``--set`` NodePort (приоритет Helm)."
        ),
    )


class KubePrometheusStackInstallRequest(BaseModel):
    """Тело POST …/addons/kube-prometheus-stack."""

    grafana_admin_user: str = Field(default="admin", min_length=1, max_length=256)
    grafana_admin_password: str = Field(..., min_length=8, max_length=256)
    chart_version: str | None = Field(
        default=None,
        max_length=64,
        description="Версия чарта kube-prometheus-stack; пусто — последняя",
    )
    values_yaml: str | None = Field(
        default=None,
        max_length=_VALUES_YAML_MAX,
        description=(
            "Полный YAML values чарта (``helm show values`` + логин/пароль Grafana из формы). "
            "Пусто — собрать на сервере из версии чарта и полей формы."
        ),
    )


class MetricsServerInstallRequest(BaseModel):
    """Тело POST …/addons/metrics-server (все поля необязательны)."""

    chart_version: str | None = Field(
        default=None,
        max_length=64,
        description="Версия чарта metrics-server; пусто — последняя",
    )
    values_yaml: str | None = Field(
        default=None,
        max_length=_VALUES_YAML_MAX,
        description=(
            "Полный YAML values чарта (``helm show values`` + args для kind). "
            "Пусто — собрать на сервере."
        ),
    )


class IstioKialiInstallRequest(BaseModel):
    """Тело POST …/addons/istio-kiali — Istio + Kiali; Kiali по умолчанию auth anonymous (без логина в UI/API)."""

    istio_chart_version: str | None = Field(
        default=None,
        max_length=64,
        description="Версия чартов istio/base и istio/istiod; пусто — последняя",
    )
    kiali_chart_version: str | None = Field(
        default=None,
        max_length=64,
        description="Версия чарта kiali/kiali-server; пусто — последняя",
    )
    values_yaml: str | None = Field(
        default=None,
        max_length=_VALUES_YAML_MAX,
        description=(
            "Полный YAML values чарта **kiali-server** (``helm show values``). "
            "Пусто — без файла ``-f`` (дефолты чарта). Если не задан ``auth.strategy`` в YAML, "
            "сервер выставляет ``anonymous`` (см. документацию Kiali; стратегия ``login`` удалена)."
        ),
    )
    istio_base_values_yaml: str | None = Field(
        default=None,
        max_length=_VALUES_YAML_MAX,
        description="Полный YAML values чарта **istio/base**; пусто — без ``-f``.",
    )
    istio_istiod_values_yaml: str | None = Field(
        default=None,
        max_length=_VALUES_YAML_MAX,
        description="Полный YAML values чарта **istio/istiod**; пусто — без ``-f``.",
    )


class HelmAddonComposeValuesRequest(BaseModel):
    """Тело POST /helm/addons/compose-values — собрать YAML для редактора (helm show values + форма)."""

    addon: Literal["ingress-nginx", "kube-prometheus-stack", "metrics-server", "istio-kiali"]
    chart_version: str | None = Field(
        default=None,
        max_length=64,
        description="Версия чарта: ingress-nginx, kube-prometheus-stack или metrics-server",
    )
    grafana_admin_user: str | None = Field(default=None, max_length=256)
    grafana_admin_password: str | None = Field(
        default=None,
        max_length=256,
        description="Для preview kube-prometheus-stack; если пусто — подставляется временный пароль в YAML",
    )
    istio_chart_version: str | None = Field(default=None, max_length=64)
    kiali_chart_version: str | None = Field(default=None, max_length=64)


class HelmAddonComposeValuesResponse(BaseModel):
    """Ответ compose-values: строки YAML для текстовых полей UI."""

    values_yaml: str = Field(..., description="Основной чарт; для istio-kiali — kiali-server")
    istio_base_values_yaml: str | None = Field(default=None, description="Только istio-kiali: istio/base")
    istio_istiod_values_yaml: str | None = Field(default=None, description="Только istio-kiali: istio/istiod")


class HelmAddonComposeBatchRequest(BaseModel):
    """Тело POST /helm/addons/compose-values-batch — одним запросом обновить все редакторы на странице аддонов."""

    ingress_chart_version: str | None = Field(default=None, max_length=64)
    grafana_admin_user: str | None = Field(default=None, max_length=256)
    grafana_admin_password: str | None = Field(default=None, max_length=256)
    kube_prometheus_chart_version: str | None = Field(default=None, max_length=64)
    metrics_server_chart_version: str | None = Field(default=None, max_length=64)
    istio_chart_version: str | None = Field(default=None, max_length=64)
    kiali_chart_version: str | None = Field(default=None, max_length=64)


class HelmAddonComposeBatchResponse(BaseModel):
    """Шесть строк YAML за один вызов (один ``helm repo update``)."""

    ingress_nginx_values_yaml: str
    kube_prometheus_values_yaml: str
    metrics_server_values_yaml: str
    kiali_values_yaml: str
    istio_base_values_yaml: str
    istio_istiod_values_yaml: str


class HelmAddonChartVersionsResponse(BaseModel):
    """Список версий чартов для выпадающих списков на странице аддонов (после helm repo update)."""

    ingress_nginx: list[str] = Field(default_factory=list)
    kube_prometheus_stack: list[str] = Field(default_factory=list)
    metrics_server: list[str] = Field(default_factory=list)
    istio: list[str] = Field(default_factory=list, description="Версии istio/base (для base+istiod)")
    kiali_server: list[str] = Field(default_factory=list)


class JournalEntryModel(BaseModel):
    """Одна запись из ``clusters/<имя>/journal/jobs_history.json``."""

    model_config = {"extra": "allow"}

    version: int | None = Field(default=None, description="Версия схемы записи в файле")
    job_id: str = ""
    kind: str = ""
    cluster_name: str = ""
    status: str = ""
    message: str | None = None
    created_at_utc: str = ""
    finished_at_utc: str | None = None
    log_lines: list[str] = Field(default_factory=list)
    result: dict[str, Any] | None = None
    source_cluster: str | None = Field(
        default=None,
        description="Имя каталога кластера в ответе GET /journal/recent",
    )


class ClusterJournalResponse(BaseModel):
    """Содержимое журнала одного кластера."""

    cluster_name: str
    file_version: int | None = None
    entries: list[JournalEntryModel] = Field(default_factory=list)


class JournalRecentResponse(BaseModel):
    """Страница журнала заданий из ``journal/jobs_history.json`` (все кластеры или один)."""

    limit: int = Field(description="Размер страницы (записей за запрос)")
    offset: int = Field(description="Смещение от начала отсортированного списка (0 = первая страница)")
    total: int = Field(ge=0, description="Всего записей во всех журналах")
    page: int = Field(ge=1, description="Номер текущей страницы с 1")
    total_pages: int = Field(ge=0, description="Число страниц при данном limit; 0 если записей нет")
    entries: list[JournalEntryModel] = Field(default_factory=list)


class ClusterDirLogEntryModel(BaseModel):
    """Запись из ``provision_log.json`` или ``helm_addon_log.json`` (формат как у развёртывания)."""

    model_config = {"extra": "allow"}

    version: int | None = None
    job_id: str = ""
    kind: str = ""
    cluster_name: str = ""
    finished_at_utc: str = ""
    status: str = ""
    message: str | None = None
    lines: list[str] = Field(default_factory=list)
    result: dict[str, Any] | None = None
    source_cluster: str | None = Field(
        default=None,
        description="Имя каталога кластера в агрегированных ответах /journal/provision и /journal/helm-addons",
    )


class JournalPagedDirLogsResponse(BaseModel):
    """Пагинация по одному последнему логу на кластер (развёртывание или Helm-аддоны)."""

    limit: int
    offset: int
    total: int
    page: int
    total_pages: int
    entries: list[ClusterDirLogEntryModel] = Field(default_factory=list)
