/**
 * V2 → V3 pattern detection.
 *
 * Analyses a project directory and returns a structured report of every v2
 * artefact that needs to be migrated to v3 (framework-agnostic Hono).
 * No files are modified here.
 */
import { existsSync, readdirSync } from 'node:fs';
import { join, relative, resolve } from 'node:path';
import ts from 'typescript';
// ---------------------------------------------------------------------------
// Known service names
// ---------------------------------------------------------------------------
const SERVICE_NAMES = [
    'kv',
    'vector',
    'stream',
    'queue',
    'email',
    'task',
    'schedule',
    'sandbox',
    'logger',
];
/** Map from service name to package */
export const SERVICE_PACKAGE_MAP = {
    kv: { pkg: '@agentuity/keyvalue', client: 'KeyValueClient' },
    vector: { pkg: '@agentuity/vector', client: 'VectorClient' },
    stream: { pkg: '@agentuity/stream', client: 'StreamClient' },
    queue: { pkg: '@agentuity/queue', client: 'QueueClient' },
    email: { pkg: '@agentuity/email', client: 'EmailClient' },
    task: { pkg: '@agentuity/task', client: 'TaskClient' },
    schedule: { pkg: '@agentuity/schedule', client: 'ScheduleClient' },
    sandbox: { pkg: '@agentuity/sandbox', client: 'SandboxClient' },
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function rel(projectDir, abs) {
    return relative(projectDir, abs);
}
function* walkFiles(dir, exts) {
    if (!existsSync(dir))
        return;
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
        const full = join(dir, entry.name);
        if (entry.isDirectory()) {
            if (['node_modules', 'dist', '.agentuity', '.git'].includes(entry.name))
                continue;
            yield* walkFiles(full, exts);
        }
        else if (entry.isFile() && exts.some((e) => entry.name.endsWith(e))) {
            yield full;
        }
    }
}
async function parseTs(filePath) {
    const src = await Bun.file(filePath).text();
    return ts.createSourceFile(filePath, src, ts.ScriptTarget.ESNext, true);
}
// ---------------------------------------------------------------------------
// AST analysis helpers
// ---------------------------------------------------------------------------
/**
 * Get properties passed to createApp() call.
 */
