#!/usr/bin/env bash
# Lendry ID — установка, Nginx, SSL и управление одной командой
#
# Использование:
#   chmod +x install.sh && ./install.sh
#   ./install.sh --install --api-domain api.example.com --frontend-domain id.example.com \
#     --docs-domain docs.example.com --email admin@example.com
#   ./install.sh --configure-domains   # изменить домены и пересобрать
#   ./install.sh --local               # локально без Nginx

set -euo pipefail

SCRIPT_VERSION="2.2.5"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"

NGINX_PREFIX="lendry-id"
NGINX_AVAILABLE="/etc/nginx/sites-available"
NGINX_ENABLED="/etc/nginx/sites-enabled"
CERTBOT_WEBROOT="/var/www/certbot"
LOCAL_CONF_DIR="${ROOT_DIR}/nginx/conf.d"
LOCAL_CERTS_DIR="${ROOT_DIR}/nginx/certs"
CUSTOM_CERT_TARGET_DIR="${LOCAL_CERTS_DIR}/custom"
DEFAULT_CERT_UPLOAD_DIR="${LOCAL_CERTS_DIR}/upload"
COMPOSE_OVERRIDE="${ROOT_DIR}/docker-compose.override.yml"
STATE_FILE="${ROOT_DIR}/.idp-install.json"
INFRA_SECRETS_FILE="${ROOT_DIR}/.idp-infra.secrets"

NON_INTERACTIVE=0
FORCE_ENV="${FORCE_ENV:-0}"
ACTION=""
INSTALL_LOCAL=0
INSTALL_INTRANET=0
OFFLINE="${OFFLINE:-0}"
NGINX_MODE="${NGINX_MODE:-auto}"
SKIP_DOCKER_PRUNE="${SKIP_DOCKER_PRUNE:-0}"
DOCKER_PRUNE_AGGRESSIVE="${DOCKER_PRUNE_AGGRESSIVE:-0}"

CLI_DOMAIN_API=""
CLI_DOMAIN_FRONTEND=""
CLI_DOMAIN_DOCS=""
CLI_DOMAIN_WS=""
CLI_DOMAIN_MINIO=""
CLI_DOMAIN_MINIO_CONSOLE=""
CLI_EMAIL=""
CLI_SSL_TYPE=""
CLI_CERT_DIR=""
CLI_CERT_FULLCHAIN=""
CLI_CERT_KEY=""
CLI_NGINX_HTTP_PORT=""
CLI_NGINX_HTTPS_PORT=""

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

log() { echo -e "${CYAN}[IdP]${NC} $*"; }
ok() { echo -e "${GREEN}[IdP]${NC} $*"; }
warn() { echo -e "${YELLOW}[IdP]${NC} $*"; }
fail() { echo -e "${RED}[IdP] ОШИБКА:${NC} $*" >&2; exit 1; }

usage() {
  cat <<EOF
${BOLD}Lendry ID v${SCRIPT_VERSION}${NC} — установка и управление

  ./install.sh                    Интерактивное меню
  ./install.sh --install          Полная установка
  ./install.sh --configure-domains  Изменить домены / Nginx / SSL
  ./install.sh --renew-ssl        Обновить сертификаты (Let's Encrypt / custom / selfsigned)
  ./install.sh --restart          Перезапуск контейнеров
  ./install.sh --docker-prune     Только очистка Docker (без пересборки)
  ./install.sh --fix-proxy        Исправить 502: host Nginx + порты 3000/3002/3003
  ./install.sh --status           Статус сервисов
  ./install.sh --reset-db            Сброс PostgreSQL (новый пароль из .env, данные удалятся)
  ./install.sh --remove-nginx     Удалить конфиги Nginx IdP

${BOLD}Режим локальной сети (без интернета / AD-домены .local .lpr):${NC}
  --intranet                      Локальная сеть: HTTP или самоподписанный HTTPS
  --ssl TYPE                      none | selfsigned | custom | letsencrypt
  --cert-dir PATH                 Папка со своими сертификатами (для --ssl custom)
  --cert-fullchain FILE           Файл сертификата (fullchain.pem, _wildcard.domain.pem, …)
  --cert-key FILE                 Файл ключа (privkey.pem, _wildcard.domain-key.pem, …)
  --nginx-http-port PORT          HTTP-порт Nginx (если 80 занят, например 8080)
  --nginx-https-port PORT         HTTPS-порт Nginx (если 443 занят, например 8443)
  --offline                       Не скачивать Docker из интернета (только если уже установлен)

${BOLD}Домены (можно указывать отдельно):${NC}
  --api-domain DOMAIN             API / Swagger
  --frontend-domain DOMAIN        Веб-интерфейс
  --docs-domain DOMAIN            Документация
  --ws-domain DOMAIN              WebSocket (пусто = API + /ws)
  --minio-domain DOMAIN           MinIO S3 (опционально)
  --minio-console-domain DOMAIN   MinIO Console (опционально)
  --email EMAIL                   Email для Let's Encrypt (только ssl=letsencrypt)

${BOLD}Прочие опции:${NC}
  -y, --yes                       Без интерактивных вопросов
  --local                         localhost:3000/3002/3003 без Nginx
  -h, --help                      Справка

${BOLD}Переменные окружения:${NC}
  FORCE_ENV=1                     Новые JWT-секреты (пароли БД/MinIO/RabbitMQ сохраняются если volume уже есть)
  OFFLINE=1                       То же, что --offline
  SKIP_DOCKER_PRUNE=1             Не очищать образы/кеш перед пересборкой
  DOCKER_PRUNE_AGGRESSIVE=1       Глубже чистить неиспользуемые образы проекта
  COMPOSE_PARALLEL_LIMIT=4        Параллельная сборка сервисов (по умолчанию 4)

${BOLD}Примеры:${NC}
  ./install.sh --intranet --install --yes \\
    --api-domain api.idpmvk.lpr \\
    --frontend-domain id.idpmvk.lpr \\
    --docs-domain docs.idpmvk.lpr \\
    --ssl custom --cert-dir /opt/idp-certs

  ./install.sh --intranet --install --yes \\
    --api-domain api.idpmvk.lpr \\
    --frontend-domain id.idpmvk.lpr \\
    --docs-domain docs.idpmvk.lpr \\
    --ssl none

  ./install.sh --intranet --ssl selfsigned \\
    --api-domain api.idpmvk.lpr --frontend-domain id.idpmvk.lpr \\
    --docs-domain docs.idpmvk.lpr

  ./install.sh --install --yes \\
    --api-domain api.id.example.ru \\
    --frontend-domain id.example.ru \\
    --docs-domain docs.example.ru \\
    --ssl letsencrypt --email admin@example.ru
  ./install.sh --configure-domains
EOF
}

parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      -y | --yes) NON_INTERACTIVE=1; shift ;;
      --install) ACTION="install"; shift ;;
      --configure-domains) ACTION="configure-domains"; shift ;;
      --renew-ssl) ACTION="renew-ssl"; shift ;;
      --restart) ACTION="restart"; shift ;;
      --docker-prune) ACTION="docker-prune"; shift ;;
      --skip-docker-prune) SKIP_DOCKER_PRUNE=1; shift ;;
      --docker-prune-aggressive) DOCKER_PRUNE_AGGRESSIVE=1; shift ;;
      --fix-proxy) ACTION="fix-proxy"; shift ;;
      --status) ACTION="status"; shift ;;
      --reset-db) ACTION="reset-db"; shift ;;
      --local) INSTALL_LOCAL=1; shift ;;
      --intranet) INSTALL_INTRANET=1; shift ;;
      --offline) OFFLINE=1; shift ;;
      --ssl) CLI_SSL_TYPE="${2:-}"; shift 2 ;;
      --cert-dir) CLI_CERT_DIR="${2:-}"; shift 2 ;;
      --cert-fullchain) CLI_CERT_FULLCHAIN="${2:-}"; shift 2 ;;
      --cert-key) CLI_CERT_KEY="${2:-}"; shift 2 ;;
      --nginx-mode) NGINX_MODE="${2:-auto}"; shift 2 ;;
      --nginx-http-port) CLI_NGINX_HTTP_PORT="${2:-}"; shift 2 ;;
      --nginx-https-port) CLI_NGINX_HTTPS_PORT="${2:-}"; shift 2 ;;
      --api-domain) CLI_DOMAIN_API="${2:-}"; shift 2 ;;
      --frontend-domain) CLI_DOMAIN_FRONTEND="${2:-}"; shift 2 ;;
      --docs-domain) CLI_DOMAIN_DOCS="${2:-}"; shift 2 ;;
      --ws-domain) CLI_DOMAIN_WS="${2:-}"; shift 2 ;;
      --minio-domain) CLI_DOMAIN_MINIO="${2:-}"; shift 2 ;;
      --minio-console-domain) CLI_DOMAIN_MINIO_CONSOLE="${2:-}"; shift 2 ;;
      --email) CLI_EMAIL="${2:-}"; shift 2 ;;
      -h | --help) usage; exit 0 ;;
      *) fail "Неизвестный аргумент: $1 (используйте --help)" ;;
    esac
  done
}

has_cmd() { command -v "$1" >/dev/null 2>&1; }

detect_os() {
  if [[ -f /etc/os-release ]]; then
    # shellcheck disable=SC1091
    . /etc/os-release
    OS_ID="${ID:-unknown}"
    OS_VERSION_ID="${VERSION_ID:-}"
  else
    OS_ID="unknown"
  fi

  case "$OS_ID" in
    ubuntu | debian | linuxmint | pop) PKG_MANAGER="apt" ;;
    fedora | rhel | centos | rocky | almalinux) PKG_MANAGER="dnf" ;;
    opensuse* | sles) PKG_MANAGER="zypper" ;;
    *) PKG_MANAGER="unknown" ;;
  esac

  log "ОС: ${OS_ID:-unknown} ${OS_VERSION_ID:-} (пакеты: $PKG_MANAGER)"
}

run_root() {
  if [[ "$(id -u)" -eq 0 ]]; then
    "$@"
  elif has_cmd sudo; then
    sudo "$@"
  else
    fail "Нужны права root/sudo для: $*"
  fi
}

preflight_checks() {
  if [[ "$(uname -s)" != "Linux" ]]; then
    warn "ОС не Linux — Nginx будет запущен в Docker (профиль proxy). Убедитесь, что Docker Desktop установлен."
    NGINX_MODE="docker"
  fi
  [[ -f docker-compose.yml ]] || fail "Запустите скрипт из корня репозитория (рядом с docker-compose.yml)"
  mkdir -p "$LOCAL_CONF_DIR" "$LOCAL_CERTS_DIR" "$DEFAULT_CERT_UPLOAD_DIR"
}

detect_nginx_mode() {
  load_env
  if [[ -n "$(env_get NGINX_MODE "")" && "$(env_get NGINX_MODE auto)" != "auto" ]]; then
    NGINX_MODE="$(env_get NGINX_MODE auto)"
  fi

  if [[ "$NGINX_MODE" == "auto" ]]; then
    if [[ "$(uname -s)" == "Linux" ]] && { [[ -d /etc/nginx ]] || has_cmd nginx; }; then
      NGINX_MODE="host"
    else
      NGINX_MODE="docker"
    fi
  fi

  log "Режим Nginx: ${NGINX_MODE} (host=системный, docker=контейнер)"
}

port_in_use() {
  local port="$1"
  if has_cmd ss; then
    ss -tln 2>/dev/null | grep -qE ":${port}[[:space:]]"
  elif has_cmd netstat; then
    netstat -tln 2>/dev/null | grep -qE ":${port}[[:space:]]"
  else
    return 1
  fi
}

host_nginx_available() {
  [[ -d /etc/nginx ]] || has_cmd nginx
}

system_nginx_is_active() {
  has_cmd systemctl && systemctl is-active --quiet nginx 2>/dev/null
}

lendry_host_nginx_configured() {
  [[ -f "${NGINX_ENABLED}/${NGINX_PREFIX}-frontend.conf" ]] \
    || [[ -L "${NGINX_ENABLED}/${NGINX_PREFIX}-frontend.conf" ]]
}

stop_docker_nginx() {
  local cid
  cid="$(docker_cmd ps -q -f name=lendry-id-nginx 2>/dev/null || true)"
  if [[ -n "$cid" ]]; then
    log "Останавливаем контейнер lendry-id-nginx (используется системный Nginx)..."
    docker_cmd compose --env-file .env --profile proxy stop nginx 2>/dev/null || true
    docker_cmd rm -f lendry-id-nginx 2>/dev/null || true
    ok "Docker Nginx остановлен"
  fi
}

