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

# DockerCP public bootstrap installer
# Usage:
#   curl -sSL https://get.dockercp.com/install.sh | sudo bash
#
# Optional environment variables:
#   DOCKERCP_PACKAGE_URL
#   DOCKERCP_INSTALL_DIR
#   DOCKERCP_APP_URL
#   DOCKERCP_DOMAIN
#   DOCKERCP_ADMIN_NAME
#   DOCKERCP_ADMIN_EMAIL
#   DOCKERCP_ADMIN_PASSWORD
#   DOCKERCP_DB_NAME
#   DOCKERCP_DB_USER
#   DOCKERCP_DB_PASSWORD
#   DOCKERCP_OVERWRITE=1

DOCKERCP_VERSION="3.1.7"
PACKAGE_URL="${DOCKERCP_PACKAGE_URL:-https://get.dockercp.com/releases/dockercp-latest.zip}"
INSTALL_DIR="${DOCKERCP_INSTALL_DIR:-/var/www/dockercp}"
DB_NAME="${DOCKERCP_DB_NAME:-dockercp_app}"
DB_USER="${DOCKERCP_DB_USER:-dockercp_user}"
DB_PASSWORD="${DOCKERCP_DB_PASSWORD:-}"
ADMIN_NAME="${DOCKERCP_ADMIN_NAME:-DockerCP Admin}"
ADMIN_EMAIL="${DOCKERCP_ADMIN_EMAIL:-}"
ADMIN_PASSWORD="${DOCKERCP_ADMIN_PASSWORD:-}"
APP_URL="${DOCKERCP_APP_URL:-}"
DOMAIN="${DOCKERCP_DOMAIN:-}"
OVERWRITE="${DOCKERCP_OVERWRITE:-0}"

MIN_ADMIN_PASSWORD_LENGTH=10
CREDENTIALS_FILE="/root/.dockercp-credentials"
NGINX_CONF="/etc/nginx/conf.d/dockercp.conf"
WEB_USER="www-data"
WEB_GROUP="www-data"
STATE_MUTATED=0

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

log() { printf "%b\n" "${BLUE}DockerCP:${NC} $*"; }
ok() { printf "%b\n" "${GREEN}✓${NC} $*"; }
warn() { printf "%b\n" "${YELLOW}!${NC} $*"; }
die() { printf "%b\n" "${RED}Error:${NC} $*" >&2; exit 1; }

cleanup() {
    if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
        rm -rf "$TMP_DIR"
    fi
}

on_failure() {
    local exit_code=$?
    cleanup
    if [[ $exit_code -ne 0 && "$STATE_MUTATED" == "1" ]]; then
        printf "%b\n" "${RED}Installation did not complete.${NC}" >&2
        cat >&2 <<RECOVERY

The server may have been partially modified. To retry:
  DOCKERCP_OVERWRITE=1 bash install.sh
  (already-installed packages, the database, and the DB user are reused safely)

To fully remove a failed install:
  rm -rf ${INSTALL_DIR}
  rm -f ${NGINX_CONF} && nginx -t && systemctl reload nginx
  mysql -u root -e "DROP DATABASE IF EXISTS \\\`${DB_NAME}\\\`; DROP USER IF EXISTS '${DB_USER}'@'localhost';"
  rm -f ${CREDENTIALS_FILE}

Database credentials (if created) were saved to: ${CREDENTIALS_FILE}
RECOVERY
    fi
    exit "$exit_code"
}
trap on_failure EXIT

run() {
    log "$*"
    "$@"
}

require_root() {
    if [[ "${EUID}" -ne 0 ]]; then
        die "Please run this installer as root or with sudo."
    fi
}

