#!/usr/bin/env python3
"""
Клиентская часть установки: проверка JWT (RS256), скачивание пакета и запись .env.

Переменные окружения (вшиваются в install.sh):
  RCSLOT_BOOTSTRAP_PUBKEY_PEM   — публичный ключ RSA для проверки JWT (обязательно)
  RCSLOT_UPDATE_BASE_URL        — базовый URL сервера обновлений (https://update.slot.shepelev.me)
                                  используется для скачивания пакета поставки

Опциональный явный путь к уже распакованному пакету (без скачивания):
  RCSLOT_BOOTSTRAP_PACKAGE_ROOT или RCSLOT_PACKAGE_ROOT
"""
from __future__ import annotations

import getpass
import io
import os
import subprocess
import sys
import tarfile
import tempfile
import urllib.request
from pathlib import Path
from typing import TextIO

import jwt

EXPECTED_TYP = "rcslot_install_v1"
COMPOSE_LICENSEE = "docker-compose.client-licensee.yml"
BOOTSTRAP_CLIENT_REVISION = "2026-05-02d"


# ---------------------------------------------------------------------------
# stdin / prompt (при pipe из curl stdin занят — читаем с /dev/tty)
# ---------------------------------------------------------------------------

_prompt_in: TextIO | None = None


def _prompt_input() -> TextIO:
    global _prompt_in
    if _prompt_in is not None:
        return _prompt_in
    if sys.stdin.isatty():
        _prompt_in = sys.stdin
        return _prompt_in
    if sys.platform != "win32":
        try:
            _prompt_in = open("/dev/tty", "r", encoding="utf-8", errors="replace")
            return _prompt_in
        except OSError:
            pass
    _prompt_in = sys.stdin
    return _prompt_in


def _prompt(name: str, default: str | None = None, secret: bool = False) -> str:
    label = name
    if default is not None:
        label += f" [{default}]"
    label += ": "
    if secret:
        v = getpass.getpass(label)
    else:
        print(label, end="", flush=True)
        line = _prompt_input().readline()
        v = line.rstrip("\r\n").strip()
    if not v and default is not None:
        return default
    return v


# ---------------------------------------------------------------------------
# Пакет поставки
# ---------------------------------------------------------------------------

def _compose_licensee_path(root: Path) -> Path:
    return root / "compose" / COMPOSE_LICENSEE


def _find_local_package(hint: Path) -> Path | None:
    """Ищет корень пакета: сам путь, затем все подкаталоги (1 уровень)."""
    hint = hint.resolve()
    if _compose_licensee_path(hint).is_file():
        return hint
    if hint.is_dir():
        for child in sorted(hint.iterdir()):
            if child.is_dir() and not child.name.startswith("."):
                if _compose_licensee_path(child).is_file():
                    return child.resolve()
    return None


def _download_package(update_base_url: str, dest: Path) -> None:
    url = update_base_url.rstrip("/") + "/client-package.tar.gz"
    print(f"Скачиваю пакет поставки с {url} ...", flush=True)
    req = urllib.request.Request(url, headers={"User-Agent": f"rcslot-bootstrap/{BOOTSTRAP_CLIENT_REVISION}"})
    with urllib.request.urlopen(req, timeout=60) as resp:
        data = resp.read()
    with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tar:
        tar.extractall(str(dest))
    print(f"Пакет распакован в {dest}", flush=True)