prepare_compose_stack() {
  resolve_nginx_mode_and_ports
  write_compose_override
  load_env
  if [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then
    stop_docker_nginx
  fi
}

resolve_nginx_mode_and_ports() {
  load_env
  detect_nginx_mode

  if system_nginx_is_active || lendry_host_nginx_configured; then
    if [[ "$NGINX_MODE" == "docker" ]]; then
      warn "На сервере активен системный Nginx — переключаем NGINX_MODE на host"
      warn "Docker Nginx (lendry-id-nginx) не совместим с системным Nginx на 443"
      NGINX_MODE="host"
      env_set NGINX_MODE "host"
    fi
  fi
  if [[ -n "$CLI_NGINX_HTTP_PORT" ]]; then
    NGINX_HTTP_PORT="$CLI_NGINX_HTTP_PORT"
    env_set NGINX_HTTP_PORT "$NGINX_HTTP_PORT"
  fi
  if [[ -n "$CLI_NGINX_HTTPS_PORT" ]]; then
    NGINX_HTTPS_PORT="$CLI_NGINX_HTTPS_PORT"
    env_set NGINX_HTTPS_PORT "$NGINX_HTTPS_PORT"
  fi

  NGINX_HTTP_PORT="${NGINX_HTTP_PORT:-$(env_get NGINX_HTTP_PORT 80)}"
  NGINX_HTTPS_PORT="${NGINX_HTTPS_PORT:-$(env_get NGINX_HTTPS_PORT 443)}"

  if [[ "$NGINX_MODE" == "docker" ]]; then
    if port_in_use "$NGINX_HTTP_PORT" || port_in_use "$NGINX_HTTPS_PORT"; then
      if host_nginx_available && [[ "$NGINX_HTTP_PORT" == "80" ]]; then
        warn "Порт ${NGINX_HTTP_PORT}/${NGINX_HTTPS_PORT} занят — переключаемся на системный Nginx (host)"
        NGINX_MODE="host"
        env_set NGINX_MODE "host"
      elif [[ "$NGINX_HTTP_PORT" == "80" && "$NON_INTERACTIVE" -eq 1 ]]; then
        warn "Порт 80 занят — Docker Nginx будет на 8080/8443"
        NGINX_HTTP_PORT="8080"
        NGINX_HTTPS_PORT="8443"
        env_set NGINX_HTTP_PORT "8080"
        env_set NGINX_HTTPS_PORT "8443"
      elif [[ "$NGINX_HTTP_PORT" == "80" ]]; then
        echo ""
        warn "Порт 80 или 443 уже занят на этом сервере."
        echo "  1) Системный Nginx (рекомендуется, если nginx уже установлен)"
        echo "  2) Docker Nginx на портах 8080 / 8443"
        echo "  3) Отмена — освободите 80/443 и повторите"
        local port_choice
        read -r -p "Выберите [1-3] [1]: " port_choice
        port_choice="${port_choice:-1}"
        case "$port_choice" in
          1)
            NGINX_MODE="host"
            env_set NGINX_MODE "host"
            ;;
          2)
            NGINX_HTTP_PORT="8080"
            NGINX_HTTPS_PORT="8443"
            env_set NGINX_HTTP_PORT "8080"
            env_set NGINX_HTTPS_PORT "8443"
            ;;
          *)
            fail "Освободите порт 80: sudo systemctl stop nginx   или укажите --nginx-mode host"
            ;;
        esac
      else
        fail "Порт ${NGINX_HTTP_PORT} или ${NGINX_HTTPS_PORT} занят. Освободите его или укажите другие: --nginx-http-port 8080"
      fi
    fi
  fi

  env_set NGINX_MODE "$NGINX_MODE"
  env_set NGINX_HTTP_PORT "${NGINX_HTTP_PORT:-80}"
  env_set NGINX_HTTPS_PORT "${NGINX_HTTPS_PORT:-443}"
  log "Nginx: mode=${NGINX_MODE}, HTTP=${NGINX_HTTP_PORT}, HTTPS=${NGINX_HTTPS_PORT}"
}

compose_uses_proxy_profile() {
  load_env
  [[ -n "$(env_get DOMAIN_API "")" ]] || return 1
  [[ "$(env_get INSTALL_MODE local)" == "local" ]] && return 1
  [[ "$(env_get NGINX_MODE docker)" == "docker" ]] || return 1
  return 0
}

# Заполняет глобальный массив COMPOSE_STACK_CMD для docker compose
build_compose_stack_cmd() {
  COMPOSE_STACK_CMD=(compose --env-file .env)
  if compose_uses_proxy_profile; then
    COMPOSE_STACK_CMD+=(--profile proxy)
  fi
}

install_packages_apt() {
  run_root apt-get update -qq
  run_root apt-get install -y -qq ca-certificates curl git openssl gnupg lsb-release jq
}

install_packages_dnf() {
  run_root dnf install -y ca-certificates curl git openssl jq
}

install_base_packages() {
  log "Проверка базовых пакетов..."
  case "$PKG_MANAGER" in
    apt) install_packages_apt ;;
    dnf) install_packages_dnf ;;
    *)
      warn "Автоустановка пакетов для $OS_ID не поддерживается. Нужны: curl, git, openssl, jq"
      ;;
  esac
}

install_docker() {
  if has_cmd docker && docker info >/dev/null 2>&1; then
    ok "Docker: $(docker --version)"
    return
  fi

  if has_cmd docker && ! docker info >/dev/null 2>&1; then
    warn "Docker найден, но daemon недоступен. Пробуем запустить..."
    run_root systemctl start docker 2>/dev/null || true
    if docker info >/dev/null 2>&1; then
      ok "Docker daemon запущен"
      return
    fi
  fi

  if [[ "$OFFLINE" -eq 1 || "$INSTALL_INTRANET" -eq 1 ]]; then
    fail "Docker не установлен или не запущен. В локальной сети установите Docker вручную (offline-пакет / Docker Desktop) и повторите запуск."
  fi

  log "Установка Docker (нужен доступ в интернет)..."
  curl -fsSL https://get.docker.com | run_root sh
  run_root systemctl enable docker >/dev/null 2>&1 || true
  run_root systemctl start docker >/dev/null 2>&1 || true

  if [[ "$(id -u)" -ne 0 ]] && has_cmd sudo; then
    if ! groups "$USER" | grep -q docker; then
      run_root usermod -aG docker "$USER" || true
      warn "Пользователь $USER добавлен в группу docker. Может потребоваться перелогиниться."
      export DOCKER_SUGGESTED_GROUP=1
    fi
  fi
  ok "Docker установлен"
}

check_docker_compose() {
  if docker compose version >/dev/null 2>&1; then
    ok "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version)"
    return
  fi
  fail "Docker Compose v2 не найден. Переустановите Docker."
}

docker_cmd() {
  if docker info >/dev/null 2>&1; then
    docker "$@"
  elif has_cmd sudo && sudo docker info >/dev/null 2>&1; then
    sudo docker "$@"
  else
    fail "Docker daemon недоступен. Запустите: sudo systemctl start docker"
  fi
}

configure_docker_build_env() {
  export DOCKER_BUILDKIT=1
  export COMPOSE_DOCKER_CLI_BUILD=1
  export BUILDKIT_PROGRESS="${BUILDKIT_PROGRESS:-auto}"
  export COMPOSE_PARALLEL_LIMIT="${COMPOSE_PARALLEL_LIMIT:-4}"
}

docker_disk_usage_brief() {
  docker_cmd system df 2>/dev/null | sed 's/^/    /' || true
}

docker_remove_stopped_project_containers() {
  local project cid
  project="$(compose_project_name)"
  while IFS= read -r cid; do
    [[ -z "$cid" ]] && continue
    docker_cmd rm -f "$cid" 2>/dev/null || true
  done < <(docker_cmd ps -aq --filter "label=com.docker.compose.project=${project}" --filter "status=exited" 2>/dev/null || true)
}

docker_prune_stale_project_images() {
  local project="$1"
  local aggressive="${2:-0}"
  local id repo

  while IFS= read -r id; do
    [[ -z "$id" ]] && continue
    if docker_cmd ps -a --filter "ancestor=${id}" -q 2>/dev/null | grep -q .; then
      continue
    fi
    docker_cmd rmi -f "$id" 2>/dev/null || true
  done < <(docker_cmd images --filter "label=com.docker.compose.project=${project}" -q 2>/dev/null | sort -u || true)

  if [[ "$aggressive" == "1" ]]; then
    while IFS= read -r repo id; do
      [[ -z "$id" || "$repo" == "<none>" ]] && continue
      case "$repo" in
        lendry-id-* | idservice-* | "${project}"-*)
          if ! docker_cmd ps -a --filter "ancestor=${id}" -q 2>/dev/null | grep -q .; then
            docker_cmd rmi -f "$id" 2>/dev/null || true
          fi
          ;;
      esac
    done < <(docker_cmd images --format '{{.Repository}} {{.ID}}' 2>/dev/null || true)
  fi
}

docker_prune_before_rebuild() {
  local aggressive="${1:-0}"

  log "Очистка Docker перед пересборкой (volumes данных не удаляются)..."
  build_compose_stack_cmd

  if [[ "$aggressive" == "1" ]]; then
    log "  Режим aggressive: остановка стека и удаление orphan-контейнеров..."
    docker_cmd "${COMPOSE_STACK_CMD[@]}" down --remove-orphans 2>/dev/null || true
  else
    log "  Удаление остановленных контейнеров проекта..."
    docker_remove_stopped_project_containers
    docker_cmd container prune -f --filter "until=24h" 2>/dev/null || true
  fi

  log "  Удаление dangling-образов и старого build cache..."
  docker_cmd image prune -f 2>/dev/null || true
  if docker_cmd builder prune --help 2>/dev/null | grep -q 'until='; then
    docker_cmd builder prune -f --filter "until=168h" 2>/dev/null || \
      docker_cmd builder prune -f 2>/dev/null || true
  else
    docker_cmd builder prune -f 2>/dev/null || true
  fi

  log "  Удаление неиспользуемых образов проекта..."
  docker_prune_stale_project_images "$(compose_project_name)" "$aggressive"

  ok "Docker очищен перед сборкой"
  docker_disk_usage_brief
}

docker_prune_after_rebuild() {
  docker_cmd image prune -f 2>/dev/null || true
}

compose_build_parallel_flags() {
  COMPOSE_BUILD_PARALLEL=()
  if docker_cmd compose build --help 2>/dev/null | grep -q -- '--parallel'; then
    COMPOSE_BUILD_PARALLEL=(--parallel)
  fi
}

compose_build_and_up() {
  local -a services=()
  if [[ $# -gt 0 ]]; then
    services=("$@")
  fi

  configure_docker_build_env
  build_compose_stack_cmd
  compose_build_parallel_flags

  if [[ "${SKIP_DOCKER_PRUNE:-0}" -ne 1 ]]; then
    docker_prune_before_rebuild "${DOCKER_PRUNE_AGGRESSIVE:-0}"
  fi

  if [[ ${#services[@]} -gt 0 ]]; then
    log "Сборка сервисов: ${services[*]} (BuildKit, limit=${COMPOSE_PARALLEL_LIMIT})..."
    docker_cmd "${COMPOSE_STACK_CMD[@]}" build "${COMPOSE_BUILD_PARALLEL[@]}" "${services[@]}"
    log "Запуск сервисов..."
    docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --remove-orphans "${services[@]}"
  else
    log "Сборка образов (BuildKit, параллельно, limit=${COMPOSE_PARALLEL_LIMIT})..."
    docker_cmd "${COMPOSE_STACK_CMD[@]}" build "${COMPOSE_BUILD_PARALLEL[@]}"
    log "Запуск контейнеров..."
    docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --remove-orphans
  fi

  if [[ "${SKIP_DOCKER_PRUNE:-0}" -ne 1 ]]; then
    docker_prune_after_rebuild
  fi
}

action_docker_prune() {
  load_env
  prepare_compose_stack
  SKIP_DOCKER_PRUNE=0
  docker_prune_before_rebuild "${DOCKER_PRUNE_AGGRESSIVE:-1}"
  ok "Очистка Docker завершена"
}

install_nginx_host() {
  if has_cmd nginx; then
    ok "Nginx уже установлен"
    return
  fi

  log "Установка Nginx..."
  case "$PKG_MANAGER" in
    apt) run_root apt-get install -y -qq nginx ;;
    dnf) run_root dnf install -y nginx ;;
    *) fail "Автоустановка Nginx для $OS_ID не поддерживается. Установите nginx вручную или используйте --nginx-mode docker" ;;
  esac

  run_root systemctl enable nginx >/dev/null 2>&1 || true
  ok "Nginx установлен"
}

install_certbot() {
  if has_cmd certbot; then
    ok "Certbot уже установлен"
    return
  fi

  if [[ "$OFFLINE" -eq 1 || "$INSTALL_INTRANET" -eq 1 ]]; then
    fail "Certbot недоступен offline. Для локальной сети используйте --ssl none или --ssl selfsigned"
  fi

  log "Установка Certbot..."
  case "$PKG_MANAGER" in
    apt) run_root apt-get install -y -qq certbot python3-certbot-nginx ;;
    dnf) run_root dnf install -y certbot ;;
    *) fail "Автоустановка Certbot для $OS_ID не поддерживается" ;;
  esac

  run_root mkdir -p "$CERTBOT_WEBROOT"
  ok "Certbot установлен"
}

