#!/bin/bash

# UniSASE Agent Application Control Script
# Supports start, stop, enable, disable, status commands

# Environment Variables
CURRENT_USER=$(whoami)
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
. "${SCRIPT_DIR}/appenv.conf"
# shellcheck source=permissions.sh
. "${SCRIPT_DIR}/permissions.sh"

# Logging function
log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "${APPCTRL_LOG_FILE}"
}

# Log an error message and also print it to stderr
log_error() {
    log_message "ERROR: $1"
    echo "Error: $1" >&2
}

# Function to get the target of a symbolic link
get_symlink_target() {
    local symlink_path="$1"
    # Use ls -l to get symlink info, extract target after " -> "
    ls -l "$symlink_path" 2>/dev/null | sed 's/.* -> //'
}

# Ensure that required directories exist and set permissions
ensure_directories_and_permissions() {
    local symlink_unisase_log_dir
    local symlink_unisase_runtime_dir
    # If log directory is a symlink, ensure target exists
    if [ -L "${UNISASE_LOG_DIR}" ]; then
        symlink_unisase_log_dir="$(get_symlink_target "${UNISASE_LOG_DIR}")"
        if [ ! -d "$symlink_unisase_log_dir" ]; then
            mkdir -p "$symlink_unisase_log_dir"
        fi
    fi

    # If runtime directory is a symlink, ensure target exists
    if [ -L "${UNISASE_RUNTIME_DIR}" ]; then
        symlink_unisase_runtime_dir="$(get_symlink_target "${UNISASE_RUNTIME_DIR}")"
        if [ ! -d "$symlink_unisase_runtime_dir" ]; then
            mkdir -p "$symlink_unisase_runtime_dir"
        fi
    fi

    # Create necessary directories
    if [ ! -d "${APP_LOG_DIR}" ]; then
        mkdir -p "${APP_LOG_DIR}"
    fi

    if [ ! -d "${AGENT_DEFAULT_LOG_DIR}" ]; then
        mkdir -p "${AGENT_DEFAULT_LOG_DIR}"
    fi

    if [ ! -d "${XRPC_LOG_DIR}" ]; then
        mkdir -p "${XRPC_LOG_DIR}"
    fi

    if [ ! -d "${TTYD_LOG_DIR}" ]; then
        mkdir -p "${TTYD_LOG_DIR}"
    fi

    # Ensure the proxy remark directory exists. The agent (running as 'nobody')
    # edits line remark files under this directory. Failure is non-fatal: only
    # remark editing would be unavailable, the agent still starts normally.
    if [ ! -d "${PROXY_REMARK_DIR}" ]; then
        if ! mkdir -p "${PROXY_REMARK_DIR}" 2>/dev/null; then
            log_message "Warning: failed to create proxy remark directory: ${PROXY_REMARK_DIR}"
        fi
    fi

    # Change owner and group for the related directories
    if [ "$CURRENT_USER" != "nobody" ]; then
        if [ -n "$symlink_unisase_log_dir" ]; then
            chown -R nobody: "$symlink_unisase_log_dir"
        fi

        if [ -n "$symlink_unisase_runtime_dir" ]; then
            chown -R nobody: "$symlink_unisase_runtime_dir"
        fi

        chown -R nobody: "${APP_LOG_DIR}"
        chown -R nobody: "${AGENT_DEFAULT_LOG_DIR}"
        chown -R nobody: "${XRPC_LOG_DIR}"
        chown -R nobody: "${TTYD_LOG_DIR}"
        chown -R nobody: "${AGENT_CONF_DIR}"
        chown -R nobody: "${XRPC_CONF_DIR}"

        # Grant write/execute permission so the agent (running as 'nobody') can
        # edit line remarks under the proxy remark directory. This is a shared PA
        # system directory, so we widen its permissions rather than chown it to
        # 'nobody'. Failure is non-fatal: only remark editing would be unavailable.
        if [ -d "${PROXY_REMARK_DIR}" ]; then
            if ! chmod 777 "${PROXY_REMARK_DIR}" 2>/dev/null; then
                log_message "Warning: failed to set permissions for proxy remark directory: ${PROXY_REMARK_DIR}"
            fi
        fi

        # Check if the socket file exists
        if [ -S "${RAM_MGMT_SOCK}" ]; then
            # Set socket permissions to allow all users to read/write/execute
            if ! chmod 777 "${RAM_MGMT_SOCK}" 2>/dev/null; then
                log_error "Failed to set permissions for floweye socket: ${RAM_MGMT_SOCK}"
                exit 1
            fi
        else
            log_error "Floweye socket not found: ${RAM_MGMT_SOCK}"
            exit 1
        fi

        # Re-establish ownership/access on external PA/system paths the agent
        # needs, as the final root-phase step before dropping to 'nobody'
        # (self-heals ipectrl ramdisk rebuild, PA upgrade, resolv.conf
        # regeneration). Its own EUID-based root check backstops a missing
        # 'whoami'. Output goes to the appctrl log.
        ensure_agent_access_permissions >> "${APPCTRL_LOG_FILE}" 2>&1
    fi
}

