#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Portiger Installer — DEV MODE
# Usage: curl -sSL https://get.portiger.io | sudo bash
#
# Architecture (LOCKED 2026-07-17):
#   - Control plane (Traefik + Portiger) runs as PLAIN containers via
#     `docker compose up -d` — NOT as swarm services. Swarm host-mode port
#     publishing proved unreliable (bindings lost on daemon restart/task churn)
#     and plain containers make self-update trivial.
#   - Docker Swarm is still always initialized: customer stacks and system
#     stacks (mail etc.) are deployed onto swarm BY Portiger itself.
#   - Mail (Stalwart) is NOT installed by this script. The answer to the mail
#     question is passed to Portiger as BOOTSTRAP_MAIL=1; Portiger installs it
#     as a system stack once healthy (before first login), generating the mail
#     admin password itself.
#   - System-stack data lives under $HOME/.portiger (host path, mounted into
#     the Portiger container at the same path so bind sources stay valid).
#
# DEV-MODE defaults (a separate prod installer will flip these):
#   - Uses locally built images (portiger/portiger:dev)
#   - FORCE_TIER=MAX (only honored by -tags dev builds)
#   - Let's Encrypt defaults to OFF (self-signed) — local machines have no
#     public IP; answer "y" at the TLS prompt on a real server
# ─────────────────────────────────────────────────────────────────────────────

set -euo pipefail

# ─────────────────────────────────────────────────────────────────────────────
# Colour helpers
# ─────────────────────────────────────────────────────────────────────────────
BOLD="\033[1m"
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
RED="\033[0;31m"
CYAN="\033[0;36m"
RESET="\033[0m"

info()    { printf "${GREEN}✓${RESET} %s\n" "$1"; }
warn()    { printf "${YELLOW}!${RESET} %s\n" "$1"; }
error()   { printf "${RED}✗${RESET} %s\n" "$1" >&2; exit 1; }
# step: brief pause so each phase is readable, not a single flash of text
step()    { printf "\n${CYAN}▶${RESET} ${BOLD}%s${RESET}\n" "$1"; sleep 1; }
prompt()  { printf "${BOLD}%s${RESET}" "$1"; }

# ─────────────────────────────────────────────────────────────────────────────
# Root check
# ─────────────────────────────────────────────────────────────────────────────
[ "$(id -u)" -eq 0 ] || error "Run as root: sudo sh install.sh"

# ─────────────────────────────────────────────────────────────────────────────
# Interactive input guard
# When run via `curl ... | sudo bash`, the script itself arrives on stdin, so
# fd 0 must NOT be exec-redirected (bash would then read the remaining script
# from the terminal and hang). Instead, open fd 3 on the terminal and point
# every prompt read at it (read -u 3).
# ─────────────────────────────────────────────────────────────────────────────
if [ -t 0 ]; then
    exec 3<&0
elif [ -e /dev/tty ] && [ -r /dev/tty ]; then
    exec 3</dev/tty
else
    error "No terminal available for interactive prompts. Download first: curl -sSL <url> -o install.sh && sudo bash install.sh"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Header
# ─────────────────────────────────────────────────────────────────────────────
PORTIGER_VERSION="0.1.0"

printf "\n"
printf "${BOLD}╔══════════════════════════════════════╗${RESET}\n"
printf "${BOLD}║%*s%s%*s║${RESET}\n" 11 "" "Portiger v${PORTIGER_VERSION}" 12 ""
printf "${BOLD}║%*s%s%*s║${RESET}\n" 11 "" "Installer (dev)" 12 ""
printf "${BOLD}╚══════════════════════════════════════╝${RESET}\n\n"

cat <<INTRO
This script bootstraps a Portiger deployment:
  - Traefik reverse proxy (plain container, Let's Encrypt ACME optional)
  - Portiger control plane (plain container)
  - Docker Swarm (initialized for the workloads Portiger manages)
  - Optional: mail server (Stalwart) — installed by Portiger itself
    as a system stack after first boot

Prerequisites:
  - A domain with DNS A records pointing to this server
  - 4 GB RAM / 2 CPU recommended
  - Ports 80 and 443 reachable from the internet

INTRO

# ─────────────────────────────────────────────────────────────────────────────
# Faz 1: Interactive questions
# ─────────────────────────────────────────────────────────────────────────────

step "Configuration"

# -- Domain (mandatory) -------------------------------------------------------
while true; do
    prompt "Domain (e.g. example.com): "
    read -u 3 -r DOMAIN
    [ -n "$DOMAIN" ] && break
    warn "Domain is required — Portiger will not work without one"
done

# Network name: dots → dashes (example.com → example-com-net)
NET_NAME="$(echo "$DOMAIN" | tr '.' '-')-net"

# -- Server public IP ----------------------------------------------------------
PUBLIC_IP=""
if command -v curl >/dev/null 2>&1; then
    PUBLIC_IP="$(curl -sSL --connect-timeout 5 https://ifconfig.me 2>/dev/null || true)"
elif command -v wget >/dev/null 2>&1; then
    PUBLIC_IP="$(wget -qO- --timeout=5 https://ifconfig.me 2>/dev/null || true)"
fi
if [ -z "$PUBLIC_IP" ]; then
    PUBLIC_IP="<sunucu-ip>"
fi

# -- Admin credentials ---------------------------------------------------------
prompt "Admin email [admin@${DOMAIN}]: "
read -u 3 -r ADMIN_EMAIL
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@${DOMAIN}}"

