/**
 * `naming-convention-fks` adapter — convention-FK relationship inference.
 *
 * Some codebases carry no explicit FK metadata: PowerSync (`column.text`
 * without `.references()`), hand-rolled Drizzle without relations, plain
 * SQL migrations without REFERENCES clauses. The relationship lives in the
 * column NAME (`club_id`, `userId`) by convention only. None of the
 * structural adapters (typescript-prisma / typescript-decorators /
 * typescript-interfaces / express-routes / react-views) catches this
 * pattern — typescript-interfaces sees `club_id: string` as a plain string
 * field, not a relationship.
 *
 * This adapter is a POST-EXTRACTION inference pass. It runs after all
 * structural adapters have populated `facts.entities[].attributes[]` and
 * walks those attributes for the `*_id` / `*Id` pattern, resolving the
 * prefix against the entity name set (singular OR plural form). When a
 * match resolves cleanly to a known entity, it emits a belongsTo
 * relationship from the owning entity to the target plus a reverse hasMany.
 *
 * Surfaced as a gap by the scoremyclays full-pipeline run 2026-05-12: 10
 * entities extracted, 0 relationships, because Supabase + PowerSync uses
 * convention FKs throughout (`club_id: column.text`).
 *
 * Cut 1 patterns:
 *   - `<entity>_id` (snake_case) or `<entity>Id` (camelCase), where the
 *     prefix maps to a known entity name (case-insensitive, singular OR
 *     plural via a small english heuristic).
 *
 * Out of scope (deferred to follow-up):
 *   - `created_by` / `assigned_to` / `owner_id` — target entity not named
 *     in the column (needs a curated semantic map OR LLM inference).
 *   - Composite FKs — same shape produces independent belongsTos; downstream
 *     can combine if it cares.
 *
 * Cardinality: forward `belongsTo` for every matched `*_id`. The reverse
 * `hasMany` is emitted ONLY when this adapter discovered the forward edge
 * itself — i.e. when a prior adapter hadn't already declared that
 * belongsTo. Rationale: rich-metadata codebases (TypeORM with explicit
 * @ManyToOne) decline to declare the inverse @OneToMany when they don't
 * navigate that way, and we shouldn't second-guess that omission. For
 * convention-only codebases (PowerSync) where no prior adapter saw the
 * relationship, BOTH directions are emitted so downstream gets a complete
 * cardinality view.
 *
 * Self-references (`parent_id` on Category → Category) emit only the
 * belongsTo regardless — `Category hasMany Category` is legal but rarely
 * the intended shape.
 *
 * Dedup: skip a candidate when the same `from|to|type` already exists in
 * the input relationships list (a higher-fidelity adapter wins).
 */
