#!/usr/bin/env python3
"""Интерактивное создание ``.env`` в корне репозитория Kind Clusters Dashboard.

Список переменных и подсказок задаётся в этом файле (не используется env.example).
В начале мастера выбирается только **docker** или **podman** — путь ``CONTAINER_SOCKET`` подставляется автоматически.
Дефолты совпадают с ``docker-compose.yml`` / приложением; Enter — записать предложенное значение.

Запуск: ``python3 scripts/setup_env_interactive.py`` или ``make setup``.

Опции: ``--output`` / ``-f`` (перезапись без вопроса).

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

from __future__ import annotations

import argparse
import logging
import os
import secrets
import shutil
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path

logger = logging.getLogger("setup_env_interactive")

# Корень репозитория Kind Clusters Dashboard (родитель каталога scripts/)
REPO_ROOT = Path(__file__).resolve().parents[1]

# Значения по умолчанию при нажатии Enter (как в docker-compose / Dockerfile / Settings).
# KUBECTL_VERSION: в Dockerfile пустой build-arg = stable.txt; здесь — закреплённый тег для воспроизводимости.
_SETUP_DEFAULTS: dict[str, str] = {
    "KIND_VERSION": "0.24.0",
    "KUBECTL_VERSION": "v1.32.0",
    "CONTAINER_SOCKET": "/var/run/docker.sock",
    "KIND_K8S_WEB_PORT": "8080",
    "KIND_K8S_PATCH_KUBECONFIG": "1",
    "CONTAINER_CLI": "docker",
    "KIND_K8S_SKIP_VERSION_LIST": "",
    "KIND_K8S_VERSION_LIST_DISPLAY": "50",
    "KIND_K8S_HUB_TAGS_MAX_PAGES": "120",
    "KIND_K8S_DEBUG": "",
    "KIND_K8S_WAIT_NODES": "1",
    "KIND_K8S_WAIT_NODES_TIMEOUT_SEC": "300",
    "KIND_K8S_APP_TITLE": "Kind Clusters Dashboard",
    "KIND_K8S_README_PATH": "",
    "KIND_K8S_UVICORN_RELOAD": "1",
    "KIND_K8S_WEB_HOST": "0.0.0.0",
    "KIND_K8S_WORKDIR": "",
    "KIND_K8S_JOBS_JSON": "",
    "COMPOSE_BUILD_FLAGS": "",
}

# Переменные, для которых по вводу «g» генерируется секрет (на будущее / другие проекты).
_SECRET_KEYS = frozenset(
    {
        "SECRET_KEY",
        "CONNECTORS_ENCRYPTION_KEY",
        "K8S_KUBECONFIG_ENCRYPTION_KEY",
        "POSTGRES_PASSWORD",
        "MINIO_SECRET_KEY",
        "ROOT_PASSWORD",
    },
)


@dataclass(frozen=True)
class _EnvPrompt:
    """Одна переменная в мастере setup: опциональный заголовок секции в .env и текст помощи."""

    section_comment: str | None
    key: str
    help_ru: str


# Порядок опроса и человекочитаемые пояснения (секции — комментарии в записанном .env).
_SETUP_PROMPTS: tuple[_EnvPrompt, ...] = (
    _EnvPrompt(
        "# --- docker-compose.yml: build-args (образ kind-k8s-tools:local) ---",
        "KIND_VERSION",
        "Версия бинарника kind при сборке образа (build-arg KIND_VERSION).",
    ),
    _EnvPrompt(
        None,
        "KUBECTL_VERSION",
        "Версия kubectl в образе (build-arg). Пустая строка в .env при ручном вводе возможна; "
        "дефолт скрипта — закреплённый тег (в Dockerfile без аргумента берётся stable.txt).",
    ),
    _EnvPrompt(
        "# --- docker-compose.yml: сокет + CONTAINER_CLI + user/HOME (доступ к сокету rootless Podman) ---",
        "CONTAINER_SOCKET",
        "",
    ),
    _EnvPrompt(
        "# --- docker-compose.yml: публикация веб-UI (хост:контейнер → …:6000 внутри) ---",
        "KIND_K8S_WEB_PORT",
        "Порт на хосте для браузера (дефолт 8080 — 6000 на хосте блокирует Chrome как ERR_UNSAFE_PORT); внутри контейнера uvicorn слушает 6000.",
    ),
    _EnvPrompt(
        "# --- docker-compose.yml: environment сервиса kind-k8s-web ---",
        "KIND_K8S_PATCH_KUBECONFIG",
        "1/true/yes — после create писать clusters/<имя>/kubeconfig.host (server с KIND_K8S_KUBECONFIG_CLIENT_HOST, иначе localhost); "
        "основной kubeconfig — как у kind. 0 или пусто — не форсировать. По умолчанию в скрипте: 1 (включено).",
    ),
    _EnvPrompt(
        None,
        "KIND_K8S_KUBECONFIG_CLIENT_HOST",
        "Имя хоста в server= для скачиваемого kubeconfig (kubectl на машине пользователя). Пусто — localhost (лучше 127.0.0.1 с TLS kind). Для доступа с другой машины — IP/имя хоста Docker.",
    ),
    _EnvPrompt(
        None,
        "KIND_K8S_SKIP_VERSION_LIST",
        "1 — не запрашивать теги kindest/node с Docker Hub (изолированная сеть); иначе пусто.",
    ),
    _EnvPrompt(
        None,
        "KIND_K8S_VERSION_LIST_DISPLAY",
        "Сколько строк показывать в интерактивном CLI при выборе версии (веб-UI отдаёт полный список).",
    ),
    _EnvPrompt(
        None,
        "KIND_K8S_HUB_TAGS_MAX_PAGES",
        "Лимит страниц Docker Hub API при сборе тегов (по умолчанию в коде 120, макс. 500); больше страниц — больше старых семверов, дольше запрос.",
    ),
    _EnvPrompt(None, "KIND_K8S_DEBUG", "1/true/yes — уровень DEBUG в логах приложения; иначе пусто (INFO)."),
    _EnvPrompt(
        None,
        "KIND_K8S_WAIT_NODES",
        "1 — после kind create ждать Ready нод через kubectl wait; 0 — не ждать.",
    ),
    _EnvPrompt(None, "KIND_K8S_WAIT_NODES_TIMEOUT_SEC", "Таймаут kubectl wait (секунды)."),
    _EnvPrompt(None, "KIND_K8S_APP_TITLE", "Заголовок OpenAPI и веб-интерфейса."),
    _EnvPrompt(
        None,
        "KIND_K8S_JOBS_JSON",
        "Только файл глобального снимка заданий (очередь и running; после перезапуска контейнера). "
        "Пусто — <корень_данных>/clusters/kind_k8s_jobs.json (корень = KIND_K8S_WORKDIR; в compose внутри контейнера /work, "
        "на хосте тот же том — обычно ./clusters/kind_k8s_jobs.json). "
        "Архив завершённых операций и страница «Журнал» — clusters/<имя>/journal/jobs_history.json; "
        "полный лог create/start/reapply — clusters/<имя>/provision_log.json; история Helm-аддонов — "
        "clusters/<имя>/helm_addon_log.json — эти пути KIND_K8S_JOBS_JSON не переопределяет.",
    ),
    _EnvPrompt(
        None,
        "KIND_K8S_README_PATH",
        "Путь к README.md для страницы /documentation; пусто — README рядом с app/ (в образе /opt/kind-k8s/README.md).",
    ),
    _EnvPrompt(
        None,
        "KIND_K8S_UVICORN_RELOAD",
        "1 — uvicorn --reload (правки в ./app без пересборки образа); 0 — один процесс без reload.",
    ),
    _EnvPrompt(
        "# --- pydantic Settings: KIND_K8S_WEB_HOST (в compose не передаётся; см. run_uvicorn.sh) ---",
        "KIND_K8S_WEB_HOST",
        "Хост привязки uvicorn при локальном запуске вне compose (в контейнере задаётся скриптом).",
    ),
    _EnvPrompt(
        "# --- Только локальный запуск app/*.py без docker-compose ---",
        "KIND_K8S_WORKDIR",
        "Корень данных на машине разработчика (в compose в контейнере задан /work литералом).",
    ),
    _EnvPrompt(
        "# --- Только Makefile (не переменные окружения compose) ---",
        "COMPOSE_BUILD_FLAGS",
        "Доп. флаги для «make docker build», например --platform linux/arm64.",
    ),
)

# Путь к сокету Docker в примерах и значение по умолчанию для CONTAINER_SOCKET (не подставляется из UID).
_DOCKER_SOCKET_EXAMPLE = "/var/run/docker.sock"


def _process_uid() -> int | None:
    """UID процесса (то же, что ``id -u`` для пользователя, запустившего скрипт)."""
    try:
        return int(os.getuid())
    except AttributeError:
        return None


def _podman_socket_path_for_user() -> str:
    """
    Путь к podman.sock для подсказок: ``$XDG_RUNTIME_DIR/podman/podman.sock`` или
    ``/run/user/<UID>/podman/podman.sock`` (UID — результат :func:`_process_uid`, иначе 1000).
    """
    xdg = (os.environ.get("XDG_RUNTIME_DIR") or "").strip()
    if xdg:
        return f"{xdg.rstrip('/')}/podman/podman.sock"
    uid = _process_uid()
    if uid is not None:
        return f"/run/user/{uid}/podman/podman.sock"
    return "/run/user/1000/podman/podman.sock"


def _unix_url_for_path(absolute_path: str) -> str:
    """Ссылка вида ``unix:///…`` для абсолютного пути к сокету."""
    p = absolute_path if absolute_path.startswith("/") else f"/{absolute_path}"
    return f"unix://{p}"