# Setup core dump configuration
setup_core_dump() {
    log_message "Setting up core dump configuration..."

    # Set core dump file size to unlimited
    ulimit -c unlimited

    log_message "Core dump configuration complete: ulimit -c $(ulimit -c)"
    log_message "Crash/Panic logs will be saved to: ${AGENT_DEFAULT_LOG_DIR}/crash.log and ${AGENT_DEFAULT_LOG_DIR}/panic.log"
}

# Check if agent binary exists
check_agent_binary() {
    if [ ! -f "${AGENT_BIN}" ]; then
        log_error "agent binary not found: ${AGENT_BIN}"
        return 1
    fi
    if [ ! -x "${AGENT_BIN}" ]; then
        log_error "agent binary is not executable: ${AGENT_BIN}"
        return 1
    fi
    return 0
}

# Check if remote access components binary exists
check_remote_access_components_binary() {
    if [ ! -f "${XRPC_BIN}" ]; then
        log_error "xrpc binary not found: ${XRPC_BIN}"
        return 1
    fi
    if [ ! -x "${XRPC_BIN}" ]; then
        log_error "xrpc binary is not executable: ${XRPC_BIN}"
        return 1
    fi
    if [ ! -f "${TTYD_BIN}" ]; then
        log_error "ttyd binary not found: ${TTYD_BIN}"
        return 1
    fi
    if [ ! -x "${TTYD_BIN}" ]; then
        log_error "ttyd binary is not executable: ${TTYD_BIN}"
        return 1
    fi
    return 0
}

# Check if all binaries exist
check_binary() {
    if ! check_agent_binary; then
        return 1
    fi
    if ! check_remote_access_components_binary; then
        return 1
    fi
    return 0
}

# Check if config file exists
check_config() {
    if [ ! -f "${AGENT_CONF_FILE}" ]; then
        log_error "config file not found: ${AGENT_CONF_FILE}"
        return 1
    fi
    return 0
}

# Compare two files and determine if they are identical.
# Prefer using cmp when available, fall back to diff.
# Returns:
#   0 - files are identical
#   1 - files are different or comparison is not possible
files_equal() {
    local file1="$1"
    local file2="$2"

    if command -v cmp >/dev/null 2>&1; then
        cmp -s "${file1}" "${file2}"
        return $?
    fi

    if command -v diff >/dev/null 2>&1; then
        diff -q "${file1}" "${file2}" >/dev/null 2>&1
        return $?
    fi

    # If neither cmp nor diff is available, treat files as different
    # to avoid mistakenly considering the config as initial.
    return 1
}

