#include "fuse.hpp"
#include "handler.hpp"
#include <fnmatch.h>
#include <dirent.h>
#include <vector>
#define FUSE_USE_VERSION 31
#include <fuse.h> // libfuse-dev; -lfuse


#define abs2rel(path) while (*path == '/') path++ // TODO: more sanitization needed?

typedef struct {
    bool is_fd;
    union {
        int fd;
        struct {
            char* buf;
            off_t len;
        };
    };
} read_ctx_t;


static struct {
    const config_t* config;
    int src_fd;
    struct stat src_stat;
} ctx = {};


static bool filter_match(const char* path, bool is_fn=false) {
    if (!*ctx.config->filter) {
        return true;
    }
    if (!is_fn) {
        const char* p = strrchr(path, '/');
        if (p) {
            path = p+1;
        }
    }
    return fnmatch(ctx.config->filter, path, 0) == 0;
}


// call external handler to get list of injected files
static int readdir_ext(const char* path, char*& buf, std::vector<const char*>& list) {
    int len = handler(ctx.config->list_handler, path, ctx.src_fd, -1, buf);
    if (len < 0) {
        return len;
    }

    char* p = buf;
    while (*p) {
        char* nl = strchr(p, '\n'); // ok as 0-terminated
        if (nl == p) { // empty line?
            p++;
        } else if (nl) {
            *nl = '\0';
            if (!strchr(p, '/')) {
                if (filter_match(p, true)) {
                    for (std::vector<const char*>::iterator it = list.begin(); it != list.end(); it++) {
                        if (strcmp(p, *it) == 0) {
                            LOG("'%s' returned '%s' for '%s' again, skipping", ctx.config->list_handler, *it, path);
                            list.erase(it);
                            break;
                        }
                    }
                    list.push_back(p);
                }
            }
            p = nl+1;
        } else { // *p but no newline
            free(buf);
            LOG("cannot parse '%s' result for '%s'", ctx.config->list_handler, path);
            return -EBADMSG;
        }
    }

    LOG_DEBUG("got %zu new entries for '%s'", list.size(), path);
    return 0;
}


// call external handler to get list of injected files, returns whether a file is found
static bool readdir_ext(const char* path) {
    if (!filter_match(path)) {
        return false;
    }

    // TODO: some better solution
    char dirname[PATH_MAX];
    if (strlen(path) >= sizeof(dirname)) {
        LOG_ERR(E2BIG, "'%s'", path);
        return false;
    }
    strcpy(dirname, path);
    char* p = strrchr(dirname, '/');
    if (p) { // not in root
        *p = '\0';
    }

    // ask list handler for parent dir
    char* ext_buf = NULL;
    std::vector<const char*> ext_list;
    int rv = readdir_ext(p? dirname: "", ext_buf, ext_list);
    if (rv < 0) {
        return false;
    }

    // search through results
    bool found = false;
    for (std::vector<const char*>::iterator it = ext_list.begin(); it != ext_list.end(); it++) {
        if (strcmp(*it, p? p+1: path) == 0) {
            found = true;
            break;
        }
    }
    free(ext_buf);
    ext_list.clear();

    LOG_DEBUG("'%s': %sfound", path, found? "": "not ");
    return found;
}


