#!/bin/bash
# ==============================================================================
# Ichill Cloud — Instalador FULL 100% automático de nova VPS (installichill)
# ==============================================================================
# Executado em VPS Rocky Linux / AlmaLinux 9 LIMPA. Instala e configura TUDO:
#   CWP7 (painel + e-mail), CSF (firewall real, incluído no CWP7), Docker CE,
#   redes/volumes Docker, código Ichill, .env, stack compose (core+odoo+caddy+
#   portal+engine), container manual (ichill_ws_proxy), monitoramento
#   (Prometheus/Grafana), timezone e restauração de backup.
#   OBS: apenas o Odoo MASTER (ichill_odoo) sobe junto; tenants/clientes são
#        provisionados sob demanda pelo core.
#
# MIRROR OFFLINE (arquivosvps):
#   Toda dependência externa (CWP7, CSF, docker-ce rpms, imagens Docker) tenta
#   primeiro o repo OFICIAL do fabricante; se falhar (repo offline/sem internet),
#   baixa do mirror da nossa VPS2 em https://repo.ichill.com.br e instala 100%
#   offline (docker load das imagens, sem build, sem Docker Hub).
#
# USO:
#   sudo ./installichill.sh                        # instalação completa (VPS PRINCIPAL)
#   sudo ./installichill.sh --node                 # instala um WORKER NODE limpo (novo nó)
#   sudo ./installichill.sh --dry-run              # simula passo a passo (não executa)
#   sudo ./installichill.sh --backup /caminho/backup.tar.gz   # restaura dados ao final
#   sudo ./installichill.sh --source /caminho/ichill       # usa outro diretório-fonte
#   sudo ./installichill.sh --skip-cwp             # não instala CWP7
#   sudo ./installichill.sh --step N               # inicia a partir do passo N
#   sudo ./installichill.sh --offline              # força uso do mirror (ignora repos oficiais)
#   sudo ./installichill.sh --mirror URL           # usa outro mirror (default: repo.ichill.com.br)
#
# MODO --node (worker node / nova VPS limpa):
#   Instala SÓ o necessário para ser um nó novo do cluster: Docker, CSF,
#   mirror offline (arquivosvps), redes/volumes, Caddy PRÓPRIO do node, timezone
#   e o ichill-agent (que se auto-registra no control plane da VPS principal).
#   NÃO clona nada da VPS2: sem stack principal (core/odoo/caddy central/homepage),
#   sem monitoring, sem deupar, sem tenants — pronto para receber novos clientes.
#
# MODOS:
#   --dry-run   imprime todos os comandos que seriam executados, passo a passo,
#               SEM efetuar nenhuma mudança no sistema. Ideal para simulação.
# ==============================================================================

set -euo pipefail

# ─── Parâmetros ────────────────────────────────────────────────────────────
DRY_RUN=0
BACKUP_FILE=""
SOURCE_DIR=""
SKIP_CWP=0
START_STEP=1
OFFLINE=0
NODE_MODE=0
MIRROR_URL="https://repo.ichill.com.br"

for arg in "$@"; do
    case "$arg" in
        --dry-run) DRY_RUN=1 ;;
        --skip-cwp) SKIP_CWP=1 ;;
        --node) NODE_MODE=1 ;;
        --backup=*) BACKUP_FILE="${arg#*=}" ;;
        --source=*) SOURCE_DIR="${arg#*=}" ;;
        --step=*) START_STEP="${arg#*=}" ;;
        --offline) OFFLINE=1 ;;
        --mirror=*) MIRROR_URL="${arg#*=}" ;;
        --help|-h)
            grep '^#' "$0" | sed -n 's/^# \{0,2\}//p' | head -40
            exit 0
            ;;
    esac
done

# ─── Constantes ────────────────────────────────────────────────────────────
PROJECT_NAME="ichill"
PROJECT_ROOT="/home/makson/public_html/${PROJECT_NAME}"
SCRIPTS_DIR="${PROJECT_ROOT}/scripts"
NETWORK="ichill_net"
CADDY_NET="ichill_caddy_admin_net"
VPS_IP="${VPS_IP:-167.86.119.151}"
TZ="America/Sao_Paulo"
MIRROR_DIR="${PROJECT_ROOT}/arquivosvps"
MIRROR_IMAGES="${MIRROR_DIR}/images"
MIRROR_SOFT="${MIRROR_DIR}/soft"
CONTROL_PLANE_URL="${CONTROL_PLANE_URL:-https://api.ichill.com.br/api/v1/admin/infra}"
AGENT_REGION="${AGENT_REGION:-auto}"
# VPN OpenVPN static-key (worker node <-> control plane) — "como a VPS1 faz hoje"
VPN_ENABLE="${VPN_ENABLE:-1}"           # 1 = instala/configura VPN (modo --node)
VPN_REMOTE="${VPN_REMOTE:-167.86.119.151}"   # IP publico do control plane (VPS2)
VPN_PORT="${VPN_PORT:-1194}"
VPN_LOCAL_IP="${VPN_LOCAL_IP:-10.0.0.3}"     # IP VPN deste node (unico por node)
VPN_CONTROL_IP="${VPN_CONTROL_IP:-10.0.0.2}" # IP VPN do control plane
VPN_KEY_URL="${VPN_KEY_URL:-${MIRROR_URL}/soft/static.key}"

