#include "address.h"

#include <util/digest/city.h>
#include <util/generic/hash.h>
#include <util/generic/map.h>
#include <util/generic/maybe.h>
#include <util/generic/strbuf.h>
#include <util/generic/string.h>
#include <util/generic/yexception.h>

#if defined(_linux_)
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netdb.h>
    #include <ifaddrs.h>
    #include <net/if.h>
#endif

#ifdef _darwin_
    #include <net/if.h>
#endif

namespace NSrvKernel {

#ifdef _linux_
    class TIfAddrsHolder {
    public:
        TIfAddrsHolder() {
            if (getifaddrs(&P_) != 0) {
                ythrow TSystemError() << "can not get interface addresses";
            }
        }

        ~TIfAddrsHolder() {
            if (P_) {
                freeifaddrs(P_);
            }
        }

    public:
        template <typename T>
        class TGenericAddressIter : public T {
        public:
            using TResult = typename T::TResult;

        public:
            TGenericAddressIter()
                : T()
            {
            }

            TGenericAddressIter(ui16 port, ifaddrs* current)
                : Current_(current)
                , Port_(port)
            {
                FillAddr();
            }

            const TResult& Get() const {
                if (!Address_) {
                    ythrow yexception() << "No address configured";
                }

                return *Address_;
            }

            TGenericAddressIter& operator++() {
                if (Current_ == nullptr) {
                    return *this;
                }

                Current_ = Current_->ifa_next;
                FillAddr();

                return *this;
            }

            bool operator==(const TGenericAddressIter& rhs) const noexcept {
                return Current_ == rhs.Current_;
            }

            bool operator!=(const TGenericAddressIter& rhs) const noexcept {
                return !(*this == rhs);
            }

            static bool IsLinkLocal(const ifaddrs* ifaddr) {
                if (ifaddr->ifa_addr->sa_family != AF_INET6) {
                    return false;
                }

                return IN6_IS_ADDR_LINKLOCAL(&(((const struct sockaddr_in6*) ifaddr->ifa_addr)->sin6_addr));
            }


        private:
            bool FillAddr() {
                Address_.Clear();

                for (; Current_ != nullptr; Current_ = Current_->ifa_next) {

                    // skippig loopback interfaces
                    // https://st.yandex-team.ru/BALANCER-659
                    // https://st.yandex-team.ru/BALANCER-650
                    if (Current_->ifa_flags & IFF_LOOPBACK) {
                        continue;
                    }

                    // BALANCER-1926
                    if (Current_->ifa_addr == nullptr) {
                        continue;
                    }

                    const int family = Current_->ifa_addr->sa_family;
                    if (family == AF_INET6 || family == AF_INET) {
                        char host[NI_MAXHOST];
                        int ret = getnameinfo(
                            Current_->ifa_addr, NAddr::SockAddrLength(Current_->ifa_addr),
                            host, NI_MAXHOST, nullptr, 0, NI_NUMERICHOST
                        );
                        if (ret != 0) {
                            return false;
                        }

                        TString hostStr(host);

                        auto it = ProcessedHostnames_.find(hostStr);
                        // making sure we don't bind duplicate addresses
                        // https://st.yandex-team.ru/BALANCER-659
                        if (it != ProcessedHostnames_.end()) {
                            if (this->SkipDuplicateHost(hostStr, it->second, Current_->ifa_name)) {
                                continue;
                            }
                        }

                        // making sure we don't bind localhost address set to some other interface
                        // https://st.yandex-team.ru/BALANCER-659
                        // https://st.yandex-team.ru/BALANCER-650
                        if (!IsIn({"127.0.0.1", "::1"}, hostStr) && !IsLinkLocal(Current_)) {
                            Address_.ConstructInPlace(this->Construct(host, Port_, Current_->ifa_addr));
                            ProcessedHostnames_[std::move(hostStr)] = Current_->ifa_name;
                            return true;
                        }
                    }
                }

                return false;
            }


        private:
            TMap<TString, TString> ProcessedHostnames_;
            TMaybe<TResult> Address_;
            const ifaddrs* Current_ = nullptr;
            ui16 Port_ = 0;
        };


        class TNetworkAddressConstructor {
        public:
            using TResult = TNetworkAddress;

            TNetworkAddress Construct(const TString& host, ui16 port, struct sockaddr* addr) {
                Y_UNUSED(addr);
                return TNetworkAddress{host, port}; // to call getaddrinfo for tests
            }

            bool SkipDuplicateHost(const TString& hostStr, const TString& prevInterface, const char* currentInterface) {
                ythrow yexception() << "Warning: got duplicate address " << hostStr
                                    << " on at least two interfaces: " << prevInterface
                                    << " and " << currentInterface;
            }
        };

        using TNetworkAddressIter = TGenericAddressIter<TNetworkAddressConstructor>;


        class TAddrDescrConstructor {
        public:
            using TResult = TAddrDescr;

