#pragma once

#include <balancer/kernel/cookie/utils/utils.h>

#include <util/datetime/base.h>
#include <util/generic/maybe.h>
#include <util/generic/strbuf.h>
#include <util/memory/blob.h>
#include <util/thread/singleton.h>

namespace NSrvKernel {

    constexpr TStringBuf PastDate = "Fri, 21 Dec 2012 12:12:12 GMT";

    // Fri, 31 Dec 9999 23:59:59 GMT
    constexpr auto MaxExpiresTstamp = TInstant::Seconds(253402300799ull);

    struct [[nodiscard]] TExpires {
        // Directly corresponds to the serialized format. Expects its fields to be consistent.
        ui32 Year = 0;    // 1970 .. 9999
        ui32 Month = 0;   // 1 .. 12
        ui32 MDay = 0;    // 1 .. 31
        ui32 Hour = 0;    // 0 .. 23
        ui32 Minute = 0;  // 0 .. 59
        ui32 Second = 0;  // 0 .. 59

    public:
        Y_BALANCER_TUPLE_OPS(TExpires, Year, Month, MDay, Hour, Minute, Second)

        [[nodiscard]]
        bool IsValid() const noexcept;

        [[nodiscard]]
        TMaybe<TInstant> ToInstant() const noexcept;

        [[nodiscard]]
        TBlob Render(char dateSep=' ', bool singleThreadedResult = true) const noexcept;

        [[nodiscard]]
        static TMaybe<TExpires> FromInstant(TInstant when) noexcept;

        [[nodiscard]]
        static TMaybe<TExpires> Parse(TBlob expires) noexcept;

        static constexpr TExpires Min() noexcept {
            return {.Year=1970, .Month=1, .MDay=1, .Hour=0, .Minute=0, .Second=0};
        }

        static constexpr TExpires Max() noexcept {
            return {.Year=9999, .Month=12, .MDay=31, .Hour=23, .Minute=59, .Second=59};
        }

        static constexpr TExpires Past() noexcept {
            return {.Year=2012, .Month=12, .MDay=21, .Hour=12, .Minute=12, .Second=12};
        }
    };


    // A poor man's way to safely cache the rendered value
    class [[nodiscard]] TMaybeExpires {
    public:
        TMaybeExpires() noexcept = default;

        TMaybeExpires(TMaybe<TExpires> e) noexcept
            : Expires_(std::move(e))
        {}

        TMaybeExpires(TExpires e) noexcept
            : Expires_(std::move(e))
        {}

        TMaybeExpires(TBlob raw, TExpires e) noexcept
            : Expires_(e)
            , Raw_(std::move(raw))
        {}

        TMaybeExpires& operator=(TMaybe<TExpires> expires) noexcept {
            Raw_.Drop();
            Expires_ = expires;
            return *this;
        }

        TMaybeExpires& operator=(TExpires expires) noexcept {
            Raw_.Drop();
            Expires_ = expires;
            return *this;
        }

        explicit operator bool() const noexcept {
            return bool(Expires_);
        }

        void Clear() noexcept {
            *this = Nothing();
        }

        [[nodiscard]]
        TExpires Get() const noexcept {
            return *Expires_;
        }

        [[nodiscard]]
        TExpires GetOrElse(TExpires e) const noexcept {
            return Expires_.GetOrElse(e);
        }

        [[nodiscard]]
        TMaybe<TExpires> Maybe() const noexcept {
            return Expires_;
        }

        [[nodiscard]]
        TBlob Raw() const noexcept {
            return Raw_;
        }

#define Y_COOKIE_EXPIRES_OP(op) \
        bool operator op (const TMaybeExpires& other) const noexcept { return Expires_ op other.Expires_; } \
        bool operator op (const TMaybe<TExpires>& other) const noexcept { return Expires_ op other; } \
        bool operator op (const TExpires& other) const noexcept { return Expires_ op other; } \
        friend bool operator op (const TMaybe<TExpires>& a, const TMaybeExpires& b) noexcept { return b op a; } \
        friend bool operator op (const TExpires& a, const TMaybeExpires& b) noexcept { return b op a; }

        Y_COOKIE_EXPIRES_OP(==)
        Y_COOKIE_EXPIRES_OP(!=)
#undef Y_COOKIE_EXPIRES_OP

    private:
        TMaybe<TExpires> Expires_;
        TBlob Raw_;
    };


    class TExpiresCache {
    public:
        TExpires Get(TInstant then) noexcept {
            then = std::min(then, MaxExpiresTstamp);
            // The idea is that in production the time flows slowly in one direction
            //      and the same Expires (in terms of delta from now) needs to be evaluated multiple times per second.
            if (then != Last_) {
                Expires_ = *TExpires::FromInstant(then);
                Last_ = then;
            }
            return Expires_;
        }

    private:
        TInstant Last_;
        TExpires Expires_;
    };

    template <ui32 Seconds>
    class TExpiresGlobalCache {
        using TSelf = TExpiresGlobalCache<Seconds>;
        static constexpr auto Delta_ = TDuration::Seconds(Seconds);
    public:
        static TExpires Get(TInstant now) noexcept {
            return FastTlsSingleton<TSelf>()->Cache_.Get(now + TSelf::Delta_);
        }

    private:
        TExpiresCache Cache_;
    };

    using TExpiresNow = TExpiresGlobalCache<0>;
}
