#pragma once

#include <balancer/kernel/helpers/errors.h>
#include <util/generic/overloaded.h>
#include <util/digest/city.h>
#include <util/network/address.h>

#include <array>
#include <variant>
#include <vector>

class TCont;

namespace NSrvKernel {

    template <class TDerived, class TIp4, class TIp6>
    class TIp4Ip6Base {
    public:
        using TSelf = TIp4Ip6Base<TDerived, TIp4, TIp6>;

        TIp4Ip6Base() = default;

        TIp4Ip6Base(const TIp4& a) noexcept
            : Impl_(a)
        {}

        TIp4Ip6Base(const TIp6& a) noexcept
            : Impl_(a)
        {}

        explicit operator bool() const noexcept {
            return !Empty();
        }

        bool Empty() const noexcept {
            return std::holds_alternative<std::monostate>(Impl_);
        }

        bool operator == (const TDerived& o) const noexcept {
            return GetRawBytes() == o.GetRawBytes();
        }

        bool operator != (const TDerived& o) const noexcept {
            return !(*this == o);
        }

        // CityHash64({}) if Empty
        ui64 Hash() const noexcept {
            return CityHash64(GetRawBytes());
        }

        // 0 if Empty
        socklen_t SizeOf() const noexcept {
            return GetRawBytes().Size();
        }

        // 0 if Empty
        int AddrFamily() const noexcept {
            return std::visit(TOverloaded{
                [](const TIp4&) { return AF_INET; },
                [](const TIp6&) { return AF_INET6; },
                [](const std::monostate&) { return 0; },
            }, Impl_);
        }

        // 0 if Empty
        ui32 AddrBits() const noexcept {
            return AddrBytes() * 8;
        }

        ui32 AddrBytes() const noexcept {
            return std::visit(TOverloaded{
                [](const TIp4&) { return 4; },
                [](const TIp6&) { return 16; },
                [](const std::monostate&) { return 0; },
            }, Impl_);
        }

        // nullptr if not v4
        const TIp4* V4() const noexcept {
            return std::get_if<TIp4>(&Impl_);
        }

        // nullptr if not v6
        const TIp6* V6() const noexcept {
            return std::get_if<TIp6>(&Impl_);
        }

        TStringBuf GetRawBytes() const noexcept {
            return std::visit(TOverloaded{
                [](const std::monostate&) { return TStringBuf(); },
                [](const auto& a) { return TStringBuf((const char*) &a, sizeof(std::decay_t<decltype(a)>)); },
            }, Impl_);
        }

    protected:
        using TImpl = std::variant<std::monostate, TIp4, TIp6>;
        TImpl Impl_;
    };


    using TIp4Raw = std::array<ui8, 4>;
    using TIp6Raw = std::array<ui16, 8>;

    class TIpSubnet;

    class TIpAddr : public TIp4Ip6Base<TIpAddr, in_addr, in6_addr> {
        friend class TSockAddr;
        friend class TIpSubnet;

    public:
        using TIp4Ip6Base::TIp4Ip6Base;

        TIpAddr() noexcept = default;

        // TODO(velavokr): constexpr
        TIpAddr(TIp4Raw s) noexcept;
        TIpAddr(TIp6Raw s) noexcept;

        TIpSubnet Subnet(ui32 prefixBits) const noexcept;

        TIpAddr Broadcast(ui32 prefixLength) const noexcept;

        // unicast loopback
        bool Loopback() const noexcept;

        // unicast link-local
        bool LinkLocal() const noexcept;

        // unicast global
        bool Global() const noexcept;


        // any kind of multicast
        bool Multicast() const noexcept;

        // multicast node-local
        bool McNodeLocal() const noexcept;

        // multicast link-local
        bool McLinkLocal() const noexcept;

        // multicast global
        bool McGlobal() const noexcept;

        /* "127.0.0.1" if in_addr
         * "::1" if in6_addr
         * "" if Empty */
        TString ToString() const noexcept;

        static TErrorOr<TIpAddr> FromIp(const TString& ip) noexcept;
    };


    const TIpAddr& Loopback4() noexcept;
    const TIpAddr& Loopback6() noexcept;
    const TIpAddr& Blackhole6() noexcept;


    class TIpSubnet {
        friend class TIpAddr;

    public:
        TIpSubnet() = default;

        TIpSubnet(TIpAddr prefix, ui32 prefixBits) noexcept;

        explicit operator bool() const noexcept {
            return !Empty();
        }

        bool Empty() const noexcept {
            return Addr_.Empty();
        }

