// ─── Helpers ──────────────────────────────────────────────────────────────
function relationTargets(field) {
    if (field.type !== 'relationship')
        return [];
    return Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo];
}
function extractIds(value) {
    if (value === null || value === undefined || value === '')
        return [];
    if (Array.isArray(value)) {
        return value.filter((v) => typeof v === 'string' && v.length > 0);
    }
    if (typeof value === 'string' && value.length > 0)
        return [value];
    return [];
}
/**
 * Walk a field schema and collect every relation reference present in the
 * document data. Returns `{ collection -> Set<id> }` and an inventory of
 * which field paths hold which ids — used by both validation and populate.
 */
export function collectRelationRefs(fields, data) {
    const byCollection = new Map();
    const refsByPath = [];
    function visit(fieldDefs, obj, prefix) {
        if (!obj || typeof obj !== 'object')
            return;
        const objRec = obj;
        for (const [name, def] of Object.entries(fieldDefs)) {
            const value = objRec[name];
            const path = prefix ? `${prefix}.${name}` : name;
            if (def.type === 'relationship') {
                const ids = extractIds(value);
                if (ids.length === 0)
                    continue;
                refsByPath.push({ path, field: def, ids });
                for (const target of relationTargets(def)) {
                    if (!byCollection.has(target))
                        byCollection.set(target, new Set());
                    for (const id of ids)
                        byCollection.get(target).add(id);
                }
            }
            else if (def.type === 'array' && Array.isArray(value)) {
                for (let i = 0; i < value.length; i++) {
                    visit(def.fields, value[i], `${path}[${i}]`);
                }
            }
        }
    }
    visit(fields, data, '');
    return { byCollection, refsByPath };
}
/**
 * Validate that every relation reference in `data` points at a live
 * document. Throws on the first broken reference with a precise error
 * (path + missing id + target collection) so admins can fix the input
 * instead of guessing.
 */
export async function validateRelations(fields, data, lookup) {
    const { byCollection, refsByPath } = collectRelationRefs(fields, data);
    if (refsByPath.length === 0)
        return;
    const found = await batchedFetch(lookup, byCollection);
    for (const ref of refsByPath) {
        const targets = relationTargets(ref.field);
        for (const id of ref.ids) {
            let resolved = false;
            for (const target of targets) {
                const docMap = found.get(target);
                if (docMap?.has(id)) {
                    resolved = true;
                    break;
                }
            }
            if (!resolved) {
                const target = targets.length === 1 ? targets[0] : targets.join(' | ');
                throw new Error(`Relation field "${ref.path}" references document "${id}" which does not exist in collection "${target}".`);
            }
        }
    }
}
/**
 * Issue **one** `fetchByIds` call across every target collection and bucket
 * the result back into a `collection -> id -> summary` map.
 *
 * The naive shape — one `await` per collection — costs N database round-trips
 * when a document carries relations into N collections (an author + media +
 * related posts trio is three). Each round-trip is ~80 ms over a typical
 * cross-region Function -> Postgres path, so the cost adds up fast on
 * cache-miss reads. Batching is safe because `RelationLookup.fetchByIds`
 * already accepts a multi-collection input (`collection: { in: collections }`
 * on the Prisma side) and every returned summary carries its source
 * `collection` field, so we can bucket without ambiguity.
 *
 * IDs are deduplicated across collections so we never ship the same id twice
 * in the `IN (...)` list. Targets are deduplicated for the same reason.
 */
