#!/bin/bash

# Copyright 2023 DFN-CERT Services GmbH
#
# Redistribution and use in source and binary forms, with or without modification, are
# permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this list of
#   conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice, this list
#   of conditions and the following disclaimer in the documentation and/or other materials
#   provided with the distribution.
# * Neither the name of the copyright holder nor the names of its contributors may be
#   used to endorse or promote products derived from this software without specific
#   prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
# SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.

# shellcheck disable=SC2034
current_dir="$(pwd)"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
script_name="$(basename "${BASH_SOURCE[0]}")"
base_name="$(basename -s .sh "${BASH_SOURCE[0]}")"
original_args=("$@")

dev_mode="off"
dev_env=""
# Check if we have two params w/o a dash
# If so, the first one should denote the internal environment
if [ "$#" -ge 2 ] && [[ ! "$2" =~ -.* ]]; then
    if [ "$1" == "devel" ]; then
        shift
        dev_mode="on"
        dev_env="devel"
    elif [ "$1" == "test" ]; then
        shift
        dev_mode="on"
        dev_env="test"
    fi
fi

base_dir="${script_dir}"
volume_conf_dir="${base_dir}/conf"
volume_logs_dir="${base_dir}/logs"
rsyslog_d_dir="${volume_conf_dir}/rsyslog.d"
volume_client_cert="${volume_conf_dir}/${base_name}.client.cert.pem"
volume_client_key="${volume_conf_dir}/${base_name}.client.key.pem"

#
# Paths inside the container. All paths are relative to the container workdir:
# WORKDIR=/home/soc
# All paths are fixed. The mount points on the host might vary, however.
#
container_conf_dir="conf"
container_client_cert="${container_conf_dir}/soc-connector.client.cert.pem"
container_client_key="${container_conf_dir}/soc-connector.client.key.pem"
container_logs_dir="logs"
container_bin_dir="bin"
container_runner_script="${container_bin_dir}/soc-connector-runner.sh"
container_etc_dir="etc"
container_client_truststore="${container_etc_dir}/soc-connector.client.truststore.pem"
container_server_truststore="${container_etc_dir}/soc-connector.server.truststore.pem"

app_name="SOC-Connector"
app_version="0.2.0"
site_name="DFN.Security"
container_name="soc-connector"

default_setup_dir="setup"
default_container_manager="podman"
default_use_sudo_on="sudo"
default_use_sudo_off=""
if [ "${dev_mode}" == "on" ]; then
    debug_level=0
    default_image_name="repo.dfn-cert.de/dfn-soc/soc-connector/rsyslog"
    default_image_tag="2.latest"
    default_image_file="soc-connector_rsyslog_${default_image_tag}.tar.gz"
    default_target_concentrators="soc-concentrator-1.${dev_env}.dfn-cert.de:443,soc-concentrator-2.${dev_env}.dfn-cert.de:443,soc-concentrator-3.${dev_env}.dfn-cert.de:443"
    default_target_concentrators_ber="${default_target_concentrators}"
    default_target_concentrators_erl="${default_target_concentrators}"
    default_target_incubator="soc-incubator-stream.${dev_env}.dfn-cert.de"
    default_target_feedback="soc-incubator.${dev_env}.dfn-cert.de"
    default_target_incubator_port="443"
    default_target_feedback_port="443"
    container_server_truststore="${container_etc_dir}/soc-connector.test.truststore.pem"
else
    debug_level=0
    default_image_name="repo.dfn-cert.de/dfn-soc/soc-connector/rsyslog"
    default_image_tag="2.latest"
    default_image_file="soc-connector_rsyslog_${default_image_tag}.tar.gz"
    default_target_concentrators="DEFAULT"
    default_target_concentrators_ber="concentrator-ber-1.soc.dfn.de:443,concentrator-ber-2.soc.dfn.de:443,concentrator-ber-3.soc.dfn.de:443"
    default_target_concentrators_erl="concentrator-erl-1.soc.dfn.de:443,concentrator-erl-2.soc.dfn.de:443,concentrator-erl-3.soc.dfn.de:443"
    default_target_incubator="incubator-stream.soc.dfn.de"
    default_target_feedback="incubator.soc.dfn.de"
    default_target_incubator_port="443"
    default_target_feedback_port="443"
    container_server_truststore="${container_etc_dir}/soc-connector.server.truststore.pem"
fi
default_syslog_listen="0.0.0.0"
default_syslog_port="1514"
default_syslog_tcp="on"
default_syslog_udp="off"
default_client_cert="*-*-*-*-*-cert.pem"
default_client_key="*-*-*-*-*-key.pem"
default_statistics="off"
default_log_to_files="off"

setup_state="incomplete"
connection_target="stage"

# ============================================================================
# --- General helper functions
# ============================================================================

cd_to() {
    pushd "$1" >/dev/null || fatal E220 "Cannot change to dir $1"
}

cd_back() {
    popd >/dev/null || fatal E220 "Cannot change back to previous dir"
}

PIDS=()
shutdown () {
    echo
    info "Shutting down ${script_name}"
    for pid in "${PIDS[@]}"; do
        kill -TERM "${pid}" >/dev/null 2>&1 && wait "${pid}"
    done
    cd_back
    exit 1
}

timestamp() {
    date +"%Y-%m-%d-%H-%M-%S"
}

timestamp_logs() {
    date +"%Y/%m/%d %H:%M:%S"
}

filesize() {
    wc -c <"$1"
}

# ============================================================================
# --- Setup parameters
# ============================================================================

show_setup() {
    if [ -f "${setup_file}" ]; then
        cat "${setup_file}"
    fi
}

reset_setup() {
    if [ -f "${setup_file}" ]; then
        rm "${setup_file}"
    fi
    info "Removed setup file: ${setup_file}"
}

save_setup() {
    tmp_setup="${setup_file}.$(timestamp)"
    tmp_basename="$(basename "${tmp_setup}")"
    debug 3 "Saving to setup file: ${setup_file}"
    set | grep -E "^SOC_CONNECTOR_[A-Za-z0-9_]+=.+" > "${tmp_setup}"
    if [ ! -f "${setup_file}" ]; then
        ln -s "${tmp_basename}" "${setup_file}"
    elif diff -q "${tmp_setup}" "${setup_file}" >/dev/null 2>&1; then
        rm "${tmp_setup}"
    else
        rm "${setup_file}" && ln -s "${tmp_basename}" "${setup_file}"
    fi
}

load_setup() {
    if [ -f "${setup_file}" ]; then
        # shellcheck disable=SC1090
        source "${setup_file}"
        loaded_setup_file="yes"
    else
        loaded_setup_file="no"
    fi
}

