#!/bin/bash -xe

# This is a generic provisioning file
# It's currentl applied to all VM types (AMI's and OVA's)


# Function to handle script failures and prevent wizard launch
function handle_error {
    echo "Installation failed. Preventing wizard launch."
    # Remove wizard if it exists to prevent automatic launch
    rm -f "$HOME/npa_publisher_wizard" 2>/dev/null
    exit 1
}

# Set error trap to catch any failures
trap 'handle_error' ERR

# Suppress interactive prompts during apt operations (kernel restart dialogs, etc.)
export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a

# Accept platform detection results from command-line arguments (passed from bootstrap.sh)
# Arguments: $1=HARDENING_SSH $2=IS_RHEL $3=IS_BWAN $4=IS_CHINA
ARG_IS_RHEL="${2:-}"
ARG_IS_BWAN="${3:-}"
ARG_IS_CHINA="${4:-}"

function is_rhel {
    # Use argument if provided, otherwise detect directly
    if [ -n "$ARG_IS_RHEL" ]; then
        [ "$ARG_IS_RHEL" = "true" ]
    else
        [ -f /etc/redhat-release ]
    fi
}

function is_bwan {
    # Use argument if provided, otherwise detect directly
    if [ -n "$ARG_IS_BWAN" ]; then
        [ "$ARG_IS_BWAN" = "true" ]
    else
        [ -d /infroot ]
    fi
}

function is_china {
    # Use argument if provided, otherwise detect directly
    if [ -n "$ARG_IS_CHINA" ]; then
        [ "$ARG_IS_CHINA" = "true" ]
    else
        local user_home=$(eval echo ~${SUDO_USER:-$(logname)})
        [ -f "$user_home/.prc_dp" ]
    fi
}

function detect_cloud_provider {
    # Use dmidecode to detect cloud provider (avoiding external network calls)
    local system_vendor=""
    if command -v dmidecode &> /dev/null; then
        system_vendor=$(sudo dmidecode -s system-manufacturer 2>/dev/null | tr '[:upper:]' '[:lower:]')
    fi
    
    # Check for AWS
    if [[ "$system_vendor" == *"amazon"* ]] || [[ -f /sys/hypervisor/uuid && $(head -c 3 /sys/hypervisor/uuid 2>/dev/null) == "ec2" ]]; then
        echo "aws"
        return 0
    fi
    
    # Check for GCP - convert to lowercase for case-insensitive comparison
    if [[ "$system_vendor" == *"google"* ]]; then
        echo "gcp"
        return 0
    fi
    
    # Check for Azure
    if [[ "$system_vendor" == *"microsoft"* ]]; then
        echo "azure"
        return 0
    fi
    
    echo "unknown"
    return 0
}

function is_aws {
    [[ "$(detect_cloud_provider)" == "aws" ]]
}

function is_gcp {
    [[ "$(detect_cloud_provider)" == "gcp" ]]
}

function check_existing_container {
    # Check if new_edge_access container is already running
    # Only check if docker is available and running
    if command -v docker &> /dev/null; then
        # Check for containers running the new_edge_access:latest image
        # Note: Podman prefixes local images with "localhost/", Docker doesn't
        # So we need to check for both formats
        local running_containers=$(docker ps --format "{{.Image}}" 2>/dev/null | grep -cE "(^|localhost/)new_edge_access:latest$" 2>/dev/null)
        if [ "$running_containers" -gt 0 ]; then
            echo "Found $running_containers NPA Publisher container(s) already running with image 'new_edge_access:latest'."
            echo "Container details:"
            docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" --filter "ancestor=new_edge_access:latest" 2>/dev/null
            echo "Exiting to prevent multiple instances, exiting as 0 for no error"
            exit 0
        fi
    else
        echo "Docker not yet available, skipping existing container check"
    fi
}

function check_tmp_permissions {
    # Check if /tmp directory has 777 permissions
    local tmp_perms=$(stat -c "%a" /tmp 2>/dev/null)
    if [ "$tmp_perms" != "1777" ] && [ "$tmp_perms" != "777" ]; then
        echo "Warning: /tmp permissions are $tmp_perms, setting to 777"
        chmod 777 /tmp
        if [ $? -eq 0 ]; then
            echo "Successfully set /tmp permissions to 777"
        else
            echo "Failed to set /tmp permissions to 777"
            exit 1
        fi
    else
        echo "/tmp permissions are correctly set ($tmp_perms)"
    fi
}

function run_prechecks {
    echo "Running pre-installation checks..."

    # Detect and display cloud provider
    local cloud_provider=$(detect_cloud_provider)
    case "$cloud_provider" in
        "aws")
            echo -e "\033[31mDetected cloud provider: AWS\033[0m"
            ;;
        "gcp")
            echo -e "\033[31mDetected cloud provider: GCP\033[0m"
            ;;
        "azure")
            echo -e "\033[31mDetected cloud provider: Azure\033[0m"
            ;;
        *)
            echo "Cloud provider: Unknown or on-premises"
            ;;
    esac

    # Check /tmp permissions first
    check_tmp_permissions

    # Check for existing container (only if docker is available)
    if command -v docker &> /dev/null; then
        check_existing_container
    else
        echo "Docker not yet available, container check will be performed after Docker installation"
    fi
    
    echo "Pre-installation checks completed successfully"
}

if [ "$PUBLISHER_REPO" = "" ] ; then
    if is_china ; then
        PUBLISHER_REPO=ns-1-registry.cn-shenzhen.cr.aliyuncs.com/npa/publisher_u22
    else
        PUBLISHER_REPO=johnneerdael/publisher-ipv6
    fi
fi

if [ "$PUBLISHER_IMAGE_TAG" = "" ] ; then
    PUBLISHER_IMAGE_TAG=udpfix
fi

if [ "$PUBLISHER_IMAGE_SIG_PATH" = "" ] && is_china ; then
    PUBLISHER_IMAGE_SIG_PATH=https://npa-ova.oss-cn-shenzhen.aliyuncs.com/publisher.netskope.com/latest/signature
