/**
 * @license
 * Copyright 2026 HopCode Team Team
 * SPDX-License-Identifier: Apache-2.0
 */
import { createReadStream, watchFile, unwatchFile, statSync } from 'node:fs';
import { createInterface } from 'node:readline';
import { createDebugLogger } from '@hoptrendy/hopcode-core';
const debugLogger = createDebugLogger('REMOTE_INPUT');
/**
 * Watches a JSONL file for remote input commands and calls the registered
 * submit function when new commands arrive.
 *
 * The watcher queues commands and retries when the TUI is busy (responding).
 * Call `notifyIdle()` when the TUI transitions to idle state to trigger
 * processing of queued commands.
 */
export class RemoteInputWatcher {
    submitFn = null;
    confirmationHandler = null;
    queue = [];
    processing = false;
    active = true;
    bytesRead = 0;
    reading = false;
    filePath;
    retryTimer = null;
    pollIntervalMs;
    constructor(filePath, options) {
        this.filePath = filePath;
        this.pollIntervalMs = options?.pollIntervalMs ?? 500;
        this.startWatching();
    }
    /**
     * Register the TUI's submit function. Called from AppContainer
     * once useHopCodeStream's submitQuery is available.
     */
    setSubmitFn(fn) {
        this.submitFn = fn;
        this.processQueue();
    }
    /**
     * Register the handler invoked when a `confirmation_response` command is
     * read from the input file. Used to bridge external approvals back into
     * the tool's `onConfirm` callback.
     */
    setConfirmationHandler(fn) {
        this.confirmationHandler = fn;
    }
    /**
     * Notify the watcher that the TUI has become idle.
     * Call this when streamingState transitions to Idle — it triggers
     * processing of any queued commands that were deferred due to TUI busy.
     */
    notifyIdle() {
        if (this.queue.length > 0 && !this.processing) {
            this.processQueue();
        }
    }
    startWatching() {
        try {
            const stat = statSync(this.filePath);
            this.bytesRead = stat.size;
        }
        catch {
            this.bytesRead = 0;
        }
        watchFile(this.filePath, { interval: this.pollIntervalMs }, () => {
            if (!this.active)
                return;
            this.readNewLines();
        });
        debugLogger.debug(`RemoteInput: watching ${this.filePath}`);
    }
    /**
     * Manually trigger a check for new input. Returns a promise that resolves
     * once any new lines have been read and processed. In production the
     * `watchFile` poll calls this automatically; tests can call it directly
     * to avoid depending on filesystem-polling timing.
     */
    checkForNewInput() {
        return this.readNewLines();
    }
    readNewLines() {
        if (!this.active || this.reading)
            return Promise.resolve();
        let currentSize;
        try {
            const stat = statSync(this.filePath);
            currentSize = stat.size;
        }
        catch {
            return Promise.resolve();
        }
        if (currentSize <= this.bytesRead)
            return Promise.resolve();
        this.reading = true;
        const stream = createReadStream(this.filePath, {
            start: this.bytesRead,
            encoding: 'utf-8',
        });
        const rl = createInterface({ input: stream, crlfDelay: Infinity });
        rl.on('line', (line) => {
            const trimmed = line.trim();
            if (!trimmed)
                return;
            try {
                const cmd = JSON.parse(trimmed);
                // confirmation_response is dispatched immediately rather than queued:
                // a pending tool call is blocking and the response must reach
                // onConfirm without waiting for any earlier `submit` to finish.
                if (cmd &&
                    cmd.type === 'confirmation_response' &&
                    typeof cmd.request_id === 'string' &&
                    typeof cmd.allowed === 'boolean') {
                    debugLogger.debug(`RemoteInput: confirmation_response for ${cmd.request_id} (allowed=${cmd.allowed})`);
                    this.confirmationHandler?.(cmd.request_id, cmd.allowed);
                }
                else if (cmd &&
                    cmd.type === 'submit' &&
                    typeof cmd.text === 'string') {
                    debugLogger.debug(`RemoteInput: queued command: ${cmd.text.slice(0, 50)}...`);
                    this.queue.push(cmd);
                }
                else {
                    debugLogger.warn(`RemoteInput: unknown command type: ${String(cmd?.type)}`);
                }
            }
            catch (_err) {
                debugLogger.warn(`RemoteInput: failed to parse line: ${trimmed}`);
            }
        });
        return new Promise((resolve) => {
            rl.on('close', () => {
                this.bytesRead = currentSize;
                this.reading = false;
                this.processQueue();
                resolve();
            });
        });
    }
    async processQueue() {
        if (this.processing || !this.submitFn || this.queue.length === 0)
            return;
        this.processing = true;
        if (this.retryTimer) {
            clearTimeout(this.retryTimer);
            this.retryTimer = null;
        }
        try {
            while (this.queue.length > 0 && this.active) {
                if (!this.submitFn)
                    break;
                const cmd = this.queue[0]; // peek, don't shift yet
                debugLogger.debug(`RemoteInput: submitting: ${cmd.text.slice(0, 50)}...`);
                try {
                    const result = await this.submitFn(cmd.text);
                    // If submitFn returns false explicitly, the TUI rejected it (busy)
                    if (result === false) {
                        debugLogger.debug('RemoteInput: TUI busy, will retry on idle');
                        this.scheduleRetry();
                        break;
                    }
                    // Success — remove from queue
                    this.queue.shift();
                }
                catch (err) {
                    debugLogger.error('RemoteInput: submit failed:', err);
                    this.queue.shift(); // remove failed command to avoid infinite retry
                }
                // Small delay between commands to let the TUI process
                if (this.queue.length > 0) {
                    await new Promise((r) => setTimeout(r, 500));
                }
            }
        }
        finally {
            this.processing = false;
        }
    }
    scheduleRetry() {
        if (this.retryTimer)
            return;
        // Retry after 2s if notifyIdle hasn't been called
        this.retryTimer = setTimeout(() => {
            this.retryTimer = null;
            if (this.queue.length > 0 && !this.processing) {
                this.processQueue();
            }
        }, 2000);
    }
    shutdown() {
        this.active = false;
        unwatchFile(this.filePath);
        if (this.retryTimer)
            clearTimeout(this.retryTimer);
        this.queue.length = 0;
        debugLogger.debug('RemoteInput: shut down');
    }
}
//# sourceMappingURL=RemoteInputWatcher.js.map