const FK_PATTERN = /^(.+?)(_id|Id)$/;
export function inferNamingConventionFks(entities, existingRelationships, options = {}) {
    const out = {
        relationships: [],
        matchedAttributes: 0,
        ignoredPrimaryKeys: 0,
        unresolvedTargets: 0,
    };
    const pluralize = options.pluralize ?? defaultPluralize;
    const singularize = options.singularize ?? defaultSingularize;
    // Name index: lowercase normalized form → canonical entity name. We
    // index both the raw lowercase entity name AND its singularized form so
    // that an entity declared as `Clubs` matches `club_id` and one declared
    // as `Club` matches `clubs_id` too.
    const nameIndex = new Map();
    for (const ent of entities) {
        const lc = ent.name.toLowerCase();
        nameIndex.set(lc, ent.name);
        const singular = singularize(lc);
        if (singular !== lc)
            nameIndex.set(singular, ent.name);
        const plural = pluralize(lc);
        if (plural !== lc)
            nameIndex.set(plural, ent.name);
    }
    // Track relationships already declared by prior adapters (the dedup
    // gate). A `from|to|type` present here is treated as authoritative.
    const preExisting = new Set();
    for (const r of existingRelationships) {
        preExisting.add(`${r.from}|${r.to}|${r.type}`);
    }
    // Track what THIS adapter has emitted so far (within-pass dedup, e.g.
    // an entity declaring both `user_id` and `userId`).
    const emitted = new Set();
    const recordEmit = (from, to, type, fieldName) => {
        const key = `${from}|${to}|${type}`;
        if (preExisting.has(key) || emitted.has(key))
            return false;
        out.relationships.push({ from, to, type, fieldName });
        emitted.add(key);
        return true;
    };
    for (const ent of entities) {
        if (!ent.attributes)
            continue;
        for (const attr of ent.attributes) {
            const match = FK_PATTERN.exec(attr.name);
            if (!match)
                continue;
            // Defensive guard against the (impossible-with-this-regex) `id` /
            // `_id` bare-PK case, plus the explicit isPrimary flag set by
            // earlier adapters.
            if (attr.isPrimary || attr.name === 'id' || attr.name === '_id') {
                out.ignoredPrimaryKeys++;
                continue;
            }
            // Identity proposal Phase 3/4 (case 7) — an FK explicitly marked
            // externally-owned (`auto=none`/`provided`/`external`) references a key
            // issued OUTSIDE SpecVerse, NOT an internal entity. The marker is
            // authoritative over the `*Id` name-heuristic: do NOT infer an internal
            // belongsTo for it (e.g. a `userId` holding a Replit-Auth uid must not be
            // linked to a coincidental internal `User` model).
            const fkAuto = String(attr.auto ?? '').toLowerCase();
            if (['none', 'provided', 'external'].includes(fkAuto)) {
                out.unresolvedTargets++;
                continue;
            }
            const prefix = match[1].toLowerCase();
            const target = nameIndex.get(prefix) ??
                nameIndex.get(singularize(prefix)) ??
                nameIndex.get(pluralize(prefix));
            if (!target) {
                out.unresolvedTargets++;
                continue;
            }
            out.matchedAttributes++;
            // Emit forward belongsTo. The reverse hasMany is only emitted when
            // we DISCOVERED the forward (it wasn't already in preExisting) — a
            // higher-fidelity adapter that omitted the reverse is trusted.
            // Self-FKs skip the reverse regardless (`X hasMany X` is rarely
            // intended even on hierarchical entities).
            const forwardWasNovel = recordEmit(ent.name, target, 'belongsTo', attr.name);
            if (forwardWasNovel && ent.name !== target) {
                recordEmit(target, ent.name, 'hasMany', attr.name);
            }
        }
    }
    return out;
}
/**
 * Tiny English pluralizer — handles common terminations. Not a full
 * pluralize library (no irregulars beyond a few common ones), but covers
 * ~95% of the entity names the analyse pipeline sees. Avoid pulling in a
 * runtime dep just for this.
 */
function defaultPluralize(s) {
    // Irregulars first.
    const IRREGULAR = {
        person: 'people',
        man: 'men',
        woman: 'women',
        child: 'children',
        foot: 'feet',
        tooth: 'teeth',
        mouse: 'mice',
        goose: 'geese',
    };
    if (IRREGULAR[s])
        return IRREGULAR[s];
    if (/(s|x|z|ch|sh)$/i.test(s))
        return s + 'es';
    if (/[bcdfghjklmnpqrstvwxz]y$/i.test(s))
        return s.slice(0, -1) + 'ies';
    if (/(?:[^f])fe?$/i.test(s))
        return s.replace(/fe?$/i, 'ves');
    return s + 's';
}
function defaultSingularize(s) {
    const IRREGULAR = {
        people: 'person',
        men: 'man',
        women: 'woman',
        children: 'child',
        feet: 'foot',
        teeth: 'tooth',
        mice: 'mouse',
        geese: 'goose',
    };
    if (IRREGULAR[s])
        return IRREGULAR[s];
    if (/ies$/i.test(s))
        return s.slice(0, -3) + 'y';
    if (/ves$/i.test(s))
        return s.slice(0, -3) + 'f';
    if (/(s|x|z|ch|sh)es$/i.test(s))
        return s.slice(0, -2);
    if (/s$/i.test(s) && !/ss$/i.test(s))
        return s.slice(0, -1);
    return s;
}
//# sourceMappingURL=naming-convention-fks.js.map