# ============================================
# [[BlockInit_section_title]]
# ============================================

# install.sh - [[BlockInit_install_script_description]]
# [[BlockInit_script_version]] 0.2.12
# [[BlockInit_author]]
# [[BlockInit_description]]

# [[BlockInit_fail-fast]]
set -euo pipefail

# [[BlockInit_clear_trap]]
TEMP_FILES=()
cleanup() {
    local exit_code=$?
    for temp_file in "${TEMP_FILES[@]}"; do
        [[ -f "$temp_file" ]] && rm -f "$temp_file"
    done
    if [[ -f "$0" && "$0" == *"install.sh"* ]]; then
        rm -f "$0"
    fi
    exit $exit_code
}
trap cleanup EXIT

# ============================================
# CONFIGURATION VARIABLES
# ============================================

# Minimum server specifications
MIN_CPU=1
MIN_RAM_GB=1
MIN_DISK_GB=20

# Recommended server specifications
RECOMMENDED_CPU=2
RECOMMENDED_RAM_GB=4
RECOMMENDED_DISK_GB=40

# System allowances (percentages)
# RAM: 10% reserved for system needs
# Disk: 85% must be free (15% used by system)
RAM_TOLERANCE=0.9
DISK_FREE_THRESHOLD=0.85

# Domain (will be requested from user or passed via parameter)
DOMAIN=""
# Subdomain (optional parameter for control panel)
SUBDOMAIN=""
# Allowed IP addresses or subnets for UFW
ALLOW_IPS=()
# BASE64 string with JSON configuration
CONFIG_BASE64=""

# ============================================
# FUNCTIONS
# ============================================

# Colors for output
BOLD_RED='\033[1;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

# Message output function
log_info() {
    echo -e "${BLUE}[INFO]${NC} $1"
}

log_check() {
    echo -e "${CYAN}[CHECK]${NC} $1"
}

log_error() {
    echo -e "${BOLD_RED}[ERROR]${NC} $1" >&2
}

log_success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

log_warning() {
    echo -e "${YELLOW}[WARNING]${NC} $1"
}

# Function to check root privileges
check_root() {
    log_check "Checking root privileges"
    
    if [[ $EUID -ne 0 ]]; then
        log_error "Script must be run as root (sudo)"
        exit 1
    fi
    
    log_success "Root privileges confirmed"
}

# Function to determine OS
check_os() {
    log_check "Determining operating system"
    
    # Checking for /etc/os-release
    if [[ ! -f /etc/os-release ]]; then
        log_error "File /etc/os-release not found. Cannot determine OS."
        exit 1
    fi
    
    # Loading OS information
    source /etc/os-release
    
    # Checking OS name
    if [[ "$ID" != "ubuntu" ]]; then
        log_error "Ubuntu expected. Detected: $ID"
        exit 1
    fi
    
    # Checking OS version (24.04 or 26.04)
    if [[ "$VERSION_ID" != "24.04" && "$VERSION_ID" != "26.04" ]]; then
        log_error "Only Ubuntu 24.04 and 26.04 are supported. Detected: $VERSION_ID"
        exit 1
    fi
    
    log_success "Ubuntu detected: $VERSION_ID"
}

