import { walkMethodBodies } from '../method-body-walker.js';
/**
 * Run the generic decorator adapter against a structural backend.
 * Caller is expected to have run `prepass.init(...)` and `prepass.index()`
 * already (the standard runPrepass flow).
 */
export async function extractTypeScriptDecorators(prepass) {
    const facts = { entities: [], relationships: [], lifecycles: [], frameworks: [] };
    const classes = await prepass.listClasses();
    const seenFrameworks = new Set();
    // Per-file enum cache so we don't re-parse the same file's enums for
    // each entity declared in it.
    const fileEnumsCache = new Map();
    for (const cls of classes) {
        if (!cls.filePath)
            continue;
        let body;
        try {
            body = await prepass.fileSourceText(cls.filePath);
        }
        catch {
            continue;
        }
        const slice = sliceClass(body, cls.startLine, cls.endLine);
        if (!slice)
            continue;
        // Detect framework from the class-level decorator block (immediately above
        // `class X {`). Walking the decorator block specifically — rather than the
        // first ~600 chars of an arbitrary lookback — handles long import blocks
        // and multi-line decorator arguments (twenty: 9-16 lines of class-level
        // decorators between @Entity and the class declaration).
        //
        // #28-jpa: when the source file is Java, @Entity → 'jpa' rather than
        // 'typeorm'. The annotations look identical but the framework label
        // is what realize-side and component-suggestion logic key off.
        const isJavaSource = cls.filePath.toLowerCase().endsWith('.java');
        const framework = detectFramework(slice.decorators, isJavaSource);
        if (framework === 'not-an-entity')
            continue;
        seenFrameworks.add(framework);
        const classBody = slice.body;
        const attributes = extractAttributes(classBody);
        facts.entities.push({
            name: cls.name,
            filePath: cls.filePath,
            startLine: cls.startLine,
            endLine: cls.endLine,
            framework: framework,
            attributes,
            isExported: !!cls.isExported,
        });
        for (const rel of extractRelationships(classBody, cls.name)) {
            facts.relationships.push(rel);
        }
        // Lifecycle detection — walk the file's enum declarations and find
        // attributes whose declared type matches an enum name. Common
        // SpecVerse pattern: `status: SomeStatus` field declared via
        // `@Column({ enum: SomeStatus })` plus an `export enum SomeStatus
        // { Open = 'open', ... }` declaration in the same file.
        //
        // Heuristic for which enum-typed field is THE lifecycle: an
        // entity may have multiple enum-typed columns (e.g. Payment with
        // both `method: PaymentMethod` AND `status: PaymentStatus`). The
        // STATUS one is what we want — it's the field that goes through
        // transitions. Selection rule (in order of precedence):
        //   1. exact-name match: a field literally named `status` or `state`
        //   2. name ends in `Status` / `State`
        //   3. first enum-typed attribute (last-resort fallback)
        let fileEnums = fileEnumsCache.get(cls.filePath);
        if (fileEnums === undefined) {
            fileEnums = extractEnums(body);
            fileEnumsCache.set(cls.filePath, fileEnums);
        }
        const enumTypedAttrs = [];
        for (const attr of attributes) {
            const typeName = (attr.declaredType ?? '').trim();
            if (!typeName)
                continue;
            const baseName = typeName.replace(/\s*\|\s*(null|undefined)\s*$/, '').trim();
            const states = fileEnums[baseName];
            if (states && states.length > 0) {
                enumTypedAttrs.push({ attr, states });
            }
        }
        if (enumTypedAttrs.length > 0) {
            const exactMatch = enumTypedAttrs.find((e) => e.attr.name === 'status' || e.attr.name === 'state');
            const suffixMatch = exactMatch
                ?? enumTypedAttrs.find((e) => /(?:Status|State)$/.test(e.attr.name));
            const chosen = suffixMatch ?? enumTypedAttrs[0];
            const transitions = extractTransitions(classBody, chosen.attr.name, chosen.states);
            facts.lifecycles.push({
                model: cls.name,
                field: chosen.attr.name,
                states: chosen.states,
                transitions: transitions.length > 0 ? transitions : undefined,
            });
        }
    }
    facts.frameworks = [...seenFrameworks];
    return facts;
}
/**
 * Pair guards + assignments produced by `walkMethodBodies` into
 * lifecycle transitions. Each method that mutates `this.<lifecycleField>`
 * to an enum member yields one transition entry; the guard (if any)
 * determines the source state(s).
 *
 * Two patterns recognised:
 *   1. `if (this.<f> !== EnumName.X) throw; ... this.<f> = EnumName.Y;`
 *      → from: [X], to: Y
 *   2. `if (this.<f> === EnumName.X) throw; ... this.<f> = EnumName.Y;`
 *      → from: allStates - [X], to: Y
 *
 * Methods without a guard but with the assignment yield a transition
 * from `allStates` (any source). The action label is the method name.
 */