            TAddrDescr Construct(const TString& host, ui16 port, const struct sockaddr* addr) {
                Y_UNUSED(host);
                // Getting all addresses of all interfaces and bind on everything
                // but localhost addresses. This behaviour emulates FreeBSD's bind
                // on ('', port) - it binds everything, but you're allowed to bind
                // ('localhost', port) at the same time, and you can't do the same
                // on linux.
                //
                // gencfg generates configs like
                // addrs       = { { ip = "*";         port = port; } }
                // admin_addrs = { { ip = "localhost"; port = port; } }
                //
                // It works perfectly on FreeBSD, but requires this hack on linux.
                //
                // If you call bind with INADDR_ANY and IN6ADDR_ANY_INIT then the actual
                // behaviour of linux depends on the value of /proc/sys/net/ipv6/bindv6only,
                // which is 0 by default and this usually leads to binding on v4 0.0.0.0
                // and on v6 ::, but the latter fails since v4 address is already bound.
                // This issue is fixed now in coroutine library by adding socket flag
                // IPV5_V6ONLY, but it still does not fix the issue with binding on
                // 127.0.0.1 after 0.0.0.0 is already bound.
                //
                // Please note, that you have to make simmetrical change in ipdispatch
                // module if you're willing to change this snippet of code.
                // https://rb.yandex-team.ru/arc/r/130999/
                // https://st.yandex-team.ru/BALANCER-650
                return TAddrDescr{addr, port};
            }

            bool SkipDuplicateHost(const TString& hostStr, const TString& prevInterface, const char* currentInterface) {
                Y_UNUSED(hostStr);
                Y_UNUSED(prevInterface);
                Y_UNUSED(currentInterface);
                return true;
            }
        };

        using TAddrDescrIter = TGenericAddressIter<TAddrDescrConstructor>;


    public:
        TNetworkAddressIter NetworkAddressBegin(ui16 port) const {
            return TNetworkAddressIter(port, P_);
        }

        TNetworkAddressIter NetworkAddressEnd() const {
            return TNetworkAddressIter();
        }

        TAddrDescrIter AddrDescrBegin(ui16 port) const {
            return TAddrDescrIter(port, P_);
        }

        TAddrDescrIter AddrDescrEnd() const {
            return TAddrDescrIter();
        }

    private:
        ifaddrs* P_ = nullptr;
    };
#endif

    void ForEachNonLocalAddress(ui16 port, std::function<void(TNetworkAddress)> reaction) {
#ifdef _linux_
        TIfAddrsHolder ifAddrs;

        for (auto netAddr = ifAddrs.NetworkAddressBegin(port); netAddr != ifAddrs.NetworkAddressEnd(); ++netAddr) {
            reaction(netAddr.Get());
        }
#else
        reaction(TNetworkAddress(port));
#endif
    }

    void ForEachNonLocalAddress(ui16 port, std::function<void(TAddrDescr)> reaction) {
#ifdef _linux_
        TIfAddrsHolder ifAddrs;

        for (auto addrDescr = ifAddrs.AddrDescrBegin(port); addrDescr != ifAddrs.AddrDescrEnd(); ++addrDescr) {
            reaction(addrDescr.Get());
        }
#else
        reaction(TAddrDescr::MatchPort(port));
#endif
    }

    ui64 IpHash(const NAddr::IRemoteAddr& addr) noexcept {
        switch (addr.Addr()->sa_family) {
        case AF_INET: {
            const auto* ipAddr = (const sockaddr_in*) addr.Addr();
            return CityHash64((const char*) &ipAddr->sin_addr.s_addr, sizeof(ipAddr->sin_addr.s_addr));
        } case AF_INET6: {
            const auto* ipAddr6 = (const sockaddr_in6*) addr.Addr();
            return CityHash64((const char*) &ipAddr6->sin6_addr.s6_addr, sizeof(ipAddr6->sin6_addr.s6_addr));
        } default:
            return ComputeHash(PrintHost(addr));
        }
    }

    static bool IsAddrAny(const NAddr::TOpaqueAddr& addr) {
        static constexpr struct in6_addr In6AddrAny = IN6ADDR_ANY_INIT;

        switch (addr.Addr()->sa_family) {
            case AF_INET:
                return reinterpret_cast<const sockaddr_in*>(addr.Addr())->sin_addr.s_addr == INADDR_ANY;
            case AF_INET6:
                return memcmp(&reinterpret_cast<const sockaddr_in6*>(addr.Addr())->sin6_addr, &In6AddrAny, sizeof(in6_addr)) == 0;
            default:
                ythrow yexception{} << "unknown family";
        }
    }

    static ui16 Port(const NAddr::TOpaqueAddr& addr) {
        switch (addr.Addr()->sa_family) {
            case AF_INET:
                return reinterpret_cast<const sockaddr_in*>(addr.Addr())->sin_port;
            case AF_INET6:
                return reinterpret_cast<const sockaddr_in6*>(addr.Addr())->sin6_port;
            default:
                ythrow yexception{} << "unknown family";
        }
    }

    static bool IsAddrAndPortEqual(const NAddr::TOpaqueAddr& a, const NAddr::TOpaqueAddr& b) {
        if ((IsAddrAny(a) || IsAddrAny(a)) && Port(a) == Port(b)) {
            return true;
        }

        return NAddr::IsSame(a, b) && Port(a) == Port(b);
    }

    bool HasConflictingAddresses(const TVector<TNetworkAddress>& first, const TVector<TNetworkAddress>& second) {
        TVector<NAddr::TOpaqueAddr> opaqueFirst;
        for (const TNetworkAddress& addr : first) {
            for (auto it = addr.Begin(); it != addr.End(); ++it) {
                opaqueFirst.emplace_back(it->ai_addr);
            }
        }

        TVector<NAddr::TOpaqueAddr> opaqueSecond;
        for (const TNetworkAddress& addr : second) {
            for (auto it = addr.Begin(); it != addr.End(); ++it) {
                opaqueSecond.emplace_back(it->ai_addr);
            }
        }

        for (const NAddr::TOpaqueAddr& a : opaqueFirst) {
            for (const NAddr::TOpaqueAddr& b : opaqueSecond) {
                if (IsAddrAndPortEqual(a, b)) {
                    return true;
                }
            }
        }

        return false;
    }
}