# Volumes criados pelo compose do ichill (prefixo ichill_)
COMPOSE_VOLUMES="caddydata pgdata redisdata rabbitdata qdrantdata miniodata odoodata odoodb_principal_pg odoodb_cliente_pg"
# Volumes extras manuais/monitoramento (sem tenants — provisionados sob demanda)
EXTRA_VOLUMES="ichill_odoo_data"
MONITORING_VOLUMES="monitoring_prometheusdata monitoring_grafanadata"
# Rede dedicada do Caddy Admin API (só core acessa)
CADDY_NET_SUBNET="172.20.0.0/24"

# ─── Helpers (dry-run vs real) ─────────────────────────────────────────────
log()  { echo -e "\n\033[1;34m$(printf '%-80s' "=== $1")===\033[0m"; }
info() { echo "  $1"; }
warn() { echo -e "  \033[1;33m[AVISO] $1\033[0m"; }
ok()   { echo -e "  \033[1;32m✓ $1\033[0m"; }

# run CMD... — executa de verdade ou imprime em modo dry-run
run() {
    if [ "$DRY_RUN" = "1" ]; then
        printf '    [DRY] '
        printf '%q ' "$@"
        echo
    else
        "$@"
    fi
}

# must: igual a run, mas usado para comandos críticos (mesmo comportamento)
must() { run "$@"; }

# write_file DEST — lê heredoc do stdin e grava em DEST (ou só mostra em dry-run)
write_file() {
    local dest="$1"
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] cat > ${dest} <<'EOF'  (conteúdo omitido)"
    else
        cat > "$dest"
    fi
}

# ─── Mirror offline ─────────────────────────────────────────────────────────
# fetch_url URL DEST — baixa de URL; em --offline pula o oficial; retorna 0 ok/1 falha
fetch_url() {
    local url="$1" dest="$2"
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] wget -q -O ${dest} ${url}"
        return 0
    fi
    if [ "$OFFLINE" = "1" ]; then
        return 1
    fi
    wget -q --timeout=30 -O "$dest" "$url" 2>/dev/null
}

# fetch_or_mirror RELPATH DEST — tenta repo oficial (se não offline); falhou → mirror
# URL oficial é derivada de RELPATH (ver chamadas); mirror é sempre MIRROR_URL/RELPATH
fetch_or_mirror() {
    local relpath="$1" dest="$2" official="$3"
    if [ -f "$dest" ] && [ "$DRY_RUN" = "0" ]; then
        info "   já existe: ${dest}"
        return 0
    fi
    if [ "$OFFLINE" = "0" ] && [ -n "$official" ]; then
        info "   tentando repo oficial: ${official}"
        if fetch_url "$official" "$dest"; then
            [ "$DRY_RUN" = "1" ] || [ -s "$dest" ] && return 0
        fi
        warn "   repo oficial falhou — usando mirror ${MIRROR_URL}/${relpath}"
    elif [ "$OFFLINE" = "1" ]; then
        info "   --offline: usando mirror ${MIRROR_URL}/${relpath}"
    fi
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] wget -q -O ${dest} ${MIRROR_URL}/${relpath}"
        return 0
    fi
    wget -q --timeout=60 -O "$dest" "${MIRROR_URL}/${relpath}" 2>/dev/null && [ -s "$dest" ]
}

# img_name REPO:TAG — converte "repo/tag:ver" no nome sanitizado do arquivo .tar.gz
img_name() {
    echo "$1" | tr "/:" "__"
}

# load_image REPO:TAG — garante a imagem carregada; tenta pull oficial, falhou → docker load do mirror
load_image() {
    local img="$1" fname fpath
    fname="$(img_name "$img").tar.gz"
    fpath="${MIRROR_IMAGES}/${fname}"
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] docker image inspect ${img} >/dev/null || (docker pull ${img} || docker load -i ${fpath})"
        return 0
    fi
    if docker image inspect "$img" >/dev/null 2>&1; then
        info "   imagem já presente: ${img}"
        return 0
    fi
    if [ "$OFFLINE" = "0" ]; then
        if docker pull "$img" >/dev/null 2>&1; then
            info "   pull OK: ${img}"
            return 0
        fi
    fi
    if [ -f "$fpath" ]; then
        info "   carregando do mirror: ${fname}"
        docker load -i "$fpath" >/dev/null 2>&1
    else
        warn "   imagem ${img} não disponível no mirror nem por pull"
    fi
}

# Confirmar root (ignorado em dry-run)
if [ "$DRY_RUN" = "0" ] && [ "$(id -u)" != "0" ]; then
    echo "ERRO: execute com sudo/root (instalação real)."
    echo "      Para simular sem alterar nada: sudo ./install.sh --dry-run"
    exit 1
fi

# ─── Detecta SO ────────────────────────────────────────────────────────────
OS_ID="unknown"; OS_VER="0"
if [ -r /etc/os-release ]; then
    OS_ID=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
    OS_VER=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