save_state() {
    echo "setup_state=${setup_state}" > "${state_file}"
    echo "connection_target=${connection_target}" >> "${state_file}"
}

load_state() {
    if [ -f "${state_file}" ]; then
        # shellcheck disable=SC1090
        source "${state_file}"
    else
        # Persist initial state, in case there is not yet a state file
        save_state
    fi
}

reset_state() {
    hide_info="$1"
    setup_state="incomplete"
    connection_target="stage"
    save_state
    if [ -n "${hide_info}" ]; then
        : # hide info
    else
        info "Resetting setup state. Use 'setup' command to configure the client again."
    fi
}

t_red='\033[1;31m'
t_green='\033[1;32m'
t_yellow='\033[1;33m'
t_blue='\033[1;34m'
t_gray='\033[1;37m'
t_reset='\033[0m'

print_state() {
    get_container_state
    if [ "${setup_state}" == "complete" ]; then
        display_connection_target="${connection_target}"
        display_container_state="${container_state}"
    else
        display_connection_target="-"
        display_container_state="-"
    fi
    # "Mode" is used synonymously to "Target"
    info "Setup: ${t_blue}${setup_state}${t_reset}  Mode: ${t_blue}${display_connection_target}${t_reset}  Connector: ${t_blue}${display_container_state}${t_reset}"
}

# ============================================================================
# --- Messages to the user
# ============================================================================

fatal() {
    echo -e "${t_red}ERROR${t_reset} - [${1}] ${2}"
    cd "${current_dir}" || echo -e "${t_red}ERROR${t_reset} - Cannot change back to original dir"
    exit 1
}

error() {
    echo -e "${t_red}ERROR${t_reset} - ${1}"
}

warning() {
    echo -e "${t_yellow}WARN${t_reset}  - ${1}"
}

info() {
    echo -e "${t_blue}INFO${t_reset}  - ${1}"
}

okay() {
    echo -e "${t_green}OK${t_reset}    - ${1}"
}

debug() {
    if [ "${debug_level}" -ge "$1" ]; then
        shift
        echo -e "${t_gray}DEBUG${t_reset} - $*"
    fi
}

log_command() {
    echo "Executing: ${*}" >&3
}

suggest() {
    echo
    echo "./${script_name} $*"
    echo
    echo "Should '${*: -1}' command be executed now [y/N]? "
    read -r answer
    case $answer in
    [yY]*)
        # just continue and return from function - calling function should know, what to do next
        echo "YES" >&3
        ;;
    *)
        # abort script
        echo "NO" >&3
        exit
        ;;
    esac
}

ask_for() {
    prompt="$1"
    var_name="$2"
    default_value="$3"
    current_value="${!var_name:-${default_value}}"
    echo
    echo -n "${prompt} [${current_value}]: "
    read -r reply
    echo
    export "${var_name}"="${reply:-${current_value}}"
}

confirm_yes() {
    prompt="$1"
    echo
    echo -n "${prompt} [yN]: "
    read -r reply
    echo
    reply="${reply:-N}"
    [ "${reply}" == "y" ] || [ "${reply}" == "Y" ] || [ "${reply}" == "j" ] || [ "${reply}" == "J" ]
}

init_logging() {
    debug 3 "Using log file ${log_file}"
    exec 3>> "${log_file}"
    exec > >(tee -a >(sed -u -r 's/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g' >&3 )) 2>&1
    cat >&3 <<EOT
# ============================================================================
$(timestamp_logs) ${site_name} ${app_name}
${script_name} ${app_version}
# ----------------------------------------------------------------------------
Command Arguments:
${original_args[*]}
# ----------------------------------------------------------------------------
Effective setup parameters:
$(set | grep -E "^SOC_CONNECTOR_")
# ----------------------------------------------------------------------------
EOT

}

# ============================================================================
# --- Check environment
# ============================================================================

assert_privileges() {
    if [ "${EUID}" -eq 0 ]; then
        debug 1 "Running ${script_name} as root"
    elif [ -n "${use_sudo}" ]; then
        debug 1 "Using '${use_sudo}' to run privileged commands"
    else
        debug 1 "Need to run privileged commands, but cannot."
        return 1
    fi
}

assert_container_manager() {
    debug 5 "Checking for container manager ${container_manager}"
    if [ "${container_manager}" == "docker" ]; then
        if ! assert_privileges; then
            fatal E201 "Need sudo or root to run container manager docker"
        fi
    fi
    if ${use_sudo} "${container_manager}" info >/dev/null; then
        okay "Container manager ${container_manager} found - using it"
    else
        fatal E210 "Container manager ${container_manager} not found or not usable"
    fi
}

check_image_file() {
    image_file_glob="$1"
    n_files="$(find . -maxdepth 1 -name "${image_file_glob}" | wc -l)"
    if [ "${n_files}" -eq 0 ]; then
        image_file=""
        warning "Found no image file matching '${image_file_glob}'."
    elif [ "${n_files}" -ge 2 ]; then
        image_file=""
        warning "Found ${n_files} images files matching '${image_file_glob}'."
        warning "Use  --image-file <filename>  to indicate which image file to use!"
    else
        image_file="$(find . -maxdepth 1 -name "${image_file_glob}" | tail -1)"
        image_file="$(basename "${image_file}")"
    fi
}

check_image() {
    if [ -n "$(${use_sudo} "${container_manager}" images -a -q "${image_name}:${image_tag}")" ]; then
        okay "Container image ${image_name}:${image_tag} available in local repository"
        return 0
    else
        info "Container image ${image_name}:${image_tag} not found in local repository"
        return 1
    fi
}

assert_container_image() {
    if check_image; then
        debug 9 "check_image returns $?"
        return 0
    fi
    if [ "${no_pull}" == "on" ]; then
        warning "'--no-pull' option given: cannot pull image from remote repository"
        fatal E305 "Cannot continue without appropriate container image in local repository"
        return 0
    fi

    info "Trying to load container image from remote repository..."
    container_out=$(${use_sudo} "${container_manager}" pull "${image_name}:${image_tag}" 2>&1)
    container_exit_code="$?"

    if [ "${container_exit_code}" -eq 0 ]; then
        if [ -n "$(${use_sudo} "${container_manager}" images -a -q "${image_name}:${image_tag}")" ]; then
            okay "Container image ${image_name}:${image_tag} is now available in local repository"
            return 0
        else
            fatal E302 "Could not pull container image ${image_name}:${image_tag} - check log file for more info"
            return 1
        fi
    else
        fatal E301 "Could not pull container image ${image_name}:${image_tag} from remote repository - check log file for more info"
        debug 1 "stdout/stderr: ${container_out}"
        return "${container_exit_code}"
    fi
}

