#pragma once

#include <util/generic/maybe.h>
#include <util/generic/strbuf.h>
#include <util/generic/string.h>
#include <util/generic/vector.h>
#include <util/string/cast.h>

#include <arpa/inet.h>

namespace NSv {
    // Remove duplicate slashes, collapse parent directory and current directory references.
    TStringBuf NormalizePathInPlace(TStringBuf) noexcept;

    class IP {
    public:
        // A host-order representation of the address. IPv4 addresses are represented
        // as if they were IPv6-mapped, i.e. `::ffff:x.y.z.w`.
        using TRaw = std::pair<ui64, ui64>;

        union {
            struct sockaddr Base;
            struct sockaddr_in Inet4;
            struct sockaddr_in6 Inet6;
        } Data = {.Base = {.sa_family = AF_UNSPEC}};

    public:
        IP() = default;

        IP(const in_addr& raw, ui32 port = 0) noexcept {
            Data.Inet4 = {.sin_family = AF_INET, .sin_port = htons(port), .sin_addr = raw};
        }

        IP(const in6_addr& raw, ui32 port = 0) noexcept {
            Data.Inet6 = {.sin6_family = AF_INET6, .sin6_port = htons(port), .sin6_addr = raw};
        }

        // Parse an IPv4 or IPv6 address, optionally wrapped in square brackets.
        // Return an AF_UNSPEC address on invalid input.
        static IP Parse(TStringBuf, ui32 port = 0) noexcept;

        // Parse either a full IP address of the specified family, or a CIDR routing prefix:
        // an integer 0-32 for v4 or 0-128 for v6 interpreted as the number of leading ones.
        static TMaybe<TRaw> ParseMask(TStringBuf, int family) noexcept;

        // Resolve a domain name. Fail with ENODATA if the name is invalid or resolves
        // to no addresses. (TODO some other code on other resolution errors?)
        static TVector<IP> Resolve(const TString&, ui32 port = 0) noexcept;

        explicit operator bool() const noexcept {
            return Data.Base.sa_family != AF_UNSPEC;
        }

        bool IsV4() const noexcept {
            return Data.Base.sa_family == AF_INET;
        }

        bool IsV6() const noexcept {
            return Data.Base.sa_family == AF_INET6;
        }

        TRaw Raw(const TRaw& mask = {-1, -1}) const noexcept {
            auto read8 = [p = Data.Inet6.sin6_addr.s6_addr](size_t d) {
                return (ui64)p[d+0] << 56 | (ui64)p[d+1] << 48 | (ui64)p[d+2] << 40 | (ui64)p[d+3] << 32
                     | (ui64)p[d+4] << 24 | (ui64)p[d+5] << 16 | (ui64)p[d+6] << 8  | (ui64)p[d+7];
            };
            return IsV6() ? TRaw{read8(0) & mask.first, read8(8) & mask.second}
                 : IsV4() ? TRaw{0, (0xFFFFull << 32 | ntohl(Data.Inet4.sin_addr.s_addr)) & mask.second}
                 : TRaw{};
        }

        ui32 Port() const noexcept {
            return IsV4() ? ntohs(Data.Inet4.sin_port) : IsV6() ? ntohs(Data.Inet6.sin6_port) : 0;
        }

        TString Format() const {
            char buf[INET6_ADDRSTRLEN + 1] = {};
            if (IsV4()) {
                inet_ntop(AF_INET, &Data.Inet4.sin_addr.s_addr, buf, INET6_ADDRSTRLEN);
            } else if (IsV6()) {
                inet_ntop(AF_INET6, &Data.Inet6.sin6_addr.s6_addr, buf, INET6_ADDRSTRLEN);
            }
            return TString(buf, strlen(buf));
        }

        TString FormatFull() const {
            return IsV4() ? TString::Join(Format(), ":", ToString(Port()))
                 : IsV6() ? TString::Join("[", Format(), "]:", ToString(Port())) : TString{};
        }
    };

    class URL {
    public:
        // [scheme://]host[:port][/path[?query][#fragment]]
        // [scheme://]host[:port]/path[?query][#fragment][:timeout in ms]
        // [scheme://]host[:port[:timeout in ms]]
        // Port defaults to 80 for http, 443 for https, 0 for everything else.
        static TMaybe<URL> Parse(TString addr, bool withTimeout = false) noexcept;

        const TString& ToString() const {
            return Full;
        }

    public:
        TStringBuf Scheme;
        TStringBuf Host;
        TStringBuf Path;
        TStringBuf Query;
        TStringBuf Fragment;
        ui32 Port = 0;
        TMaybe<ui32> Timeout;

    private:
        TString Full;
    };
}