fi
if [ "$OS_ID" != "rocky" ] && [ "$OS_ID" != "almalinux" ]; then
    warn "SO detectado: ${OS_ID} ${OS_VER}. O CWP7 exige Rocky/AlmaLinux 9. Continuando (pode falhar no passo CWP)."
else
    info "SO detectado: ${OS_ID} ${OS_VER}"
fi

# ─── Passos ────────────────────────────────────────────────────────────────
passo1_atualizar() {
    log "[1/13] Atualizando sistema e instalando dependências base"
    run dnf update -y
    run dnf install -y git curl tar unzip gzip htop wget vim nano jq \
        bind-utils telnet nc nmap openssl ca-certificates \
        perl perl-libwww-perl perl-LWP-Protocol-https
    ok "Sistema atualizado e dependências base instaladas"
}

passo2_docker() {
    log "[2/13] Instalando Docker Engine + Docker Compose plugin"
    if [ "$DRY_RUN" = "0" ] && command -v docker >/dev/null 2>&1; then
        info "Docker já instalado — pulando"
        return 0
    fi

    local docker_rpms=""
    if [ "$OFFLINE" = "1" ] || [ ! -x /usr/bin/dnf ]; then
        docker_rpms="${MIRROR_SOFT}/docker-rpms"
        info "Instalando docker-ce via rpms locais do mirror: ${docker_rpms}"
    fi

    if [ -n "$docker_rpms" ] && [ -d "$docker_rpms" ]; then
        run dnf install -y "$docker_rpms"/*.rpm || {
            warn "Falha ao instalar rpms docker do mirror — tentando repo oficial"
            docker_rpms=""
        }
    fi

    if [ -z "$docker_rpms" ]; then
        info "Instalando Docker via repo oficial (download.docker.com)..."
        run dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo || true
        if [ "$DRY_RUN" = "0" ] && ! dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null 2>&1; then
            warn "Repo oficial do Docker falhou — baixando rpms do mirror e instalando"
            run dnf install -y "${MIRROR_SOFT}/docker-rpms/"*.rpm
        else
            run dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
        fi
    fi

    run systemctl enable --now docker
    if [ "$DRY_RUN" = "1" ]; then
        info "[DRY] aguardaria docker responder (docker info)"
    else
        for i in {1..10}; do docker info >/dev/null 2>&1 && break || sleep 2; done
    fi
    ok "Docker + Compose prontos"
}

passo3_cwp() {
    log "[3/13] Instalando CentOS Web Panel 7 (CWP7) — e-mail + CSF + MariaDB"
    if [ "$SKIP_CWP" = "1" ]; then
        warn "--skip-cwp informado — CWP7 NÃO será instalado (configurar CSF/Postfix manualmente depois)"
        return 0
    fi
    if [ "$DRY_RUN" = "0" ] && [ -d "/usr/local/cwpsrv" ]; then
        info "CWP7 já presente — pulando"
        return 0
    fi
    info "Baixando instalador oficial do CWP7 (com fallback ao mirror)..."
    fetch_or_mirror "soft/cwp-el9-latest" "/usr/local/src/cwp-el9-latest" \
        "http://centos-webpanel.com/cwp-el9-latest" || true
    info "Executando instalador do CWP7 em modo NÃO-interativo (senha MySQL via pipe)..."
    info "ATENÇÃO: leva de 20 a 60 minutos."
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] (execução longa) ( cd /usr/local/src && yes \"\" | sh cwp-el9-latest )"
    else
        ( cd /usr/local/src && yes "" | sh cwp-el9-latest ) || true
        if [ -d "/usr/local/cwpsrv" ]; then
            ok "CWP7 instalado com sucesso"
        else
            warn "CWP7 não confirmado após instalação. Verificar manualmente. Continuando mesmo assim."
        fi
    fi
}

passo4_csf() {
    log "[4/13] Configurando CSF (firewall) com suporte a Docker"
    if [ "$DRY_RUN" = "0" ] && [ ! -f /etc/csf/csf.conf ]; then
        info "CSF não encontrado — baixando instalador (mirror se repo oficial offline)"
        fetch_or_mirror "soft/csf.tgz" "/tmp/csf.tgz" \
            "https://download.configserver.com/csf.tgz" || true
        run tar -xzf /tmp/csf.tgz -C /tmp
        run sh /tmp/csf/install.sh || true
    elif [ "$DRY_RUN" = "1" ]; then
        info "Verificaria existência de /etc/csf/csf.conf; se ausente baixaria CSF (oficial → mirror)"
    fi

    info "Aplicando configuração CSF para Docker (DOCKER=1, rede 172.16.0.0/12)"
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] sed -i 's/^DOCKER = .*/DOCKER = \"1\"/' /etc/csf/csf.conf"
        echo "    [DRY] sed -i 's#^DOCKER_NETWORK4 = .*#DOCKER_NETWORK4 = \"172.16.0.0/12\"#' /etc/csf/csf.conf"
        echo "    [DRY] sed -i (TCP_IN/TCP6_IN com portas de borda) + gravar csfpost.sh"
        echo "    [DRY] csf -r"
    else
        if [ -f /etc/csf/csf.conf ]; then
            sed -i 's/^DOCKER = .*/DOCKER = "1"/' /etc/csf/csf.conf
            sed -i 's#^DOCKER_NETWORK4 = .*#DOCKER_NETWORK4 = "172.16.0.0/12"#' /etc/csf/csf.conf
            sed -i 's/^TCP_IN = .*/TCP_IN = "20,21,22,4567,25,53,80,110,143,443,465,853,587,993,995,2030,2031,2082,2083,2086,2087,2095,2096"/' /etc/csf/csf.conf
            sed -i 's/^TCP6_IN = .*/TCP6_IN = "20,21,22,25,53,80,110,143,443,465,853,587,993,995,2030,2031,2082,2083,2086,2087,2095,2096"/' /etc/csf/csf.conf
        fi
        cat > /usr/local/csf/bin/csfpost.sh <<'EOF'
#!/bin/bash
# Restaura conectividade e DNAT das redes Docker apos reinicio do CSF.
for net in 172.16.0.0/12; do
  iptables -C FORWARD -s "$net" -j ACCEPT 2>/dev/null || iptables -I FORWARD -s "$net" -j ACCEPT
  iptables -C FORWARD -d "$net" -j ACCEPT 2>/dev/null || iptables -I FORWARD -d "$net" -j ACCEPT
done
if ! iptables -t nat -L DOCKER >/dev/null 2>&1 || [ "$(iptables -t nat -L DOCKER -n 2>/dev/null | wc -l)" -le 3 ]; then
  systemctl restart docker >/dev/null 2>&1
fi
exit 0
EOF
        chmod 700 /usr/local/csf/bin/csfpost.sh 2>/dev/null || true
        command -v csf >/dev/null 2>&1 && csf -r >/dev/null 2>&1 || true
    fi
    ok "CSF configurado (se presente). Portas de borda abertas."
}

