/**
 * Agent Discovery - READ-ONLY AST analysis
 *
 * Discovers agents by scanning src/agent/**\/*.ts files
 * Extracts metadata WITHOUT mutating source files
 */
import * as acornLoose from 'acorn-loose';
import { generate } from 'astring';
import { dirname, join, relative } from 'node:path';
import { existsSync } from 'node:fs';
import { formatSchemaCode } from '../format-schema';
import { toForwardSlash } from '../../../utils/normalize-path';
/**
 * Build a file-local identifier resolver that maps top-level variable names
 * to their initializer AST nodes. This allows resolving schema variable references.
 */
function buildIdentifierResolver(program) {
    const initMap = new Map();
    for (const node of program.body) {
        // const x = ... or let x = ... or var x = ...
        if (node.type === 'VariableDeclaration') {
            const decl = node;
            for (const d of decl.declarations) {
                if (d.id.type === 'Identifier' && d.init) {
                    const id = d.id;
                    initMap.set(id.name, d.init);
                }
            }
        }
        // export const x = ...
        if (node.type === 'ExportNamedDeclaration') {
            const exp = node;
            if (exp.declaration && exp.declaration.type === 'VariableDeclaration') {
                const decl = exp.declaration;
                for (const d of decl.declarations) {
                    if (d.id.type === 'Identifier' && d.init) {
                        const id = d.id;
                        initMap.set(id.name, d.init);
                    }
                }
            }
        }
    }
    return (name) => initMap.get(name);
}
/**
 * Get the property name from an AST node (Identifier or Literal).
 */
function getPropertyName(node) {
    if (node.type === 'Identifier') {
        return node.name;
    }
    if (node.type === 'Literal') {
        const lit = node;
        return typeof lit.value === 'string' ? lit.value : undefined;
    }
    return undefined;
}
/**
 * Resolve an expression by following identifier references and member access chains.
 * Applies a recursion limit to prevent infinite loops from cyclic references.
 *
 * Supported patterns:
 * - Identifiers: `AgentInput` -> resolves to variable definition
 * - Member access: `configs.agent1.schema` -> traverses object literals
 */
function resolveExpression(node, resolveIdentifier, depth = 0) {
    if (!node)
        return node;
    if (depth > 8)
        return node; // Prevent cycles / deep alias chains
    // Follow identifiers to their definitions
    if (node.type === 'Identifier') {
        const id = node;
        const resolved = resolveIdentifier(id.name);
        if (resolved) {
            return resolveExpression(resolved, resolveIdentifier, depth + 1);
        }
    }
    // Follow member expressions: configs.agent1.schema, baseSchemas.shared
    if (node.type === 'MemberExpression') {
        const memberExpr = node;
        // Skip computed properties like configs[agentName]
        if (memberExpr.computed)
            return node;
        const propName = getPropertyName(memberExpr.property);
        if (!propName)
            return node;
        // First resolve the object side (e.g., configs -> { agent1: {...} })
        const resolvedObj = resolveExpression(memberExpr.object, resolveIdentifier, depth + 1);
        // If we got an object literal, look up the property
        if (resolvedObj.type === 'ObjectExpression') {
            const obj = resolvedObj;
            for (const prop of obj.properties) {
                // Skip spread elements
                if (!prop || !('key' in prop) || !prop.key)
                    continue;
                const keyName = getPropertyName(prop.key);
                if (keyName === propName && prop.value) {
                    // Recurse into the property value
                    return resolveExpression(prop.value, resolveIdentifier, depth + 1);
                }
            }
        }
        // Couldn't resolve - return original node
        return node;
    }
    return node;
}
/**
 * Hash function for generating stable IDs
 */