# Check if agent config is still the initial default config.
# The initial config is copied from ${APP_ROOT_DIR}/config/config.yaml to
# ${AGENT_CONF_DIR}/config.yaml during afterinstall. If these two files are
# identical, we consider the agent config to be uninitialized.
check_initial_agent_config() {
    local default_config="${APP_ROOT_DIR}/config/config.yaml"

    if [ ! -f "${AGENT_CONF_FILE}" ]; then
        # No agent config file, treat as non-initial (caller will handle as error)
        return 1
    fi

    if [ ! -f "${default_config}" ]; then
        # Default config template missing, treat as non-initial to avoid blocking startup
        return 1
    fi

    if files_equal "${default_config}" "${AGENT_CONF_FILE}"; then
        return 0
    fi

    return 1
}

# Get agent process ID
get_agent_pid() {
    # Find running agent process, matching config file path
    pgrep -f "${AGENT_BIN}.*${AGENT_CONF_FILE}" 2>/dev/null | head -1
}

# Check if agent process is running
is_agent_running() {
    local pid=$(get_agent_pid)
    if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
        return 0
    else
        return 1
    fi
}

# Get monitor script process ID
get_monitor_pid() {
    # Find running monitor script process, matching script path
    pgrep -f "${MONITOR_SCRIPT}" 2>/dev/null | head -1
}

# Check if monitor script is running
is_monitor_running() {
    local pid=$(get_monitor_pid)
    [ -n "$pid" ]
}

# Start monitor script
start_monitor() {
    if is_monitor_running; then
        log_message "Monitor script is already running."
        return 0
    fi

    if [ ! -f "${MONITOR_SCRIPT}" ]; then
        log_message "Warning: monitor script not found: ${MONITOR_SCRIPT}"
        return 1
    fi

    # Start monitor script
    nohup "${MONITOR_SCRIPT}" >> "${APPCTRL_LOG_FILE}" 2>&1 &
    local monitor_pid=$!
    log_message "Monitor script started (PID: $monitor_pid)."
    return 0
}

# Stop monitor script
stop_monitor() {
    local monitor_pid=$(get_monitor_pid)
    if [ -n "$monitor_pid" ]; then
        kill -TERM "$monitor_pid" 2>/dev/null

        # Wait for monitor to actually exit (max 5 seconds)
        # With interruptible sleep fix, monitor should exit within 1 second
        local count=0
        while [ $count -lt 5 ] && kill -0 "$monitor_pid" 2>/dev/null; do
            sleep 1
            count=$((count + 1))
        done

        if kill -0 "$monitor_pid" 2>/dev/null; then
            log_message "Monitor script did not exit gracefully, force killing..."
            kill -KILL "$monitor_pid" 2>/dev/null
        fi

        log_message "Monitor script stopped."
    fi
}

# Start agent process
start_agent() {
    log_message "Starting agent process..."

    # Check if already running
    if is_agent_running; then
        local pid=$(get_agent_pid)
        log_message "UniSASE Agent is already running (PID: $pid)."
        return 0
    fi

    # Start agent
    nohup "${AGENT_BIN}" -config "${AGENT_CONF_FILE}" >> "${APPCTRL_LOG_FILE}" 2>&1 &

    # Wait a moment to check if started successfully
    sleep 2
    if is_agent_running; then
        local pid=$(get_agent_pid)
        log_message "UniSASE Agent started successfully (PID: $pid)."

        # Clear exit status file after successful startup
        if [ -f "$AGENT_EXIT_STATUS_FILE" ]; then
            rm -f "$AGENT_EXIT_STATUS_FILE" 2>/dev/null || true
            log_message "Cleared previous exit status file"
        fi

        return 0
    else
        log_error "Failed to start UniSASE Agent."
        return 1
    fi
}