# Function to check server specifications
check_hardware() {
    log_check "Checking server characteristics"
    
    local has_warning=false
    local has_critical=false
    
    # Checking CPU
    local cpu_count=$(nproc 2>/dev/null || echo "1")
    if [[ $cpu_count -lt $MIN_CPU ]]; then
        log_error "Minimum $MIN_CPU CPU cores required, detected: $cpu_count"
        has_critical=true
    elif [[ $cpu_count -lt $RECOMMENDED_CPU ]]; then
        log_warning "Recommended $RECOMMENDED_CPU CPU cores, detected: $cpu_count"
        has_warning=true
    fi
    
    # Checking RAM (in GB) with 10% allowance for system needs
    local ram_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    local ram_gb=$(awk "BEGIN {printf \"%.1f\", $ram_kb / 1024 / 1024}")
    
    # Applying tolerance: at least 90% of required value should be available
    local min_ram_check=$(awk "BEGIN {printf \"%.1f\", $MIN_RAM_GB * $RAM_TOLERANCE}")
    local recommended_ram_check=$(awk "BEGIN {printf \"%.1f\", $RECOMMENDED_RAM_GB * $RAM_TOLERANCE}")
    
    local ram_check=$(awk "BEGIN {print ($ram_gb >= $min_ram_check) ? 1 : 0}")
    local recommended_check=$(awk "BEGIN {print ($ram_gb >= $recommended_ram_check) ? 1 : 0}")
    
    if [[ $ram_check -eq 0 ]]; then
        log_error "Minimum $MIN_RAM_GB GB RAM required, detected: ${ram_gb}Гб"
        has_critical=true
    elif [[ $recommended_check -eq 0 ]]; then
        log_warning "Recommended $RECOMMENDED_RAM_GB GB RAM, detected: ${ram_gb}Гб"
        has_warning=true
    fi
    
    # Checking disk (in GB) with 15% allowance for system needs
    # Checking free space percentage on root partition
    local disk_total=$(df -BG / | awk 'NR==2 {gsub("G","",$2); print $2}')
    local disk_used=$(df -BG / | awk 'NR==2 {gsub("G","",$3); print $3}')
    local disk_free_percent
    
    # Calculating free space percentage
    if [[ $disk_total -gt 0 ]]; then
        disk_free_percent=$(awk "BEGIN {printf \"%.2f\", ($disk_total - $disk_used) / $disk_total * 100}")
    else
        disk_free_percent=0
    fi
    
    # Checking if free space is sufficient (at least 85%)
    local min_disk_with_tolerance=$(awk "BEGIN {printf \"%.0f\", $MIN_DISK_GB * $DISK_FREE_THRESHOLD + 0.5}")
    local recommended_disk_with_tolerance=$(awk "BEGIN {printf \"%.0f\", $RECOMMENDED_DISK_GB * $DISK_FREE_THRESHOLD + 0.5}")
    local min_disk_free_percent=$(awk "BEGIN {printf \"%.0f\", $DISK_FREE_THRESHOLD * 100}")
    
    # First checking free space percentage
    local size_check=$(awk "BEGIN {print ($disk_total >= $min_disk_with_tolerance) ? 1 : 0}")
    local free_check=$(awk "BEGIN {print ($disk_free_percent >= $min_disk_free_percent) ? 1 : 0}")
    
    if [[ $size_check -eq 0 || $free_check -eq 0 ]]; then
        if [[ $size_check -eq 0 ]]; then
            log_error "Minimum $MIN_DISK_GB GB free space required (with 15% allowance for system), detected: ${disk_total}Гб"
        fi
        if [[ $free_check -eq 0 ]]; then
            log_error "[[Functions_disk_free_space]]${disk_free_percent}% [[Functions_disk_free_needed]]${min_disk_free_percent}%"
        fi
        has_critical=true
    else
        local recommended_size_ok=$(awk "BEGIN {print ($disk_total >= $recommended_disk_with_tolerance) ? 1 : 0}")
        
        if [[ $recommended_size_ok -eq 0 ]]; then
            log_warning "Recommended $RECOMMENDED_DISK_GB GB free space, detected: ${disk_total}Гб"
            has_warning=true
        fi
    fi
    
    # Processing check results
    if [[ "$has_critical" == "true" ]]; then
        log_error "Server characteristics below minimum requirements!"
        read -rp "Continue installation? (yes/no): " confirm < /dev/tty
        confirm=${confirm,,} 
        if [[ "$confirm" != "yes" && "$confirm" != "y" ]]; then
            log_info "Installation canceled by user"
            exit 0
        fi
    elif [[ "$has_warning" == "true" ]]; then
        log_warning "Server characteristics below recommended but meet minimum requirements"
        read -rp "Continue installation? (yes/no): " confirm < /dev/tty
        confirm=${confirm,,} 
        if [[ "$confirm" != "yes" && "$confirm" != "y" ]]; then
            log_info "Installation canceled by user"
            exit 0
        fi
    fi
    
    log_success "Basic check completed successfully"
}

# ============================================
# SystemUpdate
# ============================================

