import { logger } from "../../../logger/index.js";
import { joinUrlParts } from "../../../util/url.js";
import { withCache } from "../../../util/cache/package/with-cache.js";
import { Datasource } from "../datasource.js";
import { Readable } from "node:stream";
import { gunzip } from "node:zlib";
import { XmlDocument } from "xmldoc";
import sax from "sax";
import { promisify } from "util";

//#region lib/modules/datasource/rpm/index.ts
const gunzipAsync = promisify(gunzip);
var RpmDatasource = class RpmDatasource extends Datasource {
	static id = "rpm";
	static repomdXmlFileName = "repomd.xml";
	constructor() {
		super(RpmDatasource.id);
	}
	/**
	* Users are able to specify custom RPM repositories as long as they follow the format.
	* There is a URI http://linux.duke.edu/metadata/common in the <sha>-primary.xml.
	* But according to this post, it's not something we can really look into or reference.
	* @see{https://lists.rpm.org/pipermail/rpm-ecosystem/2015-October/000283.html}
	*/
	customRegistrySupport = true;
	/**
	* Users can specify multiple repositories and the datasource will aggregate the releases
	* @example
	* Every Fedora release has "release" and "updates" repositories.
	* To get the latest package version, these repositories should be aggregated.
	*/
	registryStrategy = "merge";
	/**
	* Fetches the release information for a given package from the registry URL.
	*
	* @param registryUrl - the registryUrl should be the folder which contains repodata.xml and its corresponding file list <sha256>-primary.xml.gz, e.g.: https://packages.microsoft.com/azurelinux/3.0/prod/cloud-native/x86_64/repodata/
	* @param packageName - the name of the package to fetch releases for.
	* @returns The release result if the package is found, otherwise null.
	*/
	async _getReleases({ registryUrl, packageName }) {
		if (!registryUrl || !packageName) return null;
		try {
			const primaryGzipUrl = await this.getPrimaryGzipUrl(registryUrl);
			if (!primaryGzipUrl) return null;
			return await this.getReleasesByPackageName(primaryGzipUrl, packageName);
		} catch (err) {
			this.handleGenericErrors(err);
		}
	}
	getReleases(config) {
		return withCache({
			namespace: `datasource-${RpmDatasource.id}`,
			key: `${config.registryUrl}:${config.packageName}`,
			ttlMinutes: 1440,
			fallback: true
		}, () => this._getReleases(config));
	}
	async _getPrimaryGzipUrl(registryUrl) {
		const repomdUrl = joinUrlParts(registryUrl, RpmDatasource.repomdXmlFileName);
		const response = await this.http.getText(repomdUrl.toString());
		if (!response.body.startsWith("<?xml")) {
			logger.debug({
				datasource: RpmDatasource.id,
				url: repomdUrl
			}, "Invalid response format");
			throw new Error(`${repomdUrl} is not in XML format. Response body: ${response.body}`);
		}
		const primaryData = new XmlDocument(response.body).childWithAttribute("type", "primary");
		if (!primaryData) {
			logger.debug(`No primary data found in ${repomdUrl}, xml contents: ${response.body}`);
			throw new Error(`No primary data found in ${repomdUrl}`);
		}
		const locationElement = primaryData.childNamed("location");
		if (!locationElement) throw new Error(`No location element found in ${repomdUrl}`);
		const href = locationElement.attr.href;
		if (!href) throw new Error(`No href found in ${repomdUrl}`);
		return joinUrlParts(registryUrl.replace(/\/repodata\/?$/, "/"), href);
	}
	getPrimaryGzipUrl(registryUrl) {
		return withCache({
			namespace: `datasource-${RpmDatasource.id}`,
			key: registryUrl,
			ttlMinutes: 1440
		}, () => this._getPrimaryGzipUrl(registryUrl));
	}
	async getReleasesByPackageName(primaryGzipUrl, packageName) {
		let response;
		let decompressedBuffer;
		try {
			response = await this.http.getBuffer(primaryGzipUrl);
			if (response.body.length === 0) {
				logger.debug(`Empty response body from getting ${primaryGzipUrl}.`);
				throw new Error(`Empty response body from getting ${primaryGzipUrl}.`);
			}
			decompressedBuffer = await gunzipAsync(response.body);
		} catch (err) {
			logger.debug(`Failed to fetch or decompress ${primaryGzipUrl}: ${err instanceof Error ? err.message : err}`);
			throw err;
		}
		const releases = {};
		let insidePackage = false;
		let isTargetPackage = false;
		let insideName = false;
		const saxParser = sax.createStream(true, {
			lowercase: true,
			trim: true
		});
		saxParser.on("opentag", (node) => {
			if (node.name === "package" && node.attributes.type === "rpm") {
				insidePackage = true;
				isTargetPackage = false;
			}
			if (insidePackage && node.name === "name") insideName = true;
			if (insidePackage && isTargetPackage && node.name === "version") if (node.attributes.rel === void 0) {
				const version = `${node.attributes.ver}`;
				releases[version] = { version };
			} else {
				const version = `${node.attributes.ver}-${node.attributes.rel}`;
				releases[version] = { version };
			}
		});
		saxParser.on("text", (text) => {
			if (insidePackage && insideName) {
				if (text.trim() === packageName) isTargetPackage = true;
			}
		});
		saxParser.on("closetag", (tag) => {
			if (tag === "name" && insidePackage) insideName = false;
			if (tag === "package") {
				insidePackage = false;
				isTargetPackage = false;
			}
		});
		await new Promise((resolve, reject) => {
			let settled = false;
			saxParser.on("error", (err) => {
				if (settled) return;
				settled = true;
				logger.debug(`SAX parsing error in ${primaryGzipUrl}: ${err.message}`);
				setImmediate(() => saxParser.removeAllListeners());
				reject(err);
			});
			saxParser.on("end", () => {
				settled = true;
				setImmediate(() => saxParser.removeAllListeners());
				resolve();
			});
			Readable.from(decompressedBuffer).pipe(saxParser);
		});
		if (Object.keys(releases).length === 0) {
			logger.trace(`No releases found for package ${packageName} in ${primaryGzipUrl}`);
			return null;
		}
		return { releases: Object.values(releases).map((release) => ({ version: release.version })) };
	}
};

//#endregion
export { RpmDatasource };
//# sourceMappingURL=index.js.map