install_nginx_certbot() {
  if [[ "$NGINX_MODE" == "docker" ]]; then
    ok "Nginx в Docker — установка системного nginx не требуется"
    return
  fi
  install_nginx_host
}

setup_firewall() {
  if ! has_cmd ufw; then
    warn "UFW не найден — пропускаем настройку фаервола"
    return
  fi

  if ! run_root ufw status 2>/dev/null | grep -q "Status: active"; then
    warn "UFW не активен — откройте порты 80 и 443 вручную при необходимости"
    return
  fi

  log "Настройка UFW (80, 443, SSH)..."
  run_root ufw allow 22/tcp >/dev/null 2>&1 || true
  run_root ufw allow 80/tcp >/dev/null 2>&1 || true
  run_root ufw allow 443/tcp >/dev/null 2>&1 || true
  ok "Порты 80 и 443 разрешены в UFW"
}

rand_secret() {
  local length="${1:-48}"
  if has_cmd openssl; then
    openssl rand -base64 64 | tr -dc 'A-Za-z0-9' | head -c "$length"
  else
    tr -dc 'A-Za-z0-9' </dev/urandom | head -c "$length"
  fi
}

prompt() {
  local var_name="$1"
  local question="$2"
  local default="$3"
  local value

  if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
    printf -v "$var_name" '%s' "$default"
    return
  fi

  read -r -p "$question [$default]: " value
  if [[ -z "$value" ]]; then
    value="$default"
  fi
  printf -v "$var_name" '%s' "$value"
}

prompt_optional() {
  local var_name="$1"
  local question="$2"
  local default="$3"
  local value

  if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
    printf -v "$var_name" '%s' "$default"
    return
  fi

  if [[ -n "$default" ]]; then
    read -r -p "$question [$default, Enter=пропустить]: " value
    if [[ -z "$value" ]]; then
      value="$default"
    fi
  else
    read -r -p "$question [Enter=пропустить]: " value
  fi
  printf -v "$var_name" '%s' "$value"
}

validate_domain() {
  local domain="$1"
  [[ -z "$domain" ]] && return 0
  [[ "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]
}

normalize_domain() {
  local d="${1:-}"
  d="${d#https://}"
  d="${d#http://}"
  d="${d#wss://}"
  d="${d#ws://}"
  d="${d%%/*}"
  d="${d%%:*}"
  echo "$d"
}

load_env() {
  if [[ -f .env ]]; then
    # shellcheck disable=SC1091
    set -a && source .env && set +a
  fi
}

env_get() {
  local key="$1"
  local default="${2:-}"
  if [[ -f .env ]] && grep -q "^${key}=" .env; then
    grep "^${key}=" .env | head -n1 | cut -d= -f2-
  else
    echo "$default"
  fi
}

env_set() {
  local key="$1"
  local value="$2"
  local escaped
  escaped="$(printf '%s' "$value" | sed 's/[&/\]/\\&/g')"

  if [[ -f .env ]] && grep -q "^${key}=" .env; then
    sed -i "s|^${key}=.*|${key}=${escaped}|" .env
  elif [[ -f .env ]]; then
    printf '%s=%s\n' "$key" "$value" >>.env
  else
    printf '%s=%s\n' "$key" "$value" >.env
  fi
  chmod 600 .env 2>/dev/null || true
}

derive_public_urls() {
  local ssl_type="${1:-none}"

  if [[ "$INSTALL_LOCAL" -eq 1 ]]; then
    if [[ -z "${DOMAIN_API:-}" && -z "${DOMAIN_FRONTEND:-}" ]]; then
      PUBLIC_API_URL="http://localhost:3000"
      PUBLIC_FRONTEND_URL="http://localhost:3002"
      PUBLIC_DOCS_URL="http://localhost:3003"
      PUBLIC_WS_URL="ws://localhost:8085/ws"
      MINIO_PUBLIC_ENDPOINT="${MINIO_PUBLIC_ENDPOINT:-localhost:9000}"
      MINIO_USE_SSL="false"
      USE_NGINX_SSL="false"
      SSL_TYPE="none"
      INSTALL_MODE="local"
      return
    fi
  fi

  local scheme="http"
  [[ "$ssl_type" == "letsencrypt" || "$ssl_type" == "selfsigned" || "$ssl_type" == "custom" ]] && scheme="https"

  PUBLIC_API_URL="${scheme}://${DOMAIN_API}"
  PUBLIC_FRONTEND_URL="${scheme}://${DOMAIN_FRONTEND}"
  PUBLIC_DOCS_URL="${scheme}://${DOMAIN_DOCS}"

  if [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]]; then
    if [[ "$scheme" == "https" ]]; then
      PUBLIC_WS_URL="wss://${DOMAIN_WS}/ws"
    else
      PUBLIC_WS_URL="ws://${DOMAIN_WS}/ws"
    fi
  else
    if [[ "$scheme" == "https" ]]; then
      PUBLIC_WS_URL="wss://${DOMAIN_API}/ws"
    else
      PUBLIC_WS_URL="ws://${DOMAIN_API}/ws"
    fi
  fi

  if [[ -n "${DOMAIN_MINIO:-}" ]]; then
    MINIO_PUBLIC_ENDPOINT="${DOMAIN_MINIO}"
    MINIO_USE_SSL=$([[ "$scheme" == "https" ]] && echo true || echo false)
  fi

  if [[ "$ssl_type" == "selfsigned" && -n "${DOMAIN_FRONTEND:-}" ]]; then
    apply_same_origin_api_proxy_urls "$scheme"
  fi

  SSL_TYPE="$ssl_type"
  USE_NGINX_SSL=$([[ "$ssl_type" == "none" ]] && echo false || echo true)

  if [[ "$INSTALL_LOCAL" -eq 1 ]]; then
    INSTALL_MODE="local"
  elif [[ "$INSTALL_INTRANET" -eq 1 || "$ssl_type" == "none" || "$ssl_type" == "selfsigned" || "$ssl_type" == "custom" ]]; then
    INSTALL_MODE="intranet"
  elif [[ -n "${DOMAIN_API:-}" ]]; then
    INSTALL_MODE="production"
  else
    INSTALL_MODE="local"
  fi
}

apply_same_origin_api_proxy_urls() {
  local scheme="${1:-https}"
  PUBLIC_API_URL="${PUBLIC_FRONTEND_URL%/}/idp-api"
  if [[ -z "${DOMAIN_WS:-}" || "${DOMAIN_WS}" == "${DOMAIN_API}" ]]; then
    if [[ "$scheme" == "https" ]]; then
      PUBLIC_WS_URL="wss://${DOMAIN_FRONTEND}/idp-api/ws"
    else
      PUBLIC_WS_URL="ws://${DOMAIN_FRONTEND}/idp-api/ws"
    fi
  fi
}

choose_ssl_type_interactive() {
  local default="${1:-none}"

  if [[ -n "$CLI_SSL_TYPE" ]]; then
    SSL_TYPE="$CLI_SSL_TYPE"
    return
  fi

  if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
    if [[ "$INSTALL_INTRANET" -eq 1 ]]; then
      SSL_TYPE="${default:-none}"
    else
      SSL_TYPE="${default:-letsencrypt}"
    fi
    return
  fi

  echo ""
  echo -e "${BOLD}Тип SSL / доступ:${NC}"
  echo "  1) HTTP — локальная сеть, без сертификатов (http://api.idpmvk.lpr)"
  echo "  2) HTTPS самоподписанный — локальная сеть, браузер покажет предупреждение"
  echo "  3) HTTPS Let's Encrypt — нужен интернет и публичный DNS"
  echo "  4) HTTPS свои сертификаты — загрузите fullchain.pem и privkey.pem в папку на сервере"
  echo ""

  local ssl_choice
  if [[ "$INSTALL_INTRANET" -eq 1 ]]; then
    read -r -p "Выберите [1-4] [1]: " ssl_choice
    ssl_choice="${ssl_choice:-1}"
  else
    read -r -p "Выберите [1-4] [3]: " ssl_choice
    ssl_choice="${ssl_choice:-3}"
  fi

  case "$ssl_choice" in
    1) SSL_TYPE="none" ;;
    2) SSL_TYPE="selfsigned" ;;
    3) SSL_TYPE="letsencrypt" ;;
    4) SSL_TYPE="custom" ;;
    *) SSL_TYPE="$default" ;;
  esac
}

configure_domains_interactive() {
  local existing_api existing_frontend existing_docs existing_ws existing_email
  if [[ "$(env_get INSTALL_MODE "")" == "intranet" ]]; then
    INSTALL_INTRANET=1
  fi
  existing_api="$(normalize_domain "$(env_get DOMAIN_API "")")"
  existing_frontend="$(normalize_domain "$(env_get DOMAIN_FRONTEND "")")"
  existing_docs="$(normalize_domain "$(env_get DOMAIN_DOCS "")")"
  existing_ws="$(normalize_domain "$(env_get DOMAIN_WS "")")"
  existing_email="$(env_get CERTBOT_EMAIL "")"

  local default_api="${CLI_DOMAIN_API:-$existing_api}"
  local default_frontend="${CLI_DOMAIN_FRONTEND:-$existing_frontend}"
  local default_docs="${CLI_DOMAIN_DOCS:-$existing_docs}"
  local default_ws="${CLI_DOMAIN_WS:-$existing_ws}"
  local default_minio="${CLI_DOMAIN_MINIO:-$(env_get DOMAIN_MINIO "")}"
  local default_minio_console="${CLI_DOMAIN_MINIO_CONSOLE:-$(env_get DOMAIN_MINIO_CONSOLE "")}"
  local default_email="${CLI_EMAIL:-$existing_email}"

  if [[ "$INSTALL_LOCAL" -eq 1 ]]; then
    DOMAIN_API=""
    DOMAIN_FRONTEND=""
    DOMAIN_DOCS=""
    DOMAIN_WS=""
    DOMAIN_MINIO=""
    DOMAIN_MINIO_CONSOLE=""
    derive_public_urls "none"
    return
  fi

  echo ""
  echo -e "${BOLD}Настройка доменов${NC}"
  if [[ "$INSTALL_INTRANET" -eq 1 ]]; then
    echo "Локальная сеть: укажите внутренние DNS-имена (например idpmvk.lpr)."
    echo "Пропишите A-записи во внутреннем DNS или файл hosts на клиентах."
    echo "Доступ из интернета и Let's Encrypt не требуются."
  else
    echo "Укажите домен для каждого сервиса (A-запись → IP сервера)."
  fi
  echo "Enter — оставить текущее значение. localhost без Nginx: ./install.sh --local"
  echo ""

  if [[ "$INSTALL_INTRANET" -eq 1 ]]; then
    if [[ -z "$default_api" ]]; then default_api="api.idpmvk.lpr"; fi
    if [[ -z "$default_frontend" ]]; then default_frontend="id.idpmvk.lpr"; fi
    if [[ -z "$default_docs" ]]; then default_docs="docs.idpmvk.lpr"; fi
  else
    if [[ -z "$default_api" ]]; then default_api="api.example.com"; fi
    if [[ -z "$default_frontend" ]]; then default_frontend="id.example.com"; fi
    if [[ -z "$default_docs" ]]; then default_docs="docs.example.com"; fi
  fi

  prompt DOMAIN_API "Домен API / Swagger" "$default_api"
  prompt DOMAIN_FRONTEND "Домен Frontend (UI)" "$default_frontend"
  prompt DOMAIN_DOCS "Домен документации" "$default_docs"
  prompt_optional DOMAIN_WS "Домен WebSocket (пусто = ${DOMAIN_API}/ws)" "$default_ws"
  prompt_optional DOMAIN_MINIO "Домен MinIO S3 (опционально)" "$default_minio"
  prompt_optional DOMAIN_MINIO_CONSOLE "Домен MinIO Console (опционально)" "$default_minio_console"

  DOMAIN_API="$(normalize_domain "$DOMAIN_API")"
  DOMAIN_FRONTEND="$(normalize_domain "$DOMAIN_FRONTEND")"
  DOMAIN_DOCS="$(normalize_domain "$DOMAIN_DOCS")"
  DOMAIN_WS="$(normalize_domain "$DOMAIN_WS")"
  DOMAIN_MINIO="$(normalize_domain "$DOMAIN_MINIO")"
  DOMAIN_MINIO_CONSOLE="$(normalize_domain "$DOMAIN_MINIO_CONSOLE")"

  for d in "$DOMAIN_API" "$DOMAIN_FRONTEND" "$DOMAIN_DOCS" "$DOMAIN_WS" "$DOMAIN_MINIO" "$DOMAIN_MINIO_CONSOLE"; do
    [[ -z "$d" ]] && continue
    validate_domain "$d" || fail "Некорректный домен: $d"
  done

  [[ -n "$DOMAIN_API" && -n "$DOMAIN_FRONTEND" && -n "$DOMAIN_DOCS" ]] \
    || fail "Домены API, Frontend и Docs обязательны (или используйте --local)"

  choose_ssl_type_interactive "$(env_get SSL_TYPE none)"

  case "$SSL_TYPE" in
    none | selfsigned | custom | letsencrypt) ;;
    *) fail "Некорректный SSL_TYPE: $SSL_TYPE (none | selfsigned | custom | letsencrypt)" ;;
  esac

  if [[ "$SSL_TYPE" == "custom" ]]; then
    prompt_custom_cert_settings
  fi

  if [[ "$SSL_TYPE" == "letsencrypt" ]]; then
    if [[ "$INSTALL_INTRANET" -eq 1 && "$NON_INTERACTIVE" -ne 1 ]]; then
      warn "Let's Encrypt требует доступ из интернета. Для чистой локальной сети выберите HTTP или selfsigned."
    fi
    prompt CERTBOT_EMAIL "Email для Let's Encrypt" "${default_email:-admin@${DOMAIN_FRONTEND}}"
    [[ -n "$CERTBOT_EMAIL" ]] || fail "Email обязателен для Let's Encrypt"
  else
    CERTBOT_EMAIL=""
  fi

  derive_public_urls "$SSL_TYPE"
}

