import Fastify from 'fastify';
import cors from '@fastify/cors';
import cookie from '@fastify/cookie';
import rateLimit from '@fastify/rate-limit';
import fastifyStatic from '@fastify/static';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadConfig } from './config.js';
import { getDb } from './db/connection.js';
import { seedDefaultAdmin } from './db/migrations.js';
import { registerAuthMiddleware } from './auth/auth-middleware.js';
import { healthRoutes } from './api/health-routes.js';
import { authRoutes } from './auth/auth-routes.js';
import { squadsRoutes } from './api/squads-routes.js';
import { usersRoutes } from './api/users-routes.js';
import { registerWebSocket } from './ws/ws-handler.js';
import { createFileWatcher } from './watcher/file-watcher.js';
import { conversationRoutes } from './routes/conversations.js';
import { activityRoutes } from './api/activity-routes.js';
import { EngineManager } from './bridge/engine-manager.js';
import { ChatHandler } from './bridge/chat-handler.js';
import { JobRunner } from './scheduler/job-runner.js';
import { SchedulerService } from './scheduler/scheduler-service.js';
import { schedulerRoutes } from './scheduler/scheduler-routes.js';
import { chatApiRoutes } from './api/chat-api-routes.js';
import { leadRoutes } from './email/lead-routes.js';
import { campaignRoutes } from './email/campaign-routes.js';
import { snsWebhookRoutes } from './email/sns-webhook.js';
import { createSesClient } from './email/ses-client.js';
import { EmailQueueWorker } from './email/email-queue-worker.js';
import { settingsRoutes } from './api/settings-routes.js';
import { orgchartRoutes } from './api/orgchart-routes.js';
import { teamRoutes } from './api/team-routes.js';
import { integrationRoutes } from './api/integration-routes.js';
import { graphRoutes } from './api/graph-routes.js';
import { filesRoutes } from './api/files-routes.js';
import { kanbanRoutes } from './api/kanban-routes.js';
import { registryRoutes } from './api/registry-routes.js';
import { knowledgeRoutes } from './api/knowledge-routes.js';
import { runKnowledgeMigrations, IngestPipeline, createMockEmbedder, createKnowledgeWatcher } from 'expxagents-knowledge';
import { getActiveActivityId, markTaskExecutingByPosition, markTaskDoneByPosition, } from './services/pipeline-task-service.js';
import { systemRoutes } from './api/system-routes.js';
import { webhookRoutes } from './api/webhook-routes.js';
import { dashboardRoutes } from './api/dashboard-routes.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export async function buildApp(opts) {
    const config = { ...loadConfig(), ...opts?.config };
    const app = Fastify({
        logger: {
            level: config.nodeEnv === 'production' ? 'info' : 'debug',
        },
    });
    // Plugins
    await app.register(cors, { origin: config.corsOrigin, credentials: true });
    await app.register(cookie);
    await app.register(rateLimit, {
        global: false,
    });
    // Database
    const db = getDb(config.dataDir);
    await seedDefaultAdmin(db);
    // Knowledge schema tables
    runKnowledgeMigrations(db);
    db.exec('CREATE TABLE IF NOT EXISTS chunk_embeddings_store (chunk_id TEXT PRIMARY KEY, embedding TEXT NOT NULL)');
    // Expose db on the app instance for testability
    app.decorate('db', db);
    // Auth middleware
    registerAuthMiddleware(app, config.jwtSecret);
    // Routes
    await app.register(healthRoutes);
    await app.register(authRoutes, { db, jwtSecret: config.jwtSecret });
    await app.register(squadsRoutes, { squadsDir: config.squadsDir });
    await app.register(usersRoutes, { db });
    await app.register(conversationRoutes, { db });
    await app.register(activityRoutes, { db });
    await app.register(settingsRoutes);
    await app.register(orgchartRoutes, { squadsDir: config.squadsDir });
    await app.register(teamRoutes, { db });
    await app.register(integrationRoutes, { db });
    await app.register(graphRoutes, { squadsDir: config.squadsDir });
    await app.register(filesRoutes, { squadsDir: config.squadsDir });
    await app.register(kanbanRoutes, { db });
    await app.register(registryRoutes, { squadsDir: config.squadsDir });
    await app.register(knowledgeRoutes, { db });
    // Kill registry: populated after bridges are created below
    const killRegistry = { fn: (_squadCode) => { } };
    // Engine Manager (hot-swap support) — created before systemRoutes
    const engineManager = new EngineManager();
    await app.register(dashboardRoutes, { db, kill: (code) => killRegistry.fn(code) });
    await app.register(systemRoutes, {
        packageVersion: process.env.EXPXAGENTS_VERSION ?? '0.0.0',
        engineManager,
    });
    // WebSocket
    const wsContext = await registerWebSocket(app, config.jwtSecret, config.squadsDir);
    // Chat Bridge
    const chatHandler = new ChatHandler({
        db,
        bridge: engineManager.engine,
        broadcast: (squadName, message) => {
            wsContext.broadcastToSquad(squadName, message);
        },
        broadcastToAll: (message) => {
            wsContext.broadcastToAll(message);
        },
        squadsDir: config.squadsDir,
    });
    wsContext.setChatHandler(chatHandler);
    // REST API for MCP server and programmatic access
    await app.register(chatApiRoutes, () => wsContext.getChatHandler());
    // Scheduler — reuse the engine manager
    const jobRunner = opts?.jobRunner ?? new JobRunner({
        db,
        bridge: engineManager.engine,
        broadcast: (squadName, message) => {
            wsContext.broadcastToSquad(squadName, message);
        },
        broadcastToAll: (message) => {
            wsContext.broadcastToAll(message);
        },
        squadsDir: config.squadsDir,
    });
    await app.register(webhookRoutes, { jobRunner, squadsDir: config.squadsDir });
    // Populate kill registry (jobRunner uses the shared bridge)
    killRegistry.fn = (squadCode) => {
        engineManager.engine.kill(squadCode);
    };
    const schedulerService = new SchedulerService({
        db,
        jobRunner,
        squadsDir: config.squadsDir,
        broadcast: (squadName, message) => {
            wsContext.broadcastToSquad(squadName, message);
        },
    });
    schedulerService.start();
    await app.register(schedulerRoutes, { db, schedulerService });
    // Email Engine
    const sesClient = createSesClient({
        region: config.awsRegion,
        accessKeyId: config.awsAccessKeyId,
        secretAccessKey: config.awsSecretAccessKey,
    });
    let emailWorker = null;
    if (sesClient.isConfigured()) {
        emailWorker = new EmailQueueWorker({
            db,
            ses: sesClient,
            rateLimit: config.sesRateLimit,
            jwtSecret: config.jwtSecret,
            serverUrl: config.serverUrl,
            broadcast: (message) => {
                wsContext.broadcastToAll(message);
            },
        });
        emailWorker.start();
    }
    await app.register(leadRoutes, { db });
    await app.register(campaignRoutes, { db });
    await app.register(snsWebhookRoutes, { db, jwtSecret: config.jwtSecret });
    // File Watcher
    const watcher = createFileWatcher(config.squadsDir, (event) => {
        if (event.type === 'squad:state') {
            wsContext.broadcastToSquad(event.squadName, event);
        }
        else if (event.type === 'squad:inactive') {
            wsContext.broadcastToSquad(event.squadName, event);
        }
        else if (event.type === 'agent:status') {
            wsContext.broadcastToAll(event);
        }
        else if (event.type === 'chat:agent_dm') {
            wsContext.broadcastToAll(event);
        }
        else if (event.type === 'agent:step_start') {
            wsContext.broadcastToSquad(event.squadName, event);
            // Update kanban task: mark this step as executing
            const startActivityId = getActiveActivityId(db, event.squadName);
            if (startActivityId) {
                markTaskExecutingByPosition(db, { activityId: startActivityId, position: event.stepIndex });
                wsContext.broadcastToAll({ type: 'kanban:task_updated', squadName: event.squadName });
            }
        }
        else if (event.type === 'agent:step_complete') {
            wsContext.broadcastToSquad(event.squadName, event);
            // Update kanban task: mark this step as done
            const completeActivityId = getActiveActivityId(db, event.squadName);
            if (completeActivityId) {
                markTaskDoneByPosition(db, { activityId: completeActivityId, position: event.stepIndex });
                wsContext.broadcastToAll({ type: 'kanban:task_updated', squadName: event.squadName });
            }
        }
        else if (event.type === 'squad:checkpoint') {
            wsContext.broadcastToSquad(event.squadName, event);
        }
        else if (event.type === 'a2ui:update') {
            wsContext.broadcastToSquad(event.squadName, {
                type: 'a2ui:update',
                squadName: event.squadName,
                components: event.components,
            });
        }
        else if (event.type === 'squad:config_changed') {
            schedulerService.reload(event.squadName);
        }
    });
    await watcher.ready;
    // Knowledge Watcher — auto-ingest files from configured paths
    let knowledgeWatcher = null;
    if (config.knowledgeWatchPaths.length > 0) {
        const embedder = createMockEmbedder(384);
        const knowledgePipeline = new IngestPipeline(db, embedder, { max_tokens: 512, overlap: 0.2 });
        knowledgeWatcher = createKnowledgeWatcher(config.knowledgeWatchPaths, knowledgePipeline);
        app.log.info({ paths: config.knowledgeWatchPaths }, 'Knowledge watcher started');
    }
    // Cleanup on close
    app.addHook('onClose', async () => {
        if (knowledgeWatcher)
            knowledgeWatcher.stop();
        if (emailWorker)
            emailWorker.stop();
        schedulerService.stop();
        watcher.stop();
    });
    // Serve static files (production serves dashboard, dev serves status page)
    const dashboardDist = process.env.DASHBOARD_DIST ?? path.resolve(__dirname, '..', '..', 'dashboard', 'dist');
    const hasDashboard = require('fs').existsSync(path.join(dashboardDist, 'index.html'));
    if (hasDashboard) {
        await app.register(fastifyStatic, {
            root: dashboardDist,
            prefix: '/',
            wildcard: false,
        });
        app.setNotFoundHandler(async (request, reply) => {
            if (request.url.startsWith('/api/') || request.url.startsWith('/ws')) {
                return reply.code(404).send({ error: 'Not found' });
            }
            const staticExtPattern = /\.(js|css|map|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot)$/i;
            if (staticExtPattern.test(request.url)) {
                return reply.code(404).send({ error: 'Not found' });
            }
            return reply.sendFile('index.html');
        });
    }
    else {
        // No dashboard build available — serve a simple status JSON on root
        app.get('/', async () => ({
            name: 'ExpxAgents Server',
            status: 'running',
            env: config.nodeEnv,
            dashboard: 'not built — run "npm run build" in dashboard/',
            api: '/api/health',
        }));
    }
    return app;
}
//# sourceMappingURL=app.js.map