import { getMatches } from "../file-matching.js";
import { defineSource } from "../source.js";
import { nonEmptyString, optionalUrl, stringArray } from "../utilities/schema-primitives.js";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { z } from "zod";
import { parse } from "yaml";
//#region src/lib/sources/publiccode-yaml.ts
/**
* Source and parser for `publiccode.yml` / `publiccode.yaml` files.
*
* Publiccode.yml is a metadata standard for public software repositories,
* primarily used in Europe (Italy, Netherlands, etc.).
* See: https://yml.publiccode.tools/
*
* Extracts project metadata including name, version, license, contacts,
* multi-language descriptions, dependencies, and categorization.
*/
const publiccodeContactEntrySchema = z.object({
	affiliation: z.string().optional(),
	email: z.string().optional(),
	name: z.string(),
	phone: z.string().optional()
});
const publiccodeContractorEntrySchema = z.object({
	name: z.string(),
	until: z.string().optional(),
	website: z.string().optional()
});
const publiccodeDependencyEntrySchema = z.object({
	category: z.enum([
		"hardware",
		"open",
		"proprietary"
	]),
	name: z.string(),
	optional: z.boolean().optional(),
	version: z.string().optional(),
	versionMax: z.string().optional(),
	versionMin: z.string().optional()
});
const publiccodeDescriptionSchema = z.object({
	documentation: z.string().optional(),
	features: z.array(z.string()),
	genericName: z.string().optional(),
	localisedName: z.string().optional(),
	longDescription: z.string().optional(),
	shortDescription: z.string().optional()
});
const publiccodeSchema = z.object({
	applicationSuite: nonEmptyString,
	availableLanguages: stringArray,
	categories: stringArray,
	contacts: z.array(publiccodeContactEntrySchema),
	contractors: z.array(publiccodeContractorEntrySchema),
	dependencies: z.array(publiccodeDependencyEntrySchema),
	description: publiccodeDescriptionSchema.optional(),
	descriptions: z.record(z.string(), publiccodeDescriptionSchema),
	developmentStatus: nonEmptyString,
	inputTypes: stringArray,
	isBasedOn: optionalUrl,
	landingUrl: optionalUrl,
	license: nonEmptyString,
	localisationReady: z.boolean().optional(),
	logo: optionalUrl,
	mainCopyrightOwner: nonEmptyString,
	maintenanceType: nonEmptyString,
	monochromeLogo: optionalUrl,
	name: nonEmptyString,
	outputTypes: stringArray,
	platforms: stringArray,
	publiccodeYmlVersion: nonEmptyString,
	releaseDate: nonEmptyString,
	repoOwner: nonEmptyString,
	roadmap: optionalUrl,
	softwareType: nonEmptyString,
	softwareVersion: nonEmptyString,
	url: optionalUrl,
	usedBy: stringArray
});
/** Coerce YAML values that may be parsed as non-strings back to strings. */
function toString(value) {
	if (typeof value === "string") return value;
	if (typeof value === "number") return String(value);
	if (value instanceof Date) return value.toISOString().slice(0, 10);
}
/** Check if a value is a non-empty string. */
function isNonEmptyString(value) {
	return typeof value === "string" && value.trim().length > 0;
}
/** Check if a value is a plain object (not null, not array). */
function isPlainObject(value) {
	return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
}
/** Extract string array from a YAML value, filtering non-strings. */
function toStringArray(value) {
	if (!Array.isArray(value)) return [];
	return value.filter((item) => typeof item === "string");
}
/** Parse a description block from a publiccode description object. */
function parseDescription(data) {
	const features = [];
	if (Array.isArray(data.features)) {
		for (const feature of data.features) if (typeof feature === "string") features.push(feature);
		else if (isPlainObject(feature)) for (const [key, value] of Object.entries(feature)) features.push(typeof value === "string" ? `${key}: ${value}` : key);
	}
	return {
		...isNonEmptyString(data.documentation) ? { documentation: data.documentation } : {},
		features,
		...isNonEmptyString(data.genericName) ? { genericName: data.genericName } : {},
		...isNonEmptyString(data.localisedName) ? { localisedName: data.localisedName } : {},
		...isNonEmptyString(data.longDescription) ? { longDescription: data.longDescription.trim() } : {},
		...isNonEmptyString(data.shortDescription) ? { shortDescription: data.shortDescription.trim() } : {}
	};
}
/**
* Parse a publiccode.yml / publiccode.yaml file content.
* @param content - Raw YAML file content.
* @returns Parsed publiccode metadata, or `undefined` if the content is invalid.
*/
function parse$1(content) {
	let data;
	try {
		data = parse(content);
	} catch {
		return;
	}
	if (!isPlainObject(data)) return void 0;
	if (!isNonEmptyString(data.name) && !isNonEmptyString(data.url)) return void 0;
	const descriptions = {};
	let description;
	if (isPlainObject(data.description)) {
		const descObject = data.description;
		for (const [lang, langData] of Object.entries(descObject)) if (isPlainObject(langData)) descriptions[lang] = parseDescription(langData);
		const preferredLang = "en" in descriptions ? "en" : Object.keys(descriptions)[0];
		if (preferredLang) description = descriptions[preferredLang];
	}
	const contacts = [];
	if (isPlainObject(data.maintenance)) {
		const { maintenance } = data;
		if (Array.isArray(maintenance.contacts)) {
			for (const contact of maintenance.contacts) if (isPlainObject(contact) && isNonEmptyString(contact.name)) {
				const entry = { name: contact.name };
				if (isNonEmptyString(contact.email)) entry.email = contact.email;
				if (isNonEmptyString(contact.phone)) entry.phone = contact.phone;
				if (isNonEmptyString(contact.affiliation)) entry.affiliation = contact.affiliation;
				contacts.push(entry);
			}
		}
	}
	const contractors = [];
	if (isPlainObject(data.maintenance)) {
		const { maintenance } = data;
		if (Array.isArray(maintenance.contractors)) {
			for (const contractor of maintenance.contractors) if (isPlainObject(contractor) && isNonEmptyString(contractor.name)) {
				const entry = { name: contractor.name };
				const until = toString(contractor.until);
				if (until) entry.until = until;
				if (isNonEmptyString(contractor.website)) entry.website = contractor.website;
				contractors.push(entry);
			}
		}
	}
	const dependencies = [];
	if (isPlainObject(data.dependsOn)) {
		const { dependsOn } = data;
		for (const category of [
			"open",
			"proprietary",
			"hardware"
		]) {
			const categoryDeps = dependsOn[category];
			if (Array.isArray(categoryDeps)) {
				for (const dep of categoryDeps) if (isPlainObject(dep) && isNonEmptyString(dep.name)) {
					const entry = {
						category,
						name: dep.name
					};
					const version = toString(dep.version);
					if (version) entry.version = version;
					const versionMin = toString(dep.versionMin);
					if (versionMin) entry.versionMin = versionMin;
					const versionMax = toString(dep.versionMax);
					if (versionMax) entry.versionMax = versionMax;
					if (typeof dep.optional === "boolean") entry.optional = dep.optional;
					dependencies.push(entry);
				}
			}
		}
	}
	let availableLanguages = [];
	let localisationReady;
	if (isPlainObject(data.localisation)) {
		const loc = data.localisation;
		availableLanguages = toStringArray(loc.availableLanguages);
		if (typeof loc.localisationReady === "boolean") localisationReady = loc.localisationReady;
	}
	let license;
	let mainCopyrightOwner;
	let repoOwner;
	if (isPlainObject(data.legal)) {
		const { legal } = data;
		if (isNonEmptyString(legal.license)) license = legal.license;
		if (isNonEmptyString(legal.mainCopyrightOwner)) mainCopyrightOwner = legal.mainCopyrightOwner;
		if (isNonEmptyString(legal.repoOwner)) repoOwner = legal.repoOwner;
	}
	const version = toString(data.softwareVersion);
	const releaseDate = toString(data.releaseDate);
	return publiccodeSchema.parse({
		...isNonEmptyString(data.applicationSuite) ? { applicationSuite: data.applicationSuite } : {},
		availableLanguages,
		categories: toStringArray(data.categories),
		contacts,
		contractors,
		dependencies,
		...description ? { description } : {},
		descriptions,
		...isNonEmptyString(data.developmentStatus) ? { developmentStatus: data.developmentStatus } : {},
		inputTypes: toStringArray(data.inputTypes),
		...isNonEmptyString(data.isBasedOn) ? { isBasedOn: data.isBasedOn } : {},
		...isNonEmptyString(data.landingURL) ? { landingUrl: data.landingURL } : {},
		...license ? { license } : {},
		...localisationReady === void 0 ? {} : { localisationReady },
		...isNonEmptyString(data.logo) ? { logo: data.logo } : {},
		...mainCopyrightOwner ? { mainCopyrightOwner } : {},
		...isPlainObject(data.maintenance) && isNonEmptyString(data.maintenance.type) ? { maintenanceType: data.maintenance.type } : {},
		...isNonEmptyString(data.monochromeLogo) ? { monochromeLogo: data.monochromeLogo } : {},
		...isNonEmptyString(data.name) ? { name: data.name } : {},
		outputTypes: toStringArray(data.outputTypes),
		platforms: toStringArray(data.platforms),
		...isNonEmptyString(data.publiccodeYmlVersion) ? { publiccodeYmlVersion: data.publiccodeYmlVersion } : toString(data.publiccodeYmlVersion) ? { publiccodeYmlVersion: toString(data.publiccodeYmlVersion) } : {},
		...releaseDate ? { releaseDate } : {},
		...repoOwner ? { repoOwner } : {},
		...isNonEmptyString(data.roadmap) ? { roadmap: data.roadmap } : {},
		...isNonEmptyString(data.softwareType) ? { softwareType: data.softwareType } : {},
		...version ? { softwareVersion: version } : {},
		...isNonEmptyString(data.url) ? { url: data.url } : {},
		usedBy: toStringArray(data.usedBy)
	});
}
const publiccodeYamlSource = defineSource({
	async discover(context) {
		return getMatches(context.options, ["publiccode.yml", "publiccode.yaml"]);
	},
	key: "publiccodeYaml",
	async parse(input, context) {
		const data = parse$1(await readFile(resolve(context.options.path, input), "utf8"));
		if (data !== void 0) return {
			data,
			source: input
		};
	},
	phase: 1
});
//#endregion
export { publiccodeYamlSource };