function getCreateAppProps(sourceFile) {
    const props = [];
    function visit(node) {
        if (ts.isCallExpression(node) &&
            ts.isIdentifier(node.expression) &&
            node.expression.text === 'createApp') {
            const arg = node.arguments[0];
            if (arg && ts.isObjectLiteralExpression(arg)) {
                for (const prop of arg.properties) {
                    if ((ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
                        ts.isIdentifier(prop.name)) {
                        props.push(prop.name.text);
                    }
                }
            }
        }
        ts.forEachChild(node, visit);
    }
    visit(sourceFile);
    return props;
}
/**
 * Check if a source file imports createApp from @agentuity/runtime.
 */
function hasCreateAppImport(sourceFile) {
    let found = false;
    function visit(node) {
        if (found)
            return;
        if (ts.isImportDeclaration(node)) {
            const moduleSpecifier = node.moduleSpecifier.text;
            if (moduleSpecifier === '@agentuity/runtime') {
                const namedBindings = node.importClause?.namedBindings;
                if (namedBindings && ts.isNamedImports(namedBindings)) {
                    for (const element of namedBindings.elements) {
                        if (element.name.text === 'createApp') {
                            found = true;
                            return;
                        }
                    }
                }
            }
        }
        ts.forEachChild(node, visit);
    }
    visit(sourceFile);
    return found;
}
/**
 * Analyse an agent file for complexity classification.
 */
function analyseAgentFile(sourceFile) {
    let agentName = null;
    let hasSchema = false;
    let hasSetup = false;
    let hasShutdown = false;
    let hasOnEvent = false;
    let hasModuleLevelCode = false;
    let hasConfigAccess = false;
    let hasAppAccess = false;
    const ctxServices = new Set();
    // Track what's at module level vs inside createAgent
    let insideCreateAgent = false;
    function visitCreateAgentConfig(node) {
        // Inside the config object of createAgent
        if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
            const propName = node.name.text;
            if (propName === 'schema')
                hasSchema = true;
            if (propName === 'setup')
                hasSetup = true;
            if (propName === 'shutdown')
                hasShutdown = true;
            if (propName === 'on' ||
                propName === 'onStarted' ||
                propName === 'onCompleted' ||
                propName === 'onErrored') {
                hasOnEvent = true;
            }
        }
        // Look for ctx.kv, ctx.vector, etc. inside handler
        if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
            const serviceName = node.name.text;
            if (SERVICE_NAMES.includes(serviceName)) {
                // Check if accessing on a parameter-like identifier (ctx, context, c)
                if (ts.isIdentifier(node.expression)) {
                    const objName = node.expression.text;
                    if (['ctx', 'context', 'c'].includes(objName)) {
                        ctxServices.add(serviceName);
                    }
                }
            }
            // Check for ctx.config access
            if (node.name.text === 'config' && ts.isIdentifier(node.expression)) {
                if (['ctx', 'context'].includes(node.expression.text)) {
                    hasConfigAccess = true;
                }
            }
            // Check for ctx.app access
            if (node.name.text === 'app' && ts.isIdentifier(node.expression)) {
                if (['ctx', 'context'].includes(node.expression.text)) {
                    hasAppAccess = true;
                }
            }
        }
        ts.forEachChild(node, visitCreateAgentConfig);
    }
    function visit(node) {
        // Detect createAgent() call
        if (ts.isCallExpression(node) &&
            ts.isIdentifier(node.expression) &&
            node.expression.text === 'createAgent') {
            // First arg is the name
            if (node.arguments[0] && ts.isStringLiteral(node.arguments[0])) {
                agentName = node.arguments[0].text;
            }
            // Second arg is the config
            if (node.arguments[1]) {
                insideCreateAgent = true;
                visitCreateAgentConfig(node.arguments[1]);
                insideCreateAgent = false;
            }
            return; // Don't recurse into createAgent children again
        }
        // Check for module-level statements that aren't just imports/exports/type declarations
        if (!insideCreateAgent && isModuleLevelCode(node, sourceFile)) {
            hasModuleLevelCode = true;
        }
        ts.forEachChild(node, visit);
    }
    visit(sourceFile);
    // Classify complexity
    const complexityReasons = [];
    if (hasSetup)
        complexityReasons.push('has setup() lifecycle hook');
    if (hasShutdown)
        complexityReasons.push('has shutdown() lifecycle hook');
    if (hasOnEvent)
        complexityReasons.push('has event listeners');
    if (hasConfigAccess)
        complexityReasons.push('accesses ctx.config (from setup)');
    if (hasAppAccess)
        complexityReasons.push('accesses ctx.app (app state)');
    if (hasModuleLevelCode)
        complexityReasons.push('has module-level code beyond imports/exports');
    const complexity = complexityReasons.length > 0 ? 'complex' : 'simple';
    return {
        name: agentName,
        complexity,
        complexityReason: complexityReasons.length > 0 ? complexityReasons.join('; ') : undefined,
        hasSchema,
        ctxServices: [...ctxServices],
    };
}
/**
 * Check if a node is "module-level code" (not an import, export, type, or interface).
 */
