/**
 * Pre-realize spec normalization — attribute/relationship name collisions.
 *
 * Why this lives in realize (not inference)
 * ----------------------------------------
 *
 * The analyse pipeline can produce specs where a model declares an
 * attribute and a relationship sharing the same name (e.g. SaveGame
 * has both `attributes.gameState: Json` and `relationships.gameState:
 * belongsTo GameState`). That's an ambiguity the AST happily carries
 * — `attributes.gameState` and `relationships.gameState` are two
 * distinct slots in the schema — but every downstream code emitter
 * needs to project them onto a single namespace (one Prisma column
 * + one Prisma relation field; one TypeScript class member +
 * one ORM relation accessor) and the projection collides.
 *
 * Pre-engines 6.38.4 this was guarded inside `prisma/schema-generator.ts`
 * which threw on the first collision and aborted the model loop — one
 * bad model in a 24-model spec left zero schema on disk. 6.38.4 moved
 * the guard to a non-throwing auto-rename: keep the relationship's
 * canonical name and rename the colliding attribute to `<name>Data`.
 * But the rename lived inside the prisma generator, which meant:
 *   - Mongo / pg / non-Prisma specs never got the rename. Mongo
 *     tolerates parallel fields more leniently than Prisma, but the
 *     ambiguity still propagates to controllers / services /
 *     validators that read `model.attributes` and would still
 *     mention the colliding name twice.
 *   - Service / controller / route generators that read
 *     `model.attributes` saw the rename ONLY when the ORM step ran
 *     first AND mutated the shared `allModels` reference. Skip the
 *     ORM (e.g. mongo manifests) → drift.
 *
 * This module lifts the rename to a single pre-emission pass that
 * runs regardless of which ORM (if any) is in the manifest. The
 * `realizeAll` entrypoint calls `normalizeAttributeRelationshipCollisions`
 * once at the top, mutating `spec.models` in place. All subsequent
 * generators (prisma, mongo, pg, controllers, services, routes,
 * views) see the canonical renamed shape.
 *
 * Backward compatibility
 * ---------------------
 *
 * - `prisma/schema-generator.ts` still calls
 *   `resolveAttributeRelationshipCollisions(model)` defensively,
 *   but the first pass at realize-top has already mutated the
 *   model, so the second pass is a no-op (no collisions remain).
 *   We keep the call there to protect direct invocations of the
 *   schema generator that bypass realize (e.g. test fixtures).
 */
import { pluralize } from '@specverse/types';
/**
 * Walk every model in the spec and resolve attribute/relationship
 * name collisions. Returns the list of renames performed so callers
 * (realize/index.ts) can surface them in a single tidy log line
 * instead of one console.warn per model.
 *
 * Mutates spec.models in place — downstream generators read the
 * renamed shape. Idempotent: a second pass produces zero renames.
 */
export function normalizeAttributeRelationshipCollisions(spec) {
    const renames = [];
    if (!spec)
        return renames;
    // Accept either array-shape or keyed-object-shape `models:`. Both
    // are valid across the inference pipeline depending on which
    // stage handed us the spec.
    const modelsAny = spec.models;
    const models = Array.isArray(modelsAny)
        ? modelsAny
        : (modelsAny && typeof modelsAny === 'object'
            ? Object.values(modelsAny)
            : []);
    for (const model of models) {
        const r = resolveOneModel(model);
        for (const rec of r)
            renames.push(rec);
        // #94 defect 2 — drop a scalar FK attribute that a belongsTo already covers.
        dropRedundantForeignKeyAttributes(model);
    }
    return renames;
}
const normFk = (s) => String(s ?? '').toLowerCase().replace(/_/g, '');
/**
 * #94 (defect 2) — drop redundant scalar foreign-key attributes.
 *
 * When `analyse` reverse-engineers an adapter-less app it can extract a raw FK
 * column (`house_id`) as a model *attribute* AND infer the `belongsTo House`
 * relationship from it. Realize then emits TWO FK fields — the scalar
 * `house_id` (from the attribute) and `houseId` (from the relationship's
 * `@relation(fields: [houseId])`) — and the generated controller becomes
 * internally inconsistent (`validate()` requires `house_id` while the FK-connect
 * uses `houseId`), so Room/Booking creation fails at runtime.
 *
 * A `belongsTo`/`hasOne` relationship `relName: belongsTo Target` generates the
 * FK field `rel.foreignKey || (rel.name || target.toLowerCase()) + 'Id'` (see
 * `orms/templates/prisma/schema-generator.ts`). Any scalar attribute whose
 * name normalises to that FK (`house_id`, `houseId`, `houseID`) is therefore
 * redundant — the relationship is the canonical source of the FK — so we drop
 * the attribute. The primary `id` is never dropped. Idempotent.
 *
 * Mutates `model.attributes` in place; returns the drops for logging/tests.
 */
