import { randomUUID } from 'node:crypto';
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { N8nApiClient, WorkspaceSetupService, getDisplayProjectName, } from 'n8nac';
import { Command } from 'commander';
import { UpdateAiCommand } from 'n8nac/dist/commands/init-ai.js';
import { YagrAgent } from '../agent.js';
import { resolveWorkflowDir, YagrN8nConfigService } from '../config/n8n-config-service.js';
import { YagrConfigService } from '../config/yagr-config-service.js';
import { getYagrN8nWorkspaceDir } from '../config/yagr-home.js';
import { createOnboardingToken, getTelegramGatewayStatus, resetTelegramGateway, } from './telegram.js';
import { getYagrSetupStatus } from '../setup.js';
import { resolveLanguageModelConfig } from '../llm/create-language-model.js';
import { fetchAvailableModels } from '../llm/provider-discovery.js';
import { providerRequiresApiKey, YAGR_SELECTABLE_MODEL_PROVIDERS, } from '../llm/provider-registry.js';
import { resolveManagedN8nWorkflowOpen } from '../n8n-local/workflow-open.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PORT = 3789;
const VALID_PROVIDERS = [...YAGR_SELECTABLE_MODEL_PROVIDERS];
const ACTIVE_WEBUI_SURFACES = ['webui'];
const WEB_UI_HTML = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Yagr Web UI</title>
    <link rel="stylesheet" href="/styles.css" />
    <script defer src="/app.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
  </html>`;
function isAbortError(error) {
    return error instanceof Error && error.name === 'AbortError';
}
function sanitizeHost(value) {
    const trimmed = value?.trim();
    return trimmed || DEFAULT_HOST;
}
function sanitizePort(value) {
    if (!Number.isInteger(value) || Number(value) <= 0 || Number(value) > 65535) {
        return DEFAULT_PORT;
    }
    return Number(value);
}
function getWebUiConfig(configService = new YagrConfigService()) {
    const config = configService.getLocalConfig();
    return {
        host: sanitizeHost(config.gateway?.webui?.host),
        port: sanitizePort(config.gateway?.webui?.port),
    };
}
function persistWebUiConfig(configService = new YagrConfigService(), nextConfig) {
    const normalized = {
        host: sanitizeHost(nextConfig.host),
        port: sanitizePort(nextConfig.port),
    };
    configService.updateLocalConfig((localConfig) => ({
        ...localConfig,
        gateway: {
            ...localConfig.gateway,
            webui: normalized,
        },
    }));
    return normalized;
}
export function getWebUiGatewayStatus(configService = new YagrConfigService()) {
    const config = persistWebUiConfig(configService, getWebUiConfig(configService));
    return {
        configured: true,
        host: config.host,
        port: config.port,
        url: `http://${config.host}:${config.port}`,
    };
}
export function createWebUiGatewayRuntime(engineResolver, options = {}, configService = new YagrConfigService()) {
    const status = getWebUiGatewayStatus(configService);
    return {
        gateway: new WebUiGateway(engineResolver, options, configService, status),
        startupMessages: [
            `Yagr Web UI listening at ${status.url}.`,
            'Open the local UI to configure the runtime, link Telegram, and chat with Yagr.',
        ],
        onboardingLink: status.url,
    };
}
class WebUiGateway {
    engineResolver;
    options;
    configService;
    status;
    server;
    enginePromise;
    agents = new Map();
    constructor(engineResolver, options, configService, status) {
        this.engineResolver = engineResolver;
        this.options = options;
        this.configService = configService;
        this.status = status;
    }
    async start() {
        if (this.server) {
            return;
        }
        this.server = createServer(async (request, response) => {
            try {
                await this.handleRequest(request, response);
            }
            catch (error) {
                this.sendJson(response, 500, {
                    error: error instanceof Error ? error.message : String(error),
                });
            }
        });
        await new Promise((resolve, reject) => {
            this.server?.once('error', reject);
            this.server?.listen(this.status.port, this.status.host, () => resolve());
        });
    }
    async stop() {
        if (!this.server) {
            return;
        }
        const server = this.server;
        this.server = undefined;
        await new Promise((resolve, reject) => {
            server.close((error) => {
                if (error) {
                    reject(error);
                    return;
                }
                resolve();
            });
        });
    }
    async reply() { }
    async handleRequest(request, response) {
        const method = request.method ?? 'GET';
        const url = new URL(request.url ?? '/', this.status.url);
        if (method === 'GET' && url.pathname === '/') {
            this.sendText(response, 200, WEB_UI_HTML, 'text/html; charset=utf-8');
            return;
        }
        if (method === 'GET' && (url.pathname === '/styles.css' || url.pathname === '/app.css')) {
            await this.sendStaticAsset(response, 'styles.css', 'text/css; charset=utf-8');
            return;
        }
        if (method === 'GET' && url.pathname === '/app.js') {
            await this.sendStaticAsset(response, 'app.js', 'application/javascript; charset=utf-8');
            return;
        }
        if (method === 'GET' && url.pathname === '/api/config') {
            this.sendJson(response, 200, await this.buildSnapshot());
            return;
        }
        if (method === 'POST' && url.pathname === '/api/n8n/projects') {
            const body = await this.readJson(request);
            const projects = await this.fetchN8nProjects(String(body.host ?? ''), body.apiKey ? String(body.apiKey) : undefined);
            const current = new YagrN8nConfigService().getLocalConfig();
            this.sendJson(response, 200, {
                projects: projects.map((project) => ({ id: project.id, name: getDisplayProjectName(project) })),
                selectedProjectId: current.projectId,
            });
            return;
        }
        if (method === 'POST' && url.pathname === '/api/config/n8n') {
            const body = await this.readJson(request);
            const warning = await this.saveN8nConfig({
                host: String(body.host ?? ''),
                apiKey: body.apiKey ? String(body.apiKey) : undefined,
                projectId: String(body.projectId ?? ''),
                syncFolder: String(body.syncFolder ?? 'workflows'),
            });
            this.sendJson(response, 200, {
                warning,
                snapshot: await this.buildSnapshot(),
            });
            return;
        }
        if (method === 'POST' && url.pathname === '/api/llm/models') {
            const body = await this.readJson(request);
            const provider = this.assertProvider(String(body.provider ?? ''));
            const apiKey = body.apiKey !== undefined ? String(body.apiKey) : this.configService.getApiKey(provider);
            if (providerRequiresApiKey(provider) && !apiKey) {
                throw new Error(`No API key available for ${provider}. Save one first.`);
            }
            const configuredLlm = this.configService.getLocalConfig();
            const baseUrl = body.baseUrl
                ? String(body.baseUrl)
                : configuredLlm.provider === provider
                    ? configuredLlm.baseUrl
                    : undefined;
            this.sendJson(response, 200, {
                models: await fetchAvailableModels(provider, apiKey, baseUrl),
            });
            return;
        }
        if (method === 'POST' && url.pathname === '/api/config/llm') {
            const body = await this.readJson(request);
            const provider = this.assertProvider(String(body.provider ?? ''));
            const apiKey = body.apiKey ? String(body.apiKey) : undefined;
            const model = String(body.model ?? '').trim();
            if (!model) {
                throw new Error('Model is required.');
            }
            if (apiKey) {
                this.configService.saveApiKey(provider, apiKey);
            }
            this.configService.updateLocalConfig((localConfig) => ({
                ...localConfig,
                provider,
                model,
                baseUrl: body.baseUrl ? String(body.baseUrl) : undefined,
            }));
            this.sendJson(response, 200, { snapshot: await this.buildSnapshot() });
            return;
        }
        if (method === 'POST' && url.pathname === '/api/config/surfaces') {
            const body = await this.readJson(request);
            const enabledSurfaces = Array.isArray(body.enabledSurfaces)
                ? body.enabledSurfaces.filter((surface) => surface === 'telegram' || surface === 'whatsapp')
                : [];
            this.configService.setEnabledGatewaySurfaces(enabledSurfaces);
            this.sendJson(response, 200, { snapshot: await this.buildSnapshot() });
            return;
        }
        if (method === 'POST' && url.pathname === '/api/telegram/configure') {
            const body = await this.readJson(request);
            const botToken = String(body.botToken ?? '').trim();
            if (!botToken || !botToken.includes(':')) {
                throw new Error('Enter a valid Telegram BotFather token.');
            }
            const identity = await resolveTelegramBotIdentity(botToken);
            this.configService.saveTelegramBotToken(botToken);
            this.configService.enableGatewaySurface('telegram');
            this.configService.updateLocalConfig((localConfig) => ({
                ...localConfig,
                telegram: {
                    ...localConfig.telegram,
                    botUsername: identity.username,
                    onboardingToken: localConfig.telegram?.onboardingToken ?? createOnboardingToken(),
                    linkedChats: localConfig.telegram?.linkedChats ?? [],
                },
            }));
            this.sendJson(response, 200, { snapshot: await this.buildSnapshot() });
            return;
        }
        if (method === 'POST' && url.pathname === '/api/telegram/reset') {
            resetTelegramGateway(this.configService);
            this.sendJson(response, 200, { snapshot: await this.buildSnapshot() });
            return;
        }
        if (method === 'POST' && url.pathname === '/api/chat') {
            const body = await this.readJson(request);
            const message = String(body.message ?? '').trim();
            const sessionId = String(body.sessionId ?? randomUUID());
            if (!message) {
                throw new Error('Message is required.');
            }
            const setupStatus = getYagrSetupStatus(this.configService, new YagrN8nConfigService(), {
                activeSurfaces: [...ACTIVE_WEBUI_SURFACES],
            });
            if (!setupStatus.ready) {
                throw new Error(`Yagr is not ready yet. Missing: ${setupStatus.missingSteps.join(', ')}.`);
            }
            const agent = await this.resolveAgent(sessionId);
            const resolvedConfig = resolveLanguageModelConfig({}, this.configService);
            const result = await agent.run(message, {
                ...this.options,
                provider: resolvedConfig.provider,
                model: resolvedConfig.model,
                apiKey: resolvedConfig.apiKey,
                baseUrl: resolvedConfig.baseUrl,
            });
            this.sendJson(response, 200, {
                sessionId,
                response: result.text,
                requiredActions: result.requiredActions,
                finalState: result.finalState,
            });
            return;
        }
        if (method === 'POST' && url.pathname === '/api/chat/stream') {
            const body = await this.readJson(request);
            const message = String(body.message ?? '').trim();
            const sessionId = String(body.sessionId ?? randomUUID());
            if (!message) {
                throw new Error('Message is required.');
            }
            await this.handleStreamingChat(response, sessionId, message);
            return;
        }
        if (method === 'POST' && url.pathname === '/api/chat/reset') {
            const body = await this.readJson(request);
            const sessionId = String(body.sessionId ?? '');
            if (sessionId) {
                this.agents.delete(sessionId);
            }
            this.sendJson(response, 200, { ok: true });
            return;
        }
        if (method === 'GET' && url.pathname === '/api/n8n/workflow-session') {
            const target = String(url.searchParams.get('target') ?? '').trim();
            await this.sendManagedN8nWorkflowSession(response, target);
            return;
        }
        if (method === 'GET' && url.pathname === '/open/n8n-workflow') {
            const target = String(url.searchParams.get('target') ?? '').trim();
            await this.openManagedN8nWorkflow(response, target);
            return;
        }
        this.sendJson(response, 404, { error: 'Not found' });
    }
    async buildSnapshot() {
        const n8nService = new YagrN8nConfigService();
        const n8nConfig = n8nService.getLocalConfig();
        const setupStatus = getYagrSetupStatus(this.configService, n8nService, {
            activeSurfaces: [...ACTIVE_WEBUI_SURFACES],
        });
        const telegramStatus = getTelegramGatewayStatus(this.configService);
        const webUiStatus = getWebUiGatewayStatus(this.configService);
        const yagrConfig = this.configService.getLocalConfig();
        const enabledSurfaces = Array.from(new Set([...this.configService.getEnabledGatewaySurfaces(), ...ACTIVE_WEBUI_SURFACES]));
        const startableSurfaces = enabledSurfaces.filter((surface) => surface === 'webui' || (surface === 'telegram' && telegramStatus.configured));
        let availableModels = [];
        if (yagrConfig.provider) {
            const apiKey = this.configService.getApiKey(yagrConfig.provider);
            try {
                availableModels = await fetchAvailableModels(yagrConfig.provider, apiKey, yagrConfig.baseUrl);
            }
            catch {
                availableModels = [];
            }
        }
        return {
            setupStatus,
            gatewayStatus: {
                enabledSurfaces,
                startableSurfaces,
            },
            telegram: telegramStatus,
            webui: webUiStatus,
            yagr: {
                provider: yagrConfig.provider,
                model: yagrConfig.model,
                baseUrl: yagrConfig.baseUrl,
                providers: VALID_PROVIDERS.map((provider) => ({
                    provider,
                    apiKeyStored: this.configService.hasApiKey(provider),
                })),
            },
            n8n: {
                host: n8nConfig.host,
                syncFolder: n8nConfig.syncFolder,
                projectId: n8nConfig.projectId,
                projectName: n8nConfig.projectName,
                apiKeyStored: Boolean(n8nConfig.host && n8nService.getApiKey(n8nConfig.host)),
                projects: n8nConfig.projectId && n8nConfig.projectName ? [{ id: n8nConfig.projectId, name: n8nConfig.projectName }] : [],
            },
            availableModels,
        };
    }
    async fetchN8nProjects(host, apiKeyOverride) {
        const normalizedHost = host.trim();
        if (!normalizedHost) {
            throw new Error('n8n host is required.');
        }
        const configService = new YagrN8nConfigService();
        const apiKey = apiKeyOverride ?? configService.getApiKey(normalizedHost);
        if (!apiKey) {
            throw new Error('No n8n API key available for that host.');
        }
        const client = new N8nApiClient({ host: normalizedHost, apiKey });
        const connected = await client.testConnection();
        if (!connected) {
            throw new Error('Unable to connect to n8n with the provided URL and API key.');
        }
        return client.getProjects();
    }
    async sendManagedN8nWorkflowSession(response, target) {
        const session = resolveManagedN8nWorkflowOpen(target);
        if (!session.ok) {
            this.sendJson(response, session.statusCode, { error: session.error });
            return;
        }
        this.sendJson(response, 200, session.payload);
    }
    async openManagedN8nWorkflow(response, target) {
        const session = resolveManagedN8nWorkflowOpen(target);
        if (!session.ok) {
            this.sendText(response, session.statusCode, session.error, 'text/plain; charset=utf-8');
            return;
        }
        if (session.payload.mode === 'direct') {
            response.writeHead(302, { Location: session.payload.targetUrl });
            response.end();
            return;
        }
        this.sendText(response, 200, session.payload.fallbackPage, 'text/html; charset=utf-8');
    }
    async saveN8nConfig(input) {
        const host = input.host.trim();
        const projectId = input.projectId.trim();
        const syncFolder = input.syncFolder.trim() || 'workflows';
        if (!host) {
            throw new Error('n8n host is required.');
        }
        if (!projectId) {
            throw new Error('Select an n8n project first.');
        }
        const configService = new YagrN8nConfigService();
        const apiKey = input.apiKey?.trim() || configService.getApiKey(host);
        if (!apiKey) {
            throw new Error('An n8n API key is required.');
        }
        const projects = await this.fetchN8nProjects(host, apiKey);
        const selectedProject = projects.find((project) => project.id === projectId);
        if (!selectedProject) {
            throw new Error('The selected n8n project could not be found. Reload projects and try again.');
        }
        configService.saveApiKey(host, apiKey);
        configService.saveBootstrapState(host, syncFolder);
        const instanceIdentifier = await configService.getOrCreateInstanceIdentifier(host);
        const projectName = getDisplayProjectName(selectedProject);
        configService.saveLocalConfig({
            host,
            syncFolder,
            projectId: selectedProject.id,
            projectName,
            instanceIdentifier,
        });
        const workflowDir = resolveWorkflowDir({ syncFolder, instanceIdentifier, projectName });
        if (workflowDir) {
            WorkspaceSetupService.ensureWorkspaceFiles(workflowDir);
        }
        // Invalidate the cached engine and all agent sessions so the next request
        // picks up a fresh engine built from the new config (new host, new API key).
        this.enginePromise = undefined;
        this.agents.clear();
        try {
            await refreshAiContext({ host, apiKey });
            return undefined;
        }
        catch (error) {
            return `Workspace saved, but the n8n workspace instructions refresh failed: ${error instanceof Error ? error.message : String(error)}`;
        }
    }
    assertProvider(value) {
        if (!VALID_PROVIDERS.includes(value)) {
            throw new Error(`Unknown provider: ${value}`);
        }
        return value;
    }
    async resolveAgent(sessionId) {
        const existing = this.agents.get(sessionId);
        if (existing) {
            return existing;
        }
        if (!this.enginePromise) {
            this.enginePromise = this.engineResolver();
        }
        const engine = await this.enginePromise;
        const agent = new YagrAgent(engine);
        this.agents.set(sessionId, agent);
        return agent;
    }
    async readJson(request) {
        const chunks = [];
        for await (const chunk of request) {
            chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
        }
        if (chunks.length === 0) {
            return {};
        }
        return JSON.parse(Buffer.concat(chunks).toString('utf-8'));
    }
    sendJson(response, statusCode, payload) {
        response.writeHead(statusCode, {
            'Content-Type': 'application/json; charset=utf-8',
            'Cache-Control': 'no-store',
        });
        response.end(JSON.stringify(payload));
    }
    sendText(response, statusCode, text, contentType) {
        response.writeHead(statusCode, {
            'Content-Type': contentType,
            'Cache-Control': 'no-store',
        });
        response.end(text);
    }
    async sendStaticAsset(response, fileName, contentType) {
        const assetPath = path.resolve(__dirname, '..', 'webui', fileName);
        const content = await readFile(assetPath, 'utf-8');
        this.sendText(response, 200, content, contentType);
    }
    async handleStreamingChat(response, sessionId, message) {
        const setupStatus = getYagrSetupStatus(this.configService, new YagrN8nConfigService(), {
            activeSurfaces: [...ACTIVE_WEBUI_SURFACES],
        });
        if (!setupStatus.ready) {
            this.sendJson(response, 400, { error: `Yagr is not ready yet. Missing: ${setupStatus.missingSteps.join(', ')}.` });
            return;
        }
        response.writeHead(200, {
            'Content-Type': 'application/x-ndjson; charset=utf-8',
            'Cache-Control': 'no-store',
            Connection: 'keep-alive',
            'X-Accel-Buffering': 'no',
        });
        const abortController = new AbortController();
        let runFinished = false;
        let streamedText = '';
        const handleConnectionClose = () => {
            if (!runFinished && !abortController.signal.aborted) {
                abortController.abort();
            }
        };
        response.on('close', handleConnectionClose);
        const writeEvent = (event) => {
            if (response.writableEnded || response.destroyed) {
                return;
            }
            response.write(`${JSON.stringify(event)}\n`);
        };
        const pushPhaseEvent = (event) => {
            if (event.status !== 'started') {
                return;
            }
            writeEvent({
                type: 'phase',
                phase: event.phase,
                status: event.status,
                message: event.message,
            });
        };
        const pushStateEvent = (event) => {
            if (event.state === 'running' || event.state === 'streaming' || event.state === 'completed') {
                return;
            }
            writeEvent({
                type: 'state',
                state: event.state,
                message: event.message,
            });
        };
        const pushToolEvent = (event) => {
            if (event.type === 'status') {
                writeEvent({
                    type: 'progress',
                    tone: 'info',
                    title: event.toolName === 'reportProgress' ? 'Progress' : `Tool ${event.toolName}`,
                    detail: event.message,
                });
                return;
            }
            if (event.type === 'command-end') {
                if (event.exitCode === 0) {
                    return;
                }
                writeEvent({
                    type: 'progress',
                    tone: 'info',
                    title: 'Correcting commands',
                    detail: event.message,
                });
                return;
            }
            if (event.type === 'embed') {
                writeEvent({
                    type: 'embed',
                    kind: event.kind,
                    workflowId: event.workflowId,
                    url: event.url,
                    targetUrl: event.targetUrl,
                    title: event.title,
                    diagram: event.diagram,
                });
                return;
            }
        };
        const pushCompactionEvent = (event) => {
            writeEvent({
                type: 'progress',
                tone: 'info',
                title: 'Context compacted',
                detail: `${event.messagesCompacted} messages folded to keep the run responsive.`,
            });
        };
        try {
            writeEvent({
                type: 'start',
                sessionId,
                message: 'Run started. Inspecting workspace and request.',
            });
            const agent = await this.resolveAgent(sessionId);
            const resolvedConfig = resolveLanguageModelConfig({}, this.configService);
            const result = await agent.run(message, {
                ...this.options,
                provider: resolvedConfig.provider,
                model: resolvedConfig.model,
                apiKey: resolvedConfig.apiKey,
                baseUrl: resolvedConfig.baseUrl,
                onPhaseChange: async (event) => {
                    pushPhaseEvent(event);
                    await this.options.onPhaseChange?.(event);
                },
                onStateChange: async (event) => {
                    pushStateEvent(event);
                    await this.options.onStateChange?.(event);
                },
                onToolEvent: async (event) => {
                    pushToolEvent(event);
                    await this.options.onToolEvent?.(event);
                },
                onCompaction: async (event) => {
                    pushCompactionEvent(event);
                    await this.options.onCompaction?.(event);
                },
                onTextDelta: async (delta) => {
                    streamedText += delta;
                    writeEvent({ type: 'text-delta', delta });
                    await this.options.onTextDelta?.(delta);
                },
                abortSignal: abortController.signal,
            });
            runFinished = true;
            writeEvent({
                type: 'final',
                sessionId,
                response: result.text,
                finalState: result.finalState,
                requiredActions: result.requiredActions.map((action) => ({
                    title: action.title,
                    message: action.message,
                })),
            });
        }
        catch (error) {
            if (isAbortError(error)) {
                runFinished = true;
                writeEvent({
                    type: 'final',
                    sessionId,
                    response: streamedText,
                    finalState: 'stopped',
                    requiredActions: [],
                });
            }
            else {
                writeEvent({
                    type: 'error',
                    error: error instanceof Error ? error.message : String(error),
                });
            }
        }
        finally {
            response.off('close', handleConnectionClose);
            response.end();
        }
    }
}
async function resolveTelegramBotIdentity(botToken) {
    const { Telegraf } = await import('telegraf');
    const bot = new Telegraf(botToken);
    const me = await bot.telegram.getMe();
    if (!me.username) {
        throw new Error('Telegram bot username is missing. Configure the bot with BotFather first.');
    }
    return {
        username: me.username,
        firstName: me.first_name,
    };
}
async function refreshAiContext(credentials) {
    const updateAi = new UpdateAiCommand(new Command());
    const previousCwd = process.cwd();
    try {
        process.chdir(getYagrN8nWorkspaceDir());
        await updateAi.run({}, credentials);
    }
    finally {
        process.chdir(previousCwd);
    }
}
//# sourceMappingURL=webui.js.map