write_env_secrets_if_missing() {
  [[ -f .env && "$FORCE_ENV" != "1" ]] && return

  local pg_pass rmq_pass minio_pass jwt_access jwt_refresh data_key
  pg_pass="$(rand_secret 32)"
  rmq_pass="$(rand_secret 32)"
  minio_pass="$(rand_secret 24)"
  jwt_access="$(rand_secret 64)"
  jwt_refresh="$(rand_secret 64)"
  data_key="$(rand_secret 48)"

  if [[ ! -f .env ]]; then
    cp .env.example .env 2>/dev/null || touch .env
  fi

  load_infra_secrets_into_env

  if docker_volume_exists postgres_data; then
    if [[ -n "$(env_get POSTGRES_PASSWORD "")" ]]; then
      warn "Volume PostgreSQL уже существует — POSTGRES_PASSWORD в .env не меняем"
    else
      fail "Volume PostgreSQL существует, но POSTGRES_PASSWORD не найден. Восстановите .env или выполните: ./install.sh --reset-db"
    fi
    pg_pass="$(env_get POSTGRES_PASSWORD "$pg_pass")"
  fi

  if docker_volume_exists rabbitmq_data; then
    warn "Volume RabbitMQ уже существует — RABBITMQ_DEFAULT_PASS не меняем"
    rmq_pass="$(env_get RABBITMQ_DEFAULT_PASS "$rmq_pass")"
  fi

  if docker_volume_exists minio_data; then
    warn "Volume MinIO уже существует — MINIO_ROOT_PASSWORD не меняем"
    minio_pass="$(env_get MINIO_ROOT_PASSWORD "$minio_pass")"
  fi

  local need_secrets=0
  if [[ "$FORCE_ENV" == "1" ]]; then
    need_secrets=1
  elif [[ "$(env_get POSTGRES_PASSWORD change-me-postgres)" == "change-me-postgres" ]]; then
    need_secrets=1
  elif [[ "$(env_get JWT_ACCESS_SECRET change-me-access-secret)" == "change-me-access-secret" ]]; then
    need_secrets=1
  fi

  if [[ "$need_secrets" -eq 1 ]]; then
    env_set POSTGRES_PASSWORD "$pg_pass"
    env_set RABBITMQ_DEFAULT_PASS "$rmq_pass"
    env_set MINIO_ROOT_PASSWORD "$minio_pass"
    env_set JWT_ACCESS_SECRET "$jwt_access"
    env_set JWT_REFRESH_SECRET "$jwt_refresh"
    env_set DATA_ENCRYPTION_KEY "$data_key"
    save_infra_secrets
    ok "Секреты сгенерированы / синхронизированы"
  fi
}

compose_project_name() {
  load_env
  echo "${COMPOSE_PROJECT_NAME:-lendry-id}"
}

docker_volume_name() {
  echo "$(compose_project_name)_$1"
}

docker_volume_exists() {
  docker volume inspect "$(docker_volume_name "$1")" >/dev/null 2>&1
}

save_infra_secrets() {
  load_env
  cat >"$INFRA_SECRETS_FILE" <<EOF
POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}
RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS:-}
MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-}
EOF
  chmod 600 "$INFRA_SECRETS_FILE" 2>/dev/null || true
}

load_infra_secrets_into_env() {
  [[ -f "$INFRA_SECRETS_FILE" ]] || return 0
  # shellcheck disable=SC1091
  set -a && source "$INFRA_SECRETS_FILE" && set +a
  [[ -n "${POSTGRES_PASSWORD:-}" ]] && env_set POSTGRES_PASSWORD "$POSTGRES_PASSWORD"
  [[ -n "${RABBITMQ_DEFAULT_PASS:-}" ]] && env_set RABBITMQ_DEFAULT_PASS "$RABBITMQ_DEFAULT_PASS"
  [[ -n "${MINIO_ROOT_PASSWORD:-}" ]] && env_set MINIO_ROOT_PASSWORD "$MINIO_ROOT_PASSWORD"
}

verify_postgres_auth() {
  load_env
  load_infra_secrets_into_env
  [[ -f .env ]] || return 0
  docker_cmd compose ps postgres 2>/dev/null | grep -q postgres || return 0

  local user pass db
  user="$(env_get POSTGRES_USER lendry)"
  pass="$(env_get POSTGRES_PASSWORD "")"
  db="$(env_get POSTGRES_DB lendry_id)"
  [[ -n "$pass" ]] || return 0

  if docker_cmd compose exec -T -e PGPASSWORD="$pass" postgres \
    psql -U "$user" -d "$db" -c 'SELECT 1' >/dev/null 2>&1; then
    ok "PostgreSQL: пароль из .env подходит"
    save_infra_secrets
    return 0
  fi

  echo ""
  fail "PostgreSQL: пароль в .env не совпадает с данными в volume (ошибка P1000).

  Это происходит после FORCE_ENV=1 или смены POSTGRES_PASSWORD при существующей БД.

  Решения:
    1) Сбросить БД (удалит всех пользователей и данные):
         ./install.sh --reset-db --yes && ./install.sh --restart

    2) Вернуть старый POSTGRES_PASSWORD в .env (если сохранили)

    3) Если есть файл ${INFRA_SECRETS_FILE} — скопируйте пароль оттуда в .env"
}

action_reset_db() {
  load_env
  local vol
  vol="$(docker_volume_name postgres_data)"

  if [[ "$NON_INTERACTIVE" -ne 1 ]]; then
    echo ""
    warn "Будет удалён volume ${vol} — все пользователи, сессии и настройки в PostgreSQL пропадут."
    read -r -p "Продолжить? [y/N]: " confirm
    [[ "$confirm" =~ ^[Yy]$ ]] || fail "Отменено"
  fi

  log "Остановка сервисов..."
  docker_cmd compose --env-file .env --profile proxy down 2>/dev/null || docker_cmd compose --env-file .env down 2>/dev/null || true

  log "Удаление ${vol}..."
  if docker volume inspect "$vol" >/dev/null 2>&1; then
    docker volume rm "$vol" || fail "Не удалось удалить ${vol}"
  else
    warn "Volume ${vol} не найден — возможно, уже удалён"
  fi

  ok "PostgreSQL volume удалён. PostgreSQL создастся заново с паролем из .env."
  save_infra_secrets
}

persist_domain_env() {
  write_env_secrets_if_missing

  env_set INSTALL_MODE "${INSTALL_MODE:-local}"
  env_set NGINX_MODE "${NGINX_MODE:-auto}"
  env_set SSL_TYPE "${SSL_TYPE:-none}"
  env_set DOMAIN_API "${DOMAIN_API:-}"
  env_set DOMAIN_FRONTEND "${DOMAIN_FRONTEND:-}"
  env_set DOMAIN_DOCS "${DOMAIN_DOCS:-}"
  env_set DOMAIN_WS "${DOMAIN_WS:-}"
  env_set DOMAIN_MINIO "${DOMAIN_MINIO:-}"
  env_set DOMAIN_MINIO_CONSOLE "${DOMAIN_MINIO_CONSOLE:-}"
  env_set PUBLIC_API_URL "${PUBLIC_API_URL}"
  env_set PUBLIC_FRONTEND_URL "${PUBLIC_FRONTEND_URL}"
  env_set PUBLIC_DOCS_URL "${PUBLIC_DOCS_URL}"
  env_set PUBLIC_WS_URL "${PUBLIC_WS_URL}"
  env_set USE_NGINX_SSL "${USE_NGINX_SSL:-false}"
  env_set CERTBOT_EMAIL "${CERTBOT_EMAIL:-}"
  env_set SSL_CERT_DIR "${SSL_CERT_DIR:-$(env_get SSL_CERT_DIR "")}"
  env_set SSL_CERT_FULLCHAIN "${SSL_CERT_FULLCHAIN:-$(env_get SSL_CERT_FULLCHAIN "")}"
  env_set SSL_CERT_KEY "${SSL_CERT_KEY:-$(env_get SSL_CERT_KEY "")}"
  env_set MINIO_PUBLIC_ENDPOINT "${MINIO_PUBLIC_ENDPOINT:-localhost:9000}"
  env_set MINIO_USE_SSL "${MINIO_USE_SSL:-false}"

  if [[ "$(env_get INSTALL_MODE local)" == "intranet" && -z "$(env_get LDAP_USE_HOST_NETWORK "")" ]]; then
    env_set LDAP_USE_HOST_NETWORK "true"
  fi
  env_set LDAP_USE_HOST_NETWORK "$(env_get LDAP_USE_HOST_NETWORK false)"
  env_set LDAP_DNS_SERVERS "$(env_get LDAP_DNS_SERVERS "")"
  env_set LDAP_DNS_SEARCH "$(env_get LDAP_DNS_SEARCH "")"
  env_set LDAP_EXTRA_HOSTS "$(env_get LDAP_EXTRA_HOSTS "")"

  ok "Файл .env обновлён"
  derive_ldap_dns_settings
}

derive_ldap_dns_settings() {
  load_env
  local extra dns first ip
  extra="$(env_get LDAP_EXTRA_HOSTS "")"
  [[ -n "$extra" ]] || return 0

  dns="$(env_get LDAP_DNS_SERVERS "")"
  if [[ -z "$dns" ]]; then
    first="${extra%%,*}"
    ip="${first##*:}"
    ip="$(echo "$ip" | xargs)"
    if [[ -n "$ip" ]]; then
      env_set LDAP_DNS_SERVERS "$ip"
      log "LDAP_DNS_SERVERS=${ip} (из LDAP_EXTRA_HOSTS)"
    fi
  fi
}

sync_ldap_system_hosts() {
  load_env
  local extra
  extra="$(env_get LDAP_EXTRA_HOSTS "")"
  [[ -n "$extra" ]] || return 0

  log "LDAP: запись имён DC в /etc/hosts..."
  local entry host ip
  IFS=',' read -ra _entries <<< "$extra"
  for entry in "${_entries[@]}"; do
    entry="$(echo "$entry" | xargs)"
    [[ "$entry" == *:* ]] || continue
    host="${entry%%:*}"
    host="$(echo "$host" | xargs)"
    ip="${entry##*:}"
    ip="$(echo "$ip" | xargs)"
    [[ -n "$host" && -n "$ip" ]] || continue
    if run_root grep -qE "[[:space:]]${host}([[:space:]]|$)" /etc/hosts 2>/dev/null; then
      continue
    fi
    run_root sh -c "printf '%s %s\n' '${ip}' '${host}' >> /etc/hosts" || warn "Не удалось добавить ${host} в /etc/hosts (нужен sudo?)"
  done
}

