/*
** Job Arranger for ZABBIX
** Copyright (C) 2025 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "common.h"
#include "jacommon.h"
#include "logger.h"

#include "daemon.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <linux/limits.h>

const char *progname = NULL;
/* Default config file location */
static char DEFAULT_CONFIG_FILE[] = SYSCONFDIR "/jobarg_agentd.conf";

/* application TITLE */
const char title_message[] = "Job Arranger Agent (daemon)";
/* end of application TITLE */

/* application USAGE message */
const char usage_message[] = "[-Vh]"
                             " [-c <config-file>]";
/* end of application USAGE message */

/* application HELP message */
const char *help_message[] = {
    "Options:",
    "  -c --config <config-file>  Absolute path to the configuration file",
    "",
    "Other options:",
    "  -h --help                  Give this help",
    "  -V --version               Display version number",
    NULL};

/* end of application HELP message */

/* COMMAND LINE OPTIONS */
static struct jaz_option longopts[] = {
    {"config", 1, NULL, 'c'},
    {"help", 0, NULL, 'h'},
    {"version", 0, NULL, 'V'},
    {NULL}};

static char shortopts[] = "c:hV";
/* end of COMMAND LINE OPTIONS */

// Global variables
static int parent_pid = -1;
int JAZ_TASK_FLAG_FOREGROUND = 0;
char exeDirPath[PATH_MAX];
char monitor_process_path[FILE_PATH_LEN] = "/usr/local/sbin/jobarg_manager_monitor_agent";
char manager_process_path[FILE_PATH_LEN] = "/usr/local/sbin/jobarg_framework_agent";
char error_message[ERROR_MSG_SIZE];

pid_t *threads = NULL;
int threads_num = 0;
int thread_index = 0;
int exiting = 0;
int child_ready = 0;

/******************************************************************************
 *                                                                            *
 * Function: usage                                                            *
 *                                                                            *
 * Purpose: print application parameters on stdout                            *
 *                                                                            *
 * Author: Eugene Grigorjev                                                   *
 *                                                                            *
 * Comments:  usage_message - is global variable which must be initialized    *
 *                            in each zabbix application                      *
 *                                                                            *
 ******************************************************************************/
void usage()
{
    printf("usage: %s %s\n", progname, usage_message);
}

/******************************************************************************
 *                                                                            *
 * Function: help_jobarg                                                      *
 *                                                                            *
 * Purpose: show the help message                                             *
 *                                                                            *
 * Return value:                                                              *
 *                                                                            *
 * Comments:                                                                  *
 *                                                                            *
 ******************************************************************************/
void help_jobarg()
{
    const char **p = help_message;

    usage();
    printf("\n");

    while (NULL != *p)
        printf("%s\n", *p++);
}

/******************************************************************************
 *                                                                            *
 * Function: version_jobarg                                                   *
 *                                                                            *
 * Purpose: show the version of the application                               *
 *                                                                            *
 * Return value:                                                              *
 *                                                                            *
 * Comments:                                                                  *
 *                                                                            *
 ******************************************************************************/
void version_jobarg()
{
    printf("%s v%s (revision %s) (%s)\n", title_message, VERSION, REVISION, REVISION_DATE);
    printf("Compilation time: %s %s\n", __DATE__, __TIME__);
}

/******************************************************************************
 *                                                                            *
 * Function:                                                                  *
 *                                                                            *
 * Purpose:                                                                   *
 *                                                                            *
 * Parameters:                                                                *
 *                                                                            *
 * Return value:                                                              *
 *                                                                            *
 * Comments:                                                                  *
 *                                                                            *
 ******************************************************************************/
static void parse_commandline(int argc, char **argv)
{
    char ch;

    ch = '\0';
    while ((char)EOF != (ch = (char)jaz_getopt_long(argc, argv, shortopts, longopts, NULL, 0)))
    {
        switch (ch)
        {
        case 'c':
            CONFIG_FILE = strdup(jaz_optarg);
            break;
        case 'h':
            help_jobarg();
            exit(FAIL);
            break;
        case 'V':
            version_jobarg();
            exit(FAIL);
            break;
        default:
            usage();
            exit(FAIL);
            break;
        }
    }

    if (NULL == CONFIG_FILE)
        CONFIG_FILE = DEFAULT_CONFIG_FILE;
}