def _docker_host_unix_path_from_env() -> str | None:
    """Путь из ``DOCKER_HOST=unix://…``, если задан."""
    dh = (os.environ.get("DOCKER_HOST") or "").strip()
    if not dh.startswith("unix://"):
        return None
    path = dh[7:]
    if path and not path.startswith("/"):
        path = "/" + path.lstrip("/")
    return path or None


def _first_existing_socket(candidates: list[str]) -> str | None:
    """Первый существующий unix-socket из списка путей."""
    for c in candidates:
        if not c:
            continue
        p = Path(c).expanduser()
        try:
            if p.is_socket():
                return str(p.resolve())
        except OSError:
            logger.debug("Пропуск кандидата сокета (ошибка доступа): %s", p, exc_info=True)
    return None


def _detect_docker_socket_path() -> str:
    """
    Авто-поиск сокета Docker на машине, где запущен мастер: ``DOCKER_HOST``, ``/var/run/docker.sock``,
    типичные пути Docker Desktop / Colima.
    """
    cands: list[str] = []
    envp = _docker_host_unix_path_from_env()
    if envp:
        cands.append(envp)
    cands.append(_DOCKER_SOCKET_EXAMPLE)
    home = Path.home()
    cands.append(str(home / ".docker/run/docker.sock"))
    cands.append(str(home / ".colima/default/docker.sock"))
    found = _first_existing_socket(cands)
    if found:
        logger.info("Найден сокет Docker: %s", found)
        return found
    logger.info("Сокет Docker не найден на диске — подставляю стандартный путь %s", _DOCKER_SOCKET_EXAMPLE)
    return _DOCKER_SOCKET_EXAMPLE


