// this is the root process and main process
/*
** Job Arranger for ZABBIX
** Copyright (C) 2012 FitechForce, Inc. All Rights Reserved.
** Copyright (C) 2013 Daiwa Institute of Research Business Innovation Ltd. All Rights Reserved.
** Copyright (C) 2021 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 <string.h>  // for strerror
#include <errno.h>   // for errno
#include <unistd.h>
#include "common.h"
#include "cfg.h"
#include "daemon.h"
#include "jacommon.h"
#include "fatal.h"
#include "logger.h"
#include <linux/limits.h>

const char	*progname = NULL;
const char	title_message[] = "Job Arranger Server";
const char	usage_message[] = "[-hV] [-c <file>]";
const int process_num = 4;
const char noti_manager[] = "notificationmanager";
int JAZ_TASK_FLAG_FOREGROUND = 0;
int child_ready = 0;
char error_message[ERROR_MSG_SIZE];

const char	*help_message[] = {
	"Options:",
	"  -c --config <file>   Absolute path to the configuration file",
	"  -f --foreground     Run Job Arranger server in foreground",
	"",
	"Other options:",
	"  -h --help            Give this help",
	"  -V --version         Display version number",
	NULL	/* end of text */
};

// Command-line options
static struct jaz_option longopts[] = {
    {"config", 1, NULL, 'c'},
    {"foreground", 0, NULL, 'f'},
    {"help", 0, NULL, 'h'},
    {"version", 0, NULL, 'V'},
    {NULL}
};

static char shortopts[] = "c:hVf";
static int	parent_pid = -1;

unsigned char	process_type	= JA_PROCESS_TYPE_UNKNOWN;
unsigned char	daemon_type		= JAZ_DAEMON_TYPE_SERVER;

pid_t	*threads = NULL;
int	exiting = 0;
int threads_num = 0;
char monitor_process_path[FILE_PATH_LEN] = "/usr/local/sbin/jobarg_manager_monitor_server";
char manager_process_path[FILE_PATH_LEN] = "/usr/local/sbin/jobarg_framework_server";

// replace with folder read from config later
char comm_folder_path[FOLDER_PATH_LEN] = "/usr/local/bin/";

void help_jobarg()
{
    const char **p = help_message;

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

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

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__);
}

void	usage()
{
	printf("usage: %s %s\n", progname, usage_message);
}

#ifdef HAVE_SIGQUEUE
void jaz_sigusr_handler(jaz_task_t task)
{
	switch (task) {
	case JAZ_TASK_CONFIG_CACHE_RELOAD:
		if (JAZ_PROCESS_TYPE_CONFSYNCER == process_type) {
            ja_log("JASERVER300001",error_message, ERROR_MSG_SIZE);
			ja_wakeup();
		}
		break;
	default:
		break;
	}
}
#endif

// join_path for notification manager
void join_path(char *out, size_t out_size, const char *base, const char *sub)
{
    if (base[strlen(base) - 1] == '/')
        snprintf(out, out_size, "%s%s", base, sub);
    else
        snprintf(out, out_size, "%s/%s", base, sub);
}