passo5_redes_volumes() {
    log "[5/13] Criando redes e volumes Docker do Ichill"

    info "Rede principal ${NETWORK}"
    run docker network inspect "$NETWORK" >/dev/null 2>&1 || \
        run docker network create --driver bridge "$NETWORK"

    info "Rede dedicada do Caddy Admin ${CADDY_NET} (${CADDY_NET_SUBNET})"
    run docker network inspect "$CADDY_NET" >/dev/null 2>&1 || \
        run docker network create --driver bridge --subnet "$CADDY_NET_SUBNET" "$CADDY_NET"

    info "Volumes do compose (prefixo ichill_)"
    for v in $COMPOSE_VOLUMES; do
        run docker volume create "ichill_${v}" || true
    done

    info "Volumes extras"
    for v in $EXTRA_VOLUMES $MONITORING_VOLUMES; do
        run docker volume create "$v" || true
    done

    ok "Redes e volumes prontos"
}

passo6_timezone() {
    log "[6/13] Configurando timezone ${TZ}"
    run timedatectl set-timezone "$TZ" 2>/dev/null || \
        run ln -sf "/usr/share/zoneinfo/${TZ}" /etc/localtime
    ok "Timezone: ${TZ}"
}

passo7_codigo_env() {
    log "[7/13] Preparando código-fonte e .env"

    local src="${SOURCE_DIR}"
    if [ -z "$src" ]; then
        if [ -f "${PROJECT_ROOT}/docker-compose.yml" ]; then
            src="$PROJECT_ROOT"
        elif [ -f "$(pwd)/docker-compose.yml" ]; then
            src="$(pwd)"
        fi
    fi

    if [ -n "$src" ] && [ "$src" != "$PROJECT_ROOT" ]; then
        info "Copiando código de ${src} para ${PROJECT_ROOT}"
        run mkdir -p "$(dirname "$PROJECT_ROOT")"
        run cp -rp "$src" "$PROJECT_ROOT"
    elif [ -z "$src" ]; then
        warn "Nenhuma fonte de código encontrada (use --source=/caminho ou rode de dentro do repo)."
        warn "O passo 8 (compose up) será pulado."
        return 0
    fi

    if [ ! -f "${PROJECT_ROOT}/.env" ]; then
        if [ -f "${PROJECT_ROOT}/.env.example" ]; then
            info "Criando .env a partir do .env.example"
            run cp "${PROJECT_ROOT}/.env.example" "${PROJECT_ROOT}/.env"
        else
            info "Criando .env com template de segredos (REVISAR/EDITE antes de subir em produção)"
            write_file "${PROJECT_ROOT}/.env" <<'EOF'
POSTGRES_SERVER=postgres
POSTGRES_USER=makson
POSTGRES_PASSWORD=Mycodes@M@ikelly4283
POSTGRES_DB=makson_achou
POSTGRES_PASSWORD_URL=Mycodes%40M%40ikelly4283
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=CHANGE_ME_REDIS
RABBITMQ_URL=amqp://ichill:CHANGE_ME@rabbitmq:5672/
QDRANT_HOST=qdrant
QDRANT_PORT=6333
MINIO_ENDPOINT=minio:9000
MINIO_ACCESS_KEY=ichill
MINIO_SECRET_KEY=CHANGE_ME_MINIO
ODOO_URL=http://ichill_odoo:8069
ODOO_DB=makson_odoo
ODOO_USER=makson
ODOO_PASSWORD=Mycodes@M@ikelly4283
ICHILL_SSO_SECRET=CHANGE_ME_SSO
JWT_SECRET_KEY=CHANGE_ME_JWT
OPENAI_API_KEY=CHANGE_ME_OPENAI
SUPERADMIN_ODOO_PASSWORD=CHANGE_ME_SUPERADMIN
EOF
        fi
    else
        info ".env já existe — mantendo"
    fi

    run mkdir -p "${PROJECT_ROOT}/backups" "${PROJECT_ROOT}/caddy"
    if [ ! -f "${PROJECT_ROOT}/caddy/Caddyfile" ]; then
        info "Criando Caddyfile inicial de fallback"
        write_file "${PROJECT_ROOT}/caddy/Caddyfile" <<'EOF'
{
    email maksonramos.silva@gmail.com
    on_demand_tls {
        ask http://ichill_core:8000/api/v1/admin/domains/check
    }
}

:443 {
    tls {
        on_demand
    }
    reverse_proxy ichill_core:8000
}

import /etc/caddy/custom-domains.conf
EOF
    fi
    [ -f "${PROJECT_ROOT}/caddy/custom-domains.conf" ] || run touch "${PROJECT_ROOT}/caddy/custom-domains.conf"

    run mkdir -p /etc/pki/tls/certs /etc/pki/tls/private
    ok "Código e .env preparados"
}

