/**
 * Behavior step classifier — turns method-body walker output into a
 * structured candidate-step timeline that the analyse LLM can refine
 * into spec `behaviors[].steps[]` blocks. Implements Phase 2 of #27.
 *
 * The classifier is a "soft oracle" (per the
 * 2026-04-26-BEHAVIOR-EXTRACTION-FOR-ANALYSE.md proposal): it labels
 * what the patterns clearly say and falls through to `kind: 'call'` /
 * `kind: 'compute'` for unclassifiable lines. The LLM filters / merges /
 * refines the output into the final spec vocabulary.
 *
 * Why classification matters: pre-classifier the LLM sees raw method
 * bodies (200 lines, lots of boilerplate). With it, the LLM sees
 * "this method does {find, validate, create, emit, return}" — a
 * structured anchor it can map to spec language without inventing.
 *
 * Step kinds (the categorical vocabulary):
 *   - `find`       — read a record by key (`repo.findOne`, `findOneByField`)
 *   - `find-many`  — read a collection (`repo.find()`, `findAll()`)
 *   - `create`     — write a new record (`repo.save(create(...))`, `insertOne`)
 *   - `update`     — mutate an existing record (`repo.update`, `updateOneById`)
 *   - `delete`     — remove a record (`repo.delete`, `deleteOneById`)
 *   - `transition` — change a lifecycle state (assignment to enum-typed field)
 *   - `emit`       — publish an event (`eventBus.emit/publish/dispatch`)
 *   - `validate`   — precondition guard or throw outside a guard
 *   - `call`       — invoke a service method (not repo, not eventBus)
 *   - `return`     — explicit return (kept for completeness; the analyse
 *                    layer usually folds this into the behavior's
 *                    `returns:` declaration rather than a step)
 *
 * Open question (deferred to next phase): merging adjacent related
 * steps (`repo.create + repo.save` = one `create`). For now the
 * classifier emits both — the LLM can collapse.
 */
import { walkMethodBodies } from './method-body-walker.js';
/**
 * Walk a class body and return per-method candidate-step timelines.
 * Each method's candidates are ordered by source position so the
 * timeline reflects execution order.
 *
 * `lifecycleField` (optional) — when known, assignments to this field
 * are classified as `transition` rather than generic `call`/`update`.
 * Pass it from the lifecycle-detection pass (see
 * typescript-decorators.ts).
 */
export function extractCandidateSteps(classBody, options = {}) {
    const methods = walkMethodBodies(classBody);
    const out = [];
    for (const method of methods) {
        const candidates = [];
        // Each fact contributes 0 or 1 candidate steps. Push, then sort by
        // position to merge across kinds.
        for (const guard of method.guards) {
            if (guard.throws) {
                // Guard with throw = precondition.
                candidates.push({
                    kind: 'validate',
                    position: guard.position,
                    summary: `Validate this.${guard.field} ${guard.op === '!==' || guard.op === '!=' ? '==' : '!='} ${guard.valueExpr}`,
                    precondition: `this.${guard.field} ${guard.op === '!==' || guard.op === '!=' ? '==' : '!='} ${guard.valueExpr}`,
                });
            }
        }
        for (const throwStmt of method.throws) {
            // Top-level throws (not inside an extracted guard) are also
            // preconditions, but they may be reported alongside their guard's
            // throw classification above. Deduplication-by-position would be
            // more correct; for the soft-oracle stance, accept some redundancy
            // and let downstream merge.
            candidates.push({
                kind: 'validate',
                position: throwStmt.position,
                summary: `Throw ${throwStmt.errorClass ?? 'Error'}${throwStmt.argsExpr ? ' (' + throwStmt.argsExpr + ')' : ''}`,
                precondition: throwStmt.argsExpr,
            });
        }
        for (const assignment of method.thisAssignments) {
            if (options.lifecycleField && assignment.field === options.lifecycleField && assignment.enumMember) {
                candidates.push({
                    kind: 'transition',
                    position: assignment.position,
                    summary: `Transition this.${assignment.field} to ${assignment.enumMember}`,
                    transition: {
                        field: assignment.field,
                        toState: assignment.enumMember,
                    },
                });
            }
            // Non-lifecycle assignments fall through (not a step on their own).
        }
        for (const call of method.calls) {
            const classified = classifyCall(call);
            if (classified) {
                candidates.push({
                    ...classified,
                    position: call.position,
                });
            }
        }
        for (const ret of method.returns) {
            // Skip the trivial implicit `return;` — only emit when there's a
            // real value being returned (the analyse layer folds these into
            // the behavior's `returns:` declaration usually).
            if (ret.valueExpr) {
                candidates.push({
                    kind: 'return',
                    position: ret.position,
                    summary: `Return ${truncate(ret.valueExpr, 80)}`,
                });
            }
        }
        candidates.sort((a, b) => a.position - b.position);
        // Deduplicate exact-position pairs (a `validate` from a guard often
        // co-locates with its throw inside the guard body; the throw alone
        // gets reported, the guard already covered the precondition).
        const deduped = [];
        for (const step of candidates) {
            const last = deduped[deduped.length - 1];
            if (last && last.kind === 'validate' && step.kind === 'validate'
                && Math.abs(last.position - step.position) < 5) {
                continue;
            }
            deduped.push(step);
        }
        out.push({
            methodName: method.methodName,
            isAsync: method.isAsync,
            body: method.body,
            sourceLineRange: method.sourceLineRange,
            candidates: deduped,
        });
    }
    return out;
}
/**
 * Classify a single method call into a CandidateStep (or null if the
 * call shouldn't surface as a step — e.g. utility helpers like
 * `Object.assign`, `Array.from`).
 *
 * Soft-oracle stance: when in doubt, classify as `call` rather than
 * trying to be clever. The LLM has the call shape + receiver name and
 * can collapse / refine.
 */
