/**
 * @license
 * Copyright 2026 HopCode Team Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { createDebugLogger, SendMessageType } from '@hoptrendy/hopcode-core';
import { StreamJsonInputReader } from './io/StreamJsonInputReader.js';
import { StreamJsonOutputAdapter } from './io/StreamJsonOutputAdapter.js';
import { ControlContext } from './control/ControlContext.js';
import { ControlDispatcher } from './control/ControlDispatcher.js';
import { ControlService } from './control/ControlService.js';
import { isCLIUserMessage, isCLIAssistantMessage, isCLISystemMessage, isCLIResultMessage, isCLIPartialAssistantMessage, isControlRequest, isControlResponse, isControlCancel, } from './types.js';
import { createMinimalSettings } from '../config/settings.js';
import { runNonInteractive } from '../nonInteractiveCli.js';
import { finalizeStartupProfile, profileCheckpoint, } from '../utils/startupProfiler.js';
const debugLogger = createDebugLogger('NON_INTERACTIVE_SESSION');
class Session {
    userMessageQueue = [];
    monitorStartedQueue = [];
    monitorQueue = [];
    abortController;
    config;
    sessionId;
    promptIdCounter = 0;
    inputReader;
    outputAdapter;
    controlContext = null;
    dispatcher = null;
    controlService = null;
    controlSystemEnabled = null;
    shutdownHandler = null;
    initialPrompt = null;
    processingPromise = null;
    isShuttingDown = false;
    configInitialized = false;
    monitorNotificationsRegistered = false;
    monitorRegistrationsRegistered = false;
    settings;
    // Single initialization promise that resolves when session is ready for user messages.
    // Created lazily once initialization actually starts.
    initializationPromise = null;
    initializationResolve = null;
    initializationReject = null;
    constructor(config, initialPrompt, settings = createMinimalSettings()) {
        this.config = config;
        this.settings = settings;
        this.sessionId = config.getSessionId();
        this.abortController = new AbortController();
        this.initialPrompt = initialPrompt ?? null;
        this.inputReader = new StreamJsonInputReader();
        this.outputAdapter = new StreamJsonOutputAdapter(config, config.getIncludePartialMessages());
        this.setupSignalHandlers();
    }
    ensureInitializationPromise() {
        if (this.initializationPromise) {
            return;
        }
        this.initializationPromise = new Promise((resolve, reject) => {
            this.initializationResolve = () => {
                resolve();
                this.initializationResolve = null;
                this.initializationReject = null;
            };
            this.initializationReject = (error) => {
                reject(error);
                this.initializationResolve = null;
                this.initializationReject = null;
            };
        });
    }
    getNextPromptId() {
        this.promptIdCounter++;
        return `${this.sessionId}########${this.promptIdCounter}`;
    }
    async ensureConfigInitialized(options) {
        if (this.configInitialized) {
            return;
        }
        debugLogger.debug('[Session] Initializing config');
        try {
            // Bracket `config.initialize()` with the same profiler checkpoints
            // the non-stream-json branch in `gemini.tsx` uses so the
            // `config_initialize_dur` derived phase shows up in stream-json
            // startup profiles. `profileCheckpoint` is a no-op when
            // `HOPCODE_PROFILE_STARTUP` is unset, so this adds zero overhead
            // off the profiling path. Without these, stream-json profiles read
            // as missing the initialize phase entirely, which made the MCP
            // discovery timings look like they happened "before init".
            profileCheckpoint('config_initialize_start');
            await this.config.initialize(options);
            profileCheckpoint('config_initialize_end');
            // Stream-json sessions feed prompts straight to the model after init.
            // Under progressive MCP availability `initialize()` returns before
            // MCP servers settle, so we must explicitly await discovery here —
            // otherwise the first prompt would see only built-in tools.
            await this.config.waitForMcpReady();
            // Surface MCP failures on stderr — same rationale as gemini.tsx's
            // non-interactive branch: per-server errors are caught inside
            // `discoverAllMcpToolsIncremental` and never reach a TTY otherwise,
            // so a script using stream-json with broken MCP config would
            // silently run with only built-in tools.
            // Defensive against tests that pass a stubbed Config without
            // `getFailedMcpServerNames`.
            const failedMcpServers = typeof this.config.getFailedMcpServerNames === 'function'
                ? this.config.getFailedMcpServerNames()
                : [];
            if (failedMcpServers.length > 0) {
                process.stderr.write(`Warning: MCP server(s) failed to start: ${failedMcpServers.join(', ')}. ` +
                    `Continuing with built-in tools and any servers that did connect.\n`);
            }
            // Finalize the startup profile here so `config_initialize_*` and the
            // MCP discovery events captured during init/discovery make it into
            // the on-disk profile. gemini.tsx's stream-json branch deliberately
            // skips finalize because the profiler's `finalized` guard would
            // otherwise suppress every event emitted during the
            // `Session.ensureConfigInitialized` flow above.
            finalizeStartupProfile(this.config.getSessionId());
            this.configInitialized = true;
            this.registerMonitorRegistrations();
            this.registerMonitorNotifications();
        }
        catch (error) {
            debugLogger.error('[Session] Failed to initialize config:', error);
            throw error;
        }
    }
    registerMonitorNotifications() {
        if (this.monitorNotificationsRegistered) {
            return;
        }
        const registry = this.config.getMonitorRegistry();
        registry.setNotificationCallback((displayText, modelText, meta) => {
            if (this.isShuttingDown || this.abortController.signal.aborted) {
                return;
            }
            this.enqueueMonitorNotification({
                displayText,
                modelText,
                sdkNotification: {
                    task_id: meta.monitorId,
                    tool_use_id: meta.toolUseId,
                    status: meta.status,
                },
            });
        });
        this.monitorNotificationsRegistered = true;
    }
    registerMonitorRegistrations() {
        if (this.monitorRegistrationsRegistered) {
            return;
        }
        const registry = this.config.getMonitorRegistry();
        registry.setRegisterCallback((entry) => {
            if (this.isShuttingDown || this.abortController.signal.aborted) {
                return;
            }
            this.enqueueMonitorStarted({
                task_id: entry.monitorId,
                tool_use_id: entry.toolUseId,
                description: entry.description,
            });
        });
        this.monitorRegistrationsRegistered = true;
    }
    /**
     * Mark initialization as complete
     */
    completeInitialization() {
        if (this.initializationResolve) {
            debugLogger.debug('[Session] Initialization complete');
            this.initializationResolve();
            this.initializationResolve = null;
            this.initializationReject = null;
        }
    }
    /**
     * Mark initialization as failed
     */
    failInitialization(error) {
        if (this.initializationReject) {
            debugLogger.error('[Session] Initialization failed:', error);
            this.initializationReject(error);
            this.initializationResolve = null;
            this.initializationReject = null;
        }
    }
    /**
     * Wait for session to be ready for user messages
     */
    async waitForInitialization() {
        if (!this.initializationPromise) {
            return;
        }
        await this.initializationPromise;
    }
    ensureControlSystem() {
        if (this.controlContext && this.dispatcher && this.controlService) {
            return;
        }
        this.controlContext = new ControlContext({
            config: this.config,
            streamJson: this.outputAdapter,
            sessionId: this.sessionId,
            abortSignal: this.abortController.signal,
            settings: this.settings,
            permissionMode: this.config.getApprovalMode(),
            onInterrupt: () => this.handleInterrupt(),
        });
        this.dispatcher = new ControlDispatcher(this.controlContext);
        this.controlService = new ControlService(this.controlContext, this.dispatcher);
    }
    getDispatcher() {
        if (this.controlSystemEnabled !== true) {
            return null;
        }
        if (!this.dispatcher) {
            this.ensureControlSystem();
        }
        return this.dispatcher;
    }
    /**
     * Handle the first message to determine session mode (SDK vs direct).
     * This is synchronous from the message loop's perspective - it starts
     * async work but does not return a promise that the loop awaits.
     *
     * The initialization completes asynchronously and resolves initializationPromise
     * when ready for user messages.
     */
    handleFirstMessage(message) {
        if (isControlRequest(message)) {
            const request = message;
            this.controlSystemEnabled = true;
            this.ensureControlSystem();
            if (request.request.subtype === 'initialize') {
                // Start SDK mode initialization (fire-and-forget from loop perspective)
                void this.initializeSdkMode(request);
                return;
            }
            debugLogger.debug('[Session] Ignoring non-initialize control request during initialization');
            return;
        }
        if (isCLIUserMessage(message)) {
            this.controlSystemEnabled = false;
            // Start direct mode initialization (fire-and-forget from loop perspective)
            void this.initializeDirectMode(message);
            return;
        }
        this.controlSystemEnabled = false;
    }
    /**
     * SDK mode initialization flow
     * Dispatches initialize request and initializes config with MCP support
     */
    async initializeSdkMode(request) {
        this.ensureInitializationPromise();
        try {
            // Dispatch the initialize request first
            // This registers SDK MCP servers in the control context
            await this.dispatcher?.dispatch(request);
            // Get sendSdkMcpMessage callback from SdkMcpController
            // This callback is used by McpClientManager to send MCP messages
            // from CLI MCP clients to SDK MCP servers via the control plane
            const sendSdkMcpMessage = this.dispatcher?.sdkMcpController.createSendSdkMcpMessage();
            // Initialize config with SDK MCP message support
            await this.ensureConfigInitialized({ sendSdkMcpMessage });
            // Initialization complete!
            this.completeInitialization();
        }
        catch (error) {
            debugLogger.error('[Session] SDK mode initialization failed:', error);
            this.failInitialization(error instanceof Error ? error : new Error(String(error)));
        }
    }
    /**
     * Direct mode initialization flow
     * Initializes config and enqueues the first user message
     */
    async initializeDirectMode(userMessage) {
        this.ensureInitializationPromise();
        try {
            // Initialize config
            await this.ensureConfigInitialized();
            // Initialization complete!
            this.completeInitialization();
            // Enqueue the first user message for processing
            this.enqueueUserMessage(userMessage);
        }
        catch (error) {
            debugLogger.error('[Session] Direct mode initialization failed:', error);
            this.failInitialization(error instanceof Error ? error : new Error(String(error)));
        }
    }
    /**
     * Handle control request asynchronously (fire-and-forget from main loop).
     * Errors are handled internally and responses sent by dispatcher.
     */
    handleControlRequestAsync(request) {
        const dispatcher = this.getDispatcher();
        if (!dispatcher) {
            debugLogger.warn('[Session] Control system not enabled');
            return;
        }
        // Fire-and-forget: dispatch runs concurrently
        // The dispatcher's pendingIncomingRequests tracks completion
        void dispatcher.dispatch(request).catch((error) => {
            debugLogger.error('[Session] Control request dispatch error:', error);
            // Error response is already sent by dispatcher.dispatch()
        });
    }
    /**
     * Handle control response - MUST be synchronous
     * This resolves pending outgoing requests, breaking the deadlock cycle.
     */
    handleControlResponse(response) {
        const dispatcher = this.getDispatcher();
        if (!dispatcher) {
            return;
        }
        dispatcher.handleControlResponse(response);
    }
    handleControlCancel(cancelRequest) {
        const dispatcher = this.getDispatcher();
        if (!dispatcher) {
            return;
        }
        dispatcher.handleCancel(cancelRequest.request_id);
    }
    async processUserMessage(userMessage) {
        const input = extractUserMessageText(userMessage);
        if (!input) {
            debugLogger.debug('[Session] No text content in user message');
            return;
        }
        // Wait for initialization to complete before processing user messages
        await this.waitForInitialization();
        const promptId = this.getNextPromptId();
        try {
            await runNonInteractive(this.config, this.settings, input, promptId, {
                abortController: this.abortController,
                adapter: this.outputAdapter,
                controlService: this.controlService ?? undefined,
                captureMonitorNotifications: false,
                captureMonitorRegistrations: false,
            });
        }
        catch (error) {
            debugLogger.error('[Session] Query execution error:', error);
        }
    }
    async processMonitorNotification(notification) {
        await this.waitForInitialization();
        this.outputAdapter.emitUserMessage([{ text: notification.displayText }]);
        this.outputAdapter.emitSystemMessage('task_notification', notification.sdkNotification);
        const promptId = this.getNextPromptId();
        await runNonInteractive(this.config, this.settings, notification.modelText, promptId, {
            abortController: this.abortController,
            adapter: this.outputAdapter,
            controlService: this.controlService ?? undefined,
            sendMessageType: SendMessageType.Notification,
            notificationDisplayText: notification.displayText,
            captureMonitorNotifications: false,
            captureMonitorRegistrations: false,
        });
    }
    async processPendingWork() {
        if (this.isShuttingDown || this.abortController.signal.aborted) {
            return;
        }
        while ((this.userMessageQueue.length > 0 ||
            this.monitorStartedQueue.length > 0 ||
            this.monitorQueue.length > 0) &&
            !this.isShuttingDown &&
            !this.abortController.signal.aborted) {
            if (this.userMessageQueue.length > 0) {
                const userMessage = this.userMessageQueue.shift();
                try {
                    await this.processUserMessage(userMessage);
                }
                catch (error) {
                    debugLogger.error('[Session] Error processing user message:', error);
                    this.emitErrorResult(error);
                }
                continue;
            }
            const started = this.monitorStartedQueue.shift();
            if (started) {
                this.outputAdapter.emitSystemMessage('task_started', started);
                continue;
            }
            const notification = this.monitorQueue.shift();
            if (!notification) {
                continue;
            }
            try {
                await this.processMonitorNotification(notification);
            }
            catch (error) {
                debugLogger.error('[Session] Error processing monitor notification:', error);
                this.emitErrorResult(error);
            }
        }
    }
    enqueueUserMessage(userMessage) {
        this.userMessageQueue.push(userMessage);
        this.ensureProcessingStarted();
    }
    enqueueMonitorStarted(started) {
        this.monitorStartedQueue.push(started);
        this.ensureProcessingStarted();
    }
    enqueueMonitorNotification(notification) {
        this.monitorQueue.push(notification);
        this.ensureProcessingStarted();
    }
    ensureProcessingStarted() {
        if (this.processingPromise) {
            return;
        }
        this.processingPromise = this.processPendingWork().finally(() => {
            this.processingPromise = null;
            if ((this.userMessageQueue.length > 0 ||
                this.monitorStartedQueue.length > 0 ||
                this.monitorQueue.length > 0) &&
                !this.isShuttingDown &&
                !this.abortController.signal.aborted) {
                this.ensureProcessingStarted();
            }
        });
    }
    emitErrorResult(error, numTurns = 0, durationMs = 0, apiDurationMs = 0) {
        const message = error instanceof Error ? error.message : String(error);
        this.outputAdapter.emitResult({
            isError: true,
            errorMessage: message,
            durationMs,
            apiDurationMs,
            numTurns,
            usage: undefined,
        });
    }
    handleInterrupt() {
        debugLogger.info('[Session] Interrupt requested');
        this.abortController.abort();
        // Do not create a new AbortController to prevent listener leaks.
        // Subsequent queries will check signal.aborted and fail immediately.
    }
    setupSignalHandlers() {
        this.shutdownHandler = () => {
            debugLogger.info('[Session] Shutdown signal received');
            this.isShuttingDown = true;
            this.abortController.abort();
        };
        process.on('SIGINT', this.shutdownHandler);
        process.on('SIGTERM', this.shutdownHandler);
    }
    /**
     * Wait for all pending work to complete before shutdown
     */
    async waitForAllPendingWork() {
        // 1. Wait for initialization to complete (or fail)
        try {
            await this.waitForInitialization();
        }
        catch (error) {
            debugLogger.error('[Session] Initialization error during shutdown:', error);
        }
        // 2. Wait for all control request handlers using dispatcher's tracking
        if (this.dispatcher) {
            const pendingCount = this.dispatcher.getPendingIncomingRequestCount();
            if (pendingCount > 0) {
                debugLogger.debug(`[Session] Waiting for ${pendingCount} pending control request handlers`);
            }
            await this.dispatcher.waitForPendingIncomingRequests();
        }
        // 3. Wait for user message processing queue
        while (this.processingPromise) {
            debugLogger.debug('[Session] Waiting for user message processing');
            try {
                await this.processingPromise;
            }
            catch (error) {
                debugLogger.error('[Session] Error in user message processing:', error);
            }
        }
    }
    async shutdown() {
        debugLogger.debug('[Session] Shutting down');
        this.isShuttingDown = true;
        this.abortTaskRegistries();
        this.stopMonitorCallbacks();
        // Wait for all pending work
        await this.waitForAllPendingWork();
        this.abortTaskRegistries();
        this.finishShutdown();
    }
    async drainAndShutdown() {
        debugLogger.debug('[Session] Draining pending work before shutdown');
        // Abort monitors and stop callbacks first, then drain anything already
        // queued so EOF does not remain coupled to monitor process lifetime.
        this.abortTaskRegistries();
        this.stopMonitorCallbacks();
        await this.waitForAllPendingWork();
        this.abortTaskRegistries();
        this.finishShutdown();
    }
    abortTaskRegistries() {
        this.config.getMonitorRegistry().abortAll({ notify: false });
        this.config.getBackgroundShellRegistry().abortAll();
        this.config.getBackgroundTaskRegistry().abortAll();
    }
    finishShutdown() {
        this.dispatcher?.shutdown();
        this.cleanupSignalHandlers();
    }
    stopMonitorCallbacks() {
        if (!this.monitorNotificationsRegistered &&
            !this.monitorRegistrationsRegistered) {
            return;
        }
        const registry = this.config.getMonitorRegistry();
        if (this.monitorNotificationsRegistered) {
            registry.setNotificationCallback(undefined);
            this.monitorNotificationsRegistered = false;
        }
        if (this.monitorRegistrationsRegistered) {
            registry.setRegisterCallback(undefined);
            this.monitorRegistrationsRegistered = false;
        }
    }
    cleanupSignalHandlers() {
        if (this.shutdownHandler) {
            process.removeListener('SIGINT', this.shutdownHandler);
            process.removeListener('SIGTERM', this.shutdownHandler);
            this.shutdownHandler = null;
        }
    }
    /**
     * Main message processing loop
     *
     * CRITICAL: This loop must NEVER await handlers that might need to
     * send control requests and wait for responses. Such handlers must
     * be started in fire-and-forget mode, allowing the loop to continue
     * reading responses that resolve pending requests.
     *
     * Message handling order:
     * 1. control_response - FIRST, synchronously resolves pending requests
     * 2. First message - determines mode, starts async initialization
     * 3. control_request - fire-and-forget, tracked by dispatcher
     * 4. control_cancel - synchronous
     * 5. user_message - enqueued for processing
     */
    async run() {
        try {
            debugLogger.info('[Session] Starting session', this.sessionId);
            // Handle initial prompt if provided (fire-and-forget)
            if (this.initialPrompt !== null) {
                this.handleFirstMessage(this.initialPrompt);
            }
            try {
                for await (const message of this.inputReader.read()) {
                    if (this.abortController.signal.aborted) {
                        break;
                    }
                    // ============================================================
                    // CRITICAL: Handle control_response FIRST and SYNCHRONOUSLY
                    // This resolves pending outgoing requests, breaking deadlock.
                    // ============================================================
                    if (isControlResponse(message)) {
                        this.handleControlResponse(message);
                        continue;
                    }
                    // Handle first message to determine session mode
                    if (this.controlSystemEnabled === null) {
                        this.handleFirstMessage(message);
                        continue;
                    }
                    // ============================================================
                    // CRITICAL: Handle control_request in FIRE-AND-FORGET mode
                    // DON'T await - let handler run concurrently while loop continues
                    // Dispatcher's pendingIncomingRequests tracks completion
                    // ============================================================
                    if (isControlRequest(message)) {
                        this.handleControlRequestAsync(message);
                    }
                    else if (isControlCancel(message)) {
                        // Cancel is synchronous - OK to handle inline
                        this.handleControlCancel(message);
                    }
                    else if (isCLIUserMessage(message)) {
                        // User messages are enqueued, processing runs separately
                        this.enqueueUserMessage(message);
                    }
                    else if (!isCLIAssistantMessage(message) &&
                        !isCLISystemMessage(message) &&
                        !isCLIResultMessage(message) &&
                        !isCLIPartialAssistantMessage(message)) {
                        debugLogger.warn('[Session] Unknown message type:', JSON.stringify(message, null, 2));
                    }
                    if (this.isShuttingDown) {
                        break;
                    }
                }
            }
            catch (streamError) {
                debugLogger.error('[Session] Stream reading error:', streamError);
                throw streamError;
            }
            // Stdin closed - mark input as closed in dispatcher
            // This will reject all current pending outgoing requests AND any future requests
            // that might be registered by async message handlers still running
            if (this.dispatcher) {
                this.dispatcher.markInputClosed();
            }
            await this.drainAndShutdown();
        }
        catch (error) {
            debugLogger.error('[Session] Error:', error);
            await this.shutdown();
            throw error;
        }
        finally {
            this.cleanupSignalHandlers();
        }
    }
}
function extractUserMessageText(message) {
    const content = message.message.content;
    if (typeof content === 'string') {
        return content;
    }
    if (Array.isArray(content)) {
        const parts = content
            .map((block) => {
            if (!block || typeof block !== 'object') {
                return '';
            }
            if ('type' in block && block.type === 'text' && 'text' in block) {
                return typeof block.text === 'string' ? block.text : '';
            }
            return JSON.stringify(block);
        })
            .filter((part) => part.length > 0);
        return parts.length > 0 ? parts.join('\n') : null;
    }
    return null;
}
export async function runNonInteractiveStreamJson(config, input, settings = createMinimalSettings()) {
    let initialPrompt = undefined;
    if (input && input.trim().length > 0) {
        const sessionId = config.getSessionId();
        initialPrompt = {
            type: 'user',
            session_id: sessionId,
            message: {
                role: 'user',
                content: input.trim(),
            },
            parent_tool_use_id: null,
        };
    }
    const manager = new Session(config, initialPrompt, settings);
    await manager.run();
}
//# sourceMappingURL=session.js.map