// SPDX: GPL-3.0-or-later
#define FUSE_USE_VERSION 35

#include <fuse3/fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

static const char *script_path;
static int use_serial = 0;
static char serial_device[256];
static const char *END_TOKEN = "\0\0\0";

static int call_script_serial(const char *command, char **output) {
    int fd = open(serial_device, O_RDWR | O_NOCTTY);
    if (fd < 0) {
        perror("open serial device");
        return -errno;
    }

    // Write the command to the serial device
    size_t command_len = strlen(command);
    if (write(fd, command, command_len) != command_len) {
        perror("write to serial");
        close(fd);
        return -EIO;
    }

    // Read the response
    size_t size = 0;
    *output = NULL;
    char buffer[1024];
    ssize_t bytes_read;

    while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {
        *output = realloc(*output, size + bytes_read + 1);
        memcpy(*output + size, buffer, bytes_read);
        size += bytes_read;

        if (size >= 3 && memcmp(*output + size - 3, END_TOKEN, 3) == 0) {
            size -= 3; // Remove the END_TOKEN
            break;
        }
    }

    if (bytes_read < 0) {
        perror("read from serial");
        free(*output);
        close(fd);
        return -EIO;
    }

    if (*output) {
        (*output)[size] = '\0';
    }

    close(fd);
    return 0;
}

static int call_script(const char *action, const char *path, char **output) {
    char command[4096];

    snprintf(command, sizeof(command), "%s %s '%s'", script_path, action, path);

    if (use_serial) {
        return call_script_serial(command, output);
    } else {
        FILE *fp = popen(command, "r");
        if (!fp) {
            return -errno;
        }

        size_t size = 0;
        *output = NULL;
        char line[1024];
        while (fgets(line, sizeof(line), fp)) {
            size_t len = strlen(line);
            *output = realloc(*output, size + len + 1);
            memcpy(*output + size, line, len);
            size += len;
        }

        if (*output) {
            (*output)[size] = '\0';
        }

        return pclose(fp);
    }
}

static int fusescript_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi) {
    (void) fi;
    memset(stbuf, 0, sizeof(struct stat));

    if( path == NULL ) return -EINVAL;

    if (strcmp(path, "/") == 0) {
        // Root directory
        stbuf->st_mode = S_IFDIR | 0755; // Directory with permissions 0755
        stbuf->st_nlink = 2;             // Standard for directories
        return 0;
    }

    // Call the script for other paths
    char *output = NULL;
    int ret = call_script("getattr", path, &output);
    if (ret != 0) {
        free(output);
        return -EIO;
    }

    // Parse the script output
    int mode, nlink, uid, gid, size, blocks;
    if( output == NULL ) return -EIO;
    if (sscanf(output, "%d %d %d %d %d %d", &mode, &nlink, &uid, &gid, &size, &blocks) != 6) {
        free(output);
        return -EIO; // Error parsing the output
    }

    // Populate the stat structure
    stbuf->st_mode = mode;      // File mode (e.g., regular file, directory)
    stbuf->st_nlink = nlink;    // Number of hard links
    stbuf->st_uid = uid;        // User ID (owner)
    stbuf->st_gid = gid;        // Group ID
    stbuf->st_size = size;      // File size in bytes
    stbuf->st_blocks = blocks;  // Number of 512-byte blocks allocated
    stbuf->st_blksize = 512;    // Block size (optional)

    // Set default values for atime, mtime, ctime (if not handled by your script)
    stbuf->st_atime = time(NULL); // Last access time
    stbuf->st_mtime = time(NULL); // Last modification time
    stbuf->st_ctime = time(NULL); // Last status change time

    free(output);
    return 0;
}

static int fusescript_access(const char *path, int mask) {
    // Call the script for permission checks (optional)
    char *output = NULL;
    int ret = call_script("access", path, &output);
    if (ret != 0) {
        free(output);
        return 0; //-EIO;
    }

    free(output);
    return 0; // Allow all accesses for simplicity
}


static int fusescript_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                              off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
    (void) offset;
    (void) fi;
    (void) flags;

    // Add default entries for directories
    filler(buf, ".", NULL, 0, 0); // Current directory
    filler(buf, "..", NULL, 0, 0); // Parent directory

    // Call the script for additional entries
    char *output = NULL;
    int ret = call_script("readdir", path, &output);
    if (ret != 0) {
        free(output);
        return -EIO;
    }

    // Parse script output, assuming it provides one entry per line
    char *line = strtok(output, "\n");
    while (line) {
        filler(buf, line, NULL, 0, 0);
        line = strtok(NULL, "\n");
    }

    free(output);
    return 0;
}


static int fusescript_open(const char *path, struct fuse_file_info *fi) {
    (void) fi;
    char *output = NULL;
    int ret = call_script("open", path, &output);
    free(output);
    return ret == 0 ? 0 : -EIO;
}

static int fusescript_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
    (void) fi;
    char command[4096];
    snprintf(command, sizeof(command), "%s read '%s' %zu %jd", script_path, path, size, (intmax_t)offset);

    FILE *fp = popen(command, "r");
    if (!fp) {
        return -errno;
    }

    size_t bytes_read = fread(buf, 1, size, fp);
    int ret = pclose(fp);
    return ret == 0 ? bytes_read : -EIO;
}

