import path from 'path';
import fs from 'fs';
import http from 'http';
import { pathToFileURL } from 'url';
import express, { Express } from 'express';
import { Middleware, Route, HttpMethod, HttpHandler } from '../base/http';
import { RouteNotSetException } from '../base/exceptions';
import { IncorrectMethodException } from '../base/exceptions';
import { Logger, Message } from '../base/logger';
import { ServerProperties } from './ServerProperties';
import { i18nLoader, $$ } from '../base/i18n';
import { WebSocketServer, WebSocket as WS } from 'ws';
import { InvalidMiddlewareException } from '../base/exceptions';
import { InvalidRouteException } from '../base/exceptions';
import { ServerNotInitializedException } from '../base/exceptions';
import { WebSocketHandler } from '../base/websocket';
import { Guid } from '../base/util';
import { ExpressJsonMiddleware, ExpressUrlencodedMiddleware, InternalServerErrorHandler, NotFoundErrorHandler, PoweredByHeaderMiddleware, SessionMiddleware, SwaggerUiServeMiddleware, SwaggerUiSetupMiddleware } from '../middlewares';

/** @sealed */
class Server {
	private instance: Express;
	private httpServer: http.Server;
	private readonly port: number;
	private readonly host: string;
	private readonly logger: Logger;
	private i18n: i18nLoader;
	private readonly httpHandlers: {[key: string]: HttpHandler};
	private readonly wsHandlers: {[url: string]: WebSocketHandler};
	private readonly wsServers: {[url: string]: WebSocketServer};
	private initialized: boolean;

	private readonly i18nPath?: string;
	private readonly middlewaresPath?: string;
	private readonly routesPath?: string;
	private readonly viewEngine?: string;
	private readonly viewsPath?: string;
	private readonly options?: {[key: string]: any};

	private readonly swaggerComponents? : object;
	private readonly swaggerSecurity? : object;
	private readonly swaggerDocsPath? : string;
	private readonly swaggerTitle?: string;
	private readonly swaggerDescription?: string;
	private readonly swaggerApiVersion?: string;
	private readonly swaggerRoute?: string;

	private swagger: boolean = false;
	private json: boolean = false;
	private urlencoded: boolean = false;

	public constructor(properties: ServerProperties) {
		this.instance = express();
		this.httpServer = http.createServer(this.instance);
		this.port = properties.port;
		this.host = properties.host || `http://localhost:${this.port}`;
		this.i18n = i18nLoader.getInstance();
		if(properties.locale)
			this.i18n.setLocale(properties.locale);
		this.logger = new Logger();
		this.httpHandlers = {};
		this.wsHandlers = properties.wsHandlers || {};
		this.wsServers = {};
		this.json = properties.json || false;
		this.urlencoded = properties.urlencoded || false;
		this.i18nPath = properties.i18nPath;
		this.middlewaresPath = properties.middlewaresPath;
		this.routesPath = properties.routesPath;
		this.viewEngine = properties.viewEngine;
		this.viewsPath = properties.viewsPath;
		this.options = properties.options;
		if(properties.swagger) {
			this.swaggerDocsPath = properties.swagger?.docsPath;
			this.swaggerTitle = properties.swagger?.title || 'API Documentation';
			this.swaggerDescription = properties.swagger?.description || 'API Documentation';
			this.swaggerApiVersion = properties.swagger?.version ||'1.0.0';
			this.swaggerRoute = properties.swagger?.route || '/api-docs';
			this.swaggerComponents = properties.swagger?.components;
			this.swaggerSecurity = properties.swagger?.security;
			this.swagger = true;
		}
		this.initialized = false;
	}

	public async init(): Promise<Server> {
		if(this.viewEngine)
			this.instance.set('view engine', this.viewEngine);
		if(this.viewsPath)
			this.instance.set('views', this.viewsPath);
		this.registerSystemMiddlewares();
		await this.postInit();
		this.initialized = true;
		return this;
	}

	private async postInit(): Promise<void> {
		if(this.i18nPath)
			this.i18n.load(this.i18nPath);
		if(this.middlewaresPath)
			await this.registerMiddlewares(this.middlewaresPath);
		if(this.routesPath)
			await this.registerRoutes(this.routesPath);
		if(this.swaggerDocsPath)
			fs.writeFileSync(this.swaggerDocsPath, '');
		if(Object.keys(this.wsHandlers).length > 0) {
			this.registerWsServers();
			this.applyWsHandlers();
		}
	}

	private processHttpHandlers(): void {
		const handlers: HttpHandler[] = [];
		const middlewareOrders: number[] = [];
		for(const key in this.httpHandlers) {
			const handler = this.httpHandlers[key];
			if(handler instanceof Middleware) {
				const order = handler.getOrder();
				if(middlewareOrders.includes(order))
					throw `Ivalid order number: ${handler.constructor.name}`;
				middlewareOrders.push(order);
			}
			handlers.push(this.httpHandlers[key]);
		}

		handlers.sort((a, b) => a instanceof Middleware && b instanceof Middleware && (a.getOrder() - b.getOrder()) || (a instanceof Middleware && -1 || (b instanceof Middleware && 1 || 0)));

		for(const handler of handlers) {
			if(handler instanceof Middleware)
				this.addMiddleware(handler);
			else if (handler instanceof Route)
				this.addRoute(handler);
		}
	}

	public addMiddleware(middleware: Middleware): Server {
		if(middleware.getRoute() != null)
			this.instance.use(<string>middleware.getRoute(), middleware.getAction());
		else
			this.instance.use(middleware.getAction());
		return this;
	}