write_compose_override() {
  derive_ldap_dns_settings
  sync_ldap_system_hosts
  resolve_nginx_mode_and_ports
  load_env
  local mode="${NGINX_MODE:-$(env_get NGINX_MODE host)}"
  local has_domains install_mode
  has_domains="$(env_get DOMAIN_API "")"
  install_mode="$(env_get INSTALL_MODE local)"

  local nginx_fragment="" ldap_fragment="" body=""

  if [[ -n "$has_domains" && "$install_mode" != "local" && "$mode" == "docker" ]]; then
    local http_port https_port
    http_port="$(env_get NGINX_HTTP_PORT 80)"
    https_port="$(env_get NGINX_HTTPS_PORT 443)"
    log "Настройка docker-compose override (Nginx: ${mode})..."
    nginx_fragment="$(cat <<EOF
  nginx:
    ports:
      - "${http_port}:80"
      - "${https_port}:443"
EOF
)"
  fi

  ldap_fragment="$(build_ldap_compose_fragment)"
  [[ -n "$ldap_fragment" ]] && log "Настройка docker-compose override (LDAP DNS / host network)..."

  body="${nginx_fragment}${ldap_fragment}"
  if [[ -z "$body" ]]; then
    [[ -f "$COMPOSE_OVERRIDE" ]] && rm -f "$COMPOSE_OVERRIDE"
    return
  fi

  cat >"$COMPOSE_OVERRIDE" <<EOF
services:
${body}
EOF
}

build_ldap_compose_fragment() {
  load_env
  local use_host dns_servers dns_search extra_hosts
  use_host="$(env_get LDAP_USE_HOST_NETWORK false)"
  dns_servers="$(env_get LDAP_DNS_SERVERS "")"
  dns_search="$(env_get LDAP_DNS_SEARCH "")"
  extra_hosts="$(env_get LDAP_EXTRA_HOSTS "")"

  [[ "$use_host" == "true" || -n "$dns_servers" || -n "$extra_hosts" ]] || return 0

  if [[ "$use_host" == "true" ]]; then
    cat <<'EOF'

  ldap-auth:
    network_mode: host
  sso-core:
    environment:
      LDAP_AUTH_URL: http://host.docker.internal:8086
    extra_hosts:
      - "host.docker.internal:host-gateway"
EOF
    return 0
  fi

  local fragment=$'\n  ldap-auth:'
  if [[ -n "$dns_servers" ]]; then
    fragment+=$'\n    dns:'
    local server
    IFS=',' read -ra _dns <<< "$dns_servers"
    for server in "${_dns[@]}"; do
      server="$(echo "$server" | xargs)"
      [[ -n "$server" ]] && fragment+=$'\n      - "'"${server}"'"'
    done
  fi
  if [[ -n "$dns_search" ]]; then
    fragment+=$'\n    dns_search:'
    local zone
    IFS=',' read -ra _zones <<< "$dns_search"
    for zone in "${_zones[@]}"; do
      zone="$(echo "$zone" | xargs)"
      [[ -n "$zone" ]] && fragment+=$'\n      - "'"${zone}"'"'
    done
  fi
  if [[ -n "$extra_hosts" ]]; then
    fragment+=$'\n    extra_hosts:'
    local host_entry
    IFS=',' read -ra _hosts <<< "$extra_hosts"
    for host_entry in "${_hosts[@]}"; do
      host_entry="$(echo "$host_entry" | xargs)"
      [[ -n "$host_entry" ]] && fragment+=$'\n      - "'"${host_entry}"'"'
    done
  fi
  printf '%s' "$fragment"
}

HOST_EDGE_SERVICES=(api-gateway frontend docs media-ws)

recreate_host_port_services() {
  load_env
  [[ "$(env_get NGINX_MODE docker)" == "host" ]] || return 0
  [[ -n "$(env_get DOMAIN_API "")" ]] || return 0
  [[ "$(env_get INSTALL_MODE local)" == "local" ]] && return 0

  log "Пересоздание сервисов с пробросом портов на 127.0.0.1..."
  build_compose_stack_cmd
  docker_cmd "${COMPOSE_STACK_CMD[@]}" up -d --force-recreate "${HOST_EDGE_SERVICES[@]}"
}

docker_publishes_port() {
  local container="$1"
  local host_port="$2"
  docker_cmd port "$container" 2>/dev/null | grep -q "127.0.0.1:${host_port}" || \
    docker_cmd port "$container" 2>/dev/null | grep -q ":${host_port}->"
}

ensure_host_upstreams() {
  load_env
  [[ "$(env_get NGINX_MODE docker)" == "host" ]] || return 0
  [[ -n "$(env_get DOMAIN_API "")" ]] || return 0
  [[ "$(env_get INSTALL_MODE local)" == "local" ]] && return 0

  local missing=0
  for spec in "3000:API" "3002:Frontend" "3003:Docs"; do
    local port="${spec%%:*}"
    if ! port_in_use "$port"; then
      warn "Порт ${port} (${spec#*:}) не слушается на хосте"
      missing=1
    fi
  done

  if [[ "$missing" -eq 1 ]]; then
    warn "Проброс портов не применён — пересоздаём edge-сервисы (NGINX_MODE=host)"
    recreate_host_port_services
    local attempt=0
    while [[ $attempt -lt 12 ]]; do
      missing=0
      for spec in "3000:API" "3002:Frontend" "3003:Docs"; do
        local port="${spec%%:*}"
        if ! port_in_use "$port"; then
          missing=1
        fi
      done
      [[ "$missing" -eq 0 ]] && break
      attempt=$((attempt + 1))
      sleep 5
    done
    for spec in "3000:API" "3002:Frontend" "3003:Docs"; do
      local port="${spec%%:*}"
      if ! port_in_use "$port"; then
        echo ""
        warn "Диагностика:"
        docker_cmd ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null | sed 's/^/    /' || true
        docker_cmd port lendry-id-frontend 2>/dev/null | sed 's/^/    frontend: /' || echo "    frontend: порты не опубликованы"
        echo ""
        fail "Порт ${port} недоступен. Проверьте: grep NGINX_MODE .env && cat docker-compose.override.yml && docker compose logs frontend"
      fi
    done
    ok "Порты 3000/3002/3003 доступны на localhost"
  fi
}

nginx_upstream() {
  case "$1" in
    api)
      [[ "$NGINX_MODE" == "docker" ]] && echo "http://api-gateway:3000" || echo "http://127.0.0.1:3000"
      ;;
    frontend)
      [[ "$NGINX_MODE" == "docker" ]] && echo "http://frontend:3000" || echo "http://127.0.0.1:3002"
      ;;
    docs)
      [[ "$NGINX_MODE" == "docker" ]] && echo "http://docs:3000" || echo "http://127.0.0.1:3003"
      ;;
    ws)
      [[ "$NGINX_MODE" == "docker" ]] && echo "http://media-ws:8085" || echo "http://127.0.0.1:8085"
      ;;
    minio)
      [[ "$NGINX_MODE" == "docker" ]] && echo "http://minio:9000" || echo "http://127.0.0.1:9000"
      ;;
    minio-console)
      [[ "$NGINX_MODE" == "docker" ]] && echo "http://minio:9001" || echo "http://127.0.0.1:9001"
      ;;
  esac
}

nginx_cert_base_path() {
  local domain="$1"
  local ssl_type="$2"
  if [[ "$ssl_type" == "letsencrypt" ]]; then
    echo "/etc/letsencrypt/live/${domain}"
  elif [[ "$ssl_type" == "selfsigned" ]]; then
    if [[ "$NGINX_MODE" == "docker" ]]; then
      echo "/etc/nginx/certs/shared-intranet"
    else
      echo "${LOCAL_CERTS_DIR}/shared-intranet"
    fi
  elif [[ "$ssl_type" == "custom" ]]; then
    if [[ "$NGINX_MODE" == "docker" ]]; then
      echo "/etc/nginx/certs/custom"
    else
      echo "${LOCAL_CERTS_DIR}/custom"
    fi
  elif [[ "$NGINX_MODE" == "docker" ]]; then
    echo "/etc/nginx/certs/${domain}"
  else
    echo "${LOCAL_CERTS_DIR}/${domain}"
  fi
}

intranet_wildcard_base_domain() {
  local domain="${1:-}"
  [[ -z "$domain" ]] && return 0
  if [[ "$domain" == *.*.* ]]; then
    echo "${domain#*.}"
  else
    echo "$domain"
  fi
}

collect_self_signed_sans() {
  local -a sans=()
  local base domain

  load_env
  for domain in "$DOMAIN_API" "$DOMAIN_FRONTEND" "$DOMAIN_DOCS" "${DOMAIN_WS:-}" "${DOMAIN_MINIO:-}" "${DOMAIN_MINIO_CONSOLE:-}"; do
    [[ -z "$domain" ]] && continue
    sans+=("DNS:${domain}")
  done

  base="$(intranet_wildcard_base_domain "${DOMAIN_FRONTEND:-$DOMAIN_API}")"
  if [[ -n "$base" ]]; then
    sans+=("DNS:*.${base}")
    sans+=("DNS:${base}")
  fi

  (IFS=,; echo "${sans[*]}")
}

find_custom_cert_file() {
  local dir="$1"
  local preferred="${2:-}"
  shift 2 || true
  local -a fallbacks=("$@")
  local name

  if [[ -n "$preferred" ]]; then
    [[ -f "${dir}/${preferred}" ]] || fail "Файл не найден: ${dir}/${preferred}"
    echo "${dir}/${preferred}"
    return 0
  fi

  for name in "${fallbacks[@]}"; do
    if [[ -f "${dir}/${name}" ]]; then
      echo "${dir}/${name}"
      return 0
    fi
  done

  return 1
}

