"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GetServiceInterface = exports.filledAddress = exports.addressHostToUrl = exports.getHostname = void 0;
exports.filterNonLocal = filterNonLocal;
exports.getOwnServiceInterface = getOwnServiceInterface;
exports.getServiceInterface = getServiceInterface;
const Host_1 = require("../interfaces/Host");
const ip_1 = require("./ip");
const deepEqual_1 = require("./deepEqual");
const once_1 = require("./once");
const Watchable_1 = require("./Watchable");
const getHostnameRegex = /^(\w+:\/\/)?([^\/\:]+)(:\d{1,3})?(\/)?/;
const getHostname = (url) => {
    const founds = url.match(getHostnameRegex)?.[2];
    if (!founds)
        return null;
    const parts = founds.split('@');
    const last = parts[parts.length - 1];
    return last;
};
exports.getHostname = getHostname;
const nonLocalFilter = {
    exclude: {
        kind: ['localhost', 'link-local', 'bridge'],
    },
};
const publicFilter = {
    visibility: 'public',
};
const either = (...args) => (a) => args.some((x) => x(a));
const negate = (fn) => (a) => !fn(a);
const unique = (values) => Array.from(new Set(values));
const addressHostToUrl = ({ scheme, sslScheme, username, suffix }, hostname) => {
    const effectiveScheme = hostname.ssl ? sslScheme : scheme;
    let host;
    if (hostname.metadata.kind === 'ipv6') {
        host = ip_1.IPV6_LINK_LOCAL.contains(hostname.hostname)
            ? `[${hostname.hostname}%${hostname.metadata.scopeId}]`
            : `[${hostname.hostname}]`;
    }
    else {
        host = hostname.hostname;
    }
    let portStr = '';
    if (hostname.port !== null) {
        const excludePort = effectiveScheme &&
            effectiveScheme in Host_1.knownProtocols &&
            hostname.port ===
                Host_1.knownProtocols[effectiveScheme]
                    .defaultPort;
        if (!excludePort)
            portStr = `:${hostname.port}`;
    }
    return `${effectiveScheme ? `${effectiveScheme}://` : ''}${username ? `${username}@` : ''}${host}${portStr}${suffix}`;
};
exports.addressHostToUrl = addressHostToUrl;
function filterRec(hostnames, filter, invert) {
    if (filter.predicate) {
        const pred = filter.predicate;
        hostnames = hostnames.filter((h) => invert !== pred(h));
    }
    if (filter.visibility === 'public')
        hostnames = hostnames.filter((h) => invert !== h.public);
    if (filter.visibility === 'private')
        hostnames = hostnames.filter((h) => invert !== !h.public);
    if (filter.kind) {
        const kind = new Set(Array.isArray(filter.kind) ? filter.kind : [filter.kind]);
        if (kind.has('ip')) {
            kind.add('ipv4');
            kind.add('ipv6');
        }
        hostnames = hostnames.filter((h) => invert !==
            ((kind.has('mdns') && h.metadata.kind === 'mdns') ||
                (kind.has('domain') &&
                    (h.metadata.kind === 'private-domain' ||
                        h.metadata.kind === 'public-domain')) ||
                (kind.has('ipv4') && h.metadata.kind === 'ipv4') ||
                (kind.has('ipv6') && h.metadata.kind === 'ipv6') ||
                (kind.has('localhost') &&
                    ['localhost', '127.0.0.1', '::1'].includes(h.hostname)) ||
                (kind.has('link-local') &&
                    h.metadata.kind === 'ipv6' &&
                    ip_1.IPV6_LINK_LOCAL.contains(ip_1.IpAddress.parse(h.hostname))) ||
                (kind.has('bridge') &&
                    h.metadata.kind === 'ipv4' &&
                    h.metadata.gateway === 'lxcbr0') ||
                (kind.has('plugin') && h.metadata.kind === 'plugin')));
    }
    if (filter.pluginId) {
        const id = filter.pluginId;
        hostnames = hostnames.filter((h) => invert !==
            (h.metadata.kind === 'plugin' && h.metadata.packageId === id));
    }
    if (filter.exclude)
        return filterRec(hostnames, filter.exclude, !invert);
    return hostnames;
}
function isPublicIp(h) {
    return h.public && (h.metadata.kind === 'ipv4' || h.metadata.kind === 'ipv6');
}
function enabledAddresses(addr) {
    return addr.available.filter((h) => {
        if (isPublicIp(h)) {
            // Public IPs: disabled by default, explicitly enabled via SocketAddr string
            if (h.port === null)
                return true;
            const sa = h.metadata.kind === 'ipv6'
                ? `[${h.hostname}]:${h.port}`
                : `${h.hostname}:${h.port}`;
            return addr.enabled.includes(sa);
        }
        else {
            // Everything else: enabled by default, explicitly disabled via [hostname, port] tuple
            return !addr.disabled.some(([hostname, port]) => hostname === h.hostname && port === (h.port ?? 0));
        }
    });
}
/**
 * Filters out localhost and IPv6 link-local hostnames from a list.
 * Equivalent to the `nonLocal` filter on `Filled` addresses.
 */