function classifyCall(call) {
    const receiver = call.receiver;
    const method = call.method;
    const args = call.argsExpr;
    // Skip JS stdlib utility invocations (no business value).
    if (/^(Object|Array|JSON|Math|Date|Number|String|Boolean|RegExp)$/.test(receiver)) {
        return null;
    }
    // EventBus / Subject-style emissions.
    if (/(?:^|\.)eventBus$/i.test(receiver) && /^(emit|publish|dispatch|fire)$/i.test(method)) {
        const event = parseFirstStringArg(args);
        return {
            kind: 'emit',
            summary: event ? `Emit ${event} event` : `Emit event (${truncate(args, 60)})`,
            event: event ?? undefined,
        };
    }
    // RxJS-style: `subject.next('Foo', payload)` — heuristic, often used
    // for events too. Receiver name often ends in `Subject` or contains
    // `Bus` / `Stream` / `EventEmitter`.
    if (/(?:Subject|Bus|Stream|Emitter)$/.test(receiver) && /^(next|emit|publish|fire|dispatch)$/i.test(method)) {
        const event = parseFirstStringArg(args);
        return {
            kind: 'emit',
            summary: event ? `Emit ${event} event` : `Emit event via ${receiver}.${method}`,
            event: event ?? undefined,
        };
    }
    // Repository-pattern reads (TypeORM `repo.findOne`, Mongo
    // `getCollection().findOne`, pg `findOneByField`).
    if (/^(findOne|findOneBy|findOneByOrFail|findById|getById)$/i.test(method)) {
        const entity = guessEntityFromReceiver(receiver);
        return {
            kind: 'find',
            summary: `Find ${entity ?? '?'}${args ? ' by ' + truncate(args, 40) : ''}`,
            entity,
        };
    }
    if (/^(findOneByField)$/.test(method)) {
        return {
            kind: 'find',
            summary: `Find ${args ? truncate(args, 80) : 'record'}`,
            entity: parseFirstStringArg(args) ?? undefined,
        };
    }
    if (/^(find|findAll|findMany|all|list|search)$/i.test(method)) {
        const entity = guessEntityFromReceiver(receiver);
        return {
            kind: 'find-many',
            summary: `Find ${entity ?? 'records'}${args ? ' (' + truncate(args, 40) + ')' : ''}`,
            entity,
        };
    }
    // Repository-pattern writes.
    if (/^(create|save|persist|insert|insertOne)$/i.test(method)) {
        const entity = guessEntityFromReceiver(receiver) ?? parseFirstStringArg(args) ?? undefined;
        return {
            kind: 'create',
            summary: `Create ${entity ?? 'record'}`,
            entity,
        };
    }
    if (/^(insertMany)$/i.test(method)) {
        return {
            kind: 'create',
            summary: `Bulk create records`,
        };
    }
    if (/^(update|updateOne|updateOneById|updateMany|patch)$/i.test(method)) {
        const entity = guessEntityFromReceiver(receiver) ?? parseFirstStringArg(args) ?? undefined;
        return {
            kind: 'update',
            summary: `Update ${entity ?? 'record'}`,
            entity,
        };
    }
    if (/^(delete|deleteOne|deleteOneById|deleteMany|remove|destroy)$/i.test(method)) {
        const entity = guessEntityFromReceiver(receiver) ?? parseFirstStringArg(args) ?? undefined;
        return {
            kind: 'delete',
            summary: `Delete ${entity ?? 'record'}`,
            entity,
        };
    }
    // Mongo native — `getCollection('name')` followed by `.findOne` etc.
    // The walker reports these as separate calls; the find/create/update
    // patterns above already cover the second leg.
    // Anything else is a generic call (likely a service method).
    return {
        kind: 'call',
        summary: `Call ${receiver}.${method}${args ? '(' + truncate(args, 40) + ')' : '()'}`,
        callTarget: `${receiver}.${method}`,
    };
}
/**
 * Best-effort entity name extraction from a receiver expression.
 * `this.invoiceRepo` → `Invoice`. `this.payments` → `Payment`. Returns
 * undefined when no clean inference is possible.
 */
function guessEntityFromReceiver(receiver) {
    // Strip `this.` prefix.
    const tail = receiver.replace(/^this\./, '');
    // Last segment of dotted access.
    const last = tail.split('.').pop() ?? tail;
    // Common suffixes: Repo, Repository, Service, Bus, Manager. Strip them.
    const trimmed = last.replace(/(?:Repo|Repository|Service|Bus|Manager|Store|Dao)$/i, '');
    if (!trimmed)
        return undefined;
    // Pluralized — drop trailing s if Capitalised collection (e.g. `users` → `User`).
    const singular = /^[a-z]+s$/.test(trimmed) ? trimmed.slice(0, -1) : trimmed;
    // Capitalize first letter.
    return singular.charAt(0).toUpperCase() + singular.slice(1);
}
/** Parse the first argument's string-literal value, if any. */
function parseFirstStringArg(args) {
    const m = args.match(/^\s*['"]([^'"]+)['"]/);
    return m ? m[1] : null;
}
function truncate(s, max) {
    if (s.length <= max)
        return s;
    return s.slice(0, max - 1) + '…';
}
//# sourceMappingURL=behavior-step-classifier.js.map