assert_container_existence(){
    container_exists_state=$(\
        ${use_sudo} \
        "${container_manager}" \
        ps -a --format "{{ .Names }} {{ .State }}" | \
        grep "${container_name}" | \
        awk '{print $2}')
    if [ -n "${container_exists_state}" ]; then
        warning "Container ${container_name} already exists in state '${container_exists_state}'."
        force_container_deletion
    fi
}

force_container_deletion(){
    if confirm_yes "Would you like to force a deletion of the container?"; then
        container_out=$(${use_sudo} "${container_manager}" rm -f "${container_name}")
        container_exit_code="$?"
        if [ "${container_exit_code}" -eq 0 ]; then
            okay "Container ${container_name} removed"
        else
            fatal E313 "Could not force the deletion of container ${container_name}!"
        fi
    fi
}

get_container_state() {
    container_state="$(${use_sudo} "${container_manager}" container inspect -f '{{.State.Status}}' "${container_name}" 2>>/dev/null)"
    debug 1 "container_state: ${container_state}"
    if [ -z "${container_state}" ]; then
        container_state="stopped"
    fi
}

assert_provided_artefact() {
    file_pretty_name="$1"
    file_option="$2"
    file_default="$3"
    file_volume="$4"
    file_given="$5"
    if [ -z "${file_given}" ]; then
        n_files="$(find . -maxdepth 1 -name "${file_default}" | wc -l)"
        if [ "${n_files}" -eq 0 ]; then
            error "Found no ${file_pretty_name} in default path: '${file_default}'."
            # FIXME Need concrete way to get certs
            fatal E420 "See ... on how to request and receive all necessary artefacts!"
        elif [ "${n_files}" -ge 2 ]; then
            error "Found ${n_files} ${file_pretty_name} files matching '${file_default}':"

            while IFS= read -r -d '' file
            do
                echo "         - ${file}"
            done <   <(find . -maxdepth 1 -name "${file_default}" -print0)

            # FIXME Need concrete way to get certs
            fatal E421 "Remove unnecessary files or use option '--${file_option} <path>' to indicate, which file to use!"
        else
            file_found="$(find . -maxdepth 1 -name "${file_default}" | tail -1)"
        fi
    else
        if [ -e "${file_given}" ]; then
            file_found="${file_given}"
        else
            error "Found no ${file_pretty_name} in path: '${file_given}'."
            fatal E422 "Provide correct path with option '--${file_option} <path>', or use option '--default-${file_option}' to reset to default value!"
        fi
    fi
    cp "${file_found}" "${file_volume}"
}

run_in_container() {
    mode="$1"
    entrypoint="$2"
    shift 2
    command=("$@")

    target="stage"
    env_file="${volume_conf_dir}/${target}.env"
    generate_env "${target}" "${env_file}"

    container_run_opts=(\
        "--publish-all" \
        "--rm" \
        "--name" "${container_name}" \
        "--volume" "${volume_conf_dir}:/home/soc/${container_conf_dir}" \
        "--volume" "${volume_logs_dir}:/home/soc/${container_logs_dir}" \
        "--entrypoint" "${entrypoint}" \
        "--env-file" "${env_file}" \
        "--tty" \
    )
    if [ "${container_manager}" == "podman" ]; then
        container_run_opts+=("--network" "slirp4netns:enable_ipv6=true,allow_host_loopback=true")
    else
        container_run_opts+=("--network" "host")
    fi
    debug 6 "command: ${command[*]}"

    get_container_state
    if [ "${container_state}" == "stopped" ]; then
        if [ "$mode" == "--interactive" ]; then
            container_run_opts+=("--interactive")
            ${use_sudo} "${container_manager}" run "${container_run_opts[@]}" "${image_name}:${image_tag}" "${command[@]}"
        elif [ "${mode}" == "--null-stdin" ]; then
            container_out="$(${use_sudo} "${container_manager}" run "${container_run_opts[@]}" "${image_name}:${image_tag}" "${command[@]}" 2>&1 </dev/null)"
        else
            container_out="$(${use_sudo} "${container_manager}" run "${container_run_opts[@]}" "${image_name}:${image_tag}" "${command[@]}" 2>&1)"
        fi
        container_exit_code="$?"
        debug 6 "entrypoint: ${entrypoint}"
        debug 6 "params: ${command[*]}"
        debug 6 "exit_code: ${container_exit_code}"
        if [ "${container_exit_code}" -eq 0 ]; then
            debug 6 "stdout/stderr: ${container_out}"
        else
            debug 1 "stdout/stderr: ${container_out}"
        fi
        return "${container_exit_code}"
    else
        fatal E314 "Container is in state '${container_state}' - consider to stop or delete container!"
    fi
}

exec_in_container() {
    runner="$1"
    shift 1
    params=("$@")

    target="stage"
    env_file="${volume_conf_dir}/${target}.env"

    container_run_opts=(\
        "--env-file" "${env_file}" \
        "${container_name}" \
    )
    #    "--interactive" \
    #    "--tty" \
    container_out=$(${use_sudo} "${container_manager}" exec "${container_run_opts[@]}" "${runner}" "${params[@]}" 2>&1)
    container_exit_code="$?"
    debug 6 "command: ${runner}"
    debug 6 "params: ${params[*]}"
    debug 6 "exit_code: ${container_exit_code}"
    if [ "${container_exit_code}" -eq 0 ]; then
        debug 6 "stdout/stderr: ${container_out}"
        # return 0
    else
        warning "stdout/stderr: ${container_out}"
        # return "${container_exit_code}"
    fi
}