def _ensure_package(update_base_url: str | None) -> Path:
    """
    Возвращает корень пакета (где есть compose/docker-compose.client-licensee.yml).
    Сначала смотрит env RCSLOT_BOOTSTRAP_PACKAGE_ROOT, затем cwd, затем скачивает.
    """
    # 1. Явный путь из окружения
    env_root = (
        (os.environ.get("RCSLOT_BOOTSTRAP_PACKAGE_ROOT") or os.environ.get("RCSLOT_PACKAGE_ROOT") or "").strip()
    )
    if env_root:
        p = Path(env_root).expanduser().resolve()
        if _compose_licensee_path(p).is_file():
            print(f"Пакет: {p} (из RCSLOT_BOOTSTRAP_PACKAGE_ROOT)\n")
            return p
        found = _find_local_package(p)
        if found:
            print(f"Пакет: {found} (найден под {p})\n")
            return found

    # 2. cwd и его подкаталоги
    found = _find_local_package(Path.cwd())
    if found:
        print(f"Пакет: {found}\n")
        return found

    # 3. Скачать с update-сервера
    if update_base_url:
        pkg_dir = Path("/opt/rcslot-install")
        pkg_dir.mkdir(parents=True, exist_ok=True)
        try:
            _download_package(update_base_url, pkg_dir)
        except Exception as e:
            print(f"Ошибка скачивания пакета: {e}", file=sys.stderr)
            sys.exit(7)
        if _compose_licensee_path(pkg_dir).is_file():
            return pkg_dir
        # Иногда tar содержит один подкаталог
        found = _find_local_package(pkg_dir)
        if found:
            return found
        print("Пакет скачан, но compose-файл не найден — архив повреждён?", file=sys.stderr)
        sys.exit(7)

    # 4. Ничего не нашли — попросить путь
    root_s = _prompt(
        "Каталог пакета (где лежат compose/ и nginx/) — или задайте RCSLOT_BOOTSTRAP_PACKAGE_ROOT",
        str(Path.cwd()),
    )
    root = Path(root_s).expanduser().resolve()
    found = _find_local_package(root)
    if found:
        return found
    print(f"Не найден файл {_compose_licensee_path(root)}", file=sys.stderr)
    print("Укажите корень пакета поставки (скачайте архив или задайте RCSLOT_UPDATE_BASE_URL).", file=sys.stderr)
    sys.exit(6)


# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------

def _rand_hex(nbytes: int) -> str:
    import secrets
    return secrets.token_hex(nbytes)


def _run(cmd: list[str], cwd: Path) -> None:
    subprocess.run(cmd, cwd=cwd, check=True)


def _registry_host_from_image(image: str) -> str | None:
    """Извлекает хост реестра из имени образа (если это не dockerhub)."""
    part = image.split("/")[0]
    if "." in part or ":" in part:
        return part
    return None


def _docker_login(registry: str, username: str, password: str) -> None:
    print(f"docker login {registry} ...", flush=True)
    result = subprocess.run(
        ["docker", "login", registry, "-u", username, "--password-stdin"],
        input=password.encode("utf-8"),
        capture_output=True,
    )
    if result.returncode != 0:
        msg = result.stderr.decode("utf-8", errors="replace").strip()
        print(f"Ошибка docker login: {msg}", file=sys.stderr)
        sys.exit(8)
    print("docker login OK", flush=True)


def _read_pubkey() -> str:
    pem = os.environ.get("RCSLOT_BOOTSTRAP_PUBKEY_PEM", "").strip()
    if not pem:
        print("RCSLOT_BOOTSTRAP_PUBKEY_PEM не задан", file=sys.stderr)
        sys.exit(2)
    return pem


# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------

