import EventEmitter from "events";
import { IDispatchableMethods, InternalService } from "./InternalService";
import { MicroserviceState } from "./schema";
import { InternalServiceConflictError, InternalServiceNotFoundError } from "./errors";

/**
 * The InternalService constructor type
 */
type InternalServiceConstructor<T extends Microservice = Microservice> = new (microservice: T) => InternalService<T>;

/**
 * Declare EventEmitter types
 */
 interface Events {
    "ready": () => void,
	"finished": () => void
}

/**
 * Torrent IPC events
 */
export declare interface Microservice {
    on<U extends keyof Events>(event: U, listener: Events[U]): this,
    emit<U extends keyof Events>(event: U, ...args: Parameters<Events[U]>): boolean
}

/**
 * The main application class
 */
export class Microservice extends EventEmitter
{
	/**
	 * The exec promise used to wait for quit event
	 */
	#execPromise?: Promise<number>;

	/**
	 * A handler function to quit the microservice application
	 */
	#quitHandler?: (value: number | PromiseLike<number>) => void;

	/**
	 * All available services
	 */
	#services = new Map<ThisType<this>, InternalService<any>>();

	/**
	 * The current state of the microservice
	 */
	#state: MicroserviceState = MicroserviceState.Idling;

	// Event Handling ------------------------------------------------------------------------------

	/**
	 * Invoked when the application has finished booting
	 */
	protected onStateChange(state: MicroserviceState): void|Promise<void> {
		switch(state) {
			case MicroserviceState.Running:
				this.emit("ready");
				break;
			case MicroserviceState.Finished:
				this.emit("finished");
				break;
		}
	}

	// Microservice Management ---------------------------------------------------------------------

	/**
	 * Run the application
	 */
	public async exec() {
		// Exit if not in an idling state
		if (this.state !== MicroserviceState.Idling) {
			console.error("Cannot exec an already-started microservice");
			return 1;
		}
		// Run the microservice application
		let exitCode = await (async () => {
			// Create the microservice execution promise to listen for quit events
			let hasQuit = false;
			this.#execPromise = new Promise<number>(resolve => this.#quitHandler = (exitCode) => {
				resolve(exitCode);
				console.log("Quit has been invoked");
				hasQuit = true;
			});

			// Boot the microservice and internal services
			if (!await this.boot()) { // no need to check for hasQuit
				console.error("Failed to boot the microservice");
				return 1;
			}
			// Start the internal services
			if (!hasQuit && !await this.start()) {
				console.error("Failed to start the microservice");
				return 2;
			}
			// If the application has not quit, we can run the app
			let exitCode: number;
			if (!hasQuit) {
				exitCode = await this.run();
			} else {
				exitCode = await this.#execPromise;
			}

			// Shutdown the microservice
			if (!await this.shutdown()) {
				console.error("Failed to shutdown the microservice");
				return 3;
			}
			return exitCode;
		})();
		this.state = MicroserviceState.Finished;
		return exitCode;
	}

	/**
	 * Quit the application
	 */
	public async quit(code: number = 0) {
		if (this.state === MicroserviceState.Idling) {
			this.state = MicroserviceState.Finished;
			return;
		}
		if (this.state > MicroserviceState.Running || this.#quitHandler === undefined) {
			return;
		}
		this.#quitHandler(code);
	}

	// Microservice Internal Handling --------------------------------------------------------------

	/**
	 * Dispatch a method call to the services, returning a boolean indicating the result
	 */
	protected dispatch<T extends Microservice>(this: T, method: keyof IDispatchableMethods,
		onFail: (service: InternalService<T>, error: Error|string) => void)
	{
		return new Promise<boolean>(resolve => {
			let services = Array.from(this.#services.values());
			Promise.all(services.map(service => (async () => {
				try {
					await service[method]();
				} catch(e) {
					onFail(service, e);
					throw e;
				}
			})()))
			.then(() => resolve(true))
			.catch(() => resolve(false));
		});
	}

	/**
	 * Boot the application and all of the services
	 */
	protected boot() {
		console.log("Booting...");
		this.state = MicroserviceState.Booting;
		return this.dispatch("boot", (service, error) => {
			console.error(`Failed to boot service: ${service.NAME}\n`, error);
		});
	}

	/**
	 * Start the application servvices
	 */
	protected start() {
		console.log("Starting...");
		this.state = MicroserviceState.Starting;
		return this.dispatch("start", (service, error) => {
			console.error(`Failed to start service: ${service.NAME}\n`, error);
		});
	}

	/**
	 * Run the application and wait for shutdown
	 */
	protected run() {
		console.log("Running.");
		this.state = MicroserviceState.Running;
		process.on("SIGINT", this.quit.bind(this));
		return this.#execPromise!;
	}

	/**
	 * Shutdown the application
	 */
	protected shutdown() {
		console.log("Shutting down...");
		this.state = MicroserviceState.Quitting;
		process.off("SIGINT", this.quit.bind(this));
		return this.dispatch("shutdown", (service, error) => {
			console.error(`Failed to shutdown service: ${service.NAME}\n`, error);
		});
	}

	// Internal Service Management -----------------------------------------------------------------

	/**
	 * Install InternalServices into the application
	 */
	 public installServices<T extends Microservice>(this: T, InternalServices: InternalServiceConstructor<T>[]) {
		for (let InternalServiceClass of InternalServices) {
			this.installService(InternalServiceClass);
		}
	}

	/**
	 * Install a InternalService into the application
	 */
	public installService<T extends Microservice>(this: T, InternalServiceClass: InternalServiceConstructor<T>) {
		if (this.#services.has(InternalServiceClass)) {
			throw new InternalServiceConflictError(InternalServiceClass);
		}
		this.#services.set(InternalServiceClass, new InternalServiceClass(this));
	}

	// Accessors -----------------------------------------------------------------------------------

	/**
	 * Get an application services instance
	 */
	public service<T extends Microservice, U extends InternalServiceConstructor<T>>(this: T, InternalServiceClass: U) {
		if (!this.#services.has(InternalServiceClass)) {
			throw new InternalServiceNotFoundError(InternalServiceClass);
		}
		return <InstanceType<U>>this.#services.get(InternalServiceClass);
	}

	/**
	 * Get the current state of the microservice
	 */
	public get state() {
		return this.#state;
	}

	/**
	 * Set the current state and invoke an onChange event
	 */
	protected set state(state: MicroserviceState) {
		this.#state = state;
		this.onStateChange(state);
	}
}