        bool operator == (const TIpSubnet& other) const noexcept {
            return Addr_ == other.Addr_ && PrefixBits_ == other.PrefixBits_;
        }

        bool operator != (const TIpSubnet& o) const noexcept {
            return !(*this == o);
        }

        bool Contains(const TIpSubnet& other) const noexcept;

        bool Contains(const TIpAddr& other) const noexcept;

        const TIpAddr& Addr() const noexcept {
            return Addr_;
        }

        TIpAddr Broadcast() const noexcept {
            return Addr_.Broadcast(PrefixBits_);
        }

        ui32 PrefixBits() const noexcept {
            return PrefixBits_;
        }

        TString ToString() const noexcept;

        static TErrorOr<TIpSubnet> FromSubnet(const TString& subnet) noexcept;

    private:
        struct TNoop {};
        TIpSubnet(TNoop, TIpAddr prefix, ui32 prefixBits) noexcept;

    private:
        TIpAddr Addr_;
        ui32 PrefixBits_ = 0;
    };


    struct TIp6AuxFields {
        // Hardly ever used. Corresponds to the flow label ipv6 field
        // http://mural.maynoothuniversity.ie/1505/1/Maloneec2nd05.pdf
        ui32 FlowInfo = 0;
        /* You might only ever need this when dealing with link-local addresses. VERY platform-specific.
         * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/netinet_in.h.html
         * https://msdn.microsoft.com/en-us/data/ff570824(v=vs.90)
         * http://tldp.org/HOWTO/html_single/Linux+IPv6-HOWTO/#idp58634448
         * https://wiki.yandex-team.ru/users/butcher/notes/IPv6scopesandzones/
         * https://networkengineering.stackexchange.com/q/46653
         * */
        ui32 ScopeId = 0;
    };


    class TSockAddr : public NAddr::IRemoteAddr, public TIp4Ip6Base<TSockAddr, sockaddr_in, sockaddr_in6> {
    public:
        using TIp4Ip6Base::TIp4Ip6Base;

        TSockAddr() noexcept = default;

        TSockAddr(const TIpAddr& a, ui16 port, const TIp6AuxFields& = {}) noexcept;

        // nullptr if Empty
        const sockaddr* Addr() const noexcept override;

        // 0 if Empty
        socklen_t Len() const noexcept override {
            return SizeOf();
        }

        // Empty if neither sockaddr_in nor sockaddr_in6
        TIpAddr Ip() const noexcept;

        // 0 if neither sockaddr_in nor sockaddr_in6
        ui16 Port() const noexcept;

        void SetPort(ui16 port) noexcept;

        // Hardly ever needed. Returns default if not sockaddr_in6
        TIp6AuxFields Ip6AuxFields() const noexcept;

        // Hardly ever needed. Does nothing if not sockaddr_in6
        void SetIp6AuxFields(const TIp6AuxFields& aux) noexcept;

        /* "127.0.0.1:8080" if sockaddr_in
         * "[::1]:8080" if sockaddr_in6
         * "" if Empty */
        TString ToString() const noexcept;

        static TErrorOr<TSockAddr> FromIpPort(const TString& ipPort) noexcept;

        static TErrorOr<TSockAddr> FromIpPort(const TString& ip, ui16 port) noexcept;

        // May provide len validation if needed
        static TErrorOr<TSockAddr> FromSockAddr(const sockaddr& addr, socklen_t len = sizeof(sockaddr_storage)) noexcept;

        static TErrorOr<TSockAddr> FromSockAddr(const sockaddr_storage& addr) noexcept;

        static TErrorOr<TSockAddr> FromRemoteAddr(const NAddr::IRemoteAddr& addr) noexcept;

        static TErrorOr<std::vector<TSockAddr>> FromAddrInfo(const addrinfo& info) noexcept;

        static TErrorOr<std::vector<TSockAddr>> FromAddrInfo(const TNetworkAddress& info) noexcept;
    };

    int ConnectD(TCont* cont, TSocketHolder& s, const TSockAddr& addr, int type, int protocol, TInstant deadline) noexcept;

    struct TSockAddrInfo {
        std::vector<TSockAddr> Addresses;
    };
}

template <>
struct THash<NSrvKernel::TIpAddr> {
    size_t operator()(const NSrvKernel::TIpAddr& a) const noexcept {
        return a.Hash();
    }
};

template <>
struct THash<NSrvKernel::TSockAddr> {
    size_t operator()(const NSrvKernel::TSockAddr& a) const noexcept {
        return a.Hash();
    }
};