function isModuleLevelCode(node, sourceFile) {
    // Only check top-level statements
    if (node.parent !== sourceFile)
        return false;
    // End-of-file token is not code
    if (node.kind === ts.SyntaxKind.EndOfFileToken)
        return false;
    // Imports are fine
    if (ts.isImportDeclaration(node))
        return false;
    // Type-only declarations are fine
    if (ts.isTypeAliasDeclaration(node))
        return false;
    if (ts.isInterfaceDeclaration(node))
        return false;
    // Export default createAgent(...) is fine
    if (ts.isExportAssignment(node))
        return false;
    // Export declarations (re-exports) are fine
    if (ts.isExportDeclaration(node))
        return false;
    // Variable statements
    if (ts.isVariableStatement(node)) {
        // `const` declarations are fine — they're just data, schemas, or config.
        // They'll be preserved alongside the extracted function.
        if (node.declarationList.flags & ts.NodeFlags.Const ||
            node.declarationList.flags & ts.NodeFlags.Using) {
            return false;
        }
        // `let`/`var` with createAgent() is fine
        const declarations = node.declarationList.declarations;
        if (declarations.length === 1) {
            const decl = declarations[0];
            const init = decl?.initializer;
            if (init &&
                ts.isCallExpression(init) &&
                ts.isIdentifier(init.expression) &&
                init.expression.text === 'createAgent') {
                return false;
            }
            if (init &&
                ts.isAwaitExpression(init) &&
                ts.isCallExpression(init.expression) &&
                ts.isIdentifier(init.expression.expression) &&
                init.expression.expression.text === 'createAgent') {
                return false;
            }
        }
        // `let`/`var` with mutable state → complex
        return true;
    }
    // Enum declarations are fine
    if (ts.isEnumDeclaration(node))
        return false;
    // Function declarations are fine — they're just helper functions
    // that will be preserved alongside the extracted agent function.
    if (ts.isFunctionDeclaration(node))
        return false;
    // Expression statements that are just createAgent() exports are fine
    if (ts.isExpressionStatement(node)) {
        const expr = node.expression;
        if (ts.isCallExpression(expr) &&
            ts.isIdentifier(expr.expression) &&
            expr.expression.text === 'createAgent') {
            return false;
        }
    }
    // Everything else is module-level code that indicates complexity
    return true;
}
/**
 * Scan a source file for service usage patterns: c.var.kv, c.var.vector, etc.
 */