function extractTransitions(classBody, lifecycleField, allStates) {
    const out = [];
    const methods = walkMethodBodies(classBody);
    for (const method of methods) {
        // Filter to assignments touching the lifecycle field with an
        // enum-member RHS (we only care about state mutations, not bare
        // value writes like `this.status = someComputed`).
        const assignments = method.thisAssignments.filter((a) => a.field === lifecycleField && a.enumMember);
        if (assignments.length === 0)
            continue;
        // Take the FIRST assignment as the canonical transition target.
        // Multi-assignment methods (rare; usually error-flow ternaries) fall
        // through to the LLM with the first one wins.
        const target = assignments[0].enumMember;
        // Find guards on the same field. The first guard with `throws=true`
        // is the precondition that defines source states.
        const guard = method.guards.find((g) => g.field === lifecycleField && g.throws && g.enumMember);
        let from;
        if (!guard) {
            from = [...allStates];
        }
        else if (guard.op === '!==' || guard.op === '!=') {
            // `if (this.f !== X) throw` → must equal X to proceed.
            from = [guard.enumMember];
        }
        else {
            // `if (this.f === X) throw` → must NOT be X to proceed; complement.
            from = allStates.filter((s) => s !== guard.enumMember);
        }
        out.push({
            action: method.methodName,
            from,
            to: target,
        });
    }
    return out;
}
/**
 * Parse `export enum Foo { A = 'a', B, C = 3 }` declarations from a
 * file. Returns a record of enum name → array of member NAMES (not
 * values). For SpecVerse's lifecycle model the names are what matter
 * (states are referenced by enum-member-name in transition methods);
 * value mapping is downstream's concern.
 *
 * Single-line and multi-line bodies are handled. Comments are stripped
 * via line-by-line filter (TypeScript single-line comments only —
 * block comments inside the enum body are uncommon and would need a
 * proper tokenizer to handle correctly).
 */
function extractEnums(text) {
    const out = {};
    // Match `enum Foo { ... }` — `export` (TypeScript) and `public`
    // (Java) modifiers are optional. The body can be either TS-style
    // `Member = 'value'` lines or Java-style bare member names.
    // Bounded by [^}]* (enums don't contain braces themselves).
    const re = /\b(?:export\s+|public\s+)?enum\s+(\w+)\s*\{([^}]+)\}/g;
    let m;
    while ((m = re.exec(text)) !== null) {
        const name = m[1];
        const body = m[2];
        const members = [];
        for (const line of body.split(/\r?\n/)) {
            // Drop comments + trailing comma + whitespace.
            const cleaned = line.replace(/\/\/.*$/, '').trim().replace(/,\s*$/, '');
            if (!cleaned)
                continue;
            // Member shape: `Name` or `Name = value` or `Name = 'string'`.
            const memberMatch = cleaned.match(/^([A-Za-z_$][\w$]*)/);
            if (memberMatch)
                members.push(memberMatch[1]);
        }
        out[name] = members;
    }
    return out;
}
// ─────────────────────────────────────────────────────────────────────
// Detection + extraction helpers
// ─────────────────────────────────────────────────────────────────────
/**
 * Decide whether the class is an entity and which framework it uses, given
 * the class-level decorator block (collected by `sliceClass`). The block is
 * already trimmed to just `@...` decorators + their multi-line arguments +
 * the `extends` clause from the class declaration, so framework probes can
 * scan it directly without char-count guards.
 *
 * Returns `not-an-entity` to signal "skip this class".
 */