def main() -> None:
    if len(sys.argv) < 2:
        print(
            "Использование: RCSLOT_BOOTSTRAP_PUBKEY_PEM='...' python3 bootstrap_client.py '<JWT>'",
            file=sys.stderr,
        )
        sys.exit(1)

    token = sys.argv[1].strip()
    pubkey = _read_pubkey()

    try:
        data = jwt.decode(token, pubkey, algorithms=["RS256"], options={"require": ["exp", "iat"]})
    except jwt.PyJWTError as e:
        print(f"JWT недействителен или просрочен: {e}", file=sys.stderr)
        sys.exit(3)

    if data.get("typ") != EXPECTED_TYP:
        print("Неверный тип токена установки", file=sys.stderr)
        sys.exit(4)

    required = ["license_key", "site_domain", "rcslot_app_image", "rcslot_frontend_image", "next_public_app_url"]
    missing = [k for k in required if not data.get(k)]
    if missing:
        print(f"В JWT не хватает полей: {missing}", file=sys.stderr)
        sys.exit(5)

    print(f"=== RCSlot Dashboard — установка по токену (bootstrap {BOOTSTRAP_CLIENT_REVISION}) ===\n")

    update_base = (os.environ.get("RCSLOT_UPDATE_BASE_URL") or "").strip().rstrip("/")

    root = _ensure_package(update_base or None)
    compose_rel = _compose_licensee_path(root)

    env_path = root / ".env"
    if env_path.exists():
        yn = _prompt(f"Файл {env_path} уже есть. Перезаписать? (yes/N)", "N")
        if yn.lower() not in ("yes", "y", "да"):
            print("Отменено.")
            sys.exit(0)

    pg_user = _prompt("POSTGRES_USER", "dashboard")
    pg_pass = _prompt("POSTGRES_PASSWORD (пусто = сгенерировать)", "")
    if not pg_pass:
        pg_pass = _rand_hex(16)
        print(f"Сгенерирован POSTGRES_PASSWORD (сохраните): {pg_pass}")

    pg_db = _prompt("POSTGRES_DB", "dashboard")
    admin_login = _prompt("ADMIN_LOGIN", "admin")
    admin_pass = _prompt("ADMIN_PASSWORD (пусто = сгенерировать)", "", secret=True)
    if not admin_pass:
        admin_pass = _rand_hex(12)
        print(f"Сгенерирован ADMIN_PASSWORD (сохраните): {admin_pass}")

    secret_key = _prompt("SECRET_KEY (пусто = сгенерировать)", "", secret=True)
    if not secret_key:
        secret_key = _rand_hex(32)
        print("Сгенерирован SECRET_KEY (длина OK)")

    tz = _prompt("TZ", "Europe/Moscow")
    dist = data.get("distribution_mode") or "licensee"
    lic_url = data.get("license_server_url") or "https://license.slot.shepelev.me"
    disabled = data.get("disabled_features") or ""
    update_url = update_base or ""

    lines = [
        f"SECRET_KEY={secret_key}",
        f"ADMIN_LOGIN={admin_login}",
        f"ADMIN_PASSWORD={admin_pass}",
        "",
        f"POSTGRES_USER={pg_user}",
        f"POSTGRES_PASSWORD={pg_pass}",
        f"POSTGRES_DB={pg_db}",
        "",
        f"TZ={tz}",
        "",
        "RCSLOT_APP_IMAGE=" + str(data["rcslot_app_image"]),
        "RCSLOT_FRONTEND_IMAGE=" + str(data["rcslot_frontend_image"]),
        "",
        f"NEXT_PUBLIC_APP_URL={data['next_public_app_url']}",
        "",
        f"DISTRIBUTION_MODE={dist}",
        f"LICENSE_KEY={data['license_key']}",
        f"SITE_DOMAIN={data['site_domain']}",
        f"LICENSE_SERVER_URL={lic_url}",
        "",
    ]
    if update_url:
        lines += [f"UPDATE_SERVER_URL={update_url}", ""]
    if disabled:
        lines += [f"DISABLED_FEATURES={disabled}", ""]

    env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
    os.chmod(env_path, 0o600)
    print(f"\nЗаписан {env_path}\n")

    reg_user = (data.get("registry_auth_user") or "").strip()
    reg_pass = (data.get("registry_auth_password") or "").strip()

    yn = _prompt("Выполнить сейчас: docker compose pull && up -d? (y/N)", "N")
    if yn.lower() in ("y", "yes", "д", "да"):
        # docker login если в JWT есть учётные данные реестра
        if reg_user and reg_pass:
            reg_host = _registry_host_from_image(data.get("rcslot_app_image", ""))
            if reg_host:
                _docker_login(reg_host, reg_user, reg_pass)

        cmd_base = ["docker", "compose", "--env-file", str(env_path), "-f", str(compose_rel)]
        print(" ", " ".join(cmd_base + ["pull"]))
        _run(cmd_base + ["pull"], cwd=root)
        print(" ", " ".join(cmd_base + ["up", "-d"]))
        _run(cmd_base + ["up", "-d"], cwd=root)
        print("\nГотово. Проверьте https://" + str(data["site_domain"]).split("/")[0] + "/api/health")
    else:
        print("Дальше вручную из каталога пакета:")
        print(f"  docker compose --env-file .env -f compose/{COMPOSE_LICENSEE} pull && \\")
        print(f"  docker compose --env-file .env -f compose/{COMPOSE_LICENSEE} up -d")


if __name__ == "__main__":
    main()