	public addRoute(route: Route): Server {
		if(route.getRoute() == null)
			throw new RouteNotSetException();
		switch(route.getMethod()) {
		case HttpMethod.GET:
			return this.get(route);
		case HttpMethod.POST:
			return this.post(route);
		default:
			throw new IncorrectMethodException();
		}
	}

	private registerRoutesDocumentation(): Server {
		if(!this.swaggerDocsPath)
			return this;
		
		for(const routeName in this.httpHandlers) {
			const route = this.httpHandlers[routeName];
			if(!(route instanceof Route))
				continue;

			if(route.getRoute() == null)
				throw new RouteNotSetException();
	
			if(![HttpMethod.GET, HttpMethod.POST].includes(route.getMethod()))
				throw new IncorrectMethodException();
	
			const docs = route.getDocumentation();
	
			if(docs.length > 0) {
				fs.appendFileSync(this.swaggerDocsPath, `${docs}\n`);
				this.logInfo($$('org.crazydoctor.expressts.swagger.routeGenerated', { route: route.getRoute() }));
			}
		}
		return this;
	}

	public registerWsServers(): Server {
		for(const url of Object.keys(this.wsHandlers)) {
			const wsServer = this.wsServers[url] = new WebSocketServer({ noServer: true });
			const wsHandler = this.wsHandlers[url];
			wsServer.on(WebSocketHandler.Event.CONNECTION, (ws: WS) => {
				wsHandler.onConnect(ws);
				ws.on(WebSocketHandler.Event.MESSAGE, wsHandler.onMessage);
				ws.on(WebSocketHandler.Event.ERROR, wsHandler.onError);
				ws.on(WebSocketHandler.Event.CLOSE, wsHandler.onClose);
			});
		}
		return this;
	}

	public applyWsHandlers(): Server {
		this.httpServer.on('upgrade', (request, socket, head) => {
			const url = request.url?.split('?')[0];
			if(url && this.wsHandlers[url] && this.wsServers[url]) {
				const wsServer = this.wsServers[url];
				wsServer.handleUpgrade(request, socket, head, (ws) => {
					wsServer.emit(WebSocketHandler.Event.CONNECTION, ws, request);
				});
			} else {
				socket.destroy();
			}
		});
		return this;
	}

	public getWsConnections(url: string): Set<WS> | null {
		const wsServer = this.wsServers[url];
		return wsServer ? wsServer.clients : null;
	}

	public logInfo(message: string): void {
		this.logger.info(message);
	}

	public logError(message: string): void {
		this.logger.error(message);
	}

	public logWarn(message: string): void {
		this.logger.warn(message);
	}

	public log(message: Message): void {
		switch(message.type) {
		case 'warn':
			return this.logWarn(message.text);
		case 'error':
			return this.logError(message.text);
		default:
			return this.logInfo(message.text);
		}
	}

	private get(route: Route): Server {
		this.instance.get(<string>route.getRoute(), route.getAction());
		return this;
	}

	private post(route: Route): Server {
		this.instance.post(<string>route.getRoute(), route.getAction());
		return this;
	}

	private registerSystemMiddlewares(): Server {
		const json = this.json, urlencoded = this.urlencoded, swagger = this.swagger;
		
		json && this.appendHttpHandler(new ExpressJsonMiddleware(this));
		urlencoded && this.appendHttpHandler(new ExpressUrlencodedMiddleware(this));
		this.appendHttpHandler(new InternalServerErrorHandler(this));
		this.appendHttpHandler(new NotFoundErrorHandler(this));
		this.appendHttpHandler(new PoweredByHeaderMiddleware(this));
		this.appendHttpHandler(new SessionMiddleware(this));
		swagger && this.appendHttpHandler(new SwaggerUiServeMiddleware(this));
		swagger && this.appendHttpHandler(new SwaggerUiSetupMiddleware(this));
		return this;
	}

	private appendHttpHandler(handler: HttpHandler): void {
		let id = Guid.new();
		while(this.httpHandlers[id] != null)
			id = Guid.new();
		this.httpHandlers[id] = handler;
	}

	public async registerRoutes(dir: string): Promise<Server> {
		const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' });

		for(const file of files) {
			if(/\.js$/.test(file)) {
				const fileUrl = pathToFileURL(path.join(dir, file)).toString();
				const RouteClass = await import(fileUrl).then(mod => mod.default?.default ?? mod.default ?? mod);
				if(RouteClass.prototype instanceof Route) {
					this.appendHttpHandler(<HttpHandler>new RouteClass(this));
				} else throw new InvalidRouteException(fileUrl);
			}
		}
		return this;
	}

	public async registerMiddlewares(dir: string): Promise<Server> {
		const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' });

		for(const file of files) {
			if(/\.js$/.test(file)) {
				const fileUrl = pathToFileURL(path.join(dir, file)).toString();
				const MiddlewareClass = await import(fileUrl).then(mod => mod.default?.default ?? mod.default ?? mod);
				if(MiddlewareClass.prototype instanceof Middleware) {
					this.appendHttpHandler(<HttpHandler>new MiddlewareClass(this));
				} else throw new InvalidMiddlewareException(fileUrl);
			}
		}
		return this;
	}

	public getLogger(): Logger {
		return this.logger;
	}

	public i18nLoad(path: string): Server {
		this.i18n.load(path);
		return this;
	}

	public getHost(): string {
		return this.host;
	}

	public getOption(key: string): any {
		return this.options ? this.options[key] || null : null;
	}

	public start(callback?: () => any): void {
		if(!this.initialized)
			throw new ServerNotInitializedException();
		this.registerRoutesDocumentation();
		this.processHttpHandlers();
		const cb = (): void => {
			this.logInfo($$('org.crazydoctor.expressts.start', { 'port': this.port }));
			if(callback) callback();
		};
		this.httpServer.listen(this.port, cb);
	}
}

export { Server };