function detectFramework(decoratorBlock, isJavaSource = false) {
    // JPA (Java) — same @Entity / @Table annotations as TypeORM but the
    // source file is Java. Check first so the Java probe wins before the
    // TypeORM branch matches the same annotation. (Sequelize's @Table +
    // `extends Model` can't appear in Java, so checking @Entity is enough.)
    if (isJavaSource && /@Entity\s*[\s(]?/.test(decoratorBlock))
        return 'jpa';
    // Sequelize: @Table or `extends Model` (Model can appear on the class line)
    if (/@Table\s*\(/.test(decoratorBlock))
        return 'sequelize';
    if (/\bextends\s+Model\b/.test(decoratorBlock))
        return 'sequelize';
    // TypeORM / NestJS: @Entity
    if (/@Entity\s*[\s(]/.test(decoratorBlock))
        return 'typeorm';
    // Mongoose with NestJS: @Schema
    if (/@Schema\s*[\s(]/.test(decoratorBlock))
        return 'mongoose';
    return 'not-an-entity';
}
/**
 * Extract property declarations annotated with @Column / @PrimaryKey / @Id
 * etc. Returns name + declared type (best-effort). We're regex-based so we
 * only catch the common shapes; framework-specific adapters do better.
 *
 * Handles patterns like:
 *   TypeScript / TypeORM / Sequelize:
 *     @Column                              email!: string;
 *     @Column({ unique: true })            email: string;
 *     @PrimaryGeneratedColumn('uuid')      id!: string;
 *     @PrimaryKey                          id: number;
 *     @Column(DataType.STRING)             name?: string;
 *
 *   Java / JPA (#28-jpa):
 *     @Column                              private String email;
 *     @Id                                  private UUID id;
 *     @Enumerated(EnumType.STRING)         private InvoiceStatus status;
 *     @Column(unique = true, nullable = false, length = 32)
 *                                          private String invoiceNumber;
 */
function extractAttributes(body) {
    const out = [];
    const seen = new Set();
    // First: TypeScript / TypeORM / Sequelize shape — `name: type` (with
    // optional `!` / `?` after the name). Captures the decorator block,
    // the property name (group 2), and the declared TS type (group 3).
    const tsRe = /^([ \t]*@(?:Column|PrimaryKey|PrimaryGeneratedColumn|Id|Unique|CreatedAt|UpdatedAt|DeletedAt|ForeignKey|Default|AllowNull|Prop|Enumerated)(?:\([^)]*\))?[ \t]*\n[ \t]*(?:@[A-Z][a-zA-Z]*(?:\([^)]*\))?[ \t]*\n[ \t]*)*)([a-zA-Z_][a-zA-Z0-9_]*)[!?]?:\s*([^;\n=]+)/gm;
    let m;
    while ((m = tsRe.exec(body)) !== null) {
        const decoratorBlock = m[1];
        const name = m[2];
        const declaredType = m[3].trim();
        if (seen.has(name))
            continue;
        seen.add(name);
        out.push({
            name,
            declaredType,
            isPrimary: /@(?:PrimaryKey|PrimaryGeneratedColumn|Id)\b/.test(decoratorBlock),
            isUnique: /@Unique\b/.test(decoratorBlock) || /unique\s*:\s*true/i.test(decoratorBlock),
        });
    }
    // #28-jpa — Java field shape: `[modifiers] type name [= initializer];`.
    // Captures decorator block (group 1), declared Java type (group 2), and
    // field name (group 3). Modifiers (`private`, `protected`, `final`, etc.)
    // are tolerated but not required — JPA fields are conventionally `private`.
    const javaRe = /^([ \t]*@(?:Column|Id|GeneratedValue|Unique|Enumerated|JoinColumn|Lob|Temporal|Version|CreatedDate|LastModifiedDate)(?:\([^)]*\))?[ \t]*\n[ \t]*(?:@[A-Z][a-zA-Z]*(?:\([^)]*\))?[ \t]*\n[ \t]*)*)(?:(?:private|protected|public|final|static|transient|volatile)\s+)*((?:[A-Z]\w*|[a-z]\w+)(?:<[^>]+>)?(?:\[\])?)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[=;]/gm;
    while ((m = javaRe.exec(body)) !== null) {
        const decoratorBlock = m[1];
        const declaredType = m[2].trim();
        const name = m[3];
        if (seen.has(name))
            continue;
        seen.add(name);
        out.push({
            name,
            declaredType,
            isPrimary: /@(?:Id|GeneratedValue)\b/.test(decoratorBlock),
            isUnique: /@Unique\b/.test(decoratorBlock) || /unique\s*=\s*true/i.test(decoratorBlock),
        });
    }
    return out;
}
/**
 * Find relationship decorators (@OneToMany etc.) and the entity they point
 * to. Both `() => Foo` and `'Foo'` argument styles are recognised, plus
 * the Java/JPA shape (`@OneToMany(mappedBy = "...")` followed by a typed
 * Java field declaration).
 */
