#include <filesystem>
#include <string_view>
#include <ranges>
#include <fstream>
#include <deque>
#include <charconv>
#include <miniz.h>
#include <corvusoft/restbed/session.hpp>
#include <corvusoft/restbed/request.hpp>
#include <corvusoft/restbed/response.hpp>
#include "server_internal.hxx"

void Server::download(const std::shared_ptr<restbed::Session> &s) {
    const auto req = s->get_request();
    std::size_t body_len = req->get_header("Content-Length", 0);
    s->fetch(body_len, [this](const std::shared_ptr<restbed::Session> &s, const restbed::Bytes &b){
        std::string body{b.cbegin(), b.cend()};
        if (body.empty())
            return s->close(400, "empty body");
        std::string node_str, token;
        for (const auto part : std::views::split(body, '&')) {
            std::string_view part_view{part};
            auto equal_pos = part_view.find_first_of('=');
            auto key = part_view.substr(0, equal_pos);
            if (key == "node")
                node_str = part_view.substr(equal_pos+1);
            else if (key == "token")
                token = part_view.substr(equal_pos+1);
        }
        if (node_str.empty())
            return s->close(400, "Missing node");
        if (token.empty())
            return s->close(400, "Missing token");

        std::uint64_t node_id;
        auto res = std::from_chars(node_str.data(), node_str.data() + node_str.size(), node_id);
        if (res.ec != std::errc{})
            return s->close(400, "Invalid node");

        check_user() return s->close(400, "Invalid user");
        {
            std::shared_lock lock{user->node_lock};
            auto node = get_node(user, node_id);
            if (!node) return s->close(400, "Invalid node");

            auto mime = get_mime_type(node->name);

            s->yield(
                200,
                "",
                std::multimap<std::string, std::string>{
                    {"Content-Type", mime},
                    {"Content-Length", std::to_string(node->size)},
                    {"Content-Disposition", "attachment; filename=\"" + node->name + "\""}
                },
                [&](const std::shared_ptr<restbed::Session>& s) {
                    std::shared_lock lock{user->node_lock};
                    restbed::Bytes buf(1024*1024*4, 0);
                    std::ifstream f{user->user_dir / std::to_string(node->id)};
                    while (!f.eof()) {
                        f.read((char*)buf.data(), buf.size());
                        buf.resize(f.gcount());
                        s->yield(buf);
                    }
                    s->close();
                }
            );
        }
    });
}

size_t zip_write_func(void *pOpaque, mz_uint64 _file_ofs, const void *pBuf, size_t n) {
    auto s = (restbed::Session*)pOpaque;
    if (n > 0) {
        restbed::Bytes buf(n, 0);
        std::memcpy(buf.data(), pBuf, n);
        std::stringstream ss;
        ss << std::hex << n;
        s->yield(ss.str() + "\r\n");
        s->yield(buf);
        s->yield("\r\n");
    }
    return n;
}

void Server::download_multi(const std::shared_ptr<restbed::Session> &s) {
    const auto req = s->get_request();
    const auto body_len = req->get_header("Content-Length", 0);
    s->fetch(body_len, [this](const std::shared_ptr<restbed::Session> &s, const restbed::Bytes &b){
        std::string body{b.cbegin(), b.cend()};
        if (body.empty())
            return s->close(400, "empty body");
        std::string nodes_str, token;
        for (const auto part : std::views::split(body, '&')) {
            std::string_view part_view{part};
            auto equal_pos = part_view.find_first_of('=');
            auto key = part_view.substr(0, equal_pos);
            if (key == "nodes")
                nodes_str = part_view.substr(equal_pos+1);
            else if (key == "token")
                token = part_view.substr(equal_pos+1);
        }
        if (nodes_str.empty())
            return s->close(400, "Missing nodes");
        if (token.empty())
            return s->close(400, "Missing token");

        std::vector<std::uint64_t> node_ids;
        for (const auto part : std::views::split(nodes_str, '.')) {
            std::uint64_t node_id;
            auto res = std::from_chars(part.data(), part.data() + part.size(), node_id);
            if (res.ec != std::errc{})
                return s->close(400, "Invalid node " + std::string{std::string_view{part}});
            node_ids.push_back(node_id);
        }

        check_user() return s->close(400, "Invalid user");
        {
            std::shared_lock lock{user->node_lock};
            std::vector<std::shared_ptr<Node>> nodes;
            for (auto node_id : node_ids) {
                auto node = get_node(user, node_id);
                if (!node) return s->close(400, "Invalid node " + std::to_string(node_id));
                nodes.push_back(node);
            }

            s->yield(
                200,
                "",
                std::multimap<std::string, std::string>{
                    {"Content-Type", "application/zip"},
                    {"Content-Disposition", "attachment; filename=\"files.zip\""},
                    {"Transfer-Encoding", "chunked"}
                }
            );
            std::thread zip_thread{[nodes = nodes, user = user, s = s] {
                std::shared_lock lock{user->node_lock};

                mz_zip_archive archive;
                mz_zip_zero_struct(&archive);
                archive.m_pWrite = zip_write_func;
                archive.m_pIO_opaque = s.get();

                mz_zip_writer_init_v2(&archive, 0, MZ_ZIP_FLAG_WRITE_ZIP64);

                std::deque<std::pair<std::shared_ptr<Node>, std::filesystem::path>> todo;
                for (const auto &node : nodes)
                    todo.emplace_back(node, std::filesystem::path{});

                auto handle_file = [&user, &archive](const std::pair<std::shared_ptr<Node>, std::filesystem::path> &i) {
                    auto path = i.second / i.first->name;
                    auto real_path = user->user_dir / std::to_string(i.first->id);
                    mz_zip_writer_add_file(&archive, path.c_str(), real_path.c_str(), nullptr, 0, MZ_DEFAULT_COMPRESSION);
                };

                while (!todo.empty()) {
                    const auto &node = todo.front();
                    if (node.first->file) {
                        handle_file(node);
                    } else {
                        auto path = node.second / node.first->name;
                        auto dir_path = path.string() + "/";
                        mz_zip_writer_add_mem(&archive, dir_path.c_str(), nullptr, 0, 0);
                        for (const auto &child : node.first->children) {
                            auto p = std::make_pair(child, path);
                            if (child->file)
                                handle_file(p);
                            else
                                todo.push_back(p);
                        }
                    }
                    todo.pop_front();
                }

                mz_zip_writer_finalize_archive(&archive);
                mz_zip_writer_end(&archive);

                s->close("0\r\n\r\n");
            }};
            zip_thread.detach();
        }
    });
}