function filterNonLocal(hostnames) {
    return filterRec(hostnames, nonLocalFilter, false);
}
const filledAddress = (host, addressInfo) => {
    const toUrl = exports.addressHostToUrl.bind(null, addressInfo);
    const binding = host.bindings[addressInfo.internalPort];
    const hostnames = binding ? enabledAddresses(binding.addresses) : [];
    function filledAddressFromHostnames(hostnames) {
        const getNonLocal = (0, once_1.once)(() => filledAddressFromHostnames(filterRec(hostnames, nonLocalFilter, false)));
        const getPublic = (0, once_1.once)(() => filledAddressFromHostnames(filterRec(hostnames, publicFilter, false)));
        return {
            ...addressInfo,
            hostnames,
            toUrl,
            format: (format) => {
                let res = hostnames;
                if (format === 'hostname-info')
                    return res;
                const urls = hostnames.map(toUrl);
                if (format === 'url')
                    res = urls.map((u) => new URL(u));
                else
                    res = urls;
                return res;
            },
            filter: (filter) => {
                return filledAddressFromHostnames(filterRec(hostnames, filter, false));
            },
            matchesAny: (filters) => {
                const seen = new Set();
                const union = [];
                for (const f of filters) {
                    for (const h of filterRec(hostnames, f, false)) {
                        if (!seen.has(h)) {
                            seen.add(h);
                            union.push(h);
                        }
                    }
                }
                return filledAddressFromHostnames(union);
            },
            get nonLocal() {
                return getNonLocal();
            },
            get public() {
                return getPublic();
            },
        };
    }
    return filledAddressFromHostnames(hostnames);
};
exports.filledAddress = filledAddress;
const makeInterfaceFilled = async ({ effects, id, packageId, callback, }) => {
    const serviceInterfaceValue = await effects.getServiceInterface({
        serviceInterfaceId: id,
        packageId,
        callback,
    });
    if (!serviceInterfaceValue) {
        return null;
    }
    const hostId = serviceInterfaceValue.addressInfo.hostId;
    const host = await effects.getHostInfo({
        packageId,
        hostId,
        callback,
    });
    const interfaceFilled = {
        ...serviceInterfaceValue,
        host,
        addressInfo: host
            ? (0, exports.filledAddress)(host, serviceInterfaceValue.addressInfo)
            : null,
    };
    return interfaceFilled;
};
class GetServiceInterface extends Watchable_1.Watchable {
    constructor(effects, opts, options) {
        super(effects, options);
        this.opts = opts;
        this.label = 'GetServiceInterface';
    }
    fetch(callback) {
        return makeInterfaceFilled({
            effects: this.effects,
            id: this.opts.id,
            packageId: this.opts.packageId,
            callback,
        });
    }
}
exports.GetServiceInterface = GetServiceInterface;
function getOwnServiceInterface(effects, id, map, eq) {
    return new GetServiceInterface(effects, { id }, {
        map: map ?? ((a) => a),
        eq: eq ?? ((a, b) => (0, deepEqual_1.deepEqual)(a, b)),
    });
}
function getServiceInterface(effects, opts, map, eq) {
    return new GetServiceInterface(effects, opts, {
        map: map ?? ((a) => a),
        eq: eq ?? ((a, b) => (0, deepEqual_1.deepEqual)(a, b)),
    });
}
//# sourceMappingURL=getServiceInterface.js.map