import { createServer as createHTTPServer } from 'node:http';
import { ProvenanceMode } from '@mondaydotcomorg/atp-protocol';
import { log, initializeLogger } from '@mondaydotcomorg/atp-runtime';
import { shutdownLogger } from '@mondaydotcomorg/atp-runtime';
import { DynamicPolicyRegistry, } from '@mondaydotcomorg/atp-provenance';
import { MB, HOUR, MINUTE } from './core/config.js';
import { ClientSessionManager } from './client-sessions.js';
import { SandboxExecutor } from './executor/index.js';
import { CodeValidator } from './validator/index.js';
import { SearchEngine } from './search/index.js';
import { ExecutionStateManager } from './execution-state/index.js';
import { ExplorerService } from './explorer/index.js';
import { toJSONSchema, printBanner, getServerInfo } from './utils/index.js';
import { handleHTTPRequest } from './http/request-handler.js';
import { handleRoute } from './http/router.js';
import { handleInit } from './handlers/init.handler.js';
import { handleSearch, handleSearchQuery } from './handlers/search.handler.js';
import { handleExplore } from './handlers/explorer.handler.js';
import { handleExecute } from './handlers/execute.handler.js';
import { handleResume } from './handlers/resume.handler.js';
import { getDefinitions } from './handlers/definitions.handler.js';
import { shutdownAudit } from './middleware/audit.js';
import { EnvAuthProvider, MemoryCache, OpenTelemetryAuditSink, } from '@mondaydotcomorg/atp-providers';
import { APIAggregator } from './aggregator/index.js';
import { loadGraphQL } from './graphql-loader.js';
export class AgentToolProtocolServer {
    config;
    middleware = [];
    apiGroups = [];
    httpServer = null;
    responseHeaders = new Map();
    isRunning = false;
    policyRegistry;
    initPromise = null;
    sessionManager;
    executor;
    validator;
    searchEngine;
    explorerService;
    stateManager;
    approvalHandler;
    cacheProvider;
    authProvider;
    auditSink;
    compiler;
    customLogger;
    toolRulesProvider;
    constructor(config = {}) {
        const initialPolicies = config.execution?.securityPolicies ?? [];
        this.policyRegistry = new DynamicPolicyRegistry(initialPolicies);
        const logLevel = typeof config.logger === 'string' ? config.logger : 'info';
        if (typeof config.logger !== 'object') {
            initializeLogger({
                level: logLevel,
                pretty: process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test',
            });
        }
        else {
            this.customLogger = config.logger;
        }
        this.config = {
            execution: {
                timeout: config.execution?.timeout ?? 30000,
                memory: config.execution?.memory ?? 128 * MB,
                llmCalls: config.execution?.llmCalls ?? 10,
                provenanceMode: config.execution?.provenanceMode ?? ProvenanceMode.NONE,
                // Override policies to use the dynamic registry as the single entry point
                securityPolicies: [this.policyRegistry],
            },
            clientInit: {
                tokenTTL: config.clientInit?.tokenTTL ?? HOUR,
                tokenRotation: config.clientInit?.tokenRotation ?? 30 * MINUTE,
            },
            executionState: {
                ttl: config.executionState?.ttl ?? 3600,
                maxPauseDuration: config.executionState?.maxPauseDuration ?? 3600,
            },
            discovery: {
                embeddings: config.discovery?.embeddings ?? false,
            },
            audit: {
                enabled: config.audit?.enabled ?? false,
                sinks: config.audit?.sinks,
            },
            otel: {
                enabled: config.otel?.enabled ?? false,
                serviceName: config.otel?.serviceName ?? process.env.OTEL_SERVICE_NAME ?? 'agent-tool-protocol',
                serviceVersion: config.otel?.serviceVersion ?? process.env.OTEL_SERVICE_VERSION ?? '1.0.0',
                traceEndpoint: config.otel?.traceEndpoint ??
                    process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
                    (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
                        ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`
                        : 'http://localhost:4318/v1/traces'),
                metricsEndpoint: config.otel?.metricsEndpoint ??
                    process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ??
                    (process.env.OTEL_EXPORTER_OTLP_ENDPOINT
                        ? `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics`
                        : 'http://localhost:4318/v1/metrics'),
                headers: config.otel?.headers ?? this.parseOtelHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS),
                metricsInterval: config.otel?.metricsInterval ?? 60000,
                resourceAttributes: config.otel?.resourceAttributes ?? {},
            },
            logger: config.logger ?? 'info',
        };
        // Store tool rules provider for automatic filtering
        if (config.toolRulesProvider) {
            this.toolRulesProvider = config.toolRulesProvider;
            log.info('Tool rules provider configured');
        }
        if (config.providers) {
            if (config.providers.cache) {
                this.cacheProvider = config.providers.cache;
                log.info('Cache provider configured', { provider: config.providers.cache.name });
            }
            if (config.providers.auth) {
                this.authProvider = config.providers.auth;
                log.info('Token store configured', { provider: config.providers.auth.name });
            }
        }
        if (!this.cacheProvider) {
            this.cacheProvider = new MemoryCache({ maxKeys: 1000, defaultTTL: 3600 });
            log.info('Cache provider configured (default)', { provider: 'memory' });
        }
        if (!this.authProvider) {
            this.authProvider = new EnvAuthProvider();
            log.info('Token store configured (default)', { provider: 'env' });
        }
        if (this.config.otel.enabled && !this.config.audit.sinks) {
            this.config.audit.enabled = true;
            this.config.audit.sinks = [new OpenTelemetryAuditSink()];
            log.info('Auto-configured OpenTelemetry audit sink from otel config');
        }
        if (this.config.audit.enabled && this.config.audit.sinks) {
            const auditSinks = Array.isArray(this.config.audit.sinks)
                ? this.config.audit.sinks
                : [this.config.audit.sinks];
            if (auditSinks.length > 1) {
                this.auditSink = {
                    name: 'multi',
                    async write(event) {
                        await Promise.all(auditSinks.map((s) => s.write(event)));
                    },
                    async writeBatch(events) {
                        await Promise.all(auditSinks.map((s) => s.writeBatch(events)));
                    },
                };
                log.info('Audit sinks configured', { count: auditSinks.length });
            }
            else {
                this.auditSink = auditSinks[0];
                log.info('Audit sink configured', { sink: auditSinks[0]?.name });
            }
        }
        // Store compiler if provided (defaults handled in transformCodeWithCompiler)
        if (config.compiler) {
            this.compiler = config.compiler;
            log.info('Custom compiler configured', { type: config.compiler.getType() });
        }
    }
    /**
     * Add a security policy (programmatic SecurityPolicy object)
     * @example
     * server.addPolicy(preventDataExfiltration);
     */
    addPolicy(policy) {
        this.policyRegistry.addPolicy(policy);
        log.info('Security policy added', { name: policy.name });
        return this;
    }
    /**
     * Add multiple security policies
     */
    addPolicies(policies) {
        for (const policy of policies) {
            this.policyRegistry.addPolicy(policy);
        }
        log.info('Security policies added', { count: policies.length });
        return this;
    }
    /**
     * Load declarative policies from JSON configuration
     * @example
     * server.loadPolicies([{
     *   id: 'block-llm-payments',
     *   scope: { toolName: 'payment.*' },
     *   rules: [{ action: 'block', conditions: [...] }]
     * }]);
     */
    loadPolicies(configs, replace = false) {
        this.policyRegistry.loadFromConfigs(configs, replace);
        log.info('Declarative policies loaded', { count: configs.length, replace });
        return this;
    }
    /**
     * Remove a policy by name
     */
    removePolicy(name) {
        this.policyRegistry.removePolicy(name);
        log.info('Security policy removed', { name });
        return this;
    }
    /**
     * Clear all policies
     */
    clearPolicies() {
        this.policyRegistry.clear();
        log.info('All security policies cleared');
        return this;
    }
    /**
     * Get all active policies
     */
    getPolicies() {
        return this.policyRegistry.getPolicies();
    }
    /**
     * Register middleware or API groups.
     * Can be called before OR after server starts.
     * - Before start: Simply adds to configuration
     * - After start: Dynamically updates running server components
     *
     * Note: Middleware can only be added before server starts.
     * @throws {Error} If trying to add middleware after server has started
     */
    use(...items) {
        const middlewareItems = [];
        const apiGroupItems = [];
        // Separate middleware from API groups
        for (const item of items) {
            if (Array.isArray(item)) {
                apiGroupItems.push(...item);
            }
            else if (typeof item === 'function') {
                middlewareItems.push(item);
            }
            else if ('name' in item && 'type' in item) {
                apiGroupItems.push(item);
            }
        }
        // Middleware can only be added before server starts
        if (middlewareItems.length > 0) {
            if (this.isRunning) {
                throw new Error('Cannot add middleware after server has started. Add middleware before calling listen().');
            }
            this.middleware.push(...middlewareItems);
        }
        // API groups can be added anytime
        if (apiGroupItems.length > 0) {
            if (this.isRunning) {
                // Server is running - update components dynamically
                this.updateComponentsWithNewGroups(apiGroupItems);
            }
            else {
                // Server not started yet - just add to array
                this.apiGroups.push(...apiGroupItems);
            }
        }
        return this;
    }
    /**
     * Register a tool/function with optional hierarchical grouping.
     * Supports hierarchical paths like 'github/readOnly' for better organization.
     * All functions registered here will be exposed as api.{group}.{name}()
     *
     * @param name - Function name
     * @param definition - Function definition including description, input schema, handler
     * @param definition.group - Optional hierarchical group path (e.g., 'github/readOnly'). Defaults to 'custom'
     *
     * @example
     * // Simple function in default 'custom' group -> api.custom.hello()
     * server.tool('hello', {
     *   description: 'Say hello',
     *   input: { name: 'string' },
     *   handler: async (input) => ({ message: `Hello ${input.name}` })
     * });
     *
     * // Hierarchical grouping -> api.github.readOnly.getUser()
     * server.tool('getUser', {
     *   group: 'github/readOnly',
     *   description: 'Get GitHub user',
     *   input: { username: 'string' },
     *   handler: async (input) => ({ username: input.username })
     * });
     */
    tool(name, definition) {
        const groupName = definition.group || 'custom';
        let targetGroup = this.apiGroups.find((g) => g.name === groupName);
        if (!targetGroup) {
            targetGroup = {
                name: groupName,
                type: 'custom',
                description: definition.groupDescription,
                functions: [],
            };
            this.apiGroups.push(targetGroup);
        }
        else if (definition.groupDescription && !targetGroup.description) {
            targetGroup.description = definition.groupDescription;
        }
        targetGroup.functions.push({
            name,
            description: definition.description,
            inputSchema: toJSONSchema(definition.input),
            outputSchema: definition.output ? toJSONSchema(definition.output) : undefined,
            handler: definition.handler,
            metadata: definition.metadata,
        });
        return this;
    }
    /**
     * Configure approval handler for human-in-the-loop operations
     * The handler will be called when code requests approval via atp.approval.request()
     */
    onApproval(handler) {
        this.approvalHandler = handler;
        log.info('Approval handler configured');
        return this;
    }
    /**
     * Initialize server components without starting HTTP server.
     * Call this when using handler(), toExpress() or toFastify().
     */
    async start() {
        if (this.isRunning)
            return;
        this.sessionManager = new ClientSessionManager({
            cache: this.cacheProvider,
            tokenTTL: this.config.clientInit.tokenTTL,
            tokenRotation: this.config.clientInit.tokenRotation,
        });
        this.validator = new CodeValidator();
        this.executor = new SandboxExecutor({
            defaultTimeout: this.config.execution.timeout,
            maxTimeout: this.config.execution.timeout * 2,
            defaultMemoryLimit: this.config.execution.memory,
            maxMemoryLimit: this.config.execution.memory * 2,
            defaultLLMCallLimit: this.config.execution.llmCalls,
            maxLLMCallLimit: this.config.execution.llmCalls * 2,
            cacheProvider: this.cacheProvider,
        }, this.apiGroups, this.approvalHandler, this.sessionManager, this.compiler);
        for (const group of this.apiGroups) {
            log.info(`  - ${group.name}: ${group.functions?.length || 0} functions`);
        }
        this.searchEngine = new SearchEngine(this.apiGroups);
        this.explorerService = new ExplorerService(this.apiGroups);
        this.stateManager = new ExecutionStateManager(this.cacheProvider, {
            ttl: this.config.executionState.ttl,
            maxPauseDuration: this.config.executionState.maxPauseDuration,
            keyPrefix: this.config.executionState.keyPrefix,
        });
        this.isRunning = true;
    }
    async listen(port) {
        if (this.httpServer) {
            throw new Error('Server is already running');
        }
        await this.start();
        this.httpServer = createHTTPServer((req, res) => {
            handleHTTPRequest(req, res, {
                cacheProvider: this.cacheProvider,
                authProvider: this.authProvider,
                auditSink: this.auditSink,
                customLogger: this.customLogger,
                middleware: this.middleware,
                routeHandler: (ctx) => handleRoute(ctx, this),
                sessionManager: this.sessionManager,
                toolRulesProvider: this.toolRulesProvider,
            }, this.responseHeaders).catch((error) => {
                log.error('Unhandled error in HTTP request handler:', error);
                try {
                    if (!res.headersSent) {
                        res.writeHead(500, { 'Content-Type': 'application/json' });
                        res.end(JSON.stringify({ error: 'Internal server error' }));
                    }
                }
                catch (e) { }
            });
        });
        if (process.env.NODE_ENV === 'test') {
            this.httpServer.keepAliveTimeout = 5000;
            this.httpServer.headersTimeout = 6000;
        }
        return new Promise((resolve, reject) => {
            this.httpServer.listen(port, () => {
                printBanner({
                    port,
                    cacheProvider: this.cacheProvider,
                    authProvider: this.authProvider,
                    auditSink: this.auditSink,
                });
                resolve();
            });
            this.httpServer.on('error', reject);
        });
    }
    async stop() {
        if (!this.httpServer)
            return;
        this.httpServer.closeAllConnections?.();
        await new Promise((resolve, reject) => {
            this.httpServer.close((err) => (err ? reject(err) : resolve()));
        });
        shutdownAudit();
        shutdownLogger();
        await Promise.all([
            this.cacheProvider?.disconnect?.(),
            this.authProvider?.disconnect?.(),
            this.auditSink?.disconnect?.(),
            this.stateManager?.close(),
            this.sessionManager?.cleanupAll(),
        ]);
        this.isRunning = false;
    }
    getInfo() {
        return getServerInfo({
            maxTimeout: this.config.execution.timeout,
            maxMemory: this.config.execution.memory,
            maxLLMCalls: this.config.execution.llmCalls,
        });
    }
    async getDefinitions(ctx) {
        const definitions = await getDefinitions(this.apiGroups);
        if (ctx && ctx.clientId && this.sessionManager) {
            const session = await this.sessionManager.getSession(ctx.clientId);
            if (session?.guidance) {
                return {
                    ...definitions,
                    guidance: session.guidance,
                };
            }
        }
        return definitions;
    }
    async getRuntimeDefinitions(ctx) {
        const aggregator = new APIAggregator(this.apiGroups);
        let requestedApis;
        if (ctx?.query?.apis) {
            requestedApis = ctx.query.apis
                .split(',')
                .map((s) => s.trim())
                .filter(Boolean);
        }
        let clientServices;
        if (ctx?.clientId && this.sessionManager) {
            try {
                const session = await this.sessionManager.getSession(ctx.clientId);
                if (session?.services) {
                    clientServices = session.services;
                }
            }
            catch (error) { }
        }
        return aggregator.generateRuntimeTypes({
            clientServices,
            requestedApis,
        });
    }
    async handleInit(ctx) {
        if (!this.sessionManager)
            ctx.throw(503, 'Session manager not initialized');
        return await handleInit(ctx, this.sessionManager, this.auditSink);
    }
    async handleSearch(ctx) {
        if (!this.searchEngine)
            ctx.throw(503, 'Search not initialized');
        return await handleSearch(ctx, this.searchEngine, this.config);
    }
    async handleSearchQuery(ctx) {
        if (!this.searchEngine)
            ctx.throw(503, 'Search not initialized');
        return await handleSearchQuery(ctx, this.searchEngine, this.config);
    }
    async handleExplore(ctx) {
        if (!this.explorerService)
            ctx.throw(503, 'Explorer not initialized');
        return await handleExplore(ctx, this.explorerService);
    }
    async handleExecute(ctx) {
        if (!this.executor || !this.validator || !this.stateManager) {
            ctx.throw(503, 'Execution not initialized');
        }
        return await handleExecute(ctx, this.executor, this.stateManager, this.config, this.auditSink, this.sessionManager);
    }
    async handleResume(ctx, executionId) {
        if (!this.executor || !this.stateManager) {
            ctx.throw(503, 'Execution not initialized');
        }
        return await handleResume(ctx, executionId, this.executor, this.stateManager, this.config, this.sessionManager);
    }
    /**
     * Update server components with new API groups (internal method)
     * @private
     */
    updateComponentsWithNewGroups(groups) {
        // Add to apiGroups array
        this.apiGroups.push(...groups);
        // Update search engine with new groups
        if (this.searchEngine) {
            this.searchEngine = new SearchEngine(this.apiGroups);
        }
        // Update explorer service with new groups
        if (this.explorerService) {
            this.explorerService = new ExplorerService(this.apiGroups);
        }
        // Update executor with new groups
        if (this.executor) {
            this.executor = new SandboxExecutor({
                defaultTimeout: this.config.execution.timeout,
                maxTimeout: this.config.execution.timeout * 2,
                defaultMemoryLimit: this.config.execution.memory,
                maxMemoryLimit: this.config.execution.memory * 2,
                defaultLLMCallLimit: this.config.execution.llmCalls,
                maxLLMCallLimit: this.config.execution.llmCalls * 2,
                cacheProvider: this.cacheProvider,
            }, this.apiGroups, this.approvalHandler, this.sessionManager);
        }
        for (const group of groups) {
            log.info(`  ✅ Dynamically loaded: ${group.name} (${group.functions?.length || 0} functions)`);
        }
    }
    /**
     * Load an OpenAPI spec and add it to the server
     * Works both before and after server starts
     * @param source - URL or file path to OpenAPI spec
     * @param options - OpenAPI loading options
     */
    async loadOpenAPI(source, options = {}) {
        const { loadOpenAPI: loadSpec } = await import('./openapi-loader.js');
        log.info(`📚 Loading OpenAPI spec: ${options.name || 'API'} from ${source}`);
        try {
            const apiGroup = await loadSpec(source, options);
            this.use(apiGroup);
            return this;
        }
        catch (error) {
            log.error(`❌ Failed to load OpenAPI spec: ${error.message}`);
            throw error;
        }
    }
    /**
     * Load a GraphQL spec and add it to the server
     * Works both before and after server starts
     * @param source - URL or file path to GraphQL spec (SDL, introspection JSON, or endpoint URL)
     * @param options - GraphQL loading options
     */
    async loadGraphQL(source, options = {}) {
        try {
            const apiGroup = await loadGraphQL(source, options);
            this.use(apiGroup);
            return this;
        }
        catch (error) {
            log.error(`Failed to load GraphQL spec: ${error.message}`);
            throw error;
        }
    }
    /**
     * Get raw Node.js request handler for framework integration
     * Use this to integrate ATP with Express, Fastify, or any Node.js framework
     * Components will be automatically initialized on first request if not already started.
     */
    handler() {
        return async (req, res) => {
            if (!this.isRunning && !this.initPromise) {
                this.initPromise = this.start();
            }
            if (this.initPromise) {
                await this.initPromise;
                this.initPromise = null;
            }
            return handleHTTPRequest(req, res, {
                cacheProvider: this.cacheProvider,
                authProvider: this.authProvider,
                auditSink: this.auditSink,
                customLogger: this.customLogger,
                middleware: this.middleware,
                routeHandler: (ctx) => handleRoute(ctx, this),
                sessionManager: this.sessionManager,
                toolRulesProvider: this.toolRulesProvider,
            }, this.responseHeaders);
        };
    }
    /**
     * Get Express middleware
     * @example
     * const app = express();
     * app.use('/atp', server.toExpress());
     */
    toExpress() {
        const requestHandler = this.handler();
        return (req, res, next) => {
            requestHandler(req, res).catch((error) => {
                log.error('Express handler error:', error);
                next(error);
            });
        };
    }
    /**
     * Get Fastify handler
     * @example
     * fastify.all('/atp/*', server.toFastify());
     */
    toFastify() {
        const requestHandler = this.handler();
        return async (request, reply) => {
            const req = request.raw;
            const res = reply.raw;
            const fastifyReq = request;
            if (fastifyReq.body !== undefined) {
                req.body = fastifyReq.body;
            }
            await requestHandler(req, res);
        };
    }
    /**
     * Parse OpenTelemetry headers from environment variable format
     * Format: "key1=value1,key2=value2"
     */
    parseOtelHeaders(headersStr) {
        if (!headersStr)
            return {};
        const headers = {};
        const pairs = headersStr.split(',');
        for (const pair of pairs) {
            const [key, value] = pair.split('=');
            if (key && value) {
                headers[key.trim()] = value.trim();
            }
        }
        return headers;
    }
}
export function createServer(config) {
    return new AgentToolProtocolServer(config);
}
//# sourceMappingURL=create-server.js.map