function scanServiceUsageInRouteFile(sourceFile) {
    const services = new Set();
    function visit(node) {
        // Match c.var.kv, c.var.vector, etc.
        if (ts.isPropertyAccessExpression(node) &&
            ts.isIdentifier(node.name) &&
            SERVICE_NAMES.includes(node.name.text)) {
            // Check it's *.var.service
            if (ts.isPropertyAccessExpression(node.expression) &&
                ts.isIdentifier(node.expression.name) &&
                node.expression.name.text === 'var') {
                services.add(node.name.text);
            }
        }
        ts.forEachChild(node, visit);
    }
    visit(sourceFile);
    return [...services];
}
// ---------------------------------------------------------------------------
// Main detector
// ---------------------------------------------------------------------------
export async function detectV3(projectDir) {
    const absDir = resolve(projectDir);
    const findings = [];
    const result = {
        projectDir: absDir,
        findings,
        hasRuntimeDep: false,
        hasCreateApp: false,
        createAppProps: [],
        agentFiles: [],
        hasAgentBarrel: false,
        serviceUsages: [],
        allServicesUsed: [],
        hasFrontend: false,
        hasAgentuityConfig: false,
        hasViteConfig: false,
        outdatedPackages: [],
        hasReactPackage: false,
        hasFrontendPackage: false,
    };
    // ── 1. package.json analysis ────────────────────────────────────────────
    const packageJsonPath = join(absDir, 'package.json');
    if (existsSync(packageJsonPath)) {
        try {
            const packageJson = JSON.parse(await Bun.file(packageJsonPath).text());
            for (const section of ['dependencies', 'devDependencies']) {
                const deps = packageJson[section];
                if (!deps || typeof deps !== 'object')
                    continue;
                for (const [name, version] of Object.entries(deps)) {
                    if (name === '@agentuity/runtime') {
                        result.hasRuntimeDep = true;
                        result.runtimeVersion = String(version);
                    }
                    if (name === '@agentuity/react') {
                        result.hasReactPackage = true;
                    }
                    if (name === '@agentuity/frontend') {
                        result.hasFrontendPackage = true;
                    }
                    // Check for outdated packages
                    if (name.startsWith('@agentuity/')) {
                        const versionStr = String(version);
                        const needsUpdate = versionStr === 'latest' ||
                            versionStr === '*' ||
                            versionStr.startsWith('workspace:') ||
                            /^[~^]?[12]\./.test(versionStr);
                        if (needsUpdate) {
                            result.outdatedPackages.push({
                                name,
                                currentVersion: versionStr,
                                section,
                            });
                        }
                    }
                }
            }
        }
        catch {
            // Ignore parse errors
        }
    }
    if (result.hasRuntimeDep) {
        findings.push({
            id: 'v3-runtime-dep',
            severity: 'auto',
            message: `@agentuity/runtime@${result.runtimeVersion} found — will be removed`,
            file: 'package.json',
            hint: 'v3 is framework-agnostic. @agentuity/runtime is replaced by:\n' +
                '  • hono — the web framework\n' +
                '  • @agentuity/hono — middleware for telemetry + services\n' +
                '  • Individual service packages (@agentuity/keyvalue, etc.)',
        });
    }
    if (result.outdatedPackages.length > 0) {
        const packageList = result.outdatedPackages
            .map((p) => `${p.name}@${p.currentVersion}`)
            .join(', ');
        findings.push({
            id: 'v3-outdated-packages',
            severity: 'auto',
            message: `Outdated @agentuity/* packages: ${packageList}`,
            file: 'package.json',
            hint: 'All @agentuity/* packages will be updated to their v3 versions.',
        });
    }
    // ── 2. app.ts / entry point ─────────────────────────────────────────────
    const appTsPath = join(absDir, 'app.ts');
    if (existsSync(appTsPath)) {
        result.appTsPath = appTsPath;
        const sourceFile = await parseTs(appTsPath);
        result.hasCreateApp = hasCreateAppImport(sourceFile);
        if (result.hasCreateApp) {
            result.createAppProps = getCreateAppProps(sourceFile);
            findings.push({
                id: 'v3-createapp',
                severity: 'auto',
                message: 'app.ts uses createApp() from @agentuity/runtime',
                file: 'app.ts',
                hint: 'Will be rewritten to a plain Hono app at src/index.ts:\n' +
                    '\n' +
                    "  import { Hono } from 'hono';\n" +
                    "  import { agentuity } from '@agentuity/hono';\n" +
                    '\n' +
                    '  const app = new Hono();\n' +
                    "  app.use('*', agentuity());\n" +
                    '\n' +
                    '  export default app;',
            });
            // Check for cors config
            if (result.createAppProps.includes('cors')) {
                findings.push({
                    id: 'v3-cors-config',
                    severity: 'guided',
                    message: 'createApp() has cors configuration',
                    file: 'app.ts',
                    hint: 'CORS config will be migrated to hono/cors middleware.\n' +
                        "Note: Agentuity-specific options like 'sameOrigin' are not available\n" +
                        'in hono/cors. You may need to configure allowedOrigins manually.',
                });
            }
            // Check for agents reference
            if (result.createAppProps.includes('agents')) {
                findings.push({
                    id: 'v3-agents-in-createapp',
                    severity: 'auto',
                    message: 'createApp() passes agents array — concept removed in v3',
                    file: 'app.ts',
                    hint: 'Agents are converted to plain functions. The agents import will be removed.',
                });
            }
        }
        // Also scan app.ts for service usage
        const appServices = scanServiceUsageInRouteFile(sourceFile);
        if (appServices.length > 0) {
            result.serviceUsages.push({
                path: appTsPath,
                relativePath: 'app.ts',
                services: appServices,
                accessPattern: 'c.var',
            });
        }
    }
    // ── 3. Agent files ──────────────────────────────────────────────────────
    const agentDir = join(absDir, 'src', 'agent');
    if (existsSync(agentDir)) {
        for (const file of walkFiles(agentDir, ['.ts', '.tsx'])) {
            // Skip ONLY the top-level src/agent/index.ts barrel. Inside subdirectories,
            // index.ts is the agent itself, e.g. src/agent/translate/index.ts.
            if (file === join(agentDir, 'index.ts') || file === join(agentDir, 'index.tsx')) {
                continue;
            }
            const sourceFile = await parseTs(file);
            const src = await Bun.file(file).text();
            // Check if this file uses createAgent
            if (!src.includes('createAgent'))
                continue;
            const analysis = analyseAgentFile(sourceFile);
            if (!analysis.name)
                continue;
            const relPath = rel(absDir, file);
            const agentFile = {
                path: file,
                relativePath: relPath,
                name: analysis.name,
                complexity: analysis.complexity,
                complexityReason: analysis.complexityReason,
                hasSchema: analysis.hasSchema,
                ctxServices: analysis.ctxServices,
            };
            result.agentFiles.push(agentFile);
            if (analysis.complexity === 'simple') {
                findings.push({
                    id: `v3-agent-simple:${relPath}`,
                    severity: 'auto',
                    message: `Agent "${analysis.name}" is simple — will be converted to plain function`,
                    file: relPath,
                    hint: 'The createAgent() wrapper will be removed. The handler becomes a plain\n' +
                        'exported async function.' +
                        (analysis.hasSchema
                            ? ' Schema validation will be preserved in the function.'
                            : ''),
                });
            }
            else {
                findings.push({
                    id: `v3-agent-complex:${relPath}`,
                    severity: 'manual',
                    message: `Agent "${analysis.name}" is complex — requires manual migration`,
                    file: relPath,
                    hint: `Complexity: ${analysis.complexityReason}\n` +
                        '\n' +
                        'This agent uses features beyond a simple handler and cannot be\n' +
                        'automatically converted. You need to:\n' +
                        '  1. Extract the handler into a plain async function\n' +
                        '  2. Move setup logic to module-level initialization\n' +
                        '  3. Replace ctx.config/ctx.app with direct imports\n' +
                        '  4. Remove event listeners (use your own event patterns)',
                });
            }
            // Track service usage from agents
            if (analysis.ctxServices.length > 0) {
                result.serviceUsages.push({
                    path: file,
                    relativePath: relPath,
                    services: analysis.ctxServices,
                    accessPattern: 'ctx',
                });
            }
        }
    }
    // Agent barrel
    const agentBarrelPath = join(absDir, 'src', 'agent', 'index.ts');
    result.hasAgentBarrel = existsSync(agentBarrelPath);
    if (result.hasAgentBarrel) {
        findings.push({
            id: 'v3-agent-barrel',
            severity: 'auto',
            message: 'src/agent/index.ts barrel — will be removed',
            file: 'src/agent/index.ts',
            hint: 'The agents barrel exported an array of agents for createApp().\n' +
                'In v3, there is no agents concept — functions are imported directly\n' +
                'where needed.',
        });
    }
    // ── 4. Route files — service usage ──────────────────────────────────────
    const apiDir = join(absDir, 'src', 'api');
    if (existsSync(apiDir)) {
        for (const file of walkFiles(apiDir, ['.ts', '.tsx'])) {
            const sourceFile = await parseTs(file);
            const services = scanServiceUsageInRouteFile(sourceFile);
            if (services.length > 0) {
                const relPath = rel(absDir, file);
                result.serviceUsages.push({
                    path: file,
                    relativePath: relPath,
                    services,
                    accessPattern: 'c.var',
                });
            }
        }
    }
    // Also scan src/ root-level TS files
    const srcDir = join(absDir, 'src');
    if (existsSync(srcDir)) {
        for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
            if (entry.isFile() &&
                (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) &&
                entry.name !== 'services.ts' // Don't scan our own generated file
            ) {
                const file = join(srcDir, entry.name);
                const sourceFile = await parseTs(file);
                const services = scanServiceUsageInRouteFile(sourceFile);
                if (services.length > 0) {
                    const relPath = rel(absDir, file);
                    result.serviceUsages.push({
                        path: file,
                        relativePath: relPath,
                        services,
                        accessPattern: 'c.var',
                    });
                }
            }
        }
    }
    // Compute all services used
    const allServices = new Set();
    for (const usage of result.serviceUsages) {
        for (const svc of usage.services) {
            allServices.add(svc);
        }
    }
    result.allServicesUsed = [...allServices].sort();
    if (result.serviceUsages.length > 0) {
        const ctxUsages = result.serviceUsages.filter((u) => u.accessPattern === 'ctx');
        const cVarUsages = result.serviceUsages.filter((u) => u.accessPattern === 'c.var');
        if (ctxUsages.length > 0) {
            findings.push({
                id: 'v3-service-agent-ctx',
                severity: 'guided',
                message: `${ctxUsages.length} file(s) access services via ctx.* (agent context)`,
                hint: 'Service access through agent context (ctx.kv, ctx.vector, etc.) is removed\n' +
                    'in v3. A shared src/services.ts will be generated with singleton clients.\n' +
                    '\n' +
                    "  import { kv } from './services'; // or '../services'\n" +
                    "  const data = await kv.get('namespace', 'key');",
            });
        }
        if (cVarUsages.length > 0) {
            findings.push({
                id: 'v3-service-route-ctx',
                severity: 'guided',
                message: `${cVarUsages.length} file(s) access services via c.var.* (Hono context)`,
                hint: 'Service access through Hono context variables (c.var.kv, etc.) is replaced\n' +
                    'by direct imports from a shared services module.\n' +
                    '\n' +
                    "  import { kv } from './services';\n" +
                    "  const data = await kv.get('namespace', 'key');",
            });
        }
    }
    // ── 5. Frontend / SPA ───────────────────────────────────────────────────
    const webDir = join(absDir, 'src', 'web');
    result.hasFrontend = existsSync(webDir);
    if (result.hasFrontend) {
        findings.push({
            id: 'v3-spa-detected',
            severity: 'guided',
            message: 'src/web/ frontend detected',
            hint: 'In v3, SPAs are served by the Agentuity buildpack. For local development,\n' +
                "use your framework's dev server (e.g., vite dev). For production, the\n" +
                'buildpack detects static assets and injects a file server automatically.\n' +
                '\n' +
                'If you want to serve static files from your Hono app directly:\n' +
                '\n' +
                "  import { serveStatic } from 'hono/bun';\n" +
                "  app.use('/assets/*', serveStatic({ root: './src/web/dist' }));",
        });
    }
    // ── 6. agentuity.config.ts ──────────────────────────────────────────────
    const configPath = join(absDir, 'agentuity.config.ts');
    result.hasAgentuityConfig = existsSync(configPath);
    if (result.hasAgentuityConfig) {
        findings.push({
            id: 'v3-config-file',
            severity: 'auto',
            message: 'agentuity.config.ts exists — will be deleted',
            file: 'agentuity.config.ts',
            hint: 'v3 uses standard framework configuration (vite.config.ts, etc.).\n' +
                'The agentuity.config.ts file is no longer used.',
        });
    }
    // Vite config
    const viteConfigPath = join(absDir, 'vite.config.ts');
    result.hasViteConfig = existsSync(viteConfigPath);
    // ── 7. @agentuity/react deprecation ─────────────────────────────────────
    if (result.hasReactPackage) {
        findings.push({
            id: 'v3-react-deprecated',
            severity: 'manual',
            message: '@agentuity/react is deprecated and will be removed',
            file: 'package.json',
            hint: '@agentuity/react is fully deprecated in v3. Replace with:\n' +
                '\n' +
                '  • AgentuityProvider/useAuth → Your auth provider directly (better-auth, Clerk, etc.)\n' +
                '  • useAPI/createAPIClient → Hono RPC client (hc from hono/client)\n' +
                '  • useAnalytics → getAnalytics() from @agentuity/analytics\n' +
                '  • useWebRTCCall → WebRTCManager from @agentuity/frontend\n' +
                '\n' +
                'Remove @agentuity/react from package.json after migrating all imports.',
        });
    }
    // Sort: auto first, guided second, manual last
    const order = { auto: 0, guided: 1, manual: 2 };
    findings.sort((a, b) => order[a.severity] - order[b.severity]);
    return result;
}
//# sourceMappingURL=detect-v3.js.map