// fuse readdir callback
static int readdir_cb(const char* path, void* buf, fuse_fill_dir_t filler, off_t off, struct fuse_file_info* fi) {
    LOG_DEBUG("readdir '%s'", path);

    // get dir handle
    abs2rel(path);
    int fd = *path? openat(ctx.src_fd, path, O_RDONLY|O_DIRECTORY): dup(ctx.src_fd);
    if (fd == -1) {
        int e = errno;
        LOG_ERRNO("cannot open '%s' for readdir", path);
        return -e;
    }
    DIR* dp = fdopendir(fd);
    if (dp == NULL) {
        int e = errno;
        LOG_ERRNO("cannot open '%s' for readdir ptr", path);
        (void)close(fd);
        return -e;
    }
    rewinddir(dp); // in case it was dup'ed above
    int rv = 0;

    // get all injected files for this dir
    char* ext_buf = NULL;
    std::vector<const char*> ext_list;
    if ((rv = readdir_ext(path, ext_buf, ext_list)) < 0) {
        (void)closedir(dp);
        return rv;
    }

    // readdir
    struct dirent entry;
    struct dirent* result;
    while (true) {
        rv = readdir_r(dp, &entry, &result);
        if (rv != 0) break;
        if (!result) break;

        // we iterate over it all again as we don't trust the external tool to ensure uniqueness. otherwise, we could simply pass the filler.
        for (std::vector<const char*>::iterator it = ext_list.begin(); it != ext_list.end(); it++) {
            if (strcmp(entry.d_name, *it) == 0) {
                LOG("'%s' already exists in '%s', skipping", *it, path);
                ext_list.erase(it);
                break;
            }
        }

        LOG_DEBUG("* '%s'", entry.d_name);
        if (filler(buf, entry.d_name, NULL, 0) != 0) { // TODO: allow seeking TODO: provide stat?
            rv = E2BIG;
            break;
        }
    }

    // bail out upon err
    (void)closedir(dp);
    if (rv != 0) {
        LOG_ERR(rv, "readdir '%s'", path);
        if (ext_buf) {
            free(ext_buf);
        }
        return -rv;
    }

    // add injected files last
    for (std::vector<const char*>::iterator it = ext_list.begin(); it != ext_list.end(); it++) {
        LOG_DEBUG("+ '%s'", *it);
        if (filler(buf, *it, NULL, 0) != 0) {
            free(ext_buf);
            LOG_ERR(E2BIG, "readdir+ '%s'", path);
            return -E2BIG;
        }
    }
    if (ext_buf) {
        free(ext_buf);
    }

    return 0;
}


// provide dummy attrs for (yet) unexisting files
static int getattr_ext(const char* path, struct stat* stbuf) {
    // ask list handler for parent dir
    if (!readdir_ext(path)) {
        return -ENOENT;
    }

    time_t now = time(NULL);
    stbuf->st_mode = S_IFREG | (ctx.src_stat.st_mode & (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)); // take root's r/w permission mask
    stbuf->st_nlink = 1;
    stbuf->st_uid = geteuid(); // but our own id
    stbuf->st_gid = getegid();
    stbuf->st_size = 0;
    stbuf->st_atime = now;
    stbuf->st_mtime = now;
    stbuf->st_ctime = now;

    return 0;
}


// fuse stat callback
static int getattr_cb(const char* path, struct stat* stbuf) {
    LOG_DEBUG("getattr '%s'", path);

    abs2rel(path);
    if ((*path? fstatat(ctx.src_fd, path, stbuf, 0): fstat(ctx.src_fd, stbuf)) == -1) {
        if (errno == ENOENT) {
            return *path? getattr_ext(path, stbuf): -ENOENT;
        }
        int e = errno;
        LOG_ERRNO("getattr '%s'", path);
        return -e;
    }

    return 0;
}


// call external handler for file contents streamed to disk (persistent)
static int open_ext(const char* path) {
    if (!readdir_ext(path)) {
        return -ENOENT;
    }

    int fd = openat(ctx.src_fd, path, O_CREAT|O_EXCL|O_RDWR, ctx.src_stat.st_mode & (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)); // TODO: tempfile & atomic move instead?
    if (fd == -1) {
        int e = errno; // exists? -> race
        LOG_ERRNO("cannot create '%s'", path);
        return -e;
    }

    char* dummy;
    int rv = handler(ctx.config->file_handler, path, ctx.src_fd, fd, dummy);
    if (rv < 0) {
        (void)unlinkat(ctx.src_fd, path, 0);
        (void)close(fd);
        return rv;
    }

    return fd;
}


// call external handler for file contents kept in memory (non-persistent)
static int open_ext(const char* path, char*& buf, off_t& len) {
    if (!readdir_ext(path)) {
        return -ENOENT;
    }
    len = handler(ctx.config->file_handler, path, ctx.src_fd, -1, buf);
    return (len < 0)? len: 0;
}


