#include "server.h"

#include "common.h"
#include "impl.h"

#include <balancer/kernel/custom_io/stream.h>
#include <balancer/kernel/io/iobase.h>
#include <balancer/kernel/http/parser/response_builder.h>
#include <balancer/kernel/memory/alloc.h>
#include <balancer/kernel/module/module.h>
#include <balancer/kernel/regexp/regexp_pire.h>
#include <balancer/kernel/thread/threadedqueue.h>
#include <balancer/kernel/log/errorlog.h>

#include <util/stream/buffer.h>
#include <util/stream/input.h>
#include <util/stream/output.h>
#include <util/thread/factory.h>
#include <util/ysaveload.h>


using namespace NSrvKernel;
using namespace NCache;

namespace NSrvKernel {
    TError TransferHttpCode(int code, IHttpOutput* out) noexcept {
        TResponse response = BuildResponse().Version11().Code(code).ContentLength(0);
        Y_PROPAGATE_ERROR(out->SendHead(std::move(response), false, TInstant::Max()));
        return out->SendEof(TInstant::Max());
    }
}

namespace {
class TCacher
    : public TThreadedQueue::ICallback
        , public THolder<TMemoryCacher>
{
public:
    TCacher(size_t memoryLimit) noexcept
        : MemoryLimit_(memoryLimit)
    {}

    const TString& Error() const noexcept {
        return Error_;
    }

private:
    void DoRun() noexcept override try {
        this->Reset(MakeHolder<TMemoryCacher>(MemoryLimit_, 1024, 1));
    } catch (...) {
        Error_ = CurrentExceptionMessage();
    }

private:
    size_t MemoryLimit_ = 0;
    TString Error_;
};
}

Y_TLS(cache_server) {
    void MakeCacheInitTask(size_t memoryLimit, IWorkerCtl* process) noexcept {
        CacheInitTask = TCoroutine{"cache_async_init_cacher", &process->Executor(), [this, memoryLimit] {
            try {
                THolder<TCacher> cacher = Queue->Run(new TCacher(memoryLimit), TInstant::Max());

                if (!cacher || !*cacher) {
                    Y_FAIL("no cacher");
                }

                Cacher = std::move(cacher);
            } catch (...) {
            }
        }};
    }

    THolder<TCacher> Cacher;
    TThreadedQueue* Queue = nullptr;

    ui64 Responses = 0;
    ui64 Hits = 0;

    TCoroutine CacheInitTask;
};

