import { createLogger } from '@restorecommerce/logger';
import { ServerResponse } from 'node:http';
import Koa from 'koa';
import { ApolloServer } from '@apollo/server';
import { koaMiddleware } from '@as-integrations/koa';
import { GraphQLSchema, printSchema } from 'graphql';
import { ApolloGateway, LocalGraphQLDataSource, RemoteGraphQLDataSource, IntrospectAndCompose } from '@apollo/gateway';
import { facadeStatusModule } from './modules/index.js';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default';
import { createServer } from 'node:http';
import { WebSocketServer } from 'ws';
/* eslint-disable */
import { useServer } from 'graphql-ws/use/ws';
import _ from 'lodash';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { parse } from 'graphql';
import { mergeSubscribeIntoSchema } from './gql/protos/index.js';
import compose from 'koa-compose';
import { setUseSubscriptions } from './gql/protos/utils.js';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
import graphqlUploadKoa from 'graphql-upload/graphqlUploadKoa.mjs';
import Router from 'koa-router';
import { decomposeError } from './utils.js';
export * from './modules/index.js';
export * from './middlewares/index.js';
export * from './interfaces.js';
export * from './utils.js';
export * from './gql/index.js';
export class RestoreCommerceFacade {
    apolloServices = {};
    allResolvers = {};
    _server;
    _gqlServer;
    _initialized = false;
    logger;
    port;
    hostname;
    koa;
    env;
    modules = [];
    kafkaConfig;
    fileUploadOptionsConfig;
    jsonLimit;
    extraServices;
    startFns = [];
    stopFns = [];
    constructor({ koa, logger, port, hostname, env, kafka, fileUploadOptions, jsonLimit, extraServices }) {
        this.logger = logger;
        this.port = port ?? 5000;
        this.hostname = hostname ?? '127.0.0.1';
        this.koa = koa;
        this.env = env ?? 'development';
        this.kafkaConfig = kafka;
        this.fileUploadOptionsConfig = fileUploadOptions;
        this.jsonLimit = jsonLimit ? jsonLimit : '50mb';
        this.extraServices = extraServices;
        setUseSubscriptions(!!kafka);
    }
    get server() {
        return this._server;
    }
    get address() {
        const address = this._server?.address();
        if (address && typeof address === 'object') {
            return address;
        }
        return undefined;
    }
    get listening() {
        return !!this._server && this._server.listening;
    }
    useMiddleware(middleware) {
        this.koa.use(middleware);
        return this;
    }
    useModule(module) {
        this.logger.info(`Adding module: ${module.moduleName}`);
        if (this.modules.some(m => module.moduleName === m.moduleName)) {
            throw new Error(`module ${module.moduleName} already loaded`);
        }
        this.modules.push(module);
        module(this);
        return this;
    }
    supportsModule(module) {
        return this.modules.some(m => module.moduleName === m.moduleName);
    }
    addApolloService({ name, schema, url }) {
        this.logger.info(`Adding Apollo service: ${name}`);
        if ('astNode' in schema) {
            this.apolloServices[name] = { schema, url };
        }
        else if ('federatedSchema' in schema && 'resolvers' in schema) {
            this.apolloServices[name] = { schema: schema.federatedSchema, url };
            this.allResolvers = _.merge(this.allResolvers, schema.resolvers);
        }
    }
    onStart(fn) {
        debugger;
        this.startFns.push(fn);
    }
    onStop(fn) {
        this.stopFns.push(fn);
    }
    async runFnQueue(fns) {
        const _fns = [...fns];
        const runQueue = async () => {
            const fn = _fns.shift();
            if (fn) {
                try {
                    await fn();
                }
                catch (error) {
                    this.logger.error('Error in start/stop function', error);
                }
                await runQueue();
            }
        };
        await runQueue();
    }
    async start() {
        if (!this._initialized) {
            this.runFnQueue(this.startFns);
            await this.mountApolloServer();
            this._initialized = true;
        }
        return new Promise((resolve, reject) => {
            try {
                this.server?.listen(this.port, this.hostname, () => {
                    const address = this.address;
                    if (address) {
                        this.logger.info(`Service is listening on ${address.address}:${address.port}`);
                    }
                    else {
                        this.logger.info(`Service is listening`);
                    }
                    resolve();
                });
            }
            catch (err) {
                reject(err);
            }
        });
    }
    async stop() {
        return await new Promise(async (resolve, reject) => {
            await this.runFnQueue(this.stopFns);
            await this._gqlServer?.stop().catch((error) => {
                if (error) {
                    this.logger.error(`Error stopping GQLServer`, decomposeError(error));
                }
                else {
                    this.logger.info(`GQLServer stopped`);
                    this._gqlServer = undefined;
                }
            });
            if (this._server?.listening) {
                this._server?.close((error) => {
                    if (error) {
                        this.logger.error(`Error stopping service`, decomposeError(error));
                        reject(error);
                    }
                    else {
                        this.logger.info(`Service stopped`);
                        this._server = undefined;
                        resolve();
                    }
                });
            }
            else {
                resolve();
            }
        });
    }
    async mountApolloServer() {
        const serviceList = Object.keys(this.apolloServices).map(key => {
            return {
                name: key,
                url: this.apolloServices[key].url ?? `local`,
            };
        });
        if (this.extraServices) {
            serviceList.push(...this.extraServices);
        }
        const plugin = {
            disposables: new Array(),
            async serverWillStart() {
                return {
                    async drainServer() {
                        await Promise.allSettled(plugin.disposables?.map(d => d?.dispose()));
                    },
                };
            },
        };
        const gateway = new ApolloGateway({
            logger: this.logger,
            // serviceList,
            supergraphSdl: new IntrospectAndCompose({
                subgraphs: serviceList
            }),
            debug: true,
            buildService: ({ name, url }) => {
                if (url !== 'local') {
                    return new RemoteGraphQLDataSource({
                        url,
                        willSendRequest({ request, context }) {
                            if (context?.authorization) {
                                request.http.headers.set('authorization', context.authorization);
                            }
                            else if (context?.request?.header?.authorization) {
                                request.http.headers.set('authorization', context.request.header.authorization);
                            }
                        },
                    });
                }
                else {
                    const schema = this.apolloServices[name]?.schema;
                    if (schema) {
                        return new LocalGraphQLDataSource(schema);
                    }
                    else {
                        throw new Error('Invalid schema ' + name);
                    }
                }
            }
        });
        this._server = createServer(this.koa.callback());
        const wsServer = new WebSocketServer({
            server: this._server,
            path: '/graphql',
        });
        gateway.onSchemaLoadOrUpdate(schemaContext => {
            try {
                const typeDefs = printSchema(new GraphQLSchema({
                    subscription: schemaContext.apiSchema.getSubscriptionType()
                }));
                const schema = makeExecutableSchema({
                    typeDefs: parse(typeDefs + `
          type Query { sample: String }
          `),
                    resolvers: {
                        Subscription: {
                            ...this.allResolvers?.Subscription
                        }
                    }
                });
                if ('Subscription' in this.allResolvers) {
                    mergeSubscribeIntoSchema(schema.getSubscriptionType(), this.allResolvers?.Subscription);
                }
                plugin.disposables.push(useServer({
                    schema,
                    context: async (ctx, message, args) => {
                        const newCtx = this.koa.createContext(ctx.extra.request, new ServerResponse(ctx.extra.request));
                        const fn = await compose(this.koa.middleware);
                        await fn(newCtx);
                        newCtx.kafkaConfig = this.kafkaConfig;
                        newCtx.logger = this.logger;
                        return newCtx;
                    },
                }, wsServer));
            }
            catch (error) {
                this.logger?.warn('Gateway onSchemaLoadOrUpdate:', decomposeError(error));
            }
        });
        this._gqlServer = new ApolloServer({
            gateway,
            introspection: true,
            allowBatchedHttpRequests: true,
            plugins: [
                ApolloServerPluginDrainHttpServer({ httpServer: this._server }),
                ApolloServerPluginLandingPageLocalDefault({
                    embed: true
                }),
                plugin,
            ],
            includeStacktraceInErrorResponses: true,
            formatError: (error) => {
                this.logger.error('Error while processing request', { message: error.message });
                this.logger.debug('Error while processing request', { error });
                return {
                    message: error.message,
                    locations: error.locations,
                    stack: error
                };
            },
        });
        await this._gqlServer.start();
        // set maxFile size and maximum files via Facade config of `createFacade`
        const maxFileSize = this.fileUploadOptionsConfig?.maxFileSize ?? 10000000;
        const maxFiles = this.fileUploadOptionsConfig?.maxFiles ?? 20;
        this.koa.use(graphqlUploadKoa({
            maxFileSize,
            maxFiles
        }));
        this.koa.use(cors({
            credentials: true,
            origin: '*',
            exposeHeaders: ["x-jwt"]
        }));
        const apolloGraphQLRouter = new Router();
        apolloGraphQLRouter.use(bodyParser({ jsonLimit: this.jsonLimit }));
        apolloGraphQLRouter.all('/graphql', koaMiddleware(this._gqlServer, {
            context: async ({ ctx }) => ctx,
        }));
        this.koa.use(apolloGraphQLRouter.routes());
    }
}
export const createFacade = (config) => {
    const koa = new Koa();
    if (config.env) {
        koa.env = config.env;
    }
    if (config.keys) {
        koa.keys = config.keys;
    }
    let loggerCfg;
    if (config.logger) {
        loggerCfg = config.logger;
        loggerCfg.esTransformer = (msg) => {
            msg.fields = JSON.stringify(msg.fields);
            return msg;
        };
    }
    const logger = loggerCfg ?? createLogger(loggerCfg);
    koa.context.logger = logger;
    return new RestoreCommerceFacade({
        koa,
        logger,
        port: config.port,
        hostname: config.hostname,
        env: config.env,
        kafka: config.kafka,
        fileUploadOptions: config.fileUploadOptions,
        jsonLimit: config.jsonLimit,
        extraServices: config.extraServices
    }).useModule(facadeStatusModule);
};
//# sourceMappingURL=index.js.map