while true; do
    prompt "Admin password (min 8 chars): "
    read -u 3 -rs ADMIN_PASSWORD
    printf "\n"
    [ ${#ADMIN_PASSWORD} -ge 8 ] && break
    warn "Password must be at least 8 characters"
done

# -- Mail server ---------------------------------------------------------------
# The script does NOT install mail. The answer is handed to Portiger via
# BOOTSTRAP_MAIL; Portiger deploys Stalwart as a system stack once healthy
# and generates the mail admin password itself (visible in the panel).
MAIL_ENABLED=0
prompt "Install mail server (Stalwart, auto-installed by Portiger)? [y/N]: "
read -u 3 -r MAIL_CHOICE
if [ "$MAIL_CHOICE" = "y" ] || [ "$MAIL_CHOICE" = "Y" ]; then
    MAIL_ENABLED=1
    info "Portiger will install the mail server on first boot"

    # Pre-flight: Stalwart binds 25/465/587/143/993 on the host.
    # A local MTA (postfix/exim) on port 25 would send it into a restart loop.
    BUSY_MAIL_PORTS=""
    for p in 25 465 587 143 993; do
        if ss -tln 2>/dev/null | grep -qE "[:.]${p}[[:space:]]"; then
            BUSY_MAIL_PORTS="${BUSY_MAIL_PORTS} ${p}"
        fi
    done
    if [ -n "$BUSY_MAIL_PORTS" ]; then
        warn "Ports already in use on this host:${BUSY_MAIL_PORTS}"
        warn "The mail server cannot start while these are occupied."
        warn "If a local MTA is running: systemctl disable --now postfix (or exim4)"
        prompt "Continue anyway? [y/N]: "
        read -u 3 -r PORT_CONFLICT_CHOICE
        if [ "$PORT_CONFLICT_CHOICE" != "y" ] && [ "$PORT_CONFLICT_CHOICE" != "Y" ]; then
            error "Aborted — free the ports and re-run the installer"
        fi
    fi
fi

# -- TLS mode -----------------------------------------------------------------
# Let's Encrypt needs a public IP + resolving DNS records. Dev-mode default is
# OFF: Traefik serves its default self-signed certificate — browsers warn,
# but everything works. Answer "y" on a real server.
ACME_ENABLED=0
prompt "Use Let's Encrypt TLS? Requires public IP + DNS records. [y/N]: "
read -u 3 -r ACME_CHOICE
if [ "$ACME_CHOICE" = "y" ] || [ "$ACME_CHOICE" = "Y" ]; then
    ACME_ENABLED=1
    prompt "Let's Encrypt contact email [${ADMIN_EMAIL}]: "
    read -u 3 -r ACME_EMAIL
    ACME_EMAIL="${ACME_EMAIL:-${ADMIN_EMAIL}}"
else
    ACME_EMAIL="${ADMIN_EMAIL}"
    warn "Local mode: Traefik will serve its default self-signed certificate"
    warn "Browsers will show a certificate warning — this is expected"
fi

info "Configuration complete"

# -- DNS notification (after questions — MAIL_ENABLED is now known) ------------
step "DNS records required"

printf "\n"
printf "${BOLD}┌──────────────────────────────────────────────────────────┐${RESET}\n"
printf "${BOLD}│${RESET}  ${BOLD}DNS Kayıtları — kurulumdan önce DNS provider'ında${RESET}     ${BOLD}│${RESET}\n"
printf "${BOLD}│${RESET}  ${BOLD}aşağıdaki A kayıtlarını oluştur:${RESET}                        ${BOLD}│${RESET}\n"
printf "${BOLD}│${RESET}                                                          ${BOLD}│${RESET}\n"
printf "${BOLD}│${RESET}  ${CYAN}portiger.%-15s${RESET} → ${GREEN}%s${RESET}      ${BOLD}│${RESET}\n" "${DOMAIN}" "${PUBLIC_IP}"
printf "${BOLD}│${RESET}  ${CYAN}traefik.%-16s${RESET} → ${GREEN}%s${RESET}      ${BOLD}│${RESET}\n" "${DOMAIN}" "${PUBLIC_IP}"
if [ "$MAIL_ENABLED" -eq 1 ]; then
printf "${BOLD}│${RESET}  ${CYAN}mailserver.%-13s${RESET} → ${GREEN}%s${RESET}      ${BOLD}│${RESET}\n" "${DOMAIN}" "${PUBLIC_IP}"
printf "${BOLD}│${RESET}  ${CYAN}mail.%-18s${RESET} → ${GREEN}%s${RESET}      ${BOLD}│${RESET}\n" "${DOMAIN}" "${PUBLIC_IP}"
printf "${BOLD}│${RESET}                                                          ${BOLD}│${RESET}\n"
printf "${BOLD}│${RESET}  MX  ${DOMAIN} → mail.${DOMAIN}                             ${BOLD}│${RESET}\n"
fi
printf "${BOLD}└──────────────────────────────────────────────────────────┘${RESET}\n"
if [ "$ACME_ENABLED" -eq 1 ]; then
    printf "  ${YELLOW}⚠  DNS kayıtları çözülmeden ACME sertifika alımı çalışmaz.${RESET}\n\n"
else
    printf "  ${YELLOW}⚠  Local mode: DNS yerine /etc/hosts kullanabilirsin:${RESET}\n"
    printf "     127.0.0.1 portiger.${DOMAIN} traefik.${DOMAIN} mailserver.${DOMAIN}\n\n"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Faz 2: System preparation
# ─────────────────────────────────────────────────────────────────────────────

step "System preparation"

# -- OS / distro detection ----------------------------------------------------
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$ARCH" in
    x86_64)  ARCH="amd64" ;;
    aarch64) ARCH="arm64" ;;
    *)       error "Unsupported architecture: $ARCH" ;;