MODULE_WITH_TLS(cache_server) {
public:
    TModule(const TModuleParams& params)
        : TModuleBase(params)
    {
        Y_WARN_ONCE("the module is deprecated and will be erased soon");
        Config->ForEach(this);
    }

private:
    START_PARSE {
        STATS_ATTR;

        ON_KEY("check_modified", CheckModified_) {
            return;
        }

        ON_KEY("valid_for", ValidFor_) {
            return;
        }

        ON_KEY("memory_limit", MemoryLimit_) {
            return;
        }

        ON_KEY("async_init", AsyncInit_) {
            return;
        }
    } END_PARSE

private:
    class TMeta: public IMeta {
    private:
        class TSize: private IOutputStream {
        public:
            TSize()
                : Size_(0)
            {
                TMeta().Store(this);
            }

            size_t Size() const noexcept {
                return Size_;
            }

        private:
            void DoWrite(const void*, size_t size) override {
                Size_ += size;
            }

        private:
            size_t Size_;
        };

        const TString& GetSerialized() {
            if (!Serialized.Defined()) {
                TBufferOutput out;
                Serialized = TString();
                ::Save<TInstant>(&out, ValidUntil);
                ::Save<TInstant>(&out, LastModified);
                ::Save<TString>(&out, ETag);
                Serialized = TString(out.Buffer().Data(), out.Buffer().Size());
            }
            return Serialized.GetRef();
        }

    public:
        TInstant ValidUntil;
        TInstant LastModified;
        TString ETag;    // ETag value cannot be empty, as it is enclosed in "" - see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
        TMaybe<TString> Serialized;

        TMeta() noexcept
            : ValidUntil(TInstant::Zero()) // if compared, always is out of date
            , LastModified(TInstant::Max()) // if compared, always is modified
        {}

        size_t Size() {
            return GetSerialized().size();
        }

        void Load(IInputStream* in) override {
            ::Load<TInstant>(in, ValidUntil);
            ::Load<TInstant>(in, LastModified);
            ::Load<TString>(in, ETag);
        }

        void Store(IOutputStream* out) override {
            const TString& serialized = GetSerialized();
            out->Write(serialized.data(), serialized.size());
        }
    };

private:
    void TryStore(TMeta* meta, const TChunkList& data, const TInstant& deadline, const TString& id, TTls& tls) const /* throws */ {
        try {
            const THolder<IIoOutput> output(Store(tls.Cacher->Get(), meta, tls.Queue, deadline, id));

            if (!!output) {
                TChunksInput input(data.Copy());
                TryRethrowError(Transfer(&input, output.Get(), TInstant::Max()));
            }
        } catch (const yexception& e) {
            throw TStoreException() << e.what();
        }
    }

    void TryRetryStore(TMeta* meta, const TChunkList& data, const TInstant& deadline, const TString& id, TTls& tls) const /* throws */ {
        std::exception_ptr exc = nullptr;

        try {
            TryStore(meta, data, deadline, id, tls);
        } catch (const TStoreException&) {
            exc = std::current_exception();
        }

        if (exc) {
            if (MakeSpace(tls.Cacher->Get(), tls.Queue, deadline, id, meta->Size() + data.size())) {
                TryStore(meta, data, deadline, id, tls);
            } else {
                std::rethrow_exception(exc);
            }
        }
    }

private:
    THolder<TTls> DoInitTls(IWorkerCtl* process) override {
        auto tls = MakeHolder<TTls>();
        tls->Queue = process->ThreadedQueue("cache");
        if (!AsyncInit_) {
            auto cacher = MakeHolder<TCacher>(MemoryLimit_);

            cacher->Run();

            if (!*cacher) {
                throw yexception() << "failed to create TMemoryCacher: " << cacher->Error();
            }

            tls->Cacher = std::move(cacher);
        } else {
            tls->MakeCacheInitTask(MemoryLimit_, process);
        }
        return tls;
    }

    TError DoRun(const TConnDescr& descr, TTls& tls) const noexcept override {
        const TInstant now = Now();
        TRequest* const request = descr.Request;

        if (request->RequestLine().Method == EMethod::GET) {
            if (!tls.Cacher) {
                descr.ExtraAccessLog << " not initialized";
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "not initialized");
                return TransferHttpCode(HTTP_NOT_FOUND, descr.Output);
            }

            ++tls.Responses;

            const auto id = ToString(request->RequestLine().GetURL());

            LOG_ERROR(TLOG_INFO, descr, "Cache: GET " << id << " request");
            descr.ExtraAccessLog << " get " << id;
            Y_DEFER {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "get " + id);
            };

            TMeta meta;

            THolder<IIoInput> cacheInput(Load(tls.Cacher->Get(), &meta, tls.Queue, TInstant::Max(), id));

            if (!!cacheInput && (CheckModified_ || meta.ValidUntil >= now)) {
                LOG_ERROR(TLOG_INFO, descr, "Cache: \"" << id << "\" hit");
                descr.ExtraAccessLog << " hit " << (meta.ValidUntil >= now ? "valid" : "not valid");

                ++tls.Hits;

                TResponse response;
                Y_PROPAGATE_ERROR(response.Parse("HTTP/1.1 200 OK\r\n\r\n"));
                response.Headers().Add(TLastModified::Key(), TLastModified::Value(meta.LastModified));
                response.Headers().Add(TValidUntil::Key(), TValidUntil::Value(meta.ValidUntil));
                if (!!meta.ETag) {
                    response.Headers().Add(TETag::Key(), meta.ETag);
                }

                Y_PROPAGATE_ERROR(descr.Output->SendHead(std::move(response), false, TInstant::Max()));
                Y_PROPAGATE_ERROR(Transfer(cacheInput.Get(), descr.Output, TInstant::Max()));
            } else {
                LOG_ERROR(TLOG_INFO, descr, "Cache: \"" << id << "\" not hit");
                descr.ExtraAccessLog << " not hit";

                Y_PROPAGATE_ERROR(TransferHttpCode(HTTP_NOT_FOUND, descr.Output));
            }
        } else if (request->RequestLine().Method == EMethod::PUT) {
            if (!tls.Cacher) {
                descr.ExtraAccessLog << " not initialized";
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "not initialized");
                return TransferHttpCode(HTTP_SERVICE_UNAVAILABLE, descr.Output);
            }

            const auto id = ToString(request->RequestLine().GetURL());

            LOG_ERROR(TLOG_INFO, descr, "Cache: PUT \"" << id << "\" request");
            descr.ExtraAccessLog << " put " << id;
            Y_DEFER {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "put " + id);
            };

            TChunksOutput dataOutput;
            Y_PROPAGATE_ERROR(Transfer(descr.Input, &dataOutput, TInstant::Max()));

            TMeta meta;

            THolder<IIoInput> cacheInput;

            bool validate = false;

            if (const auto cacheActionHeaderValue = request->Headers().GetFirstValue(Default<TCacheAction>())) {
                validate = Match(Default<TValidate>(), cacheActionHeaderValue);
            }

            if (validate) {
                descr.ExtraAccessLog << " bump";
                cacheInput.Reset(Load(tls.Cacher->Get(), &meta, tls.Queue, TInstant::Max(), id));
            } else {
                descr.ExtraAccessLog << " store";
            }

            if (const auto lastModifiedHeaderValue = request->Headers().GetFirstValue(Default<TLastModified>())) try {
                meta.LastModified = TLastModified::ParseValue(lastModifiedHeaderValue);
            } catch (const TLastModified::TParseException&) {}

            meta.ValidUntil = now + ValidFor_;

            if (const auto etagHeaderValue = request->Headers().GetFirstValue(Default<TETag>())) {
                meta.ETag = ToString(etagHeaderValue);
            }

            if (validate) {
                if (!!cacheInput) {
                    LOG_ERROR(TLOG_INFO, descr, "Cache: bumping \"" << id << '\"');

                    TChunksOutput out;
                    Y_PROPAGATE_ERROR(Transfer(cacheInput.Get(), &out, TInstant::Max()));

                    cacheInput.Destroy();

                    try {
                        TryRetryStore(&meta, out.Chunks(), TInstant::Max(), id, tls);
                    } Y_TRY_STORE(TStoreException, yexception);

                    Y_PROPAGATE_ERROR(TransferHttpCode(HTTP_OK, descr.Output));

                    descr.ExtraAccessLog << " ok";
                } else {
                    LOG_ERROR(TLOG_INFO, descr, "Cache: no content to bump \"" << id << '\"');

                    Y_PROPAGATE_ERROR(TransferHttpCode(HTTP_NOT_FOUND, descr.Output));

                    descr.ExtraAccessLog << " fail";
                }
            } else {
                LOG_ERROR(TLOG_INFO, descr, "Cache: storing \"" << id << '\"');

                Y_TRY(TError, error) {
                    try {
                        TryRetryStore(&meta, dataOutput.Chunks(), TInstant::Max(), id, tls);
                    } Y_TRY_STORE(TStoreException, yexception);

                    Y_PROPAGATE_ERROR(TransferHttpCode(HTTP_OK, descr.Output));
                    descr.ExtraAccessLog << " ok";
                    return {};
                } Y_CATCH {
                    if (const auto* e = error.GetAs<TStoreException>()) {
                        LOG_ERROR(TLOG_INFO, descr, "Cache: failed to store \"" << id << '\"');
                        descr.ExtraAccessLog << " failed";
                        return TransferHttpCode(HTTP_INTERNAL_SERVER_ERROR, descr.Output);
                    } else {
                        return error;
                    }
                }

                return {};
            }
        } else {
            LOG_ERROR(TLOG_INFO, descr, "Cache: invalid method \"" << request->RequestLine().Method << '\"');
            descr.ExtraAccessLog << " invalid method " << request->RequestLine().Method;
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "invalid method " + ToString(request->RequestLine().Method));

            Y_PROPAGATE_ERROR(TransferHttpCode(HTTP_METHOD_NOT_ALLOWED, descr.Output));
        }
        return {};
    }

    bool DoExtraAccessLog() const noexcept override {
        return true;
    }

private:
    TString StatsAttr_;
    bool CheckModified_ = true;
    TDuration ValidFor_ = TDuration::Zero();
    size_t MemoryLimit_ = 0;
    bool AsyncInit_ = false;
};

IModuleHandle* NModCacheServer::Handle() {
    return TModule::Handle();
}
