//#region src/validate.ts
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const TEL_RE = /^[+()\-\s\d]{3,}$/;
/**
* Default per-field stored-string ceiling — the payload-size DoS floor when a field omits an
* explicit `max`. The module PERSISTS validated values, so an unbounded declared field is a
* storage-amplification vector the module can and does bound (defense-in-depth). The host still
* caps request-body bytes (parse-time); this caps what gets STORED. Generous (100k chars) so no
* realistic free-text submission is rejected; an adopter sets an explicit `max` to tighten it.
*/
const DEFAULT_MAX_TEXT = 1e5;
/** FormData→Record for the anti-spam honeypot check. Multi-value keys collapse to arrays. */
function toRecord(raw) {
	if (typeof FormData !== "undefined" && raw instanceof FormData) {
		const out = {};
		for (const key of new Set(raw.keys())) {
			const all = raw.getAll(key);
			out[key] = all.length > 1 ? all.map(String) : all[0];
		}
		return out;
	}
	return raw;
}
function readField(raw, name, multi) {
	if (typeof FormData !== "undefined" && raw instanceof FormData) {
		if (!raw.has(name)) return {
			present: false,
			value: void 0
		};
		if (multi) return {
			present: true,
			value: raw.getAll(name).map(String)
		};
		const v = raw.get(name);
		return {
			present: true,
			value: v === null ? void 0 : v
		};
	}
	const rec = raw;
	if (!Object.prototype.hasOwnProperty.call(rec, name)) return {
		present: false,
		value: void 0
	};
	return {
		present: true,
		value: rec[name]
	};
}
function checkPattern(f, s) {
	if (f.pattern === void 0) return null;
	let re;
	try {
		re = new RegExp(f.pattern);
	} catch {
		return null;
	}
	if (!re.test(s)) return {
		error: "pattern",
		message: `${f.name} does not match the required pattern`
	};
	return null;
}
function coerceField(f, value) {
	switch (f.type) {
		case "number": {
			const n = typeof value === "number" ? value : Number(String(value).trim());
			if (!Number.isFinite(n)) return {
				error: "type",
				message: `${f.name} must be a number`
			};
			if (f.min !== void 0 && n < f.min) return {
				error: "min",
				message: `${f.name} must be >= ${f.min}`
			};
			if (f.max !== void 0 && n > f.max) return {
				error: "max",
				message: `${f.name} must be <= ${f.max}`
			};
			return { value: n };
		}
		case "multiselect": {
			const arr = (Array.isArray(value) ? value : [value]).map(String);
			const allowed = new Set((f.options ?? []).map((o) => o.value));
			for (const v of arr) if (!allowed.has(v)) return {
				error: "option",
				message: `${f.name} contains a value that is not an allowed option`
			};
			if (f.min !== void 0 && arr.length < f.min) return {
				error: "min",
				message: `${f.name} requires at least ${f.min}`
			};
			const maxCount = f.max ?? f.options?.length ?? 0;
			if (arr.length > maxCount) return {
				error: "max",
				message: `${f.name} allows at most ${maxCount}`
			};
			return { value: arr };
		}
		case "select":
		case "radio": {
			const s = String(value);
			if (!new Set((f.options ?? []).map((o) => o.value)).has(s)) return {
				error: "option",
				message: `${f.name} is not an allowed option`
			};
			return { value: s };
		}
		case "email": {
			const s = String(value).trim();
			if (s.length > DEFAULT_MAX_TEXT) return {
				error: "max",
				message: `${f.name} is too long`
			};
			if (!EMAIL_RE.test(s)) return {
				error: "type",
				message: `${f.name} must be a valid email`
			};
			return checkPattern(f, s) ?? { value: s };
		}
		case "url": {
			const s = String(value).trim();
			if (s.length > DEFAULT_MAX_TEXT) return {
				error: "max",
				message: `${f.name} is too long`
			};
			try {
				new URL(s);
			} catch {
				return {
					error: "type",
					message: `${f.name} must be a valid URL`
				};
			}
			return checkPattern(f, s) ?? { value: s };
		}
		case "tel": {
			const s = String(value).trim();
			if (s.length > DEFAULT_MAX_TEXT) return {
				error: "max",
				message: `${f.name} is too long`
			};
			if (!TEL_RE.test(s)) return {
				error: "type",
				message: `${f.name} must be a valid phone number`
			};
			return checkPattern(f, s) ?? { value: s };
		}
		case "date": {
			const s = String(value).trim();
			if (s.length > DEFAULT_MAX_TEXT) return {
				error: "max",
				message: `${f.name} is too long`
			};
			if (Number.isNaN(Date.parse(s))) return {
				error: "type",
				message: `${f.name} must be a valid date`
			};
			return checkPattern(f, s) ?? { value: s };
		}
		default: {
			const s = String(value);
			const maxLen = f.max ?? DEFAULT_MAX_TEXT;
			if (f.min !== void 0 && s.length < f.min) return {
				error: "min",
				message: `${f.name} must be at least ${f.min} characters`
			};
			if (s.length > maxLen) return {
				error: "max",
				message: `${f.name} must be at most ${maxLen} characters`
			};
			return checkPattern(f, s) ?? { value: s };
		}
	}
}
/**
* HARD FLOOR. Validate + coerce an untrusted submission against a form.
* Drops undeclared keys (only declared fields appear in the result), enforces
* required/min/max/pattern/options, returns typed FieldError[] on failure.
*/
function validateSubmission(form, raw) {
	const errors = [];
	const clean = {};
	for (const f of form.fields) {
		if (f.type === "checkbox") {
			const { present, value } = readField(raw, f.name, false);
			const checked = present && value !== false && value !== "" && value !== "false" && value != null;
			if (f.required && !checked) {
				errors.push({
					field: f.name,
					code: "required",
					message: `${f.name} is required`
				});
				continue;
			}
			clean[f.name] = checked;
			continue;
		}
		const multi = f.type === "multiselect";
		const { present, value } = readField(raw, f.name, multi);
		if (!present || value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0) {
			if (f.required) errors.push({
				field: f.name,
				code: "required",
				message: `${f.name} is required`
			});
			else if (f.default !== void 0) clean[f.name] = f.default;
			continue;
		}
		const res = coerceField(f, value);
		if ("error" in res) errors.push({
			field: f.name,
			code: res.error,
			message: res.message
		});
		else clean[f.name] = res.value;
	}
	if (errors.length > 0) return {
		ok: false,
		error: errors
	};
	return {
		ok: true,
		value: clean
	};
}
//#endregion
export { validateSubmission as n, toRecord as t };