passo7b_mirror_download() {
    log "[7b/13] Baixando mirror offline (arquivosvps) para este node"
    if [ "$NODE_MODE" = "0" ]; then
        info "  (apenas no modo --node) — pulando"
        return 0
    fi
    if [ "$DRY_RUN" = "0" ] && [ -d "${MIRROR_DIR}/images" ] && \
       [ -n "$(ls -A "${MIRROR_DIR}/images" 2>/dev/null)" ]; then
        info "Mirror local já existe (${MIRROR_DIR}) — mantendo"
        return 0
    fi
    info "Baixando mirror de ${MIRROR_URL} (soft + imagens Docker)..."
    info "  Isso garante instalação 100% offline sem depender de repos externos."
    run mkdir -p "$MIRROR_DIR"
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] wget -q -O ${MIRROR_DIR}/archivo.tar.gz ${MIRROR_URL}/arquivosvps.tar.gz"
        echo "    [DRY] tar -xzf ${MIRROR_DIR}/archivo.tar.gz -C ${MIRROR_DIR} --strip-components=1"
        return 0
    fi
    local tgz="${MIRROR_DIR}/arquivosvps.tar.gz"
    wget -q --timeout=120 -O "$tgz" "${MIRROR_URL}/arquivosvps.tar.gz" 2>/dev/null
    if [ -s "$tgz" ]; then
        tar -xzf "$tgz" -C "$MIRROR_DIR" --strip-components=1 2>/dev/null && rm -f "$tgz"
        ok "Mirror baixado (${MIRROR_DIR})"
    else
        warn "Falha ao baixar mirror. A instalação seguirá dependendo dos repos oficiais."
        rm -f "$tgz"
    fi
}

passo7c_agent() {
    log "[7c/13] Instalando ichill-agent (auto-registro no control plane)"
    if [ "$NODE_MODE" = "0" ]; then
        info "  (apenas no modo --node) — pulando"
        return 0
    fi
    local agent_dir="/opt/ichill-agent"
    local agent_bin="${agent_dir}/ichill-agent"
    if [ "$DRY_RUN" = "0" ] && systemctl is-active --quiet ichill-agent 2>/dev/null; then
        info "ichill-agent já ativo — mantendo"
        return 0
    fi
    info "Compilando agente Go a partir do source (ou baixando binário do mirror)..."
    if [ -f "${PROJECT_ROOT}/ichill-agent/main.go" ]; then
        if [ "$DRY_RUN" = "0" ]; then
            mkdir -p "$agent_dir"
            cd "${PROJECT_ROOT}/ichill-agent" || return 1
            if command -v go >/dev/null 2>&1; then
                go build -o "$agent_bin" . 2>/dev/null || true
            fi
            if [ ! -x "$agent_bin" ] && command -v docker >/dev/null 2>&1; then
                info "Go não instalado — compilando via container golang do mirror"
                docker run --rm -v "${PROJECT_ROOT}/ichill-agent:/src" -w /src \
                    golang:1.26-alpine sh -c "go build -o /src/ichill-agent ." 2>/dev/null || true
            fi
            cd - >/dev/null 2>&1 || true
        else
            echo "    [DRY] go build -o ${agent_bin} . (ou container golang do mirror)"
        fi
    else
        warn "ichill-agent/main.go não encontrado — baixando binário pré-compilado do mirror"
        run wget -q -O "$agent_bin" "${MIRROR_URL}/soft/ichill-agent" 2>/dev/null || true
        run chmod +x "$agent_bin" 2>/dev/null || true
    fi

    if [ "$DRY_RUN" = "0" ] && [ ! -x "$agent_bin" ]; then
        warn "Falha ao obter o ichill-agent — o node não se auto-registrará. Instalar manualmente depois."
        return 0
    fi

    info "Criando serviço systemd ichill-agent (control plane: ${CONTROL_PLANE_URL})"
    write_file /etc/systemd/system/ichill-agent.service <<EOF
[Unit]
Description=Ichill Node Agent
After=network.target docker.service

[Service]
Type=simple
WorkingDirectory=${agent_dir}
ExecStart=${agent_bin}
Environment=CONTROL_PLANE_URL=${CONTROL_PLANE_URL}
Environment=CONTROL_PLANE_URL_BACKUP=${CONTROL_PLANE_URL_BACKUP}
Environment=AGENT_REGION=${AGENT_REGION}
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
    run systemctl daemon-reload
    run systemctl enable --now ichill-agent || true
    ok "ichill-agent instalado e iniciado (heartbeat a cada 30s)"
}