/******************************************************************************
 *                                                                            *
 * Function:                                                                  *
 *                                                                            *
 * Purpose:                                                                   *
 *                                                                            *
 * Parameters:                                                                *
 *                                                                            *
 * Return value:                                                              *
 *                                                                            *
 * Comments:                                                                  *
 *                                                                            *
 ******************************************************************************/
static void jaz_load_config()
{
    cfg_line cfg[] = {
        /* PARAMETER, VAR, DATA_TYPE, MAX_SIZE */
        {"LogType", &config.log_type_str, TYPE_STRING, sizeof(config.log_type_str)},
        {"JaLogFile", &config.log_file, TYPE_STRING, sizeof(config.log_file)},
        {"JaLogMessageFile", &config.log_message_file_path, TYPE_STRING, sizeof(config.log_message_file_path)},
        {"LogFileSize", &config.log_file_size, TYPE_INT, sizeof(config.log_message_file_path)},
        {"DebugLevel", &config.log_level, TYPE_INT, sizeof(config.log_level)},
        {"JaPidFile", &config.pid_file, TYPE_STRING, sizeof(config.pid_file)},
        {"AllowRoot", &config.allow_root, TYPE_INT, sizeof(config.allow_root)}};

    // memory reset here before loading the config
    memset(&config, 0, sizeof(config));

    // ** variables must be initialized in C before passing to Go **
    if (LoadConfigForC(CONFIG_FILE, cfg, sizeof(cfg) / sizeof(cfg[0]), 1) != 0)
    {
        exit(FAIL);
    }
}

/******************************************************************************
 *                                                                            *
 * Function: get_process_name(int process_type)                               *
 *                                                                            *
 * Purpose: to fetch process's name                                           *
 *                                                                            *
 * Parameters: process_type                                                   *
 *  process_type - define which process name to fetch                         *
 *                                                                            *
 * Return value: process's name                                               *
 *                                                                            *
 ******************************************************************************/
char *get_process_name(int process_type)
{
    switch (process_type)
    {
    case MONITOR_PROCESS:
        return "jobarg_manager_monitor";
    case AGENT_MANAGER_PROCESS:
        return "agent_manager";

    default:
        return "unknown_jobarg_agent_process";
    }
}

/******************************************************************************
 *                                                                            *
 * Function: init_process_args(int process_type, char *process_path,          *
 *              char *process_arg_list[PROCESS_ARG_COUNT])                    *
 *                                                                            *
 * Purpose: to prepare arguments for executing processes                      *
 *                                                                            *
 * Parameters: process_type, process_path, process_arg_list                   *
 *  process_type - define which process to create                             *
 *  process_path - define file path for process execution                     *
 *  process_arg_list[] - define arguments for process execution               *
 *      process_arg_list[0] - process name                                    *
 *      process_arg_list[1] - arg[-config-path]                               *
 *      process_arg_list[2] - full file path for jobarg_server.conf           *
 *      process_arg_list[3] - arg[-monitor-pid]                               *
 *      process_arg_list[4] - process ID or jobarg_manager_monitor process     *
 *      process_arg_list[5] - arg[-go-plugin]                                 *
 *      process_arg_list[6] - full file path for the Go Manager plugin        *
 *                              executable file                               *
 *      process_arg_list[7] - arg[-comm-path]                                 *
 *      process_arg_list[8] - folder path for communication transaction files *
 *      process_arg_list[9] - flag arg[-use-db]                               *
 *      process_arg_list[10] - flag arg[-use-tcp]                             *
 *      process_arg_list[11] - arg[-unix-socket-path]                         *
 *      process_arg_list[12] - full file path for unix socket domain file     *
 *      process_arg_list[13] - full file path for another unix socket domain  *
 *                              file                                          *
 *      process_arg_list[14] - etc                                            *
 *                                                                            *
 * Return value: resutl(Succeed 0 , Failed -1)                                *
 *                                                                            *
 ******************************************************************************/