def _podman_uidgid_for_env() -> str:
    """
    Строка ``uid:gid`` для ``KIND_K8S_CONTAINER_UIDGID`` при Podman.

    Берётся владелец файла сокета (как у ``make podman …``), чтобы на macOS и в VM
    не расходилось с ``id -u`` / ``id -g`` интерактивного мастера.
    """
    script = Path(__file__).resolve().parent / "detect_podman_socket.py"
    try:
        proc = subprocess.run(
            [sys.executable, str(script), "--print-owner"],
            capture_output=True,
            text=True,
            timeout=25,
        )
        if proc.returncode == 0:
            s = (proc.stdout or "").strip()
            parts = s.split(":", 1)
            if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
                return s
    except (OSError, subprocess.TimeoutExpired):
        logger.debug("detect_podman_socket.py --print-owner недоступен", exc_info=True)
    try:
        return f"{os.getuid()}:{os.getgid()}"
    except AttributeError:
        return "1000:1000"


def _detect_podman_socket_path() -> str:
    """
    Путь к ``podman.sock`` — тот же алгоритм, что ``scripts/detect_podman_socket.py``
    (``podman info``, ``podman machine inspect`` на macOS, типичные пути).
    """
    script = Path(__file__).resolve().parent / "detect_podman_socket.py"
    try:
        proc = subprocess.run(
            [sys.executable, str(script)],
            capture_output=True,
            text=True,
            timeout=35,
        )
        if proc.returncode == 0:
            line = (proc.stdout or "").strip().splitlines()
            candidate = (line[0] if line else "").strip()
            if candidate:
                logger.info("Путь к сокету Podman (detect_podman_socket.py): %s", candidate)
                return candidate
    except (OSError, subprocess.TimeoutExpired) as e:
        logger.debug("detect_podman_socket.py не удалось: %s", e)
    fallback = _podman_socket_path_for_user()
    logger.info("Сокет Podman не определён скриптом — подставляю типичный путь %s", fallback)
    return fallback


