#!/usr/bin/env bash
set -euo pipefail

readonly GUM_URL="https://static.afi-d.ru/bin/gum-linux-amd64"
readonly STATE_FILE="$(pwd)/klvd-helm-install-state.ini"
readonly OCI_CHART="oci://registry-ru-2.afi-d.ru/afi/klvd/charts/klvd-node"
readonly REGISTRY="registry-ru-2.afi-d.ru"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SECRET_INPUT_SH="$SCRIPT_DIR/helm/scripts/secret-input.sh"
[[ -f "$SECRET_INPUT_SH" ]] || SECRET_INPUT_SH="$SCRIPT_DIR/scripts/secret-input.sh"
SECRET_INPUT_URL="https://static.afi-d.ru/products/klvd/install/secret-input.sh"

ensure_secret_input() {
    if [[ -f "$SECRET_INPUT_SH" ]]; then return 0; fi
    warn "secret-input.sh не найден рядом ($SCRIPT_DIR/helm/scripts/) — скачивается с static.afi-d.ru."
    mkdir -p "$TMP_DIR"
    local out="$TMP_DIR/secret-input.sh"
    download_verified "$SECRET_INPUT_URL" "$out" "secret-input.sh" \
        || die "Не удалось скачать secret-input.sh ($SECRET_INPUT_URL). Скачайте klvd-install-helm.sh вместе с helm/scripts/secret-input.sh."
    SECRET_INPUT_SH="$out"
}

GUM=""
TMP_DIR="/tmp/klvd-helm-install"

log()   { printf '\033[1;34m[klvd]\033[0m %s\n' "$*"; }
warn()  { printf '\033[1;33m[!]\033[0m %s\n' "$*"; }
err()   { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; }
die()   { err "$*"; exit 1; }
ok()    { printf '\033[1;32m[✓]\033[0m %s\n' "$*"; }

state_get() {
    [[ -f "$STATE_FILE" ]] || return 0
    local k="$1"
    grep -E "^${k}=" "$STATE_FILE" 2>/dev/null | head -1 | cut -d'=' -f2- || true
}
state_set() {
    local k="$1" v="$2"
    case "$k" in
        PG_PASSWORD|SECRET_KEY|REGISTRY_PASS) return 0 ;;
    esac
    mkdir -p "$(dirname "$STATE_FILE")"
    touch "$STATE_FILE"; chmod 600 "$STATE_FILE"
    local tmp="$STATE_FILE.tmp.$$"
    awk -v k="$k" -v v="$v" 'BEGIN{f=0} $0~"^"k"="{print k"="v;f=1;next}{print} END{if(!f)print k"="v}' \
        "$STATE_FILE" > "$tmp" 2>/dev/null || die "state_set: не удалось записать $STATE_FILE."
    mv "$tmp" "$STATE_FILE"; chmod 600 "$STATE_FILE"
}
step_done() { [[ "$(state_get "$1")" == "done" ]]; }
mark_done() { state_set "$1" "done"; }

run_spin() {
    local msg="$1"; shift
    if [[ ! -t 1 ]]; then log "$msg…"; "$@"; return $?; fi
    "$@" & local pid=$!
    local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' i=0
    while kill -0 "$pid" 2>/dev/null; do
        printf '\r\033[36m%s\033[0m %s' "${spin:i%10:1}" "$msg"; sleep 0.08; i=$((i+1))
    done
    printf '\r\033[K'; wait "$pid"
}
download_verified() {
    local url="$1" dest="$2" label="${3:-артефакт}"
    wget -qO "$dest" "$url" || { rm -f "$dest"; return 1; }
    local sum_file="$dest.sha256" want hash
    if wget -qO "$sum_file" "$url.sha256" 2>/dev/null && [[ -s "$sum_file" ]]; then
        want="$(awk '{print $1}' "$sum_file" | tr -dc 'a-fA-F0-9')"
        hash="$(sha256sum "$dest" | awk '{print $1}')"
        if [[ -z "$want" || "${want,,}" != "${hash,,}" ]]; then
            rm -f "$dest" "$sum_file"; die "Контрольная сумма $label не совпала (ожидалась ${want:-?}, получена $hash)."
        fi
        ok "$label: контрольная сумма совпадает."
    fi
}
fetch_gum() {
    if step_done step_gum && [[ -x "$(state_get gum_path)" ]]; then
        GUM="$(state_get gum_path)"; return
    fi
    mkdir -p "$TMP_DIR"
    local out="$TMP_DIR/gum"
    if download_verified "$GUM_URL" "$out" "gum (TUI-промпты)"; then
        chmod +x "$out"; GUM="$out"
    else
        warn "gum недоступен ($GUM_URL) — переключение на простой read-ввод."
        GUM=""
    fi
    state_set gum_path "$GUM"; mark_done step_gum
}

