#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <time.h>
#include <getopt.h>
#include <pthread.h>

#include "cfg.h"
#include "logger.h"

#include "../../libs/clibs/jacommon.h"
#include "../../libs/clibs/common.h"
#include "../../libs/clibs/fatal.h"

const char	*progname = NULL;
const char	title_message[] = "Job Arranger Server - Monitor";
const char	usage_message[] = "[-h] [-c <file>]";
const char	*help_message[] = {
	"Options:",
	"  -c -config-path <file>   Absolute path to the configuration file",
	"  -h -help            Give this help",
	NULL	/* end of text */
};

static char shortopts[] = "t:c:h";
int JOBARG_TYPE = -1;
volatile sig_atomic_t msg_ready = 0;
char msg_buf[128];

char error_message[ERROR_MSG_SIZE] = {0};

static struct option longopts[] = {
    {"config-path", required_argument, NULL, 'c'},
    {"help", no_argument, NULL, 'h'},
    {NULL, 0, NULL, 0}
};

typedef struct {
    pid_t   pid;         // Process ID (key)
    int     signal;        // Last received signal
    time_t  timestamp;  // Last heartbeat time
} ProcessInfo;

typedef struct {
    ProcessInfo *process_info;
    size_t count;
    size_t capacity;
    pthread_mutex_t lock;
} ProcessRegistry;

ProcessRegistry process_registry;

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

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

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

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

void argument_checker(int argc, char *argv[]){
    int opt = 0;
    int option_index = 0;

    while ((opt = getopt_long(argc, argv, shortopts, longopts, &option_index)) != -1){
        printf("Option: %d, arg: %s \n", opt, optarg);
        switch (opt)
        {
        case 'c':
            if (!optarg) { fprintf(stderr, "Option -c requires argument\n"); exit(FAIL); }
                if (CONFIG_FILE) free(CONFIG_FILE);
                CONFIG_FILE = strdup(optarg);
                if (!CONFIG_FILE) { fprintf(stderr, "Failed to allocate memory for CONFIG_FILE\n"); exit(FAIL); }
                printf("Config path: %s\n", CONFIG_FILE);
                break;
        case 't':
            if (!optarg) { fprintf(stderr, "Option -t requires argument\n"); exit(FAIL); }
            JOBARG_TYPE = atoi(optarg);

            if (JOBARG_TYPE != 0 && JOBARG_TYPE != 1) {
                fprintf(stderr, "Invalid value for -t: %s. Must be 0 (server) or 1 (agent).\n", optarg);
                exit(FAIL);
            }

            printf("JOBARG_TYPE: %d (%s)\n", JOBARG_TYPE,
                JOBARG_TYPE == 0 ? "server" : "agent");
            break;
        case 'h':
            help();
            exit(SUCCEED);
            break;
        default:
            usage();
            exit(FAIL);
            break;
        }
    }
    if (CONFIG_FILE[0] == '\0') {
        fprintf(stderr, "Error: --config-path is a required option\n");
        usage();
        exit(FAIL);
    }
}

int init_process_registry(ProcessRegistry *process_registry) {
    process_registry->process_info = malloc(sizeof(ProcessInfo) * MAX_ARRAY_CNT);
    if (!process_registry->process_info) return -1;
    process_registry->count = 0;
    process_registry->capacity = MAX_ARRAY_CNT;
    return pthread_mutex_init(&process_registry->lock, NULL);
}

void free_process_registry(ProcessRegistry *process_registry) {
    pthread_mutex_destroy(&process_registry->lock);
    free(process_registry->process_info);
}

static int resize_process_registry(ProcessRegistry *process_registry) {
    if (process_registry->count >= process_registry->capacity) {
        size_t new_cap = process_registry->capacity + MAX_ARRAY_CNT;
        ProcessInfo *new_info = realloc(process_registry->process_info, sizeof(ProcessInfo) * new_cap);
        if (!new_info) return -1;
        process_registry->process_info = new_info;
        process_registry->capacity = new_cap;
    }
    return 0;
}

void remove_process(ProcessRegistry *process_registry, pid_t pid) {
    pthread_mutex_lock(&process_registry->lock);

    for (size_t i = 0; i < process_registry->count; ++i) {
        if (process_registry->process_info[i].pid == pid) {
            process_registry->process_info[i] = process_registry->process_info[process_registry->count - 1];  // swap with last
            process_registry->count--;
            int len = snprintf(msg_buf, sizeof(msg_buf),
                       "Remove process with PID %d\n", pid);
            break;
        }
    }
    pthread_mutex_unlock(&process_registry->lock);
}

int get_expected_time_diff(int signal) {
    if (signal == RT_SIGNAL1) {
        return 30;
    } else if (signal == RT_SIGNAL2) {
        return 60;
    } else if (signal == RT_SIGNAL3) {
        return 90;
    } else if (signal == RT_SIGNAL4) {
        return 120;
    } else if (signal == RT_SIGNAL5) {
        return 150;
    } else {
        return 0;
    }
}

void monitor_process() {
    const char *function_name = "monitor_process";
    time_t now = time(NULL);

    pthread_mutex_lock(&process_registry.lock);

    for (size_t i = 0; i < process_registry.count; ++i) {
        ProcessInfo *info = &process_registry.process_info[i];
        int allowed_diff = get_expected_time_diff(info->signal);
        int actual_diff = (int)(now - info->timestamp);

        if (allowed_diff == 0) {
            // printf("[MONITOR0] Unknown signal %d from PID %d\n", info->signal, info->pid);
            ja_log("JAMONITOR300001", error_message, ERROR_MSG_SIZE, function_name, info->signal, info->pid);
            continue;
        }

        if (allowed_diff > 0 && ( actual_diff > allowed_diff)) {
            // printf("[MONITOR0] PID:%d timeout. Expected time diff: %d, Signal time diff:%d\n",process_registry.process_info[i].pid, allowed_diff, actual_diff);
            ja_log("JAMONITOR300002", error_message, ERROR_MSG_SIZE, function_name, process_registry.process_info[i].pid, allowed_diff, actual_diff);
            kill(process_registry.process_info[i].pid, SIGHUP);
            remove_process(&process_registry, process_registry.process_info[i].pid);
        }
    }

    pthread_mutex_unlock(&process_registry.lock);
}