# Stop agent process
stop_agent() {
    log_message "Stopping agent process..."

    # Stop agent process
    local pid=$(get_agent_pid)
    if [ -z "$pid" ]; then
        log_message "UniSASE Agent is not running."
        return 0
    fi

    # Send TERM signal
    if kill -TERM "$pid" 2>/dev/null; then
        # Wait for process to terminate
        local count=0
        while [ $count -lt 10 ] && kill -0 "$pid" 2>/dev/null; do
            sleep 1
            count=$((count + 1))
        done

        # If still not terminated, force kill
        if kill -0 "$pid" 2>/dev/null; then
            log_message "Force stopping UniSASE Agent..."
            kill -KILL "$pid" 2>/dev/null
        fi

        log_message "UniSASE Agent stopped."
        return 0
    else
        log_error "Failed to stop UniSASE Agent."
        return 1
    fi
}

# Start remote access components (xrpc and ttyd)
start_remote_access_components() {
    log_message "Starting remote access components..."

    # Start xrpc if config file exists
    if [ -f "${XRPC_CONF_FILE}" ]; then
        if [ -x "${XRPC_CTL_BIN}" ] && [ -x "${XRPC_BIN}" ]; then
            "${XRPC_CTL_BIN}" start >> "${APPCTRL_LOG_FILE}" 2>&1
            log_message "xrpc start command executed"
        else
            log_message "Warning: xrpc control script not found or not executable"
        fi
    else
        log_message "xrpc config file not found, skipping xrpc startup"
    fi

    # Start ttyd
    if [ -x "${TTYD_CTL_BIN}" ] && [ -x "${TTYD_BIN}" ]; then
        "${TTYD_CTL_BIN}" start >> "${APPCTRL_LOG_FILE}" 2>&1
        log_message "ttyd start command executed"
    else
        log_message "Warning: ttyd control script not found or not executable"
    fi
}

# Stop remote access components (xrpc and ttyd)
stop_remote_access_components() {
    log_message "Stopping remote access components..."

    # Stop xrpc
    if [ -x "${XRPC_CTL_BIN}" ]; then
        "${XRPC_CTL_BIN}" stop >> "${APPCTRL_LOG_FILE}" 2>&1
        log_message "xrpc stop command executed"
    else
        log_message "Warning: xrpc control script not found or not executable"
    fi

    # Stop ttyd
    if [ -x "${TTYD_CTL_BIN}" ]; then
        "${TTYD_CTL_BIN}" stop >> "${APPCTRL_LOG_FILE}" 2>&1
        log_message "ttyd stop command executed"
    else
        log_message "Warning: ttyd control script not found or not executable"
    fi
}

# Start unisase agent application
# Parameters:
#   - $1: Optional component selector ("agent", "remote-access", or empty for all)
unisase_agent_start() {
    local component="${1:-all}"

    case "$component" in
        agent)
            log_message "Starting UniSASE Agent (agent only)..."

            # Setup core dump configuration
            setup_core_dump

            # Check binary and config file
            if ! check_agent_binary || ! check_config; then
                return 1
            fi

            # Start agent process
            if ! start_agent; then
                return 1
            fi

            # Start monitor script
            if ! start_monitor; then
                return 1
            fi

            return 0
            ;;

        remote-access)
            log_message "Starting remote access components only..."

            # Check binary
            if ! check_remote_access_components_binary; then
                return 1
            fi

            # Start remote access components only
            start_remote_access_components

            log_message "Remote access components started."
            return 0
            ;;

        all|*)
            log_message "Starting UniSASE Agent (all components)..."

            # Setup core dump configuration
            setup_core_dump

            # Check binary and config file
            if ! check_binary || ! check_config; then
                return 1
            fi

            # Start remote access components
            start_remote_access_components

            # If agent config is still the initial default config, this indicates
            # a fresh installation where the user has not yet modified the
            # configuration. In this case, we skip starting the agent and monitor,
            # but still return success.
            if check_initial_agent_config; then
                log_message "Agent config is initial default config; skipping agent and monitor startup (starting remote access only)"
                return 0
            fi

            # Start agent process
            if ! start_agent; then
                return 1
            fi

            # Start monitor script
            if ! start_monitor; then
                return 1
            fi

            return 0
            ;;
    esac
}

