#include <jni.h>
#include <android/log.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define LOG_TAG "GitHugNativeGit"

typedef int (*git_main_fn)(int argc, const char **argv);
typedef void (*git_init_fn)(const char **argv);

struct output_buffer {
    char *data;
    size_t length;
    size_t capacity;
};

struct reader_state {
    int fd;
    struct output_buffer output;
};

static pthread_mutex_t git_mutex = PTHREAD_MUTEX_INITIALIZER;
static void *git_handle = NULL;
static git_main_fn git_main = NULL;
static git_init_fn git_init = NULL;

static int append_output(struct output_buffer *buffer, const char *data, size_t length) {
    if (length == 0) {
        return 0;
    }
    size_t required = buffer->length + length + 1;
    if (required > buffer->capacity) {
        size_t next_capacity = buffer->capacity == 0 ? 4096 : buffer->capacity;
        while (next_capacity < required) {
            next_capacity *= 2;
        }
        char *next = realloc(buffer->data, next_capacity);
        if (next == NULL) {
            return -1;
        }
        buffer->data = next;
        buffer->capacity = next_capacity;
    }
    memcpy(buffer->data + buffer->length, data, length);
    buffer->length += length;
    buffer->data[buffer->length] = '\0';
    return 0;
}

static void *read_output(void *arg) {
    struct reader_state *state = (struct reader_state *)arg;
    char chunk[4096];
    for (;;) {
        ssize_t count = read(state->fd, chunk, sizeof(chunk));
        if (count > 0) {
            if (append_output(&state->output, chunk, (size_t)count) != 0) {
                break;
            }
        } else if (count == 0) {
            break;
        } else if (errno != EINTR) {
            break;
        }
    }
    close(state->fd);
    return NULL;
}

static jobjectArray make_result(JNIEnv *env, int exit_code, const char *output) {
    jclass string_class = (*env)->FindClass(env, "java/lang/String");
    jobjectArray result = (*env)->NewObjectArray(env, 2, string_class, NULL);
    char exit_text[32];
    snprintf(exit_text, sizeof(exit_text), "%d", exit_code);
    jstring exit_string = (*env)->NewStringUTF(env, exit_text);
    jstring output_string = (*env)->NewStringUTF(env, output != NULL ? output : "");
    (*env)->SetObjectArrayElement(env, result, 0, exit_string);
    (*env)->SetObjectArrayElement(env, result, 1, output_string);
    (*env)->DeleteLocalRef(env, exit_string);
    (*env)->DeleteLocalRef(env, output_string);
    return result;
}

static jobjectArray make_error(JNIEnv *env, const char *message) {
    return make_result(env, -1, message);
}

static int load_git(const char *library_path) {
    if (git_init != NULL && git_main != NULL) {
        return 0;
    }
    git_handle = dlopen(library_path, RTLD_NOW | RTLD_GLOBAL);
    if (git_handle == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "dlopen failed: %s", dlerror());
        return -1;
    }
    git_init = (git_init_fn)dlsym(git_handle, "init_git");
    if (git_init == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "dlsym init_git failed: %s", dlerror());
        return -1;
    }
    git_main = (git_main_fn)dlsym(git_handle, "cmd_main");
    if (git_main == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "dlsym cmd_main failed: %s", dlerror());
        return -1;
    }
    return 0;
}

static char **copy_string_array(JNIEnv *env, jobjectArray source, int *count_out) {
    int count = source == NULL ? 0 : (*env)->GetArrayLength(env, source);
    char **values = calloc((size_t)count + 1, sizeof(char *));
    if (values == NULL) {
        return NULL;
    }
    for (int i = 0; i < count; i++) {
        jstring item = (jstring)(*env)->GetObjectArrayElement(env, source, i);
        const char *chars = (*env)->GetStringUTFChars(env, item, NULL);
        values[i] = strdup(chars != NULL ? chars : "");
        (*env)->ReleaseStringUTFChars(env, item, chars);
        (*env)->DeleteLocalRef(env, item);
        if (values[i] == NULL) {
            for (int j = 0; j < i; j++) {
                free(values[j]);
            }
            free(values);
            return NULL;
        }
    }
    *count_out = count;
    return values;
}

static void free_string_array(char **values, int count) {
    if (values == NULL) {
        return;
    }
    for (int i = 0; i < count; i++) {
        free(values[i]);
    }
    free(values);
}

struct saved_env {
    char *name;
    char *previous;
    int had_previous;
};