passo7e_vpn() {
    log "[7e/13] Configurando VPN OpenVPN static-key (tunnel para o control plane)"
    if [ "$NODE_MODE" = "0" ]; then
        info "  (apenas no modo --node) — pulando"
        return 0
    fi
    if [ "$VPN_ENABLE" != "1" ]; then
        info "  VPN desabilitada (VPN_ENABLE=0) — usando control plane via URL publica"
        return 0
    fi
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] dnf install -y openvpn"
        echo "    [DRY] mkdir -p /etc/openvpn; wget -O /etc/openvpn/static.key ${VPN_KEY_URL}"
        echo "    [DRY] cat > /etc/openvpn/server.conf (ifconfig ${VPN_LOCAL_IP} ${VPN_CONTROL_IP}, secret static.key, port ${VPN_PORT})"
        echo "    [DRY] systemctl enable --now openvpn-server"
        return 0
    fi
    if ! command -v openvpn >/dev/null 2>&1; then
        info "Instalando OpenVPN..."
        run dnf install -y openvpn || { warn "Falha ao instalar openvpn — VPN nao configurada"; return 0; }
    fi
    mkdir -p /etc/openvpn
    if [ ! -f /etc/openvpn/static.key ]; then
        info "Baixando static.key do mirror (${VPN_KEY_URL})..."
        run wget -q --timeout=60 -O /etc/openvpn/static.key "${VPN_KEY_URL}" 2>/dev/null || {
            warn "Nao foi possivel baixar static.key — gerando local (deve casar com o control plane)";
            run openvpn --genkey secret /etc/openvpn/static.key;
        }
        chmod 600 /etc/openvpn/static.key
    fi
    info "Criando OpenVPN server (node ${VPN_LOCAL_IP} <-> control ${VPN_CONTROL_IP})..."
    write_file /etc/openvpn/server.conf <<EOF
# VPN Ichill worker node — static-key (como a VPS1)
dev tun0
ifconfig ${VPN_LOCAL_IP} ${VPN_CONTROL_IP}
secret /etc/openvpn/static.key
port ${VPN_PORT}
proto udp
keepalive 10 60
persist-tun
persist-key
cipher AES-256-CBC
auth SHA256
verb 3
EOF
    run systemctl enable --now openvpn-server >/dev/null 2>&1 || run systemctl enable --now openvpn@server >/dev/null 2>&1 || true
    # Ajusta CONTROL_PLANE_URL para o IP VPN do control plane (se nao foi sobrescrito)
    if [ "$CONTROL_PLANE_URL" = "https://api.ichill.com.br/api/v1/admin/infra" ]; then
        CONTROL_PLANE_URL="http://${VPN_CONTROL_IP}:8000/api/v1/admin/infra"
        info "CONTROL_PLANE_URL ajustado para VPN: ${CONTROL_PLANE_URL}"
    fi
    ok "VPN configurada (tunnel ${VPN_LOCAL_IP} -> ${VPN_CONTROL_IP})"
}

passo7d_node_caddy() {
    log "[7d/13] Instalando Caddy próprio do node (gerenciado pelo control plane)"
    if [ "$NODE_MODE" = "0" ]; then
        info "  (apenas no modo --node) — pulando"
        return 0
    fi
    if [ "$DRY_RUN" = "0" ] && docker ps -a --format '{{.Names}}' | grep -qx ichill_caddy; then
        info "ichill_caddy já existe — mantendo"
        return 0
    fi
    info "Criando Caddyfile base do node (domínios adicionados pelo control plane via agente)"
    run mkdir -p /etc/caddy
    write_file /etc/caddy/Caddyfile <<'EOF'
{
  email makson@ichill.com.br
  admin 0.0.0.0:2019
}
:443 {
  tls {
    on_demand
  }
  respond 404
}
import /etc/caddy/custom-domains.conf
EOF
    [ -f /etc/caddy/custom-domains.conf ] || run touch /etc/caddy/custom-domains.conf

    load_image "caddy:2-alpine"
    run docker run -d --name ichill_caddy \
        --network "$NETWORK" \
        --restart always \
        -p 80:80 \
        -p 443:443 \
        -v /etc/caddy:/etc/caddy:ro \
        -v caddydata:/data \
        caddy:2-alpine
    ok "Caddy do node pronto (admin API 0.0.0.0:2019 acessível pelo agente)"
}