function extractRelationships(body, fromName) {
    const out = [];
    // TypeScript / TypeORM / Sequelize shape:
    //   @OneToMany(() => Type, t => t.parent)  fieldName: Foo[];
    //
    // The trailing `fieldName:` colon syntax doesn't appear in Java, so this
    // regex won't false-match Java sources. (The `name:` inside
    // `@JoinColumn({ name: 'fk_id' })` is a known imprecision — the
    // captured fieldName can be `name` instead of the actual field. The
    // target type is extracted from the decorator args, not from fieldName,
    // so the relationship's `from → to` shape is still correct; only the
    // fieldName itself is unreliable for TS sources with stacked
    // `@JoinColumn` decorators.)
    const tsRe = /@(OneToMany|HasMany|ManyToOne|BelongsTo|OneToOne|HasOne|ManyToMany)\s*\(((?:[^()]|\([^()]*\))*)\)[\s\S]{0,300}?([a-zA-Z_][a-zA-Z0-9_]*)[!?]?\s*:/g;
    let m;
    while ((m = tsRe.exec(body)) !== null) {
        const kind = m[1];
        const args = m[2];
        const fieldName = m[3];
        let to;
        const arrow = args.match(/=>\s*([A-Z][A-Za-z0-9_]*)/);
        const quoted = args.match(/['"]([A-Z][A-Za-z0-9_]*)['"]/);
        const bareIdent = args.match(/(?:type\s*:\s*)?([A-Z][A-Za-z0-9_]*)/);
        if (arrow)
            to = arrow[1];
        else if (quoted)
            to = quoted[1];
        else if (bareIdent)
            to = bareIdent[1];
        if (!to)
            continue;
        out.push({ from: fromName, to, type: relationshipType(kind), fieldName });
    }
    // #28-jpa — Java/JPA shape:
    //   @OneToMany(mappedBy = "customer")    // inverse: target inside List<>
    //   private List<Invoice> invoices;
    //
    //   @ManyToOne(optional = false)         // forward: target IS the field type
    //   @JoinColumn(name = "customer_id")
    //   private Customer customer;
    //
    // We capture the decorator + its args (or empty parens), then walk
    // forward through any number of additional `@…` annotations + access
    // modifiers, and read the Java type and field name. For OneToMany /
    // ManyToMany the target lives INSIDE `List<X>` / `Set<X>`; for
    // ManyToOne / OneToOne it's the bare type. The trailing `[=;]` anchors
    // the match to a Java field declaration (`private Customer customer;`
    // or `private InvoiceStatus status = InvoiceStatus.OPEN`).
    const javaRe = /@(OneToMany|ManyToOne|OneToOne|ManyToMany)(?:\s*\(((?:[^()]|\([^()]*\))*)\))?[ \t]*\n(?:[ \t]*@[A-Z][A-Za-z]*(?:\([^)]*\))?[ \t]*\n)*[ \t]*(?:(?:private|protected|public|final|static)\s+)*((?:List|Set|Collection)<\s*([A-Z]\w*)\s*>|([A-Z]\w*))\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[=;]/gm;
    while ((m = javaRe.exec(body)) !== null) {
        const kind = m[1];
        const collectionTarget = m[4]; // type inside List<...>
        const bareTarget = m[5]; // bare type name
        const fieldName = m[6];
        const to = collectionTarget || bareTarget;
        if (!to)
            continue;
        out.push({ from: fromName, to, type: relationshipType(kind), fieldName });
    }
    return out;
}
/** Map decorator kind to the canonical relationship type. */
function relationshipType(kind) {
    if (kind === 'OneToMany' || kind === 'HasMany')
        return 'hasMany';
    if (kind === 'ManyToOne' || kind === 'BelongsTo')
        return 'belongsTo';
    if (kind === 'OneToOne' || kind === 'HasOne')
        return 'hasOne';
    return 'manyToMany';
}
/**
 * Slice a class into two pieces: the class-level decorator block (immediately
 * above `class X {`) and the class body (from `class X {` through endLine).
 *
 * The decorator block is built by walking UPWARD from the class line and
 * collecting every line that is either:
 *   - blank,
 *   - starts with `@` (a decorator), or
 *   - is a continuation of an unclosed parenthesised decorator argument.
 *
 * It also includes the class-declaration line itself (so `extends Model` is
 * visible to framework detection). The walk stops at the first
 * non-decorator-non-blank line (typically an `import`, `}`, or comment).
 *
 * This handles real-world shapes that the previous fixed-line lookback
 * missed — e.g. twenty's class-level decorator stacks like:
 *   @Entity({ name: 'view', schema: 'core' })
 *   @Index(...)
 *   @Unique('IDX_...', [...])
 *   @Index(...)
 *   ...
 *   export class ViewEntity { ... }
 * which can run 9-16 lines before the class declaration, well past any
 * fixed lookback or char-count slice.
 *
 * Returns null if line numbers are missing or out of range.
 */
function sliceClass(fileText, startLine, endLine) {
    if (!startLine || !endLine || endLine <= startLine)
        return null;
    const lines = fileText.split('\n');
    if (startLine - 1 >= lines.length)
        return null;
    const classLineIdx = startLine - 1;
    const decoratorLines = [];
    // Walk UP from class line, tracking paren balance contributed by lines we visit.
    // Going backward, ')' tokens INCREASE depth (unclosed-from-above) and '(' tokens
    // DECREASE depth. While depth > 0 we're inside a multi-line decorator argument.
    let parenDepth = 0;
    for (let i = classLineIdx - 1; i >= 0 && i >= classLineIdx - 60; i--) {
        const line = lines[i];
        const opens = (line.match(/\(/g) || []).length;
        const closes = (line.match(/\)/g) || []).length;
        parenDepth += closes - opens;
        if (line.trim() === '') {
            decoratorLines.unshift(line);
            // blank lines OK as long as we haven't seen a decorator yet, OR we're inside parens
            if (decoratorLines.some((l) => l.trim().startsWith('@')) || parenDepth > 0)
                continue;
            // a leading blank line (no decorators yet) is benign — keep walking
            continue;
        }
        if (parenDepth > 0) {
            decoratorLines.unshift(line);
            continue;
        }
        if (line.trim().startsWith('@')) {
            decoratorLines.unshift(line);
            continue;
        }
        // Hit a non-decorator, non-continuation, non-blank line — stop.
        break;
    }
    // Include the class declaration line (covers `extends Model`).
    decoratorLines.push(lines[classLineIdx]);
    const decorators = decoratorLines.join('\n');
    const body = lines.slice(classLineIdx, Math.min(lines.length, endLine + 1)).join('\n');
    return { decorators, body };
}
//# sourceMappingURL=typescript-decorators.js.map