"""Сохранение полного журнала развёртывания кластера в ``clusters/<имя>/provision_log.json``.

Журнал операций Helm-аддонов — ``helm_addon_log.json``: **история** операций (массив ``entries``),
старый формат «один объект в файле» при чтении мигрируется автоматически.

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

from __future__ import annotations

import json
import logging
import os
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from core.cluster_lifecycle import validate_cluster_name
from kind_k8s_paths import clusters_dir

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

PROVISION_LOG_FILENAME = "provision_log.json"
PROVISION_LOG_VERSION = 1

HELM_ADDON_LOG_FILENAME = "helm_addon_log.json"
"""Имя файла истории Helm; корневой объект с полем ``entries`` (версия файла 2)."""
HELM_ADDON_LOG_VERSION = 1
"""Версия схемы одной записи внутри ``entries``."""
HELM_ADDON_LOG_FILE_VERSION = 2
"""Версия обёртки файла (список ``entries``)."""

_locks_guard = threading.Lock()
_helm_log_locks: dict[str, threading.Lock] = {}


def _helm_cluster_lock(cluster_name: str) -> threading.Lock:
    with _locks_guard:
        if cluster_name not in _helm_log_locks:
            _helm_log_locks[cluster_name] = threading.Lock()
        return _helm_log_locks[cluster_name]


def _max_helm_addon_entries() -> int:
    raw = (os.environ.get("KIND_K8S_HELM_ADDON_LOG_MAX_ENTRIES") or "500").strip()
    try:
        return max(20, min(int(raw), 5000))
    except ValueError:
        return 500


def provision_log_file_path(cluster_name: str) -> Path:
    """Путь к JSON с журналом операции для кластера ``cluster_name``."""
    return clusters_dir() / cluster_name.strip() / PROVISION_LOG_FILENAME


def helm_addon_log_file_path(cluster_name: str) -> Path:
    """Путь к JSON с историей журналов установки/удаления Helm-аддонов."""
    return clusters_dir() / cluster_name.strip() / HELM_ADDON_LOG_FILENAME


def write_cluster_provision_log(
    *,
    cluster_name: str,
    job_id: str,
    job_kind: str,
    status: str,
    message: str | None,
    lines: list[str],
    result: dict[str, Any] | None,
) -> Path | None:
    """
    Атомарно записать полный журнал (все строки без обрезки буфера задания).

    Возвращает путь к файлу или ``None``, если каталог кластера не существует.
    """
    name = cluster_name.strip()
    cdir = clusters_dir() / name
    if not cdir.is_dir():
        logger.debug("Каталог кластера отсутствует, provision_log не пишем: %s", cdir)
        return None

    path = cdir / PROVISION_LOG_FILENAME
    payload: dict[str, Any] = {
        "version": PROVISION_LOG_VERSION,
        "job_id": job_id,
        "kind": job_kind,
        "cluster_name": name,
        "finished_at_utc": datetime.now(timezone.utc).isoformat(),
        "status": status,
        "message": message,
        "lines": list(lines),
        "result": result,
    }
    tmp = path.with_suffix(path.suffix + ".tmp")
    try:
        text = json.dumps(payload, ensure_ascii=False, indent=2, default=str)
        tmp.write_text(text, encoding="utf-8")
        tmp.replace(path)
        logger.info("Сохранён журнал развёртывания: %s (%s строк)", path, len(lines))
        return path
    except OSError as e:
        logger.warning("Не удалось записать %s: %s", path, e)
        try:
            tmp.unlink(missing_ok=True)
        except OSError:
            pass
        return None


def _helm_addon_rows_from_parsed_json(data: dict[str, Any], cname: str) -> list[dict[str, Any]]:
    """Разобрать содержимое ``helm_addon_log.json`` в плоский список строк для UI/API."""
    raw_entries = data.get("entries")
    if isinstance(raw_entries, list) and raw_entries:
        out: list[dict[str, Any]] = []
        for e in raw_entries:
            if isinstance(e, dict):
                row = dict(e)
                row["source_cluster"] = cname
                out.append(row)
        return out
    # Легаси: один объект операции в корне файла
    if data.get("job_id") is not None or data.get("finished_at_utc") is not None:
        row = dict(data)
        row["source_cluster"] = cname
        return [row]
    return []


def write_cluster_helm_addon_log(
    *,
    cluster_name: str,
    job_id: str,
    job_kind: str,
    status: str,
    message: str | None,
    lines: list[str],
    result: dict[str, Any] | None,
) -> Path | None:
    """
    Добавить запись в историю ``helm_addon_log.json`` (новые записи в начале списка).

    Файл хранит ``{"file_version": 2, "entries": [ ... ]}``; лимит длины — ``KIND_K8S_HELM_ADDON_LOG_MAX_ENTRIES``.
    """
    name = cluster_name.strip()
    cdir = clusters_dir() / name
    if not cdir.is_dir():
        logger.debug("Каталог кластера отсутствует, helm_addon_log не пишем: %s", cdir)
        return None

    path = cdir / HELM_ADDON_LOG_FILENAME
    new_entry: dict[str, Any] = {
        "version": HELM_ADDON_LOG_VERSION,
        "job_id": job_id,
        "kind": job_kind,
        "cluster_name": name,
        "finished_at_utc": datetime.now(timezone.utc).isoformat(),
        "status": status,
        "message": message,
        "lines": list(lines),
        "result": result,
    }

    lock = _helm_cluster_lock(name)
    with lock:
        entries: list[dict[str, Any]] = []
        if path.is_file():
            try:
                raw = json.loads(path.read_text(encoding="utf-8"))
                if isinstance(raw, dict):
                    ex = raw.get("entries")
                    if isinstance(ex, list):
                        entries = [e for e in ex if isinstance(e, dict)]
                    elif raw.get("job_id") is not None or raw.get("finished_at_utc") is not None:
                        legacy = {k: v for k, v in raw.items() if k != "source_cluster"}
                        entries = [legacy]
            except (OSError, json.JSONDecodeError) as e:
                logger.warning("helm_addon_log %s не прочитан, создаём заново: %s", path, e)
                entries = []

        entries.insert(0, new_entry)
        cap = _max_helm_addon_entries()
        if len(entries) > cap:
            entries = entries[:cap]

        payload: dict[str, Any] = {
            "file_version": HELM_ADDON_LOG_FILE_VERSION,
            "entries": entries,
        }
        tmp = path.with_suffix(path.suffix + ".tmp")
        try:
            tmp.write_text(
                json.dumps(payload, ensure_ascii=False, indent=2, default=str),
                encoding="utf-8",
            )
            tmp.replace(path)
            logger.info(
                "Сохранён журнал Helm-аддонов: %s (запись %s, всего записей в файле: %s)",
                path,
                job_id,
                len(entries),
            )
            return path
        except OSError as e:
            logger.warning("Не удалось записать %s: %s", path, e)
            try:
                tmp.unlink(missing_ok=True)
            except OSError:
                pass
            return None


def collect_cluster_dir_logs_page_sync(
    *,
    filename: str,
    limit: int,
    offset: int,
) -> tuple[list[dict[str, Any]], int]:
    """
    Собрать JSON с диска по кластерам.

    - ``provision_log.json`` — по одной строке на кластер (как раньше).
    - ``helm_addon_log.json`` — по одной строке на **каждую** запись в ``entries`` (или одна для легаси-файла).

    Сортировка по ``finished_at_utc`` (новые первыми), затем пагинация.
    """
    lim = max(1, min(limit, 100))
    off = max(0, offset)
    root = clusters_dir()
    if not root.is_dir():
        return [], 0
    rows: list[dict[str, Any]] = []
    for sub in sorted(root.iterdir()):
        if not sub.is_dir():
            continue
        cname = sub.name
        if not validate_cluster_name(cname):
            continue
        path = sub / filename
        if not path.is_file():
            continue
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError):
            continue
        if not isinstance(data, dict):
            continue
        if filename == HELM_ADDON_LOG_FILENAME:
            rows.extend(_helm_addon_rows_from_parsed_json(data, cname))
        else:
            row = dict(data)
            row["source_cluster"] = cname
            rows.append(row)

    def sort_key(r: dict[str, Any]) -> str:
        return str(r.get("finished_at_utc") or "")

    rows.sort(key=sort_key, reverse=True)
    total = len(rows)
    chunk = rows[off : off + lim]
    return chunk, total