find_custom_cert_file_auto() {
  local dir="$1"
  local kind="$2"
  local f base

  shopt -s nullglob
  if [[ "$kind" == "key" ]]; then
    for f in "$dir"/*-key.pem "$dir"/*_key.pem "$dir"/*.key; do
      if [[ -f "$f" ]]; then
        shopt -u nullglob
        echo "$f"
        return 0
      fi
    done
    shopt -u nullglob
    return 1
  fi

  for f in "$dir"/_wildcard*.pem "$dir"/*wildcard*.pem "$dir"/*.pem "$dir"/*.crt; do
    [[ -f "$f" ]] || continue
    base="$(basename "$f")"
    [[ "$base" == *-key.pem || "$base" == *_key.pem ]] && continue
    shopt -u nullglob
    echo "$f"
    return 0
  done
  shopt -u nullglob
  return 1
}

resolve_custom_cert_source_dir() {
  if [[ -n "$CLI_CERT_DIR" ]]; then
    SSL_CERT_DIR="$CLI_CERT_DIR"
  elif [[ -n "${SSL_CERT_DIR:-}" ]]; then
    :
  elif [[ -n "$(env_get SSL_CERT_DIR "")" ]]; then
    SSL_CERT_DIR="$(env_get SSL_CERT_DIR "")"
  elif [[ -d "$DEFAULT_CERT_UPLOAD_DIR" ]]; then
    SSL_CERT_DIR="$DEFAULT_CERT_UPLOAD_DIR"
  fi
}

prompt_custom_cert_settings() {
  resolve_custom_cert_source_dir
  local default_dir="${SSL_CERT_DIR:-$DEFAULT_CERT_UPLOAD_DIR}"
  local default_fullchain="${CLI_CERT_FULLCHAIN:-$(env_get SSL_CERT_FULLCHAIN "")}"
  local default_key="${CLI_CERT_KEY:-$(env_get SSL_CERT_KEY "")}"

  if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
    SSL_CERT_DIR="${SSL_CERT_DIR:-$default_dir}"
    SSL_CERT_FULLCHAIN="${SSL_CERT_FULLCHAIN:-$default_fullchain}"
    SSL_CERT_KEY="${SSL_CERT_KEY:-$default_key}"
    [[ -d "$SSL_CERT_DIR" ]] || fail "Папка с сертификатами не найдена: $SSL_CERT_DIR (укажите --cert-dir PATH)"
    return 0
  fi

  echo ""
  echo -e "${BOLD}Свои SSL-сертификаты:${NC}"
  echo "  Положите на сервер файлы сертификата и ключа, затем укажите папку."
  echo "  Поддерживаются имена: fullchain.pem, cert.pem, _wildcard.domain.pem"
  echo "  и ключи: privkey.pem, key.pem, _wildcard.domain-key.pem"
  echo "  Рекомендуемая папка: ${DEFAULT_CERT_UPLOAD_DIR}/"
  echo ""

  prompt SSL_CERT_DIR "Папка с сертификатами на сервере" "$default_dir"
  [[ -d "$SSL_CERT_DIR" ]] || fail "Папка не найдена: $SSL_CERT_DIR"

  prompt_optional SSL_CERT_FULLCHAIN "Имя файла сертификата (полная цепочка, пусто = авто)" "$default_fullchain"
  prompt_optional SSL_CERT_KEY "Имя файла приватного ключа (пусто = авто)" "$default_key"
}

validate_custom_certificate_pair() {
  local fullchain="$1"
  local key="$2"

  [[ -f "$fullchain" ]] || fail "Файл сертификата не найден: $fullchain"
  [[ -f "$key" ]] || fail "Файл ключа не найден: $key"
  [[ -r "$fullchain" ]] || fail "Нет прав на чтение сертификата: $fullchain"
  [[ -r "$key" ]] || fail "Нет прав на чтение ключа: $key"

  has_cmd openssl || return 0

  openssl x509 -in "$fullchain" -noout >/dev/null 2>&1 \
    || fail "Некорректный файл сертификата: $fullchain"

  if openssl rsa -in "$key" -check -noout >/dev/null 2>&1 \
    || openssl ec -in "$key" -check -noout >/dev/null 2>&1 \
    || openssl pkey -in "$key" -noout >/dev/null 2>&1; then
    :
  else
    fail "Некорректный файл приватного ключа: $key"
  fi

  if ! openssl x509 -noout -modulus -in "$fullchain" 2>/dev/null \
    | cmp -s <(openssl rsa -noout -modulus -in "$key" 2>/dev/null) - 2>/dev/null \
    && ! openssl x509 -noout -modulus -in "$fullchain" 2>/dev/null \
    | cmp -s <(openssl ec -noout -modulus -in "$key" 2>/dev/null) - 2>/dev/null \
    && ! openssl x509 -noout -pubkey -in "$fullchain" 2>/dev/null \
    | cmp -s <(openssl pkey -pubout -in "$key" 2>/dev/null) - 2>/dev/null; then
    warn "Не удалось проверить соответствие сертификата и ключа — продолжаем установку"
  fi
}

install_custom_certificates() {
  local force="${1:-}"
  load_env

  resolve_custom_cert_source_dir
  SSL_CERT_DIR="${SSL_CERT_DIR:-$(env_get SSL_CERT_DIR "")}"
  SSL_CERT_FULLCHAIN="${SSL_CERT_FULLCHAIN:-$(env_get SSL_CERT_FULLCHAIN "")}"
  SSL_CERT_KEY="${SSL_CERT_KEY:-$(env_get SSL_CERT_KEY "")}"

  [[ -n "$SSL_CERT_DIR" ]] || fail "Не задана папка с сертификатами (SSL_CERT_DIR или --cert-dir)"
  [[ -d "$SSL_CERT_DIR" ]] || fail "Папка с сертификатами не найдена: $SSL_CERT_DIR"

  local src_fullchain src_key
  src_fullchain="$(find_custom_cert_file "$SSL_CERT_DIR" "$SSL_CERT_FULLCHAIN" \
    fullchain.pem cert.pem certificate.pem server.crt)" \
    || src_fullchain="$(find_custom_cert_file_auto "$SSL_CERT_DIR" cert)" \
    || fail "Не найден файл сертификата в ${SSL_CERT_DIR}. Положите fullchain.pem, _wildcard.domain.pem и т.п., либо укажите --cert-fullchain"
  src_key="$(find_custom_cert_file "$SSL_CERT_DIR" "$SSL_CERT_KEY" \
    privkey.pem key.pem server.key)" \
    || src_key="$(find_custom_cert_file_auto "$SSL_CERT_DIR" key)" \
    || fail "Не найден файл ключа в ${SSL_CERT_DIR}. Положите privkey.pem, domain-key.pem и т.п., либо укажите --cert-key"

  validate_custom_certificate_pair "$src_fullchain" "$src_key"

  if [[ "$force" != "--force" \
    && -f "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" \
    && -f "${CUSTOM_CERT_TARGET_DIR}/privkey.pem" ]] \
    && cmp -s "$src_fullchain" "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" \
    && cmp -s "$src_key" "${CUSTOM_CERT_TARGET_DIR}/privkey.pem"; then
    log "Сертификаты уже установлены: ${CUSTOM_CERT_TARGET_DIR}/"
    return 0
  fi

  log "Установка своих сертификатов из ${SSL_CERT_DIR}..."
  mkdir -p "$CUSTOM_CERT_TARGET_DIR"
  cp "$src_fullchain" "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem"
  cp "$src_key" "${CUSTOM_CERT_TARGET_DIR}/privkey.pem"
  chmod 644 "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" 2>/dev/null || true
  chmod 600 "${CUSTOM_CERT_TARGET_DIR}/privkey.pem" 2>/dev/null || true

  env_set SSL_CERT_DIR "$SSL_CERT_DIR"
  env_set SSL_CERT_FULLCHAIN "$SSL_CERT_FULLCHAIN"
  env_set SSL_CERT_KEY "$SSL_CERT_KEY"

  ok "Сертификаты установлены: ${CUSTOM_CERT_TARGET_DIR}/"
  if has_cmd openssl; then
    log "Срок действия: $(openssl x509 -in "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" -noout -enddate 2>/dev/null | cut -d= -f2-)"
    log "Домены (SAN): $(openssl x509 -in "${CUSTOM_CERT_TARGET_DIR}/fullchain.pem" -noout -ext subjectAltName 2>/dev/null | sed 's/ *//g' || echo "не указаны")"
  fi
}

generate_shared_self_signed_cert() {
  local cert_dir="${LOCAL_CERTS_DIR}/shared-intranet"
  local base sans cn

  mkdir -p "$cert_dir"
  base="$(intranet_wildcard_base_domain "${DOMAIN_FRONTEND:-$DOMAIN_API}")"
  [[ -z "$base" ]] && base="${DOMAIN_FRONTEND:-$DOMAIN_API}"
  cn="*.${base}"
  sans="$(collect_self_signed_sans)"

  if [[ -f "${cert_dir}/fullchain.pem" && -f "${cert_dir}/privkey.pem" ]]; then
    return 0
  fi

  log "Общий самоподписанный сертификат: ${cn} (${sans})"
  if has_cmd openssl; then
    openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
      -keyout "${cert_dir}/privkey.pem" \
      -out "${cert_dir}/fullchain.pem" \
      -subj "/CN=${cn}/O=Lendry ID Intranet/C=RU" \
      -addext "subjectAltName=${sans}" 2>/dev/null || \
    openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
      -keyout "${cert_dir}/privkey.pem" \
      -out "${cert_dir}/fullchain.pem" \
      -subj "/CN=${cn}/O=Lendry ID Intranet/C=RU"
    chmod 600 "${cert_dir}/privkey.pem" 2>/dev/null || true
  else
    fail "openssl не найден — нужен для самоподписанных сертификатов"
  fi
}

generate_all_self_signed_certs() {
  local force="${1:-}"
  load_env
  if [[ "$force" == "--force" ]]; then
    rm -f "${LOCAL_CERTS_DIR}/shared-intranet/fullchain.pem" "${LOCAL_CERTS_DIR}/shared-intranet/privkey.pem"
  fi
  generate_shared_self_signed_cert
  ok "Самоподписанный сертификат создан: ${LOCAL_CERTS_DIR}/shared-intranet"
}

nginx_conf_target() {
  local label="$1"
  if [[ "$NGINX_MODE" == "docker" ]]; then
    echo "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-${label}.conf"
  else
    echo "${NGINX_AVAILABLE}/${NGINX_PREFIX}-${label}.conf"
  fi
}

nginx_publish_config() {
  local label="$1"
  local conf
  conf="$(nginx_conf_target "$label")"
  if [[ "$NGINX_MODE" == "host" ]]; then
    run_root ln -sf "$conf" "${NGINX_ENABLED}/${NGINX_PREFIX}-${label}.conf"
  fi
}

nginx_proxy_headers="        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_set_header X-Forwarded-Host \$host;
        proxy_read_timeout 300s;
        proxy_connect_timeout 75s;"

nginx_write_conf() {
  local conf="$1"
  if [[ "$NGINX_MODE" == "docker" ]]; then
    cat >"$conf"
  else
    run_root tee "$conf" >/dev/null
  fi
}

nginx_ssl_directives() {
  local domain="$1"
  local ssl_type="$2"
  local cert_base
  cert_base="$(nginx_cert_base_path "$domain" "$ssl_type")"
  if [[ "$ssl_type" == "letsencrypt" ]]; then
    cat <<EOF
    ssl_certificate ${cert_base}/fullchain.pem;
    ssl_certificate_key ${cert_base}/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
EOF
  else
    cat <<EOF
    ssl_certificate ${cert_base}/fullchain.pem;
    ssl_certificate_key ${cert_base}/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
EOF
  fi
}

nginx_http_server_preamble() {
  local domain="$1"
  local ssl_type="$2"
  if [[ "$ssl_type" == "letsencrypt" ]]; then
    cat <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${domain};

    location /.well-known/acme-challenge/ {
        root ${CERTBOT_WEBROOT};
    }

    location / {
        return 301 https://\$host\$request_uri;
    }
}

EOF
  elif [[ "$ssl_type" == "selfsigned" || "$ssl_type" == "custom" ]]; then
    cat <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${domain};

    location / {
        return 301 https://\$host\$request_uri;
    }
}

EOF
  fi
}

build_ws_block() {
  local ws_on_api="$1"
  [[ "$ws_on_api" != "true" ]] && return 0
  local upstream
  upstream="$(nginx_upstream ws)"
  cat <<EOF
    location /ws {
        proxy_pass ${upstream}/ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_read_timeout 86400s;
    }

EOF
}

write_nginx_site_api() {
  local domain="$1"
  local ssl_type="${2:-none}"
  local ws_on_api="${3:-true}"
  local conf upstream ws_block http_preamble ssl_block
  conf="$(nginx_conf_target api)"
  upstream="$(nginx_upstream api)"
  ws_block="$(build_ws_block "$ws_on_api")"
  http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"

  if [[ "$ssl_type" == "none" ]]; then
    nginx_write_conf "$conf" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${domain};
    client_max_body_size 100m;
${ws_block}
    location / {
        proxy_pass ${upstream};
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }
}
EOF
  else
    ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
    nginx_write_conf "$conf" <<EOF
${http_preamble}server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${domain};
${ssl_block}
    client_max_body_size 100m;
${ws_block}
    location / {
        proxy_pass ${upstream};
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }
}
EOF
  fi
  nginx_publish_config "api"
}

build_nginx_idp_api_proxy_block() {
  local api_upstream ws_upstream
  api_upstream="$(nginx_upstream api)"
  ws_upstream="$(nginx_upstream ws)"
  cat <<EOF
    location /idp-api/ws {
        proxy_pass ${ws_upstream}/ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_read_timeout 86400s;
    }

    location /idp-api/ {
        proxy_pass ${api_upstream}/;
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }

    location /oauth/ {
        proxy_pass ${api_upstream}/oauth/;
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }

    location /.well-known/ {
        proxy_pass ${api_upstream}/.well-known/;
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }

EOF
}

write_nginx_site_frontend() {
  local domain="$1"
  local ssl_type="${2:-none}"
  local conf upstream http_preamble ssl_block idp_api_block
  conf="$(nginx_conf_target frontend)"
  upstream="$(nginx_upstream frontend)"
  http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
  idp_api_block="$(build_nginx_idp_api_proxy_block)"

  if [[ "$ssl_type" == "none" ]]; then
    nginx_write_conf "$conf" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${domain};
${idp_api_block}
    location / {
        proxy_pass ${upstream};
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }
}
EOF
  else
    ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
    nginx_write_conf "$conf" <<EOF
${http_preamble}server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${domain};
${ssl_block}
    client_max_body_size 50m;
${idp_api_block}
    location / {
        proxy_pass ${upstream};
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }
}
EOF
  fi
  nginx_publish_config "frontend"
}

write_nginx_site_docs() {
  local domain="$1"
  local ssl_type="${2:-none}"
  local conf upstream http_preamble ssl_block
  conf="$(nginx_conf_target docs)"
  upstream="$(nginx_upstream docs)"
  http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"

  if [[ "$ssl_type" == "none" ]]; then
    nginx_write_conf "$conf" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${domain};
    location = / { return 302 /docs; }
    location / {
        proxy_pass ${upstream};
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }
}
EOF
  else
    ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
    nginx_write_conf "$conf" <<EOF
