const DEFAULT_HEURISTIC = 'liberal';
export async function extractTypeScriptInterfaces(prepass, options = {}) {
    const heuristic = options.entityShapeHeuristic ?? DEFAULT_HEURISTIC;
    const facts = {
        entities: [],
        relationships: [],
        lifecycles: [],
        declaredTypes: [],
    };
    // Phase 1: gather all class + interface declarations.
    const classes = await prepass.listClasses();
    const interfaces = await prepass.listInterfaces();
    const fileSourceCache = new Map();
    // Phase 2: parse each declaration for fields.
    const allDecls = [];
    for (const sym of [...classes, ...interfaces]) {
        if (!sym.filePath)
            continue;
        let source = fileSourceCache.get(sym.filePath);
        if (!source) {
            try {
                source = await prepass.fileSourceText(sym.filePath);
                fileSourceCache.set(sym.filePath, source);
            }
            catch {
                continue;
            }
        }
        const decl = parseDeclaration(source, sym, sym.kind === 'interface' ? 'interface' : 'class');
        if (decl)
            allDecls.push(decl);
    }
    // Also scan files for `type X = { ... }` aliases — neither listClasses
    // nor listInterfaces returns these, but they're common entity-shape
    // carriers in modern TS code.
    const typeAliases = await scanTypeAliases(prepass);
    allDecls.push(...typeAliases);
    // Phase 3: classify each declaration's shape (entity / dto / utility).
    const entityNames = new Set();
    for (const decl of allDecls) {
        decl.shape = classifyShape(decl, heuristic);
        if (decl.shape === 'entity')
            entityNames.add(decl.name);
    }
    // Phase 4: split into entities vs non-entities.
    for (const decl of allDecls) {
        if (decl.shape === 'entity')
            facts.entities.push(decl);
        else
            facts.declaredTypes.push(decl);
    }
    // Phase 5: infer relationships from entity-typed fields.
    for (const ent of facts.entities) {
        for (const field of ent.fields) {
            const typeName = field.declaredType.replace(/\[\]$/, '').replace(/\| ?null/g, '').replace(/\| ?undefined/g, '').trim();
            if (!entityNames.has(typeName))
                continue;
            // Field is typed as a known entity — infer relationship.
            const isHasMany = field.isArray;
            facts.relationships.push({
                from: ent.name,
                to: typeName,
                type: isHasMany ? 'hasMany' : 'belongsTo',
                fieldName: field.name,
            });
        }
    }
    // Phase 6: infer lifecycles from literal-union type aliases.
    // Symmetric with typescript-decorators.ts's enum→lifecycle path —
    // when an entity has a STATUS-SUGGESTING field typed by a
    // `type X = "a" | "b" | "c"` alias, treat it as a state machine.
    //
    // Field-name heuristic (deliberate): only fields whose NAME
    // implies a workflow state get promoted. `platform: "ios"|"android"`
    // is an enum (classification), NOT a state machine. Without this
    // gate the spec ends up with a User.lifecycles.platform block which
    // is semantically wrong + creates schema validation noise.
    //
    // STATE-FIELD names recognised: status, state, phase, mode,
    // lifecycle, flow, stage, step. Add new names here cautiously —
    // false-positives produce phantom lifecycles in the realize stack.
    const literalUnions = await scanLiteralUnionAliases(prepass);
    const STATE_FIELD_NAMES = /^(status|state|phase|mode|lifecycle|flow|stage|step)$/i;
    if (literalUnions.size > 0) {
        for (const ent of facts.entities) {
            for (const field of ent.fields) {
                if (field.isArray)
                    continue;
                if (!STATE_FIELD_NAMES.test(field.name))
                    continue;
                const typeName = field.declaredType
                    .replace(/\| ?null/g, '')
                    .replace(/\| ?undefined/g, '')
                    .trim();
                const states = literalUnions.get(typeName);
                if (!states || states.length === 0)
                    continue;
                // One lifecycle per entity (first match wins).
                if (facts.lifecycles.some((lc) => lc.model === ent.name))
                    continue;
                facts.lifecycles.push({
                    model: ent.name,
                    field: field.name,
                    states,
                    transitions: [],
                });
            }
        }
    }
    return facts;
}
/**
 * Scan source files for `type X = "a" | "b" | "c"` literal-union
 * declarations. Returns a map: alias name → state list.
 *
 * Supports both inline-string and multi-line union forms:
 *   type Status = "a" | "b" | "c";
 *   type Status =
 *     | "a"
 *     | "b"
 *     | "c";
 *
 * Mixed-type unions (`"a" | number`) and unions with non-string members
 * are excluded — only pure string-literal unions become lifecycles.
 */