// create dir helper function for notification manager
int create_dir(const char *path)
{
    if (mkdir(path, 0755) == -1 && errno != EEXIST) {
        perror(path);
        return -1;
    }
    return 0;
}


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)},
        {"JaPidFile",           &config.pid_file,               TYPE_STRING,    sizeof(config.pid_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)},
        {"TmpDir",              &config.tmp_dir,                TYPE_STRING,    sizeof(config.tmp_dir)},
    };

    // 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]), 0) != 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_server_monitor";
        case DB_SYNC_MANAGER_PROCESS:
            return "db_sync_manager";
        case NOTIFICATION_MANAGER_PROCESS:
            return "notification_manager";
        case JOBNET_MANAGER_PROCESS:
            return "jobnet_manager";
        case FLOW_MANAGER_PROCESS:
            return "flow_manager";
        case ICON_EXECUTION_MANAGER_PROCESS:
            return "icon_execution_manager";
        case TRAPPER_MANAGER_PROCESS:
            return "trapper_manager";
        case ICON_RESULT_MANAGER_PROCESS:
            return "icon_result_manager";
        case ZABBIX_LINK_MANAGER_PROCESS:
            return "zabbix_link_manager";
        case TIMEOUT_MANAGER_PROCESS:
            return "timeout_manger";
        case HOUSEKEEPER_MANAGER_PROCESS:
            return "housekeeper_manager";
        case RECOVERY_MANAGER_PROCESS:
            return "recovery_manager";
        
        default:
            return "unknown_jobarg_server_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_server_monitor process     *
 *      process_arg_list[5] - arg[-go-plugin]                                 *
 *      process_arg_list[6] -                                                 *
 *      process_arg_list[7] -                                                 *
 *      process_arg_list[8] -                                                 *
 *      process_arg_list[9] -                                                 *
 *      process_arg_list[10] -                                                *
 *                                                                            *
 * Return value: resutl(Succeed 0 , Failed -1)                                *
 *                                                                            *
 ******************************************************************************/
 int init_process_args(int process_type, char *process_path, char process_arg_list[PROCESS_ARG_COUNT][FILE_PATH_LEN]) {
    const char *function_name = "init_process_args";
    int arg_index = 0;

    for (int i = 0; i < PROCESS_ARG_COUNT; i++) {
        memset(process_arg_list[i], 0x00, FILE_PATH_LEN);
    }

    // Arg[0]: process name
    snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s", get_process_name(process_type));
    // Arg[1]: config flag
    if (process_type == MONITOR_PROCESS) {
        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s","-t");
        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%d", 0);
        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s", "-c");
    } else {
        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s","-config-path");
    }

    // Arg[2]: config file path
    snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s", CONFIG_FILE);

    if (process_type == MONITOR_PROCESS) {
        snprintf(process_path, FILE_PATH_LEN, "%s",monitor_process_path);
    } else if (process_type < PROCESS_TYPE_COUNT) {
        snprintf(process_path, FILE_PATH_LEN,"%s", manager_process_path);

        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s","-c-monitor-pid");
        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%d", threads[0]);

        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "-go-plugin-id");
        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%d", process_type);

        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s","-target-server");
        snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s","server");

        switch (process_type) {
            case DB_SYNC_MANAGER_PROCESS:
            case NOTIFICATION_MANAGER_PROCESS:
            case JOBNET_MANAGER_PROCESS:
            case FLOW_MANAGER_PROCESS:
            case ICON_RESULT_MANAGER_PROCESS:
            case ZABBIX_LINK_MANAGER_PROCESS:
            case TIMEOUT_MANAGER_PROCESS:
            case HOUSEKEEPER_MANAGER_PROCESS:
            case RECOVERY_MANAGER_PROCESS:
                snprintf(process_arg_list[arg_index++], FILE_PATH_LEN,"%s", "-use-db");
                break;

            case TRAPPER_MANAGER_PROCESS:
                snprintf(process_arg_list[arg_index++], FILE_PATH_LEN, "%s","-use-tcp");
                break;
        }
    } else {
        ja_log("JASERVER200003", error_message, ERROR_MSG_SIZE, 
            function_name, get_process_name(process_type),errno, 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, ", ");
    }

    ja_log("JASERVER400002", 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) {
        ja_log("JASERVER200002", 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][FILE_PATH_LEN];

        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);
        }

        char *argv[PROCESS_ARG_COUNT];
        for (int i = 0; i < PROCESS_ARG_COUNT; i++) {
            argv[i] = process_arg_list[i];
        }
        argv[PROCESS_ARG_COUNT - 1] = NULL;

        ja_log("JASERVER000004", error_message, ERROR_MSG_SIZE, 
            process_type, get_process_name(process_type), 1);
        ja_log("JASERVER400004", error_message, ERROR_MSG_SIZE, 
            function_name, get_process_name(process_type), process_path);
        execv(process_path, argv);
        ja_log("JASERVER200004", 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 kill_all_thread()
{
    int exit_status;
    for (int process_type = HOUSEKEEPER_MANAGER_PROCESS;process_type >= 0;process_type--)
    {
        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;

    ja_log("JASERVER200005", error_message, ERROR_MSG_SIZE, 
        function_name);

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

    kill_all_thread();

    jaz_free(threads);

    ja_log("JASERVER000002", error_message, ERROR_MSG_SIZE, VERSION, REVISION);
	
    exit(SUCCEED);
}

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);

    // Group 1: wait for SIGUSR1 (child ready)
    for (int process_type = 0; process_type <= RECOVERY_MANAGER_PROCESS; process_type++) {
        child_ready = 0;
        execute_process(process_type);
        while (!child_ready) {
            usleep(1000);
        }
    }

    // Group 2
    for (int process_type = ICON_EXECUTION_MANAGER_PROCESS; process_type <= JOBNET_MANAGER_PROCESS; process_type++) {
        execute_process(process_type);
    }

    // Group 3
    for (int process_type = TIMEOUT_MANAGER_PROCESS; process_type <= HOUSEKEEPER_MANAGER_PROCESS; process_type++) {
        execute_process(process_type);
    }
}

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);
    }
    ja_log("JASERVER100004", error_message, ERROR_MSG_SIZE, function_name, get_process_name(process_type), pid, exit_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
            "file",                             //logging type
            config.log_file_size * 1024 * 1024, //max log file size
            config.log_level,                   //log level
            0,                                  //target type => 0 - server, 1 - agent
            error_message, 
            sizeof(error_message)) == -1){
        printf("Failed to initialize the logger: %s\n", error_message);
        return -1;
    }
    return 0;
}