${http_preamble}server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${domain};
${ssl_block}
    location = / { return 302 /docs; }
    location / {
        proxy_pass ${upstream};
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }
}
EOF
  fi
  nginx_publish_config "docs"
}

write_nginx_site_ws() {
  local domain="$1"
  local ssl_type="${2:-none}"
  local conf upstream http_preamble ssl_block
  conf="$(nginx_conf_target ws)"
  upstream="$(nginx_upstream ws)"
  http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"

  if [[ "$ssl_type" == "none" ]]; then
    nginx_write_conf "$conf" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${domain};
    location / {
        proxy_pass ${upstream}/ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_read_timeout 86400s;
    }
}
EOF
  else
    ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
    nginx_write_conf "$conf" <<EOF
${http_preamble}server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${domain};
${ssl_block}
    location / {
        proxy_pass ${upstream}/ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
        proxy_read_timeout 86400s;
    }
}
EOF
  fi
  nginx_publish_config "ws"
}

write_nginx_site_minio() {
  local domain="$1"
  local kind="$2"
  local label="$3"
  local ssl_type="${4:-none}"
  local conf upstream http_preamble ssl_block
  conf="$(nginx_conf_target "$label")"
  upstream="$(nginx_upstream "$kind")"
  http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"

  if [[ "$ssl_type" == "none" ]]; then
    nginx_write_conf "$conf" <<EOF
server {
    listen 80;
    listen [::]:80;
    server_name ${domain};
    client_max_body_size 1024m;
    proxy_buffering off;
    location / {
        proxy_pass ${upstream};
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }
}
EOF
  else
    ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
    nginx_write_conf "$conf" <<EOF
${http_preamble}server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name ${domain};
${ssl_block}
    client_max_body_size 1024m;
    proxy_buffering off;
    location / {
        proxy_pass ${upstream};
        proxy_http_version 1.1;
${nginx_proxy_headers}
    }
}
EOF
  fi
  nginx_publish_config "$label"
}

remove_nginx_configs() {
  log "Удаление конфигов Nginx IdP..."
  rm -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true
  if [[ "$NGINX_MODE" == "host" ]]; then
    run_root rm -f \
      "${NGINX_ENABLED}/${NGINX_PREFIX}-"*.conf \
      "${NGINX_AVAILABLE}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true
    run_root nginx -t >/dev/null 2>&1 && run_root systemctl reload nginx || true
  fi
  ok "Конфиги Nginx IdP удалены"
}

write_all_nginx_configs() {
  load_env
  local ssl_type="${1:-$(env_get SSL_TYPE none)}"
  local ws_on_api="true"
  [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]] && ws_on_api="false"

  remove_nginx_configs
  write_nginx_site_api "$DOMAIN_API" "$ssl_type" "$ws_on_api"
  write_nginx_site_frontend "$DOMAIN_FRONTEND" "$ssl_type"
  write_nginx_site_docs "$DOMAIN_DOCS" "$ssl_type"

  if [[ "$ws_on_api" == "false" ]]; then
    write_nginx_site_ws "$DOMAIN_WS" "$ssl_type"
  else
    rm -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-ws.conf" 2>/dev/null || true
    run_root rm -f "${NGINX_ENABLED}/${NGINX_PREFIX}-ws.conf" "${NGINX_AVAILABLE}/${NGINX_PREFIX}-ws.conf" 2>/dev/null || true
  fi

  [[ -n "${DOMAIN_MINIO:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO" minio minio "$ssl_type"
  [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO_CONSOLE" minio-console minio-console "$ssl_type"

  if [[ "$NGINX_MODE" == "host" ]]; then
    if run_root nginx -t; then
      run_root systemctl reload nginx || warn "Не удалось перезагрузить Nginx (systemctl reload nginx)"
    else
      warn "Конфигурация Nginx содержит ошибки — проверьте сертификаты в ${LOCAL_CERTS_DIR}"
    fi
  else
    ok "Конфиги Nginx: ${LOCAL_CONF_DIR}"
  fi
}

write_nginx_http_only() {
  write_all_nginx_configs "none"
}

write_nginx_ssl_configs() {
  load_env
  write_all_nginx_configs "$(env_get SSL_TYPE selfsigned)"
}

obtain_certificate() {
  local domain="$1"
  local email="$2"

  [[ -z "$domain" ]] && return 0

  log "Получение SSL для ${domain}..."
  if ! run_root certbot certonly --webroot -w "$CERTBOT_WEBROOT" \
    --email "$email" --agree-tos --no-eff-email \
    -d "$domain" --non-interactive; then
    fail "Не удалось получить SSL для ${domain}. Проверьте DNS A-запись → IP сервера и порты 80/443."
  fi
  ok "Сертификат для ${domain} готов"
}

obtain_all_certificates() {
  load_env
  [[ "$(env_get SSL_TYPE none)" == "letsencrypt" ]] || return 0

  local email="${CERTBOT_EMAIL:-}"
  [[ -n "$email" ]] || fail "CERTBOT_EMAIL не задан"

  write_nginx_http_only

  obtain_certificate "$DOMAIN_API" "$email"
  obtain_certificate "$DOMAIN_FRONTEND" "$email"
  obtain_certificate "$DOMAIN_DOCS" "$email"

  if [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]]; then
    obtain_certificate "$DOMAIN_WS" "$email"
  fi
  [[ -n "${DOMAIN_MINIO:-}" ]] && obtain_certificate "$DOMAIN_MINIO" "$email"
  [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && obtain_certificate "$DOMAIN_MINIO_CONSOLE" "$email"

  if [[ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]]; then
    run_root mkdir -p /etc/letsencrypt
    if [[ "$OFFLINE" -ne 1 ]]; then
      run_root curl -fsSL https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf \
        -o /etc/letsencrypt/options-ssl-nginx.conf 2>/dev/null || true
    fi
    run_root openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null || true
  fi

  write_all_nginx_configs "letsencrypt"
  setup_certbot_renewal
}

setup_certbot_renewal() {
  if has_cmd systemctl && systemctl list-unit-files 2>/dev/null | grep -q certbot.timer; then
    run_root systemctl enable certbot.timer >/dev/null 2>&1 || true
    run_root systemctl start certbot.timer >/dev/null 2>&1 || true
    ok "Автообновление SSL (certbot.timer) включено"
  else
    warn "certbot.timer не найден — настройте cron: certbot renew --quiet && systemctl reload nginx"
  fi
}

setup_nginx_proxy() {
  load_env
  [[ -n "${DOMAIN_API:-}" ]] || return 0
  [[ "$INSTALL_LOCAL" -eq 1 ]] && return 0

  resolve_nginx_mode_and_ports
  write_compose_override
  if [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then
    stop_docker_nginx
  fi
  install_nginx_certbot

  local ssl_type
  ssl_type="$(env_get SSL_TYPE none)"

  case "$ssl_type" in
    none)
      write_nginx_http_only
      ok "Nginx: HTTP — локальная сеть, интернет не нужен"
      ;;
    selfsigned)
      generate_all_self_signed_certs
      write_all_nginx_configs "selfsigned"
      ok "Nginx: HTTPS самоподписанный — импортируйте CA в доверенные на клиентах или примите предупреждение браузера"
      ;;
    custom)
      install_custom_certificates
      write_all_nginx_configs "custom"
      ok "Nginx: HTTPS со своими сертификатами из ${CUSTOM_CERT_TARGET_DIR}/"
      ;;
    letsencrypt)
      setup_firewall
      install_certbot
      run_root mkdir -p "$CERTBOT_WEBROOT"
      obtain_all_certificates
      ;;
    *)
      fail "Неизвестный SSL_TYPE: $ssl_type"
      ;;
  esac
}

setup_nginx_stack() {
  setup_nginx_proxy
}

wait_for_service() {
  local name="$1"
  local max_attempts="${2:-60}"
  local attempt=0

  log "Ожидание сервиса ${name}..."
  while [[ $attempt -lt $max_attempts ]]; do
    if docker_cmd compose ps --status running "$name" 2>/dev/null | grep -q "$name"; then
      local cid health
      cid="$(docker_cmd compose ps -q "$name" 2>/dev/null | head -n1)"
      health="$(docker_cmd inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid" 2>/dev/null || echo "none")"
      if [[ "$health" == "healthy" || "$health" == "none" ]]; then
        ok "Сервис ${name} готов"
        return 0
      fi
    fi
    attempt=$((attempt + 1))
    sleep 5
  done
  warn "Сервис ${name} не стал healthy — проверьте: docker compose logs $name"
}

deploy_stack() {
  prepare_compose_stack
  verify_postgres_auth

  log "Сборка и запуск контейнеров (10–20 минут при первом запуске)..."
  compose_build_and_up
  recreate_host_port_services
  ensure_host_upstreams

  wait_for_service "sso-core" 120
  wait_for_service "api-gateway" 90
  wait_for_service "frontend" 60
  wait_for_service "docs" 60
  if compose_uses_proxy_profile; then
    wait_for_service "nginx" 30
  fi
}

save_install_state() {
  local ts
  ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  cat >"$STATE_FILE" <<EOF
{"version":"${SCRIPT_VERSION}","updated_at":"${ts}","domains":{"api":"${DOMAIN_API:-}","frontend":"${DOMAIN_FRONTEND:-}","docs":"${DOMAIN_DOCS:-}","ws":"${DOMAIN_WS:-}"}}
EOF
}

print_intranet_hint() {
  load_env
  [[ "$(env_get INSTALL_MODE local)" == "intranet" ]] || return 0

  local server_ip
  server_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
  [[ -z "$server_ip" ]] && server_ip="<IP-сервера>"

  echo -e "  ${BOLD}Локальная сеть (DNS / hosts):${NC}"
  echo "    Пропишите во внутреннем DNS или C:\\Windows\\System32\\drivers\\etc\\hosts:"
  echo "    ${server_ip}  ${DOMAIN_API} ${DOMAIN_FRONTEND} ${DOMAIN_DOCS}"
  echo ""
  if [[ "$(env_get SSL_TYPE none)" == "selfsigned" ]]; then
    echo "    Сертификат: ${LOCAL_CERTS_DIR}/shared-intranet (общий для всех доменов)"
    echo "    Браузер покажет предупреждение — примите сертификат для ${DOMAIN_FRONTEND}."
    echo "    API вызывается через ${DOMAIN_FRONTEND}/idp-api — отдельно открывать ${DOMAIN_API} не нужно."
    echo ""
  fi
  if [[ "$(env_get LDAP_USE_HOST_NETWORK false)" == "true" ]]; then
    echo "    LDAP: контейнер ldap-auth использует сеть хоста (резолвит AD-имена как сервер)."
  elif [[ -n "$(env_get LDAP_DNS_SERVERS "")" || -n "$(env_get LDAP_EXTRA_HOSTS "")" ]]; then
    echo "    LDAP: для ldap-auth настроены DNS/extra_hosts из .env"
  else
    echo "    LDAP: если AD-имена не резолвятся — LDAP_USE_HOST_NETWORK=true или IP DC в LDAP_HOST."
  fi
  echo ""
}

print_summary() {
  load_env

  echo ""
  echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}"
  echo -e "${GREEN} Lendry ID — развёртывание завершено${NC}"
  echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}"
  echo ""
  echo -e "  ${BOLD}Режим:${NC}          ${INSTALL_MODE:-local} | SSL: ${SSL_TYPE:-none} | Nginx: ${NGINX_MODE:-auto}"
  echo -e "  ${BOLD}API / Swagger:${NC}  ${PUBLIC_API_URL}/docs"
  echo -e "  ${BOLD}Frontend:${NC}       ${PUBLIC_FRONTEND_URL}"
  echo -e "  ${BOLD}Документация:${NC}   ${PUBLIC_DOCS_URL}/docs"
  echo -e "  ${BOLD}WebSocket:${NC}      ${PUBLIC_WS_URL}"
  echo ""
  print_intranet_hint
  echo -e "  ${BOLD}Управление:${NC}     ./install.sh"
  echo -e "  ${BOLD}Смена доменов:${NC}  ./install.sh --configure-domains"
  echo -e "  ${BOLD}Логи:${NC}           docker compose logs -f api-gateway nginx"
  echo -e "  ${BOLD}Очистка Docker:${NC} ./install.sh --docker-prune"
  echo ""
  echo "  Первый зарегистрированный пользователь станет супер-администратором."
  echo "  Секреты в .env — не коммитьте в git."
  echo ""

  if [[ "${DOCKER_SUGGESTED_GROUP:-}" == "1" ]]; then
    warn "Docker без sudo: выполните newgrp docker или перелогиньтесь"
  fi
}

