#include <memory>
#include <fstream>
#include <stack>
#include <unordered_set>
#include "server_internal.hxx"

mrpc::Node node_to_node(const std::shared_ptr<Node>& node) {
    return {
        .id = node->id,
        .name = node->name,
        .file = node->file,
        .preview = node->preview,
        .parent = node->id == 0 ? std::nullopt : std::optional{node->parent->id},
        .size = node->file ? std::optional{node->size} : std::nullopt,
        .children = std::nullopt
    };
}

std::shared_ptr<Node> get_node(const std::shared_ptr<User>& user, std::uint64_t id) {
    std::shared_lock lock{user->node_lock};
    const auto &entry = user->nodes.find(id);
    return entry == user->nodes.end() ? nullptr : entry->second;
}

std::string get_path(std::shared_ptr<Node> node) {
    std::string ret;
    while(node) {
        if (node->id != 0) {
            ret.insert(0, node->name);
            ret.insert(0, "/");
        }
        node = node->parent;
    }
    return ret.empty() ? "/" : ret;
}

void Server::delete_node(const std::shared_ptr<User> &user, std::uint64_t id, const std::function<void(std::string)>& log) {
    std::unique_lock lock{user->node_lock};
    std::stack<std::shared_ptr<Node>> todo;
    {
        auto start = user->nodes.find(id);
        if (start == user->nodes.end()) return;
        todo.push(start->second);
    }
    while (!todo.empty()) {
        auto node = todo.top();
        auto log_path = get_path(node);

        if (!node->children.empty()) {
            log("Entering " + log_path + "\n");
            for (const auto &child : node->children)
                todo.push(child);
            node->children.clear();
            continue;
        }

        log("Deleting " + log_path + "...");
        if (node->file) {
            auto path = user->user_dir / std::to_string(node->id);
            std::filesystem::remove(path);
            if (node->preview)
                std::filesystem::remove(path.replace_extension("png"));
        }
        if (node->parent)
            node->parent->children.remove(node);
        node->parent.reset();
        user->nodes.erase(node->id);
        log(" Done\n");

        todo.pop();
    }
}

std::uint64_t Server::nodes_size(const std::shared_ptr<User> &user, const std::vector<std::uint64_t> &ids) {
    std::uint64_t total = 0;
    std::deque<Node*> todo;
    for (const auto &id : ids) {
        auto node = get_node(user, id);
        if (node)
            todo.push_back(node.get());
    }

    while (!todo.empty()) {
        auto node = todo.front();
        if (node->file) {
            total += node->size;
        } else {
            for (const auto &child : node->children)
                todo.push_back(child.get());
        }
        todo.pop_front();
    }

    return total;
}

mrpc::Response<mrpc::Node> Server::FS_get_node(std::string &&token, std::uint64_t &&node_id) {
    check_user_response();
    auto node = get_node(user, node_id);
    if (!node) return { .e = "Invalid node" };

    mrpc::Node n = node_to_node(node);
    if (!node->file) {
        std::vector<mrpc::Node> children;
        children.reserve(node->children.size());
        for (const auto &child : node->children)
            children.push_back(node_to_node(child));
        n.children = std::move(children);
    }
    return { .o = std::move(n) };
}

mrpc::Response<std::vector<mrpc::PathSegment>> Server::FS_get_path(std::string &&token, std::uint64_t &&node_id) {
    check_user_response();
    std::shared_lock lock{user->node_lock};
    auto node = get_node(user, node_id);
    if (!node) return { .e = "Invalid node" };

    std::vector<mrpc::PathSegment> segments;
    while (node) {
        if (node->file) {
            segments.push_back(std::move(mrpc::PathSegment { .name = node->name }));
        } else {
            segments.push_back(std::move(mrpc::PathSegment { .name = node->id == 0 ? "/" : node->name, .id = node->id }));
        }
        node = node->parent;
    }
    std::reverse(segments.begin(), segments.end());

    return { .o = std::move(segments) };
}

mrpc::Response<std::uint64_t> Server::FS_get_nodes_size(std::string &&token, std::vector<std::uint64_t> &&nodes) {
    check_user_response();
    std::shared_lock lock{user->node_lock};
    return { .o = nodes_size(user, nodes) };
}

