/**
 * @license
 * Copyright 2026 HopCode Team Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { createWriteStream, fstatSync, openSync, constants, } from 'node:fs';
import { createDebugLogger } from '@hoptrendy/hopcode-core';
import { StreamJsonOutputAdapter } from '../nonInteractive/io/index.js';
const debugLogger = createDebugLogger('DUAL_OUTPUT');
/**
 * Structured-event kinds this bridge version is known to emit. Exposed to
 * consumers in `session_start.data.supported_events` so they can
 * feature-detect rather than sniffing the stream or hard-coding a minimum
 * CLI version.
 *
 * When adding a new event kind, append it here and bump the handshake
 * `protocol_version` below so consumers can gate on the combination.
 */
export const SUPPORTED_EVENTS = [
    'system',
    'user',
    'assistant',
    'stream_event',
    'result',
    'control_request',
    'control_response',
];
/**
 * Monotonically-increasing integer bumped whenever the wire protocol
 * changes in a way consumers might care about (new event types,
 * new payload fields that are not purely additive, etc.).
 *
 * History:
 *   1 — initial release (session_start, session_end, full stream-json).
 */
export const DUAL_OUTPUT_PROTOCOL_VERSION = 1;
/**
 * Bridges TUI-mode events to a sidecar StreamJsonOutputAdapter that writes
 * structured JSON events to a secondary output channel (fd or file).
 *
 * This enables "dual output" mode: the TUI renders normally on stdout while
 * a parallel JSON event stream is emitted on a separate channel for
 * programmatic consumption by IDE extensions, web frontends, CI pipelines, etc.
 *
 * Usage:
 *   hopcode --json-fd 3        # JSON events written to fd 3
 *   hopcode --json-file /path  # JSON events written to file/FIFO
 */