# Stop unisase agent application
# Parameters:
#   - $1: Optional component selector ("agent", "remote-access", or empty for all)
unisase_agent_stop() {
    local component="${1:-all}"

    case "$component" in
        agent)
            log_message "Stopping UniSASE Agent (agent only)..."

            # Stop monitor script
            stop_monitor

            # Stop agent process
            stop_agent
            return $?
            ;;

        remote-access)
            log_message "Stopping remote access components only..."

            # Stop remote access components only
            stop_remote_access_components

            log_message "Remote access components stopped."
            return 0
            ;;

        all|*)
            log_message "Stopping UniSASE Agent (all components)..."

            # Stop monitor script
            stop_monitor

            # Stop remote access components
            stop_remote_access_components

            # Stop agent process
            stop_agent
            return $?
            ;;
    esac
}

# Restart unisase agent application
# Parameters:
#   - $1: Optional component selector ("agent", "remote-access", or empty for all)
unisase_agent_restart() {
    local component="${1:-all}"
    unisase_agent_stop "$component"
    sleep 1
    unisase_agent_start "$component"
}

# Enable unisase agent application
unisase_agent_enable() {
    unisase_agent_start
}

# Disable unisase agent application
unisase_agent_disable() {
    unisase_agent_stop
}

# Get unisase agent application status
unisase_agent_status() {
    local agent_running=false
    local monitor_running=false

    if is_agent_running; then
        local pid=$(get_agent_pid)
        log_message "Status: UniSASE Agent is running (PID: $pid)."
        agent_running=true
    else
        log_message "Status: UniSASE Agent is not running."
    fi

    if is_monitor_running; then
        local monitor_pid=$(get_monitor_pid)
        log_message "Status: Monitor script is running (PID: $monitor_pid)."
        monitor_running=true
    else
        log_message "Status: Monitor script is not running."
    fi

    if $agent_running; then
        echo "enable"
        return 0
    else
        echo "disable"
        return 1
    fi
}

main() {
    case "$1" in
        start)
            unisase_agent_start "$2"
            ;;
        stop)
            unisase_agent_stop "$2"
            ;;
        restart)
            unisase_agent_restart "$2"
            ;;
        enable)
            unisase_agent_enable
            ;;
        disable)
            unisase_agent_disable
            ;;
        status)
            unisase_agent_status
            ;;
        *)
            echo "unknown action!"
            ;;
    esac
}

# Change owner and group for the related directories and set permissions for floweye socket before switching to 'nobody' user
# The floweye command communicates with the system through a Unix Domain Socket (RAM_MGMT_SOCK).
# By default, the 'nobody' user does not have write permissions to this socket.
# We need to set the socket permissions to 0777 to allow the agent running as 'nobody'
# to execute floweye commands successfully.
ensure_directories_and_permissions

if [ "$CURRENT_USER" != "nobody" ]; then
    # Switch to 'nobody' user with safe argument passing
    # Reconstruct the original command using shell-escaped arguments
    exec su -s /bin/bash nobody -c "$(printf '%q ' "$0" "$@")"
fi

# The following code is executed as 'nobody' user (after switching to 'nobody' user)

# Usage: ./appctrl {start|stop|restart} [component] | {enable|disable|status}
#
# Commands:
#   start [component]          - Start unisase agent application
#       start                  - Start all components (agent + remote access)
#       start agent            - Start agent process and monitor only
#       start remote-access    - Start remote access components only (xrpc + ttyd)
#
#   stop [component]           - Stop unisase agent application
#       stop                   - Stop all components (agent + remote access)
#       stop agent             - Stop agent process and monitor only (keep remote access running)
#       stop remote-access     - Stop remote access components only (keep agent running)
#
#   restart [component]        - Restart unisase agent application
#       restart                - Restart all components (agent + remote access)
#       restart agent          - Restart agent process and monitor only
#       restart remote-access  - Restart remote access components only
#
#   enable                     - Enable unisase agent application (same as start)
#   disable                    - Disable unisase agent application (same as stop)
#   status                     - Get unisase agent application status
main "$@"