function hash(...val) {
    const hasher = new Bun.CryptoHasher('sha256');
    val.forEach((v) => hasher.update(v));
    return hasher.digest().toHex();
}
function hashSHA1(...val) {
    const hasher = new Bun.CryptoHasher('sha1');
    val.forEach((v) => hasher.update(v));
    return hasher.digest().toHex();
}
function getAgentId(projectId, deploymentId, filename, version) {
    return `agentid_${hashSHA1(projectId, deploymentId, filename, version)}`;
}
function generateStableAgentId(projectId, name) {
    return `agent_${hashSHA1(projectId, name)}`.substring(0, 64);
}
function getEvalId(projectId, deploymentId, filename, name, version) {
    return `evalid_${hashSHA1(projectId, deploymentId, filename, name, version)}`;
}
function generateStableEvalId(projectId, agentId, name) {
    return `eval_${hashSHA1(projectId, agentId, name)}`.substring(0, 64);
}
/**
 * Check if a property key matches a given name.
 * Handles both Identifier keys (schema) and Literal keys ('schema').
 */
function isKeyNamed(prop, name) {
    if (!prop || !prop.key)
        return false;
    if (prop.key.type === 'Identifier') {
        return prop.key.name === name;
    }
    if (prop.key.type === 'Literal') {
        const lit = prop.key;
        return typeof lit.value === 'string' && lit.value === name;
    }
    return false;
}
/**
 * Extract schema code from createAgent call arguments.
 * Resolves variable references to their actual definitions when possible.
 *
 * Supported patterns:
 * - Inline schema: `schema: { input: s.object({...}), output: s.object({...}) }`
 * - Variable reference: `schema: { input: AgentInput, output: AgentOutput }`
 * - Schema object variable: `schema: schemaVar` where schemaVar is a top-level const
 * - Shorthand: `schema: { input, output }` where input/output are top-level consts
 *
 * Unsupported patterns (returns empty or partial result):
 * - Config alias: `createAgent('x', configVar)` - config must be inline object
 * - Schema from member access: `schema: configs.agent1.schema`
 * - Schema from function call: `schema: getSchema()`
 * - Destructured variables: `const { schema } = config`
 * - Cross-file imports (falls back to identifier name)
 */
function extractSchemaCode(callargexp, resolveIdentifier) {
    let schemaObj;
    // Find the schema property
    for (const prop of callargexp.properties) {
        // Skip spread elements or any non-Property nodes
        if (!prop || !('key' in prop) || !prop.key)
            continue;
        if (isKeyNamed(prop, 'schema')) {
            // Resolve the schema value if it's an identifier (e.g., schema: schemaVar)
            let valueNode = prop.value;
            valueNode = resolveExpression(valueNode, resolveIdentifier);
            if (valueNode.type === 'ObjectExpression') {
                schemaObj = valueNode;
                break;
            }
        }
    }
    if (!schemaObj) {
        return {};
    }
    let inputSchemaCode;
    let outputSchemaCode;
    // Extract input and output schema code
    for (const prop of schemaObj.properties) {
        // Skip spread elements or any non-Property nodes
        if (!prop || !('key' in prop) || !prop.key)
            continue;
        if (isKeyNamed(prop, 'input') && prop.value) {
            // Resolve variable reference if the value is an identifier
            const resolvedValue = resolveExpression(prop.value, resolveIdentifier);
            inputSchemaCode = formatSchemaCode(generate(resolvedValue));
        }
        else if (isKeyNamed(prop, 'output') && prop.value) {
            // Resolve variable reference if the value is an identifier
            const resolvedValue = resolveExpression(prop.value, resolveIdentifier);
            outputSchemaCode = formatSchemaCode(generate(resolvedValue));
        }
    }
    return { inputSchemaCode, outputSchemaCode };
}
/**
 * Parse object expression to extract metadata
 */
function parseObjectExpressionToMap(expr) {
    const result = new Map();
    for (const prop of expr.properties) {
        if (prop.value.type === 'Literal') {
            const value = prop.value;
            result.set(prop.key.name, String(value.value));
        }
    }
    return result;
}
/**
 * Extract metadata from createAgent call (READ-ONLY)
 */
