import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
import { enrichContextWithTraceState } from './otel/tracestate';
import { formatMessage } from './logger/util';
import { AGENT_RUNTIME, AGENT_IDS, isInsideAgentRuntime } from './_config';
import { generateId } from './session';
import WaitUntilHandler from './_waituntil';
import { registerServices, createServices } from './_services';
import { getAgentAsyncLocalStorage } from './_context';
import { getLogger, getTracer, setGlobalLogger, setGlobalTracer } from './_server';
import { getThreadProvider, getSessionProvider, getSessionEventProvider } from './_services';
import * as runtimeConfig from './_config';
/**
 * Flag to track if standalone initialization has already been performed.
 * Prevents duplicate initialization across multiple createAgentContext() calls.
 */
let standaloneInitialized = false;
/**
 * Create a minimal console-based logger for standalone mode.
 * This logger outputs to console without OpenTelemetry integration.
 */
function createStandaloneLogger() {
    const logLevel = (process.env.AGENTUITY_LOG_LEVEL || 'info');
    const levels = ['trace', 'debug', 'info', 'warn', 'error'];
    const currentLevelIndex = levels.indexOf(logLevel);
    const shouldLog = (level) => levels.indexOf(level) >= currentLevelIndex;
    const logger = {
        trace: (message, ...args) => {
            if (shouldLog('trace'))
                console.debug('[TRACE]', formatMessage(false, undefined, message, args));
        },
        debug: (message, ...args) => {
            if (shouldLog('debug'))
                console.debug('[DEBUG]', formatMessage(false, undefined, message, args));
        },
        info: (message, ...args) => {
            if (shouldLog('info'))
                console.info('[INFO]', formatMessage(false, undefined, message, args));
        },
        warn: (message, ...args) => {
            if (shouldLog('warn'))
                console.warn('[WARN]', formatMessage(false, undefined, message, args));
        },
        error: (message, ...args) => {
            if (shouldLog('error'))
                console.error('[ERROR]', formatMessage(false, undefined, message, args));
        },
        fatal: (message, ...args) => {
            console.error('[FATAL]', formatMessage(false, undefined, message, args));
            process.exit(1);
        },
        child: () => logger,
    };
    return logger;
}
/**
 * Initialize standalone runtime globals.
 * This sets up minimal logger, tracer, and services for standalone execution.
 * Called automatically by createAgentContext() when not running inside agent runtime.
 */
function initializeStandaloneRuntime() {
    if (standaloneInitialized) {
        return;
    }
    const logger = createStandaloneLogger();
    const tracer = trace.getTracer('standalone-agent');
    // Set global state
    setGlobalLogger(logger);
    setGlobalTracer(tracer);
    // App state is already initialized as {} in _server.ts module-level var
    // Initialize services (will use local services since not authenticated)
    const serverUrl = `http://127.0.0.1:${process.env.PORT || '3500'}`;
    createServices(logger, undefined, serverUrl);
    standaloneInitialized = true;
    logger.debug('Standalone runtime initialized');
}
/**
 * Standalone agent context for executing agents outside of HTTP requests.
 *
 * This context provides the same infrastructure as HTTP request contexts:
 * - OpenTelemetry tracing with proper span hierarchy
 * - Session and thread management (save/restore)
 * - Background task handling (waitUntil)
 * - Session event tracking (start/complete)
 * - Access to all services (kv, stream, vector)
 *
 * @example
 * ```typescript
 * import { createAgentContext } from '@agentuity/runtime';
 * import myAgent from './agents/my-agent';
 *
 * // Simple usage:
 * const ctx = createAgentContext();
 * const result = await ctx.invoke(() => myAgent.run(input));
 *
 * // With custom session tracking:
 * const ctx = createAgentContext({
 *   sessionId: discordMessage.id,
 *   trigger: 'discord'
 * });
 * const result = await ctx.invoke(() => myAgent.run(input));
 *
 * // Reuse context for multiple agents:
 * const ctx = createAgentContext();
 * const result1 = await ctx.invoke(() => agent1.run(input1));
 * const result2 = await ctx.invoke(() => agent2.run(result1));
 * ```
 */