esac

PKG_MANAGER=""
if command -v apt-get >/dev/null 2>&1; then
    PKG_MANAGER="apt"
elif command -v dnf >/dev/null 2>&1; then
    PKG_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
    PKG_MANAGER="yum"
elif command -v apk >/dev/null 2>&1; then
    PKG_MANAGER="apk"
else
    error "No supported package manager found (apt/dnf/yum/apk)"
fi

DISTRO=""
if [ -f /etc/os-release ]; then
    DISTRO="$(. /etc/os-release && echo "$NAME")"
fi

info "Platform  : ${OS}/${ARCH}"
info "Distro    : ${DISTRO:-unknown}"
info "Pkg mgr   : ${PKG_MANAGER}"

# -- Package helpers ----------------------------------------------------------
pkg_install() {
    for pkg in "$@"; do
        case "$PKG_MANAGER" in
            apt) apt-get install -y -qq "$pkg" >/dev/null 2>&1 ;;
            dnf) dnf install -y -q "$pkg" >/dev/null 2>&1 ;;
            yum) yum install -y -q "$pkg" >/dev/null 2>&1 ;;
            apk) apk add --quiet "$pkg" >/dev/null 2>&1 ;;
        esac
    done
}

pkg_update() {
    case "$PKG_MANAGER" in
        apt) apt-get update -qq >/dev/null 2>&1 ;;
        dnf|yum) : ;;
        apk) apk update --quiet >/dev/null 2>&1 ;;
    esac
}

INSTALL_LOG="/var/log/portiger-install.log"

pkg_upgrade() {
    case "$PKG_MANAGER" in
        apt)
            DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a \
                apt-get upgrade -y -qq >>"$INSTALL_LOG" 2>&1 ;;
        dnf) dnf upgrade -y -q >>"$INSTALL_LOG" 2>&1 ;;
        yum) yum update -y -q >>"$INSTALL_LOG" 2>&1 ;;
        apk) apk upgrade --quiet >>"$INSTALL_LOG" 2>&1 ;;
    esac
}

pkg_update
info "Package lists updated"
warn "Upgrading system packages — this can take several minutes..."
warn "Watch progress in another terminal: tail -f ${INSTALL_LOG}"
pkg_upgrade
info "System packages upgraded"

# -- curl --------------------------------------------------------------------
if ! command -v curl >/dev/null 2>&1; then
    warn "curl not found, installing..."
    pkg_install curl
    command -v curl >/dev/null 2>&1 || error "Failed to install curl"
    info "curl installed"
else
    info "curl OK"
fi