async function scanLiteralUnionAliases(prepass) {
    const out = new Map();
    let files;
    try {
        files = await prepass.listFiles({ glob: '**/*.ts', lang: 'typescript' });
    }
    catch {
        return out;
    }
    if (!files || files.length === 0)
        return out;
    const fileLimit = 500;
    const targets = files.slice(0, fileLimit);
    for (const file of targets) {
        let source;
        try {
            source = await prepass.fileSourceText(file.path ?? file);
        }
        catch {
            continue;
        }
        // Match: `type X =` capture name and right-hand-side up to `;` or
        // newline. Allow leading `export ` and any whitespace.
        const re = /(?:export\s+)?type\s+([A-Z][a-zA-Z0-9_]*)\s*=\s*([^;]+?);/g;
        let m;
        while ((m = re.exec(source)) !== null) {
            const name = m[1];
            const rhs = m[2].trim();
            // RHS must be entirely string literals separated by `|`. Allow
            // leading `|` (multi-line form). Reject if any part isn't a
            // quoted string literal.
            const stripped = rhs.replace(/^\|\s*/, '');
            const parts = stripped.split('|').map((s) => s.trim());
            const states = [];
            let ok = true;
            for (const p of parts) {
                const lit = p.match(/^['"`]([^'"`]+)['"`]$/);
                if (!lit) {
                    ok = false;
                    break;
                }
                states.push(lit[1]);
            }
            if (ok && states.length >= 2) {
                // Don't overwrite if a more specific alias already won — first
                // declaration wins. Reduces accidental overrides from re-exports.
                if (!out.has(name))
                    out.set(name, states);
            }
        }
    }
    return out;
}
function parseDeclaration(source, sym, declKind) {
    // Slice from startLine to end-of-file. The backend's endLine may not
    // include the closing brace (off-by-one or trailing-whitespace bug);
    // scan-balanced from `{` is the reliable path.
    const lines = source.split('\n');
    const start = Math.max(0, sym.startLine - 1);
    const slice = lines.slice(start).join('\n');
    // Find the body block — between `{` and the matching `}`.
    const openIdx = slice.indexOf('{');
    if (openIdx < 0)
        return null;
    const body = sliceBalancedBraces(slice, openIdx);
    if (!body)
        return null;
    // Pass the INTERIOR of the body to parseFields (between the outer
    // braces), not the body inclusive of them. parseFields' aggressive
    // `{[^{}]*}` strip would otherwise wipe the whole body when it has
    // no nested braces.
    const interior = body.slice(1, -1);
    const fields = parseFields(interior);
    if (fields.length === 0)
        return null;
    return {
        name: sym.name,
        filePath: sym.filePath,
        startLine: sym.startLine,
        endLine: sym.endLine,
        kind: declKind,
        fields,
        shape: 'unknown', // classified later
    };
}
/** Extract fields from a class/interface body. Recognises:
 *    name: Type
 *    name?: Type
 *    name: Type[]
 *    name: Type | null
 *    readonly name: Type
 *    public name: Type   (TS class field syntax)
 */
function parseFields(body) {
    const fields = [];
    // Strip line comments + block comments to avoid matching shapes inside JSDoc.
    let cleaned = body
        .replace(/\/\*[\s\S]*?\*\//g, '')
        .replace(/\/\/[^\n]*/g, '');
    // Strip constructor parameter blocks. TypeScript's auto-field syntax
    // (`constructor(private foo: Bar, ...)` declares class fields), but
    // those are dependency-injection params, NOT entity attributes. The
    // regex below otherwise captures them as fields, polluting the entity
    // shape classification (every NestJS service looks like an entity
    // with 10-20 "fields" of injected dependencies).
    cleaned = cleaned.replace(/constructor\s*\([^)]*\)/g, 'constructor()');
    // Strip method-body blocks. A field shape `foo: Bar` followed by a
    // method block `methodName() { ... }` would otherwise let the method's
    // body content (which can contain `key: value` shapes inside object
    // literals) leak into our regex.
    cleaned = cleaned.replace(/\{[^{}]*\}/g, '{}');
    // Field-name regex anchored at line start (post-cleanup). We capture
    // just the name + optional `?` + the position of `:`. The type string
    // is then extracted via balanced parsing to handle generic types like
    // `Record<string, unknown>` whose `,` would otherwise terminate a
    // naive regex match.
    const headerRe = /^[ \t]*(?:public\s+|private\s+|protected\s+|readonly\s+|static\s+)*([a-zA-Z_$][\w$]*)\s*(\?)?\s*:[ \t]*/gm;
    let m;
    const seen = new Set();
    while ((m = headerRe.exec(cleaned)) !== null) {
        const name = m[1];
        if (seen.has(name))
            continue;
        if (/^(constructor|get|set)$/i.test(name))
            continue;
        // Read the type expression starting after the colon, with balanced
        // angle/square/paren depth so `Record<string, unknown>`,
        // `Array<{x: number}>`, `(a: T) => U` are kept intact.
        const typeStart = headerRe.lastIndex;
        const typeStr = readBalancedType(cleaned, typeStart);
        if (!typeStr)
            continue;
        const declaredType = typeStr.trim();
        if (!declaredType)
            continue;
        // Skip method declarations — type starts with `(` or contains `=>`.
        if (declaredType.startsWith('(') || /=>/.test(declaredType))
            continue;
        seen.add(name);
        const isOptional = !!m[2] || /\|\s*(null|undefined)\b/.test(declaredType);
        const isArray = /\[\]$/.test(declaredType) || /^Array</.test(declaredType);
        fields.push({
            name,
            declaredType,
            isOptional,
            isArray,
        });
    }
    return fields;
}
/** Read a TypeScript type expression starting at `pos`, terminating at
 *  the next `;`, `,` (at depth 0), `\n` (at depth 0), or `}`. Tracks
 *  angle-bracket / square / paren depth so generic types are kept whole. */
function readBalancedType(s, pos) {
    let angle = 0, square = 0, paren = 0;
    let i = pos;
    while (i < s.length) {
        const c = s[i];
        if (c === '<')
            angle++;
        else if (c === '>' && angle > 0)
            angle--;
        else if (c === '[')
            square++;
        else if (c === ']' && square > 0)
            square--;
        else if (c === '(')
            paren++;
        else if (c === ')' && paren > 0)
            paren--;
        else if (angle === 0 && square === 0 && paren === 0) {
            if (c === ';' || c === ',' || c === '\n' || c === '}')
                break;
        }
        i++;
    }
    return s.slice(pos, i);
}
/** Find the substring between `{` at openIdx and its matching `}`,
 *  inclusive of the braces. Returns null if no match. */
function sliceBalancedBraces(s, openIdx) {
    let depth = 0;
    for (let i = openIdx; i < s.length; i++) {
        const c = s[i];
        if (c === '{')
            depth++;
        else if (c === '}') {
            depth--;
            if (depth === 0)
                return s.slice(openIdx, i + 1);
        }
    }
    return null;
}
/** Scan source files for `type X = { ... }` aliases that the backend's
 *  listInterfaces doesn't surface. Best-effort regex; assumes the type
 *  alias body fits within ~50 lines and uses standard TS syntax. */
async function scanTypeAliases(prepass) {
    const decls = [];
    // Find .ts files only.
    let files;
    try {
        files = await prepass.listFiles({ glob: '**/*.ts', lang: 'typescript' });
    }
    catch {
        return decls;
    }
    if (!files || files.length === 0)
        return decls;
    // Limit scan to keep cost bounded — adapter-free codebases can have
    // hundreds of files; we cap at 500 for now.
    const fileLimit = 500;
    const targets = files.slice(0, fileLimit);
    for (const file of targets) {
        let source;
        try {
            source = await prepass.fileSourceText(file.path ?? file);
        }
        catch {
            continue;
        }
        const lines = source.split('\n');
        // Match: `export type X = { ... }` or `type X = { ... }`
        const re = /(?:export\s+)?type\s+([A-Z][a-zA-Z0-9_]*)\s*=\s*\{/g;
        let m;
        while ((m = re.exec(source)) !== null) {
            const name = m[1];
            const openIdx = source.indexOf('{', m.index + m[0].length - 1);
            if (openIdx < 0)
                continue;
            const body = sliceBalancedBraces(source, openIdx);
            if (!body)
                continue;
            const fields = parseFields(body);
            if (fields.length === 0)
                continue;
            // Compute startLine/endLine from byte offsets.
            const startLine = source.slice(0, m.index).split('\n').length;
            const endLine = startLine + body.split('\n').length - 1;
            decls.push({
                name,
                filePath: file.path ?? file,
                startLine,
                endLine,
                kind: 'type',
                fields,
                shape: 'unknown',
            });
        }
    }
    return decls;
}
// ── Shape classification ─────────────────────────────────────────────
/** Decide whether a declaration is a persistent entity, a DTO shape, or
 *  utility based on its field set + the heuristic mode. */
function classifyShape(decl, heuristic) {
    const fieldNames = new Set(decl.fields.map((f) => f.name));
    // Class names ending in known service-shaped suffixes are NOT entities,
    // even if their constructor params or static state happens to include
    // an `id` field. Excludes the dependency-injection cases.
    if (decl.kind === 'class' && /(?:Service|Controller|Adapter|Cache|Provider|Factory|Repository|Store|Engine|Manager|Evaluator|Resolver|Worker|Job|Listener|Subscriber|Handler|Helper|Util|Builder|Visitor|Validator|Mapper|Transformer|Pipeline|Renderer|Driver)$/.test(decl.name)) {
        return 'utility';
    }
    // Has an id-shaped field?
    const hasId = fieldNames.has('id') || fieldNames.has('_id') || fieldNames.has('uuid');
    if (heuristic === 'liberal') {
        if (hasId)
            return 'entity';
    }
    else {
        const hasTimestamp = fieldNames.has('createdAt') || fieldNames.has('updatedAt') || fieldNames.has('created_at') || fieldNames.has('updated_at');
        if (hasId && hasTimestamp)
            return 'entity';
    }
    // Heuristic non-entity classification (best-effort signal for future
    // types: schema work — TODO #49):
    // - HTTP request/response by name suffix
    if (/(?:Request|Response|Dto|Input|Output|Payload|Args|Body|Params)$/.test(decl.name)) {
        return 'dto-shape';
    }
    // - Tiny utility (≤2 fields, no id) — likely config or computation aid
    if (decl.fields.length <= 2 && !hasId)
        return 'utility';
    return 'unknown';
}
//# sourceMappingURL=typescript-interfaces.js.map