fi

if [ "$PUBLISHER_USER" = "" ] ; then
    INSTALL_USER=$(logname)
else
    INSTALL_USER=$PUBLISHER_USER
fi

if [ "$INSTALL_USER" = "" ] ; then
    echo "No username specificed. Exit!"
    exit 1
fi

if [ "$INSTALL_USER" = "root" ] ; then
    echo "Do not use the root to install the publisher. Exit!"
    exit 1
fi

HOME=`eval echo ~$INSTALL_USER`
if [ "$HOME" = "" ] ; then
    echo "Can not find the $INSTALL_USER home directory. Exit!"
    exit 1
fi

if is_bwan ; then
    BWAN_HOME=/infroot/npa_publisher
    INSTALL_HOME=$BWAN_HOME
else
    INSTALL_HOME=$HOME
fi

if is_rhel ; then
    if is_china ; then
        DOCKER_CE_REPO=http://mirrors.aliyun.com/docker-ce/linux/rhel/docker-ce.repo
    else   
        DOCKER_CE_REPO=https://download.docker.com/linux/rhel/docker-ce.repo
    fi
else
    if is_china ; then
        curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/docker-archive-keyring.gpg
        DOCKER_CE_REPO="deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
    else
        curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor --yes -o /etc/apt/trusted.gpg.d/docker-archive-keyring.gpg
        DOCKER_CE_REPO="deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
    fi
fi


function change_to_user_home {
    echo "Change to user home directory: $HOME for installation"
    cd "$HOME" || { echo "Failed to change to home directory"; exit 1; }
}

function update_packages {
    if is_rhel ; then
        # Try normal update first, fallback to --skip-broken if dependency issues occur
        # This handles upstream RHEL repository synchronization issues where some packages
        # have dependency conflicts during repository updates
        if ! yum update -y; then
            echo "Warning: Standard yum update failed, retrying with --skip-broken to skip problematic packages"
            yum update -y --skip-broken || {
                echo "Warning: yum update with --skip-broken also failed, continuing anyway"
                echo "This may indicate temporary RHEL repository issues"
                return 0  # Don't fail the installation
            }
        fi
    else
        apt-get -y update
        apt-get -y upgrade
    fi
}

CENTOS_VERSION=9
function install_docker_ce {
    if is_rhel ; then
        if ! command -v sg &> /dev/null; then
            echo "Installing sg command temporarily for group substitution..."
            yum install -y sg || echo "Warning: Failed to install sg command, but continuing anyway"
        fi

        # Install Podman with Docker compatibility layer for RHEL
        echo "Installing Podman with Docker compatibility on RHEL..."
        yum install -y podman podman-docker jq
        
        # Install official docker-compose-plugin for native compose support
        # This is the recommended approach per https://www.redhat.com/en/blog/podman-docker-compose
        # The plugin provides native "docker compose" (with space) command that works with Podman
        # and supports stdin input (docker compose -f - up -d) unlike podman-compose
        echo "Installing docker-compose-plugin for native compose support..."
        
        # Add Docker's official repository for RHEL to get latest docker-compose-plugin
        yum-config-manager --add-repo $DOCKER_CE_REPO
        
        # Install latest docker-compose-plugin from repository
        yum install -y docker-compose-plugin

        # Configure podman socket to start during boot (mimics docker daemon)
        # The docker-compose-plugin communicates with Podman via this socket
        systemctl enable podman.socket
        systemctl start podman.socket
        
        # Enable podman-restart service to honor --restart policies across reboots
        # Without this, containers with --restart=always won't auto-start after system reboot
        # This is the Podman equivalent of Docker's daemon managing container restarts
        systemctl enable podman-restart.service
        
        # Suppress "Emulate Docker CLI using podman" message
        # Create /etc/containers/nodocker to silence the compatibility message
        touch /etc/containers/nodocker
        
        # Configure default registry to docker.io for short-name resolution
        # This allows "docker pull netskopeprivateaccess/image" to work without docker.io/ prefix
        # Create or update /etc/containers/registries.conf
        cat > /etc/containers/registries.conf.d/00-shortnames.conf <<EOF
# Default registry for short names - automatically prepend docker.io
# This makes Podman behave like Docker for image pulls
unqualified-search-registries = ["docker.io"]

# Set short-name mode to "permissive" to avoid TTY prompts
# This allows non-interactive image pulls with short names
short-name-mode = "permissive"
EOF

        # Create docker group for compatibility (podman-docker doesn't create it automatically)
        # This allows 'sg docker' commands to work as expected
        if ! getent group docker > /dev/null 2>&1; then
            groupadd docker
            echo "Created docker group for Podman compatibility"
        fi

        # Enable SSH user to run podman/docker command
        # Note: podman-docker provides 'docker' command that maps to podman
        usermod -a -G docker $INSTALL_USER
        
        # Verify docker command is available via podman-docker compatibility
        if ! command -v docker &> /dev/null; then
            echo "Warning: Docker compatibility layer not properly installed"
            exit 1
        fi
        
        echo "Podman with Docker compatibility successfully installed on RHEL"
    else
        # Install packages to allow apt to use a repository over HTTPS:
        apt-get install -y apt-transport-https ca-certificates curl software-properties-common
        add-apt-repository -y $DOCKER_CE_REPO
        apt-get -y update 
        
        # Install Docker CE
        apt-get install -y docker-ce jq
        
        while ! [[ "`service docker status`" =~ "running" ]]; do sleep 1; done

        # Enable SSH user to run docker command
        usermod -a -G docker $INSTALL_USER
    fi
}


