import { log } from "../log.js";
import { getMatches } from "../file-matching.js";
import { defineSource } from "../source.js";
import { nonEmptyString, optionalUrl, parseJsonRecord } from "../utilities/schema-primitives.js";
import { splitCommaSeparated } from "../utilities/template-helpers.js";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import is from "@sindresorhus/is";
import { z } from "zod";
//#region src/lib/sources/codemeta-json.ts
/**
* CodeMeta JSON metadata source.
*
* Reads a codemeta.json file (v1, v2, or v3) and normalizes it into a
* consistent shape with predictable types. No enrichment, no JSON-LD
* expansion — just an honest representation of what's in the file.
*
* Uses Zod preprocess schemas to handle v1/v2/v3 normalization:
*   - `@`-prefixed JSON-LD keys stripped, v1 property names remapped
*   - `{"@type": "xsd:anyURI", "@value": "..."}` unwrapped to plain strings
*   - Person/org objects normalized (`@type`→`type`, `@id`→`id`, affiliation-as-object)
*   - Comma-separated strings split to arrays, `{"name":"..."}` objects unwrapped
*   - Dependencies normalized (string → `{name: string}`)
*/
/**
* A string that also unwraps `{"@value": "..."}` JSON-LD typed values.
* Empty/whitespace strings become undefined via `nonEmptyString`.
*/
const codeMetaString = z.preprocess((value) => {
	if (typeof value === "string") return value;
	if (is.plainObject(value) && typeof value["@value"] === "string") return value["@value"];
}, nonEmptyString);
/** Same as codeMetaString but semantically a URL field. */
const codeMetaUrl = z.preprocess((value) => {
	if (typeof value === "string") return value;
	if (is.plainObject(value) && typeof value["@value"] === "string") return value["@value"];
}, optionalUrl);
/**
* A string array that handles CodeMeta's polymorphic inputs:
*   - Single comma-separated string → split to array (common in v1)
*   - Array of strings → pass through
*   - Array of `{"name":"..."}` objects → extract name strings (e.g. programmingLanguage)
*/
const codeMetaStringArray = z.preprocess((value) => {
	if (value === void 0 || value === null) return;
	if (typeof value === "string") return value.includes(",") ? splitCommaSeparated(value) : [value];
	if (Array.isArray(value)) return value.map((item) => {
		if (typeof item === "string") return item.trim();
		if (is.plainObject(item)) return typeof item.name === "string" ? item.name.trim() : "";
		log.warn("Invalid type found in codemeta json parser");
		return "";
	}).filter((s) => s.length > 0);
}, z.array(z.string()).optional()).optional();
/**
* License field — preserve as string or string[].
*/
const codeMetaLicense = z.preprocess((value) => {
	if (typeof value === "string") return value;
	if (Array.isArray(value)) {
		const filtered = value.filter((l) => typeof l === "string");
		return filtered.length > 0 ? filtered : void 0;
	}
}, z.union([z.string(), z.array(z.string())]).optional()).optional();
const codeMetaPersonOrOrgSchema = z.object({
	affiliation: nonEmptyString,
	email: nonEmptyString,
	familyName: nonEmptyString,
	givenName: nonEmptyString,
	id: nonEmptyString,
	name: nonEmptyString,
	type: z.enum(["Organization", "Person"]).optional(),
	url: optionalUrl
});
/**
* Preprocess a raw person/org value into a normalized shape.
* Handles: plain string → {name: string}, `@type`→`type`, `@id`→`id`,
* affiliation-as-object → affiliation string.
*/
function preprocessPersonOrOrg(value) {
	if (typeof value === "string") return { name: value };
	if (!is.plainObject(value)) return;
	const result = {};
	if (typeof value["@type"] === "string") {
		const rawType = value["@type"].toLowerCase();
		if (rawType === "person") result.type = "Person";
		else if (rawType === "organization") result.type = "Organization";
	}
	if (typeof value["@id"] === "string") result.id = value["@id"];
	if (typeof value.name === "string") result.name = value.name;
	if (typeof value.givenName === "string") result.givenName = value.givenName;
	if (typeof value.familyName === "string") result.familyName = value.familyName;
	if (typeof value.email === "string") result.email = value.email;
	if (typeof value.url === "string") result.url = value.url;
	if (typeof value.affiliation === "string") result.affiliation = value.affiliation;
	else if (is.plainObject(value.affiliation) && typeof value.affiliation.name === "string") result.affiliation = value.affiliation.name;
	if (result.name ?? result.givenName ?? result.familyName ?? result.email) return result;
}
/**
* A person/org array that normalizes single values to arrays,
* and each element through `preprocessPersonOrOrg`.
*/
const codeMetaPersonArray = z.preprocess((value) => {
	if (value === void 0 || value === null) return;
	const normalized = (Array.isArray(value) ? value : [value]).map((item) => preprocessPersonOrOrg(item)).filter((p) => p !== void 0);
	return normalized.length > 0 ? normalized : void 0;
}, z.array(codeMetaPersonOrOrgSchema).optional()).optional();
const codeMetaDependencySchema = z.object({
	identifier: nonEmptyString,
	name: nonEmptyString,
	runtimePlatform: nonEmptyString,
	version: nonEmptyString
});
/**
* Preprocess a raw dependency value into a normalized shape.
* Handles: plain string → {name: string}.
*/
function preprocessDependency(value) {
	if (typeof value === "string") return { name: value };
	if (!is.plainObject(value)) return;
	const dep = {};
	if (typeof value.name === "string") dep.name = value.name;
	if (typeof value.identifier === "string") dep.identifier = value.identifier;
	if (typeof value.version === "string") dep.version = value.version;
	if (typeof value.runtimePlatform === "string") dep.runtimePlatform = value.runtimePlatform;
	if (dep.name ?? dep.identifier) return dep;
}
/**
* A dependency array that normalizes single values to arrays,
* and each element through `preprocessDependency`.
*/
const codeMetaDependencyArray = z.preprocess((value) => {
	if (value === void 0 || value === null) return;
	const normalized = (Array.isArray(value) ? value : [value]).map((item) => preprocessDependency(item)).filter((d) => d !== void 0);
	return normalized.length > 0 ? normalized : void 0;
}, z.array(codeMetaDependencySchema).optional()).optional();
const codeMetaJsonDataSchema = z.object({
	applicationCategory: codeMetaString,
	applicationSubCategory: codeMetaString,
	author: codeMetaPersonArray,
	buildInstructions: codeMetaUrl,
	codeRepository: codeMetaUrl,
	continuousIntegration: codeMetaUrl,
	contributor: codeMetaPersonArray,
	copyrightHolder: codeMetaPersonArray,
	copyrightYear: z.preprocess((v) => {
		if (typeof v === "number") return v;
		if (typeof v === "string") {
			const parsed = Number.parseInt(v, 10);
			return Number.isNaN(parsed) ? void 0 : parsed;
		}
	}, z.number().optional()),
	dateCreated: codeMetaString,
	dateModified: codeMetaString,
	datePublished: codeMetaString,
	description: codeMetaString,
	developmentStatus: codeMetaString,
	downloadUrl: codeMetaUrl,
	funder: codeMetaPersonArray,
	funding: codeMetaString,
	identifier: codeMetaString,
	installUrl: codeMetaUrl,
	isAccessibleForFree: z.boolean().optional(),
	issueTracker: codeMetaUrl,
	keywords: codeMetaStringArray,
	license: codeMetaLicense,
	maintainer: codeMetaPersonArray,
	name: codeMetaString,
	operatingSystem: codeMetaStringArray,
	programmingLanguage: codeMetaStringArray,
	readme: codeMetaUrl,
	relatedLink: codeMetaLicense,
	releaseNotes: codeMetaString,
	runtimePlatform: codeMetaStringArray,
	softwareHelp: codeMetaUrl,
	softwareRequirements: codeMetaDependencyArray,
	softwareSuggestions: codeMetaDependencyArray,
	softwareVersion: codeMetaString,
	url: codeMetaUrl,
	version: codeMetaString
});
/** Map v1 property names to their v2/v3 equivalents. */
const v1PropertyMap = {
	agents: "author",
	contIntegration: "continuousIntegration",
	depends: "softwareRequirements",
	suggests: "softwareSuggestions",
	title: "name"
};
/**
* Parse a codemeta.json file, normalizing v1/v2/v3 into a consistent shape.
*/
function parse(content) {
	const raw = parseJsonRecord(content);
	if (!raw) return void 0;
	const migrated = migrateV1Properties(raw);
	if (migrated.datePublished === void 0 && typeof migrated.dateReleased === "string") migrated.datePublished = migrated.dateReleased;
	return codeMetaJsonDataSchema.parse(migrated);
}
/**
* Strip `@`-prefixed JSON-LD keys and remap v1 property names.
*/
function migrateV1Properties(raw) {
	const result = {};
	for (const [key, value] of Object.entries(raw)) {
		if (key.startsWith("@")) continue;
		const mappedKey = v1PropertyMap[key] ?? key;
		if (!(mappedKey in result)) result[mappedKey] = value;
	}
	return result;
}
const codemetaJsonSource = defineSource({
	async discover(context) {
		return getMatches(context.options, ["codemeta.json"]);
	},
	key: "codemetaJson",
	async parse(input, context) {
		const data = parse(await readFile(resolve(context.options.path, input), "utf8"));
		if (data !== void 0) return {
			data,
			source: input
		};
	},
	phase: 1
});
//#endregion
export { codeMetaJsonDataSchema, codemetaJsonSource };