detect_os() {
    if [[ -f /etc/os-release ]]; then
        # shellcheck disable=SC1091
        source /etc/os-release
        OS_ID="${ID:-unknown}"
        OS_LIKE="${ID_LIKE:-}"
    else
        die "Unable to detect operating system."
    fi

    case "$OS_ID" in
        ubuntu|debian)
            PKG_FAMILY="apt"
            ;;
        almalinux|rocky|centos|rhel|fedora)
            PKG_FAMILY="dnf"
            ;;
        *)
            if [[ "$OS_LIKE" == *"debian"* ]]; then
                PKG_FAMILY="apt"
            elif [[ "$OS_LIKE" == *"rhel"* || "$OS_LIKE" == *"fedora"* ]]; then
                PKG_FAMILY="dnf"
            else
                die "Unsupported OS: ${OS_ID}. Supported: Ubuntu, Debian, AlmaLinux, Rocky Linux, CentOS/RHEL compatible systems."
            fi
            ;;
    esac

    ok "Detected OS: ${PRETTY_NAME:-$OS_ID} (${PKG_FAMILY})"
}

ask_value() {
    local var_name="$1"
    local prompt="$2"
    local default_value="${3:-}"
    local secret="${4:-0}"
    local current_value="${!var_name:-}"

    if [[ -n "$current_value" ]]; then
        return
    fi

    if [[ ! -e /dev/tty ]]; then
        if [[ -n "$default_value" ]]; then
            printf -v "$var_name" "%s" "$default_value"
            return
        fi

        die "Missing required value: ${var_name}. Re-run with ${var_name}=value before the command."
    fi

    if [[ "$secret" == "1" ]]; then
        while [[ -z "${!var_name:-}" ]]; do
            read -r -s -p "$prompt: " "$var_name" < /dev/tty
            printf "\n" > /dev/tty
        done
    else
        if [[ -n "$default_value" ]]; then
            read -r -p "$prompt [$default_value]: " "$var_name" < /dev/tty
            if [[ -z "${!var_name:-}" ]]; then
                printf -v "$var_name" "%s" "$default_value"
            fi
        else
            while [[ -z "${!var_name:-}" ]]; do
                read -r -p "$prompt: " "$var_name" < /dev/tty
            done
        fi
    fi
}

# ---------------------------------------------------------------------------
# Value collection + validation. EVERYTHING here runs BEFORE any package
# install, Docker install, database creation, file copy, or Nginx change.
# If any check fails, the server is untouched.
# ---------------------------------------------------------------------------