function extractAgentMetadata(code, filename, projectId, deploymentId) {
    const ast = acornLoose.parse(code, {
        ecmaVersion: 'latest',
        sourceType: 'module',
    });
    // Build identifier resolver for resolving schema variable references
    const resolveIdentifier = buildIdentifierResolver(ast);
    // Calculate file version (hash of contents)
    const version = hash(code);
    // Find createAgent calls
    for (const node of ast.body) {
        if (node.type === 'ExportDefaultDeclaration') {
            const declaration = node.declaration;
            if (declaration.type === 'CallExpression') {
                const callExpr = declaration;
                if (callExpr.callee.type === 'Identifier' &&
                    callExpr.callee.name === 'createAgent' &&
                    callExpr.arguments.length >= 2) {
                    // First arg is agent name
                    const nameArg = callExpr.arguments[0];
                    const name = String(nameArg.value);
                    // Second arg is config object
                    const callargexp = callExpr.arguments[1];
                    // Extract schemas (with variable resolution)
                    const { inputSchemaCode, outputSchemaCode } = extractSchemaCode(callargexp, resolveIdentifier);
                    // Extract description from either direct property or metadata object
                    let description;
                    for (const prop of callargexp.properties) {
                        // Check for direct description property
                        if (prop.key.name === 'description' && prop.value.type === 'Literal') {
                            description = String(prop.value.value);
                            break; // Direct description takes precedence
                        }
                        // Also check metadata.description for backwards compat
                        if (prop.key.name === 'metadata' && prop.value.type === 'ObjectExpression') {
                            const metadataMap = parseObjectExpressionToMap(prop.value);
                            if (!description) {
                                description = metadataMap.get('description');
                            }
                            break;
                        }
                    }
                    // Generate IDs
                    const id = getAgentId(projectId, deploymentId, filename, version);
                    const agentId = generateStableAgentId(projectId, name);
                    return {
                        filename,
                        name,
                        id,
                        agentId,
                        version,
                        description,
                        inputSchemaCode,
                        outputSchemaCode,
                    };
                }
            }
        }
        // Also check variable declarations (e.g., const agent = createAgent(...))
        if (node.type === 'VariableDeclaration') {
            const declarations = node
                .declarations;
            for (const decl of declarations) {
                if (decl.init && decl.init.type === 'CallExpression') {
                    const callExpr = decl.init;
                    if (callExpr.callee.type === 'Identifier' &&
                        callExpr.callee.name === 'createAgent' &&
                        callExpr.arguments.length >= 2) {
                        const nameArg = callExpr.arguments[0];
                        const name = String(nameArg.value);
                        const callargexp = callExpr.arguments[1];
                        const { inputSchemaCode, outputSchemaCode } = extractSchemaCode(callargexp, resolveIdentifier);
                        let description;
                        for (const prop of callargexp.properties) {
                            // Check for direct description property
                            if (prop.key.name === 'description' && prop.value.type === 'Literal') {
                                description = String(prop.value.value);
                                break; // Direct description takes precedence
                            }
                            // Also check metadata.description for backwards compat
                            if (prop.key.name === 'metadata' && prop.value.type === 'ObjectExpression') {
                                const metadataMap = parseObjectExpressionToMap(prop.value);
                                if (!description) {
                                    description = metadataMap.get('description');
                                }
                                break;
                            }
                        }
                        const id = getAgentId(projectId, deploymentId, filename, version);
                        const agentId = generateStableAgentId(projectId, name);
                        return {
                            filename,
                            name,
                            id,
                            agentId,
                            version,
                            description,
                            inputSchemaCode,
                            outputSchemaCode,
                        };
                    }
                }
            }
        }
    }
    return null;
}
/**
 * Extract evals from a file (READ-ONLY)
 * Finds createEval calls regardless of whether they're exported or not
 */