int init_process_args(int process_type, char *process_path, char *process_arg_list[PROCESS_ARG_COUNT])
{
    const char *function_name = "init_process_args";
    int socket_count;
    for (int i = 0; i < PROCESS_ARG_COUNT; i++)
    {
        process_arg_list[i] = malloc(FILE_PATH_LEN);
        if (!process_arg_list[i])
        {
            perror("malloc failed");
            return -1; // Return failure if memory allocation fails
        }
    }

    snprintf(process_arg_list[0], PROCESS_NAME_LEN, "%s", get_process_name(process_type));
    snprintf(process_arg_list[2], FILE_PATH_LEN, CONFIG_FILE);

    if (process_type == MONITOR_PROCESS)
    {
        snprintf(process_path, FILE_PATH_LEN, monitor_process_path);
        snprintf(process_arg_list[1], PROCESS_ARG_LEN, "-c");
        snprintf(process_arg_list[3], PROCESS_ARG_LEN, "-t");
        snprintf(process_arg_list[4], PROCESS_ID_LEN, "%d", 1);
        process_arg_list[5] = NULL;
    }
    else if (process_type == AGENT_MANAGER_PROCESS)
    {
        snprintf(process_path, FILE_PATH_LEN, manager_process_path);
        snprintf(process_arg_list[1], PROCESS_ARG_LEN, "-config-path");
        snprintf(process_arg_list[3], PROCESS_ARG_LEN, "-c-monitor-pid");
        snprintf(process_arg_list[4], PROCESS_ID_LEN, "%d", threads[0]);
        snprintf(process_arg_list[5], PROCESS_ARG_LEN, "-go-plugin-id");
        snprintf(process_arg_list[6], PROCESS_ARG_LEN, "%d", AGENT_MANAGER_PROCESS);
        snprintf(process_arg_list[7], PROCESS_ARG_LEN, "-target-server");
        snprintf(process_arg_list[8], PROCESS_ARG_LEN, "agent");
        snprintf(process_arg_list[9], PROCESS_ARG_LEN, "-use-tcp");
        process_arg_list[10] = NULL;
    }
    else
    {
        write_log("JAAGENT200003", error_message, ERROR_MSG_SIZE,
                  function_name, get_process_name(process_type), strerror(errno));
        return -1;
    }

    char args_combined[1024] = "";
    for (int i = 0; i < PROCESS_ARG_COUNT && process_arg_list[i] != NULL; i++)
    {
        strcat(args_combined, process_arg_list[i]);
        strcat(args_combined, ", ");
    }

    write_log("JAAGENT400002", error_message, ERROR_MSG_SIZE,
              function_name, get_process_name(process_type), args_combined);
    return 1;
}

/******************************************************************************
 *                                                                            *
 * Function: execute_process(int process_type)                                *
 *                                                                            *
 * Purpose: to create server processes                                        *
 *                                                                            *
 * Parameters: process_type                                                   *
 *  process_type - define which process to create                             *
 *                                                                            *
 * Return value: resutl(Succeed 0 , Failed -1)                                *
 *                                                                            *
 ******************************************************************************/
int execute_process(int process_type)
{
    const char *function_name = "execute_process";
    pid_t pid = fork();

    if (pid < 0)
    {
        write_log("JAAGENT200002", error_message, ERROR_MSG_SIZE,
                  function_name, get_process_name(process_type), strerror(errno));
        return -1;
    }
    else if (pid == 0)
    {
        char monitor_pid[sizeof(pid_t)];
        char *process_path = malloc(FILE_PATH_LEN);
        char *process_arg_list[PROCESS_ARG_COUNT];

        if (init_process_args(process_type, process_path, process_arg_list) != 1)
        {
            free(process_path);
            for (int i = 0; i < PROCESS_ARG_COUNT; i++)
            {
                free(process_arg_list[i]);
            }
            exit(1);
        }

        int log_process_type = process_type;
        if (process_type == AGENT_MANAGER_PROCESS) {
            log_process_type = 1;
        }

        write_log("JAAGENT000004", error_message, ERROR_MSG_SIZE,
                  log_process_type, get_process_name(process_type), 1);
        write_log("JAAGENT400004", error_message, ERROR_MSG_SIZE,
                  function_name, get_process_name(process_type), process_path);
        execv(process_path, process_arg_list);
        write_log("JAAGENT200004", error_message, ERROR_MSG_SIZE,
                  function_name, get_process_name(process_type), strerror(errno));
        exit(1);
    }
    else
    {
        threads[process_type] = pid;
        return 0;
    }
}

/******************************************************************************
 *                                                                            *
 * Function: get_process_index(pid_t pid)                                     *
 *                                                                            *
 * Purpose: to get process index of JA-server processes ID                    *
 *                                                                            *
 * Parameters: pid                                                            *
 *  pid - ID of the target process                                            *
 *                                                                            *
 * Return value: index of the process in the list                             *
 *                                                                            *
 ******************************************************************************/