refresh_host_nginx_configs() {
  load_env
  [[ "$(env_get NGINX_MODE docker)" == "host" ]] || return 0
  [[ -n "$(env_get DOMAIN_API "")" ]] || return 0
  [[ "$(env_get INSTALL_MODE local)" == "local" ]] && return 0

  local ssl_type
  ssl_type="$(env_get SSL_TYPE none)"
  case "$ssl_type" in
    none) write_nginx_http_only || warn "Не удалось записать HTTP-конфиги Nginx (нужен sudo?)" ;;
    selfsigned)
      generate_all_self_signed_certs
      write_all_nginx_configs "selfsigned" || warn "Не удалось обновить Nginx (нужен sudo?)"
      ;;
    custom)
      install_custom_certificates
      write_all_nginx_configs "custom" || warn "Не удалось обновить Nginx (нужен sudo?)"
      ;;
    letsencrypt)
      write_all_nginx_configs "letsencrypt" || warn "Не удалось обновить Nginx (нужен sudo?)"
      ;;
  esac
}

check_local_upstream() {
  local label="$1" url="$2"
  if curl -fsS --max-time 3 "$url" >/dev/null 2>&1; then
    echo -e "  ${GREEN}✔${NC} ${label}: ${url}"
  else
    echo -e "  ${RED}✘${NC} ${label}: ${url} — недоступен"
  fi
}

show_upstream_health() {
  load_env
  [[ "$(env_get NGINX_MODE docker)" != "host" ]] && return 0
  [[ "$(env_get INSTALL_MODE local)" == "local" ]] && return 0
  [[ -n "$(env_get DOMAIN_API "")" ]] || return 0

  echo "  Upstream (host Nginx → localhost):"
  check_local_upstream "API" "http://127.0.0.1:3000/health"
  check_local_upstream "Swagger" "http://127.0.0.1:3000/docs"
  check_local_upstream "Frontend" "http://127.0.0.1:3002"
  check_local_upstream "Docs" "http://127.0.0.1:3003/docs"
  echo ""
}

show_status() {
  load_env
  echo ""
  echo -e "${BOLD}Статус Lendry ID${NC}"
  echo ""
  echo "  INSTALL_MODE:     $(env_get INSTALL_MODE local)"
  echo "  SSL_TYPE:         $(env_get SSL_TYPE none)"
  if [[ "$(env_get SSL_TYPE none)" == "custom" ]]; then
    echo "  SSL_CERT_DIR:     $(env_get SSL_CERT_DIR -)"
    echo "  SSL установлен:   ${CUSTOM_CERT_TARGET_DIR}/"
  fi
  echo "  NGINX_MODE:       $(env_get NGINX_MODE auto)"
  echo "  USE_NGINX_SSL:    $(env_get USE_NGINX_SSL false)"
  echo "  DOMAIN_API:       $(env_get DOMAIN_API -)"
  echo "  DOMAIN_FRONTEND:  $(env_get DOMAIN_FRONTEND -)"
  echo "  DOMAIN_DOCS:      $(env_get DOMAIN_DOCS -)"
  echo "  DOMAIN_WS:        $(env_get DOMAIN_WS -)"
  echo ""
  docker_cmd compose ps 2>/dev/null || warn "Контейнеры не запущены"
  echo ""
  echo -e "  ${BOLD}Docker disk:${NC}"
  docker_disk_usage_brief
  echo ""
  if docker_cmd ps -q -f name=lendry-id-nginx 2>/dev/null | grep -q .; then
    if [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then
      warn "КОНФЛИКТ: контейнер lendry-id-nginx запущен при NGINX_MODE=host — выполните: ./install.sh --fix-proxy"
    else
      echo "  Docker Nginx:     запущен (режим docker; curl 127.0.0.1:3002 не нужен)"
    fi
  fi
  if [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then
    docker_cmd port lendry-id-frontend 3000/tcp 2>/dev/null | sed 's/^/  frontend port: /' || \
      warn "frontend не публикует 3000/tcp на хост — выполните: ./install.sh --fix-proxy"
  fi
  echo ""
  show_upstream_health
  if has_cmd nginx && [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then
    run_root nginx -t 2>&1 | sed 's/^/  /'
  fi
}

action_full_install() {
  configure_domains_interactive
  persist_domain_env
  setup_nginx_proxy
  deploy_stack
  save_install_state
  print_summary
}

action_configure_domains() {
  if [[ ! -f .env ]]; then
    warn ".env не найден — выполняем полную установку"
    action_full_install
    return
  fi

  load_env
  configure_domains_interactive
  persist_domain_env

  if [[ "$INSTALL_LOCAL" -eq 1 ]]; then
    remove_nginx_configs
  fi

  setup_nginx_proxy
  write_compose_override

  log "Пересборка frontend/docs с новыми URL..."
  deploy_stack
  save_install_state
  print_summary
}

apply_intranet_selfsigned_runtime() {
  load_env
  [[ "$(env_get SSL_TYPE none)" == "selfsigned" ]] || return 0
  [[ -n "$(env_get DOMAIN_FRONTEND "")" ]] || return 0

  derive_public_urls "selfsigned"
  env_set PUBLIC_API_URL "${PUBLIC_API_URL}"
  env_set PUBLIC_WS_URL "${PUBLIC_WS_URL}"
  env_set INTERNAL_API_URL "http://api-gateway:3000"
  env_set INTERNAL_WS_URL "http://media-ws:8085"

  log "Пересборка frontend (same-origin /idp-api)..."
  compose_build_and_up frontend
}

action_renew_ssl() {
  load_env
  local ssl_type
  ssl_type="$(env_get SSL_TYPE none)"

  if [[ "$ssl_type" == "none" ]]; then
    if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
      fail "SSL отключён (SSL_TYPE=none). Сначала настройте домены и SSL:
  ./install.sh --configure-domains --intranet \\
    --api-domain api.idpmvk.lpr --frontend-domain sso.idpmvk.lpr --docs-domain docs.idpmvk.lpr \\
    --ssl custom --cert-dir /opt/idp-certs"
    fi

    echo ""
    warn "SSL сейчас отключён (SSL_TYPE=none)."
    echo ""
    echo "  a) Настроить домены и SSL (рекомендуется) — откроется пункт 2"
    echo "  b) Установить / обновить свои сертификаты (custom) — домены должны быть уже в .env"
    echo "  c) Отмена"
    echo ""
    local renew_choice
    read -r -p "Выберите [a/b/c]: " renew_choice
    case "$renew_choice" in
      a | A)
        action_configure_domains
        return
        ;;
      b | B)
        ssl_type="custom"
        SSL_TYPE="custom"
        prompt_custom_cert_settings
        [[ -n "$(env_get DOMAIN_API "")" && -n "$(env_get DOMAIN_FRONTEND "")" && -n "$(env_get DOMAIN_DOCS "")" ]] \
          || fail "Домены не заданы. Сначала выполните пункт 2 (Настроить домены) или укажите DOMAIN_API, DOMAIN_FRONTEND, DOMAIN_DOCS в .env"
        derive_public_urls "custom"
        env_set SSL_TYPE "custom"
        env_set USE_NGINX_SSL "true"
        env_set SSL_CERT_DIR "${SSL_CERT_DIR}"
        env_set SSL_CERT_FULLCHAIN "${SSL_CERT_FULLCHAIN:-}"
        env_set SSL_CERT_KEY "${SSL_CERT_KEY:-}"
        env_set PUBLIC_API_URL "${PUBLIC_API_URL}"
        env_set PUBLIC_FRONTEND_URL "${PUBLIC_FRONTEND_URL}"
        env_set PUBLIC_DOCS_URL "${PUBLIC_DOCS_URL}"
        env_set PUBLIC_WS_URL "${PUBLIC_WS_URL}"
        ;;
      *)
        log "Отменено"
        return 0
        ;;
    esac
  fi

  case "$ssl_type" in
    letsencrypt)
      install_certbot
      run_root certbot renew --quiet
      write_all_nginx_configs "letsencrypt"
      ok "Let's Encrypt сертификаты обновлены"
      ;;
    selfsigned)
      generate_all_self_signed_certs --force
      write_all_nginx_configs "selfsigned"
      apply_intranet_selfsigned_runtime
      ok "Самоподписанные сертификаты пересозданы, frontend обновлён"
      ;;
    custom)
      install_custom_certificates --force
      write_all_nginx_configs "custom"
      if [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then
        run_root nginx -t >/dev/null 2>&1 && run_root systemctl reload nginx || warn "Перезагрузите Nginx вручную: sudo systemctl reload nginx"
      fi
      ok "Свои сертификаты установлены из $(env_get SSL_CERT_DIR -)"
      ;;
    none)
      fail "SSL отключён (SSL_TYPE=none). Выберите пункт 2 или: ./install.sh --configure-domains --ssl custom --cert-dir /path"
      ;;
  esac
}

action_fix_host_proxy() {
  [[ -f .env ]] || fail "Файл .env не найден — сначала выполните ./install.sh --install"
  load_env
  env_set NGINX_MODE "host"
  NGINX_MODE="host"
  NON_INTERACTIVE=1

  log "Исправление прокси (host Nginx + порты 127.0.0.1)..."

  log "Шаг 1/4: остановка Docker Nginx..."
  stop_docker_nginx

  log "Шаг 2/4: compose override (LDAP / Nginx)..."
  write_compose_override

  log "Шаг 3/4: пересоздание api-gateway, frontend, docs, media-ws..."
  compose_build_and_up "${HOST_EDGE_SERVICES[@]}"
  ensure_host_upstreams

  log "Шаг 4/4: конфиги системного Nginx..."
  refresh_host_nginx_configs

  apply_intranet_selfsigned_runtime

  log "Шаг 5/5: пересоздание ldap-auth и sso-core..."
  compose_build_and_up ldap-auth sso-core

  ok "Прокси исправлен — проверьте: curl -I http://127.0.0.1:3002"
  show_status
}

action_restart() {
  load_env
  prepare_compose_stack
  refresh_host_nginx_configs
  log "Пересборка и перезапуск контейнеров..."
  compose_build_and_up
  recreate_host_port_services
  ensure_host_upstreams
  ok "Контейнеры перезапущены"
  show_status
}

show_menu() {
  while true; do
    echo ""
    echo -e "${BOLD}══════════════════════════════════════════════════════════════${NC}"
    echo -e "${BOLD} Lendry ID v${SCRIPT_VERSION} — установка и управление${NC}"
    echo -e "${BOLD}══════════════════════════════════════════════════════════════${NC}"
    [[ -f .env ]] && load_env && echo -e "  Режим: ${INSTALL_MODE:-local} | API: ${DOMAIN_API:-localhost:3000}"
    echo ""
    echo "  1) Полная установка / переустановка стека"
    echo "  2) Настроить / изменить домены и Nginx + SSL"
    echo "  3) SSL-сертификаты (обновить / включить custom / Let's Encrypt)"
    echo "  4) Перезапуск / пересборка контейнеров (+ очистка Docker)"
    echo "  5) Статус сервисов"
    echo "  6) Удалить конфиги Nginx IdP"
    echo "  7) Сброс PostgreSQL (если ошибка P1000 / пароль БД)"
    echo "  8) Очистить Docker (образы/кеш, без пересборки)"
    echo "  0) Выход"
    echo ""

    local choice
    read -r -p "Выберите пункт [0-8]: " choice

    case "$choice" in
      1) NON_INTERACTIVE=0; action_full_install ;;
      2) NON_INTERACTIVE=0; action_configure_domains ;;
      3) action_renew_ssl ;;
      4) action_restart ;;
      5) show_status ;;
      6) remove_nginx_configs; ok "Готово" ;;
      7) action_reset_db; action_restart ;;
      8) action_docker_prune ;;
      0) exit 0 ;;
      *) warn "Неверный пункт" ;;
    esac
  done
}

main() {
  parse_args "$@"
  preflight_checks
  detect_os
  install_base_packages
  install_docker
  check_docker_compose

  case "${ACTION:-}" in
    install) action_full_install ;;
    configure-domains) action_configure_domains ;;
    renew-ssl) action_renew_ssl ;;
    restart) action_restart ;;
    docker-prune) action_docker_prune ;;
    fix-proxy) action_fix_host_proxy ;;
    status) show_status ;;
    reset-db) action_reset_db ;;
    remove-nginx) remove_nginx_configs ;;
    "")
      if [[ -f .env ]] && [[ "$NON_INTERACTIVE" -ne 1 ]]; then
        show_menu
      else
        action_full_install
      fi
      ;;
    *) fail "Неизвестное действие: $ACTION" ;;
  esac
}

main "$@"