async function extractEvalMetadata(evalsPath, relativeEvalsPath, agentId, projectId, deploymentId, logger) {
    const evalsFile = Bun.file(evalsPath);
    if (!(await evalsFile.exists())) {
        return [];
    }
    try {
        const evalsSource = await evalsFile.text();
        return extractEvalsFromSource(evalsSource, relativeEvalsPath, agentId, projectId, deploymentId, logger);
    }
    catch (error) {
        logger.warn(`Failed to parse evals from ${evalsPath}: ${error}`);
        return [];
    }
}
/**
 * Extract evals from source code (READ-ONLY)
 * Finds all createEval calls in the source, exported or not
 */
function extractEvalsFromSource(source, filename, agentId, projectId, deploymentId, logger) {
    // Quick check - skip if no createEval in source
    if (!source.includes('createEval')) {
        return [];
    }
    try {
        const transpiler = new Bun.Transpiler({ loader: 'ts', target: 'bun' });
        const contents = transpiler.transformSync(source);
        const version = hash(contents);
        const ast = acornLoose.parse(contents, { ecmaVersion: 'latest', sourceType: 'module' });
        const evals = [];
        // Recursively find all createEval calls in the AST
        function findCreateEvalCalls(node) {
            if (!node || typeof node !== 'object')
                return;
            const n = node;
            // Check if this is a createEval call (either direct or method call)
            // Direct: createEval('name', {...})
            // Method: agent.createEval('name', {...})
            let isCreateEvalCall = false;
            if (n.type === 'CallExpression' && n.callee && typeof n.callee === 'object') {
                const callee = n.callee;
                // Direct function call: createEval(...)
                if (callee.type === 'Identifier' &&
                    callee.name === 'createEval') {
                    isCreateEvalCall = true;
                }
                // Method call: someAgent.createEval(...)
                if (callee.type === 'MemberExpression' &&
                    callee.property &&
                    callee.property.type === 'Identifier' &&
                    callee.property.name === 'createEval') {
                    isCreateEvalCall = true;
                }
            }
            if (isCreateEvalCall) {
                const callExpr = n;
                let evalName;
                let description;
                if (callExpr.arguments.length >= 2) {
                    // Format: agent.createEval('name', { config })
                    const nameArg = callExpr.arguments[0];
                    evalName = String(nameArg.value);
                    const callargexp = callExpr.arguments[1];
                    if (callargexp.properties) {
                        for (const prop of callargexp.properties) {
                            if (prop.key.name === 'metadata' && prop.value.type === 'ObjectExpression') {
                                const metadataMap = parseObjectExpressionToMap(prop.value);
                                description = metadataMap.get('description');
                                break;
                            }
                        }
                    }
                }
                else if (callExpr.arguments.length === 1) {
                    // Format: agent.createEval(presetEval({ name: '...', ... }))
                    // or: agent.createEval(presetEval()) - uses preset's default name
                    // or: agent.createEval({ name: '...', ... })
                    const arg = callExpr.arguments[0];
                    // Handle CallExpression: presetEval({ name: '...' }) or presetEval()
                    if (arg.type === 'CallExpression') {
                        const innerCall = arg;
                        // Try to get name from the call arguments first
                        if (innerCall.arguments.length >= 1) {
                            const configArg = innerCall.arguments[0];
                            if (configArg.type === 'ObjectExpression' && configArg.properties) {
                                const configMap = parseObjectExpressionToMap(configArg);
                                evalName = configMap.get('name');
                                description = configMap.get('description');
                            }
                        }
                        // Fallback: use the callee name as the eval name (e.g., politeness())
                        if (!evalName && innerCall.callee) {
                            const callee = innerCall.callee;
                            if (callee.type === 'Identifier') {
                                evalName = callee.name;
                            }
                        }
                    }
                    // Handle ObjectExpression: { name: '...', handler: ... }
                    if (arg.type === 'ObjectExpression') {
                        const configArg = arg;
                        if (configArg.properties) {
                            const configMap = parseObjectExpressionToMap(configArg);
                            evalName = configMap.get('name');
                            description = configMap.get('description');
                        }
                    }
                }
                if (evalName) {
                    const id = getEvalId(projectId, deploymentId, filename, evalName, version);
                    const identifier = generateStableEvalId(projectId, agentId, evalName);
                    logger.trace(`Found eval '${evalName}' in ${filename} (identifier: ${identifier})`);
                    evals.push({
                        id,
                        identifier,
                        name: evalName,
                        filename,
                        version,
                        description,
                        agentIdentifier: agentId,
                        projectId,
                    });
                }
            }
            // Recursively search child nodes
            for (const key of Object.keys(n)) {
                const value = n[key];
                if (Array.isArray(value)) {
                    for (const item of value) {
                        findCreateEvalCalls(item);
                    }
                }
                else if (value && typeof value === 'object') {
                    findCreateEvalCalls(value);
                }
            }
        }
        findCreateEvalCalls(ast);
        return evals;
    }
    catch (error) {
        logger.warn(`Failed to parse evals from ${filename}: ${error}`);
        return [];
    }
}
/**
 * Discover all agents in src/agent directory (READ-ONLY)
 */