int get_process_index(pid_t pid)
{

    // need to add conditional block if some process will be spwaned multiple times
    for (int i = 0; i < threads_num; i++)
    {
        if (threads[i] == -1)
            break;
        if (pid == threads[i])
            return i;
    }
    return -1;
}

void handler(int sig)
{
    child_ready = 1;
}

void start_all_processes() {

    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGRTMAX, &sa, NULL);

    // wait for monitor (child ready)
    execute_process(MONITOR_PROCESS);
    while (!child_ready) {
        usleep(1000);
    }

    // Agent manager process
    execute_process(AGENT_MANAGER_PROCESS);
}

void kill_all_thread()
{
    int processes[2] = {MONITOR_PROCESS, AGENT_MANAGER_PROCESS};

    int exit_status;
    for (int i = 0; i < 2; i++) 
    {
        int process_type = processes[i];
        pid_t pid = threads[process_type];
        if (pid <= 0) continue;

        // Check whether the process has terminated within TIMEOUT_SEC seconds
        int waited = 0;
        int status;
        while (waited < TIMEOUT_SEC) {
            // Send SIGTERM to prompt normal termination
            kill(pid, SIGTERM);
            pid_t ret = waitpid(pid, &status, WNOHANG);
            if (ret == pid) {
                // Check for normal or abnormal termination
                break;
            }
            sleep(1);
            waited++;
        }

        // If the process has not terminated, send SIGKILL
        while (kill(pid, 0) == 0) {  
            kill(pid, SIGKILL);

            // Recover the forced termination with wait (required: prevent zombies)
            waitpid(pid, NULL, 0);
        }
        threads[process_type] = 0;
    }
    while (waitpid(-1, &exit_status, WNOHANG) > 0);  // Zombie countermeasures

}

void jaz_on_exit()
{
    const char *function_name = "jaz_on_exit";
    sigset_t set;

    write_log("JAAGENT200005", error_message, ERROR_MSG_SIZE,
              function_name);

    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);
    sigprocmask(SIG_BLOCK, &set, NULL);

    kill_all_thread();

    jaz_free(threads);

    write_log("JAAGENT000002", error_message, ERROR_MSG_SIZE, VERSION, REVISION);

    exit(SUCCEED);
}

void child_exit_handler(pid_t pid, int status)
{
    const char *function_name = "child_exit_handler";

    int exit_status;
    int process_type = get_process_index(pid);
    if (WIFEXITED(status))
    {
        exit_status = WEXITSTATUS(status);
    }
    else
    {
        exit_status = WTERMSIG(status);
    }

    write_log("JAAGENT100002", error_message, ERROR_MSG_SIZE,
                      function_name, get_process_name(process_type), pid, status);

    kill_all_thread();

    start_all_processes();

}

int init_logging()
{
    if (InitLogger(
            config.log_file,                    // log file path
            config.log_message_file_path,       // log message file path
            config.log_type_str,                // logging type
            config.log_file_size * 1024 * 1024, // max log file size
            config.log_level,                   // log level
            1,                                  // target type => 0 - server, 1 - agent
            error_message,
            ERROR_MSG_SIZE) == -1)
    {
        printf("failed to initialize the logger: %s\n", error_message);
        exit(EXIT_FAILURE);
    }
    return 0;
}

int JAZ_MAIN_ENTRY()
{
    const char *function_name = "JAZ_MAIN_ENTRY";
    int status;
    pid_t pid;
    parent_pid = (int)getpid();

    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGRTMAX, &sa, NULL);

    write_log("JAAGENT000001", error_message, ERROR_MSG_SIZE, VERSION, REVISION);

    threads_num = PROCESS_TYPE_COUNT + 1;

    threads = malloc(threads_num * sizeof(pid_t));

    write_log("JAAGENT000003", error_message, ERROR_MSG_SIZE);

    start_all_processes();

    write_log("JAAGENT000005", error_message, ERROR_MSG_SIZE, function_name);

    while (1)
    {
        while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
        {
            child_exit_handler(pid, status);
        }
        sleep(1);
    }

    write_log("JAAGENT000006", error_message, ERROR_MSG_SIZE, function_name);
    return 0;
}

int main(int argc, char **argv)
{

    progname = get_program_name(argv[0]);
    parse_commandline(argc, argv);

    jaz_load_config();

    daemon_start(config.allow_root);
    exit(SUCCEED);
}