// fuse open callback
static int open_cb(const char *path, struct fuse_file_info* fi) {
    LOG_DEBUG("open '%s'", path);

    if ((fi->flags & O_WRONLY) || (fi->flags & O_RDWR)) {
        return -EROFS; // not sure if needed
    }
    fi->direct_io = 1;
    fi->keep_cache = 0;

    // get fd by local file or newly created one
    abs2rel(path);
    if (!*path) return -EISDIR;
    int fd = openat(ctx.src_fd, path, O_RDONLY); // O_NOFOLLOW?
    if (fd == -1) {
        if (errno != ENOENT) {
            return -errno;
        }
        if (ctx.config->persistent) { // create file now from external tool
            fd = open_ext(path);
            if (fd < 0) { // valid fd otherwise
                return fd;
            }
        }
    }

    // pass along fd or buf with file contents
    read_ctx_t* data = (read_ctx_t*)malloc(sizeof(read_ctx_t));
    if (fd != -1) {
        data->is_fd = true;
        data->fd = fd;
    } else {
        data->is_fd = false;
        int rv = open_ext(path, data->buf, data->len);
        if (rv < 0) {
            free(data);
            return rv;
        }
    }
    fi->fh = (intptr_t)data;

    return 0;
}


// fuse close callback
static int release_cb(const char* path, struct fuse_file_info* fi) {
    LOG_DEBUG("release '%s'", path);
    if (!fi->fh) {
        return -EINVAL;
    }

    read_ctx_t* data = (read_ctx_t*)fi->fh;
    if (data->is_fd) {
        (void)close(data->fd);
    } else {
        free(data->buf);
    }
    free(data);
    fi->fh = 0;

    return 0;
}


// fuse read callback
static int read_cb(const char* path, char* buf, size_t len, off_t off, struct fuse_file_info* fi) {
    LOG_DEBUG("read '%s': %ld:%zu", path, (long)off, len);

    // get fd or buf from open
    if (!fi->fh) {
        return -EINVAL;
    }
    const read_ctx_t* data = (read_ctx_t*)fi->fh;

    int rv = 0;
    if (data->is_fd) {
        while (rv < (int)len) {
            ssize_t r = pread(data->fd, buf+rv, len-rv, off+rv);
            if (r == 0) {
                break; // eof
            } else if (r < 0) {
                rv = -errno;
                break;
            } else {
                rv += r;
            }
        }
    } else {
        if (off >= data->len) {
            return 0;
        } else if (off + (off_t)len > data->len) {
            len = data->len - off;
        }

        memcpy(buf, data->buf, len);
        rv = len;
    }

    if (rv < 0) {
        LOG_ERR(-rv, "cannot read from '%s'", path);
    }
    return rv;
}


// actual main
bool fifuma_fuse_loop(const config_t& config) {
    LOG_DEBUG("source: '%s'", config.src);
    LOG_DEBUG("mountpoint: '%s'", config.dst);
    LOG_DEBUG("persistent: %d", config.persistent? 1: 0);
    LOG_DEBUG("filter: '%s'", *config.filter? config.filter: "*");
    LOG_DEBUG("dir-handler: '%s'", config.list_handler);
    LOG_DEBUG("file-handler: '%s'", config.file_handler);

    // setup global context
    ctx.config = &config;
    ctx.src_fd = open(config.src, O_RDONLY|O_DIRECTORY);
    if (ctx.src_fd == -1) { // get fd before mounting
        LOG_ERRNO("open(%s,O_DIRECTORY)", config.src);
        return 1;
    }
    if (fstat(ctx.src_fd, &ctx.src_stat) == -1) {
        LOG_ERRNO("fstat(%s)", ctx.config->src);
        return -EBADF;
    }

    // setup fuse args
    bool overlay = (strcmp(config.src, config.dst) == 0);
    const char* argv[] = {
        "fifuma",
        "-s", // single-threaded
        "-f", // foreground
        "-o",
        overlay? "nonempty,ro,auto_unmount,default_permissions": "ro,auto_unmount,default_permissions",
        config.dst, // mountpoint
        NULL
    };
    int argc = (sizeof(argv) / sizeof(*argv)) - 1;

    // setup fuse hooks
    struct fuse_operations ops = {};
    ops.getattr = getattr_cb;
    ops.open = open_cb;
    ops.read = read_cb;
    ops.readdir = readdir_cb;
    ops.release = release_cb;

    LOG_DEBUG("starting up...");
    return fuse_main(argc, (char**)argv, &ops, &ctx) == 0;
}