# -- openssl -----------------------------------------------------------------
if ! command -v openssl >/dev/null 2>&1; then
    warn "openssl not found, installing..."
    pkg_install openssl
    command -v openssl >/dev/null 2>&1 || error "Failed to install openssl"
    info "openssl installed"
else
    info "openssl OK"
fi

# -- Docker ------------------------------------------------------------------
if ! command -v docker >/dev/null 2>&1; then
    warn "Docker not found, installing from get.docker.com..."
    curl -fsSL https://get.docker.com | sh >/dev/null 2>&1
    command -v docker >/dev/null 2>&1 || error "Docker installation failed"
    systemctl enable docker --now >/dev/null 2>&1
    info "Docker installed"
else
    DOCKER_VER="$(docker --version 2>/dev/null | awk '{print $3}' | tr -d ',')"
    info "Docker OK (${DOCKER_VER})"
fi

# -- Docker daemon running ---------------------------------------------------
if ! docker info >/dev/null 2>&1; then
    warn "Docker daemon not running, starting..."
    systemctl start docker
    sleep 2
    docker info >/dev/null 2>&1 || error "Docker daemon failed to start"
    info "Docker daemon started"
fi

# -- Docker Compose plugin ---------------------------------------------------
if ! docker compose version >/dev/null 2>&1; then
    warn "Docker Compose plugin not found, installing..."
    case "$PKG_MANAGER" in
        apt) pkg_install docker-compose-plugin ;;
        dnf|yum) pkg_install docker-compose-plugin ;;
        *)
            COMPOSE_VER="$(curl -sSL https://api.github.com/repos/docker/compose/releases/latest \
                | grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')"
            mkdir -p /usr/local/lib/docker/cli-plugins
            curl -sSL "https://github.com/docker/compose/releases/download/${COMPOSE_VER}/docker-compose-${OS}-${ARCH}" \
                -o /usr/local/lib/docker/cli-plugins/docker-compose >/dev/null 2>&1
            chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
            ;;
    esac
    docker compose version >/dev/null 2>&1 || error "Docker Compose installation failed"
    info "Docker Compose installed"
else
    COMPOSE_VER="$(docker compose version --short 2>/dev/null)"
    info "Compose OK (${COMPOSE_VER})"
fi

# -- Docker Swarm init ----------------------------------------------------------
# Swarm is for the WORKLOADS Portiger manages (customer + system stacks).
# Traefik and Portiger themselves run as plain containers below.
step "Docker Swarm"
if docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null | grep -q 'active'; then
    info "Swarm already active"
else
    warn "Initializing Docker Swarm..."
    # Advertise on the primary non-loopback IPv4
    SWARM_ADDR="$(ip -4 addr show scope global | grep inet | head -1 | awk '{print $2}' | cut -d/ -f1)"
    if [ -n "$SWARM_ADDR" ]; then
        docker swarm init --advertise-addr "$SWARM_ADDR" >/dev/null 2>&1 \
            || error "Swarm init failed"
    else
        docker swarm init >/dev/null 2>&1 \
            || error "Swarm init failed"
    fi
    info "Swarm initialized"
fi

# -- Firewall ----------------------------------------------------------------
step "Firewall"

setup_ufw() {
    ufw --force enable >/dev/null 2>&1 || true
    ufw allow 80/tcp >/dev/null 2>&1
    ufw allow 443/tcp >/dev/null 2>&1
    ufw allow 2377/tcp >/dev/null 2>&1
    ufw allow 7946/tcp >/dev/null 2>&1
    ufw allow 7946/udp >/dev/null 2>&1
    ufw allow 4789/udp >/dev/null 2>&1
    if [ "$MAIL_ENABLED" -eq 1 ]; then
        for port in 25 465 587 143 993 110 995; do
            ufw allow "${port}/tcp" >/dev/null 2>&1
        done
    fi
    info "UFW configured"
}

setup_firewalld() {
    systemctl enable firewalld --now >/dev/null 2>&1 || true
    for svc in http https; do
        firewall-cmd --permanent --add-service="$svc" >/dev/null 2>&1 || true
    done
    for port in 2377/tcp 7946/tcp 7946/udp 4789/udp; do
        firewall-cmd --permanent --add-port="$port" >/dev/null 2>&1 || true
    done
    if [ "$MAIL_ENABLED" -eq 1 ]; then
        for port in 25 465 587 143 993 110 995; do
            firewall-cmd --permanent --add-port="${port}/tcp" >/dev/null 2>&1 || true
        done
    fi
    firewall-cmd --reload >/dev/null 2>&1 || true
    info "firewalld configured"
}

