/**
 * SSE API Route: GET /api/feature-logs
 *
 * Streams feature agent log output to the client via Server-Sent Events.
 * Resolves the feature to find its agentRunId, reads the corresponding
 * worker log file, and watches for new content.
 *
 * - Accepts ?featureId query parameter (required)
 * - Sends existing log content as SSE "initial" event
 * - Sends new log content as SSE "log" events
 * - Sends heartbeat comments every 30 seconds to keep connection alive
 * - Cleans up watchers/intervals on client disconnect
 */
import { resolve } from '../../../lib/server-container.js';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { readFile, stat } from 'node:fs/promises';
import { watch } from 'node:fs';
// Force dynamic — SSE streams must never be statically optimized or cached
export const dynamic = 'force-dynamic';
const HEARTBEAT_INTERVAL_MS = 30_000;
const POLL_INTERVAL_MS = 2_000;
/**
 * Create a short-lived SSE error response.
 */
function sseError(error) {
    const stream = new ReadableStream({
        start(controller) {
            const encoder = new TextEncoder();
            controller.enqueue(encoder.encode(`event: error\ndata: ${JSON.stringify({ error })}\n\n`));
            controller.close();
        },
    });
    return new Response(stream, {
        headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            Connection: 'keep-alive',
        },
    });
}
export async function GET(request) {
    try {
        const url = new URL(request.url);
        const featureId = url.searchParams.get('featureId');
        if (!featureId?.trim()) {
            return sseError('featureId is required');
        }
        const featureRepo = resolve('IFeatureRepository');
        const feature = await featureRepo.findById(featureId);
        if (!feature) {
            return sseError(`Feature not found: ${featureId}`);
        }
        if (!feature.agentRunId) {
            return sseError(`Feature "${feature.name}" has no agent run`);
        }
        const logPath = join(homedir(), '.shep', 'logs', `worker-${feature.agentRunId}.log`);
        const stream = new ReadableStream({
            start(controller) {
                const encoder = new TextEncoder();
                let stopped = false;
                let bytesRead = 0;
                let watcher = null;
                let pollInterval = null;
                function enqueue(text) {
                    if (stopped)
                        return;
                    try {
                        controller.enqueue(encoder.encode(text));
                    }
                    catch {
                        // Stream may be closed
                    }
                }
                /**
                 * Read any new content from the log file starting at `bytesRead`.
                 * Returns the content read, or null if the file doesn't exist yet.
                 */
                async function readNewContent() {
                    try {
                        const fileStat = await stat(logPath);
                        if (fileStat.size <= bytesRead)
                            return null;
                        const content = await readFile(logPath, 'utf-8');
                        const newContent = content.slice(bytesRead);
                        bytesRead = content.length;
                        return newContent || null;
                    }
                    catch {
                        // File doesn't exist yet — agent may have just started
                        return null;
                    }
                }
                /**
                 * Check for new content and send it as a log event.
                 */
                async function checkForUpdates() {
                    if (stopped)
                        return;
                    try {
                        const content = await readNewContent();
                        if (content) {
                            enqueue(`event: log\ndata: ${JSON.stringify({ content })}\n\n`);
                        }
                    }
                    catch {
                        // Ignore read errors during polling
                    }
                }
                // Initialize: read existing content and set up watchers
                (async () => {
                    try {
                        // Send existing content as initial event
                        const initialContent = await readNewContent();
                        if (initialContent) {
                            enqueue(`event: initial\ndata: ${JSON.stringify({ content: initialContent })}\n\n`);
                        }
                        if (stopped)
                            return;
                        // Watch for file changes with fs.watch
                        try {
                            watcher = watch(logPath, { persistent: false }, () => {
                                void checkForUpdates();
                            });
                            watcher.on('error', () => {
                                // Watcher error — fallback poll will handle it
                            });
                        }
                        catch {
                            // File may not exist yet — poll will handle it
                        }
                        // Fallback poll every 2s (handles cases where fs.watch misses events
                        // or the file doesn't exist yet)
                        pollInterval = setInterval(() => {
                            void checkForUpdates();
                        }, POLL_INTERVAL_MS);
                    }
                    catch {
                        // Initialization error — stream will still stay open for heartbeats
                    }
                })();
                // Heartbeat to keep connection alive
                const heartbeatInterval = setInterval(() => {
                    enqueue(': heartbeat\n\n');
                }, HEARTBEAT_INTERVAL_MS);
                // Cleanup on client disconnect
                const cleanup = () => {
                    stopped = true;
                    if (watcher) {
                        watcher.close();
                        watcher = null;
                    }
                    if (pollInterval) {
                        clearInterval(pollInterval);
                        pollInterval = null;
                    }
                    clearInterval(heartbeatInterval);
                    try {
                        controller.close();
                    }
                    catch {
                        // Stream may already be closed
                    }
                };
                request.signal.addEventListener('abort', cleanup, { once: true });
            },
        });
        return new Response(stream, {
            headers: {
                'Content-Type': 'text/event-stream',
                'Cache-Control': 'no-cache',
                Connection: 'keep-alive',
            },
        });
    }
    catch (error) {
        // eslint-disable-next-line no-console
        console.error('[SSE route] GET /api/feature-logs error:', error);
        return new Response(JSON.stringify({ error: String(error) }), {
            status: 500,
            headers: { 'Content-Type': 'application/json' },
        });
    }
}