export async function discoverAgents(srcDir, projectId, deploymentId, logger) {
    const agentsDir = join(srcDir, 'agent');
    const agents = [];
    // Check if agent directory exists
    if (!existsSync(agentsDir)) {
        logger.trace('No agent directory found at %s', agentsDir);
        return agents;
    }
    const transpiler = new Bun.Transpiler({ loader: 'ts', target: 'bun' });
    // Scan all .ts files in agent directory
    const glob = new Bun.Glob('**/*.ts');
    for await (const file of glob.scan(agentsDir)) {
        const filePath = join(agentsDir, file);
        // Skip eval.ts files (processed separately)
        if (file.endsWith('/eval.ts') || file === 'eval.ts') {
            continue;
        }
        try {
            const source = await Bun.file(filePath).text();
            const contents = transpiler.transformSync(source);
            // Use 'src/' prefix for consistency with bun bundler and registry imports
            const rootDir = join(srcDir, '..');
            const relativeFilename = toForwardSlash(relative(rootDir, filePath));
            const agentMetadata = extractAgentMetadata(contents, relativeFilename, projectId, deploymentId);
            if (agentMetadata) {
                logger.trace('Discovered agent: %s at %s', agentMetadata.name, relativeFilename);
                // Collect evals from multiple sources
                const allEvals = [];
                // 1. Extract evals from the agent file itself (agent.createEval() pattern)
                const evalsInAgentFile = extractEvalsFromSource(source, relativeFilename, agentMetadata.agentId, projectId, deploymentId, logger);
                if (evalsInAgentFile.length > 0) {
                    logger.trace('Found %d eval(s) in agent file for %s', evalsInAgentFile.length, agentMetadata.name);
                    allEvals.push(...evalsInAgentFile);
                }
                // 2. Check for evals in separate eval.ts file in same directory
                const agentDir = dirname(filePath);
                const evalsPath = join(agentDir, 'eval.ts');
                const relativeEvalsPath = toForwardSlash(relative(rootDir, evalsPath));
                const evalsInSeparateFile = await extractEvalMetadata(evalsPath, relativeEvalsPath, agentMetadata.agentId, projectId, deploymentId, logger);
                if (evalsInSeparateFile.length > 0) {
                    logger.trace('Found %d eval(s) in eval.ts for agent %s', evalsInSeparateFile.length, agentMetadata.name);
                    allEvals.push(...evalsInSeparateFile);
                }
                if (allEvals.length > 0) {
                    agentMetadata.evals = allEvals;
                    logger.trace('Total %d eval(s) for agent %s', allEvals.length, agentMetadata.name);
                }
                agents.push(agentMetadata);
            }
        }
        catch (error) {
            logger.warn(`Failed to parse agent file ${filePath}: ${error}`);
        }
    }
    logger.debug('Discovered %d agent(s)', agents.length);
    return agents;
}
//# sourceMappingURL=agent-discovery.js.map