int JAZ_MAIN_ENTRY(){
    int status;
    int process_index;
    
    pid_t pid;

	struct sigaction	phan;
    
    const char *function_name = "SERVER_MAIN_ENTRY";

    uid_t uid = getuid();                     // Get current user ID
    struct passwd *pw = getpwuid(uid);       // Get user info

    if (pw) {
        printf("User name: %s\n", pw->pw_name);
    } else {
        perror("getpwuid");
    }

    char path_buf[PATH_MAX];

    // tmp_dir/notificationmanager
    char nm_path[PATH_MAX];
    join_path(nm_path, sizeof(nm_path), config.tmp_dir, noti_manager);

    // Create parent: .../notificationmanager
    create_dir(nm_path);

    // tmp_dir/notificationmanager/in
    join_path(path_buf, sizeof(path_buf), nm_path, "in");
    create_dir(path_buf);

    // tmp_dir/notificationmanager/end
    join_path(path_buf, sizeof(path_buf), nm_path, "end");
    create_dir(path_buf);

    // tmp_dir/notificationmanager/err
    join_path(path_buf, sizeof(path_buf), nm_path, "error");
    create_dir(path_buf);

    ja_log("JASERVER000001", error_message, ERROR_MSG_SIZE, VERSION, REVISION);
    parent_pid = (int)getpid();
    
    threads_num = PROCESS_TYPE_COUNT;

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

    ja_log("JASERVER000003", error_message, ERROR_MSG_SIZE);
    
    start_all_processes();
    
    ja_log("JASERVER000005", error_message, ERROR_MSG_SIZE, function_name);

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

    ja_log("JASERVER000006", error_message, ERROR_MSG_SIZE, function_name);
    return 0;
}

int main(int argc, char **argv)
{
    char ch = '\0';
    int opt_c = 0;

    progname = get_program_name(argv[0]);

    // Parse the command-line arguments
    while ((char)EOF != (ch = (char)jaz_getopt_long(argc, argv, shortopts, longopts, NULL,0)))
    {
        switch (ch)
        {
        case 'c':
            opt_c++;
            if (NULL == CONFIG_FILE)
                CONFIG_FILE = jaz_strdup(CONFIG_FILE, jaz_optarg);
            break;
        case 'h':
            help_jobarg();
            exit(SUCCEED);
            break;
        case 'V':
            version_jobarg();
            exit(SUCCEED);
            break;
        case 'f':
            JAZ_TASK_FLAG_FOREGROUND = 1;
            break;
        default:
            usage();
            exit(FAIL);
            break;
        }
    }

    // Ensure the configuration file is specified
    if (1 > opt_c)
    {
        printf("Option \"-c\" or \"--config\" must be specified.\n");
        exit(FAIL);
    }

    if (NULL == CONFIG_FILE)
    {
        CONFIG_FILE = jaz_strdup(CONFIG_FILE, SYSCONFDIR "/jobarg_server.conf");
    }

    // Load the configuration file
    jaz_load_config();

    return daemon_start(CONFIG_ALLOW_ROOT);
}