setup_iptables() {
    warn "No firewall manager detected — Docker manages iptables directly"
    warn "Ensure your cloud provider's security group / external firewall allows:"
    printf "    80/tcp, 443/tcp, 2377/tcp, 7946/tcp+udp, 4789/udp"
    if [ "$MAIL_ENABLED" -eq 1 ]; then
        printf ", 25/tcp, 465/tcp, 587/tcp, 143/tcp, 993/tcp, 110/tcp, 995/tcp"
    fi
    printf "\n"
}

if command -v ufw >/dev/null 2>&1; then
    setup_ufw
    warn "Docker bypasses UFW rules via iptables. If you see unexpected open ports,"
    warn "this is expected Docker behaviour — service ports are published via iptables NAT."
elif command -v firewall-cmd >/dev/null 2>&1; then
    setup_firewalld
else
    setup_iptables
fi

# ─────────────────────────────────────────────────────────────────────────────
# Faz 3: Directories + permissions
# ─────────────────────────────────────────────────────────────────────────────

step "Directory structure"

PORTIGER_HOME="/opt/portiger"
TRAEFIK_DIR="${PORTIGER_HOME}/traefik"
PORTIGER_DIR="${PORTIGER_HOME}/portiger"

# System-stack data base (host side). Portiger gets this mounted at the SAME
# path so the bind sources it computes for system stacks stay valid host paths.
HOST_DATA_DIR="${HOME}/.portiger"

mkdir -p "$PORTIGER_HOME" "$TRAEFIK_DIR" "$PORTIGER_DIR"
mkdir -p "${HOST_DATA_DIR}/data/portiger"
chmod 700 "$PORTIGER_HOME"
chmod 700 "$HOST_DATA_DIR"
info "Created ${PORTIGER_HOME}/"
info "Created ${HOST_DATA_DIR}/ (system-stack data)"

# ─────────────────────────────────────────────────────────────────────────────
# Faz 4: Network
# ─────────────────────────────────────────────────────────────────────────────

step "Docker network"

if docker network inspect "$NET_NAME" >/dev/null 2>&1; then
    info "Network '${NET_NAME}' already exists"
else
    docker network create \
        --driver overlay \
        --attachable \
        "$NET_NAME" >/dev/null 2>&1 \
        || error "Failed to create overlay network '${NET_NAME}'"
    info "Network '${NET_NAME}' created (overlay, attachable)"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Faz 5: Compose + .env generation (Traefik + Portiger only)
# ─────────────────────────────────────────────────────────────────────────────

step "Generating configurations"

# -- Secrets (idempotent: never overwrite existing) ---------------------------
PORTIGER_SECRET=""
JWT_SECRET=""

if [ -f "${PORTIGER_DIR}/.env" ]; then
    info "Portiger .env already exists — keeping existing secrets"
    PORTIGER_SECRET="$(grep '^PORTIGER_SECRET=' "${PORTIGER_DIR}/.env" | cut -d= -f2-)"
    JWT_SECRET="$(grep '^JWT_SECRET=' "${PORTIGER_DIR}/.env" | cut -d= -f2-)"
else
    PORTIGER_SECRET="$(openssl rand -hex 32)"
    JWT_SECRET="$(openssl rand -hex 32)"
fi

# -- Traefik dashboard password (htpasswd) ------------------------------------
# Persisted in traefik/.env so re-runs print the password actually in use;
# a fresh random password is only generated on first install.
# Uses Apache apr1 format (supported by Traefik's basicauth middleware).
if [ -f "${TRAEFIK_DIR}/.env" ] && grep -q '^TRAEFIK_DASHBOARD_PASS=' "${TRAEFIK_DIR}/.env"; then
    TRAEFIK_DASHBOARD_PASS="$(grep '^TRAEFIK_DASHBOARD_PASS=' "${TRAEFIK_DIR}/.env" | cut -d= -f2-)"
elif [ -f "${TRAEFIK_DIR}/docker-compose.yml" ]; then
    # Legacy re-run: compose exists but the password was never persisted.
    TRAEFIK_DASHBOARD_PASS="(set on a previous run — delete traefik/docker-compose.yml + .env and re-run to rotate)"
else
    TRAEFIK_DASHBOARD_PASS="$(openssl rand -base64 12)"
fi
TRAEFIK_HTPASSWD="$(openssl passwd -apr1 "$TRAEFIK_DASHBOARD_PASS")"

# Emits the certresolver label for a router — or nothing in local mode.
# Without any router referencing "le", the ACME resolver stays inert and
# Traefik serves its default self-signed certificate.
certresolver_label() {
    if [ "$ACME_ENABLED" -eq 1 ]; then
        printf '      - "traefik.http.routers.%s.tls.certresolver=le"' "$1"
    fi
}