# Function to check system updates
check_updates() {
    log_check "Checking system updates"
    
    # Getting the number of available updates
    local updates_count
    updates_count=$(apt list --upgradable 2>/dev/null | grep -c "upgradable" || echo "0")
    
    if [[ "$updates_count" -gt 0 ]]; then
        log_warning "Updates available: $updates_count"
        read -rp "Proceed with system upgrade? (yes/no): " confirm < /dev/tty
        confirm=${confirm,,} 
        if [[ "$confirm" != "yes" && "$confirm" != "y" ]]; then
            log_info "Installation canceled by user"
            return 0
        else
            return 1
        fi
    else
        log_success "System is up to date, no updates available"
        return 1
    fi
}

# Function to upgrade system with progress
upgrade_system() {
    log_info "Starting system upgrade"
    
        
    DEBIAN_FRONTEND=noninteractive apt-get update -y > /dev/null 2>&1
    
    log_info "Running apt-get upgrade"
    DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -o APT::Status-Fd=1 2>/dev/null | while IFS= read -r line; do
        if [[ $line == pmstatus:* ]]; then
            local percent=$(echo "$line" | cut -d: -f3)
            
            # Determining terminal width
            # If tput fails for some reason, default to 80
            local cols=$(tput cols 2>/dev/null || echo 80)
            
            # Calculating bar size
            # Subtract approximately 25-30 characters for [PROGRESS] prefix, brackets and percentages
            local reserved=25
            local bar_size=$(( cols - reserved ))
            
            # Protection against too small screen
            [[ $bar_size -lt 10 ]] && bar_size=10
            
            # Drawing the bar
            local filled=$(( percent * bar_size / 100 ))
            local empty=$(( bar_size - filled ))
            
            local fill_chars=$(printf "%${filled}s" | tr ' ' '#')
            local empty_chars=$(printf "%${empty}s" | tr ' ' '.')
            
            # Output the bar, clearing the line via \r and \e
            printf "\r${BLUE}[PROGRESS]${NC} [%s%s] %d%%" "$fill_chars" "$empty_chars" "$percent"
        fi
    done
    # Full line cleanup before final message
    printf "\r\033[K"
        
    log_success "System successfully upgraded"
}

# Function to install jq and ufw
install_required_packages() {
    log_check "Installing required packages"
    
    # Updating package list before installation
    log_info "Running apt-get update"
    if ! apt-get update > /dev/null 2>&1; then
        log_error "Failed to update package list"
        exit 1
    fi
    
    # Installing jq
    if command -v jq > /dev/null 2>&1; then
        log_info "jq already installed"
    else
        log_info "Installing package: jq"
        if ! apt-get install -y jq > /dev/null 2>&1; then
            log_error "Failed to install package: jq"
            exit 1
        fi
        log_success "Package installed: jq"
    fi
    
    # Installing ufw
    if command -v ufw > /dev/null 2>&1; then
        log_info "ufw already installed"
    else
        log_info "Installing package: ufw"
        if ! apt-get install -y ufw > /dev/null 2>&1; then
            log_error "Failed to install package: ufw"
            exit 1
        fi
        log_success "Package installed: ufw"
    fi
}

# ============================================
# ParameterParsing
# ============================================

