import { InternalService } from "./InternalService";
import assert from "assert";

/**
 * Application InternalService map
 */
interface InternalServiceMap {
	[name: string]: InternalService
}

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

/**
 * Microservice states
 */
export enum MicroserviceState {
	Idling,
	Booting,
	Starting,
	Running,
	Quitting,
	Finished
}

/**
 * The main application class
 */
export class Microservice
{
	/**
	 * Maintain a static reference to the application instance
	 */
	private static __instance: Microservice;

	/**
	 * A handler function to quit the microservice application
	 */
	private __quitHandler!: (value: number | PromiseLike<number>) => void;

	/**
	 * All available services
	 */
	protected services: InternalServiceMap = {};

	/**
	 * The current state of the microservice
	 */
	protected state: MicroserviceState;

	/**
	 * Return the current application instance
	 */
	public static instance() {
		return this.__instance;
	}

	/**
	 * Create a new application instance
	 */
	public constructor() {
		Microservice.__instance = this;
		this.state = MicroserviceState.Idling;
	}

	// Overridable --------------------------------------------------------------------------------

	/**
	 * Invoked when the application has finished booting
	 */
	protected onStart(): void|Promise<void> {}

	// Application Management ----------------------------------------------------------------------

	/**
	 * Boot the application and all of the services
	 */
	protected async boot() {
		let InternalServices = Object.values(this.services);
		return Promise.all(InternalServices.map(InternalService => InternalService.boot()));
	}

	/**
	 * Shutdown the application
	 */
	protected shutdown() {
		let InternalServices = Object.values(this.services);
		return Promise.all(InternalServices.map(InternalService => InternalService.shutdown()));
	}

	/**
	 * Start the application
	 */
	public async exec() {
		// Exit if not in an idling state
		if (this.state !== MicroserviceState.Idling) {
			return -1;
		}

		try {
			// Boot the microservice
			console.log("Booting services...");
			this.state = MicroserviceState.Booting;
			await this.boot();

			// Linking the internal services
			console.log("Linking services...");
			for (let service of Object.values(this.services)) {
				service.link(this);
			}

			// Start the microservice
			console.log("Starting services...");
			this.state = MicroserviceState.Starting
			await this.onStart();
			for (let service of Object.values(this.services)) {
				service.start();
			}
		} catch(e) {
			console.error("Failed to start the microservice:", e);
			return 1;
		}

		// Run the microservice
		console.log("Running");
		this.state = MicroserviceState.Running;
		process.on("SIGINT", this.quit.bind(this));
		let exitCode = await new Promise<number>((resolve) => this.__quitHandler = resolve);

		// Shutdown the microservice
		console.log("Shutting down...");
		await this.shutdown().catch(() => {
			console.log("Error ocurred during shutdown...");
			exitCode = 1;
		});

		// Return the exit code
		return exitCode;
	}

	/**
	 * Quit the application
	 */
	public async quit(code: number = 0) {
		if (this.state !== MicroserviceState.Running) {
			return;
		}
		this.__quitHandler(code);
	}

	// InternalService 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>) {
		let InternalService = new InternalServiceClass(this);
		if (InternalService.NAME in this.services) {
			throw new Error("Install Service Error: Attempted to register multiple services with the same name");
		}
		this.services[InternalService.NAME] = InternalService;
	}

	/**
	 * Get all available services
	 */
	public serviceList() {
		return Object.keys(this.services);
	}

	/**
	 * Get an application services instance
	 */
	public service<T extends InternalService<Microservice>>(InternalServiceName: string) {
		assert(InternalServiceName in this.services);
		return <T>this.services[InternalServiceName];
	}
}