assert_client_certificate() {
    # check existence of cert file and key file
    assert_provided_artefact \
        "client certificate" \
        "client-cert" \
        "${default_client_cert}" \
        "${volume_client_cert}" \
        "${SOC_CONNECTOR_CLIENT_CERT}"
    assert_provided_artefact \
        "client key" \
        "client-key" \
        "${default_client_key}" \
        "${volume_client_key}" \
        "${SOC_CONNECTOR_CLIENT_KEY}"
    okay "Client certificate and key files found"

    # check that certificate has been issued by expected CA
    if run_in_container --stdout openssl verify -CAfile "${container_client_truststore}" "${container_client_cert}"; then
        okay "Client certificate issued by expected CA"
    else
        debug 6 "openssl verify -CAfile" "${container_client_truststore}" "${container_client_cert}"
        fatal E410 "Cannot verify client certificate against expected CA!"
    fi

    # check that public and private keys do match
    if run_in_container --stdout openssl x509 -noout -modulus -in "${container_client_cert}"; then
        public_key_modulus="${container_out}"
        debug 6 "public_key_modulus: ${public_key_modulus}"
    else
        fatal E411 "Cannot read public key modulus from client certificate"
    fi
    if run_in_container --stdout openssl rsa -noout -modulus -in "${container_client_key}" -passin pass:no_pass; then
        private_key_modulus="${container_out}"
        debug 6 "private_key_modulus: ${private_key_modulus}"
    else
        fatal E412 "Cannot read private key modulus from client key"
    fi
    if [ "${public_key_modulus}" != "${private_key_modulus}" ]; then
        fatal E430 "Client certificate and client key do not match"
    else
        okay "Client certificate and key do match and are usable"
    fi

    if run_in_container --stdout openssl x509 -noout -subject --nameopt multiline -in "${container_client_cert}"; then
        participant_id="$(\
            echo "${container_out}" \
            | grep -E '^\s*commonName\s+=' \
            | sed 's/^\s*commonName\s*=\s*[^a-fA-F0-9-]*//' \
            | sed 's/[^a-fA-F0-9-].*$//' \
            | tr '[:upper:]' '[:lower:]' \
        )"
        SOC_CONNECTOR_PARTICIPANT_ID="${participant_id}"
        lc="${participant_id: -1}"
        if [ "${lc}" == '0' ] || [ "${lc}" == '2' ] || [ "${lc}" == '4' ] || [ "${lc}" == '6' ] || \
           [ "${lc}" == '8' ] || [ "${lc}" == 'a' ] || [ "${lc}" == 'c' ] || [ "${lc}" == 'e' ]; then
            target_location="ber"
        else
            target_location="erl"
        fi
        SOC_CONNECTOR_TARGET_LOCATION="${target_location}"
        save_setup --no-copy
        okay "Found participant ID: ${participant_id}"
    else
        fatal E401 "Cannot extract Participant ID from client certificate!"
    fi
}

assert_connectivity() {
    if run_in_container --stdout getent hosts "${target_incubator}"; then
        target_incubator_ip="$(echo "${container_out}" | head -1 | awk '{ print $1 }')"
        okay "Successfully resolved ${target_incubator} to ${target_incubator_ip}"
    else
        fatal E501 "Cannot DNS resolve ${target_incubator}"
    fi

    if run_in_container --stdout "${container_runner_script}" tls-handshake \
                    -CAfile "${container_server_truststore}" \
                    -cert "${container_client_cert}" \
                    -key "${container_client_key}" \
                    -connect "${target_incubator}:${target_incubator_port}" \
                    -verify 3 -verify_return_error -brief; then
        okay "Successfully connected to incubator service"
    else
        fatal E502 "Cannot connect to incubator service"
    fi
}

assert_local_port_available() {
    syslog_port="${SOC_CONNECTOR_SYSLOG_PORT:-"${default_syslog_port}"}"

    if [[ "${syslog_port}" =~ ^[0-9]+$ ]]; then
        if [ "${syslog_port}" -lt 1024 ]; then
            info "Local syslog port ${syslog_port} is privileged"
            if ! assert_privileges; then
                fatal E202 "Need sudo or root to open privileged port ${syslog_port}"
            fi
        elif [ "${syslog_port}" -ge 1024 ]; then
            debug 5 "Local syslog port ${syslog_port} is not privileged"
        else
            fatal E204 "Invalid syslog port ${syslog_port} given"
        fi
    else
        fatal E204 "Syslog port '${syslog_port}' is not a number"
    fi
}

assert_local_port_reserved() {
    syslog_port="${SOC_CONNECTOR_SYSLOG_PORT:-"${default_syslog_port}"}"
    if grep -qw "${syslog_port}" /etc/services; then
        fatal E203 "Local syslog port ${syslog_port} is already reserved"
    else
        okay "Local syslog port ${syslog_port} is available and will be used"
    fi
}

assert_setup() {
    if [ "${setup_state}" != "complete" ]; then
        fatal E102 "Execute and complete 'setup', before executing '${script_command}'"
    fi
}

generate_env() {
    target="$1"
    env_file="$2"

    if [ "${target}" == "stage" ]; then
        # Stage means, we want to talk to the incubator.
        target_bootstrap="${target_incubator}:${target_incubator_port}"
    elif [ "${target_concentrators}" == "DEFAULT" ]; then
        # DEFAULT means, use pre-configured targets
        if [ "${target_location}" == "ber" ]; then
            # Location Berlin
            target_bootstrap="${default_target_concentrators_ber}"
        else
            # Other location (= Erlangen)
            target_bootstrap="${default_target_concentrators_erl}"
        fi
    else
        # Target has been set via command line option
        target_bootstrap="${target_concentrators}"
    fi

    statistics="${SOC_CONNECTOR_STATISTICS:-${default_statistics}}"
    if [ "${statistics}" != "on" ]; then
        statistics="off"
    fi
    log_to_files="${SOC_CONNECTOR_LOG_TO_FILES:-${default_log_to_files}}"
    if [ "${log_to_files}" != "on" ]; then
        log_to_files="off"
        call_log_to_logfiles=""
        logfiles_store=""
    else
        call_log_to_logfiles="call log_to_files"
        logfiles_store="/home/soc/${container_logs_dir}/hosts/%hostname:::secpath-replace%/%programname:::secpath-replace%.log"
    fi

    cat >"${env_file}" <<EOT
TZ=UTC
RSYSLOG_PARTICIPANT_ID=${SOC_CONNECTOR_PARTICIPANT_ID}
RSYSLOG_TARGET_BOOTSTRAP=${target_bootstrap}
RSYSLOG_CLIENT_CERT=/home/soc/${container_client_cert}
RSYSLOG_CLIENT_KEY=/home/soc/${container_client_key}
RSYSLOG_SERVER_TRUSTSTORE=/home/soc/${container_server_truststore}
RSYSLOG_ENABLE_STATISTICS=${statistics}
RSYSLOG_ENABLE_LOGFILES=${log_to_files}
RSYSLOG_CALL_LOG_TO_LOGFILES=${call_log_to_logfiles}
RSYSLOG_LOGFILES_STORE=${logfiles_store}
EOT

    if [ "${debug_level}" -ge 9 ]; then
        cat >>"${env_file}" <<EOT
RSYSLOG_DEBUG=debug nologfuncflow noprintmutexaction nostdout
RSYSLOG_DEBUGLOG=/home/soc/${container_logs_dir}/debug
EOT
    fi
}