mrpc::Response<mrpc::CreateNodeInfo> Server::FS_create_node(std::string &&token, bool &&file, std::uint64_t &&parent_id, std::string &&name) {
    check_user_response();
    auto parent = get_node(user, parent_id);
    if (!parent) return { .e = "Invalid parent" };

    {
        std::unique_lock lock{user->node_lock};

        std::shared_ptr<Node> child = nullptr;
        for (const auto &c: parent->children) {
            if (c->name == name) {
                child = c;
                break;
            }
        }
        if (child)
            return {.o = mrpc::CreateNodeInfo{
                .id = child->id,
                .exists = true,
                .file = child->file
            }};
        auto id = user->next_node_id++;
        child = std::make_shared<Node>(Node{
            .id = id,
            .name = name,
            .file = file,
            .preview = false,
            .parent = parent,
            .size = 0
        });
        if (file) {
            auto path = files_dir / std::to_string(user->id) / std::to_string(id);
            std::ofstream{path};
        }
        user->nodes.emplace(id, child);
        parent->children.push_back(child);
        save();
        return {.o = mrpc::CreateNodeInfo{
            .id = id,
            .exists = false,
            .file = file
        }};
    }
}

std::optional<std::string> Server::FS_move_nodes(std::string &&token, std::vector<std::uint64_t> &&node_ids_vec, std::uint64_t &&parent_id) {
    check_user_optional();
    auto parent = get_node(user, parent_id);
    if (!parent) return "Invalid parent";

    if (node_ids_vec.empty())
        return std::nullopt;

    std::unordered_set<std::uint64_t> node_ids{node_ids_vec.begin(), node_ids_vec.end()};

    {
        std::unique_lock lock{user->node_lock};

        {
            auto node_parent = parent;
            while (parent) {
                if (node_ids.contains(node_parent->id))
                    return "Tried to move node into one of it's subfolders";
                node_parent = node_parent->parent;
            }
        }

        std::vector<std::shared_ptr<Node>> nodes;
        std::unordered_set<std::string> node_names;
        nodes.reserve(node_ids.size());
        node_names.reserve(node_ids.size());
        for (auto id : node_ids) {
            auto node = get_node(user, id);
            if (!node) return "Invalid node " + std::to_string(id);
            if (!node_names.emplace(node->name).second)
                return "Tried to move multiple nodes with the name " + node->name;
            nodes.push_back(node);
        }

        for (const auto &child : parent->children) {
            if (node_names.contains(child->name))
                return "Tried to overwrite existing folder/file";
        }

        for (const auto &node : nodes) {
            node->parent->children.remove(node);
            node->parent = parent;
            parent->children.push_back(node);
        }
    }
    save();

    return std::nullopt;
}

void Server::FS_delete_nodes(std::string &&token, std::vector<std::uint64_t> &&nodes, mrpc::MRPCStream<std::string> &&stream) {
    check_user() {
        stream.close();
        return;
    }

    std::thread deleter{[this, nodes = std::move(nodes), user = std::move(user), stream = std::move(stream)](){
        for (const auto& node : nodes) {
            if (node == 0)
                continue;
            delete_node(user, node, [&stream](const std::string &log){ stream.send(log); });
        }
        stream.close();
        save();
    }};
    deleter.detach();
}

mrpc::Response<std::string> Server::FS_download_preview(std::string &&token, std::uint64_t &&node_id) {
    check_user_response();
    std::shared_lock lock{user->node_lock};
    auto node = get_node(user, node_id);
    if (!node) return { .e = "Invalid node" };

    if (!node->preview)
        return { .e = "No preview" };

    std::vector<std::uint8_t> preview;

    auto path = (user->user_dir / std::to_string(node_id)).replace_extension("png");

    auto size = std::filesystem::file_size(path);
    preview.resize(size);

    std::FILE *f = std::fopen(path.c_str(), "rb");
    std::fread(preview.data(), sizeof(std::uint8_t), size, f);
    std::fclose(f);

    return { .o = Botan::base64_encode(preview) };
}

mrpc::Response<std::string> Server::FS_get_mime(std::string &&token, std::uint64_t &&node_id) {
    check_user_response();
    auto node = get_node(user, node_id);
    if (!node) return { .e = "Invalid node" };
    if (!node->file) return { .e = "Node is a directory" };
    return { .o = get_mime_type(node->name) };
}