export function dropRedundantForeignKeyAttributes(model) {
    const out = [];
    if (!model)
        return out;
    const wasArray = Array.isArray(model.attributes);
    const attributes = wasArray
        ? model.attributes
        : Object.values(model.attributes || {});
    const relationships = Array.isArray(model.relationships)
        ? model.relationships
        : Object.values(model.relationships || {});
    // Map norm(FK field) -> relationship, for the relationship types that hold a FK.
    const fkByNorm = new Map();
    for (const rel of relationships) {
        if (rel?.type !== 'belongsTo' && rel?.type !== 'hasOne')
            continue;
        const base = rel?.name || String(rel?.target ?? '').toLowerCase();
        if (!base)
            continue;
        const fkField = rel?.foreignKey || `${base}Id`;
        fkByNorm.set(normFk(fkField), rel);
    }
    if (fkByNorm.size === 0)
        return out;
    const toDrop = new Set();
    for (const attr of attributes) {
        const an = attr?.name;
        if (!an || normFk(an) === 'id')
            continue; // never drop the primary key
        const rel = fkByNorm.get(normFk(an));
        if (!rel)
            continue;
        const fkField = rel?.foreignKey || `${rel?.name || String(rel?.target ?? '').toLowerCase()}Id`;
        toDrop.add(an);
        out.push({
            modelName: String(model.name ?? '<anonymous>'),
            droppedAttribute: an,
            relationshipName: String(rel?.name || rel?.target || 'unknown'),
            relationshipTarget: String(rel?.target || 'unknown'),
            foreignKeyField: fkField,
        });
        console.warn(`   ⚠️  Schema: model "${model.name}" attribute "${an}" dropped — redundant with the ` +
            `${rel.type} "${rel?.name || rel?.target}" foreign key (${fkField})`);
    }
    if (toDrop.size === 0)
        return out;
    if (wasArray) {
        model.attributes = model.attributes.filter((a) => !toDrop.has(a?.name));
    }
    else {
        const rebuilt = {};
        for (const [key, val] of Object.entries(model.attributes)) {
            if (toDrop.has(key) || toDrop.has(val?.name))
                continue;
            rebuilt[key] = val;
        }
        model.attributes = rebuilt;
    }
    return out;
}
/**
 * Resolve collisions for a single model. Exported so prisma's
 * schema-generator can keep its defensive call site for direct
 * invocations (the call becomes a no-op when realize has already
 * normalized; non-zero only when the generator is invoked outside
 * the realize pipeline).
 */
export function resolveOneModel(model) {
    const out = [];
    if (!model)
        return out;
    const wasArray = Array.isArray(model.attributes);
    const attributes = wasArray
        ? model.attributes
        : Object.values(model.attributes || {});
    const relationships = Array.isArray(model.relationships)
        ? model.relationships
        : Object.values(model.relationships || {});
    const attrNameToObj = new Map();
    for (const attr of attributes) {
        if (attr?.name)
            attrNameToObj.set(attr.name, attr);
    }
    const localRenames = [];
    for (const rel of relationships) {
        // Relationships emit a field named after `rel.name` (or a derived
        // default when name is absent). The default for hasMany/manyToMany
        // is the pluralised target lowercase; for belongsTo/hasOne it's
        // the bare target lowercase. Match all forms.
        const defaultName = rel?.type === 'hasMany' || rel?.type === 'manyToMany'
            ? pluralize(String(rel?.target || '').toLowerCase())
            : String(rel?.target || '').toLowerCase();
        const candidate = rel?.name || defaultName;
        if (!candidate)
            continue;
        const collidingAttr = attrNameToObj.get(candidate);
        if (!collidingAttr)
            continue;
        let renamed = `${candidate}Data`;
        let suffix = 2;
        while (attrNameToObj.has(renamed)) {
            renamed = `${candidate}Data${suffix++}`;
        }
        console.warn(`   ⚠️  Schema: model "${model.name}" attribute "${candidate}" renamed to "${renamed}" ` +
            `(collides with ${rel.type} relationship to ${rel.target})`);
        attrNameToObj.delete(candidate);
        collidingAttr.name = renamed;
        attrNameToObj.set(renamed, collidingAttr);
        localRenames.push({ from: candidate, to: renamed });
        out.push({
            modelName: String(model.name ?? '<anonymous>'),
            fromAttributeName: candidate,
            toAttributeName: renamed,
            relationshipType: String(rel?.type || 'unknown'),
            relationshipTarget: String(rel?.target || 'unknown'),
        });
    }
    // Object-form attributes carry the field name as the object key too.
    // Rebuild the object so downstream emitters that read Object.entries
    // (e.g. controller-generator's validation walker) see the renamed key.
    // Array-form needs no rewrite — the inner `.name` is the canonical
    // handle.
    if (!wasArray && localRenames.length > 0 && model.attributes) {
        const rebuilt = {};
        for (const [key, val] of Object.entries(model.attributes)) {
            const rename = localRenames.find((r) => r.from === key);
            rebuilt[rename ? rename.to : key] = val;
        }
        model.attributes = rebuilt;
    }
    return out;
}
//# sourceMappingURL=normalize-collisions.js.map