/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import http from 'node:http';
import { randomUUID } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { EventEmitter } from 'node:events';
import { WebSocketServer } from 'ws';
const __dirname = dirname(fileURLToPath(import.meta.url));
const indexHtml = readFileSync(join(__dirname, '../../client/index.html'), 'utf-8');
const mainJs = readFileSync(join(__dirname, '../client/main.js'), 'utf-8');
/**
 * DevTools Viewer
 *
 * Receives logs via WebSocket from CLI sessions.
 */
export class DevTools extends EventEmitter {
    static instance;
    logs = [];
    consoleLogs = [];
    server = null;
    wss = null;
    sessions = new Map();
    heartbeatTimer = null;
    port = 25417;
    constructor() {
        super();
    }
    static getInstance() {
        if (!DevTools.instance) {
            DevTools.instance = new DevTools();
        }
        return DevTools.instance;
    }
    addInternalConsoleLog(payload, sessionId, timestamp) {
        const entry = {
            ...payload,
            id: randomUUID(),
            sessionId,
            timestamp: timestamp || Date.now(),
        };
        this.consoleLogs.push(entry);
        if (this.consoleLogs.length > 5000)
            this.consoleLogs.shift();
        this.emit('console-update', entry);
    }
    addInternalNetworkLog(payload, sessionId, timestamp) {
        if (!payload.id)
            return;
        const existingIndex = this.logs.findIndex((l) => l.id === payload.id);
        if (existingIndex > -1) {
            const existing = this.logs[existingIndex];
            // Handle chunk accumulation
            if (payload.chunk) {
                const chunks = existing.chunks || [];
                chunks.push(payload.chunk);
                this.logs[existingIndex] = {
                    ...existing,
                    chunks,
                    sessionId: sessionId || existing.sessionId,
                };
            }
            else {
                this.logs[existingIndex] = {
                    ...existing,
                    ...payload,
                    sessionId: sessionId || existing.sessionId,
                    chunks: existing.chunks, // Preserve chunks
                    response: payload.response
                        ? { ...existing.response, ...payload.response }
                        : existing.response,
                };
            }
            this.emit('update', this.logs[existingIndex]);
        }
        else if (payload.url) {
            const entry = {
                ...payload,
                sessionId,
                timestamp: timestamp || Date.now(),
                chunks: payload.chunk ? [payload.chunk] : undefined,
            };
            this.logs.push(entry);
            if (this.logs.length > 2000)
                this.logs.shift();
            this.emit('update', entry);
        }
    }
    getUrl() {
        return `http://127.0.0.1:${this.port}`;
    }
    getPort() {
        return this.port;
    }
    stop() {
        return new Promise((resolve) => {
            if (this.heartbeatTimer) {
                clearInterval(this.heartbeatTimer);
                this.heartbeatTimer = null;
            }
            if (this.wss) {
                this.wss.close();
                this.wss = null;
            }
            if (this.server) {
                this.server.close(() => resolve());
                this.server = null;
            }
            else {
                resolve();
            }
            // Reset singleton so a fresh start() is possible
            DevTools.instance = undefined;
        });
    }
    start() {
        return new Promise((resolve) => {
            if (this.server) {
                resolve(this.getUrl());
                return;
            }
            this.server = http.createServer((req, res) => {
                res.setHeader('Access-Control-Allow-Origin', '*');
                // API routes
                if (req.url === '/events') {
                    res.writeHead(200, {
                        'Content-Type': 'text/event-stream',
                        'Cache-Control': 'no-cache',
                        Connection: 'keep-alive',
                    });
                    // Send full snapshot on connect
                    const snapshot = JSON.stringify({
                        networkLogs: this.logs,
                        consoleLogs: this.consoleLogs,
                        sessions: Array.from(this.sessions.keys()),
                    });
                    res.write(`event: snapshot\ndata: ${snapshot}\n\n`);
                    // Incremental updates
                    const onNetwork = (log) => {
                        res.write(`event: network\ndata: ${JSON.stringify(log)}\n\n`);
                    };
                    const onConsole = (log) => {
                        res.write(`event: console\ndata: ${JSON.stringify(log)}\n\n`);
                    };
                    const onSession = () => {
                        const sessions = Array.from(this.sessions.keys());
                        res.write(`event: session\ndata: ${JSON.stringify(sessions)}\n\n`);
                    };
                    this.on('update', onNetwork);
                    this.on('console-update', onConsole);
                    this.on('session-update', onSession);
                    req.on('close', () => {
                        this.off('update', onNetwork);
                        this.off('console-update', onConsole);
                        this.off('session-update', onSession);
                    });
                }
                else if (req.url === '/' || req.url === '/index.html') {
                    res.writeHead(200, { 'Content-Type': 'text/html' });
                    res.end(indexHtml);
                }
                else if (req.url === '/assets/main.js') {
                    res.writeHead(200, { 'Content-Type': 'application/javascript' });
                    res.end(mainJs);
                }
                else {
                    res.writeHead(404);
                    res.end('Not Found');
                }
            });
            this.server.on('error', (e) => {
                if (typeof e === 'object' &&
                    e !== null &&
                    'code' in e &&
                    e.code === 'EADDRINUSE') {
                    this.port++;
                    this.server?.listen(this.port, '127.0.0.1');
                }
            });
            this.server.listen(this.port, '127.0.0.1', () => {
                this.setupWebSocketServer();
                resolve(this.getUrl());
            });
        });
    }
    setupWebSocketServer() {
        if (!this.server)
            return;
        this.wss = new WebSocketServer({ server: this.server, path: '/ws' });
        this.wss.on('connection', (ws) => {
            let sessionId = null;
            ws.on('message', (data) => {
                try {
                    const message = JSON.parse(data.toString());
                    // Handle registration first
                    if (message.type === 'register') {
                        sessionId = message.sessionId;
                        this.sessions.set(sessionId, {
                            sessionId,
                            ws,
                            lastPing: Date.now(),
                        });
                        // Notify session update
                        this.emit('session-update');
                        // Send registration acknowledgement
                        ws.send(JSON.stringify({
                            type: 'registered',
                            sessionId,
                            timestamp: Date.now(),
                        }));
                    }
                    else if (sessionId) {
                        this.handleWebSocketMessage(sessionId, message);
                    }
                }
                catch {
                    // Invalid WebSocket message
                }
            });
            ws.on('close', () => {
                if (sessionId) {
                    this.sessions.delete(sessionId);
                    this.emit('session-update');
                }
            });
            ws.on('error', () => {
                // WebSocket error — no action needed
            });
        });
        // Heartbeat mechanism
        this.heartbeatTimer = setInterval(() => {
            const now = Date.now();
            this.sessions.forEach((session, sessionId) => {
                if (now - session.lastPing > 30000) {
                    session.ws.close();
                    this.sessions.delete(sessionId);
                }
                else {
                    // Send ping
                    session.ws.send(JSON.stringify({ type: 'ping', timestamp: now }));
                }
            });
        }, 10000);
    }
    handleWebSocketMessage(sessionId, message) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return;
        switch (message.type) {
            case 'pong':
                session.lastPing = Date.now();
                break;
            case 'console':
                this.addInternalConsoleLog(message.payload, sessionId, message.timestamp);
                break;
            case 'network':
                this.addInternalNetworkLog(message.payload, sessionId, message.timestamp);
                break;
            default:
                break;
        }
    }
}