assert_rsyslog_configuration() {
    target="stage"
    env_file="${volume_conf_dir}/${target}.env"
    preceding_file="${rsyslog_d_dir}/soc-preceding.conf"

    generate_env "${target}" "${env_file}"

    if [ -f "${env_file}" ]; then
        okay "Configuration created and provided"
    else
        fatal E120 "Cannot create or provide stage configuration"
    fi

    target="production"
    env_file="${volume_conf_dir}/${target}.env"
    generate_env "${target}" "${env_file}"

    if [ -f "${env_file}" ]; then
        okay "Configuration created and provided"
    else
        fatal E120 "Cannot create or provide production configuration"
    fi

    # creates preceding file, if debug level is high enough;
    # otherwise it is removed
    if [ "${debug_level}" -ge 9 ]; then
        cat >>"${preceding_file}" <<EOT
if \$inputName == "rsyslogd" then {
  action(type="omfile" file="/home/soc/${container_logs_dir}/rsyslog.log")
  stop
}
EOT
        info "Created config to move rsyslog specific messages into a separate file"
    else
        cat >>"${preceding_file}" <<EOT
if \$inputName == "rsyslogd" then {
  stop
}
EOT
        info "Created config to drop log messages from rsyslog itself"
    fi

}

assert_container_stopped() {
    get_container_state
    if [ "${container_state}" == "running" ]; then
        fatal E310 "Cannot execute '${script_command}', while container is running."
    fi
}

assert_container_started() {
    get_container_state
    if [ "${container_state}" != "running" ]; then
        fatal E311 "Cannot execute '${script_command}', while container is not running."
    fi
}

# ============================================================================
# --- Commands
# ============================================================================

do_help() {
    usage
}

do_status() {
    print_state
}

do_logs() {
    cat "${log_file}"
}

do_setup() {
    init_logging

    if [ "${setup_state}" == "complete" ]; then
        if confirm_yes "Setup ${setup_dir} seems to be complete - are you sure to reprocess setup?"; then
            info "Reprocessing setup ${setup_dir}"
        else
            exit 0
        fi
    fi

    save_setup
    reset_state hide_info

    mkdir -p "${volume_conf_dir}" || fatal E221 "Cannot create directory ${volume_conf_dir}"
    mkdir -p "${volume_logs_dir}" || fatal E221 "Cannot create directory ${volume_logs_dir}"
    mkdir -p "${rsyslog_d_dir}" || fatal E221 "Cannot create directory ${rsyslog_d_dir}"

    # Check that local port for syslog is available
    # - do we need privileged access to open port?
    assert_local_port_available

    # Check that local port is not reserved by another process
    #assert_local_port_reserved

    # Check if we have and can use requested container manager.
    # If not, we can only inform the user and abort.
    assert_container_manager

    # Container must not be running during setup
    assert_container_stopped

    # Make sure that required is available in local repository.
    # If not, pull from remote repository - unless explicitly asked not to.
    # If pulling fails, we can only inform the user and abort.
    assert_container_image

    # Check existence of container, regardless of its state, and ask for
    # a force deletion, if it exists
    assert_container_existence

    # Check for TLS client certificate
    # - if not found, hint to user, how to proceed
    # - if found, check if cert is correct and has matching key
    # - if correct, extract participant UUID and set it
    assert_client_certificate

    # Check connectivity
    # - can we resolve external names?
    #   - services.soc.dfn.de
    #   - incubator.soc.dfn.de
    # - can we access port 80 on incubator?
    # - can we access port 443 on incubator/public?
    assert_connectivity

    # Provide rsyslog configuration
    # - rssyslog.conf
    # - rsyslog.d/*.conf
    # - concentrator.env + incubator.env
    assert_rsyslog_configuration

    # Mark setup as complete
    setup_state="complete"
    connection_target="stage"
    save_state

    okay "SOC-Connector setup completed!"
}

do_reset() {
    init_logging
    assert_container_stopped

    if confirm_yes "Completely reset setup ${setup_dir}?"; then
        reset_state
        rm -f "${setup_file}"* || fatal E112 "Cannot delete setup files from ${setup_dir}"
        rm -f "${log_file}"*   || fatal E112 "Cannot delete setup files from ${setup_dir}"
        rm -f "${state_file}"  || fatal E112 "Cannot delete setup files from ${setup_dir}"
        if [ "$(ls -A "${setup_dir}")" ]; then
            fatal E110 "Setup directory ${setup_dir} contains unexpected files, cannot delete."
        else
            rmdir "${setup_dir}" || fatal E111 "Cannot delete setup diretory ${setup_dir}"
        fi
    fi
}

do_load() {
    init_logging
    assert_container_manager
    assert_container_stopped

    image_file="${current_image_file:-${default_image_file}}"
    check_image_file "$(basename "${image_file}")"
    if [ -n "${image_file}" ]; then
        info "Loading local image file ${image_file}"
        log_command "${use_sudo}" "${container_manager}" load -i "${image_file}"
        container_out=$(${use_sudo} "${container_manager}" load -q -i "${image_file}")
        container_exit_code="$?"
        if [ "${container_exit_code}" -eq 0 ]; then
            okay "Loaded container image into local repository"
        else
            fatal E303 "Cannot load image file ${image_file}, due to a container manager error"
        fi

        return "${container_exit_code}"
    else
        fatal E303 "Cannot load image file; is the filename correct?"
        return 1
    fi
}

do_update() {
    init_logging
    assert_container_manager
    assert_container_stopped

    # Check if update is necessary: get images IDs from remote and local repositories
    # If they are different, an update is available.
    remote_id=$(
        ${use_sudo} "${container_manager}" manifest inspect "${image_name}:${image_tag}" 2>/dev/null \
        | sed -n '/"config": {/,/"layers":/ p' | grep digest | sed 's/ *"digest": "sha256:\(.*\)"/\1/'
    )
    if [ -z "${remote_id}" ]; then
        warning "Cannot get latest image ID from  repository - trying to update anyway"
    else
        local_id=$(${use_sudo} "${container_manager}" inspect --format "{{.Id}}" "${image_name}:${image_tag}" 2>/dev/null)
        if [ "${remote_id}" == "${local_id}" ]; then
            info "Already have newest container image - update not necessary"
            return
        else
            info "Newer container image available - trying to update now. Please wait.."
        fi
    fi

    container_out=$(${use_sudo} "${container_manager}" pull -q "${image_name}:${image_tag}")
    container_exit_code="$?"

    if [ "${container_exit_code}" -eq 0 ]; then
        info "Update ok"
        reset_state
    else
        debug 6 "${container_out}"
        fatal E304 "Update failed, due to a container manager error"
    fi

    return "${container_exit_code}"
}