valid_email() {
    [[ "$1" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]
}

valid_identifier() {
    # Safe MySQL database/user name: letters, digits, underscore; 1-32 chars.
    [[ "$1" =~ ^[A-Za-z0-9_]{1,32}$ ]]
}

valid_url() {
    [[ "$1" =~ ^https?://[A-Za-z0-9.-]+(:[0-9]+)?(/.*)?$ ]]
}

valid_domain() {
    [[ "$1" =~ ^[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])?)+$ ]]
}

prompt_admin_password() {
    if [[ -n "$ADMIN_PASSWORD" ]]; then
        return
    fi

    if [[ ! -e /dev/tty ]]; then
        die "Missing required value: ADMIN_PASSWORD (DOCKERCP_ADMIN_PASSWORD). It must be at least ${MIN_ADMIN_PASSWORD_LENGTH} characters."
    fi

    local first="" second=""
    while true; do
        read -r -s -p "Admin password (min ${MIN_ADMIN_PASSWORD_LENGTH} chars): " first < /dev/tty
        printf "\n" > /dev/tty

        if (( ${#first} < MIN_ADMIN_PASSWORD_LENGTH )); then
            warn "Password too short (${#first} chars). Minimum is ${MIN_ADMIN_PASSWORD_LENGTH}." > /dev/tty
            continue
        fi

        read -r -s -p "Confirm admin password: " second < /dev/tty
        printf "\n" > /dev/tty

        if [[ "$first" != "$second" ]]; then
            warn "Passwords do not match. Try again." > /dev/tty
            continue
        fi

        ADMIN_PASSWORD="$first"
        break
    done
}

random_password() {
    tr -dc 'A-Za-z0-9_%+=.-' < /dev/urandom | head -c 24 || true
}

server_ip() {
    local ip
    ip="$(curl -fsSL --max-time 5 https://api.ipify.org 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || true)"
    printf "%s" "${ip:-127.0.0.1}"
}

prepare_values() {
    if [[ -z "$DB_PASSWORD" ]]; then
        DB_PASSWORD="$(random_password)"
        DB_PASSWORD_GENERATED=1
    else
        DB_PASSWORD_GENERATED=0
    fi

    if [[ -z "$APP_URL" ]]; then
        if [[ -n "$DOMAIN" ]]; then
            APP_URL="http://${DOMAIN}"
        else
            APP_URL="http://$(server_ip)"
        fi
    fi

    ask_value "ADMIN_EMAIL" "Admin email"
    prompt_admin_password
    ask_value "ADMIN_NAME" "Admin name" "$ADMIN_NAME"
}

validate_inputs() {
    local errors=()

    valid_email "$ADMIN_EMAIL" || errors+=("Admin email is invalid: ${ADMIN_EMAIL}")

    if (( ${#ADMIN_PASSWORD} < MIN_ADMIN_PASSWORD_LENGTH )); then
        errors+=("Admin password must be at least ${MIN_ADMIN_PASSWORD_LENGTH} characters long (got ${#ADMIN_PASSWORD}).")
    fi

    [[ -n "$ADMIN_NAME" ]] || errors+=("Admin name must not be empty.")

    valid_identifier "$DB_NAME" || errors+=("Database name must match [A-Za-z0-9_]{1,32}: ${DB_NAME}")
    valid_identifier "$DB_USER" || errors+=("Database username must match [A-Za-z0-9_]{1,32}: ${DB_USER}")

    if [[ "$DB_PASSWORD" == *$'\n'* ]]; then
        errors+=("Database password must not contain newlines.")
    fi

    valid_url "$APP_URL" || errors+=("Application URL must be a valid http(s) URL: ${APP_URL}")

    if [[ -n "$DOMAIN" ]] && ! valid_domain "$DOMAIN"; then
        errors+=("Domain is not a valid hostname: ${DOMAIN}")
    fi

    case "$INSTALL_DIR" in
        /*) : ;;
        *) errors+=("Install directory must be an absolute path: ${INSTALL_DIR}") ;;
    esac
    case "$INSTALL_DIR" in
        /|/bin|/boot|/dev|/etc|/lib|/proc|/root|/run|/sbin|/sys|/usr|/var)
            errors+=("Install directory is a protected system path: ${INSTALL_DIR}") ;;
    esac

    if [[ -d "$INSTALL_DIR" && "$(find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l)" -gt 0 && "$OVERWRITE" != "1" ]]; then
        errors+=("Install directory is not empty: ${INSTALL_DIR}. Re-run with DOCKERCP_OVERWRITE=1 to replace it (existing .env and storage/ are preserved).")
    fi

    if (( ${#errors[@]} > 0 )); then
        printf "%b\n" "${RED}Pre-flight validation failed. Nothing has been installed or changed.${NC}" >&2
        for e in "${errors[@]}"; do
            printf "  - %s\n" "$e" >&2
        done
        exit 1
    fi

    ok "Pre-flight validation passed"
    ok "Installation URL: $APP_URL"
}

check_required_commands() {
    local missing=()
    for cmd in curl; do
        command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
    done

    if (( ${#missing[@]} > 0 )); then
        die "Missing required commands: ${missing[*]}. Install them and re-run."
    fi

    if [[ "$PKG_FAMILY" == "apt" ]]; then
        command -v apt-get >/dev/null 2>&1 || die "apt-get not found on an apt-family OS."
    else
        command -v dnf >/dev/null 2>&1 || die "dnf not found on a dnf-family OS."
    fi
}

check_ports() {
    # Port 80 must be free or already owned by nginx.
    local holder
    holder="$(ss -ltnp 2>/dev/null | awk '$4 ~ /:80$/ {print $NF}' | head -n1 || true)"

    if [[ -n "$holder" && "$holder" != *"nginx"* ]]; then
        die "Port 80 is already in use by another process (${holder}). Free it before installing DockerCP."
    fi
}

# ---------------------------------------------------------------------------
# Mutating steps begin here.
# ---------------------------------------------------------------------------

apt_lock_held() {
    local lock
    for lock in /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock; do
        [[ -e "$lock" ]] || continue

        if command -v fuser >/dev/null 2>&1 && fuser "$lock" >/dev/null 2>&1; then
            return 0
        fi

        if command -v lslocks >/dev/null 2>&1 && lslocks -n -o PATH 2>/dev/null | grep -Fxq "$lock"; then
            return 0
        fi
    done

    return 1
}

wait_for_apt() {
    local timeout=300
    local interval=5
    local waited=0
    local announced=0

    while apt_lock_held; do
        if [[ "$announced" == "0" ]]; then
            log "Waiting for system package manager to become available..."
            announced=1
        fi

        if (( waited >= timeout )); then
            die "System package manager is still locked after ${timeout} seconds. Finish/stop unattended updates, then re-run the installer."
        fi

        sleep "$interval"
        waited=$((waited + interval))
    done
}

apt_run() {
    wait_for_apt
    run apt-get -o DPkg::Lock::Timeout=300 "$@"
}

install_packages_apt() {
    export DEBIAN_FRONTEND=noninteractive

    apt_run update -y
    apt_run install -y \
        ca-certificates curl wget unzip tar git rsync gnupg lsb-release software-properties-common \
        nginx mariadb-server \
        php-cli php-fpm php-mysql php-mbstring php-curl php-xml php-zip php-gd php-intl php-bcmath php-common
}

install_packages_dnf() {
    run dnf install -y dnf-plugins-core epel-release || true
    run dnf install -y \
        ca-certificates curl wget unzip tar git rsync nginx mariadb-server \
        php php-cli php-fpm php-mysqlnd php-mbstring php-curl php-xml php-zip php-gd php-intl php-bcmath php-common
}

install_system_packages() {
    STATE_MUTATED=1

    if [[ "$PKG_FAMILY" == "apt" ]]; then
        install_packages_apt
    else
        install_packages_dnf
    fi

    systemctl enable --now mariadb >/dev/null 2>&1 || systemctl enable --now mysql >/dev/null 2>&1 \
        || die "Failed to start MariaDB/MySQL service."
    systemctl enable --now nginx >/dev/null 2>&1 || die "Failed to start Nginx service."
    systemctl enable --now php-fpm >/dev/null 2>&1 || systemctl enable --now "php$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')-fpm" >/dev/null 2>&1 || true

    ok "System packages installed"
}

install_docker() {
    if command -v docker >/dev/null 2>&1; then
        ok "Docker CLI is already installed"
    else
        log "Installing Docker"
        curl -fsSL https://get.docker.com -o /tmp/dockercp-get-docker.sh
        sh /tmp/dockercp-get-docker.sh
        rm -f /tmp/dockercp-get-docker.sh
    fi

    systemctl enable --now docker >/dev/null 2>&1 || die "Failed to start Docker service."

    command -v docker >/dev/null 2>&1 || die "Docker CLI is not available after installation."

    if docker compose version >/dev/null 2>&1; then
        ok "Docker Compose plugin is available"
    elif command -v docker-compose >/dev/null 2>&1 && docker-compose --version >/dev/null 2>&1; then
        ok "Docker Compose standalone is available"
    else
        die "Docker Compose was not found after installation. Install the docker-compose-plugin package and re-run."
    fi

    docker network create dockercp-public >/dev/null 2>&1 || true

    # PHP-FPM runs DockerCP as the web user; allow it to access Docker.
    if getent group docker >/dev/null 2>&1 && id -u "$WEB_USER" >/dev/null 2>&1; then
        usermod -aG docker "$WEB_USER" || true
    fi
}

sql_escape() {
    # Escape a value for use inside single quotes in MySQL:
    # backslash first, then single quote.
    local v="$1"
    v="${v//\\/\\\\}"
    v="${v//\'/\\\'}"
    printf "%s" "$v"
}

create_database() {
    log "Creating database and database user"

    # DB_NAME / DB_USER already validated against ^[A-Za-z0-9_]{1,32}$
    local esc_password
    esc_password="$(sql_escape "$DB_PASSWORD")"

    mysql -u root <<SQL
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${esc_password}';
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${esc_password}';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL

    # Persist credentials immediately so a later failure never loses a
    # generated password.
    umask 077
    cat > "$CREDENTIALS_FILE" <<CREDS
# DockerCP database credentials (generated $(date -Is))
DB_NAME=${DB_NAME}
DB_USER=${DB_USER}
DB_PASSWORD=${DB_PASSWORD}
CREDS
    chmod 600 "$CREDENTIALS_FILE"
    umask 022

    ok "Database ready: ${DB_NAME} (credentials saved to ${CREDENTIALS_FILE})"
}

download_release() {
    TMP_DIR="$(mktemp -d)"
    PACKAGE_FILE="${TMP_DIR}/dockercp.zip"
    EXTRACT_DIR="${TMP_DIR}/extract"

    log "Downloading DockerCP package from ${PACKAGE_URL}"
    curl -fL "$PACKAGE_URL" -o "$PACKAGE_FILE"

    mkdir -p "$EXTRACT_DIR"
    unzip -q "$PACKAGE_FILE" -d "$EXTRACT_DIR"

    SOURCE_DIR="$(find "$EXTRACT_DIR" -type f -path "*/install/cli-install.php" -printf '%h\n' | sed 's#/install$##' | head -n 1)"

    if [[ -z "${SOURCE_DIR:-}" || ! -d "$SOURCE_DIR" ]]; then
        die "Downloaded package does not contain install/cli-install.php. Check DOCKERCP_PACKAGE_URL."
    fi

    # Package must never contain runtime/private files.
    local forbidden
    forbidden="$(cd "$SOURCE_DIR" && find . \( -name '.env' -o -name 'installed.lock' -o -name 'error_log' -o -name '*.zip' \) -type f 2>/dev/null | head -5 || true)"
    if [[ -n "$forbidden" ]]; then
        die "Downloaded package contains forbidden runtime files (${forbidden}). Refusing to install. Report this to DockerCP."
    fi

    ok "Package downloaded and verified"
}

install_files() {
    if [[ -d "$INSTALL_DIR" && "$(find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l)" -gt 0 && "$OVERWRITE" != "1" ]]; then
        die "Install directory is not empty: $INSTALL_DIR. Re-run with DOCKERCP_OVERWRITE=1 to replace it."
    fi

    mkdir -p "$INSTALL_DIR"

    # Preserve runtime state on re-install: never delete .env or storage/.
    rsync -a --delete \
        --exclude='/.env' \
        --exclude='/storage/' \
        "$SOURCE_DIR"/ "$INSTALL_DIR"/

    mkdir -p \
        "$INSTALL_DIR/storage" \
        "$INSTALL_DIR/storage/logs" \
        "$INSTALL_DIR/storage/cache" \
        "$INSTALL_DIR/storage/sessions" \
        "$INSTALL_DIR/storage/uploads" \
        "$INSTALL_DIR/storage/uploads/logos" \
        "$INSTALL_DIR/storage/uploads/blog" \
        "$INSTALL_DIR/storage/backups" \
        "$INSTALL_DIR/storage/snapshots"

    # 755 dirs / 644 files instead of blanket 755 on everything.
    find "$INSTALL_DIR" -type d -exec chmod 755 {} +
    find "$INSTALL_DIR" -type f -exec chmod 644 {} +
    chmod -R 775 "$INSTALL_DIR/storage"

    chown -R "$WEB_USER:$WEB_GROUP" "$INSTALL_DIR" 2>/dev/null || chown -R nginx:nginx "$INSTALL_DIR" 2>/dev/null || true

    # DockerCP writes generated compose files and operational state here.
    mkdir -p /etc/dockercp/projects /etc/dockercp/backups /etc/dockercp/logs
    chown -R "$WEB_USER:$WEB_GROUP" /etc/dockercp 2>/dev/null || true
    chmod -R u+rwX,g+rwX,o-rwx /etc/dockercp 2>/dev/null || true

    ok "Application files installed at $INSTALL_DIR"
}

run_application_installer() {
    local database_sql_path="install/database/database.sql"
    local check_output=""
    local install_output=""

    if [[ ! -f "$INSTALL_DIR/$database_sql_path" ]]; then
        database_sql_path="database.sql"
    fi

    if [[ ! -f "$INSTALL_DIR/$database_sql_path" ]]; then
        die "database.sql was not found in the installed package."
    fi

    log "Preparing DockerCP application"

    cd "$INSTALL_DIR"

    if ! check_output="$(php install/cli-install.php --check --database-sql-path="${database_sql_path}" 2>&1)"; then
        printf "%s\n" "$check_output" >&2
        die "Server requirement checks failed. Fix the reported items and re-run."
    fi

    if ! install_output="$(php install/cli-install.php \
        --app-url="${APP_URL}" \
        --db-host="localhost" \
        --db-port="3306" \
        --db-database="${DB_NAME}" \
        --db-username="${DB_USER}" \
        --db-password="${DB_PASSWORD}" \
        --admin-name="${ADMIN_NAME}" \
        --admin-email="${ADMIN_EMAIL}" \
        --admin-password="${ADMIN_PASSWORD}" \
        --admin-timezone="UTC" \
        --database-sql-path="${database_sql_path}" 2>&1)"; then
        printf "%s\n" "$install_output" >&2
        die "DockerCP application installer failed."
    fi

    # Idempotent bootstrap flags: only append once.
    if ! grep -q '^DOCKERCP_INSTALL_CHANNEL=' "$INSTALL_DIR/.env" 2>/dev/null; then
        {
            echo ""
            echo "DOCKERCP_DEFAULT_PLAN=free"
            echo "DOCKERCP_INSTALL_CHANNEL=bootstrap"
            echo "DOCKERCP_FREE_PLAN_ENABLED=true"
        } >> "$INSTALL_DIR/.env"
    fi

    # .env must not be public-readable, but PHP-FPM must be able to read it.
    chown root:"$WEB_GROUP" "$INSTALL_DIR/.env" 2>/dev/null || chown root:nginx "$INSTALL_DIR/.env" 2>/dev/null || true
    chmod 640 "$INSTALL_DIR/.env" 2>/dev/null || true

    chown -R "$WEB_USER:$WEB_GROUP" "$INSTALL_DIR/storage" 2>/dev/null || true
    chmod -R u+rwX,g+rwX,o-rwx "$INSTALL_DIR/storage" 2>/dev/null || true

    # Restart PHP-FPM after Docker group membership and file permissions.
    systemctl restart php-fpm >/dev/null 2>&1 \
        || systemctl restart "php$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')-fpm" >/dev/null 2>&1 \
        || systemctl restart php8.3-fpm >/dev/null 2>&1 || true

    ok "Application prepared"
}
detect_php_fpm_socket() {
    local socket

    # Debian/Ubuntu: /run/php/php8.3-fpm.sock - RHEL family: /run/php-fpm/www.sock
    socket="$(find /run/php /run/php-fpm -maxdepth 1 -type s -name '*.sock' 2>/dev/null | sort -V | tail -n 1 || true)"

    if [[ -n "$socket" ]]; then
        PHP_FPM_TARGET="unix:${socket}"
        return
    fi

    PHP_FPM_TARGET="127.0.0.1:9000"
}

nginx_has_existing_default_server_80() {
    local file

    for file in /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf /etc/nginx/sites-enabled/*; do
        [[ -f "$file" ]] || continue
        [[ "$file" != "$NGINX_CONF" ]] || continue

        if grep -Eq '^[[:space:]]*listen[[:space:]][^;]*80[^;]*default_server' "$file"; then
            return 0
        fi
    done

    return 1
}

configure_nginx() {
    detect_php_fpm_socket

    local server_names="_ localhost 127.0.0.1"
    if [[ -n "$DOMAIN" ]]; then
        server_names="$DOMAIN _ localhost 127.0.0.1"
    fi

    # Make DockerCP the default HTTP vhost for bare-IP installs. Ubuntu ships
    # /etc/nginx/sites-enabled/default as the default_server, which otherwise
    # catches http://SERVER-IP requests and returns the stock Nginx 404 page.
    rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf 2>/dev/null || true
    rm -f "$NGINX_CONF" 2>/dev/null || true

    local listen_directive="listen 80 default_server;"
    if nginx_has_existing_default_server_80; then
        listen_directive="listen 80;"
    fi

    # NOTE: filename must end in .conf or nginx's conf.d include never loads it.
    cat > "$NGINX_CONF" <<NGINX
server {
    ${listen_directive}
    server_name ${server_names};

    root ${INSTALL_DIR};
    index index.php index.html;

    client_max_body_size 100M;
    server_tokens off;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # ------------------------------------------------------------------
    # Deny rules first. Nginx does not read .htaccess: everything the
    # Apache rules protected must be denied here explicitly.
    # ------------------------------------------------------------------

    # Hidden dotfiles (.env, .git, .htaccess, .user.ini, ...)
    location ~ /\.(?!well-known) {
        deny all;
    }

    # Sensitive extensions anywhere in the tree
    location ~* \.(env|sql|sh|log|lock|bak|backup|old|swp|dist|ini|md|patch|zip|tar|gz|yml|yaml)\$ {
        deny all;
    }

    # Private directories
    location ^~ /storage/  { deny all; }
    location ^~ /includes/ { deny all; }
    location ^~ /config/   { deny all; }
    location ^~ /database/ { deny all; }
    location ^~ /nginx/    { deny all; }
    location ^~ /patches/  { deny all; }
    location ^~ /releases/ { deny all; }
    location ^~ /webhooks/ { deny all; }

    # The CLI installer already ran; the web installer must not be
    # publicly reachable on a customer server.
    location ^~ /install/ { deny all; }

    # Stray PHP error logs and extension-less build files
    location ~ /(error_log|Dockerfile)\$ { deny all; }

    # ------------------------------------------------------------------
    # Routing: serve real files, then clean URLs (/app/auth/login ->
    # /app/auth/login.php), then fall back to the front controller.
    # ------------------------------------------------------------------
    location / {
        try_files \$uri \$uri/ @clean_php;
    }

    location @clean_php {
        rewrite ^/(.+?)/?\$ /\$1.php last;
    }

    location ~ \.php\$ {
        try_files \$uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT \$document_root;
        fastcgi_param PHP_VALUE "session.cookie_httponly=1 \n session.use_strict_mode=1";
        fastcgi_pass ${PHP_FPM_TARGET};
    }
}
NGINX

    local nginx_test_output=""
    if ! nginx_test_output="$(nginx -t 2>&1)"; then
        rm -f "$NGINX_CONF"
        printf "%s
" "$nginx_test_output" >&2
        die "Generated Nginx config failed validation and was removed. Existing sites were not touched."
    fi

    # Reload if running; fall back to a full restart if the service is not
    # active (reload fails on an inactive unit).
    systemctl reload nginx >/dev/null 2>&1 || systemctl restart nginx >/dev/null 2>&1 \
        || die "Nginx failed to reload/restart after configuration. Run 'systemctl status nginx' and 'journalctl -u nginx' for details."

    ok "Web server configured"
}

configure_firewall() {
    # Open HTTP/HTTPS when a host firewall is already active (DigitalOcean
    # and other providers ship Ubuntu images with ufw enabled and only SSH
    # allowed, which makes the panel unreachable from the browser even
    # though the install succeeded). Never enables a firewall that the
    # server does not already use.
    if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
        ufw allow 80/tcp >/dev/null 2>&1 || true
        ufw allow 443/tcp >/dev/null 2>&1 || true
        ok "Firewall: HTTP (80) and HTTPS (443) allowed (ufw)"
    elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
        firewall-cmd --permanent --add-service=http >/dev/null 2>&1 || true
        firewall-cmd --permanent --add-service=https >/dev/null 2>&1 || true
        firewall-cmd --reload >/dev/null 2>&1 || true
        ok "Firewall: HTTP (80) and HTTPS (443) allowed (firewalld)"
    fi
}

php_fpm_status_for_diagnostics() {
    local unit
    local php_unit=""

    if command -v php >/dev/null 2>&1; then
        php_unit="php$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null)-fpm"
    fi

    for unit in php-fpm "$php_unit" php8.4-fpm php8.3-fpm php8.2-fpm php8.1-fpm php8.0-fpm; do
        [[ -n "$unit" ]] || continue
        if systemctl status "$unit" >/dev/null 2>&1; then
            systemctl --no-pager --full status "$unit" 2>&1 | sed -n '1,30p'
            return
        fi
    done

    systemctl --no-pager --full --type=service 2>/dev/null | grep -E 'php.*fpm' | sed -n '1,30p' || \
        echo "No active php-fpm systemd service was detected."
}

print_web_failure_diagnostics() {
    local code="$1"

    {
        echo
        echo "Web server diagnostics:"
        echo "  Nginx config: ${NGINX_CONF}"
        echo "  Login check: curl -I http://127.0.0.1/app/auth/login.php"
        echo "  Curl response code: ${code}"
        echo
        echo "Nginx status:"
        systemctl --no-pager --full status nginx 2>&1 | sed -n '1,30p' || true
        echo
        echo "PHP-FPM status:"
        php_fpm_status_for_diagnostics || true
        echo
    } >&2
}

verify_install() {
    # Silent on success. Fails only on real fatal issues.
    local login_url="http://127.0.0.1/app/auth/login.php"
    local code="000"
    local path

    if ! systemctl is-active --quiet nginx; then
        print_web_failure_diagnostics "$code"
        die "Nginx is not running after installation."
    fi

    if ! ss -ltn 2>/dev/null | awk 'NR > 1 {print $4}' | grep -Eq '(:|\*)80$'; then
        print_web_failure_diagnostics "$code"
        die "Nginx is not listening on port 80 after installation."
    fi

    # The production readiness check is the actual local login URL response.
    # Do not make nginx -T string-matching a fatal condition: Nginx can load
    # valid includes differently across distributions and images.
    code="$(curl -I -s -o /dev/null -w '%{http_code}' --max-time 10 "$login_url" || true)"
    if [[ "$code" != "200" ]]; then
        print_web_failure_diagnostics "$code"
        die "The DockerCP login page is not reachable after installation (HTTP ${code})."
    fi

    for path in "/.env" "/install/index.php" "/storage/installed.lock" "/install/database/database.sql"; do
        code="$(curl -I -s -o /dev/null -w '%{http_code}' --max-time 10 "http://127.0.0.1${path}" || true)"
        if [[ "$code" != "403" && "$code" != "404" ]]; then
            die "Sensitive path ${path} is publicly reachable (HTTP ${code}). Aborting for safety; review ${NGINX_CONF}."
        fi
    done
}

print_success() {
    printf "\n%bDockerCP installation completed successfully.%b\n\n" "$GREEN" "$NC"
    cat <<EOF
Login URL:
  ${APP_URL}/app/auth/login.php

Admin email:
  ${ADMIN_EMAIL}

Install directory:
  ${INSTALL_DIR}

Database credentials:
  ${CREDENTIALS_FILE}

Important:
  Point your domain DNS to this server IP before enabling SSL.

EOF
}
main() {
    require_root

    cat <<EOF
============================================================
 DockerCP Server Control Panel Installer
 Version: ${DOCKERCP_VERSION}
============================================================
EOF

    # ---- read-only phase: nothing on the server changes ----
    detect_os
    check_required_commands
    prepare_values
    validate_inputs
    check_ports

    # ---- mutating phase ----
    install_system_packages
    install_docker
    create_database
    download_release
    install_files
    run_application_installer
    configure_nginx
    configure_firewall
    verify_install
    print_success
}

main "$@"