static void restore_environment(struct saved_env *saved, int count) {
    for (int i = count - 1; i >= 0; i--) {
        if (saved[i].had_previous) {
            setenv(saved[i].name, saved[i].previous, 1);
        } else {
            unsetenv(saved[i].name);
        }
        free(saved[i].name);
        free(saved[i].previous);
    }
    free(saved);
}

static struct saved_env *apply_environment(char **entries, int count, int *applied_out) {
    struct saved_env *saved = calloc((size_t)count, sizeof(struct saved_env));
    if (saved == NULL) {
        return NULL;
    }
    int applied = 0;
    for (int i = 0; i < count; i++) {
        char *separator = strchr(entries[i], '=');
        if (separator == NULL || separator == entries[i]) {
            continue;
        }
        *separator = '\0';
        const char *name = entries[i];
        const char *value = separator + 1;
        const char *previous = getenv(name);
        saved[applied].name = strdup(name);
        saved[applied].previous = previous == NULL ? NULL : strdup(previous);
        saved[applied].had_previous = previous != NULL;
        if (saved[applied].name == NULL || (previous != NULL && saved[applied].previous == NULL)) {
            *separator = '=';
            restore_environment(saved, applied + 1);
            return NULL;
        }
        setenv(name, value, 1);
        applied++;
        *separator = '=';
    }
    *applied_out = applied;
    return saved;
}

JNIEXPORT jobjectArray JNICALL
Java_solutions_tretter_githugandroid_NativeGitBridge_runGitMainNative(
    JNIEnv *env,
    jclass clazz,
    jstring library_path,
    jstring working_directory,
    jobjectArray argv_array,
    jobjectArray environment_array
) {
    (void)clazz;
    const char *library_path_chars = (*env)->GetStringUTFChars(env, library_path, NULL);
    const char *working_directory_chars = (*env)->GetStringUTFChars(env, working_directory, NULL);
    int argc = 0;
    int envc = 0;
    char **argv = copy_string_array(env, argv_array, &argc);
    char **env_entries = copy_string_array(env, environment_array, &envc);
    if (library_path_chars == NULL || working_directory_chars == NULL || argv == NULL || env_entries == NULL) {
        return make_error(env, "Native Git execution failed: out of memory");
    }

    int pipe_fds[2];
    if (pipe(pipe_fds) != 0) {
        free_string_array(argv, argc);
        free_string_array(env_entries, envc);
        (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars);
        (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars);
        return make_error(env, "Native Git execution failed: could not create output pipe");
    }

    pthread_mutex_lock(&git_mutex);

    pid_t child = fork();
    if (child < 0) {
        close(pipe_fds[0]);
        close(pipe_fds[1]);
        pthread_mutex_unlock(&git_mutex);
        free_string_array(argv, argc);
        free_string_array(env_entries, envc);
        (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars);
        (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars);
        return make_error(env, "Native Git execution failed: could not fork");
    }

    if (child == 0) {
        close(pipe_fds[0]);
        dup2(pipe_fds[1], STDOUT_FILENO);
        dup2(pipe_fds[1], STDERR_FILENO);
        close(pipe_fds[1]);

        int applied_env = 0;
        struct saved_env *saved_env = apply_environment(env_entries, envc, &applied_env);
        (void)saved_env;
        chdir(working_directory_chars);

        if (load_git(library_path_chars) != 0) {
            const char *error = dlerror();
            fprintf(stderr, "%s\n", error != NULL ? error : "Native Git execution failed: init_git/cmd_main not found");
            fflush(stdout);
            fflush(stderr);
            _exit(127);
        }

        git_init((const char **)argv);
        int child_exit_code = git_main(argc, (const char **)argv);
        fflush(stdout);
        fflush(stderr);
        _exit(child_exit_code);
    }

    close(pipe_fds[1]);
    struct reader_state reader = { .fd = pipe_fds[0], .output = {0} };
    read_output(&reader);

    int child_status = 0;
    while (waitpid(child, &child_status, 0) < 0 && errno == EINTR) {
    }
    pthread_mutex_unlock(&git_mutex);

    int exit_code = -1;
    if (WIFEXITED(child_status)) {
        exit_code = WEXITSTATUS(child_status);
    } else if (WIFSIGNALED(child_status)) {
        exit_code = 128 + WTERMSIG(child_status);
    }

    jobjectArray result = make_result(env, exit_code, reader.output.data);

    free(reader.output.data);
    free_string_array(argv, argc);
    free_string_array(env_entries, envc);
    (*env)->ReleaseStringUTFChars(env, library_path, library_path_chars);
    (*env)->ReleaseStringUTFChars(env, working_directory, working_directory_chars);
    return result;
}