function setup_docker_hub_auth {
    echo "Setting up Docker Hub authentication via dhbootstrap..."

    if is_rhel; then
        echo "Skipping Docker Hub auth setup on RHEL (uses Podman)"
        return 0
    fi

    # Install dependencies for pass credential store
    apt-get install -y gnupg2 pass golang-docker-credential-helpers

    # Verify docker-credential-pass is available
    if ! command -v docker-credential-pass &>/dev/null; then
        echo "WARNING: docker-credential-pass not found after install, skipping Docker Hub auth"
        return 0
    fi

    # Download dhbootstrap binary to user's home (not /tmp, which may be noexec)
    local dhbootstrap_bin="$HOME/.dhbootstrap"
    local dhbootstrap_url="${S3_PUBLISHER_GENERIC_PATH:-https://s3-us-west-2.amazonaws.com/publisher.netskope.com/latest/generic}/dhbootstrap"
    if ! curl -fsSL "$dhbootstrap_url" -o "$dhbootstrap_bin"; then
        echo "WARNING: Failed to download dhbootstrap, skipping Docker Hub auth"
        return 0
    fi
    chown "$INSTALL_USER" "$dhbootstrap_bin"
    chmod 700 "$dhbootstrap_bin"

    # Generate GPG key for docker-bootstrap@local (required by dhbootstrap for pass init)
    sudo -u "$INSTALL_USER" -H bash -c '
        if ! gpg --list-keys "docker-bootstrap@local" &>/dev/null; then
            gpg --batch --gen-key <<GPGEOF
%no-protection
Key-Type: RSA
Key-Length: 2048
Name-Real: Docker Bootstrap
Name-Email: docker-bootstrap@local
Expire-Date: 0
%commit
GPGEOF
        fi
    '

    # Run dhbootstrap as the service user to configure Docker credential store
    sudo -u "$INSTALL_USER" -H "$dhbootstrap_bin"

    # Clean up
    rm -f "$dhbootstrap_bin"

    echo "Docker Hub authentication configured successfully"
}

function load_publisher_image {
    # Load docker image
    # sg command will execute thit with docker group permissions (this shell doesn't have it yet because we just loaded it)
    if [ -f $HOME/publisher_docker.tgz ]; then
        sg docker -c "gunzip -c $HOME/publisher_docker.tgz | docker load"
    else
        sg docker -c "docker pull $PUBLISHER_REPO:$PUBLISHER_IMAGE_TAG"
        sg docker -c "docker tag $PUBLISHER_REPO:$PUBLISHER_IMAGE_TAG new_edge_access:latest"
    fi
}

function verify_publisher_image {
    curl $PUBLISHER_IMAGE_SIG_PATH/$PUBLISHER_IMAGE_TAG/npa-publisher-image-signed-id.sig > docker-image-id.sig
    docker manifest inspect $PUBLISHER_REPO:$PUBLISHER_IMAGE_TAG | jq '.config.digest' -r | sed 's/"//g'| tr -d "\n" > customer-image-id.txt
    openssl sha256 -verify npa-publisher-public.pem -signature docker-image-id.sig customer-image-id.txt

    if [ $? -ne 0 ]; then
        echo "The Publisher image verification failed ! Please contact Netskope Support"
        exit 1
    fi
}

function prepare_for_publisher_start {
    if is_bwan ; then
        # Put resources and logs folders under /infroot/npa_publisher to persist them
        # and link them to user's home/logname directory for publisher usage
        mkdir -p $INSTALL_HOME/resources
        mkdir -p $INSTALL_HOME/logs
        ln -sfn $INSTALL_HOME/resources $HOME/resources
        ln -sfn $INSTALL_HOME/logs $HOME/logs
    else
        # Let's create folders for publisher and set them to be owner user (vs root)
        # If we don't create them explicitly then docker engine will create it for us (under root user)
        # Use sudo -u (non-interactive) instead of sudo -i -u to avoid sourcing .bash_profile
        # which would trigger the wizard before it's been extracted
        sudo -u $INSTALL_USER mkdir -p $HOME/resources
        sudo -u $INSTALL_USER mkdir -p $HOME/logs
    fi
}

