/**
 * Pure helpers behind `<ComponentBlockField>`. Extracted so vitest can
 * exercise them in node without spinning up jsdom — the React component
 * just composes these into UI.
 *
 * Kept structurally typed (no peer-dep import of types here would be
 * cleaner, but the `PropType` discriminated union is the contract we
 * promise consumers, so we lean on the package's types).
 */
import { detectDiscriminator, findVariant, } from '@actuate-media/component-blocks/discriminated-union';
/**
 * Compute the list of components an editor can pick from given the
 * manifest + the optional allow-list. Returns the full list when no
 * filter is provided; returns `[]` when every entry is filtered out
 * (the caller renders an error in that case).
 */
export function getAllowedComponents(manifest, allow) {
    if (!allow || allow.length === 0)
        return manifest.components;
    return manifest.components.filter((c) => allow.includes(c.name));
}
/**
 * JSON-encode any value so it round-trips cleanly through the
 * JsonFallback textarea. Previously the helper short-circuited for
 * strings (returning the raw string), which made the textarea
 * stateful: typing `"hello"` parsed to the JS string `hello`,
 * re-rendered as bare `hello`, then `JSON.parse('hello')` threw on
 * the next edit and the field "locked" as a raw string. Always
 * stringifying keeps the textarea visually consistent with the
 * underlying JSON value.
 *
 * Falls back to `String(v)` for values that can't be JSON-encoded
 * (e.g. circular refs) so the field remains editable.
 */
export function safeJsonStringify(v) {
    try {
        return JSON.stringify(v, null, 2);
    }
    catch {
        return String(v);
    }
}
/**
 * Map an `<select>` change-event value to an enum-prop value.
 *
 * The placeholder `<option value="">Select…</option>` clears the field
 * to `undefined`; any other value is the stringified array index into
 * `options`. Extracted so we can test the clear semantics without
 * spinning up jsdom: previously the inline handler used `Number("")`,
 * which evaluates to `0` and silently picked the first option instead
 * of clearing.
 */
export function parseEnumSelection(raw, options) {
    if (raw === '')
        return undefined;
    const idx = Number(raw);
    if (!Number.isInteger(idx) || idx < 0 || idx >= options.length)
        return undefined;
    return options[idx];
}
/**
 * Produce a sensible empty value for a freshly-created array item or
 * required prop. The admin form generator never wants `undefined` in
 * an array; that breaks downstream PropInput defaults and confuses
 * validators.
 */
export function defaultForType(type) {
    switch (type.kind) {
        case 'string':
            return '';
        case 'number':
            return 0;
        case 'boolean':
            return false;
        case 'enum':
            return type.values[0] ?? '';
        case 'literal':
            return type.value;
        case 'array':
            return [];
        case 'object':
            return Object.fromEntries(type.fields.filter((f) => f.required).map((f) => [f.name, defaultForType(f.type)]));
        case 'union': {
            // Discriminated unions DO have a sensible structural default:
            // pick the first variant and seed its required fields (plus the
            // discriminator). This makes a Hero with a `cta: Link | Modal`
            // prop render an editable form on first paint instead of forcing
            // the editor through the JSON textarea fallback.
            const detected = detectDiscriminator(type);
            if (detected) {
                const variant = detected.variants[0];
                const out = { [detected.field]: variant.value };
                for (const field of variant.remainingFields) {
                    if (field.required)
                        out[field.name] = defaultForType(field.type);
                }
                return out;
            }
            return null;
        }
        default:
            // `reference`, `unknown`: there's no single sensible structural
            // default. We return `null` instead of `undefined` so the value
            // survives a JSON round-trip — `JSON.stringify` turns
            // `undefined` array items into `null` anyway, which would
            // contradict the documented "never undefined in an array"
            // invariant and surface as the literal string `null` in the
            // JsonFallback textarea.
            return null;
    }
}
/**
 * Seed an empty props object for a freshly-picked component using each
 * prop's explicit `defaultValue` when set, else the structural default
 * from {@link defaultForType} (only for required props — optional
 * unset props stay absent so they render as empty inputs).
 */
export function seedPropsForComponent(spec) {
    const out = {};
    for (const prop of spec.props) {
        if (prop.defaultValue !== undefined && prop.defaultValue !== null) {
            out[prop.name] = prop.defaultValue;
            continue;
        }
        if (prop.required) {
            const seed = defaultForType(prop.type);
            // For non-discriminated `union` / `reference` / `unknown` kinds
            // defaultForType returns `null` (so arrays survive a JSON
            // round-trip), but seeding a top-level required prop with
            // `null` would immediately trip the validator's "Missing
            // required prop" branch on first render — the user would see
            // a red error before they touched anything. Leave those
            // required props absent so the absence and the validation
            // message describe the same state honestly; the user provides
            // a value via the JsonFallback. Discriminated unions return
            // a structural default and seed cleanly.
            if (seed !== null) {
                out[prop.name] = seed;
            }
        }
    }
    return out;
}
/**
 * Compute the next value for a discriminated-union field when the
 * editor switches variants. Pure helper so the React component stays
 * thin and the seeding rules can be unit-tested without rendering.
 *
 * Seeding rules:
 *   1. The discriminator field is always written to the variant's
 *      tag value.
 *   2. Optional fields shared by both variants survive the switch
 *      (preserves "edited the same label" intent across, e.g.,
 *      Link → Modal where both carry `label`).
 *   3. Required fields exclusive to the new variant are seeded by
 *      priority: explicit `field.defaultValue` first (matches
 *      `seedPropsForComponent`), else the structural default from
 *      `defaultForType` — but only when that default is non-null.
 *      A `null` from `defaultForType` (reference / unknown / non-
 *      discriminated union types) is treated as "no sensible default"
 *      and the field is left absent so the validator's
 *      "missing required prop" message and the data agree.
 *   4. Fields exclusive to the previous variant are dropped so the
 *      value stays structurally valid against the chosen variant.
 */
