#include "module.h"

#include <balancer/kernel/custom_io/stream.h>
#include <balancer/kernel/fs/threadedfile.h>
#include <balancer/kernel/http/parser/httpencoder.h>
#include <balancer/kernel/http/parser/http.h>
#include <balancer/kernel/http/parser/response_builder.h>
#include <balancer/kernel/log/errorlog.h>
#include <balancer/kernel/module/module.h>
#include <balancer/kernel/thread/threadedqueue.h>
#include <balancer/kernel/fs/fs_validations.h>

#include <library/cpp/http/misc/parsed_request.h>

#include <util/folder/dirut.h>
#include <util/system/fstat.h>
#include <util/string/split.h>
#include <util/string/strip.h>

#include <utility>

using namespace NConfig;
using namespace NSrvKernel;

Y_TLS(static) {
    TThreadedQueue* Queue = nullptr;
};

MODULE_WITH_TLS(static) {
public:
    TModule(const TModuleParams& moduleParams)
        : TModuleBase(moduleParams)
        , Index_("index.html")
    {
        Config->ForEach(this);

        if (!Dir_ && !File_) {
            ythrow TConfigParseError() << "no dir nor file configured";
        }

        if (!!Dir_ && !!File_) {
            ythrow TConfigParseError() << "both dir and file configured";
        }

        if (!!Dir_) {
            Dir_ = RealPath(Dir_);

            if (!Dir_.EndsWith('/')) {
                Dir_.append('/');
            }
        }
    }
private:
    class TIfMatchFsm : public TFsm, public TWithDefaultInstance<TIfMatchFsm> {
    public:
        TIfMatchFsm() noexcept
            : TFsm("If-Match", TFsm::TOptions().SetCaseInsensitive(true))
        {}
    };

    class TIfNoneMatchFsm : public TFsm, public TWithDefaultInstance<TIfNoneMatchFsm> {
    public:
        TIfNoneMatchFsm() noexcept
            : TFsm("If-None-Match", TFsm::TOptions().SetCaseInsensitive(true))
        {}
    };

    class TIfModifiedSinceFsm : public TFsm, public TWithDefaultInstance<TIfModifiedSinceFsm> {
    public:
        TIfModifiedSinceFsm() noexcept
            : TFsm("If-Modified-Since", TFsm::TOptions().SetCaseInsensitive(true))
        {}
    };

    class TIfUnmodifiedSinceFsm : public TFsm, public TWithDefaultInstance<TIfUnmodifiedSinceFsm> {
    public:
        TIfUnmodifiedSinceFsm() noexcept
            : TFsm("If-Unmodified-Since", TFsm::TOptions().SetCaseInsensitive(true))
        {}
    };

    class TConditionalHeadersFsm : public TFsm, public TWithDefaultInstance<TConditionalHeadersFsm> {
    public:
        TConditionalHeadersFsm() noexcept
            : TFsm(TIfMatchFsm::Instance() // 0
                | TIfNoneMatchFsm::Instance() // 1
                | TIfModifiedSinceFsm::Instance() // 2
                | TIfUnmodifiedSinceFsm::Instance()) // 3
        {}
    };

    enum EHeadersConditionResult {
        MODIFIED,
        NOT_MODIFIED,
        PRECOND_FAILED,
    };

    class THeadersCondition {
    private:
        const TRequest* const Request_ = nullptr;
    public:
        explicit THeadersCondition(const TConnDescr& descr) noexcept
            : Request_(descr.Request)
        {}

        TErrorOr<EHeadersConditionResult> Check(TStringBuf etag, ui64 mtime) noexcept {
            bool ifNoneMatchHeaderPresent = false;
            bool ifMatchHeaderPresent = false;

            bool ifMatchHeaderFailed = false;
            bool ifNoneMatchHeaderFailed = false;
            bool hasModifiedSince = false;
            bool hasUnmodifiedSince = false;

            TInstant fileMTime = TInstant::Seconds(mtime);
            TInstant modifiedSince;
            TInstant unmodifiedSince;

            const TConditionalHeadersFsm& conditionalHeadersFsm = TConditionalHeadersFsm::Instance();
            for (const auto& header: Request_->Headers()) {
                TMatcher matcher(conditionalHeadersFsm);
                if (Match(matcher, header.first.AsStringBuf()).Final()) {
                    for (auto& hdrVal : header.second) {
                        auto headerValue = ToString(hdrVal);
                        switch (*matcher.MatchedRegexps().first) {
                            /*
                             * HTTP 2616 14.24 and 7232 3.1 If-Match:
                             * If "*" or "ETag" is matched then skip this header because the result of a
                             * request with either an If-None-Match or an If-Modified-Since is not defined.
                             * Otherwise, this resource was modified because ETag is defined for all resources.
                             */
                            case 0:
                                ifMatchHeaderPresent = true;
                                ifMatchHeaderFailed = true;

                                {
                                    for (const auto &token: StringSplitter(headerValue).Split(',')) {
                                        TStringBuf value = StripString(token.Token());

                                        if (AsciiEqualsIgnoreCase(value, "*") || AsciiEqualsIgnoreCase(value, etag)) {
                                            ifMatchHeaderFailed = false;
                                        }
                                    }
                                }
                                break;
                            /*
                             * HTTP 2616 14.26 and 7232 3.2 If-None-Match:
                             * Since the file has only one ETag and this tag or "*" is present in entity list
                             * then we must return 304 Not Modified. Otherwise, we must ignore If-Modified-Since.
                             */
                            case 1:
                                ifNoneMatchHeaderPresent = true;

                                {
                                    for (const auto &token: StringSplitter(headerValue).Split(',')) {
                                        TStringBuf value = StripString(token.Token());

                                        if (AsciiEqualsIgnoreCase(value, "*") || AsciiEqualsIgnoreCase(value, etag)) {
                                            ifNoneMatchHeaderFailed = true;
                                        }
                                    }
                                }
                                break;
                            /*
                             * HTTP 2616 14.25 and 7232 3.3 If-Modified-Since:
                             * Just check if this file has been modified.
                             */
                            case 2:
                                hasModifiedSince = true;
                                try {
                                    modifiedSince = TInstant::ParseRfc822(headerValue);
                                } Y_TRY_STORE(TDateTimeParseException, yexception);
                                break;
                            /*
                             * HTTP 2616 14.28 and 7232 3.4 If-Unmodified-Since:
                             * If the requested variant has been modified since the specified time,
                             * the server MUST NOT perform the requested operation, and MUST return
                             * a 412 (Precondition Failed).
                             * Skip this header if If-Match is present.
                             */
                            case 3:
                                hasUnmodifiedSince = true;
                                try {
                                    unmodifiedSince = TInstant::ParseRfc822(headerValue);
                                } Y_TRY_STORE(TDateTimeParseException, yexception);
                                break;
                        }
                    }
                }
            }

            if (ifMatchHeaderFailed) {
                return EHeadersConditionResult::MODIFIED;
            }

            if (ifNoneMatchHeaderFailed) {
                return EHeadersConditionResult::NOT_MODIFIED;
            }

            if (!ifNoneMatchHeaderPresent && hasModifiedSince && modifiedSince && fileMTime >= modifiedSince) {
                return EHeadersConditionResult::NOT_MODIFIED;
            }

            if (!ifMatchHeaderPresent && hasUnmodifiedSince && unmodifiedSince && fileMTime >= unmodifiedSince) {
                return EHeadersConditionResult::PRECOND_FAILED;
            }

            return EHeadersConditionResult::MODIFIED;
        }
    };
private:
    THolder<TTls> DoInitTls(IWorkerCtl* process) override {
        auto tls = MakeHolder<TTls>();
        tls->Queue = process->ThreadedQueue("fs");
        return tls;
    }

    START_PARSE {
        STATS_ATTR;

        ON_KEY("dir", Dir_) {
            return;
        }

        ON_KEY("index", Index_) {
            return;
        }

        ON_KEY("file", File_) {
            return;
        }

        ON_KEY("etag_inode", ETagInode_) {
            return;
        }
        ON_KEY("expires", ExpiresDuration_) {
            return;
        }
    } END_PARSE

    bool DoExtraAccessLog() const override {
        return true;
    }

    TError DoRun(const TConnDescr& descr, TTls& tls) const noexcept override {
        TString path;
        if (TError error = SkipAll(descr.Input, TInstant::Max())) { // reading body for not failing on parsing of next keepalive request
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client read error");
            return error;
        }

        auto logSucc = [&](const int code, ui64 len) {
            descr.ExtraAccessLog << " w:" << len << " succ " << code;
        };

        auto buildEmptyResponse = [&](const int code) -> TResponse {
            logSucc(code, 0);
            return BuildResponse().Version11().Code(code).ContentLength(0);
        };

        const TRequest* const request = descr.Request;
        const EMethod method = request->RequestLine().Method;
        if (method != EMethod::GET && method != EMethod::HEAD) {
            Y_TRY(TError, error) {
                Y_PROPAGATE_ERROR(descr.Output->SendHead(buildEmptyResponse(HTTP_METHOD_NOT_ALLOWED), false, TInstant::Max()));
                return descr.Output->SendEof(TInstant::Max());
            } Y_CATCH {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                return error;
            };
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "method not allowed");
            return {};
        }

        if (!!Dir_) {
            TString file = request->RequestLine().GetURL();

            if (!file.size() || file == "/") {
                file = Index_;
            }
            path = Dir_ + file;
        } else {
            path = File_;
        }

        // TThreadedStat uses stat in case of normal file and lstat in case of symlink
        auto fileStat = TThreadedStat(path, tls.Queue).Stat();
        if (!fileStat.IsFile() || fileStat.IsDir()) {
            Y_TRY(TError, error) {
                Y_PROPAGATE_ERROR(descr.Output->SendHead(buildEmptyResponse(HTTP_NOT_FOUND), false, TInstant::Max()));
                return descr.Output->SendEof(TInstant::Max());
            } Y_CATCH {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                return error;
            };
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "not found");
            return {};
        }

        if (!((fileStat.Mode & S_IRGRP) || (fileStat.Mode & S_IRUSR) || (fileStat.Mode & S_IROTH))) {
            Y_TRY(TError, error) {
                Y_PROPAGATE_ERROR(descr.Output->SendHead(buildEmptyResponse(HTTP_FORBIDDEN), false, TInstant::Max()));
                return descr.Output->SendEof(TInstant::Max());
            } Y_CATCH {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                return error;
            };
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "forbidden");
            return {};
        }

        /*
         * TFileError exception is hidden on the lower layer.
         * If a realpath returns not 0 result TString will be empty.
         * Symlinks outside Dir are forbidden.
         */
        path = TThreadedRealPath(path, tls.Queue).FileRealPath();
        if (!!Dir_ && !path.StartsWith(Dir_)) {
            Y_TRY(TError, error) {
                Y_PROPAGATE_ERROR(descr.Output->SendHead(buildEmptyResponse(HTTP_FORBIDDEN), false, TInstant::Max()));
                return descr.Output->SendEof(TInstant::Max());
            } Y_CATCH {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                return error;
            };
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "forbidden");
            return {};
        }

        TStringStream ss("\"");
        ss.Reserve(16);
        if (ETagInode_) {
            ss << Hex(fileStat.INode, 0) << "-";
        }
        ss << Hex(fileStat.Size, 0) << "-" << Hex(fileStat.MTime, 0) << "\"";
        TString etag(ss.Str());

        /*
         * HTTP 7232 4.1: All dates must be in GMT time zone
         * Prepare conditional headers for 304 Not Modified and 200 OK
         */
        auto addResponseConditionalHeaders = [&](THeaders& headers) {
            headers.Add("Date", TInstant::Now().ToRfc822String());
            headers.Add("ETag", etag);
            headers.Add("Last-Modified", TInstant::Seconds(fileStat.MTime).ToRfc822String());
            if (!ExpiresDuration_) {
                headers.Add("Expires", TInstant::Zero().ToRfc822String());
                headers.Add("Cache-Control", "public, max-age=0");
            } else {
                headers.Add("Expires", (TInstant::Now() + ExpiresDuration_).ToRfc822String());
                headers.Add("Cache-Control", TStringBuilder() << "public, max-age=" << ExpiresDuration_.Seconds());
            }
        };
        /*
         * HTTP 2616 13.3.4: Check all headers except If-Range.
         * Should match If-Modified-Since, If-Unmodified-Since,
         * If-Match, If-None-Match. Range specific behaviour is
         * not implemented.
         */
        THeadersCondition cond(descr);
        EHeadersConditionResult result;
        if (TError error = cond.Check(etag, fileStat.MTime).AssignTo(result)) {
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "date parse error");
            return error;
        }
        if (result == EHeadersConditionResult::NOT_MODIFIED) {
            /*
             * HTTP 2616 10.3.5:
             * Must include Date, ETag, Expires, Cache-Control
             */
            TResponse response = buildEmptyResponse(HTTP_NOT_MODIFIED);
            addResponseConditionalHeaders(response.Headers());
            Y_TRY(TError, error) {
                Y_PROPAGATE_ERROR(descr.Output->SendHead(std::move(response), false, TInstant::Max()));
                return descr.Output->SendEof(TInstant::Max());
            } Y_CATCH {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                return error;
            };
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "not modified");
            return {};
        } else if (result == EHeadersConditionResult::PRECOND_FAILED) {
            Y_TRY(TError, error) {
                Y_PROPAGATE_ERROR(descr.Output->SendHead(buildEmptyResponse(HTTP_PRECONDITION_FAILED), false, TInstant::Max()));
                return descr.Output->SendEof(TInstant::Max());
            } Y_CATCH {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                return error;
            };
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "precondition failed");
            return {};
        }

        TResponse response = BuildResponse().Version11().Code(HTTP_OK).ChunkedTransfer();
        if (path.EndsWith(".html")) {
            response.Headers().Add("Content-Type", "text/html");
        }
        addResponseConditionalHeaders(response.Headers());

        if (TError error = descr.Output->SendHead(std::move(response), false, TInstant::Max())) {
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
            return error;
        }

        ui64 len = 0;
        if (!!path) {
            Y_TRY(TError, error) {
                return ThreadedFileSend(path, tls.Queue, descr.Output).AssignTo(len);
            } Y_CATCH {
                auto mess = GetErrorMessage(error);
                descr.ExtraAccessLog << " send_error " << mess;
                LOG_ERROR(TLOG_ERR, descr, "static error " << GetErrorMessage(error));
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "send error");
                return error;
            }
        }

        if (TError error = descr.Output->SendEof(TInstant::Max())) {
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
            return error;
        }
        logSucc(HTTP_OK, len);
        descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "success");
        return {};
    }

private:
    TString Dir_;
    TString Index_;
    TString File_;
    bool ETagInode_ = false;
    TDuration ExpiresDuration_ = TDuration::Zero();

    TString StatsAttr_;
};

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