function configure_publisher_wizard_to_start_on_user_ssh {
    # There is a problem with the docker that sometimes it starts really slow and unavailable on first login
    # We depend on Docker being ready, so we want to wait for it explicitly
    # Note: On RHEL with Podman, we use podman.socket instead of docker.service
    if is_rhel ; then
        echo "while [ \"\`systemctl is-active podman.socket\`\" != \"active\" ]; do echo \"Waiting for Podman socket to start. It can take a minute.\"; sleep 10; done" >> $HOME/.bash_profile
    else 
        echo "while ! [[ \"\`sudo service docker status\`\" =~ \"running\" ]]; do echo \"Waiting for Docker daemon to start. It can take a minute.\"; sleep 10; done" >> $HOME/.bash_profile
        # Allow to run service docker status under sudo without entering a password
        echo "$INSTALL_USER ALL=(ALL) NOPASSWD: /usr/sbin/service docker status" >> /etc/sudoers.d/npa
    fi
    # Configure publisher wizard to run on each SSH
    cat << 'EOF' >> $HOME/.bash_profile
cat << "BANNER"
 _       __     __                             __       
| |     / /__  / /________  ____ ___  ___     / /_____  
| | /| / / _ \/ / ___/ __ \/ __ `__ \/ _ \   / __/ __ \ 
| |/ |/ /  __/ / /__/ /_/ / / / / / /  __/  / /_/ /_/ / 
|__/|__/\___/_/\___/\____/_/ /_/ /_/\___/   \__/\____/  
        _   __     __       __                          
       / | / /__  / /______/ /______  ____  ___         
      /  |/ / _ \/ __/ ___/ //_/ __ \/ __ \/ _ \        
     / /|  /  __/ /_(__  ) ,< / /_/ / /_/ /  __/        
    /_/ |_/\___/\__/____/_/|_|\____/ .___/\___/         
                                  /_/

BANNER
echo -e "\e[1;32mPlease enter sudo ./npa_publisher_wizard to launch the publisher wizard\e[0m"
EOF
    # Configure publisher wizard to run on each SSH
    # On RHEL with SELinux enforcing, use /opt directory which can be relabeled
    if is_rhel && [ "$(getenforce 2>/dev/null)" = "Enforcing" ]; then
        # RHEL no need to 
        echo "Skipped wizard extraction on SSH for RHEL"
    else
        # Ubuntu or SELinux disabled - use home directory with :z flag
        echo "docker run -v \$HOME:/home/host_home:z --rm --entrypoint cp new_edge_access:latest /home/npa_publisher_wizard /home/host_home/npa_publisher_wizard" >> $HOME/.bash_profile
    fi
}

function configure_publisher_wizard_to_start_on_boot {
    # Extract wizard for a launch on boot
    # On RHEL with SELinux, we cannot relabel /home/ec2-user directly
    # Create a dedicated directory for the wizard that can be safely relabeled
    if is_rhel && [ "$(getenforce 2>/dev/null)" = "Enforcing" ]; then
        # Create a dedicated directory for wizard extraction
        mkdir -p /opt/npa_wizard
        chmod 755 /opt/npa_wizard
        sg docker -c "docker run --pull=never -v /opt/npa_wizard:/home/host_home:Z --rm --entrypoint cp new_edge_access:latest /home/npa_publisher_wizard /home/host_home/npa_publisher_wizard"
        # Copy from /opt to user's home
        cp /opt/npa_wizard/npa_publisher_wizard $INSTALL_HOME/npa_publisher_wizard
        chmod +x $INSTALL_HOME/npa_publisher_wizard
    else
        # Ubuntu or SELinux disabled - use home directory directly with :z flag
        sg docker -c "docker run -v $INSTALL_HOME:/home/host_home:z --rm --entrypoint cp new_edge_access:latest /home/npa_publisher_wizard /home/host_home/npa_publisher_wizard"
        chmod +x $INSTALL_HOME/npa_publisher_wizard
    fi
    
    if is_bwan ; then
        # Link wizard from /infroot/npa_publisher to user's home
        ln -sfn $INSTALL_HOME/npa_publisher_wizard $HOME/npa_publisher_wizard
    fi
}

function create_systemd_service_for_aws_auto_registration {
    # This function should only run on AWS
    if ! is_aws; then
        echo "Skipping AWS auto registration setup - not running on AWS (detected: $(detect_cloud_provider))"
        return 0
    fi
    
    # Check if the source service file exists and has content
    if [ ! -f "$HOME/npa-publisher.service" ]; then
        echo "Warning: npa-publisher.service file not found at $HOME/npa-publisher.service"
        echo "Skipping systemd service creation to prevent masked service"
        return 0
    fi
    
    if [ ! -s "$HOME/npa-publisher.service" ]; then
        echo "Warning: npa-publisher.service file is empty at $HOME/npa-publisher.service"
        echo "Skipping systemd service creation to prevent masked service"
        return 0
    fi
    
    # Create a systemd service to start us on boot
    mv $HOME/npa-publisher.service /usr/lib/systemd/system
    chown root:root /usr/lib/systemd/system/npa-publisher.service
    # Restore SELinux context if needed as move from user home to systemd folder, the SELinux context might be different (See the stat info)
    # stat /usr/lib/systemd/system/npa-publisher.service
    # File: /usr/lib/systemd/system/npa-publisher.service
    # Size: 204             Blocks: 8          IO Block: 4096   regular file
    # Device: fd03h/64771d    Inode: 107988      Links: 1
    # Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
    # Context: unconfined_u:object_r:user_home_t:s0
    # Access: 2025-07-10 04:16:30.000000000 +0000
    # Modify: 2025-06-02 08:23:03.000000000 +0000
    # Change: 2025-07-10 04:17:14.734184684 +0000
    # Birth: 2025-07-10 04:17:14.733184675 +0000
    
    # After change the Context will be: unconfined_u:object_r:systemd_unit_file_t:s0

    # Check if the restorecon command exists
    if command -v restorecon &> /dev/null; then
        echo "Restoring SELinux context for npa-publisher.service"
        if restorecon -v "/usr/lib/systemd/system/npa-publisher.service"; then
            echo "SELinux context successfully restored"
        else
            echo "Warning: Failed to restore SELinux context, but continuing anyway"
            echo "This may be normal if SELinux is not enabled on this system"
        fi
    else
        echo "restorecon command not found, skipping SELinux context restoration"
        echo "This is expected on non-SELinux systems"
    fi

    systemctl daemon-reload
    
    # Verify the service file is valid before enabling
    if systemctl cat npa-publisher.service &>/dev/null; then
        systemctl enable npa-publisher
        echo "npa-publisher systemd service successfully enabled"
    else
        echo "Warning: npa-publisher.service is invalid or masked, skipping enable"
        echo "Service file status:"
        ls -la /usr/lib/systemd/system/npa-publisher.service 2>/dev/null || echo "Service file not found"
        return 0
    fi
}

function launch_publisher {
    # ToDo: We should move this to publisher wizard
    # Configure for a publisher to start automatically
    HOST_OS_TYPE=ubuntu

    # Pass NAT46 pool6 prefix to container if available (Jool always installed on host).
    # NAT46 activation is controlled by ~/resources/.nat46_config.json (wizard manages this).
    local nat46_env=""
    local pool6_file="$INSTALL_HOME/resources/.jool_pool6"
    if [ -f "$pool6_file" ]; then
        local pool6_prefix
        pool6_prefix=$(cat "$pool6_file")
        nat46_env="-e NPA_NAT46_POOL6=${pool6_prefix}"
    fi

    if is_rhel ; then
        # RHEL with :z for SELinux, telling the shared volumes to be relabeled
        HOST_OS_TYPE=centos
        sg docker -c "docker run --restart always --network=host --privileged -e HOST_OS_TYPE=$HOST_OS_TYPE $nat46_env -v $INSTALL_HOME/resources:/home/resources:z -v $INSTALL_HOME/logs:/home/logs:z -v /etc/alternatives:/home/netinfo:ro -v $INSTALL_HOME/resources/Corefile:/etc/coredns/Corefile:ro -d new_edge_access:latest"
    else
        # Ubuntu, run without :z on volume mounts as not using SELinux
        sg docker -c "docker run --restart always --network=host --privileged -e HOST_OS_TYPE=$HOST_OS_TYPE $nat46_env -v $INSTALL_HOME/resources:/home/resources -v $INSTALL_HOME/logs:/home/logs -v /etc/alternatives:/home/netinfo:ro -v $INSTALL_HOME/resources/Corefile:/etc/coredns/Corefile:ro -d new_edge_access:latest"
    fi
}

function hardening_ssh {
    # Update sshd_config
    if is_rhel ; then
        echo "Skipping SSH hardening on RHEL "
    else
        # 5.3.4 Ensure SSH access is limited | allow users
        # 5.3.6 Ensure SSH X11 forwarding is disabled
        # 5.3.7 Ensure SSH MaxAuthTries is set to 4 or less
        # 5.3.9 Ensure SSH HostbasedAuthentication is disabled
        # 5.3.10 Ensure SSH root login is disabled
        # 5.3.11 Ensure SSH PermitEmptyPasswords is disabled
        # 5.3.13 Ensure only strong Ciphers are used
        # 5.3.14 Ensure only strong MAC algorithms are used
        # 5.3.15 Ensure only strong Key Exchange algorithms are used
        # 5.3.20 Ensure SSH AllowTcpForwarding is disabled
        # 5.3.22 Ensure SSH MaxSessions is limited to 10
        # Set TCPKeepAlive to no
        # Set ClientAliveCountMax to 1
        echo "AllowUsers $INSTALL_USER" >> /etc/ssh/sshd_config
        sed -i 's/^#*MaxAuthTries [0-9]\+/MaxAuthTries 2/' /etc/ssh/sshd_config
        sed -i 's/^#*X11Forwarding yes/X11Forwarding no/' /etc/ssh/sshd_config
        echo "HostbasedAuthentication no" >> /etc/ssh/sshd_config
        echo "PermitRootLogin no" >> /etc/ssh/sshd_config
        echo "PermitEmptyPasswords no" >> /etc/ssh/sshd_config
        echo "Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr" >> /etc/ssh/sshd_config
        echo "MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256" >> /etc/ssh/sshd_config
        echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256" >> /etc/ssh/sshd_config
        echo "AllowTcpForwarding no" >> /etc/ssh/sshd_config
        echo "MaxSessions 10" >> /etc/ssh/sshd_config
        sed -i 's/^#*TCPKeepAlive [yes|no]\+/TCPKeepAlive no/' /etc/ssh/sshd_config
        sed -i 's/^#*ClientAliveCountMax [0-9]\+/ClientAliveCountMax 1/' /etc/ssh/sshd_config
        echo "HostbasedAcceptedKeyTypes -ssh-rsa" >> /etc/ssh/sshd_config
        echo "HostKeyAlgorithms -ssh-rsa" >> /etc/ssh/sshd_config
        echo "PubkeyAcceptedKeyTypes -ssh-rsa" >> /etc/ssh/sshd_config
    fi
}

function hardening_disable_root_login_to_all_devices {
    #Disable ALL root login, ssh, console, tty1...
    echo > /etc/securetty
}

function hardening_remove_root_password {
    passwd -d root
    passwd --lock root
}

function hardening_disable_ctrl_alt_del {
    TARGET_UNIT="ctrl-alt-del.target"
    UNIT_PATH="/etc/systemd/system/${TARGET_UNIT}"

    # Check if the unit is already masked to /dev/null
    if [ -L "${UNIT_PATH}" ] && [ "$(readlink -f "${UNIT_PATH}")" == "/dev/null" ]; then
        echo "${TARGET_UNIT} is already correctly masked. No action needed."
        return 0
    fi

    # Check if the unit exists as a symlink (but not correctly masked)
    if [ -L "${UNIT_PATH}" ]; then
        echo "${TARGET_UNIT} exists as a symlink to $(readlink -f "${UNIT_PATH}"). Removing it first."
        rm -f "${UNIT_PATH}" || { echo "Failed to remove existing symlink for ${TARGET_UNIT}."; return 1; }
        systemctl daemon-reload # Reload after manual symlink removal
    fi

    # Attempt to mask the unit
    systemctl mask "${TARGET_UNIT}" || { echo "Failed to mask ${TARGET_UNIT}."; return 1; }
    echo "${TARGET_UNIT} successfully masked."
}

# Remove Linux firmware
function hardening_remove_linux_firmware {
    if is_rhel ; then
        yum remove linux-firmware -y
    else
        kernel_version=$(uname -r)
        distro=$(echo "$kernel_version" | awk -F '-' '{print $NF}')
        if [ "$distro" != "generic" ] ; then
                apt-get remove linux-firmware -y
        fi
    fi
}

function hardening_install_cracklib {
    if is_rhel ; then
        echo "Skipping cracklib installation on RHEL" 
    else
        apt-get install cracklib-runtime -y
    fi
}

function install_network_utils {
    if is_rhel ; then
        echo "Skipping network utilities installation on RHEL"
    else
        apt-get install -y net-tools bind9-utils
    fi
}

function configure_firewall_npa {
    if is_rhel ; then
        yum install -y firewalld
        # Update the public firewalld zone for NPA specific functionality
        true || systemctl restart dbus
        systemctl enable firewalld
        systemctl start firewalld
        firewall-cmd --reload
        firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" destination address="191.1.1.1/32" port protocol="tcp" port="784" accept'
        firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" destination address="191.1.1.1/32" port protocol="udp" port="785" accept'

        # Check if publisher_tunnel zone already exists
        if firewall-cmd --get-zones | grep -q "publisher_tunnel"; then
            echo "Firewall zone 'publisher_tunnel' already exists, skipping creation"
        else
            echo "Creating firewall zone 'publisher_tunnel'"
            firewall-cmd --permanent --new-zone=publisher_tunnel
        fi

        firewall-cmd --permanent --zone=publisher_tunnel --add-interface=tun0
        firewall-cmd --permanent --zone=publisher_tunnel --add-rich-rule='rule family="ipv4" port protocol="tcp" port="53" accept'
        firewall-cmd --permanent --zone=publisher_tunnel --add-rich-rule='rule family="ipv4" port protocol="udp" port="53" accept'
        firewall-cmd --permanent --zone=publisher_tunnel --add-rich-rule='rule family="ipv4" destination address="191.1.1.1/32" port protocol="tcp" port="784" accept'
        firewall-cmd --permanent --zone=publisher_tunnel --add-rich-rule='rule family="ipv4" destination address="191.1.1.1/32" port protocol="udp" port="785" accept'
        firewall-cmd --permanent --zone=publisher_tunnel --add-forward
        firewall-cmd --permanent --zone=publisher_tunnel --set-target=ACCEPT
        firewall-cmd --reload
        systemctl restart firewalld
    else
        # Ubuntu use ufw as firewall by default
        apt-get install -y ufw
        echo y | ufw enable
        ufw allow to 191.1.1.1/32 proto tcp port 784
        ufw allow to 191.1.1.1/32 proto udp port 785
        ufw allow in on tun0 to any port 53 proto tcp
        ufw allow in on tun0 to any port 53 proto udp
        ufw allow 22/tcp
        ufw allow in on lo
        ufw deny in from 127.0.0.0/8
        ufw deny in from ::1
        ufw deny in proto udp to any port 123
        ufw reload
    fi
}

function configure_docker_daemon {
    if is_rhel ; then
        # For RHEL with Podman, skip daemon config as /etc/docker/ doesn't exist
        # Podman doesn't need this configuration for --network=host mode
        echo "Skipping daemon configuration on RHEL (Podman doesn't require it)"
    else
        # For Ubuntu/Debian with Docker, keep the original configuration
        echo -e "{\n\"bridge\": \"none\",\n\"iptables\": false\n}" > /etc/docker/daemon.json
    fi
}

function generate_corefile_from_host_dns() {
    local corefile_path="$INSTALL_HOME/resources/Corefile"

    echo "Checking for existing Corefile at $corefile_path..."

    # Ensure resources directory exists
    mkdir -p "$INSTALL_HOME/resources"
    
    # Check if Corefile already exists and is valid
    if [ -f "$corefile_path" ]; then
        # Basic validation - check if it's a valid Corefile by looking for CoreDNS syntax
        if grep -q "forward\s\+\." "$corefile_path" 2>/dev/null || grep -q "bind\s\+127.0.0.1" "$corefile_path" 2>/dev/null; then
            echo "Valid Corefile already exists at $corefile_path, skipping generation"
            return 0
        else
            echo "Existing Corefile appears invalid, will regenerate"
        fi
    fi
    
    echo "Generating Corefile based on host DNS configuration..."
    
    # Capture host DNS servers using the existing capture script logic
    local dns_servers=()
    if [ -f /etc/resolv.conf ]; then
        # Extract active nameserver entries, excluding loopback addresses
        while IFS= read -r line; do
            if [[ $line =~ ^nameserver[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]] || [[ $line =~ ^nameserver[[:space:]]+([0-9a-fA-F:]+) ]]; then
                local dns_server="${BASH_REMATCH[1]}"
                # Filter out loopback addresses
                if [[ ! "$dns_server" =~ ^127\. ]] && [[ ! "$dns_server" =~ ^::1$ ]] && [[ ! "$dns_server" =~ ^0\.0\.0\.0$ ]]; then
                    dns_servers+=("$dns_server")
                fi
            fi
        done < /etc/resolv.conf
        
        # If no active nameservers found, try commented ones (AWS EC2 case)
        if [ ${#dns_servers[@]} -eq 0 ]; then
            echo "No active nameservers found, checking commented entries..."
            while IFS= read -r line; do
                if [[ $line =~ ^#[[:space:]]*nameserver[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) ]] || [[ $line =~ ^#[[:space:]]*nameserver[[:space:]]+([0-9a-fA-F:]+) ]]; then
                    local dns_server="${BASH_REMATCH[1]}"
                    # Filter out loopback addresses
                    if [[ ! "$dns_server" =~ ^127\. ]] && [[ ! "$dns_server" =~ ^::1$ ]] && [[ ! "$dns_server" =~ ^0\.0\.0\.0$ ]]; then
                        dns_servers+=("$dns_server")
                    fi
                fi
            done < /etc/resolv.conf
        fi
        
        if [ ${#dns_servers[@]} -gt 0 ]; then
            echo "Using host DNS servers: ${dns_servers[*]}"
        else
            echo "No valid DNS servers found, using fallback DNS servers"
            dns_servers=("8.8.8.8" "8.8.4.4" "1.1.1.1")
        fi
    else
        echo "WARNING: /etc/resolv.conf not found, using fallback DNS servers"
        dns_servers=("8.8.8.8" "8.8.4.4" "1.1.1.1")
    fi
    
    # Format upstream servers for CoreDNS (space-separated)
    local upstream_servers="${dns_servers[*]}"
    
    # NAT46 is NOT enabled at provisioning time — the wizard controls this.
    # Always generate with template AAAA block (blocks IPv6 by default).
    # When the user enables NAT46 via wizard, it regenerates the Corefile with
    # the nat46 block and removes template AAAA.
    local nat46_block=""
    local template_block="
    template IN AAAA {
        rcode NXDOMAIN
    }
"
    echo "Corefile: NAT46 disabled at provisioning (wizard controls enablement)"

    # Generate Corefile (nat46 and template AAAA are mutually exclusive)
    cat > "$corefile_path" << EOF
.:53 {
    bind 127.0.0.1

    forward . $upstream_servers {
        policy sequential
        health_check 5s
        max_fails 3
        expire 10s
    }
${nat46_block}${template_block}

    cache {
        success 9984 3600
        denial 9984 5
        prefetch 1 60s 30%
    }

    errors
    log
    loop
    reload
    ready
    health 127.0.0.1:8080
    prometheus 127.0.0.1:9153
}
EOF
    
    echo "Corefile generated at $corefile_path"
    echo "Corefile configuration: upstream_servers='$upstream_servers'"
}

# Generate a unique ULA /96 prefix for Jool pool6 (RFC 4193)
# Each publisher instance gets its own prefix to avoid conflicts on shared networks.
# Format: fdXX:XXXX:XXXX:XXXX:XXXX:XXXX::/96
# The 40-bit global ID is randomly generated, subnet ID is 0, and the
# last 32 bits are left free for Jool to embed the IPv4 address.
function generate_ula_prefix {
    local state_file="$INSTALL_HOME/resources/.jool_pool6"

    # Reuse existing prefix if already generated (idempotent across re-provisioning)
    if [ -f "$state_file" ]; then
        NPA_NAT46_POOL6=$(cat "$state_file")
        echo "Reusing existing Jool pool6 prefix: $NPA_NAT46_POOL6"
        return 0
    fi

    # Generate 40-bit random global ID (5 bytes) per RFC 4193 section 3.2.2
    local rand_hex
    rand_hex=$(head -c 5 /dev/urandom | od -An -tx1 | tr -d ' \n')

    # Build ULA /96: fd + 40-bit global ID + 16-bit subnet (0000) + 32-bit zero (for IPv4 embedding)
    # fd XX:XXXX:XXXX:0000:0000:0000::/96
    local b1="${rand_hex:0:2}"
    local b2="${rand_hex:2:4}"
    local b3="${rand_hex:6:4}"
    NPA_NAT46_POOL6="fd${b1}:${b2}:${b3}::/96"

    # Persist so upgrades/re-provisions keep the same prefix
    mkdir -p "$(dirname "$state_file")"
    echo "$NPA_NAT46_POOL6" > "$state_file"
    echo "Generated new Jool pool6 ULA prefix: $NPA_NAT46_POOL6"
}

function install_jool_on_host {
    echo "Installing Jool SIIT for NAT46 support..."

    # Generate or reuse a unique ULA prefix for this publisher
    generate_ula_prefix

    local jool_version="${JOOL_VERSION:-4.1.12}"
    local jool_url="https://github.com/NICMx/Jool/releases/download/v${jool_version}"

    if is_rhel; then
        yum install -y gcc make dkms kernel-devel-$(uname -r) libnl3-devel pkgconfig || {
            echo "Warning: Failed to install Jool prerequisites on RHEL"
            return 1
        }

        # RHEL: build Jool from source tarball since .deb packages are not available
        curl -fsSL "${jool_url}/jool-${jool_version}.tar.gz" -o /tmp/jool.tar.gz
        tar -xzf /tmp/jool.tar.gz -C /tmp
        cd /tmp/jool-${jool_version}
        dkms install . || {
            echo "ERROR: Failed to build Jool DKMS module on RHEL"
            cd -
            rm -rf /tmp/jool-${jool_version} /tmp/jool.tar.gz
            return 1
        }
        cd src/usr
        ./configure && make && make install
        cd -
        rm -rf /tmp/jool-${jool_version} /tmp/jool.tar.gz
    else
        apt-get install -y build-essential dkms linux-headers-$(uname -r) libnl-genl-3-dev pkg-config || {
            echo "Warning: Failed to install Jool prerequisites"
            return 1
        }

        # Ubuntu/Debian: install .deb packages
        curl -fsSL "${jool_url}/jool-dkms_${jool_version}-1_all.deb" -o /tmp/jool-dkms.deb
        curl -fsSL "${jool_url}/jool-tools_${jool_version}-1_amd64.deb" -o /tmp/jool-tools.deb
        dpkg -i /tmp/jool-dkms.deb || apt-get install -f -y
        dpkg -i /tmp/jool-tools.deb || apt-get install -f -y
        rm -f /tmp/jool-dkms.deb /tmp/jool-tools.deb
    fi

    # Verify jool_siit kernel module can load
    modprobe jool_siit || {
        echo "ERROR: Failed to load jool_siit kernel module"
        return 1
    }

    # Persist module across reboots so the wizard can create instances on enable
    echo "jool_siit" >> /etc/modules-load.d/jool.conf

    # Jool SIIT instance, routes, forwarding, and persistence are NOT configured here.
    # The wizard's enableNAT46() handles all activation when the user enables NAT46.

    echo "Jool SIIT installed successfully (activation deferred to wizard)"
}

function disable_coredumps {
    sh -c "echo 'kernel.core_pattern=|/bin/false' > /etc/sysctl.d/50-coredump.conf"
    sysctl -p /etc/sysctl.d/50-coredump.conf
}

function create_host_os_info_cronjob {  
    echo "*/5 * * * * root cd $HOME/resources && ./npa_publisher_collect_host_os_info.sh" > /etc/cron.d/npa_publisher_collect_host_os_info
}

function create_auto_upgrade_cronjob {
    echo "*/1 * * * * root cd $HOME/resources && ./npa_publisher_auto_upgrade.sh" > /etc/cron.d/npa_publisher_auto_upgrade
}

function disable_systemd_resolved {
    if is_rhel ; then
        echo "No need to bypass the systemd-resolved on CentOS"
    else
        # Check if /etc/resolv.conf is a symbolic link
        if [ -L /etc/resolv.conf ]; then
            echo "/etc/resolv.conf is a symbolic link, proceeding with update"
            rm -f /etc/resolv.conf || true
            ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf || true
            echo "Successfully updated /etc/resolv.conf symlink"
        elif [ -f /etc/resolv.conf ]; then
            echo "Warning: /etc/resolv.conf is a static file (not a symbolic link)"
            echo "Skipping resolv.conf modification to preserve existing configuration"
            echo "File type: $(file /etc/resolv.conf)"
        else
            echo "Warning: /etc/resolv.conf does not exist, creating symlink"
            ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf || true
        fi
    fi
}

function disable_release_motd {
    if is_rhel ; then
        echo "No need to disable release motd on CentOS"
    elif [ -f /etc/update-motd.d/91-release-upgrade ]; then
        chmod 644 /etc/update-motd.d/91-release-upgrade
    else
        echo "release-upgrade file not found"
    fi
}

function leave_password_expiry_disabled_flag {
    if is_rhel ; then
        echo "No need to leave the password expiry policy flag"
    else
        echo "disabled by default" > $HOME/resources/.password_expiry_disabled
    fi
}

function remove_unnecessary_utilities {
    if is_rhel ; then
        # Note: sg command is part of shadow-utils package (core system utilities)
        # and cannot be removed separately without breaking system functionality
        return 0
    else
        apt-get -y remove iputils-ping 
        apt-get -y remove wget
        apt-get -y remove curl
        apt-get -y remove netcat-openbsd
        snap remove lxd
    fi
}

function reconfig_systemd-networkd {
    if is_rhel ; then
        # RHEL uses NetworkManager, not systemd-networkd
        echo "Skipping systemd-networkd configuration on RHEL (uses NetworkManager instead)"
        return 0
    fi
    
    # Remove any existing ManageForeignRoutes and ManageForeignRoutingPolicyRules settings
    sudo sed -i -E '/^\s*#?\s*ManageForeignRoutes\s*=.*/d' /etc/systemd/networkd.conf
    sudo sed -i -E '/^\s*#?\s*ManageForeignRoutingPolicyRules\s*=.*/d' /etc/systemd/networkd.conf

    # Add the settings under [Network] section
    sudo sed -i '/\[Network\]/a ManageForeignRoutes=no\nManageForeignRoutingPolicyRules=no' /etc/systemd/networkd.conf
}

function configure_nonat_mode {
    touch $INSTALL_HOME/resources/.nonat
}

function configure_nonat_mode_for_gcp {
    # This function should only run on GCP
    if ! is_gcp; then
        echo "Skipping .nonat flag creation - not running on GCP (detected: $(detect_cloud_provider))"
        return 0
    fi
    
    echo "Detected GCP platform, creating .nonat flag"
    touch $HOME/resources/.nonat
    echo "Successfully created $HOME/resources/.nonat flag for GCP"
}

function move_files_to_resources {
    cp npa-publisher-public.pem $HOME/resources/npa-publisher-public.pem
    cp $HOME/.prc_dp $HOME/resources/.prc_dp
}

# Function to execute a command and exit on failure
function execute_with_error_handling {
    local func_name="$1"
    echo "Executing: $func_name"
    if ! "$func_name"; then
        echo -e "\033[31mERROR: $func_name failed. Installation aborted.\033[0m"
        exit 1
    fi
}

# Main steps
if is_bwan ; then
    echo "Installing NPA publisher on a BWAN machine"
    execute_with_error_handling load_publisher_image
    execute_with_error_handling prepare_for_publisher_start
    execute_with_error_handling configure_publisher_wizard_to_start_on_boot
    execute_with_error_handling configure_nonat_mode
    execute_with_error_handling generate_corefile_from_host_dns
    execute_with_error_handling install_jool_on_host
    execute_with_error_handling launch_publisher
else
    # Run pre-installation checks first
    run_prechecks
    execute_with_error_handling change_to_user_home
    
    execute_with_error_handling update_packages
    execute_with_error_handling install_docker_ce
    
    # Check for existing container again after Docker is installed (in case it wasn't available during prechecks)
    # This check happens before configuring firewall to avoid messing up user's firewall settings
    check_existing_container
    
    execute_with_error_handling setup_docker_hub_auth

    execute_with_error_handling install_network_utils
    execute_with_error_handling configure_firewall_npa
    execute_with_error_handling configure_docker_daemon

    execute_with_error_handling load_publisher_image
    execute_with_error_handling prepare_for_publisher_start
    if is_china ; then
        execute_with_error_handling verify_publisher_image
        execute_with_error_handling move_files_to_resources
    fi

    execute_with_error_handling configure_publisher_wizard_to_start_on_user_ssh
    execute_with_error_handling configure_publisher_wizard_to_start_on_boot

    # We need this currently only on AWS
    execute_with_error_handling create_systemd_service_for_aws_auto_registration

    execute_with_error_handling create_host_os_info_cronjob
    execute_with_error_handling create_auto_upgrade_cronjob
    execute_with_error_handling disable_systemd_resolved
    execute_with_error_handling generate_corefile_from_host_dns
    execute_with_error_handling install_jool_on_host

    # Configure nonat mode for GCP before launching publisher
    execute_with_error_handling configure_nonat_mode_for_gcp

    execute_with_error_handling launch_publisher

    # hardening ssh if needed
    if [ "$#" -ge 1 ] && [ "$1" = "hardening_ssh_yes" ]; then
        execute_with_error_handling hardening_ssh
    fi

    execute_with_error_handling hardening_install_cracklib
    execute_with_error_handling hardening_disable_root_login_to_all_devices
    execute_with_error_handling hardening_remove_root_password
    execute_with_error_handling hardening_disable_ctrl_alt_del
    execute_with_error_handling hardening_remove_linux_firmware
    execute_with_error_handling disable_coredumps
    execute_with_error_handling disable_release_motd
    execute_with_error_handling leave_password_expiry_disabled_flag
    execute_with_error_handling remove_unnecessary_utilities
    execute_with_error_handling reconfig_systemd-networkd

fi
