import { Microservice } from "@autoplex/microservice";
import { FastifyInstance } from "fastify";
import fastifyHttpProxy from "fastify-http-proxy";
import { handleMiddleware, HandlerMethodWithMiddleware, MiddlewareMethod } from "./middleware";
import { WebServerService } from "./WebServerService";

/**
 * The route factory function structure
 */
export type RouteFactory<M extends MiddlewareMethod<any> = MiddlewareMethod<void>, A extends Microservice = Microservice> = ((factory: RouteRegisterFactory<M, A>) => void)
                         | ((factory: RouteRegisterFactory<M, A>, app: A) => void);


export class RouteRegisterFactory<M extends MiddlewareMethod<any> = MiddlewareMethod<void>, A extends Microservice = Microservice>
{
	/**
	 * The application instance
	 */
	protected readonly app: A;

	/**
	 * The Fastify server instance
	 */
	protected readonly fastify: FastifyInstance;

	/**
	 * The webserver instance
	 */
	protected readonly webserver: WebServerService<A>;

	/**
	 * The list of middleware
	 */
	protected middleware: M[] = [];

	/**
	 * The current route prefix
	 */
	protected pathPrefix: string = "";

	/**
	 * Create a new route factory
	 */
	public constructor(webserver: WebServerService<A>, fastify: FastifyInstance, app: A) {
		this.app = app;
		this.fastify = fastify;
		this.webserver = webserver;
	}

	/**
	 * Register a group of routes under a common prefix and middleware
	 */
	public prefix<T extends MiddlewareMethod<any>>(prefix: string, middleware: T[], factory: RouteFactory<(M|T), A>) {
		let prefixBackup = this.pathPrefix;
		this.pathPrefix += prefix;
		this.group(middleware, factory);
		this.pathPrefix = prefixBackup;
	}

	/**
	 * Register a group of routes under common middleware
	 */
	public group<T extends MiddlewareMethod<any>>(middleware: T[], factory: RouteFactory<(M|T), A>) {
		let middlewareBackup = this.middleware;
		this.middleware = this.middleware.concat(<any>middleware);
		factory(<RouteRegisterFactory<(M|T), A>>this, this.app);
		this.middleware = middlewareBackup;
	}

	/**
	 * Register a GET request
	 */
	public get<T extends MiddlewareMethod<any>>(path: string, handler: HandlerMethodWithMiddleware<M[]>): void;
	public get<T extends MiddlewareMethod<any>>(path: string, middleware: T[], handler: HandlerMethodWithMiddleware<(T|M)[]>): void;
	public get<T extends MiddlewareMethod<any>>(path: string, middleware: HandlerMethodWithMiddleware<(T|M)[]>|T[], handler?: HandlerMethodWithMiddleware<(T|M)[]>) {
		type Handler = HandlerMethodWithMiddleware<(T|M)[]>;
		handler = (handler ?? <Handler>middleware);
		middleware = (middleware instanceof Array) ? <any>this.middleware.concat(<any>middleware) : this.middleware;
		this.fastify.get(`${this.pathPrefix}${path}`, handleMiddleware(<(T|M)[]>middleware, <Handler>handler));
	}

	/**
	 * Register a POST request
	 */
	public post<T extends MiddlewareMethod<any>>(path: string, handler: HandlerMethodWithMiddleware<M[]>): void;
	public post<T extends MiddlewareMethod<any>>(path: string, middleware: T[], handler: HandlerMethodWithMiddleware<(T|M)[]>): void;
	public post<T extends MiddlewareMethod<any>>(path: string, middleware: HandlerMethodWithMiddleware<(T|M)[]>|T[], handler?: HandlerMethodWithMiddleware<(T|M)[]>) {
		type Handler = HandlerMethodWithMiddleware<(T|M)[]>;
		handler = (handler ?? <Handler>middleware);
		middleware = (middleware instanceof Array) ? <any>this.middleware.concat(<any>middleware) : this.middleware;
		this.fastify.post(`${this.pathPrefix}${path}`, handleMiddleware(<(T|M)[]>middleware, <Handler>handler));
	}

	/**
	 * Register a PUT request
	 */
	public put<T extends MiddlewareMethod<any>>(path: string, handler: HandlerMethodWithMiddleware<M[]>): void;
	public put<T extends MiddlewareMethod<any>>(path: string, middleware: T[], handler: HandlerMethodWithMiddleware<(T|M)[]>): void;
	public put<T extends MiddlewareMethod<any>>(path: string, middleware: HandlerMethodWithMiddleware<(T|M)[]>|T[], handler?: HandlerMethodWithMiddleware<(T|M)[]>) {
		type Handler = HandlerMethodWithMiddleware<(T|M)[]>;
		handler = (handler ?? <Handler>middleware);
		middleware = (middleware instanceof Array) ? <any>this.middleware.concat(<any>middleware) : this.middleware;
		this.fastify.put(`${this.pathPrefix}${path}`, handleMiddleware(<(T|M)[]>middleware, <Handler>handler));
	}

	/**
	 * Register a DELETE request
	 */
	public delete<T extends MiddlewareMethod<any>>(path: string, handler: HandlerMethodWithMiddleware<M[]>): void;
	public delete<T extends MiddlewareMethod<any>>(path: string, middleware: T[], handler: HandlerMethodWithMiddleware<(T|M)[]>): void;
	public delete<T extends MiddlewareMethod<any>>(path: string, middleware: HandlerMethodWithMiddleware<(T|M)[]>|T[], handler?: HandlerMethodWithMiddleware<(T|M)[]>) {
		type Handler = HandlerMethodWithMiddleware<(T|M)[]>;
		handler = (handler ?? <Handler>middleware);
		middleware = (middleware instanceof Array) ? <any>this.middleware.concat(<any>middleware) : this.middleware;
		this.fastify.delete(`${this.pathPrefix}${path}`, handleMiddleware(<(T|M)[]>middleware, <Handler>handler));
	}

	 /**
	 * Register a proxy route
	 */
	public proxy<T extends MiddlewareMethod<any>>(path: string, upstream: string, middleware?: T[]) {
		this.log(`Registering proxy: ${this.pathPrefix}${path} -> ${upstream}`);
		this.fastify.register(fastifyHttpProxy, {
			prefix: `${this.pathPrefix}${path}`,
			beforeHandler: middleware ? handleMiddleware(this.middleware.concat(<any>middleware)) : undefined,
			upstream
		});
	}

	/**
	 * Log under the WebServer service
	 */
	public log(...args: any[]) {
		this.webserver.log(...args);
	}
}