do_start() {
    init_logging

    # Check that local port is not reserved by another process
    #assert_local_port_reserved

    assert_setup
    entrypoint="rsyslogd"

    target="${connection_target}"
    env_file="${volume_conf_dir}/${target}.env"

    syslog_listen="${SOC_CONNECTOR_SYSLOG_LISTEN:-${default_syslog_listen}}"
    syslog_port="${SOC_CONNECTOR_SYSLOG_PORT:-${default_syslog_port}}"
    syslog_tcp="${SOC_CONNECTOR_SYSLOG_TCP:-${default_syslog_tcp}}"
    syslog_udp="${SOC_CONNECTOR_SYSLOG_UDP:-${default_syslog_udp}}"
    container_run_opts=(\
        "--rm" \
        "--name" "${container_name}" \
        "--volume" "${volume_conf_dir}:/home/soc/${container_conf_dir}" \
        "--volume" "${volume_logs_dir}:/home/soc/${container_logs_dir}" \
        "--entrypoint" "/usr/sbin/rsyslogd" \
        "--env-file" "${env_file}" \
        "--detach" \
    )
    if [ "${container_manager}" == "podman" ]; then
        container_run_opts+=("--network" "slirp4netns:enable_ipv6=true,allow_host_loopback=true")
    fi
    command=("-n" "-f/home/soc/${container_etc_dir}/rsyslog.conf" "-o/home/soc/${container_logs_dir}/config-dump.conf")

    if [ "${syslog_tcp}" == "on" ]; then
        container_run_opts+=("--publish" "${syslog_listen}:${syslog_port}:514/tcp")
    fi
    if [ "${syslog_udp}" == "on" ]; then
        container_run_opts+=("--publish" "${syslog_listen}:${syslog_port}:514/udp")
    fi
    if [ "${syslog_tcp}" != "on" ] && [ "${syslog_udp}" != "on" ]; then
        fatal E205 "At least one of TCP or UPD must be active for local syslog port ${syslog_port}"
    fi

    get_container_state

    if [ "${container_state}" != "running" ]; then
        container_out=$(${use_sudo} "${container_manager}" run "${container_run_opts[@]}" "${image_name}:${image_tag}" "${command[@]}" 2>&1)
        container_exit_code="$?"

        if [ "${container_exit_code}" -eq 0 ]; then
            okay "Container ${container_name} started"
        else
            error "${container_out}"
            # Try to remove the container, just in case. But ignore any additional errors that might occur.
            container_out=$(${use_sudo} "${container_manager}" rm "${container_name}" 2>&1)
            fatal E312 "Cannot start container ${container_name}"
        fi
        return "${container_exit_code}"
    else
        warning "Container ${container_name} is already running - cannot start again."
        return 0
    fi
}

do_stop() {
    init_logging
    get_container_state

    if [ "${container_state}" == "running" ]; then
        container_out=$(${use_sudo} "${container_manager}" stop "${container_name}")
        container_exit_code="$?"
        if [ "${container_exit_code}" -eq 0 ] ; then
            okay "Container ${container_name} stopped"
        else
            error "Cannot stop container ${container_name}"
        fi
        return "${container_exit_code}"
    elif [ -z "${container_state}" ]; then
        warning "Container ${container_name} is not running - cannot stop"
        return 0
    else
        warning "Container ${container_name} is in state '${container_state}' - cannot stop"
        return 0
    fi
}

do_delete() {
    init_logging
    assert_container_manager
    assert_container_stopped

    get_container_state
    if [ "${container_state}" != "stopped" ]; then
        container_out=$(${use_sudo} "${container_manager}" rm "${container_name}")
        container_exit_code="$?"

        if [ "${container_exit_code}" -eq 0 ] ; then
            okay "Container ${container_name} removed"
        else
            error "Could not remove container ${container_name} - try to stop it first!"
        fi
        return "${container_exit_code}"
    else
        warning "Container ${container_name} not found - cannot delete."
        return 0
    fi
}

do_purge() {
    init_logging
    assert_container_manager

    get_container_state
    if [ "${container_state}" != "stopped" ]; then
        fatal E306 "Cannot purge running container, stop SOC-Connector first."
    fi
    if [ -n "$(${use_sudo} "${container_manager}" images -a -q "${image_name}:${image_tag}")" ]; then
        container_out=$(${use_sudo} "${container_manager}" image rm "${image_name}:${image_tag}")
        container_exit_code="$?"
        if [ "${container_exit_code}" -eq 0 ]; then
            reset_state
            okay "Container image ${image_name}:${image_tag} purged from local repository"
        fi
        return "${container_exit_code}"
    else
        warning "Container image ${image_name}:${image_tag} not found - already purged?"
        return 0
    fi
}

do_shell() {
    assert_container_manager

    run_in_container --interactive "/bin/bash"
}

do_shell_exec() {
    assert_container_manager

    get_container_state
    if [ "${container_state}" == "running" ]; then
        ${use_sudo} "${container_manager}" exec -it "${container_name}" /bin/bash
    elif [ -z "${container_state}" ]; then
        warning "Container ${container_name} is not running - cannot open a shell"
    else
        warning "Container ${container_name} is in state '${container_state}' - cannot open a shell"
    fi
}

send_test_message() {
    msg="$1"
    line_id="$2"
    info "submitting log message #${line_id}      : ${msg}"
    exec_in_container "${container_runner_script}" submit "${msg}" || fatal E503 "Cannot submit log message"
}
receive_test_message() {
    msg="$1"
    receiver="$2"
    exec_in_container "${container_runner_script}" "${receiver}" \
        "${container_server_truststore}" \
        "${container_client_cert}" \
        "${container_client_key}" \
        "${target_feedback}:${target_feedback_port}/feedback" \
        "${participant_id}" || fatal E503 "Cannot receive log message"
    received=$(echo "${container_out}" | grep "${msg}")
    [ -n "${received}" ]
}
wait_for_confirmation() {
    msg="$1"
    retry_delay="$2"
    max_retries="$3"
    receiver="$4"
    retries=0
    until [ "${retries}" -ge "${max_retries}" ] || receive_test_message "${msg}" "${receiver}"; do
        if [ "${retries}" -eq 0 ]; then
            echo -n "        waiting for confirmation .."
        fi
        echo -n "."
        sleep "${retry_delay}"
        : $(( retries++ ))
    done
    if [ "${retries}" -gt 0 ]; then
        echo ""
    fi
    if [ "${retries}" -eq "${max_retries}" ]; then
        fatal E504 "Max retries exceeded: SOC System did not receive/confirm log message"
    else
        info "SOC System confirmed log message : ${received}"
    fi
}