export class DualOutputBridge {
    adapter;
    stream;
    sessionId;
    active = true;
    shutdownPromise = null;
    constructor(config, target, options = {}) {
        this.sessionId = config.getSessionId();
        if ('fd' in target) {
            // Reject stdin/stdout/stderr to prevent corrupting TUI output
            if (target.fd <= 2) {
                throw new Error(`--json-fd ${target.fd}: file descriptors 0 (stdin), 1 (stdout), and 2 (stderr) ` +
                    'are reserved. Use fd 3 or higher.');
            }
            // Validate fd is open before attempting to use it
            try {
                fstatSync(target.fd);
            }
            catch {
                throw new Error(`--json-fd ${target.fd}: file descriptor is not open. ` +
                    'The caller must provide this fd via spawn stdio configuration ' +
                    'or shell redirection (e.g., 3>/tmp/events.jsonl).');
            }
            this.stream = createWriteStream('', { fd: target.fd });
        }
        else {
            // Open with O_WRONLY|O_NONBLOCK to avoid blocking the event loop on FIFOs.
            // On FIFO, a regular open(O_WRONLY) blocks until a reader connects.
            // O_NONBLOCK makes it return immediately (ENXIO if no reader yet, which
            // createWriteStream handles via its internal retry/error mechanism).
            try {
                const fd = openSync(target.filePath, constants.O_WRONLY | constants.O_NONBLOCK);
                this.stream = createWriteStream('', { fd });
            }
            catch (err) {
                const code = err.code;
                // ENXIO: FIFO has no reader yet — fall back to blocking open.
                // ENOENT: regular file doesn't exist yet — create it.
                if (code === 'ENXIO' || code === 'ENOENT') {
                    this.stream = createWriteStream(target.filePath, { flags: 'w' });
                }
                else {
                    throw err;
                }
            }
        }
        this.stream.on('error', (err) => {
            const code = err.code;
            // Consumer disconnected — gracefully stop writing, don't crash the TUI
            if (code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED') {
                debugLogger.warn('DualOutput: consumer disconnected, disabling');
            }
            else {
                debugLogger.error('DualOutput stream error:', err);
            }
            // Disable on any stream error to prevent repeated write failures
            this.active = false;
        });
        this.adapter = new StreamJsonOutputAdapter(config, true, // includePartialMessages — always emit streaming events
        this.stream);
        // Announce the session immediately so consumers can correlate the channel
        // with a session before any other event arrives. The data payload also
        // serves as a capability handshake: consumers can read `protocol_version`
        // and `supported_events` to feature-detect without sniffing the stream.
        try {
            this.adapter.emitSystemMessage('session_start', {
                session_id: this.sessionId,
                cwd: process.cwd(),
                protocol_version: DUAL_OUTPUT_PROTOCOL_VERSION,
                version: options.version,
                supported_events: [...SUPPORTED_EVENTS],
            });
        }
        catch (err) {
            debugLogger.error('DualOutput session_start error:', err);
            this.active = false;
        }
    }
    processEvent(event) {
        if (!this.active)
            return;
        try {
            this.adapter.processEvent(event);
        }
        catch (err) {
            debugLogger.error('DualOutput processEvent error:', err);
            this.active = false;
        }
    }
    startAssistantMessage() {
        if (!this.active)
            return;
        try {
            this.adapter.startAssistantMessage();
        }
        catch (err) {
            debugLogger.error('DualOutput startAssistantMessage error:', err);
            this.active = false;
        }
    }
    finalizeAssistantMessage() {
        if (!this.active)
            return;
        try {
            this.adapter.finalizeAssistantMessage();
        }
        catch (err) {
            debugLogger.error('DualOutput finalizeAssistantMessage error:', err);
            this.active = false;
        }
    }
    emitUserMessage(parts) {
        if (!this.active)
            return;
        try {
            this.adapter.emitUserMessage(parts);
        }
        catch (err) {
            debugLogger.error('DualOutput emitUserMessage error:', err);
            this.active = false;
        }
    }
    emitToolResult(request, response) {
        if (!this.active)
            return;
        try {
            this.adapter.emitToolResult(request, response);
        }
        catch (err) {
            debugLogger.error('DualOutput emitToolResult error:', err);
            this.active = false;
        }
    }
    /** Whether the underlying stream is still writable. */
    get isConnected() {
        return this.active;
    }
    /**
     * Emits a `can_use_tool` permission request so an external consumer can
     * approve or deny the tool call. Pairs with {@link emitControlResponse}.
     */
    emitPermissionRequest(requestId, toolName, toolUseId, input, blockedPath = null) {
        if (!this.active)
            return;
        try {
            this.adapter.emitPermissionRequest(requestId, toolName, toolUseId, input, blockedPath);
        }
        catch (err) {
            debugLogger.error('DualOutput emitPermissionRequest error:', err);
            this.active = false;
        }
    }
    /**
     * Emits the result of a permission decision (made either in the TUI or by
     * the external consumer) so all observers stay in sync.
     */
    emitControlResponse(requestId, allowed) {
        if (!this.active)
            return;
        try {
            this.adapter.emitControlResponse(requestId, allowed);
        }
        catch (err) {
            debugLogger.error('DualOutput emitControlResponse error:', err);
            this.active = false;
        }
    }
    /**
     * Emits a `control_response` with subtype `error` — used when an external
     * `confirmation_response` cannot be satisfied (unknown request_id, the
     * tool call already resolved, stream already closed, etc.). Lets
     * consumers retry or surface the error instead of silently hanging.
     */
    emitControlError(requestId, message) {
        if (!this.active)
            return;
        try {
            this.adapter.emitControlError(requestId, message);
        }
        catch (err) {
            debugLogger.error('DualOutput emitControlError error:', err);
            this.active = false;
        }
    }
    /** General-purpose system event escape hatch. */
    emitSystemMessage(subtype, data) {
        if (!this.active)
            return;
        try {
            this.adapter.emitSystemMessage(subtype, data);
        }
        catch (err) {
            debugLogger.error('DualOutput emitSystemMessage error:', err);
            this.active = false;
        }
    }
    shutdown() {
        if (this.shutdownPromise)
            return this.shutdownPromise;
        // Try to emit session_end before tearing the stream down so consumers
        // get a definitive termination signal rather than inferring it from
        // EPIPE. Failures here are swallowed — the stream may already be in an
        // error state if the consumer disconnected first.
        if (this.active) {
            try {
                this.adapter.emitSystemMessage('session_end', {
                    session_id: this.sessionId,
                });
            }
            catch {
                // ignore — stream likely already closed
            }
        }
        this.active = false;
        this.shutdownPromise = new Promise((resolve) => {
            if (this.stream.closed) {
                resolve();
                return;
            }
            const cleanup = () => {
                this.stream.off('close', onClose);
                this.stream.off('error', onError);
            };
            const onClose = () => {
                cleanup();
                resolve();
            };
            const onError = (err) => {
                debugLogger.debug('DualOutput: stream error during shutdown:', err);
            };
            this.stream.once('close', onClose);
            this.stream.once('error', onError);
            try {
                this.stream.end();
            }
            catch (err) {
                cleanup();
                debugLogger.debug('DualOutput: stream end error during shutdown:', err);
                resolve();
            }
        });
        return this.shutdownPromise;
    }
}
//# sourceMappingURL=DualOutputBridge.js.map