void register_process(ProcessRegistry *process_registry, pid_t pid, int signal) {
    const char *function_name = "register_process";
    char err_msg[ERROR_MSG_SIZE] = {0};
    pthread_mutex_lock(&process_registry->lock);
    int available_index = -1;
    time_t now = time(NULL);
    msg_ready = 1; // flag that a message is ready

    for (size_t i = 0; i < process_registry->count; ++i) {
        if (process_registry->process_info[i].pid == pid) {
            process_registry->process_info[i].signal = signal;
            process_registry->process_info[i].timestamp = now;
            pthread_mutex_unlock(&process_registry->lock);
            int len = snprintf(msg_buf, sizeof(msg_buf),
                       "Updated existing PID: %d with signal: %d", pid, signal);
            return;
        } else if(process_registry->process_info[i].pid == 0){
            available_index = process_registry->process_info[i].pid;
        }
    }

    if (resize_process_registry(process_registry) == 0) {
        process_registry->process_info[process_registry->count].pid = pid;
        process_registry->process_info[process_registry->count].signal = signal;
        process_registry->process_info[process_registry->count].timestamp = now;
        process_registry->count++;
        int len = snprintf(msg_buf, sizeof(msg_buf),
                       "Registered new PID %d with signal %d\n", pid, signal);
    }

    pthread_mutex_unlock(&process_registry->lock);
}

void signal_handler(int signal, siginfo_t *info, void *context) {
    const char *function_name = "signal_handler";
    char err_msg[ERROR_MSG_SIZE] = {0};
    switch (signal){
        case SIGTERM:
            exit(EXIT_SIGTERM);
            break;

		case SIGSEGV:
        case SIGILL:
        case SIGFPE:
        case SIGBUS:
            print_fatal_info(signal, info, context);
            exit(SUCCEED);

        default:
            if(signal == SIGRTMIN){
                remove_process(&process_registry, info->si_pid);
                break;
            }
            register_process(&process_registry, info->si_pid, signal);
            break;
    }
}

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
            JOBARG_TYPE,                                  //target type => 0 - server, 1 - agent
            error_message, 
            sizeof(error_message)) == -1){
        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)},
    };

    // 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]), JOBARG_TYPE) != 0)
    {
        exit(FAIL);
    }
}

void init_signal_handler(){
    const char *function_name = "init_signal_handler";
    struct sigaction sa;
    sa.sa_flags = SA_SIGINFO;  // Use the SA_SIGINFO flag to get siginfo_t
    sa.sa_sigaction = signal_handler;  // Set the signal handler function

   // 🔴 Block ALL signals while handling one signal
    sigfillset(&sa.sa_mask);

    // Register signal handler
    if (sigaction(RT_SIGNAL, &sa, NULL) == -1) {
        ja_log("JAMONITOR200006", error_message, ERROR_MSG_SIZE, function_name);
        exit(1);
    }
    if (sigaction(RT_SIGNAL1, &sa, NULL) == -1) {
        ja_log("JAMONITOR200007", error_message, ERROR_MSG_SIZE, function_name);
        exit(1);
    }
    if (sigaction(RT_SIGNAL2, &sa, NULL) == -1) {
        ja_log("JAMONITOR200008", error_message, ERROR_MSG_SIZE, function_name);
        exit(1);
    }
    if (sigaction(RT_SIGNAL3, &sa, NULL) == -1) {
        ja_log("JAMONITOR200009", error_message, ERROR_MSG_SIZE, function_name);
        exit(1);
    }
    if (sigaction(SIGTERM, &sa, NULL) == -1) {
        ja_log("JAMONITOR200010", error_message, ERROR_MSG_SIZE, function_name);
        exit(1);
    }
    if (sigaction(SIGSEGV, &sa, NULL) == -1) exit(1);
    if (sigaction(SIGILL, &sa, NULL) == -1) exit(1);
    if (sigaction(SIGBUS, &sa, NULL) == -1) exit(1);
    if (sigaction(SIGFPE, &sa, NULL) == -1) exit(1);
}

int main(int argc, char *argv[]){
    const char *function_name = "main";
    progname = get_program_name(argv[0]);
    pid_t ppid = getppid();

    if (kill(ppid, SIGRTMAX) == -1) {
        fprintf(stderr, "Failed to send signal to %d: %s\n", 
        ppid, strerror(errno));
    }
 
    argument_checker(argc,argv);

    jaz_load_config();

    if (init_logging() != 0) {
        ja_log("JAMONITOR200001", error_message, ERROR_MSG_SIZE, function_name);
        return 1;
    }

    ja_log("JAMONITOR400001", error_message, ERROR_MSG_SIZE, function_name, progname);

    if (init_process_registry(&process_registry) != 0) {
        ja_log("JAMONITOR200002", error_message, ERROR_MSG_SIZE, function_name);
        return 1;
    }

    init_signal_handler();
  
    ja_log("JAMONITOR400002", error_message, ERROR_MSG_SIZE, function_name);

    while(ppid == getppid()){
        monitor_process();
        if(msg_ready) {
            ja_log("JAMONITOR400004", error_message, ERROR_MSG_SIZE, function_name,msg_buf);
            msg_ready = 0;
        }
        sleep(1);
    }

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

    return 0;

}
