/**
 * `express-routes` adapter — surfaces HTTP routes from Express-style
 * router files as synthetic *Controller classes.
 *
 * Why this exists: idle-meta-style codebases keep their HTTP API in
 * `routes/<resource>.ts` modules using `const router = Router()` +
 * `router.<verb>(path, handler)` declarations. There are NO class-based
 * `*Controller`s — but the API surface is real and architecturally
 * meaningful. Without this adapter the spec loses the entire
 * controllers: layer (every action collapses into services or
 * Unassigned).
 *
 * Each route file becomes one synthetic controller in the prepass
 * output. Each `router.<verb>(path, handler)` becomes one method on
 * that controller. The handler's body is extracted verbatim so the
 * existing candidate-step classifier can run on it unmodified.
 *
 * Detection criteria (must match BOTH):
 *   - File contains `Router()` or `import { Router }` from express
 *   - File has at least one `router.<verb>(...)` call where verb is
 *     get|post|put|patch|delete|all|head|options
 *
 * Naming derivation:
 *   - filename `auth.ts` → `AuthController`
 *   - filename `games.v3.ts` → `GamesV3Controller`
 *   - filename `user-profile.ts` → `UserProfileController`
 *
 * Action-name derivation (per handler, in priority order):
 *   1. Named handler reference: `router.post('/x', registerHandler)` →
 *      strip `Handler` suffix → `register`
 *   2. Single-word path with no parameters: `router.post('/login', ...)` →
 *      `login`
 *   3. Verb + path PascalCase: `router.get('/users/:id', ...)` → `getUsers`
 *   4. Fallback: `<verb><N>` where N is the handler ordinal
 */
