import {define, inject, singleton, Util, Define} from "@appolo/inject";
import {IEnv, App, IApp} from "@appolo/engine";
import {IOptions} from "../common/IOptions";
import {HandlersManager} from "../handlers/handlersManager";
import {RepliesManager} from "../handlers/repliesManager";
import url = require("url");
import {
    createRabbit,
    IConnectionOptions,
    IExchangeOptions,
    IOptions as RabbitOptions,
    IQueueOptions, IBindingOptions
} from "appolo-rabbit";
import {ExchangeDefaults, QueueDefaults, ReplyQueueDefaults, RequestQueueDefaults} from "../common/defaults";
import {IHandlerMetadata, IHandlerMetadataOptions, IHandlerProperties} from "../common/interfaces";
import {BaseHandlersManager} from "../handlers/baseHandlersManager";
import {HandlerSymbol, ReplySymbol, RequestSymbol} from "../common/decorators";
import {Reflector, Strings, Arrays} from "@appolo/utils";

@define()
@singleton()
export class TopologyManager {
    @inject() private moduleOptions: IOptions;
    @inject() private env: IEnv;
    @inject() private app: App;
    @inject() private handlersManager: HandlersManager;
    @inject() private repliesManager: RepliesManager;

    private _queues: IQueueOptions[];
    private _requests: IQueueOptions[];
    private _exchanges: IQueueOptions[];
    private _connection: IConnectionOptions;
    private _replyQueue: IQueueOptions;


    public appendEnv(name: string): string {
        return name ? (this.moduleOptions.addEnvToNames ? (`${name}-${this.envName}`) : name) : "";
    }

    public get envName(): string {
        return (this.env as any).name || this.env.type || "production";
    }

    public get connection(): IConnectionOptions {
        return this._connection
    }

    public getDefaultQueueName(): string {
        return this._queues.length ? this._queues[0].name : "";
    }

    public getDefaultRequestQueueName(): string {
        return this._requests.length ? this._requests[0].name : ""
    }

    public getDefaultExchangeName(): string {
        return this._exchanges[0].name
    }

    public buildTopology(): RabbitOptions {

        this._connection = this._createConnection();

        this._exchanges = this._createExchanges();

        this._replyQueue = this._createReplyQueue();

        this._queues = this._createQueues();

        this._requests = this._createRequestQueues();

        this._createHandlers(HandlerSymbol, this.handlersManager, this.getDefaultQueueName());
        this._createHandlers(ReplySymbol, this.repliesManager, this.getDefaultRequestQueueName());

        let config = <RabbitOptions>{
            connection: this._connection,
            queues: this._queues,
            requestQueues: this._requests,
            replyQueue: this._replyQueue,
            exchanges: this._exchanges,
            bindings: this._createBindings()
        };

        return config;
    }

    private _createQueues(): IQueueOptions[] {
        let queues = this.moduleOptions.queues || [];

        if (this.moduleOptions.queue) {
            queues.unshift(Strings.isString(this.moduleOptions.queue) ? {name: this.moduleOptions.queue} : this.moduleOptions.queue);
        }

        queues = queues.map(queue => Object.assign({}, QueueDefaults, queue, {name: this.appendEnv(queue.name)}));

        return queues;

    }

    private _createRequestQueues(): IQueueOptions[] {
        let requestQueues = this.moduleOptions.requestQueues || [];

        if (this.moduleOptions.requestQueue) {
            requestQueues.unshift(Strings.isString(this.moduleOptions.requestQueue) ? {name: this.moduleOptions.requestQueue} : this.moduleOptions.requestQueue);
        }

        requestQueues = requestQueues.map(queue => Object.assign({}, RequestQueueDefaults, queue, {name: this.appendEnv(queue.name)}));

        return requestQueues;
    }


    private _createReplyQueue(): IQueueOptions {
        let replyQueue = null;

        if (this.moduleOptions.replyQueue) {
            replyQueue = Strings.isString(this.moduleOptions.replyQueue) ? {name: this.moduleOptions.replyQueue} : this.moduleOptions.replyQueue;

            replyQueue = Object.assign({}, ReplyQueueDefaults, replyQueue, {name: this.appendEnv(replyQueue.name)})
        }

        return replyQueue;
    }