async function batchedFetch(lookup, byCollection) {
    const found = new Map();
    if (byCollection.size === 0)
        return found;
    const collections = [];
    const ids = new Set();
    for (const [collection, idSet] of byCollection.entries()) {
        collections.push(collection);
        found.set(collection, new Map());
        for (const id of idSet)
            ids.add(id);
    }
    if (ids.size === 0)
        return found;
    const byId = await lookup.fetchByIds(collections, Array.from(ids));
    for (const [id, summary] of byId) {
        // Defensive: only bucket into collections we actually asked for. A
        // lookup that returns an out-of-band collection (custom implementation
        // bug) shouldn't poison the result map.
        const bucket = found.get(summary.collection);
        if (bucket)
            bucket.set(id, summary);
    }
    return found;
}
function shouldPopulatePath(path, paths) {
    // Opt-in: with no paths specified, nothing gets populated. Callers must
    // explicitly pass '*' to expand everything or list paths by name.
    if (!paths)
        return false;
    if (paths === '*' || (Array.isArray(paths) && paths.includes('*')))
        return true;
    if (!Array.isArray(paths))
        return false;
    // Top-level only — strip array indexers.
    const top = path.replace(/\[\d+\].*$/, '').split('.')[0];
    return paths.includes(top);
}
/**
 * Replace stored IDs with `{ id, title, slug, status, collection }` summaries
 * for every relation field in the document. The original IDs remain
 * accessible via the summary's `id` field, so callers that wrote raw IDs
 * still see them after a populated read.
 *
 * Returns a *new* document object; the input is not mutated.
 */
export async function populateRelations(fields, data, lookup, options = {}) {
    const { byCollection, refsByPath } = collectRelationRefs(fields, data);
    const eligibleRefs = refsByPath.filter((r) => shouldPopulatePath(r.path, options.paths));
    if (eligibleRefs.length === 0)
        return data;
    // Limit the batch to ids we'll actually use.
    const trimmedByCollection = new Map();
    for (const ref of eligibleRefs) {
        for (const target of relationTargets(ref.field)) {
            const all = byCollection.get(target);
            if (!all)
                continue;
            for (const id of ref.ids) {
                if (all.has(id)) {
                    if (!trimmedByCollection.has(target))
                        trimmedByCollection.set(target, new Set());
                    trimmedByCollection.get(target).add(id);
                }
            }
        }
    }
    const found = await batchedFetch(lookup, trimmedByCollection);
    // Deep-clone the data so we don't mutate the caller's object.
    const cloned = JSON.parse(JSON.stringify(data));
    function lookupSummary(field, id) {
        for (const target of relationTargets(field)) {
            const summary = found.get(target)?.get(id);
            if (summary)
                return summary;
        }
        // Reference not found — return the raw id rather than dropping it, so
        // the frontend at least sees that a relation was present.
        return id;
    }
    function replace(fieldDefs, obj, prefix) {
        for (const [name, def] of Object.entries(fieldDefs)) {
            const value = obj[name];
            const path = prefix ? `${prefix}.${name}` : name;
            if (def.type === 'relationship' && shouldPopulatePath(path, options.paths)) {
                if (def.hasMany || Array.isArray(value)) {
                    const ids = extractIds(value);
                    if (ids.length === 0)
                        continue;
                    obj[name] = ids.map((id) => lookupSummary(def, id));
                }
                else if (typeof value === 'string' && value.length > 0) {
                    obj[name] = lookupSummary(def, value);
                }
            }
            else if (def.type === 'array' && Array.isArray(value)) {
                for (let i = 0; i < value.length; i++) {
                    const item = value[i];
                    if (item && typeof item === 'object') {
                        replace(def.fields, item, `${path}[${i}]`);
                    }
                }
            }
        }
    }
    replace(fields, cloned, '');
    return cloned;
}
/**
 * Parse the `?populate=` query param into an options object.
 *   - `?populate=*`          → expand all relations
 *   - `?populate=true`       → expand all relations (alias)
 *   - `?populate=a,b`        → expand only `a` and `b`
 *   - missing / `false`      → no expansion
 */
export function parsePopulateParam(raw) {
    if (!raw)
        return null;
    if (raw === 'true' || raw === '*')
        return { paths: '*' };
    if (raw === 'false' || raw === '0')
        return null;
    const parts = raw
        .split(',')
        .map((s) => s.trim())
        .filter(Boolean);
    if (parts.length === 0)
        return null;
    if (parts.includes('*'))
        return { paths: '*' };
    return { paths: parts };
}
//# sourceMappingURL=relations.js.map