# -- Traefik compose (plain container — NOT a swarm service) -------------------
step "  Traefik"

if [ ! -f "${TRAEFIK_DIR}/docker-compose.yml" ]; then
    cat > "${TRAEFIK_DIR}/docker-compose.yml" <<TRAEFIK_COMPOSE
# Generated by Portiger install.sh — $(date -u +%Y-%m-%dT%H:%M:%SZ)
# Traefik — reverse proxy / edge router (plain container).
# Discovers BOTH plain containers (docker provider: Portiger, itself) and
# swarm services (swarm provider: customer + system stacks).

services:
  traefik:
    image: traefik:v3.7
    container_name: traefik
    restart: always
    command:
      # Plain-container provider (Portiger, dashboard)
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=${NET_NAME}"
      # Swarm provider (workloads deployed by Portiger)
      - "--providers.swarm=true"
      - "--providers.swarm.exposedbydefault=false"
      - "--providers.swarm.network=${NET_NAME}"
      # Entrypoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.websecure.address=:443"
      # Internal entrypoint for ping + metrics (container-internal, never published)
      - "--entrypoints.traefik.address=:8080"
      # ACME Let's Encrypt (inert unless a router references resolver "le")
      - "--certificatesresolvers.le.acme.email=${ACME_EMAIL}"
      - "--certificatesresolvers.le.acme.storage=/acme/acme.json"
      - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
      # Dashboard (no --api.insecure — routed via labels below)
      - "--api.dashboard=true"
      # Metrics for Portiger
      - "--metrics.prometheus=true"
      - "--metrics.prometheus.addrouterslabels=true"
      # Ping for healthcheck
      - "--ping=true"
      - "--log.level=INFO"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - acme_data:/acme
    networks:
      - ${NET_NAME}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(\`traefik.${DOMAIN}\`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.tls=true"
$(certresolver_label dashboard)
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.middlewares=dashboard-auth@docker"
      - "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:${TRAEFIK_HTPASSWD}"
      # Dummy port: provider requires a service port even though the dashboard
      # router targets api@internal (silences "port is missing")
      - "traefik.http.services.dashboard.loadbalancer.server.port=8080"
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/ping"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s

volumes:
  acme_data:

networks:
  ${NET_NAME}:
    external: true
    name: ${NET_NAME}
TRAEFIK_COMPOSE

    chmod 600 "${TRAEFIK_DIR}/docker-compose.yml"
    info "  traefik/docker-compose.yml created"
else
    info "  traefik/docker-compose.yml already exists — skipping"
fi

# Traefik .env
if [ ! -f "${TRAEFIK_DIR}/.env" ]; then
    cat > "${TRAEFIK_DIR}/.env" <<TRAEFIK_ENV
# Traefik configuration — generated by Portiger install.sh
ACME_EMAIL=${ACME_EMAIL}
DOMAIN=${DOMAIN}
NET_NAME=${NET_NAME}
TRAEFIK_DASHBOARD_PASS=${TRAEFIK_DASHBOARD_PASS}
TRAEFIK_ENV
    chmod 600 "${TRAEFIK_DIR}/.env"
fi

# -- Portiger compose (plain container — NOT a swarm service) ------------------
step "  Portiger"

if [ ! -f "${PORTIGER_DIR}/docker-compose.yml" ]; then
    cat > "${PORTIGER_DIR}/docker-compose.yml" <<PORTIGER_COMPOSE
# Generated by Portiger install.sh — $(date -u +%Y-%m-%dT%H:%M:%SZ)
# Portiger — control plane (plain container). Deploys workloads onto swarm.

services:
  portiger:
    image: portiger/portiger:dev
    container_name: portiger
    restart: always
    environment:
      LISTEN_ADDR: ":8080"
      PORTIGER_SECRET: "${PORTIGER_SECRET}"
      JWT_SECRET: "${JWT_SECRET}"
      JWT_TTL: "2h"
      DB_DSN: "file:/data/portiger.db?_pragma=foreign_keys(1)&_pragma=busy_timeout(5000)"
      SEED_ADMIN_USERNAME: "${ADMIN_EMAIL}"
      SEED_ADMIN_PASSWORD: "${ADMIN_PASSWORD}"
      PUBLIC_URL: "https://portiger.${DOMAIN}"
      LOG_LEVEL: "info"
      LOG_FORMAT: "json"
      FORCE_TIER: "MAX"
      METRICS_TRAEFIK_URL: "http://traefik:8080/metrics"
      PORTAL_URL: ""
      # System-stack bootstrap: Portiger installs these itself once healthy,
      # before first login (idempotent; failures surface in the panel).
      BOOTSTRAP_MAIL: "${MAIL_ENABLED}"
      # Host-side base for system-stack bind mounts (mounted at same path)
      PORTIGER_DATA_DIR: "${HOST_DATA_DIR}"
      DOMAIN: "${DOMAIN}"
      PORTIGER_NET: "${NET_NAME}"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${HOST_DATA_DIR}:${HOST_DATA_DIR}
      - ${HOST_DATA_DIR}/data/portiger:/data
    networks:
      - ${NET_NAME}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portiger.rule=Host(\`portiger.${DOMAIN}\`)"
      - "traefik.http.routers.portiger.entrypoints=websecure"
      - "traefik.http.routers.portiger.tls=true"
$(certresolver_label portiger)
      - "traefik.http.services.portiger.loadbalancer.server.port=8080"

networks:
  ${NET_NAME}:
    external: true
    name: ${NET_NAME}
PORTIGER_COMPOSE

    chmod 600 "${PORTIGER_DIR}/docker-compose.yml"
    info "  portiger/docker-compose.yml created"
else
    info "  portiger/docker-compose.yml already exists — skipping"
fi

# Portiger .env
if [ ! -f "${PORTIGER_DIR}/.env" ]; then
    cat > "${PORTIGER_DIR}/.env" <<PORTIGER_ENV
# Portiger configuration — generated by Portiger install.sh
# WARNING: Do not lose PORTIGER_SECRET — DB field encryption depends on it.
PORTIGER_SECRET=${PORTIGER_SECRET}
JWT_SECRET=${JWT_SECRET}
SEED_ADMIN_USERNAME=${ADMIN_EMAIL}
SEED_ADMIN_PASSWORD=${ADMIN_PASSWORD}
PUBLIC_URL=https://portiger.${DOMAIN}
FORCE_TIER=MAX
DOMAIN=${DOMAIN}
NET_NAME=${NET_NAME}
BOOTSTRAP_MAIL=${MAIL_ENABLED}
PORTIGER_DATA_DIR=${HOST_DATA_DIR}
PORTIGER_ENV
    chmod 600 "${PORTIGER_DIR}/.env"
    info "  portiger/.env created"
else
    info "  portiger/.env already exists — skipping"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Faz 6: Start control plane (docker compose up)
# ─────────────────────────────────────────────────────────────────────────────

step "Starting control plane"

compose_up() {
    NAME="$1"; DIR="$2"
    printf "  Starting %s..." "$NAME"
    if docker compose --project-directory "$DIR" up -d >>"$INSTALL_LOG" 2>&1; then
        printf " ${GREEN}OK${RESET}\n"
    else
        printf " ${RED}FAILED${RESET}\n"
        warn "  Check: docker compose --project-directory ${DIR} logs"
    fi
}

# Wait until a container is running (and healthy, if it defines a healthcheck)
wait_container() {
    CNAME="$1"; CLABEL="$2"; CTRIES="${3:-45}"
    printf "  Waiting for %s" "$CLABEL"
    i=1
    while [ "$i" -le "$CTRIES" ]; do
        CSTATE="$(docker inspect -f '{{.State.Status}}{{if .State.Health}}:{{.State.Health.Status}}{{end}}' "$CNAME" 2>/dev/null || true)"
        if [ "$CSTATE" = "running" ] || [ "$CSTATE" = "running:healthy" ]; then
            printf " ${GREEN}OK${RESET}\n"
            return 0
        fi
        printf "."
        sleep 2
        i=$((i + 1))
    done
    printf " ${YELLOW}TIMEOUT${RESET}\n"
    warn "  ${CLABEL} is not healthy yet — check: docker logs ${CNAME}"
    return 0
}

compose_up "traefik"  "$TRAEFIK_DIR"
wait_container "traefik" "Traefik"

compose_up "portiger" "$PORTIGER_DIR"
wait_container "portiger" "Portiger"

if [ "$MAIL_ENABLED" -eq 1 ]; then
    info "Mail server: Portiger will install Stalwart as a system stack shortly"
    info "  Admin password is generated by Portiger — see the Mail page after login"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Faz 7: Mesh proxy warm (pull-only)
# ─────────────────────────────────────────────────────────────────────────────

step "Mesh proxy (warm)"
if docker image inspect portiger/mesh-proxy:v1 >/dev/null 2>&1; then
    info "portiger/mesh-proxy:v1 already present locally"
elif docker pull portiger/mesh-proxy:v1 >/dev/null 2>&1; then
    info "portiger/mesh-proxy:v1 pulled (warmed, not launched)"
else
    warn "portiger/mesh-proxy:v1 not found locally and pull failed — mesh will not be available"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Faz 8: Health check
# ─────────────────────────────────────────────────────────────────────────────

step "Health check"

# Resolve locally so the check works even before public DNS exists.
printf "  Waiting for portiger..."
HEALTH_OK=0
for i in $(seq 1 30); do
    if curl -sk --connect-timeout 3 \
        --resolve "portiger.${DOMAIN}:443:127.0.0.1" \
        "https://portiger.${DOMAIN}/api/health" >/dev/null 2>&1; then
        HEALTH_OK=1
        printf " ${GREEN}OK${RESET}\n"
        break
    fi
    printf "."
    sleep 2
done

if [ "$HEALTH_OK" -eq 0 ]; then
    printf " ${YELLOW}TIMEOUT${RESET}\n"
    warn "Portiger is still starting — check: docker logs portiger"
    if [ "$ACME_ENABLED" -eq 1 ]; then
        warn "It may take a few minutes for Let's Encrypt certificates to be issued"
    fi
    warn "Try: curl -sk https://portiger.${DOMAIN}/api/health"
fi

# ─────────────────────────────────────────────────────────────────────────────
# Summary
# ─────────────────────────────────────────────────────────────────────────────

printf "\n"
printf "${BOLD}╔══════════════════════════════════════╗${RESET}\n"
printf "${BOLD}║   ${GREEN}Portiger is ready!${RESET}${BOLD}                  ║${RESET}\n"
printf "${BOLD}╚══════════════════════════════════════╝${RESET}\n\n"

printf "  ${BOLD}Portiger${RESET}     → ${BOLD}https://portiger.${DOMAIN}${RESET}\n"
printf "  ${BOLD}Traefik${RESET}     → ${BOLD}https://traefik.${DOMAIN}${RESET}\n"
printf "  ${BOLD}  Username:${RESET} admin\n"
printf "  ${BOLD}  Password:${RESET} ${TRAEFIK_DASHBOARD_PASS}\n"
printf "  ${BOLD}Admin${RESET}        → ${ADMIN_EMAIL}\n"
printf "  ${BOLD}Network${RESET}      → ${NET_NAME} (overlay, attachable)\n"

if [ "$MAIL_ENABLED" -eq 1 ]; then
    printf "  ${BOLD}Mail${RESET}         → installed by Portiger as a system stack\n"
    printf "  ${BOLD}Mail Admin${RESET}  → ${BOLD}https://mailserver.${DOMAIN}${RESET} (after install)\n"
    printf "  ${BOLD}  Password:${RESET} generated — see panel → Mail\n"
fi

printf "\n"
printf "  ${BOLD}Directories${RESET}\n"
printf "    /opt/portiger/traefik/     (compose + .env)\n"
printf "    /opt/portiger/portiger/    (compose + .env)\n"
printf "    ${HOST_DATA_DIR}/data/     (Portiger + system-stack data)\n"
printf "\n"

printf "  ${BOLD}Commands${RESET}\n"
printf "    docker ps                            # control-plane containers\n"
printf "    docker logs -f portiger              # Portiger logs\n"
printf "    docker stack ls                      # workloads (swarm)\n"
printf "    docker service ls                    # workload services\n"
printf "\n"

if [ "$MAIL_ENABLED" -eq 1 ]; then
    printf "  ${BOLD}DNS records required${RESET}\n"
    printf "    A   portiger.${DOMAIN}    → ${PUBLIC_IP:-<server-ip>}\n"
    printf "    A   traefik.${DOMAIN}     → ${PUBLIC_IP:-<server-ip>}\n"
    printf "    A   mailserver.${DOMAIN} → ${PUBLIC_IP:-<server-ip>}\n"
    printf "    A   mail.${DOMAIN}       → ${PUBLIC_IP:-<server-ip>}\n"
    printf "    MX  ${DOMAIN}            → mail.${DOMAIN}\n"
    printf "    SPF TXT record: v=spf1 mx -all\n"
    printf "    DKIM: generate via mailserver WebUI (Settings → DKIM)\n"
    printf "\n"
else
    printf "  ${BOLD}DNS records required${RESET}\n"
    printf "    A   portiger.${DOMAIN}  → ${PUBLIC_IP:-<server-ip>}\n"
    printf "    A   traefik.${DOMAIN}   → ${PUBLIC_IP:-<server-ip>}\n"
    printf "\n"
fi

printf "  ${BOLD}Verification${RESET}\n"
printf "    curl -sk https://portiger.${DOMAIN}/api/health\n"
printf "\n"

printf "${GREEN}✓ bitirildi${RESET}\n"
