const { Temporal } = require("@js-temporal/polyfill");
const { URLPattern } = require("urlpattern-polyfill");
const require_docloader = require("./docloader-VJ0pkUIi.cjs");
let _logtape_logtape = require("@logtape/logtape");
//#region src/nodeinfo/semver.ts
const MAX_LENGTH = 256;
/**
* A single `0`, or a non-zero digit followed by zero or more digits.
*/
const NUMERIC_IDENTIFIER = "0|[1-9]\\d*";
/**
* Zero or more digits, followed by a letter or hyphen, and then zero or more letters, digits, or hyphens.
*/
const NON_NUMERIC_IDENTIFIER = "\\d*[a-zA-Z-][a-zA-Z0-9-]*";
/**
* Three dot-separated numeric identifiers.
*/
const VERSION_CORE = `(?<major>${NUMERIC_IDENTIFIER})\\.(?<minor>${NUMERIC_IDENTIFIER})\\.(?<patch>${NUMERIC_IDENTIFIER})`;
/**
* A numeric identifier, or a non-numeric identifier.
*/
const PRERELEASE_IDENTIFIER = `(?:${NUMERIC_IDENTIFIER}|${NON_NUMERIC_IDENTIFIER})`;
/**
* A hyphen, followed by one or more dot-separated pre-release version identifiers.
* @example "-pre.release"
*/
const PRERELEASE = `(?:-(?<prerelease>${PRERELEASE_IDENTIFIER}(?:\\.${PRERELEASE_IDENTIFIER})*))`;
/**
* Any combination of digits, letters, or hyphens.
*/
const BUILD_IDENTIFIER = "[0-9A-Za-z-]+";
/**
* A version, followed optionally by a pre-release version and build metadata.
*/
const FULL_VERSION = `v?${VERSION_CORE}${PRERELEASE}?${`(?:\\+(?<buildmetadata>${BUILD_IDENTIFIER}(?:\\.${BUILD_IDENTIFIER})*))`}?`;
const FULL_REGEXP = new RegExp(`^${FULL_VERSION}$`);
/**
* Attempt to parse a string as a semantic version, returning a SemVer object.
*
* @example Usage
* ```ts
* import { parseSemVer } from "@fedify/fedify/nodeinfo";
* import { assertEquals } from "@std/assert";
*
* const version = parseSemVer("1.2.3");
* assertEquals(version, {
*   major: 1,
*   minor: 2,
*   patch: 3,
*   prerelease: [],
*   build: [],
* });
* ```
*
* @throws {TypeError} If the input string is invalid.
* @param value The version string to parse
* @returns A valid SemVer
* @since 1.2.0
*/
function parseSemVer(value) {
	if (typeof value !== "string") throw new TypeError(`Cannot parse version as version must be a string: received ${typeof value}`);
	if (value.length > MAX_LENGTH) throw new TypeError(`Cannot parse version as version length is too long: length is ${value.length}, max length is ${MAX_LENGTH}`);
	value = value.trim();
	const groups = value.match(FULL_REGEXP)?.groups;
	if (!groups) throw new TypeError(`Cannot parse version: ${value}`);
	return {
		major: parseNumber(groups.major, `Cannot parse version ${value}: invalid major version`),
		minor: parseNumber(groups.minor, `Cannot parse version ${value}: invalid minor version`),
		patch: parseNumber(groups.patch, `Cannot parse version ${value}: invalid patch version`),
		prerelease: groups.prerelease ? parsePrerelease(groups.prerelease) : [],
		build: groups.buildmetadata ? parseBuild(groups.buildmetadata) : []
	};
}
/**
* Returns true if the value is a valid SemVer number.
*
* Must be a number. Must not be NaN. Can be positive or negative infinity.
* Can be between 0 and MAX_SAFE_INTEGER.
* @param value The value to check
* @returns True if its a valid semver number
*/
function isValidNumber(value) {
	return typeof value === "number" && !Number.isNaN(value) && (!Number.isFinite(value) || 0 <= value && value <= Number.MAX_SAFE_INTEGER);
}
const NUMERIC_IDENTIFIER_REGEXP = new RegExp(`^${NUMERIC_IDENTIFIER}$`);
function parsePrerelease(prerelease) {
	return prerelease.split(".").filter(Boolean).map((id) => {
		if (NUMERIC_IDENTIFIER_REGEXP.test(id)) {
			const number = Number(id);
			if (isValidNumber(number)) return number;
		}
		return id;
	});
}
function parseBuild(buildmetadata) {
	return buildmetadata.split(".").filter(Boolean);
}
function parseNumber(input, errorMessage) {
	const number = Number(input);
	if (!isValidNumber(number)) throw new TypeError(errorMessage);
	return number;
}
function formatNumber(value) {
	return value.toFixed(0);
}
/**
* Format a SemVer object into a string.
*
* @example Usage
* ```ts
* import { formatSemVer } from "@fedify/fedify/nodeinfo";
* import { assertEquals } from "@std/assert";
*
* const semver = {
*   major: 1,
*   minor: 2,
*   patch: 3,
* };
* assertEquals(formatSemVer(semver), "1.2.3");
* ```
*
* @param version The SemVer to format
* @returns The string representation of a semantic version.
* @since 1.2.0
*/
function formatSemVer(version) {
	const major = formatNumber(version.major);
	const minor = formatNumber(version.minor);
	const patch = formatNumber(version.patch);
	const pre = version.prerelease?.join(".") ?? "";
	const build = version.build?.join(".") ?? "";
	return [[`${major}.${minor}.${patch}`, pre].filter((v) => v).join("-"), build].filter((v) => v).join("+");
}
//#endregion
//#region src/nodeinfo/client.ts
const logger = (0, _logtape_logtape.getLogger)([
	"fedify",
	"nodeinfo",
	"client"
]);
const DEFAULT_MAX_REDIRECTION = 5;
/**
* Fetches a URL while guarding against SSRF.  Before every request—including
* each redirect hop—the destination is validated with
* {@link validatePublicUrl}, so that loopback, link-local, and private
* addresses, as well as non-HTTP(S) schemes such as `data:`, are rejected.
* Redirects are followed manually with a cap, so that a public URL cannot
* bounce the request to an internal address.
* @param url The URL to fetch.
* @param headers The request headers.
* @returns The final response, or `null` if the URL is disallowed or there
*          are too many redirects.
*/
async function fetchPublicUrl(url, headers) {
	let current = url instanceof URL ? url : new URL(url);
	let redirected = 0;
	while (true) {
		try {
			await require_docloader.validatePublicUrl(current.href);
		} catch (e) {
			if (e instanceof require_docloader.UrlError) {
				logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
					url: current.href,
					error: e
				});
				return null;
			}
			throw e;
		}
		const response = await fetch(current, {
			headers,
			redirect: "manual"
		});
		if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
			redirected++;
			if (redirected >= DEFAULT_MAX_REDIRECTION) {
				logger.error("Too many redirections ({redirections}) while fetching {url}.", {
					redirections: redirected,
					url: current.href
				});
				return null;
			}
			const next = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? current : response.url);
			if (next.protocol !== current.protocol) {
				logger.error("Refused a redirect to a different protocol ({protocol} to {redirectedProtocol}) while fetching NodeInfo.", {
					protocol: current.protocol,
					redirectedProtocol: next.protocol
				});
				return null;
			}
			current = next;
			continue;
		}
		return response;
	}
}
async function getNodeInfo(url, options = {}) {
	try {
		let nodeInfoUrl = url;
		if (!options.direct) {
			const wellKnownUrl = new URL("/.well-known/nodeinfo", url);
			const wellKnownResponse = await fetchPublicUrl(wellKnownUrl, {
				Accept: "application/json",
				"User-Agent": typeof options.userAgent === "string" ? options.userAgent : require_docloader.getUserAgent(options.userAgent)
			});
			if (wellKnownResponse == null) return void 0;
			if (!wellKnownResponse.ok) {
				logger.error("Failed to fetch {url}: {status} {statusText}", {
					url: wellKnownUrl.href,
					status: wellKnownResponse.status,
					statusText: wellKnownResponse.statusText
				});
				return;
			}
			const wellKnownRd = await wellKnownResponse.json();
			const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
			if (link == null || link.href == null) {
				logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
					url: wellKnownUrl.href,
					resourceDescriptor: wellKnownRd
				});
				return;
			}
			nodeInfoUrl = link.href;
		}
		const response = await fetchPublicUrl(nodeInfoUrl, {
			Accept: "application/json",
			"User-Agent": typeof options.userAgent === "string" ? options.userAgent : require_docloader.getUserAgent(options.userAgent)
		});
		if (response == null) return void 0;
		if (!response.ok) {
			logger.error("Failed to fetch NodeInfo document from {url}: {status} {statusText}", {
				url: nodeInfoUrl.toString(),
				status: response.status,
				statusText: response.statusText
			});
			return;
		}
		const data = await response.json();
		if (options.parse === "none") return data;
		return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
	} catch (error) {
		logger.error("Failed to fetch NodeInfo document from {url}: {error}", {
			url: url.toString(),
			error
		});
		return;
	}
}
/**
* Parses a NodeInfo document.
* @param data A JSON value that complies with the NodeInfo schema.
* @param options Options for parsing the NodeInfo document.
* @returns The parsed NodeInfo document if it is valid.  Otherwise, `null`
*          is returned.
* @since 1.2.0
*/
function parseNodeInfo(data, options = {}) {
	if (typeof data !== "object" || data == null || !("software" in data)) return null;
	const software = parseSoftware(data.software, options);
	if (software == null) return null;
	let protocols = [];
	if ("protocols" in data && Array.isArray(data.protocols)) {
		const ps = data.protocols.map(parseProtocol);
		protocols = ps.filter((p) => p != null);
		if (ps.length != protocols.length && !options.tryBestEffort) return null;
	} else if (!options.tryBestEffort) return null;
	let services;
	if ("services" in data) {
		if (typeof data.services === "object" && data.services != null) {
			const ss = parseServices(data.services, options);
			if (ss == null) {
				if (!options.tryBestEffort) return null;
			} else services = ss;
		} else if (!options.tryBestEffort) return null;
	}
	let openRegistrations;
	if ("openRegistrations" in data) {
		if (typeof data.openRegistrations === "boolean") openRegistrations = data.openRegistrations;
		else if (!options.tryBestEffort) return null;
	}
	let usage = {
		users: {},
		localPosts: 0,
		localComments: 0
	};
	if ("usage" in data) {
		const u = parseUsage(data.usage, options);
		if (u == null) {
			if (!options.tryBestEffort) return null;
		} else usage = u;
	}
	let metadata;
	if ("metadata" in data) {
		if (typeof data.metadata === "object" && data.metadata != null) metadata = Object.fromEntries(Object.entries(data.metadata));
		else if (!options.tryBestEffort) return null;
	}
	const result = {
		software,
		protocols,
		usage
	};
	if (services != null) result.services = services;
	if (openRegistrations != null) result.openRegistrations = openRegistrations;
	if (metadata != null) result.metadata = metadata;
	return result;
}
function parseSoftware(data, options = {}) {
	if (typeof data !== "object" || data == null) {
		if (!options.tryBestEffort) data = {};
		return null;
	}
	let name;
	if ("name" in data && typeof data.name === "string" && data.name.match(/^\s*[A-Za-z0-9-]+\s*$/)) {
		if (!data.name.match(/^[a-z0-9-]+$/) && !options.tryBestEffort) return null;
		name = data.name.trim().toLowerCase();
	} else return null;
	let version;
	if ("version" in data && typeof data.version === "string") try {
		version = parseSemVer(data.version);
	} catch {
		if (!options.tryBestEffort) return null;
		const parts = data.version.split(".").map((p) => parseInt(p, 10));
		version = {
			major: !isNaN(parts[0]) ? parts[0] : 0,
			minor: !isNaN(parts[1]) ? parts[1] : 0,
			patch: !isNaN(parts[2]) ? parts[2] : 0,
			build: [],
			prerelease: []
		};
	}
	else {
		if (!options.tryBestEffort) return null;
		version = {
			major: 0,
			minor: 0,
			patch: 0,
			build: [],
			prerelease: []
		};
	}
	let repository;
	if ("repository" in data) {
		if (typeof data.repository === "string") try {
			repository = new URL(data.repository);
		} catch {
			if (!options.tryBestEffort) return null;
		}
		else if (!options.tryBestEffort) return null;
	}
	let homepage;
	if ("homepage" in data) {
		if (typeof data.homepage === "string") try {
			homepage = new URL(data.homepage);
		} catch {
			if (!options.tryBestEffort) return null;
		}
		else if (!options.tryBestEffort) return null;
	}
	const result = {
		name,
		version
	};
	if (repository != null) result.repository = repository;
	if (homepage != null) result.homepage = homepage;
	return result;
}
function parseProtocol(data) {
	if (data === "activitypub" || data === "buddycloud" || data === "dfrn" || data === "diaspora" || data === "libertree" || data === "ostatus" || data === "pumpio" || data === "tent" || data === "xmpp" || data === "zot") return data;
	return null;
}
function parseServices(data, options = {}) {
	if (!(typeof data === "object") || data == null) {
		if (options.tryBestEffort) return {};
		return null;
	}
	let inbound;
	if ("inbound" in data && Array.isArray(data.inbound)) {
		const is = data.inbound.map(parseInboundService);
		inbound = is.filter((i) => i != null);
		if (is.length > inbound.length && !options.tryBestEffort) return null;
	}
	let outbound;
	if ("outbound" in data && Array.isArray(data.outbound)) {
		const os = data.outbound.map(parseOutboundService);
		outbound = os.filter((o) => o != null);
		if (os.length > outbound.length && !options.tryBestEffort) return null;
	}
	const result = {};
	if (inbound != null) result.inbound = inbound;
	if (outbound != null) result.outbound = outbound;
	return result;
}
function parseInboundService(data) {
	if (data === "atom1.0" || data === "gnusocial" || data === "imap" || data === "pnut" || data === "pop3" || data === "pumpio" || data === "rss2.0" || data === "twitter") return data;
	return null;
}
function parseOutboundService(data) {
	if (data === "atom1.0" || data === "blogger" || data === "buddycloud" || data === "diaspora" || data === "dreamwidth" || data === "drupal" || data === "facebook" || data === "friendica" || data === "gnusocial" || data === "google" || data === "insanejournal" || data === "libertree" || data === "linkedin" || data === "livejournal" || data === "mediagoblin" || data === "myspace" || data === "pinterest" || data === "pnut" || data === "posterous" || data === "pumpio" || data === "redmatrix" || data === "rss2.0" || data === "smtp" || data === "tent" || data === "tumblr" || data === "twitter" || data === "wordpress" || data === "xmpp") return data;
	return null;
}
function parseUsage(data, options = {}) {
	if (typeof data !== "object" || data == null) return null;
	const users = {};
	if ("users" in data && typeof data.users === "object" && data.users != null) {
		if ("total" in data.users) if (typeof data.users.total === "number") users.total = data.users.total;
		else {
			if (!options.tryBestEffort) return null;
			if (typeof data.users.total === "string") {
				const n = parseInt(data.users.total);
				if (!isNaN(n)) users.total = n;
			}
		}
		if ("activeHalfyear" in data.users) if (typeof data.users.activeHalfyear === "number") users.activeHalfyear = data.users.activeHalfyear;
		else {
			if (!options.tryBestEffort) return null;
			if (typeof data.users.activeHalfyear === "string") {
				const n = parseInt(data.users.activeHalfyear);
				if (!isNaN(n)) users.activeHalfyear = n;
			}
		}
		if ("activeMonth" in data.users) if (typeof data.users.activeMonth === "number") users.activeMonth = data.users.activeMonth;
		else {
			if (!options.tryBestEffort) return null;
			if (typeof data.users.activeMonth === "string") {
				const n = parseInt(data.users.activeMonth);
				if (!isNaN(n)) users.activeMonth = n;
			}
		}
	} else if (!options.tryBestEffort) return null;
	let localPosts = 0;
	if ("localPosts" in data) if (typeof data.localPosts === "number") localPosts = data.localPosts;
	else {
		if (!options.tryBestEffort) return null;
		if (typeof data.localPosts === "string") {
			const n = parseInt(data.localPosts);
			if (!isNaN(n)) localPosts = n;
		}
	}
	let localComments = 0;
	if ("localComments" in data) if (typeof data.localComments === "number") localComments = data.localComments;
	else {
		if (!options.tryBestEffort) return null;
		if (typeof data.localComments === "string") {
			const n = parseInt(data.localComments);
			if (!isNaN(n)) localComments = n;
		}
	}
	return {
		users,
		localPosts,
		localComments
	};
}
//#endregion
//#region src/nodeinfo/types.ts
/**
* Converts a {@link NodeInfo} object to a JSON value.
* @param nodeInfo The {@link NodeInfo} object to convert.
* @returns The JSON value that complies with the NodeInfo schema.
* @throws {TypeError} If the {@link NodeInfo} object is invalid.
*/
function nodeInfoToJson(nodeInfo) {
	if (!nodeInfo.software.name.match(/^[a-z0-9-]+$/)) throw new TypeError("Invalid software name.");
	if (nodeInfo.protocols.length < 1) throw new TypeError("At least one protocol must be supported.");
	if (nodeInfo.usage.users.total != null && (nodeInfo.usage.users.total < 0 || !Number.isInteger(nodeInfo.usage.users.total))) throw new TypeError("Invalid total users.");
	if (nodeInfo.usage.users.activeHalfyear != null && (nodeInfo.usage.users.activeHalfyear < 0 || !Number.isInteger(nodeInfo.usage.users.activeHalfyear))) throw new TypeError("Invalid active halfyear users.");
	if (nodeInfo.usage.users.activeMonth != null && (nodeInfo.usage.users.activeMonth < 0 || !Number.isInteger(nodeInfo.usage.users.activeMonth))) throw new TypeError("Invalid active month users.");
	if (nodeInfo.usage.localPosts < 0 || !Number.isInteger(nodeInfo.usage.localPosts)) throw new TypeError("Invalid local posts.");
	if (nodeInfo.usage.localComments < 0 || !Number.isInteger(nodeInfo.usage.localComments)) throw new TypeError("Invalid local comments.");
	return {
		"$schema": "http://nodeinfo.diaspora.software/ns/schema/2.1#",
		version: "2.1",
		software: {
			name: nodeInfo.software.name,
			version: formatSemVer(nodeInfo.software.version),
			repository: nodeInfo.software.repository?.href,
			homepage: nodeInfo.software.homepage?.href
		},
		protocols: nodeInfo.protocols,
		services: nodeInfo.services == null ? {
			inbound: [],
			outbound: []
		} : {
			inbound: nodeInfo.services.inbound ?? [],
			outbound: nodeInfo.services.outbound ?? []
		},
		openRegistrations: nodeInfo.openRegistrations ?? false,
		usage: {
			users: nodeInfo.usage.users,
			localPosts: nodeInfo.usage.localPosts,
			localComments: nodeInfo.usage.localComments
		},
		metadata: nodeInfo.metadata ?? {}
	};
}
//#endregion
Object.defineProperty(exports, "formatSemVer", {
	enumerable: true,
	get: function() {
		return formatSemVer;
	}
});
Object.defineProperty(exports, "getNodeInfo", {
	enumerable: true,
	get: function() {
		return getNodeInfo;
	}
});
Object.defineProperty(exports, "nodeInfoToJson", {
	enumerable: true,
	get: function() {
		return nodeInfoToJson;
	}
});
Object.defineProperty(exports, "parseNodeInfo", {
	enumerable: true,
	get: function() {
		return parseNodeInfo;
	}
});
Object.defineProperty(exports, "parseSemVer", {
	enumerable: true,
	get: function() {
		return parseSemVer;
	}
});