passo8_compose_up() {
    log "[8/13] Subindo stack principal do Ichill (compose ichill)"
    local compose_path="${PROJECT_ROOT}/docker-compose.yml"
    # Em dry-run, o cp do passo 7 não ocorreu de verdade: checa a fonte informada
    if [ "$DRY_RUN" = "1" ] && [ -n "$SOURCE_DIR" ]; then
        compose_path="${SOURCE_DIR}/docker-compose.yml"
    fi
    if [ ! -f "$compose_path" ]; then
        warn "docker-compose.yml não encontrado (${compose_path}) — pulando compose up"
        return 0
    fi
    info "docker compose build (imagens locais: core, engine, portal, odoo...)"

    # Modo offline: carrega imagens do mirror antes de subir (sem Docker Hub)
    if [ "$OFFLINE" = "1" ]; then
        info "   --offline: carregando imagens do mirror (${MIRROR_IMAGES})"
        if [ -d "$MIRROR_IMAGES" ]; then
            for f in "$MIRROR_IMAGES"/*.tar.gz; do
                [ -f "$f" ] || continue
                if [ "$DRY_RUN" = "1" ]; then
                    echo "    [DRY] docker load -i ${f}"
                else
                    info "   load: $(basename "$f")"
                    docker load -i "$f" >/dev/null 2>&1 || warn "   falha ao carregar $(basename "$f")"
                fi
            done
        else
            warn "   MIRROR_IMAGES não encontrado (${MIRROR_IMAGES}) — compose pode falhar no pull"
        fi
        # Após load, sobe SEM build (imagens já prontas)
        run docker compose --project-directory "$PROJECT_ROOT" up -d --no-build
        ok "Stack principal inicializada (offline, imagens do mirror)"
        return 0
    fi

    run docker compose --project-directory "$PROJECT_ROOT" build
    info "docker compose up -d (rede $NETWORK + rede $CADDY_NET com IPs fixos)"
    run docker compose --project-directory "$PROJECT_ROOT" up -d
    ok "Stack principal inicializada"
}

passo9_containers_manuais() {
    log "[9/13] Criando container manual (ichill_ws_proxy)"
    info "Nota: apenas o Odoo MASTER (ichill_odoo) sobe junto. Tenants/clientes"
    info "      (ex: odoo_cliente_teste) são provisionados sob demanda pelo core."

    info "Proxy nginx ws (ichill_ws_proxy)"
    if [ "$DRY_RUN" = "0" ] && docker ps -a --format '{{.Names}}' | grep -qx ichill_ws_proxy; then
        info "ichill_ws_proxy já existe"
    elif [ -f /tmp/nginx-ws.conf ]; then
        load_image "nginx:alpine"
        run docker run -d --name ichill_ws_proxy \
            --network "$NETWORK" \
            --restart always \
            -p 127.0.0.1:8073:80 \
            -e TZ="$TZ" \
            -v /etc/localtime:/etc/localtime:ro \
            -v /usr/share/zoneinfo:/usr/share/zoneinfo:ro \
            -v /tmp/nginx-ws.conf:/etc/nginx/conf.d/default.conf:ro \
            nginx:alpine
    else
        warn "/tmp/nginx-ws.conf não encontrado — pulando ws_proxy"
    fi
    ok "Containers manuais prontos"
}

passo10_monitoring() {
    log "[10/13] Subindo stack de monitoramento"
    local monitoring_path="${PROJECT_ROOT}/monitoring/docker-compose.yml"
    if [ "$DRY_RUN" = "1" ] && [ -n "$SOURCE_DIR" ]; then
        monitoring_path="${SOURCE_DIR}/monitoring/docker-compose.yml"
    fi
    if [ ! -f "$monitoring_path" ]; then
        warn "monitoring/docker-compose.yml não encontrado — pulando"
        return 0
    fi
    if [ "$OFFLINE" = "1" ]; then
        info "   --offline: load das imagens de monitoramento do mirror"
        for img in "prom/prometheus:latest" "gcr.io/cadvisor/cadvisor:latest" \
                   "prometheuscommunity/postgres-exporter:latest" "grafana/grafana:latest"; do
            load_image "$img"
        done
        run docker compose --project-directory "${PROJECT_ROOT}/monitoring" up -d --no-build
    else
        run docker compose --project-directory "${PROJECT_ROOT}/monitoring" up -d
    fi
    ok "Monitoramento inicializado"
}

passo11_deupar() {
    log "[11/13] Subindo stack Deupar (opcional)"
    if [ ! -f /home/makson/public_html/deupar/docker-compose.yml ]; then
        info "Deupar não presente — pulando"
        return 0
    fi
    if [ "$OFFLINE" = "1" ]; then
        info "   --offline: load das imagens deupar do mirror"
        for img in "deupar-deupar_web:latest" "deupar-deupar_api_go:latest" \
                   "deupar-deupar_go_core:latest" "postgis/postgis:15-3.4-alpine" \
                   "redis:7-alpine"; do
            load_image "$img"
        done
        run docker compose --project-directory /home/makson/public_html/deupar up -d --no-build
    else
        run docker compose --project-directory /home/makson/public_html/deupar build
        run docker compose --project-directory /home/makson/public_html/deupar up -d
    fi
    ok "Deupar inicializado"
}

passo12_restore() {
    log "[12/13] Restaurando backup (se informado)"
    if [ -z "$BACKUP_FILE" ]; then
        info "Nenhum --backup informado — pulando restauração"
        return 0
    fi
    if [ ! -f "$BACKUP_FILE" ]; then
        warn "Arquivo de backup não encontrado: ${BACKUP_FILE} — pulando"
        return 0
    fi
    if [ -f "${SCRIPTS_DIR}/restore.sh" ]; then
        run bash "${SCRIPTS_DIR}/restore.sh" "$BACKUP_FILE"
    else
        warn "restore.sh não encontrado — restauração manual necessária"
    fi
    ok "Restauração concluída"
}

passo13_health() {
    log "[13/13] Verificação de saúde da stack"
    if [ "$DRY_RUN" = "1" ]; then
        echo "    [DRY] aguardaria 10s e testaria:"
        echo "    [DRY]   curl -s http://localhost:8000/docs"
        echo "    [DRY]   curl -s http://localhost:8000/api/v1/health"
        echo "    [DRY]   docker ps (espera ~23 containers Up)"
        return 0
    fi
    sleep 10
    CORE_UP=false
    for i in {1..12}; do
        if curl -s http://localhost:8000/api/v1/health >/dev/null 2>&1; then
            CORE_UP=true
            break
        fi
        info "Aguardando API do Ichill (tentativa $i/12)..."
        sleep 5
    done
    if [ "$CORE_UP" = true ]; then
        ok "API do Ichill: ONLINE e SAUDÁVEL!"
    else
        warn "API do Ichill não respondeu no timeout. Verificar: docker compose logs ichill-cloud"
    fi
    info "Containers ativos: $(docker ps -q | wc -l)"
}

# ─── Execução ───────────────────────────────────────────────────────────────
log "Ichill Cloud — Instalador FULL (dry-run=$DRY_RUN, início no passo $START_STEP)"
if [ "$DRY_RUN" = "1" ]; then
    info "MODO SIMULAÇÃO (--dry-run): nenhum comando será executado, apenas exibido."
fi
info "SO: ${OS_ID} ${OS_VER} | IP: ${VPS_IP} | Projeto: ${PROJECT_ROOT}"

# Ordem de execução: nome da função por passo
if [ "$NODE_MODE" = "1" ]; then
    PASSOS=(
        "1:passo1_atualizar"
        "2:passo2_docker"
        "4:passo4_csf"
        "5:passo5_redes_volumes"
        "6:passo6_timezone"
        "7:passo7_codigo_env"
        "7b:passo7b_mirror_download"
        "7e:passo7e_vpn"
        "7c:passo7c_agent"
        "7d:passo7d_node_caddy"
        "13:passo13_health"
    )
else
    PASSOS=(
        "1:passo1_atualizar"
        "2:passo2_docker"
        "3:passo3_cwp"
        "4:passo4_csf"
        "5:passo5_redes_volumes"
        "6:passo6_timezone"
        "7:passo7_codigo_env"
        "8:passo8_compose_up"
        "9:passo9_containers_manuais"
        "10:passo10_monitoring"
        "11:passo11_deupar"
        "12:passo12_restore"
        "13:passo13_health"
    )
fi
# should_run NUM — true se o passo NUM deve rodar dado START_STEP
# Suporta sub-passos alfanuméricos ("7b"); "7b" roda quando START_STEP <= 7
should_run() {
    local num="$1"
    if [[ "$num" =~ ^[0-9]+$ ]]; then
        [ "$((10#$num))" -ge "$((10#$START_STEP))" ]
    else
        local base="${num%%[a-z]*}"
        [ "$((10#$base))" -ge "$((10#$START_STEP))" ]
    fi
}
for entry in "${PASSOS[@]}"; do
    num="${entry%%:*}"
    fn="${entry##*:}"
    if should_run "$num"; then "$fn"; fi
done

echo
echo "=============================================================================="
if [ "$DRY_RUN" = "1" ]; then
    echo " SIMULAÇÃO CONCLUÍDA — nenhuma alteração foi feita no sistema."
    if [ "$NODE_MODE" = "1" ]; then
        echo " Para instalar um WORKER NODE de verdade: sudo ./installichill.sh --node [--offline]"
    else
        echo " Para instalar de verdade: sudo ./installichill.sh [--backup=...] [--skip-cwp] [--offline]"
    fi
elif [ "$NODE_MODE" = "1" ]; then
    echo " WORKER NODE PRONTO — registrado no control plane (ver /admin/infra)! "
    echo " READY PARA RECEBER NOVOS TENANTS!"
else
    echo " INSTALAÇÃO COMPLETA DO ICHILL CLOUD CONCLUÍDA COM SUCESSO!"
    echo " STATUS DO SERVIDOR: READY!"
fi
echo "=============================================================================="
