// Conservative hard caps so a single field can't be used to balloon the row /
// exhaust memory even when the schema omits explicit length rules.
export const FORM_MAX_TEXT_LEN = 5_000;
export const FORM_MAX_TEXTAREA_LEN = 50_000;
export const FORM_MAX_MULTISELECT_ITEMS = 200;
/** The exact email format the server accepts for `email`-format fields. */
export const FORM_EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
/** The exact phone format the server accepts for `phone`-format fields. */
export const FORM_PHONE_RE = /^[+]?[0-9\s().-]{6,20}$/;
/** Field types that never carry a submitted value. */
export const FORM_NON_INPUT_TYPES = new Set([
    'richtext',
    'divider',
    'honeypot',
]);
const TRUTHY = new Set(['true', 'on', '1', 'yes', 'checked']);
export function asString(value) {
    if (value == null)
        return '';
    if (typeof value === 'string')
        return value;
    if (typeof value === 'number' || typeof value === 'boolean')
        return String(value);
    return '';
}
export function coerceBoolean(value) {
    if (typeof value === 'boolean')
        return value;
    if (typeof value === 'number')
        return value === 1;
    return TRUTHY.has(asString(value).trim().toLowerCase());
}
function evalRule(rule, values) {
    const current = asString(values[rule.fieldKey]).toLowerCase().trim();
    const target = asString(rule.value).toLowerCase().trim();
    switch (rule.operator) {
        case 'equals':
            return current === target;
        case 'notEquals':
            return current !== target;
        case 'contains':
            return current.includes(target);
        case 'isEmpty':
            return current === '';
        case 'isNotEmpty':
            return current !== '';
        default:
            return true;
    }
}
/** A field is visible unless its `show` rules exist and not all of them pass. */
export function isFieldVisible(field, values) {
    const show = field.conditionalLogic?.show;
    if (!show || show.length === 0)
        return true;
    return show.every((rule) => evalRule(rule, values));
}
/** A field is conditionally required when its `require` rules all pass. */
export function isFieldConditionallyRequired(field, values) {
    const require = field.conditionalLogic?.require;
    if (!require || require.length === 0)
        return false;
    return require.every((rule) => evalRule(rule, values));
}
export function isValuePresent(raw) {
    if (raw === undefined || raw === null)
        return false;
    if (typeof raw === 'string')
        return raw.trim() !== '';
    if (Array.isArray(raw))
        return raw.length > 0;
    return true;
}
export function isHttpUrl(value) {
    try {
        const url = new URL(value);
        return url.protocol === 'http:' || url.protocol === 'https:';
    }
    catch {
        return false;
    }
}
/**
 * Validate a single field's value with the same rules the server applies.
 *
 * Pass `allValues` (the full form state) so conditional visibility/required
 * logic evaluates correctly; a hidden field always validates as ok, matching
 * the server (hidden fields are skipped, not required, not stored).
 */
export function validateFormFieldClient(field, value, allValues = {}) {
    if (FORM_NON_INPUT_TYPES.has(field.type))
        return { ok: true };
    if (!isFieldVisible(field, allValues))
        return { ok: true };
    const label = field.label || field.key;
    const rules = field.validationRules ?? {};
    const required = Boolean(field.required) ||
        Boolean(rules.required) ||
        isFieldConditionallyRequired(field, allValues);
    const fail = (message) => ({ ok: false, message });
    if (field.type === 'file') {
        // The client knows file presence from its own input state; only the
        // required check is expressible here.
        if (required && !isValuePresent(value))
            return fail(`${label} is required.`);
        return { ok: true };
    }
    if (!isValuePresent(value)) {
        return required ? fail(`${label} is required.`) : { ok: true };
    }
    switch (field.type) {
        case 'checkbox':
        case 'consent': {
            const bool = coerceBoolean(value);
            if ((field.type === 'consent' || required) && !bool) {
                return fail(`${label} must be accepted.`);
            }
            return { ok: true };
        }
        case 'number': {
            const num = typeof value === 'number' ? value : Number(asString(value).trim());
            if (!Number.isFinite(num))
                return fail(`${label} must be a number.`);
            if (rules.min != null && num < rules.min) {
                return fail(`${label} must be at least ${rules.min}.`);
            }
            if (rules.max != null && num > rules.max) {
                return fail(`${label} must be at most ${rules.max}.`);
            }
            return { ok: true };
        }
        case 'multiselect': {
            const list = Array.isArray(value) ? value : [value];
            if (list.length > FORM_MAX_MULTISELECT_ITEMS) {
                return fail(`${label} has too many selections.`);
            }
            const cleaned = list.map((v) => asString(v).trim()).filter((v) => v !== '');
            const allowed = new Set((field.options ?? []).map((o) => o.value));
            if (allowed.size > 0 && cleaned.some((v) => !allowed.has(v))) {
                return fail(`${label} contains an invalid selection.`);
            }
            return { ok: true };
        }
        case 'select':
        case 'radio': {
            const val = asString(value).trim();
            const allowed = new Set((field.options ?? []).map((o) => o.value));
            if (allowed.size > 0 && !allowed.has(val)) {
                return fail(`${label} contains an invalid selection.`);
            }
            return { ok: true };
        }
        case 'date': {
            const d = new Date(asString(value).trim());
            if (Number.isNaN(d.getTime()))
                return fail(`${label} is not a valid date.`);
            return { ok: true };
        }
        default: {
            // text, email, phone, url, textarea, hidden
            const s = asString(value).trim();
            const maxLen = rules.maxLength ?? (field.type === 'textarea' ? FORM_MAX_TEXTAREA_LEN : FORM_MAX_TEXT_LEN);
            if (s.length > maxLen) {
                return fail(`${label} must be at most ${maxLen} characters.`);
            }
            if (rules.minLength != null && s.length < rules.minLength) {
                return fail(`${label} must be at least ${rules.minLength} characters.`);
            }
            const format = rules.format ??
                (field.type === 'email'
                    ? 'email'
                    : field.type === 'url'
                        ? 'url'
                        : field.type === 'phone'
                            ? 'phone'
                            : undefined);
            if (format === 'email' && !FORM_EMAIL_RE.test(s)) {
                return fail(`${label} must be a valid email address.`);
            }
            if (format === 'phone' && !FORM_PHONE_RE.test(s)) {
                return fail(`${label} must be a valid phone number.`);
            }
            if (format === 'url' && !isHttpUrl(s)) {
                return fail(`${label} must be a valid URL.`);
            }
            if (rules.pattern) {
                try {
                    if (!new RegExp(rules.pattern).test(s)) {
                        return fail(`${label} is not in the expected format.`);
                    }
                }
                catch {
                    // An invalid pattern in the schema must never block input; schema
                    // validation already surfaces bad patterns to the editor.
                }
            }
            return { ok: true };
        }
    }
}
/**
 * Validate the whole form client-side. Returns the first error per field,
 * keyed by field key. An empty object means the form would pass the server's
 * field validation (spam/honeypot/file checks still run server-side).
 */
export function validateFormValuesClient(fields, values) {
    const errors = {};
    for (const field of fields) {
        const result = validateFormFieldClient(field, values[field.key], values);
        if (!result.ok && result.message)
            errors[field.key] = result.message;
    }
    return errors;
}
//# sourceMappingURL=client-validation.js.map