export class StandaloneAgentContext {
    // Immutable context properties (safe for concurrent access)
    agent = {};
    logger;
    tracer;
    kv;
    stream;
    vector;
    sandbox;
    queue;
    email;
    schedule;
    task;
    config;
    app;
    current;
    [AGENT_RUNTIME];
    // Note: The following are mutable and will be set per-invocation via AsyncLocalStorage
    // They exist on the interface for compatibility but are overwritten during invoke()
    sessionId;
    state;
    session;
    thread;
    auth;
    [AGENT_IDS];
    // Immutable options stored from constructor
    parentContext;
    trigger;
    initialSessionId;
    constructor(options) {
        // Auto-initialize if not inside agent runtime and globals are not set
        let logger = getLogger();
        let tracer = getTracer();
        let app = {};
        if (!logger || !tracer) {
            // Check if we're inside the agent runtime (dev server or cloud)
            if (isInsideAgentRuntime()) {
                // Inside runtime but globals not set - this is an error
                throw new Error('Global state not initialized. This should not happen inside the agent runtime. ' +
                    'Please report this issue.');
            }
            // Not inside runtime - auto-initialize for standalone use
            initializeStandaloneRuntime();
            // Re-fetch globals after initialization
            logger = getLogger();
            tracer = getTracer();
            app = {};
        }
        this.logger = logger;
        this.tracer = tracer;
        this.app = app;
        this.config = {};
        this.state = new Map();
        this.parentContext = options?.parentContext ?? context.active();
        this.trigger = options?.trigger ?? 'manual';
        this.initialSessionId = options?.sessionId;
        // Session ID will be set properly in invoke() after span is created
        this.sessionId = options?.sessionId ?? 'pending';
        // Thread and session will be restored in invoke()
        this.thread =
            options?.thread ??
                {
                    id: 'pending',
                    state: {
                        loaded: false,
                        dirty: false,
                        get: async () => undefined,
                        set: async () => { },
                        has: async () => false,
                        delete: async () => { },
                        clear: async () => { },
                        entries: async () => [],
                        keys: async () => [],
                        values: async () => [],
                        size: async () => 0,
                        push: async () => { },
                    },
                    getMetadata: async () => ({}),
                    setMetadata: async () => { },
                    addEventListener: () => { },
                    removeEventListener: () => { },
                    destroy: async () => { },
                    empty: async () => true,
                };
        this.session =
            options?.session ??
                {
                    id: 'pending',
                    thread: this.thread,
                    state: new Map(),
                    metadata: {},
                    addEventListener: () => { },
                    removeEventListener: () => { },
                    serializeUserData: () => undefined,
                };
        this.auth = null;
        // Create isolated runtime state
        this[AGENT_RUNTIME] = {
            agents: new Map(),
            agentConfigs: new Map(),
            agentEventListeners: new WeakMap(),
        };
        // Register services (kv, stream, vector)
        registerServices(this, true); // true = populate agents registry
    }
    waitUntil(_callback) {
        // This will be called from within invoke() where waitUntilHandler is in scope
        // We need to access the per-call waitUntilHandler from the current invocation
        // This is handled by updating the context during invoke() via AsyncLocalStorage
        throw new Error('waitUntil must be called from within invoke() execution context');
    }
    /**
     * Execute a function within this agent context.
     *
     * This method:
     * 1. Creates an OpenTelemetry span for the invocation
     * 2. Restores/creates session and thread
     * 3. Sends session start event
     * 4. Executes the function within AsyncLocalStorage context
     * 5. Waits for background tasks (waitUntil)
     * 6. Saves session and thread
     * 7. Sends session complete event
     *
     * @param fn - Function to execute (typically () => agent.run(input))
     * @param options - Optional configuration for the invocation
     * @returns Promise that resolves to the function's return value
     *
     * @example
     * ```typescript
     * const result = await ctx.invoke(() => myAgent.run({ userId: '123' }));
     * ```
     *
     * @example
     * ```typescript
     * // Multiple agents in sequence:
     * const result = await ctx.invoke(async () => {
     *   const step1 = await agent1.run(input);
     *   return agent2.run(step1);
     * });
     * ```
     */
    async invoke(fn, options) {
        const threadProvider = getThreadProvider();
        const sessionProvider = getSessionProvider();
        const sessionEventProvider = getSessionEventProvider();
        const storage = getAgentAsyncLocalStorage();
        // Create per-invocation state (prevents race conditions on concurrent calls)
        const waitUntilHandler = new WaitUntilHandler(this.tracer);
        const agentIds = new Set();
        let invocationSessionId = this.initialSessionId ?? 'pending';
        let invocationThread;
        let invocationSession;
        const invocationState = new Map();
        // Create a per-call context that inherits from this but has isolated mutable state
        const callContext = Object.create(this);
        callContext.sessionId = invocationSessionId;
        callContext.state = invocationState;
        callContext[AGENT_IDS] = agentIds;
        callContext.waitUntil = (callback) => {
            waitUntilHandler.waitUntil(callback);
        };
        // Execute within parent context (for distributed tracing)
        return await context.with(this.parentContext, async () => {
            // Build enriched traceState BEFORE span creation so the
            // recording span inherits it and it gets exported to OTLP.
            const projectId = runtimeConfig.getProjectId();
            const orgId = runtimeConfig.getOrganizationId();
            const deploymentId = runtimeConfig.getDeploymentId();
            const isDevMode = runtimeConfig.isDevMode();
            const enrichedContext = enrichContextWithTraceState(context.active(), {
                pid: projectId,
                oid: orgId,
                did: deploymentId,
                d: isDevMode ? '1' : undefined,
            });
            // Create a span for this invocation (similar to otelMiddleware's HTTP span)
            return await trace.getTracer('standalone-agent').startActiveSpan(options?.spanName ?? 'agent-invocation', {
                kind: SpanKind.INTERNAL, // Not HTTP, but internal invocation
                attributes: {
                    trigger: this.trigger,
                },
            }, enrichedContext, async (span) => {
                const sctx = span.spanContext();
                // Generate sessionId from traceId if not provided
                invocationSessionId =
                    this.initialSessionId ??
                        (sctx?.traceId ? `sess_${sctx.traceId}` : generateId('sess'));
                callContext.sessionId = invocationSessionId;
                // Restore thread and session (like otelMiddleware does)
                // For standalone contexts, we create a simple thread/session if not provided
                // The threadProvider.restore expects a Hono context with cookie/header access
                // For standalone contexts without HTTP, we just create a new thread
                const { DefaultThread, generateId: genId } = await import('./session');
                const threadId = genId('thrd');
                // Create a no-op restore function for standalone contexts
                const restoreFn = async () => ({ state: new Map(), metadata: {} });
                invocationThread = new DefaultThread(threadProvider, threadId, restoreFn);
                callContext.thread = invocationThread;
                invocationSession = await sessionProvider.restore(invocationThread, invocationSessionId);
                callContext.session = invocationSession;
                // Send session start event (if configured)
                const shouldSendSession = !!(orgId && projectId);
                let canSendSessionEvents = true;
                if (shouldSendSession) {
                    await sessionEventProvider
                        .start({
                        id: invocationSessionId,
                        orgId,
                        projectId,
                        threadId: invocationThread.id,
                        routeId: 'standalone', // No route for standalone contexts
                        deploymentId,
                        devmode: isDevMode,
                        environment: runtimeConfig.getEnvironment(),
                        method: 'STANDALONE',
                        url: '',
                        trigger: this.trigger,
                        metadata: Object.keys(invocationSession.metadata).length > 0
                            ? invocationSession.metadata
                            : undefined,
                    })
                        .catch((ex) => {
                        canSendSessionEvents = false;
                        this.logger.error('error sending session start event: %s', ex);
                    });
                }
                let hasPendingWaits = false;
                try {
                    // Execute function within AsyncLocalStorage context with per-call context
                    const result = await storage.run(callContext, fn);
                    // Wait for background tasks (like otelMiddleware does)
                    if (waitUntilHandler.hasPending()) {
                        hasPendingWaits = true;
                        waitUntilHandler
                            .waitUntilAll(this.logger, invocationSessionId)
                            .then(async () => {
                            this.logger.debug('wait until finished for session %s', invocationSessionId);
                            await sessionProvider.save(invocationSession);
                            await threadProvider.save(invocationThread);
                            span.setStatus({ code: SpanStatusCode.OK });
                            if (shouldSendSession && canSendSessionEvents) {
                                const userData = invocationSession.serializeUserData();
                                sessionEventProvider
                                    .complete({
                                    id: invocationSessionId,
                                    threadId: (await invocationThread.empty())
                                        ? null
                                        : invocationThread.id,
                                    statusCode: 200, // Success
                                    agentIds: Array.from(agentIds),
                                    userData,
                                    metadata: Object.keys(invocationSession.metadata).length > 0
                                        ? invocationSession.metadata
                                        : undefined,
                                })
                                    .then(() => { })
                                    .catch((ex) => this.logger.error('session complete failed: %s', ex instanceof Error ? ex.message : ex));
                            }
                        })
                            .catch(async (ex) => {
                            this.logger.error('wait until errored for session %s. %s', invocationSessionId, ex);
                            if (ex instanceof Error) {
                                span.recordException(ex);
                            }
                            const message = ex.message ?? String(ex);
                            span.setStatus({
                                code: SpanStatusCode.ERROR,
                                message,
                            });
                            this.logger.error(message);
                            if (shouldSendSession && canSendSessionEvents) {
                                const userData = invocationSession.serializeUserData();
                                sessionEventProvider
                                    .complete({
                                    id: invocationSessionId,
                                    threadId: (await invocationThread.empty())
                                        ? null
                                        : invocationThread.id,
                                    statusCode: 500, // Error
                                    error: message,
                                    agentIds: Array.from(agentIds),
                                    userData,
                                    metadata: Object.keys(invocationSession.metadata).length > 0
                                        ? invocationSession.metadata
                                        : undefined,
                                })
                                    .then(() => { })
                                    .catch((ex) => this.logger.error('session complete failed: %s', ex instanceof Error ? ex.message : ex));
                            }
                        })
                            .finally(() => {
                            span.end();
                        });
                    }
                    else {
                        span.setStatus({ code: SpanStatusCode.OK });
                        if (shouldSendSession && canSendSessionEvents) {
                            const userData = invocationSession.serializeUserData();
                            sessionEventProvider
                                .complete({
                                id: invocationSessionId,
                                threadId: (await invocationThread.empty())
                                    ? null
                                    : invocationThread.id,
                                statusCode: 200,
                                agentIds: Array.from(agentIds),
                                userData,
                                metadata: Object.keys(invocationSession.metadata).length > 0
                                    ? invocationSession.metadata
                                    : undefined,
                            })
                                .then(() => { })
                                .catch((ex) => this.logger.error('session complete failed: %s', ex instanceof Error ? ex.message : ex));
                        }
                    }
                    return result;
                }
                catch (ex) {
                    if (ex instanceof Error) {
                        span.recordException(ex);
                    }
                    const message = ex.message ?? String(ex);
                    span.setStatus({
                        code: SpanStatusCode.ERROR,
                        message,
                    });
                    this.logger.error(message);
                    if (shouldSendSession && canSendSessionEvents) {
                        const userData = invocationSession.serializeUserData();
                        sessionEventProvider
                            .complete({
                            id: invocationSessionId,
                            threadId: (await invocationThread.empty()) ? null : invocationThread.id,
                            statusCode: 500,
                            error: message,
                            agentIds: Array.from(agentIds),
                            userData,
                            metadata: Object.keys(invocationSession.metadata).length > 0
                                ? invocationSession.metadata
                                : undefined,
                        })
                            .then(() => { })
                            .catch((ex) => this.logger.error('session complete failed: %s', ex instanceof Error ? ex.message : ex));
                    }
                    throw ex;
                }
                finally {
                    if (!hasPendingWaits) {
                        try {
                            await sessionProvider.save(invocationSession);
                            await threadProvider.save(invocationThread);
                        }
                        finally {
                            span.end();
                        }
                    }
                }
            });
        });
    }
    /**
     * Execute an agent with the given input within this context.
     *
     * This is a convenience method that wraps `invoke()` for cleaner syntax
     * when running a single agent.
     *
     * @param agent - The agent to execute (must have a `run` method)
     * @param input - Input to pass to the agent (if agent requires input)
     * @returns Promise that resolves to the agent's output
     *
     * @example
     * ```typescript
     * import { createAgentContext } from '@agentuity/runtime';
     * import myAgent from './agents/my-agent';
     *
     * const ctx = createAgentContext();
     * const result = await ctx.run(myAgent, { name: 'World' });
     * ```
     *
     * @example
     * ```typescript
     * // Agent without input
     * const result = await ctx.run(statusAgent);
     * ```
     *
     * @example
     * ```typescript
     * // Multiple agents in sequence
     * const ctx = createAgentContext();
     * const step1 = await ctx.run(preprocessAgent, rawData);
     * const step2 = await ctx.run(processAgent, step1);
     * const result = await ctx.run(postprocessAgent, step2);
     * ```
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    async run(agent, input) {
        // Handle both agents with and without input
        // The agent.run type varies based on whether input schema is defined
        const runFn = agent.run;
        return this.invoke(() => runFn(input));
    }
}
/**
 * Create a standalone agent context for executing agents outside of HTTP requests.
 *
 * This is useful for Discord bots, cron jobs, CLI scripts, sandboxes, or any scenario
 * where you need to run agents but don't have an HTTP request context.
 *
 * **Auto-initialization**: When running outside of the Agentuity runtime (dev server or cloud),
 * this function automatically initializes minimal runtime globals. No manual setup required.
 *
 * @param options - Optional configuration for the context
 * @returns A StandaloneAgentContext instance
 *
 * @example
 * ```typescript
 * import { createAgentContext } from '@agentuity/runtime';
 * import myAgent from './agents/my-agent';
 *
 * // Simple usage with ctx.run() (recommended):
 * const ctx = createAgentContext();
 * const result = await ctx.run(myAgent, { name: 'World' });
 *
 * // Discord bot example:
 * client.on('messageCreate', async (message) => {
 *   const ctx = createAgentContext({
 *     sessionId: message.id,
 *     trigger: 'discord'
 *   });
 *   const response = await ctx.run(chatAgent, { message: message.content });
 *   await message.reply(response.text);
 * });
 *
 * // Cron job example:
 * cron.schedule('0 * * * *', async () => {
 *   const ctx = createAgentContext({ trigger: 'cron' });
 *   await ctx.run(cleanupAgent);
 * });
 *
 * // Multiple agents in sequence:
 * const ctx = createAgentContext();
 * const step1 = await ctx.run(preprocessAgent, rawData);
 * const result = await ctx.run(processAgent, step1);
 * ```
 */
export function createAgentContext(options) {
    return new StandaloneAgentContext(options);
}
//# sourceMappingURL=_standalone.js.map