do_test() {
    assert_container_manager
    assert_container_started

    for line in {1..3}; do
        msg="SOC-Connector/Incubator test message $(date +%s)"
        send_test_message     "${msg}" "${line}/3"
        wait_for_confirmation "${msg}" 1 10 receive
        if [ "${line}" -lt 3 ]; then
            sleep 1
        fi
    done
}

do_sample() {
    assert_container_manager
    assert_container_started

    while : ; do
        wait_for_confirmation "" 1 1000 sample
    done
}

do_deploy() {
    assert_container_manager
    assert_container_stopped

    connection_target="production"
    save_state
    okay "Switched to production settings - connected to SOC Concentrator"
}

# ============================================================================
# --- Main
# ============================================================================

usage() {
    cat <<EOT
Usage: ${script_name} COMMAND [OPTIONS...]

COMMANDS:
help
        Show this text
setup
        Check preconditions for setup and operation of ${app_name}.
        Finalize local configuration or give hints (by error code),
        how to proceed.
start
        Start ${app_name} in the background.
stop
        Stop ${app_name}.
status
        Show current status of setup (incomplete/complete),
        current target (incubator/concentrator) and current
        run status (running/stopped).
test
        Connect to SOC incubator system, which is compatible to the
        real SOC concentrator system, submit a few artificial log messages
        and wait for confirmation that these log messages have been received.
sample
        Connect to SOC incubator system, which is compatible to the
        real SOC concentrator system, submit any incoming log messages
        to the incubator system, wait for it's confirmation and dump
        what ever it has received. Runs until stopped by <CTRL-C>.
deploy
        After setup and tests have been completed, connect to the real
        SOC concentrator system for delivery of log messages to be
        analyzed.
reset
        Reset local configuration and restore all default values.
        The setup process (and tests) have to repeated after a reset.
update
        Check if there is a newer version of the container image available.
        If so, download the image and use it from now on.
        The setup process (and tests) have to repeated after a reset.
load
        If automatic download of the container image fails, download the image
        file manually and use this command to load it into the local image
        repository.
        The setup process (and tests) have to repeated after loading a new
        image to actually use it.
logs
        Dump the setup log file. Please note: The contains only messages from
        this script. Not any runtime logs from the SOC-Connector nor any
        log messages that have been sent to SOC-Connector to be delivered into
        the DFN.Security Infrastructure.
delete
        Delete the current SOC-Connector container. Please note: Deleting is normally
        not necessary, as the container instance will be deleted automatically,
        whenever the process is stopped.
purge
        Purge the SOC-Connector container image from the local container repository.

COMMON OPTIONS:
--setup-dir <SETUP_DIR>
        Alternative directory to store all setup artefacts (setup command)
        or to read all setup settings from (all other commands).
        The setup command will create this directory, if it does not exist.
        Default is subdirectory 'setup/' in the directory, where this script is located.
--debug <LEVEL>
        Activate debug output. LEVEL should be a number between 1 and 9.
        Higher debug level activated more debug messages.

SETUP OPTIONS:
--sudo
        Use sudo for all container related commands. This is necessary, if Docker is used
        of if a privileged syslog port is used. The current user must have appropriate
        sudoer rights and the script will eventually ask for the users password to activated
        sudo mode. (Default: do not use sudo)
--docker
        Use Docker as container manager. (Default: do not use Docker)
--podman
        Use Podman as container manager. (Default: use Podman)
--syslog-port <SYSLOG_PORT>
        The port on which the SOC-Connector will listen for syslog messages from local servers
        in order to forward these messages into the DFN.Security infrastructure.
        (Default: Port ${default_syslog_port})
--syslog-listen <IP_ADDRESS>
        Local interface on which the syslog port should be opened.
        (Default: ${default_syslog_listen}, i.e. SOC-Connector will listen on all available interfaces.)
--syslog-tcp (on|off)
        Should SOC-Connector listen to TCP messages on the syslog port?
        (Default: ${default_syslog_tcp})
--syslog-udp (on|off)
        Should SOC-Connector listen to UDP messages on the syslog port?
        (Default: ${default_syslog_udp})

LOAD OPTIONS:
--image-file <IMAGE_FILE>
        Load specified image file into the local image repository. Use this option, if the
        name of the downloaded image file changed during transport.
        (Default: ${default_image_file})

EOT
}

#
# First of all, check if there is an alternative setup directory given.
# This option is valid for all commands
#
get_setup() {
    common_options=()
    while [ $# -gt 0 ]; do
        key="$1"
        case $key in
        --setup-dir)
            SOC_CONNECTOR_SETUP_DIR=$2
            shift 2
            ;;
        *)
            common_options+=("$1")
            shift
            ;;
        esac
    done

    setup_dir="${SOC_CONNECTOR_SETUP_DIR:-${default_setup_dir}}"
    if [ "${script_command}" == 'setup' ]; then
        mkdir -p "${setup_dir}" || fatal E103 "Cannot create directory ${setup_dir} for setup files"
        [ -w "${setup_dir}" ] || fatal E104 "Cannot write to setup directory ${setup_dir}"
    elif [ ! -d "${setup_dir}" ]; then
        if [ "${script_command}" == 'status' ]; then
            print_state
            exit
        elif [ "${script_command}" == 'logs' ]; then
            warning "Cannot get ${script_command}: Execute 'setup' first!"
            exit
        else
            fatal E101 "Execute and complete 'setup', before executing '${script_command}'"
        fi
    fi
    setup_dir="$(cd "${setup_dir}" && pwd)"
    okay "Using setup from ${setup_dir}"

    setup_file="${setup_dir}/${base_name}.conf"
    state_file="${setup_dir}/${base_name}.state"
    log_file="${setup_dir}/${base_name}.log"
}

#
# Check a few common options that are possible for all/most commands.
#
parse_common_options() {
    command_options=()
    while [ $# -gt 0 ]; do
        key="$1"
        case $key in
        --debug)
            debug_level=$2
            shift 2
            ;;
        *)
            command_options+=("$1")
            shift
            ;;
        esac
    done

    debug 1 "debug_level: ${debug_level}"
    debug 1 "dev_mode: ${dev_mode}"
    debug 1 "dev_env: ${dev_env}"
}