    private _createExchanges(): IExchangeOptions[] {

        let exchanges = this.moduleOptions.exchanges || [];

        if (this.moduleOptions.exchange) {
            exchanges.unshift(Strings.isString(this.moduleOptions.exchange) ? {name: this.moduleOptions.exchange} : this.moduleOptions.exchange);

        }

        exchanges = exchanges.map(exchange => Object.assign({}, ExchangeDefaults, exchange, {name: this.appendEnv(exchange.name)}));

        return exchanges;
    }

    private _createConnection(): IConnectionOptions {
        let connection: IConnectionOptions = this.moduleOptions.connection as IConnectionOptions;

        if (Strings.isString(this.moduleOptions.connection)) {
            connection = {uri: this.moduleOptions.connection}
        }

        if (connection.uri) {
            connection = Object.assign({}, connection, this._parseUri(connection.uri))
        }

        return connection;
    }

    private _parseUri(uri: string) {
        let amqp = new URL(uri);
        return {
            username: amqp.username,
            password: amqp.password,
            hostname: amqp.hostname,
            port: parseInt(amqp.port) || 5672,
            vhost: amqp.pathname.substring(1),
        }
    }

    private _createBindings(): IBindingOptions[] {
        let messageHandlers = [], replyHandlers = [], bindings: IBindingOptions[] = [];

        if (this.moduleOptions.handleEvents) {
            messageHandlers = this.handlersManager.getHandlersProperties();
            replyHandlers = this.repliesManager.getHandlersProperties();
        }

        let handlers: IHandlerProperties[] = messageHandlers.concat(replyHandlers);

        handlers.forEach(handler => {
            bindings.push({
                exchange: handler.exchange,
                queue: handler.queue,
                keys: [handler.routingKey || handler.eventName]
            })
        });

        bindings = Arrays.uniqBy(bindings, handler => handler.exchange + handler.queue + handler.keys.join());

        return bindings;
    }

    public addMessageHandler(fn: Function) {

        let metaData = Reflector.getFnOwnMetadata<IHandlerMetadata>(HandlerSymbol, fn),
            define = this.app.discovery.getClassDefinition(fn);

        return this.addHandler(fn, define, metaData, this.handlersManager, this.getDefaultQueueName())
    }


    public addReplyMessageHandler(fn: Function) {

        let metaData = Reflector.getFnOwnMetadata<IHandlerMetadata>(ReplySymbol, fn),
            define = this.app.discovery.getClassDefinition(fn);

        return this.addHandler(fn, define, metaData, this.repliesManager, this.getDefaultRequestQueueName())

    }

    private _createHandlers(symbol: string, manager: BaseHandlersManager, defaultQueue: string) {

        let exported = this.app.tree.parent.discovery.findAllReflectData<IHandlerMetadata>(symbol);

        exported.forEach((item) => this.addHandler(item.fn, item.define, item.metaData, manager, defaultQueue))
    }

    public addHandler(fn: Function, define: Define, metaData: IHandlerMetadata, manager: BaseHandlersManager, defaultQueue: string): { eventName: string, options: Required<IHandlerMetadataOptions>, define: Define, propertyKey: string }[] {

        let output = []

        Object.keys(metaData || {}).forEach(key => {
            let handler = metaData[key];

            (handler.events || []).forEach(item => {

                let dto = this._addHandler(item.eventName, item.options, defaultQueue, manager, define, handler.propertyKey)

                output.push(dto);
            })
        });

        return output;
    }

    private _addHandler(eventName: string | ((app: IApp) => string), options: IHandlerMetadataOptions, defaultQueue: string, manager: BaseHandlersManager, define: Define, propertyKey: string): { eventName: string, options: Required<IHandlerMetadataOptions>, define: Define, propertyKey: string } {

        options = options || {};

        if (typeof eventName == "function") {
            eventName = eventName(define.definition.injector ? define.definition.injector.get("app") : this.app);
        }

        let queue = this.appendEnv(options.queue) || defaultQueue,
            exchange = this.appendEnv(options.exchange) || this.getDefaultExchangeName(),
            routingKey = options.routingKey || eventName;

        if (!queue) {
            throw new Error(`no queue defined for ${eventName}`)
        }

        if (!exchange) {
            throw new Error(`no exchange defined for ${eventName}`)
        }


        options = Object.assign({}, options, {queue, exchange, routingKey});


        manager.register(eventName, options, define, propertyKey);

        return {eventName, options: options as Required<IHandlerMetadataOptions>, define, propertyKey}

    }


}
