#pragma once

#include <balancer/kernel/regexp/regexp_pire.h>
#include <util/string/ascii.h>


namespace CacheHelpers {
#define HEADER_FSM(TName, value)                                            \
    struct TName final : public TFsm, public TWithDefaultInstance<TName> {  \
        TName()                                                             \
            : TFsm(value, TFsm::TOptions().SetCaseInsensitive(true))        \
        {}                                                                  \
    }

    HEADER_FSM(TCacheControlFsm, "Cache-Control");
    HEADER_FSM(TPragmaFsm, "Pragma");
    HEADER_FSM(TAuthorizationFsm, "Authorization");
    HEADER_FSM(TVaryFsm, "Vary");
    HEADER_FSM(TExpiresFsm, "Expires");

#undef HEADER_FSM

#define HEADER_VALUE_FSM(TName, value)                                                  \
    struct TName final : public TFsm, public TWithDefaultInstance<TName> {              \
        TName()                                                                         \
            : TFsm(value, TFsm::TOptions().SetCaseInsensitive(true).SetSurround(true))  \
        {}                                                                              \
    }

    HEADER_VALUE_FSM(TNoCacheFsm, "no[- ]cache");
    HEADER_VALUE_FSM(TNoStoreFsm, "no-store");
    HEADER_VALUE_FSM(TOnlyIfCachedFsm, "only-if-cached");
    HEADER_VALUE_FSM(TPrivateFsm, "private");
    HEADER_VALUE_FSM(TMaxAgeFsm, "max-age");
    HEADER_VALUE_FSM(TSMaxAgeFsm, "s-maxage");
    HEADER_VALUE_FSM(TAcceptEncodingVaryFsm, "accept-encoding");

#undef HEADER_VALUE_FSM

    class TCacheHeaderNamesFsm : public TFsm, public TWithDefaultInstance<TCacheHeaderNamesFsm> {
    public:
        TCacheHeaderNamesFsm()
                : TFsm(
                TCacheControlFsm::Instance() // 0
                        | TPragmaFsm::Instance()       // 1
                        | TAuthorizationFsm::Instance() // 2
        )
        {}
    };

    class TBackendHeaderNamesFsm : public TFsm, public TWithDefaultInstance<TBackendHeaderNamesFsm> {
    public:
        TBackendHeaderNamesFsm() noexcept
                : TFsm(
                TCacheControlFsm::Instance() // 0
                        | TVaryFsm::Instance()         // 1
                        | TExpiresFsm::Instance()      // 2
        )
        {}
    };

    struct THeadersPreProcessResult {
        bool CanRequestCache() const noexcept {
            return !CacheControlNoCache && !PragmaNoCache && !MayHaveBody;
        }

        bool MayHaveBody = false;
        bool MethodIsCacheable = false;
        bool IsHeadRequest = false;
        bool CacheControlNoCache = false;
        bool PragmaNoCache = false;
        bool CacheControlOnlyIfCached = false;
        bool CacheControlNoStore = false;
        bool HasAuthorizationHeader = false;
    };

    struct TBackendHeadersProcessResult {
        bool MayCalculateRecordLifetime() const noexcept {
            return IgnoreCacheControl || HasExpires || CacheControlMaxAge || CacheControlSMaxAge;
        }

        bool ValidVary() const noexcept {
            if (HasVary) {
                return HasAcceptEncodingVary;
            }

            return true;
        }

        bool MayStoreInCache() const noexcept {
            return Status == 200
                    && !CacheControlNoStore && !CacheControlNoCache
                    && !CacheControlPrivate && ValidVary() && MayCalculateRecordLifetime();
        }

        ui32 Status = 0;
        bool CacheControlNoStore = false;
        bool CacheControlNoCache = false;
        bool CacheControlPrivate = false;
        bool HasVary = false;
        bool HasAcceptEncodingVary = false;
        bool HasExpires = false;
        bool CacheControlMaxAge = false;
        bool CacheControlSMaxAge = false;
        bool IgnoreCacheControl = true;
    };