export function switchUnionVariant(current, union, variant) {
    const obj = isPlainObject(current) ? current : {};
    const next = { [union.field]: variant.value };
    const carryable = new Set(variant.remainingFields.map((f) => f.name));
    for (const [k, v] of Object.entries(obj)) {
        if (k === union.field)
            continue;
        if (carryable.has(k))
            next[k] = v;
    }
    for (const field of variant.remainingFields) {
        if (!field.required || next[field.name] !== undefined)
            continue;
        if (field.defaultValue !== undefined && field.defaultValue !== null) {
            next[field.name] = field.defaultValue;
            continue;
        }
        const seed = defaultForType(field.type);
        if (seed !== null) {
            next[field.name] = seed;
        }
    }
    return next;
}
function isPlainObject(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
 * Build a minimal structural validator that mirrors the rules the
 * cms-core `validateComponentBlockValue` runs server-side, so the form
 * can show feedback without a round-trip. Kept in sync with cms-core
 * by inspection; the canonical implementation lives there.
 */
export function buildClientValidator(manifest, allow) {
    // An empty `allow` array is treated the same way `getAllowedComponents`
    // treats it: as "no filter". Without this, the picker shows every
    // component while the validator rejects every selection, leading to
    // a UI/validator deadlock.
    const allowed = allow && allow.length > 0 ? new Set(allow) : null;
    return (value) => {
        const spec = manifest.components.find((c) => c.name === value.component);
        if (!spec) {
            const known = manifest.components.map((c) => c.name).join(', ') || '<none>';
            return `Unknown component '${value.component}'. Manifest knows: ${known}.`;
        }
        if (allowed && !allowed.has(value.component)) {
            return `Component '${value.component}' is not in the allow-list for this field.`;
        }
        for (const propSpec of spec.props) {
            const v = value.props[propSpec.name];
            if (v === undefined || v === null) {
                if (propSpec.required) {
                    return `Missing required prop '${propSpec.name}' for component '${value.component}'.`;
                }
                continue;
            }
            const err = clientShapeError(v, propSpec);
            if (err)
                return err;
        }
        return true;
    };
}
function clientShapeError(value, prop) {
    const t = prop.type;
    switch (t.kind) {
        case 'string':
            return typeof value === 'string' ? null : `Prop '${prop.name}' must be a string.`;
        case 'number':
            return typeof value === 'number' && !Number.isNaN(value)
                ? null
                : `Prop '${prop.name}' must be a number.`;
        case 'boolean':
            return typeof value === 'boolean' ? null : `Prop '${prop.name}' must be a boolean.`;
        case 'enum':
            return t.values.includes(value)
                ? null
                : `Prop '${prop.name}' must be one of: ${t.values.join(', ')}.`;
        case 'array':
            return Array.isArray(value) ? null : `Prop '${prop.name}' must be an array.`;
        case 'object': {
            if (value === null || typeof value !== 'object' || Array.isArray(value)) {
                return `Prop '${prop.name}' must be an object.`;
            }
            // Recurse: check that each required nested field is present and
            // structurally compatible. Without this, passing `{}` for an
            // object prop with required `label` and `href` children sailed
            // through the client validator while the server-side
            // validateComponentBlockValue would reject it on save — a
            // silent gap in the "live structural validation" promise.
            const obj = value;
            for (const field of t.fields) {
                const child = obj[field.name];
                if (child === undefined || child === null) {
                    if (field.required) {
                        return `Prop '${prop.name}.${field.name}' is required but missing.`;
                    }
                    continue;
                }
                const childError = clientShapeError(child, {
                    ...field,
                    // Build a dotted path so parsePerPropErrors can attribute
                    // the error to the nested input, not just the outer prop.
                    name: `${prop.name}.${field.name}`,
                });
                if (childError)
                    return childError;
            }
            return null;
        }
        case 'union': {
            // Discriminated unions get the same recursive treatment as plain
            // objects: route the value into the matching variant's schema
            // and validate against that. Non-discriminated unions remain
            // unvalidated on the client (server still catches them) — the
            // shape is too open to assert structurally.
            const detected = detectDiscriminator(t);
            if (!detected)
                return null;
            if (value === null || typeof value !== 'object' || Array.isArray(value)) {
                return `Prop '${prop.name}' must be an object with a '${detected.field}' discriminator.`;
            }
            const variant = findVariant(value, detected);
            if (!variant) {
                const allowed = detected.variants.map((v) => JSON.stringify(v.value)).join(', ');
                return `Prop '${prop.name}.${detected.field}' must be one of: ${allowed}.`;
            }
            // Recurse into the variant's fields, scoped to the prop path.
            // We treat the variant as an inline object so the existing
            // object-branch logic applies verbatim.
            return clientShapeError(value, {
                name: prop.name,
                required: prop.required,
                type: variant.type,
            });
        }
        default:
            return null;
    }
}
/**
 * The validator returns a single error string. The form wants to show
 * the message under the specific input that triggered it — so we parse
 * the message back into a sparse `{ propPath → message }` map. Pure
 * function so it's trivial to unit-test.
 */
export function parsePerPropErrors(message) {
    if (!message)
        return {};
    const propMatch = /Prop '([^']+)'/.exec(message);
    if (propMatch)
        return { [propMatch[1]]: message };
    const missing = /Missing required prop '([^']+)'/.exec(message);
    if (missing)
        return { [missing[1]]: message };
    return {};
}
//# sourceMappingURL=component-block-helpers.js.map