def ask_container_engine() -> tuple[str, str]:
    """Спросить только docker или podman; вернуть ``(CONTAINER_CLI, CONTAINER_SOCKET)`` с автоподбором пути."""
    print(
        "\n--- Движок контейнеров для compose ---\n"
        "  Укажите, чем пользуетесь на этом компьютере. Путь к сокету (CONTAINER_SOCKET) подставится сам;\n"
        "  CONTAINER_CLI в .env будет docker или podman (в образе веб-UI к сокету ходит клиент docker).\n",
    )
    pod_hint = _podman_socket_path_for_user()
    print("  1  или  d / docker   — Docker")
    print(f"  2  или  p / podman   — Podman (типичный путь при отсутствии сокета: {pod_hint})")
    print("  Enter — вариант 1 (Docker)")
    raw = input("  Ваш выбор [1]: ").strip().lower()

    if raw in ("", "1", "d", "docker"):
        cli, sock = "docker", _detect_docker_socket_path()
    elif raw in ("2", "p", "podman"):
        cli, sock = "podman", _detect_podman_socket_path()
    else:
        print("  Не распознано — принимаю Docker.")
        cli, sock = "docker", _detect_docker_socket_path()

    warn = ""
    try:
        if not Path(sock).is_socket():
            warn = " Внимание: по этому пути сейчас нет socket-файла — проверьте, что движок запущен, или поправьте CONTAINER_SOCKET в .env."
    except OSError:
        warn = " Внимание: нет доступа к пути сокета — при необходимости поправьте CONTAINER_SOCKET в .env."
    if cli == "podman":
        ug = _podman_uidgid_for_env()
        print(
            f"  Для rootless Podman в .env также будут записаны KIND_K8S_CONTAINER_UIDGID={ug} "
            "и KIND_K8S_CONTAINER_HOME=/tmp (иначе в контейнере будет permission denied на сокет).\n",
        )
    else:
        print(
            "  Для Docker в .env будут KIND_K8S_CONTAINER_UIDGID=0:0 и KIND_K8S_CONTAINER_HOME=/root.\n",
        )
    print(f"  В .env: CONTAINER_CLI={cli}, CONTAINER_SOCKET={sock}.{warn}\n")
    return cli, sock


def _compose_user_env_lines(engine_cli: str) -> list[str]:
    """
    Строки для .env: путь монтирования сокета и URI для ``DOCKER_HOST``, затем ``user``/``HOME``.

    Для Podman сокет в контейнере — ``/run/podman/podman.sock``, для Docker — ``/var/run/docker.sock``
    (должно совпадать с ``KIND_K8S_REMOTE_SOCKET_URI``).
    """
    el = engine_cli.strip().lower()
    block: list[str] = []
    if el == "podman":
        block.extend(
            [
                "# Том сокета внутри контейнера и URI API (не docker.sock — явно путь Podman)\n",
                "CONTAINER_SOCKET_MOUNT_TARGET=/run/podman/podman.sock\n",
                "KIND_K8S_REMOTE_SOCKET_URI=unix:///run/podman/podman.sock\n",
            ],
        )
    else:
        block.extend(
            [
                "CONTAINER_SOCKET_MOUNT_TARGET=/var/run/docker.sock\n",
                "KIND_K8S_REMOTE_SOCKET_URI=unix:///var/run/docker.sock\n",
            ],
        )
    block.append(
        "# KIND_K8S_CONTAINER_UIDGID — user: в compose; KIND_K8S_CONTAINER_HOME — переменная HOME в контейнере\n",
    )
    if el == "podman":
        ug = _podman_uidgid_for_env()
        block.append(f"KIND_K8S_CONTAINER_UIDGID={ug}\n")
        block.append("KIND_K8S_CONTAINER_HOME=/tmp\n")
    else:
        block.append("KIND_K8S_CONTAINER_UIDGID=0:0\n")
        block.append("KIND_K8S_CONTAINER_HOME=/root\n")
    return block


def _build_env_file_header() -> str:
    """Заголовок записываемого .env."""
    pod = _podman_socket_path_for_user()
    url = _unix_url_for_path(pod)
    uid = _process_uid()
    uid_line = (
        f"# UID пользователя, запустившего мастер (os.getuid / id -u): {uid}\n"
        if uid is not None
        else "# UID недоступен на этой ОС.\n"
    )
    return (
        "# Файл .env для Kind Clusters Dashboard\n"
        "# Создан интерактивно: scripts/setup_env_interactive.py\n"
        "#\n"
        "# В docker-compose.yml заданы литералами (не из этого файла): KIND_K8S_IN_CONTAINER, KIND_K8S_WORKDIR=/work.\n"
        "# DOCKER_HOST в контейнере берётся из KIND_K8S_REMOTE_SOCKET_URI (см. ниже).\n"
        "#\n"
        "# CONTAINER_SOCKET и CONTAINER_CLI — в мастере (только docker/podman); путь к сокету подбирается автоматически.\n"
        "# KIND_K8S_CONTAINER_UIDGID / KIND_K8S_CONTAINER_HOME — для доступа к сокету (Podman rootless: ваш uid:gid).\n"
        f"# Справочно, типичные пути: Docker — {_DOCKER_SOCKET_EXAMPLE}; Podman — {pod} (URL: {url}).\n"
        f"{uid_line}"
        "#\n\n"
    )