# Function to decode and parse Base64 JSON
parse_config_params() {
    log_check "Parsing configuration parameters"
    
    # Getting command line parameters
    if [[ $# -gt 0 ]]; then
        CONFIG_BASE64="$1"
    fi
    
    # Initializing configuration variables
    DOMAIN=""
    SUBDOMAIN=""
    ALLOW_IPS=()
    
    # If Base64 string is passed, try to parse JSON
    if [[ -n "$CONFIG_BASE64" ]]; then
        # Decoding Base64
        local decoded_json
        decoded_json=$(echo "$CONFIG_BASE64" | base64 -d 2>/dev/null) || true
        
        # Checking if JSON is valid
        if [[ -n "$decoded_json" ]] && echo "$decoded_json" | jq empty 2>/dev/null; then
            # Extracting DOMAIN from JSON
            local json_domain
            json_domain=$(echo "$decoded_json" | jq -r '.domain // empty' 2>/dev/null) || true
            if [[ -n "$json_domain" ]]; then
                DOMAIN="$json_domain"
                log_success "Domain loaded from configuration: $DOMAIN"
            else
                log_info "Domain not found in configuration, using default value"
            fi
            
            # Extracting subdomain from JSON
            local json_subdomain
            json_subdomain=$(echo "$decoded_json" | jq -r '.subdomain // empty' 2>/dev/null) || true
            if [[ -n "$json_subdomain" ]]; then
                SUBDOMAIN="$json_subdomain"
                log_info "Subdomain loaded from configuration: $SUBDOMAIN"
            fi
            
            # Extracting allowed IP addresses from JSON
            local allow_ip_count
            allow_ip_count=$(echo "$decoded_json" | jq '.allow_ip | length' 2>/dev/null) || echo "0"
            if [[ "$allow_ip_count" -gt 0 ]]; then
                ALLOW_IPS=()
                local i=0
                while [[ $i -lt $allow_ip_count ]]; do
                    local ip
                    ip=$(echo "$decoded_json" | jq -r ".allow_ip[$i]" 2>/dev/null) || true
                    if [[ -n "$ip" ]]; then
                        ALLOW_IPS+=("$ip")
                    fi
                    ((i++))
                done
                log_info "Allowed IP addresses loaded from configuration: ${ALLOW_IPS[*]}"
            fi
        else
            log_warning "Invalid Base64 format of configuration string"
        fi
    else
        log_info "Configuration not provided, using default values"
    fi
    
    log_success "Configuration parameters processed"
}

# Function to configure UFW
setup_ufw() {
    log_check "Configuring UFW"
    
    # Checking if there are active UFW rules
    local ufw_status
    ufw_status=$(ufw status 2>/dev/null | head -1) || true
    
    if [[ "$ufw_status" == *"active"* ]]; then
        log_warning "Warning! Existing UFW rules will be reset!"
    fi
    
    # Resetting all rules
    ufw --force reset > /dev/null 2>&1
    
    # Denying all incoming connections by default
    ufw --force default deny incoming > /dev/null 2>&1
    
    # Allowing all outgoing connections by default
    ufw --force default allow outgoing > /dev/null 2>&1
    
    # Allowing http, https, ssh only from specified IP addresses
    # If ALLOW_IPS is not empty, allow only for specified IP addresses
    if [[ ${#ALLOW_IPS[@]} -gt 0 ]]; then
        log_info "Allowing http, https, ssh only from specified IP addresses"
        for ip in "${ALLOW_IPS[@]}"; do
            if [[ -n "$ip" ]]; then
                ufw --force allow from "$ip" to any port 80 > /dev/null 2>&1
                ufw --force allow from "$ip" to any port 443 > /dev/null 2>&1
                ufw --force allow from "$ip" to any port 22 > /dev/null 2>&1
                log_info "Allowed IP address for ports: $ip (http, https, ssh)"
            fi
        done
        log_info "Only specified IP addresses have access to http, https, ssh"
    else
        log_info "Allowing http, https, ssh for everyone (ALLOW_IPS not specified)"
        ufw --force allow http > /dev/null 2>&1
        ufw --force allow https > /dev/null 2>&1
        ufw --force allow ssh > /dev/null 2>&1
    fi
    
    # Enabling UFW
    ufw --force enable > /dev/null 2>&1
    
    log_success "UFW successfully configured"
}

# ============================================
# Docker
# ============================================

# Installing Docker from official source
install_docker() {
    log_check "Installing Docker"
    
    # Checking for Docker
    if command -v docker > /dev/null 2>&1; then
        log_info "Docker is already installed"
        log_success "Docker successfully installed"
        return 0
    fi
    
    # Installing required dependencies
    log_info "Installing required dependencies"
    apt-get update > /dev/null 2>&1
    apt-get install -y ca-certificates curl > /dev/null 2>&1
    
    # Adding Docker GPG key
    log_info "Adding Docker GPG key"
    install -m 0755 -d /etc/apt/keyrings > /dev/null 2>&1
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc > /dev/null 2>&1
    chmod a+r /etc/apt/keyrings/docker.asc > /dev/null 2>&1
    
    # Adding Docker repository
    log_info "Adding Docker repository"
    # Determine Ubuntu codename (supports both new and old Ubuntu versions)
    local ubuntu_codename
    ubuntu_codename=$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
    tee /etc/apt/sources.list.d/docker.sources > /dev/null << EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: ${ubuntu_codename}
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
    
    # Installing Docker packages
    log_info "Installing Docker packages"
    apt-get update > /dev/null 2>&1
    apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin > /dev/null 2>&1
    
    # Enabling and starting Docker
    log_info "Enabling and starting Docker"
    systemctl enable docker > /dev/null 2>&1
    systemctl start docker > /dev/null 2>&1
    
    # Configuring Docker daemon
    log_info "Configuring Docker daemon"
    
    # Create Docker daemon config with proper settings
    mkdir -p /etc/docker
    cat > /etc/docker/daemon.json << 'DOCKEREOF'
{
    "log-driver": "json-file",
    "log-opts": {
        "max-size": "10m",
        "max-file": "3"
    },
    "storage-driver": "overlay2"
}
DOCKEREOF
    
    # Reload systemd and restart docker
    systemctl daemon-reload > /dev/null 2>&1
    systemctl restart docker > /dev/null 2>&1
    
    log_success "Docker successfully installed"
    log_success "Docker Compose successfully installed"
}

# ============================================
# CrowdSec
# ============================================

# Installing CrowdSec from official repository
install_crowdsec() {
    log_check "Installing CrowdSec"
    
    # Checking for CrowdSec
    if command -v crowdsec > /dev/null 2>&1; then
        log_info "CrowdSec is already installed"
        log_success "CrowdSec successfully installed"
        return 0
    fi
    
    # Install CrowdSec from official repository using official installer script
    log_info "Installing CrowdSec"
    
    # Run official installer script (this adds the repository)
    curl -s https://install.crowdsec.net | sh
    
    # Update package lists after repository was added
    apt-get update > /dev/null 2>&1
    
    # Install CrowdSec
    apt-get install -y crowdsec > /dev/null 2>&1
    
    # Verify installation
    if command -v crowdsec > /dev/null 2>&1; then
        log_success "CrowdSec successfully installed"
    else
        log_error "Failed to install package: crowdsec"
        exit 1
    fi
}

# Installing crowdsec-firewall-bouncer-nftables
install_crowdsec_bouncer() {
    log_check "Installing crowdsec-firewall-bouncer-nftables"
    
    # Check if bouncer is already installed
    if command -v cs-firewall-bouncer > /dev/null 2>&1 || [[ -f /usr/local/bin/crowdsec-firewall-bouncer ]]; then
        log_info "Bouncer successfully installed"
        return 0
    fi
    
    # Install the nftables bouncer
    log_info "Installing crowdsec-firewall-bouncer-nftables"
    if ! apt-get install -y crowdsec-firewall-bouncer-nftables > /dev/null 2>&1; then
        log_error "Failed to install package: crowdsec-firewall-bouncer-nftables"
        exit 1
    fi
    
    # Configure the bouncer
    if [[ -f /etc/crowdsec/bouncers/crowdsec-firewall-bouncer-nftables.yaml ]]; then
        # Ensure bouncer is configured to use nftables
        log_info "Bouncer successfully installed"
    fi
    
    log_success "Bouncer successfully installed"
}

# Installing crowdsecurity/nginx collection
install_crowdsec_nginx_collection() {
    log_check "Installing crowdsecurity/nginx collection"
    
    # Install nginx collection using cscli
    if command -v cscli > /dev/null 2>&1; then
        cscli collections install crowdsecurity/nginx > /dev/null 2>&1
        log_success "Collection crowdsecurity/nginx installed"
    else
        log_error "cscli not installed"
        exit 1
    fi
}

# Configuring acquis.yaml to read logs from journalctl
configure_crowdsec_acquis() {
    log_check "Configuring acquis.yaml to read logs from journalctl"
    
    # Create acquis.d directory for modular configuration
    mkdir -p /etc/crowdsec/acquis.d
    
    # Copy acquisition config file from conf.d (or create if not exists)
    if [[ -f "/opt/silesco.io/compose/conf.d/silesco-nginx.yaml" ]]; then
        cp /opt/silesco.io/compose/conf.d/silesco-nginx.yaml /etc/crowdsec/acquis.d/silesco-nginx.yaml
    else
        # Fallback: create file inline if not found in conf.d
        cat > /etc/crowdsec/acquis.d/silesco-nginx.yaml << 'CROWDSEACQUIS'
# CrowdSec acquisition configuration
# Reading logs from journalctl for Docker nginx container
#

source: journalctl
journalctl_filter:
  - "_SYSTEMD_UNIT=docker.service"
  - "CONTAINER_TAG=nginx"
labels:
  type: nginx
CROWDSEACQUIS
    fi
    
    # Set proper permissions
    chmod 644 /etc/crowdsec/acquis.d/silesco-nginx.yaml
    
    # Reload CrowdSec to apply new configuration
    if command -v crowdsec > /dev/null 2>&1; then
        systemctl reload crowdsec > /dev/null 2>&1 || true
    fi
    
    log_success "acquis.yaml configured"
}

# Running CrowdSec tests
run_crowdsec_tests() {
    log_check "Running CrowdSec tests"
    
    # Register with CAPI (Central API) first
    if command -v cscli > /dev/null 2>&1; then
        log_info "Registering with CAPI"
        cscli capi register > /dev/null 2>&1 || true
        systemctl reload crowdsec > /dev/null 2>&1 || true
    fi
    
    # Run CAPI test
    log_info "CAPI test"
    if command -v cscli > /dev/null 2>&1; then
        local capi_status
        capi_status=$(cscli capi status 2>&1) || true
        
        # Check for 1 occurrence of "successfully" and 3 occurrences of "enabled"
        local successfully_count
        local enabled_count
        successfully_count=$(echo "$capi_status" | grep -c "successfully" || echo "0")
        enabled_count=$(echo "$capi_status" | grep -c "enabled" || echo "0")
        
        if [[ "$successfully_count" -ge 1 && "$enabled_count" -ge 3 ]]; then
            log_success "CAPI test passed"
        else
            log_warning "CAPI test failed (successfully: $successfully_count, enabled: $enabled_count)"
        fi
    fi
    
    # Test ping endpoint (silesco.io/ping/)
    log_info "Testing ping endpoint (silesco.io/ping/1)"
    local ping_response
    ping_response=$(curl -s -w "%{http_code}" -o /dev/null "https://silesco.io/ping/1" 2>/dev/null || echo "000")
    if [[ "$ping_response" == "200" ]]; then
        log_success "Ping endpoint test passed (silesco.io/ping/1)"
    else
        log_warning "Ping endpoint test failed (silesco.io/ping/1) - HTTP: $ping_response"
    fi
    
    # Test SSH endpoint (silesco.io/ssh/)
    log_info "Testing SSH endpoint (silesco.io/ssh/)"
    local ssh_response
    ssh_response=$(curl -s -w "%{http_code}" -o /dev/null "https://silesco.io/ssh/" 2>/dev/null || echo "000")
    if [[ "$ssh_response" == "200" ]]; then
        log_success "SSH endpoint test passed (silesco.io/ssh/)"
    else
        log_warning "SSH endpoint test failed (silesco.io/ssh/) - HTTP: $ssh_response"
    fi
    
    log_success "All tests passed successfully"
}

# Main function entry point
main() {
    log_info "Starting server preparation script"
    
    check_root
    check_os
    check_hardware
    
    # Check and install updates (must be before parse_config_params as jq may be needed)
    if check_updates; then
        upgrade_system
    fi
    
    # Install required packages (jq is needed for parsing)
    install_required_packages
    
    # Parse command line parameters (jq is now installed)
    parse_config_params "$@"
    
    # Configure UFW
    setup_ufw
    
    # Install Docker and Docker Compose
    install_docker
    
    # Install and configure CrowdSec
    install_crowdsec
    install_crowdsec_bouncer
    install_crowdsec_nginx_collection
    configure_crowdsec_acquis
    run_crowdsec_tests
    
    log_success "Basic check completed successfully"
}

main "$@"