g_header() {
    if [[ -x "$GUM" ]]; then "$GUM" style --border rounded --padding "0 2" --border-foreground 4 --bold "$1"
    else printf '\n=== %s ===\n' "$1"; fi
}
g_input() {
    local label="$1" def="${2:-}" out=""
    if [[ -x "$GUM" ]]; then
        local args=(--prompt "› " --header "$label" --placeholder "введите и нажмите Enter…")
        [[ -n "$def" ]] && args+=(--value "$def")
        if out="$("$GUM" input "${args[@]}" 2>/dev/null)"; then out="${out:-$def}"; else out="$def"; fi
    else local ans; read -r -p "$label${def:+ [$def]}: " ans || ans=""; out="${ans:-$def}"; fi
    printf '%s\n' "$out"
}
g_password() {
    local label="$1" out=""
    if [[ -x "$GUM" ]]; then
        if out="$("$GUM" input --prompt "🔒 " --header "$label" --placeholder "ввод скрыт…" --password 2>/dev/null)"; then :; else out=""; fi
    else local ans; read -rs -p "$label: " ans; echo >&2; out="$ans"; fi
    printf '%s\n' "$out"
}
g_confirm() {
    local q="$1" def="${2:-true}" hint
    [[ "$def" == true ]] && hint="Y/n" || hint="y/N"
    if [[ -x "$GUM" ]]; then "$GUM" confirm --default="$def" "$q" 2>/dev/null
    else local ans; read -r -p "$q [$hint] " ans || ans=""; { [[ "$def" == true && -z "$ans" ]] || [[ "$ans" =~ ^[Yy] ]]; }; fi
}
g_choose_many() {
    local header="$1"; shift
    local opts=() item value label
    for item in "$@"; do value="${item%%|*}"; label="${item#*|}"; opts+=("$label [$value]"); done
    if [[ -x "$GUM" ]]; then
        log "Перемещение — стрелки ↑↓, отметить/снять — Пробел, подтвердить выбор — Ввод."
        "$GUM" choose --no-limit --header "$header" --selected-prefix "✓ " --unselected-prefix "  " "${opts[@]}" 2>/dev/null \
            | sed -E 's/^.*\[([^][]+)]$/\1/'
    else
        local i=1; log "$header"
        for item in "$@"; do value="${item%%|*}"; label="${item#*|}"; printf '  %d) %s [%s]\n' "$i" "$label" "$value"; ((i++)); done
        local ans; read -r -p "номера через запятую: " ans || ans=""
        tr ',' '\n' <<<"$ans" | while read -r i; do i="${i//[[:space:]]/}"; [[ "$i" =~ ^[0-9]+$ ]] || continue
            ((i>=1 && i<=$#)) || continue; item="${!i}"; printf '%s\n' "${item%%|*}"; done
    fi
}

check_prereqs() {
    command -v kubectl >/dev/null || die "kubectl не найден в PATH."
    command -v helm   >/dev/null || die "helm не найден в PATH (нужен ≥3.8 для OCI)."
    command -v openssl >/dev/null || die "openssl не найден."
    local ctx; ctx="$(kubectl config current-context 2>/dev/null || echo "?")"
    log "Текущий kube-context: $ctx"
    g_confirm "Продолжить установку в этот контекст?" true || die "Прервано пользователем."
}

ask_namespace() {
    if step_done step_ns; then NS="$(state_get ns)"; log "namespace (из state): $NS"; return; fi
    g_header "Namespace"
    NS="$(g_input "Namespace для установки" "klvd")"
    if ! kubectl get namespace "$NS" >/dev/null 2>&1; then
        g_confirm "Namespace $NS не существует. Создать?" true && kubectl create namespace "$NS" \
            || die "Создайте namespace $NS вручную и перезапустите."
    fi
    state_set ns "$NS"; mark_done step_ns
}

detect_minikube() {
    if step_done step_minikube; then MINIKUBE="$(state_get minikube)"; log "minikube (из state): $MINIKUBE"; return; fi
    local ctx; ctx="$(kubectl config current-context 2>/dev/null || true)"
    MINIKUBE="no"
    if [[ "$ctx" == *minikube* ]] || kubectl get nodes -o jsonpath='{.items[*].status.nodeInfo.kernelVersion}' 2>/dev/null | grep -qi minikube \
        || command -v minikube >/dev/null && minikube status >/dev/null 2>&1; then
        MINIKUBE="yes"
        g_header "minikube detected"
        warn "minikube: LoadBalancer без \`minikube tunnel\` не выдаёт IP — для IMAP/SMTP предлагается NodePort."
        warn "Ingress: предлагается \`minikube addons enable ingress\` (className nginx)."
        if g_confirm "Включить ingress addon minikube (если выключен)?" true; then
            minikube addons enable ingress 2>/dev/null || warn "minikube addons enable ingress не выполнено — включите вручную."
        fi
    fi
    state_set minikube "$MINIKUBE"; mark_done step_minikube
}

ask_ingress() {
    if step_done step_ingress; then
        INGRESS_ENABLED="$(state_get ingress_enabled)"
        INGRESS_HOST="$(state_get ingress_host)"
        INGRESS_CLASS="$(state_get ingress_class)"
        INGRESS_PUBLIC_PORT="$(state_get ingress_public_port)"
        log "ingress (из state): enabled=$INGRESS_ENABLED host=$INGRESS_HOST"; return
    fi
    g_header "Ingress (доступ к WUI снаружи)"
    log "Ingress открывает панель управления (WUI) снаружи кластера по домену."
    log "Без ingress WUI доступен только внутри кластера (ClusterIP) — через kubectl port-forward."
    INGRESS_ENABLED="false"
    if g_confirm "Установить ingress для доступа к WUI снаружи?" true; then
        INGRESS_ENABLED="true"
        INGRESS_HOST="$(g_input "Домен (host) для WUI" "klvd.example.com")"
        INGRESS_CLASS="$(g_input "ingressClassName" "$( [[ "$MINIKUBE" == yes ]] && echo nginx || echo nginx )")"
        INGRESS_PUBLIC_PORT="$(g_input "Публичный порт ingress (может отличаться от порта WUI)" "443")"
    else
        warn "WUI останется ClusterIP. Доступ: kubectl port-forward svc/<wui> <wui_port>:<wui_port>"
    fi
    state_set ingress_enabled "$INGRESS_ENABLED"
    state_set ingress_host "$INGRESS_HOST"
    state_set ingress_class "$INGRESS_CLASS"
    state_set ingress_public_port "$INGRESS_PUBLIC_PORT"
    mark_done step_ingress
}

ask_pg() {
    if step_done step_pg; then
        PG_HOST="$(state_get pg_host)"; PG_PORT="$(state_get pg_port)"
        PG_DB="$(state_get pg_db)"; log "PG (из state): $PG_HOST:$PG_PORT/$PG_DB"; return
    fi
    g_header "Внешний PostgreSQL (кластер Клавдия)"
    log "Укажите параметры внешней PostgreSQL-совместимой СУБД, уже работающей с кластером."
    log "Пользователь должен обладать всеми правами в рамках базы данных"
    log "(создание и изменение таблиц, индексов, схем) — приложение применяет миграции само."
    PG_HOST="$(g_input "PostgreSQL host" "")"
    [[ -n "$PG_HOST" ]] || die "PG host обязателен."
    PG_PORT="$(g_input "PostgreSQL port" "5432")"
    PG_DB="$(g_input "База" "appdb")"
    state_set pg_host "$PG_HOST"; state_set pg_port "$PG_PORT"; state_set pg_db "$PG_DB"
    mark_done step_pg
}
ask_solr() {
    if step_done step_solr; then SOLR_URL="$(state_get solr_url)"; log "Solr (из state): $SOLR_URL"; return; fi
    g_header "Внешний Solr (кластер Клавдия)"
    SOLR_URL="$(g_input "Solr URL" "http://solr.klvd.svc.cluster.local:8983/solr")"
    state_set solr_url "$SOLR_URL"; mark_done step_solr
}
ask_wui_port() {
    if step_done step_wui_port; then WUI_PORT="$(state_get wui_port)"; log "WUI port (из state): $WUI_PORT"; return; fi
    g_header "Порт WUI"
    WUI_PORT="$(g_input "KLVD_WEB_LISTEN_PORT (контейнер/Service)" "8001")"
    state_set wui_port "$WUI_PORT"; mark_done step_wui_port
}

ask_roles() {
    if step_done step_roles; then ROLES="$(state_get roles)"; log "roles (из state): $ROLES"; return; fi
    g_header "Роли узла (cap)"
    log "Роли определяют службы узла. Панель управления (ui) добавляется принудительно."
    log "Роли можно менять позже через панель — выбор задаёт лишь разрешённый набор."
    local sel
    sel="$(g_choose_many "Выберите роли (ui добавляется принудительно)" \
        "ui|WUI (веб-панель)" "storage|Storage (chunks SQLite)" "importer|Импорт (EWS/IMAP)" \
        "indexer|Индексация Solr" "imap|IMAP-сервер" "smtp|SMTP-сервер")"
    local roles="" r
    while read -r r; do [[ -n "$r" ]] && roles="${roles:+$roles,}$r"; done <<<"$sel"
    [[ ",$roles," == *",ui,"* ]] || roles="${roles:+$roles,}ui"
    ROLES="$roles"
    state_set roles "$ROLES"; mark_done step_roles
}

ask_replicas() {
    if step_done step_replicas; then REPLICAS="$(state_get replicas)"; log "replicas (из state): $REPLICAS"; return; fi
    g_header "Масштабирование"
    log "Каждая реплика StatefulSet — отдельный узел кластера со своим node UID."
    log "Для первой установки достаточно 1; число реплик можно увеличить позже через helm upgrade."
    REPLICAS="$(g_input "replicaCount (каждая реплика = узел кластера)" "1")"
    state_set replicas "$REPLICAS"; mark_done step_replicas
}

ask_service_types() {
    if step_done step_svctype; then
        IMAP_TYPE="$(state_get imap_type)"; SMTP_TYPE="$(state_get smtp_type)"
        log "service types (из state): imap=$IMAP_TYPE smtp=$SMTP_TYPE"; return
    fi
    g_header "Тип Service для IMAP/SMTP"
    local def_lb="LoadBalancer"
    [[ "$MINIKUBE" == yes ]] && def_lb="NodePort"
    IMAP_TYPE="$(g_input "IMAP Service type (LoadBalancer|NodePort|ClusterIP)" "$def_lb")"
    SMTP_TYPE="$(g_input "SMTP Service type (LoadBalancer|NodePort|ClusterIP)" "$def_lb")"
    state_set imap_type "$IMAP_TYPE"; state_set smtp_type "$SMTP_TYPE"; mark_done step_svctype
}

ask_registry_auth() {
    if step_done step_regcred; then
        PULL_SECRETS="$(state_get pull_secrets)"; log "imagePullSecrets (из state): ${PULL_SECRETS:-none}"; return
    fi
    g_header "Доступ к образу (registry-ru-2.afi-d.ru)"
    PULL_SECRETS=""
    if g_confirm "Нужен imagePullSecret для $REGISTRY (приватный registry)?" false; then
        local u p
        u="$(g_input "registry username" "")"
        p="$(g_password "registry password/token")"
        [[ -n "$u" && -n "$p" ]] || die "username/password обязательны."
        kubectl -n "$NS" create secret docker-registry klvd-regcred \
            --docker-server="$REGISTRY" --docker-username="$u" --docker-password="$p" >/dev/null \
            || die "kubectl create secret docker-registry не выполнен."
        PULL_SECRETS="klvd-regcred"
        ok "imagePullSecret klvd-regcred создан в $NS."
    fi
    state_set pull_secrets "$PULL_SECRETS"; mark_done step_regcred
}

do_secret() {
    if step_done step_secret; then SECRET_NAME="$(state_get secret_name)"; log "secret (из state): $SECRET_NAME"; return; fi
    ensure_secret_input
    # shellcheck source=helm/scripts/secret-input.sh
    . "$SECRET_INPUT_SH"
    g_header "Секрет klvd-node (pg_user, pg_password, klvd_key, ssl_cert)"
    log "Единый k8s Secret содержит учётные данные и сертификат узла."
    log "Можно использовать существующий Secret, подготовить файлы или сгенерировать новый."
    SECRET_NAME="$(secret_resolve "$NS" "klvd-node-secrets" "appuser")"
    state_set secret_name "$SECRET_NAME"; mark_done step_secret
}

resolve_chart_version() {
    if step_done step_version; then CHART_VERSION="$(state_get chart_version)"; log "chart version (из state): $CHART_VERSION"; return; fi
    g_header "Версия чарта"
    CHART_VERSION="$(g_input "Версия чарта (пусто = последняя из OCI registry)" "")"
    if [[ -z "$CHART_VERSION" ]]; then
        log "Определение последней версии чарта…"
        CHART_VERSION="$(helm show chart "$OCI_CHART" 2>/dev/null | grep -m1 -E '^version:' | awk '{print $2}')"
        [[ -n "$CHART_VERSION" ]] || die "Не удалось определить версию чарта. Укажите явно."
        ok "Последняя версия: $CHART_VERSION"
    fi
    state_set chart_version "$CHART_VERSION"; mark_done step_version
}

write_values() {
    local f="$1"
    {
        cat <<EOF
# Сгенерировано klvd-install-helm.sh $(date -u 2>/dev/null || true)
roles: "$ROLES"
replicaCount: $((REPLICAS))

postgresql:
  host: "$PG_HOST"
  port: "$PG_PORT"
  database: "$PG_DB"
  ssl: "require"
  existingSecret: "$SECRET_NAME"

solr:
  url: "$SOLR_URL"

secrets:
  existingSecret: "$SECRET_NAME"
  managed: false

wui:
  listenPort: $((WUI_PORT))
  instanceName: klvd-wui
  webThreads: "8"

imap:
  service: { type: $IMAP_TYPE, port: 993, altPort: 143 }
smtp:
  service: { type: $SMTP_TYPE, port: 25, submissionPort: 587 }

service:
  type: ClusterIP
  port: $((WUI_PORT))
EOF
        if [[ "$INGRESS_ENABLED" == true ]]; then
            cat <<EOF
ingress:
  enabled: true
  className: "$INGRESS_CLASS"
  publicPort: $((INGRESS_PUBLIC_PORT))
  hosts:
    - host: "$INGRESS_HOST"
      paths:
        - { path: /, pathType: Prefix }
  tls: []
EOF
        else
            echo "ingress:"
            echo "  enabled: false"
        fi
        if [[ "$MINIKUBE" == yes ]]; then
            echo 'persistence:'
            echo '  appData: { size: 1Gi, storageClass: "standard" }'
            echo '  logs:    { enabled: true, size: 1Gi, storageClass: "standard" }'
        fi
        if [[ -n "$PULL_SECRETS" ]]; then
            echo 'image:'
            echo '  pullSecrets:'
            for s in ${PULL_SECRETS//,/ }; do echo "    - name: $s"; done
        fi
    } > "$f"
    chmod 600 "$f"
}

do_install() {
    if step_done step_install; then log "helm install уже выполнен (state)."; return; fi
    g_header "helm install"
    local vf="$TMP_DIR/values.yaml"
    mkdir -p "$TMP_DIR"
    write_values "$vf"
    log "Сгенерирован values: $vf"
    RELEASE="${RELEASE:-klvd}"
    log "helm install $RELEASE $OCI_CHART --version $CHART_VERSION -n $NS"
    helm install "$RELEASE" "$OCI_CHART" --version "$CHART_VERSION" -n "$NS" -f "$vf"
    state_set release "$RELEASE"
    mark_done step_install
    ok "Установка выполнена."
}

print_notes() {
    g_header "Готово"
    local wui_url
    if [[ "$INGRESS_ENABLED" == true ]]; then
        wui_url="https://$INGRESS_HOST:$INGRESS_PUBLIC_PORT/"
    else
        wui_url="kubectl port-forward -n $NS svc/$RELEASE-klvd-node-wui $WUI_PORT:$WUI_PORT  → https://127.0.0.1:$WUI_PORT/"
    fi
    cat <<EOF

WUI:           $wui_url
IMAP Service:  $RELEASE-klvd-node-imap ($IMAP_TYPE, порт 993)
SMTP Service:  $RELEASE-klvd-node-smtp ($SMTP_TYPE, порт 25/587)
Secret:        $SECRET_NAME (namespace $NS)

ВАЖНО:
  • IMAP/SMTP слушатели поднимаются ТОЛЬКО после включения роли в панели WUI
    (раздел «Кластер/Узлы»). placement.imap_ip править НЕ нужно (bind 0.0.0.0).
  • Поды:    kubectl get pods -n $NS -l app.kubernetes.io/instance=$RELEASE
  • Логи:    kubectl logs -n $NS statefulset/$RELEASE-klvd-node
  • Масштаб: helm upgrade $RELEASE $OCI_CHART --version $CHART_VERSION -n $NS -f $TMP_DIR/values.yaml --set replicaCount=N
EOF
    [[ "$MINIKUBE" == yes ]] && echo "  • minikube: minikube service $RELEASE-klvd-node-imap -n $NS  (или minikube tunnel для LB/ingress:443)"
}

main() {
    if [[ "${1:-}" == "--reset" ]]; then
        rm -f "$STATE_FILE"; log "Сброшен прогресс (удалён $STATE_FILE)."; exit 0
    fi
    [[ -t 0 ]] || warn "Нет TTY — gum-промпты заменяются read (может потребоваться ввод)."

    g_header "Установка узла САП «Клавдий» в Kubernetes"
    log "Установщик развернёт узел через helm OCI-чарт в существующем k8s-кластере."
    log "Понадобятся внешние PostgreSQL и Solr — локальные в кластер не разворачиваются."
    log "Прогресс сохраняется (klvd-helm-install-state.ini); секреты в нём не хранятся."

    fetch_gum
    check_prereqs
    ask_namespace
    detect_minikube
    ask_ingress
    ask_pg
    ask_solr
    ask_wui_port
    ask_roles
    ask_replicas
    ask_service_types
    ask_registry_auth
    do_secret
    resolve_chart_version
    do_install
    print_notes
    ok "klvd-node установлен в namespace $NS."
}

main "$@"
