#include <balancer/serval/contrib/cone/cone.hh>
#include <balancer/serval/core/config.h>

#include <util/datetime/systime.h>
#include <util/folder/path.h>
#include <util/generic/string.h>
#include <util/string/strip.h>
#include <util/system/file.h>
#include <util/system/fstat.h>

#define E(message) \
    "<html>\n"                                        \
        "<head><title>" message "</title></head>\n"   \
        "<body bgcolor=\"white\">\n"                  \
            "<center><h1>" message "</h1></center>\n" \
            "<hr><center>nginx/1.8.1</center>\n"      \
        "</body>\n"                                   \
    "</html>\n"

namespace {
    class TThreadPool {
    public:
        TThreadPool(size_t n) {
            Threads.resize(n);
            for (auto& g : Threads) {
                g = cone::thread([]() {
                    return cone::event{}.wait();
                });
            }
        }

        template <typename... Args>
        cone::ref Spawn(Args&&... args) {
            return cone::ref{Threads[I_++ % Threads.size()].get(), std::forward<Args>(args)...};
        }

    private:
        TVector<cone::guard> Threads;
        std::atomic<size_t> I_{0};
    };
}

static NSv::TAction Static(const YAML::Node& args, NSv::TAuxData& aux) {
    const auto& dir = args["static"];
    CHECK_NODE(args, dir, "`static` requires an argument");
    CHECK_NODE(dir, dir.IsScalar(), "`static` argument must be a string");
    auto root = TFsPath(dir.Scalar());
    if (root.IsRelative()) {
        root = TFsPath::Cwd() / root;
    }
    // XXX these non-threadsafe caches...need to call something that triggers `TFsPath::InitSplit`.
    Y_UNUSED(root.RelativePath(root));

    NSv::TAction On404 = [](NSv::IStreamPtr& req) {
        return req->WriteHead({404, {{"cache-control", "no-cache"}, {"content-type", "text/html"}}})
            && req->Write(E("404 Not Found")) && req->Close();
    };
    if (const auto& e404 = args["if-not-found"]) {
        On404 = aux.Action(e404);
    }

    NSv::TAction On405 = [](NSv::IStreamPtr& req) {
        return req->WriteHead({405, {{"cache-control", "no-cache"}, {"content-type", "text/html"}, {"allow", "GET"}}})
            && req->Write(E("405 Method Not Allowed")) && req->Close();
    };
    if (const auto& e405 = args["if-not-get"]) {
        On405 = aux.Action(e405);
    }

    // TODO configurable thread pool size; also, other actions might need thread pools too.
    return [=, pool = NSv::StaticData(0, []{
        return TThreadPool{8};
    })](NSv::IStreamPtr& req) {
        auto rqh = req->Head();
        if (!rqh MUN_RETHROW) {
            return false;
        }
        if (rqh->Method != "GET" && rqh->Method != "HEAD") {
            return On405(req);
        }

        const TFsPath path = root / StripStringLeft(rqh->Path(), [](const auto& c) {
            return *c == '/';
        });
        const TFileStat stat(path); // XXX should this be done in the thread pool?
        // The first condition should never be true due to path normalization (see `NormalizePathInPlace`).
        if (!path.IsSubpathOf(root) || !stat.IsFile() || stat.IsDir() || !(stat.Mode & (S_IRUSR | S_IRGRP))) {
            return On404(req);
        }

        char mtime[128] = {};
        NSv::THead head{200, {}};
        if (stat.MTime) {
            struct tm t;
            auto mod = rqh->find("if-modified-since");
            if (mod != rqh->end() && mod->second.size() < sizeof(mtime)) {
                memcpy(mtime, mod->second.data(), mod->second.size());
                // FIXME locale-dependent
                auto r = strptime(mtime, "%a, %d %b %Y %H:%M:%S GMT", &t);
                if (r && !*r && TimeGM(&t) >= stat.MTime) {
                    return req->WriteHead(304) && req->Close();
                }
            }
            if (GmTimeR(&stat.MTime, &t) && strftime(mtime, sizeof(mtime), "%a, %d %b %Y %H:%M:%S GMT", &t)) {
                head.emplace("last-modified", mtime);
            }
        }

        if (!req->WriteHead(head) MUN_RETHROW) {
            return false;
        }
        TStringBuf next;
        std::atomic<bool> haveNext{false};
        cone::event exchange;
        cone::guard reader = pool->Spawn([&]{
            char buf[8192]; // XXX maybe 2 or 3 buffers to read+write concurrently?
            TFileHandle input(path, OpenExisting | RdOnly | Seq);
            while (true) {
                // Too late to go back on that 200 code; handle all errors as EOF.
                next = TStringBuf(buf, input.IsOpen() ? Max(input.Read(buf, sizeof(buf)), 0) : 0);
                haveNext = true;
                exchange.wake();
                // If `wait_if` observes `haveNext == false` or is woken, only this thread
                // can switch it back to `true`, so no need for a `while` loop.
                if (haveNext && !exchange.wait_if([&]{
                    return !!haveNext;
                }) MUN_RETHROW) {
                    return false;
                }
            }
        });
        while (true) {
            if (!haveNext && !exchange.wait_if([&]{
                return !haveNext;
            }) MUN_RETHROW) {
                return false;
            }
            if (next ? !req->Write(next) : !req->Close() MUN_RETHROW) {
                return false;
            }
            haveNext = false;
            exchange.wake();
        }
    };
}

SV_DEFINE_ACTION("static", Static);