static int fusescript_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
    (void) fi;
    char command[4096];
    snprintf(command, sizeof(command), "echo -n '%.*s' | %s write '%s' %zu %jd", (int)size, buf, script_path, path, size, (intmax_t)offset);

    int ret = system(command);
    return ret == 0 ? size : -EIO;
}

static int fusescript_mkdir(const char *path, mode_t mode) {
    char command[4096];
    snprintf(command, sizeof(command), "%s mkdir '%s' %o", script_path, path, mode);

    int ret = system(command);
    return ret == 0 ? 0 : -EIO;
}

static int fusescript_unlink(const char *path) {
    char command[4096];
    snprintf(command, sizeof(command), "%s unlink '%s'", script_path, path);

    int ret = system(command);
    return ret == 0 ? 0 : -EIO;
}

static int fusescript_rmdir(const char *path) {
    char command[4096];
    snprintf(command, sizeof(command), "%s rmdir '%s'", script_path, path);

    int ret = system(command);
    return ret == 0 ? 0 : -EIO;
}

// Add getxattr function
static int fusescript_getxattr(const char *path, const char *name, char *value, size_t size) {
    char command[4096];
    snprintf(command, sizeof(command), "%s getxattr '%s' '%s'", script_path, path, name);

    FILE *fp = popen(command, "r");
    if (!fp) {
        return -errno;
    }

    char *output = NULL;
    size_t output_len = 0;
    char line[1024];
    while (fgets(line, sizeof(line), fp)) {
        size_t len = strlen(line);
        output = realloc(output, output_len + len + 1);
        memcpy(output + output_len, line, len);
        output_len += len;
    }

    if (output) {
        output[output_len] = '\0';
    }

    int ret = pclose(fp);
    if (ret != 0 || !output) {
        free(output);
        return -EIO;
    }

    if (size == 0) {
        // Return the size of the required buffer
        free(output);
        return output_len;
    } else if (size < output_len) {
        // Buffer too small
        free(output);
        return -ERANGE;
    }

    // Copy the attribute value
    memcpy(value, output, output_len);
    free(output);
    return output_len;
}

// Add setxattr function
static int fusescript_setxattr(const char *path, const char *name, const char *value, size_t size, int flags) {
    char value_str[4096];
    snprintf(value_str, sizeof(value_str), "%.*s", (int)size, value);

    char command[4096];
    snprintf(command, sizeof(command), "%s setxattr '%s' '%s' '%s'", script_path, path, name, value_str);

    int ret = system(command);
    return ret == 0 ? 0 : -EIO;
}

// Add listxattr function
static int fusescript_listxattr(const char *path, char *list, size_t size) {
    char command[4096];
    snprintf(command, sizeof(command), "%s listxattr '%s'", script_path, path);

    FILE *fp = popen(command, "r");
    if (!fp) {
        return -errno;
    }

    char *output = NULL;
    size_t output_len = 0;
    char line[1024];
    while (fgets(line, sizeof(line), fp)) {
        size_t len = strlen(line);
        output = realloc(output, output_len + len + 1);
        memcpy(output + output_len, line, len);
        output_len += len;
    }
    fprintf(stderr, "listxattr script output: %s\n", output);
    fprintf(stderr, "listxattr output length: %zu\n", output_len);

    if (output) {
        output[output_len] = '\0';
    }

    int ret = pclose(fp);
    if (ret != 0 || !output) {
        free(output);
        return -EIO;
    }

    if (size == 0) {
        // Return the size of the required buffer
        free(output);
        return output_len;
    } else if (size < output_len) {
        // Buffer too small
        free(output);
        return -ERANGE;
    }

    // Copy the attribute list
    memcpy(list, output, output_len);
    free(output);
    return output_len;
}

static const struct fuse_operations fusescript_ops = {
    .getattr = fusescript_getattr,
    .readdir = fusescript_readdir,
    .open = fusescript_open,
    .access  = fusescript_access,
    .read = fusescript_read,
    .write = fusescript_write,
    .mkdir = fusescript_mkdir,
    .unlink = fusescript_unlink,
    .rmdir = fusescript_rmdir,
    .getxattr = fusescript_getxattr,
    .setxattr = fusescript_setxattr,
    .listxattr = fusescript_listxattr,
};

int main(int argc, char *argv[]) {
    if (argc < 3) {
        fprintf(stderr, "Usage: %s <script_or_serialdevice> <mountpoint>\n", argv[0]);
        return 1;
    }

    if (strncmp(argv[2], "/dev/", 5) == 0) {
        use_serial = 1;
        strncpy(serial_device, argv[2], sizeof(serial_device) - 1);
        serial_device[sizeof(serial_device) - 1] = '\0';

        if (argc < 4) {
            fprintf(stderr, "Missing mountpoint for serial device\n");
            return 1;
        }

        script_path = realpath(argv[1], NULL);
        if (!script_path) {
            perror("realpath");
            return 1;
        }

        argv[1] = argv[3];
        return fuse_main(argc - 2, argv + 1, &fusescript_ops, NULL);
    } else {
        script_path = realpath(argv[1], NULL);
        if (!script_path) {
            perror("realpath");
            return 1;
        }

        argv[1] = argv[2];
        return fuse_main(argc - 1, argv + 1, &fusescript_ops, NULL);
    }
}