def _configure_logging() -> None:
    logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")


def _default_for_key(key: str) -> str:
    """Значение по умолчанию для ключа (только словарь скрипта)."""
    return _SETUP_DEFAULTS.get(key, "")


def _ask_line(
    key: str,
    default: str,
    *,
    help_ru: str,
    collected: dict[str, str],
) -> str | None:
    """
    Спросить одну переменную.

    Возвращает ``None``, если строку в ``.env`` не записывать (пропуск).
    """
    extra = ""
    if key in _SECRET_KEYS:
        extra = "  [g] — сгенерировать случайное значение"

    hint = default.replace("\n", " ")[:72]
    if len(default) > 72:
        hint += "…"
    if not default:
        hint = "пустая строка"

    print(f"\n{key}")
    print(f"  {help_ru}")
    print(f"  По умолчанию: {hint!r}{extra}")
    print("  Enter — подставить это значение в .env;  -  или «пропустить» — не добавлять строку")
    raw = input("  Значение: ").strip()

    if raw in ("-", "пропустить", "skip"):
        return None

    if not raw:
        return default

    if raw.lower() == "g" and key in _SECRET_KEYS:
        generated = secrets.token_urlsafe(32)
        print(f"  Сгенерировано ({len(generated)} символов).")
        return generated

    return raw


def _default_database_url(collected: dict[str, str]) -> str | None:
    """Сборка DATABASE_URL из POSTGRES_* (если такие ключи появятся в мастере)."""
    u = collected.get("POSTGRES_USER")
    p = collected.get("POSTGRES_PASSWORD")
    d = collected.get("POSTGRES_DB")
    if u is not None and p is not None and d is not None:
        return f"postgresql://{u}:{p}@postgres:5432/{d}"
    return None


def run(*, output: Path, force: bool = False) -> int:
    output = output.resolve()

    if output.exists() and not force:
        print(f"Файл уже существует: {output}")
        ans = input("Перезаписать? [y/N]: ").strip().lower()
        if ans not in ("y", "yes", "д", "да"):
            print("Выход без изменений.")
            return 0

    print(
        "\nИнтерактивное заполнение .env (список переменных в scripts/setup_env_interactive.py).\n"
        "Enter без ввода — записать значение по умолчанию из подсказки.\n",
    )

    engine_cli, engine_sock = ask_container_engine()

    collected: dict[str, str] = {}
    out_chunks: list[str] = [_build_env_file_header()]

    for spec in _SETUP_PROMPTS:
        if spec.section_comment:
            out_chunks.append(f"{spec.section_comment}\n")

        if spec.key == "CONTAINER_SOCKET":
            collected["CONTAINER_SOCKET"] = engine_sock
            collected["CONTAINER_CLI"] = engine_cli
            out_chunks.append(f"CONTAINER_SOCKET={engine_sock}\n")
            out_chunks.append(f"CONTAINER_CLI={engine_cli}\n")
            out_chunks.extend(_compose_user_env_lines(engine_cli))
            continue

        default = _default_for_key(spec.key)
        if spec.key == "DATABASE_URL":
            built = _default_database_url(collected)
            if built is not None:
                default = built
                print("\n--- DATABASE_URL: собрано из POSTGRES_* ---")

        value = _ask_line(spec.key, default, help_ru=spec.help_ru, collected=collected)
        if value is None:
            logger.debug("Пропуск переменной %s", spec.key)
            continue
        collected[spec.key] = value
        out_chunks.append(f"{spec.key}={value}\n")

    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text("".join(out_chunks), encoding="utf-8")
    print(f"\nГотово: записан файл {output}")
    return 0


def main() -> None:
    _configure_logging()
    parser = argparse.ArgumentParser(
        description="Интерактивное заполнение .env для Kind Clusters Dashboard (переменные задаются в этом скрипте)",
    )
    parser.add_argument(
        "--output",
        type=Path,
        default=REPO_ROOT / ".env",
        help="Куда записать .env (по умолчанию: .env в корне репозитория)",
    )
    parser.add_argument("-f", "--force", action="store_true", help="Не спрашивать подтверждение перезаписи")
    args = parser.parse_args()
    raise SystemExit(run(output=args.output, force=args.force))


if __name__ == "__main__":
    main()