/** True when the source looks like an Express router file. */
export function isExpressRouterFile(source) {
    // Must reference Router (named import or call) AND have ≥1 router.<verb>(...) call.
    const hasRouter = /\bRouter\s*\(\s*\)/.test(source) || /from\s+['"]express['"]/.test(source);
    if (!hasRouter)
        return false;
    return /\b\w+\.(?:get|post|put|patch|delete|all|head|options)\s*\(/.test(source);
}
/** PascalCase a filename basename, stripping the extension first. */
function fileBasenameToPascalCase(basename) {
    const noExt = basename.replace(/\.(ts|js|tsx|jsx|mjs|cjs)$/, '');
    return noExt
        .split(/[-_./]+/)
        .filter((p) => p.length > 0)
        .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
        .join('');
}
/** PascalCase a single segment of a route path. */
function segmentToPascalCase(seg) {
    if (!seg)
        return '';
    return seg
        .split(/[-_]/)
        .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
        .join('');
}
/**
 * Find the matching close brace for an open brace at `openIdx`. Naive
 * brace-balancer — sufficient for handler bodies (no string-literal
 * backslash escapes to worry about that contain unbalanced braces in
 * practice). Returns -1 if unmatched.
 */
function findMatchingBrace(source, openIdx) {
    let depth = 1;
    let inString = null;
    let inLineComment = false;
    let inBlockComment = false;
    for (let i = openIdx + 1; i < source.length; i++) {
        const c = source[i];
        const n = source[i + 1];
        if (inLineComment) {
            if (c === '\n')
                inLineComment = false;
            continue;
        }
        if (inBlockComment) {
            if (c === '*' && n === '/') {
                inBlockComment = false;
                i++;
            }
            continue;
        }
        if (inString) {
            if (c === '\\') {
                i++;
                continue;
            }
            if (c === inString)
                inString = null;
            continue;
        }
        if (c === '/' && n === '/') {
            inLineComment = true;
            i++;
            continue;
        }
        if (c === '/' && n === '*') {
            inBlockComment = true;
            i++;
            continue;
        }
        if (c === '"' || c === "'" || c === '`') {
            inString = c;
            continue;
        }
        if (c === '{')
            depth++;
        else if (c === '}') {
            depth--;
            if (depth === 0)
                return i;
        }
    }
    return -1;
}
/** Find the matching close paren for an open paren at `openIdx`. */
function findMatchingParen(source, openIdx) {
    let depth = 1;
    let inString = null;
    for (let i = openIdx + 1; i < source.length; i++) {
        const c = source[i];
        if (inString) {
            if (c === '\\') {
                i++;
                continue;
            }
            if (c === inString)
                inString = null;
            continue;
        }
        if (c === '"' || c === "'" || c === '`') {
            inString = c;
            continue;
        }
        if (c === '(')
            depth++;
        else if (c === ')') {
            depth--;
            if (depth === 0)
                return i;
        }
        else if (c === '{') {
            // Skip object-literal / arrow-body braces.
            const close = findMatchingBrace(source, i);
            if (close < 0)
                return -1;
            i = close;
        }
    }
    return -1;
}
/**
 * Derive an action method name from the route definition + handler.
 * Resolution order (per file-header doc).
 */
function deriveMethodName(verb, routePath, handlerExpr, ordinal) {
    // 1. Named handler reference (NOT an inline arrow/function expression).
    const nameRefMatch = handlerExpr.match(/^([a-zA-Z_$][\w$]*)\s*$/);
    if (nameRefMatch) {
        const name = nameRefMatch[1].replace(/Handler$/, '');
        if (name && /^[a-z]/.test(name))
            return name;
    }
    // 2. Single segment, no params, lowercase-friendly.
    const cleanedPath = routePath.replace(/^\//, '').replace(/\/$/, '');
    if (cleanedPath && !/[:/]/.test(cleanedPath)) {
        return cleanedPath.replace(/[-_.]/g, '').toLowerCase();
    }
    // 3. Verb + path PascalCase. Skip params (`:id`).
    const segments = cleanedPath
        .split('/')
        .filter((s) => s.length > 0 && !s.startsWith(':'));
    if (segments.length > 0) {
        const path = segments.map(segmentToPascalCase).join('');
        return verb + path;
    }
    // 4. Fallback.
    return `${verb}${ordinal}`;
}
/** Given an ordered list of methodNames, append _N to disambiguate duplicates. */
function deduplicateMethodNames(handlers) {
    const seen = new Map();
    for (const h of handlers) {
        const count = seen.get(h.methodName) ?? 0;
        if (count > 0) {
            h.methodName = `${h.methodName}_${count + 1}`;
        }
        seen.set(h.methodName, count + 1);
    }
}
/**
 * Scan an Express router file and extract every `router.<verb>(path, handler)` call.
 * Returns one `ExpressRouteHandler` per call — handler body included verbatim.
 */
export function parseExpressRoutes(source) {
    const out = [];
    // Match `<ident>.<verb>(` — capture the verb. The leading identifier
    // is usually `router` but not always (some codebases use `app.get`,
    // `route.post`, etc.); accept any identifier the source uses.
    const re = /\b\w+\.(get|post|put|patch|delete|all|head|options)\s*\(/g;
    let m;
    let ordinal = 0;
    while ((m = re.exec(source)) !== null) {
        const verb = m[1];
        const argsOpenIdx = m.index + m[0].length - 1; // position of `(`
        const argsCloseIdx = findMatchingParen(source, argsOpenIdx);
        if (argsCloseIdx < 0)
            continue;
        const argsStr = source.slice(argsOpenIdx + 1, argsCloseIdx);
        // Find the path string (first quoted argument).
        const pathMatch = argsStr.match(/^\s*(['"`])([^'"`]+)\1/);
        if (!pathMatch)
            continue;
        const routePath = pathMatch[2];
        // Extract the handler arg — skip the path + any middleware args, take the LAST argument
        // (the one whose body we want). Handlers can be:
        //   (a) inline arrow: `async (req, res) => { ... }` or `(req, res) => expr`
        //   (b) named: `someHandler` or `asyncHandler(myFn)`
        // For our purpose, we want the BODY when (a), the NAME when (b).
        // Walk the args from start, skipping until we find the last comma at depth 0.
        let lastTopComma = -1;
        let depth = 0;
        let strChar = null;
        for (let i = 0; i < argsStr.length; i++) {
            const c = argsStr[i];
            if (strChar) {
                if (c === '\\') {
                    i++;
                    continue;
                }
                if (c === strChar)
                    strChar = null;
                continue;
            }
            if (c === '"' || c === "'" || c === '`') {
                strChar = c;
                continue;
            }
            if (c === '(' || c === '[' || c === '{')
                depth++;
            else if (c === ')' || c === ']' || c === '}')
                depth--;
            else if (c === ',' && depth === 0)
                lastTopComma = i;
        }
        if (lastTopComma < 0)
            continue;
        const handlerExpr = argsStr.slice(lastTopComma + 1).trim();
        let body = '';
        let isAsync = false;
        // (a) Inline arrow.
        const arrowMatch = handlerExpr.match(/^(async\s+)?\(([^)]*)\)\s*=>\s*\{/);
        if (arrowMatch) {
            isAsync = !!arrowMatch[1];
            // Find the opening brace of the arrow body in the ORIGINAL source.
            // The handlerExpr starts at `argsOpenIdx + 1 + lastTopComma + 1`,
            // possibly with leading whitespace. Locate the first `{` after that.
            const handlerStartInSource = argsOpenIdx + 1 + lastTopComma + 1;
            const braceIdx = source.indexOf('{', handlerStartInSource);
            if (braceIdx > 0 && braceIdx < argsCloseIdx) {
                const closeIdx = findMatchingBrace(source, braceIdx);
                if (closeIdx > 0)
                    body = source.slice(braceIdx + 1, closeIdx);
            }
        }
        else {
            // (b) Named handler — wrapped or bare. We can't see the body, but
            // extractCandidateSteps wants something to walk. Synthesize a
            // pseudo-body with a single call so it produces a `call` step.
            // The microcall LLM gets the original source location anyway.
            const wrapperMatch = handlerExpr.match(/^([a-zA-Z_$][\w$]*)\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/);
            const handlerName = wrapperMatch?.[2] ?? handlerExpr.match(/^[a-zA-Z_$][\w$]*/)?.[0];
            body = handlerName ? `${handlerName}(req, res);` : '';
            // We don't know if the named function is async without more
            // analysis; assume false — the microcall LLM will see the actual
            // declaration when authoring the body.
        }
        // Compute source line range — start at the line of `router.<verb>(`,
        // end at the line of the closing `)`.
        const startOffset = m.index;
        const endOffset = argsCloseIdx;
        const startLine = source.slice(0, startOffset).split('\n').length;
        const endLine = source.slice(0, endOffset + 1).split('\n').length;
        const methodName = deriveMethodName(verb, routePath, handlerExpr, ordinal);
        out.push({
            methodName,
            isAsync,
            body,
            sourceLineRange: [startLine, endLine],
            httpVerb: verb,
            routePath,
        });
        ordinal++;
    }
    deduplicateMethodNames(out);
    return out;
}
/**
 * Wrap a list of handlers in a synthetic class body. The output is
 * suitable for `walkMethodBodies` / `extractCandidateSteps` to consume.
 * Each handler becomes a `<methodName>(req, res) { <body> }` member.
 */
export function synthesizeControllerClass(controllerName, handlers) {
    const lines = [];
    lines.push(`class ${controllerName} {`);
    for (const h of handlers) {
        const asyncWord = h.isAsync ? 'async ' : '';
        lines.push(`  ${asyncWord}${h.methodName}(req, res) {`);
        // Preserve body indentation reasonably; avoid empty lines breaking
        // the regex-based walker.
        const bodyLines = h.body.split('\n').map((l) => '  ' + l);
        lines.push(...bodyLines);
        lines.push('  }');
    }
    lines.push('}');
    return lines.join('\n');
}
/** Derive a controller name from a route file's basename. */
export function deriveControllerName(filePath) {
    const basename = filePath.split('/').pop() ?? filePath;
    const pascal = fileBasenameToPascalCase(basename);
    return /Controller$/.test(pascal) ? pascal : `${pascal}Controller`;
}
//# sourceMappingURL=express-routes.js.map