    THeadersPreProcessResult PreprocessHeaders(const TRequest& request, bool ignoreCacheControl) noexcept {
        THeadersPreProcessResult retval;

        const TRequestLine& requestLine = request.RequestLine();
        if (requestLine.Method == EMethod::GET) {
            retval.MethodIsCacheable = true;
        } else if (requestLine.Method == EMethod::HEAD) {
                retval.MethodIsCacheable = false;
                retval.IsHeadRequest = true;
            }
        retval.MayHaveBody = request.Props().ChunkedTransfer || request.Props().ContentLength > 0;

        const TCacheHeaderNamesFsm& headerNamesFsm = TCacheHeaderNamesFsm::Instance();

        for (const auto& header : request.Headers()) {
            TMatcher matcher(headerNamesFsm);
            if (Match(matcher, header.first.AsStringBuf()).Final()) {
                for (const auto& headerValue : header.second) {
                    switch (*matcher.MatchedRegexps().first) {
                    case 0: // cache-control
                        // Pass ignore cache var here for consistency
                        if (!ignoreCacheControl) {
                            if (!retval.CacheControlNoCache &&
                                Match(TNoCacheFsm::Instance(), headerValue.AsStringBuf())) {
                                retval.CacheControlNoCache = true;
                                // TODO: maybe break after this
                            }
                            if (!retval.CacheControlOnlyIfCached &&
                                Match(TOnlyIfCachedFsm::Instance(), headerValue.AsStringBuf())) {
                                retval.CacheControlOnlyIfCached = true;
                            }
                            if (!retval.CacheControlNoStore &&
                                Match(TNoStoreFsm::Instance(), headerValue.AsStringBuf())) {
                                retval.CacheControlNoStore = true;
                            }
                        }
                        break;
                    case 1: // pragma
                        if (!retval.PragmaNoCache && Match(TNoCacheFsm::Instance(), headerValue.AsStringBuf())) {
                            retval.PragmaNoCache = true;
                            // TODO: maybe break after this
                        }
                        break;
                    case 2: // authorization
                        retval.HasAuthorizationHeader = true;
                        break;
                    }
                }
            }
        }

        return retval;
    }

    bool MayStoreInCache(const TResponse& response, bool ignoreCacheControl) noexcept {
        TBackendHeadersProcessResult retval;
        retval.IgnoreCacheControl = ignoreCacheControl;

        const TResponseLine& responseLine = response.ResponseLine();
        const ui32 status = responseLine.StatusCode;
        retval.Status = status;
        if (status != 200) {
            return false;
        }

        for (const auto& header: response.Headers()) {
            TMatcher matcher(TBackendHeaderNamesFsm::Instance());
            if (Match(matcher, header.first.AsStringBuf()).Final()) {
                for (const auto& headerValue : header.second) {
                    switch (*matcher.MatchedRegexps().first) {
                    case 0: // cache-control
                        if (!ignoreCacheControl) {
                            if (!retval.CacheControlNoStore &&
                                Match(TNoStoreFsm::Instance(), headerValue.AsStringBuf())) {
                                retval.CacheControlNoStore = true;
                            }
                            if (!retval.CacheControlNoCache &&
                                Match(TNoCacheFsm::Instance(), headerValue.AsStringBuf())) {
                                retval.CacheControlNoCache = true;
                            }
                            if (!retval.CacheControlPrivate &&
                                Match(TPrivateFsm::Instance(), headerValue.AsStringBuf())) {
                                retval.CacheControlPrivate = true;
                            }
                            if (!retval.CacheControlMaxAge &&
                                Match(TMaxAgeFsm::Instance(), headerValue.AsStringBuf())) {
                                retval.CacheControlMaxAge = true;
                            }
                            if (!retval.CacheControlSMaxAge &&
                                Match(TSMaxAgeFsm::Instance(), headerValue.AsStringBuf())) {
                                retval.CacheControlSMaxAge = true;
                            }
                        }
                        break;
                    case 1: // vary
                        if (AsciiEqualsIgnoreCase(headerValue.AsStringBuf(), "accept-encoding")) {
                            retval.HasAcceptEncodingVary = true;
                        }
                        retval.HasVary = true;
                        break;
                    case 2: // expires
                        retval.HasExpires = true;
                        break;
                    }
                }
            }

        }

        return retval.MayStoreInCache();
    }
}  // namespace