parse_setup_options() {
    while [ $# -gt 0 ]; do
        key="$1"
        shift
        case $key in
        --sudo)
            SOC_CONNECTOR_USE_SUDO="on"
            ;;
        --docker)
            SOC_CONNECTOR_CONTAINER_MANAGER="docker"
            ;;
        --podman)
            SOC_CONNECTOR_CONTAINER_MANAGER="podman"
            ;;
        --image-name)
            SOC_CONNECTOR_IMAGE_NAME="$1"
            shift
            ;;
        --image-tag)
            SOC_CONNECTOR_IMAGE_TAG="$1"
            shift
            ;;
        --image-file)
            current_image_file=$1
            shift
            ;;
        --no-pull)
            no_pull="on"
            ;;
        --client-cert)
            SOC_CONNECTOR_CLIENT_CERT=$1
            shift
            ;;
        --client-key)
            SOC_CONNECTOR_CLIENT_KEY=$1
            shift
            ;;
        --target-concentrators)
            SOC_CONNECTOR_TARGET_CONCENTRATORS=$1
            shift
            ;;
        --target-incubator)
            SOC_CONNECTOR_TARGET_INCUBATOR=$1
            shift
            ;;
        --target-feedback)
            SOC_CONNECTOR_TARGET_FEEDBACK=$1
            shift
            ;;
        --target-incubator-port)
            SOC_CONNECTOR_TARGET_INCUBATOR_PORT=$1
            shift
            ;;
        --target-feedback-port)
            SOC_CONNECTOR_TARGET_FEEDBACK_PORT=$1
            shift
            ;;
        --syslog-listen)
            SOC_CONNECTOR_SYSLOG_LISTEN=$1
            shift
            ;;
        --syslog-port)
            SOC_CONNECTOR_SYSLOG_PORT=$1
            shift
            ;;
        --syslog-tcp)
            SOC_CONNECTOR_SYSLOG_TCP=$1
            shift
            ;;
        --syslog-udp)
            SOC_CONNECTOR_SYSLOG_UDP=$1
            shift
            ;;
        --statistics)
            SOC_CONNECTOR_STATISTICS=$1
            shift
            ;;
        --log-to-files)
            SOC_CONNECTOR_LOG_TO_FILES=$1
            shift
            ;;
        *)
            fatal E001 "Unknown option ${key}"
            ;;
        esac
    done

}

parse_load_options() {
    while [ $# -gt 0 ]; do
        key="$1"
        case $key in
        --image-file)
            current_image_file=$2
            shift 2
            ;;
        *)
            fatal E001 "Unknown option ${key}"
            shift
            ;;
        esac
    done

}

get_effective_setup() {
    image_name="${SOC_CONNECTOR_IMAGE_NAME:-${default_image_name}}"
    image_tag="${SOC_CONNECTOR_IMAGE_TAG:-${default_image_tag}}"
    container_manager="${SOC_CONNECTOR_CONTAINER_MANAGER:-${default_container_manager}}"
    target_incubator="${SOC_CONNECTOR_TARGET_INCUBATOR:-${default_target_incubator}}"
    target_incubator_port="${SOC_CONNECTOR_TARGET_INCUBATOR_PORT:-${default_target_incubator_port}}"
    target_feedback="${SOC_CONNECTOR_TARGET_FEEDBACK:-${default_target_feedback}}"
    target_feedback_port="${SOC_CONNECTOR_TARGET_FEEDBACK_PORT:-${default_target_feedback_port}}"
    target_concentrators="${SOC_CONNECTOR_TARGET_CONCENTRATORS:-${default_target_concentrators}}"

    if [ "${SOC_CONNECTOR_USE_SUDO}" == "on" ]; then
        if [ -n "${SOC_CONNECTOR_USE_SUDO_ON}" ]; then
            use_sudo="${SOC_CONNECTOR_USE_SUDO_ON}"
        else
            use_sudo="${default_use_sudo_on}"
        fi
    else
        if [ -n "${SOC_CONNECTOR_USE_SUDO_OFF}" ]; then
            use_sudo="${SOC_CONNECTOR_USE_SUDO_OFF}"
        else
            use_sudo="${default_use_sudo_off}"
        fi
    fi
    participant_id="${SOC_CONNECTOR_PARTICIPANT_ID}"
    target_location="${SOC_CONNECTOR_TARGET_LOCATION}"
}

check_command() {
    case $script_command in
        help \
        |status \
        |logs \
        |setup \
        |reset \
        |load \
        |update\
        |start \
        |stop \
        |delete \
        |purge \
        |test \
        |sample \
        |deploy \
        |shell \
        |shell-exec \
    )
        return 0
        ;;
    *)
        fatal E002 "Unknown command: '${script_command}'"
        return 1
        ;;
    esac
}

execute_command() {
    # if a new command is added, add it
    # in function "check_command" as well
    case $script_command in
    status)
        do_status
        ;;
    logs)
        do_logs
        ;;
    setup)
        do_setup
        ;;
    reset)
        do_reset
        ;;
    load)
        do_load
        ;;
    update)
        do_update
        ;;
    start)
        do_start
        ;;
    stop)
        do_stop
        ;;
    delete)
        do_delete
        ;;
    purge)
        do_purge
        ;;
    test)
        do_test
        ;;
    sample)
        do_sample
        ;;
    deploy)
        do_deploy
        ;;
    shell)
        do_shell
        ;;
    shell-exec)
        do_shell_exec
        ;;
    *)
        fatal E002 "Unknown command ${script_command}"
        ;;
    esac
}

# ============================================================================
# --- Execute
# ============================================================================

# Everything happens relativ to the directory where this script lives in
cd_to "${base_dir}"

# The first command line parameter must be the command.
script_command="$1"
shift

# Test if command exists
check_command

if [ -z "${script_command}" ]; then
    script_command="help"
fi
if [ "${script_command}" == "help" ]; then
    do_help
    exit
fi

# Load setup settings, if available. Do whatever has to be done to prepare script execution.
get_setup "$@"
if [ "${script_command}" != "setup" ]; then
    load_setup
fi
load_state
parse_common_options "${common_options[@]}"
if [ "${loaded_setup_file}" == "yes" ]; then
    debug 5 "Loaded settings from file ${setup_file}."
else
    debug 5 "File ${setup_file} not found, using default settings."
fi

# Get command specific command line options.
if [ "${script_command}" == "setup" ]; then
    parse_setup_options "${command_options[@]}"
elif [ "${script_command}" == "load" ]; then
    parse_load_options "${command_options[@]}"
elif [ "${#command_options[@]}" -ge 1 ]; then
    fatal E001 "Invalid options for command '${script_command}': ${command_options[*]} "
fi
get_effective_setup

# Finally: Execute the requested command.
trap shutdown TERM INT
execute_command
