import { pathExistsSync } from 'fs-extra';
import { ChildProcess, spawn } from 'child_process';
import { delay } from '../Utils';
import { Main } from '../Main';
import { ProcessStatusState } from '../CameraRunner';
import { StatusType } from '../Status';

const PREFIX = '[UnityRunner]';

const LOG_OUTPUT = !process.argv.includes('--no-output-log');

export class UnityRunner {
	private _Main: Main;

	state: UnityRunnerState;
	startOrigin: StartOrigin;

	message?: string = 'Awaiting startup delay...';
	error?: string;

	bootTime = Date.now();

	startTime: number = null;
	output: { current: string[]; last: string[] } = { current: [], last: [] };

	process: ChildProcess;

	constructor(Main: Main) {
		this._Main = Main;
	}

	handle(command: string, ...args: any[]) {
		const callback: (response: {
			succeed: boolean;
			message?: string;
		}) => void = args[0];
		if (typeof callback !== 'function') return;

		switch (command) {
			case 'restart':
				return callback(this.requestRestart(StartOrigin.Manual));
			case 'stop':
				return callback(this.requestStop());
			case 'start':
				return callback(this.requestStart(StartOrigin.Manual));
		}
	}

	private statusClock: NodeJS.Timeout;
	startStatusClock() {
		clearInterval(this.statusClock);
		this.statusClock = setInterval(() => {
			this.broadcastState();
		}, 3000);
	}

	requestRestart(startOrigin: StartOrigin): {
		succeed: boolean;
		message?: string;
	} {
		if (this.state !== 'RUNNING')
			return {
				succeed: false,
				message:
					'Cannot restart when process is not running. It is probably restarting already.',
			};

		this.restart(startOrigin, true);
		return { succeed: true };
	}

	requestStop(): { succeed: boolean; message?: string } {
		if (this.state !== 'RUNNING')
			return {
				succeed: false,
				message:
					'Cannot stop when process is not running. It is probably restarting already.',
			};

		this.stop();
		return { succeed: true };
	}

	requestStart(startOrigin: StartOrigin): {
		succeed: boolean;
		message?: string;
	} {
		if (this.state !== 'STOPPED')
			return {
				succeed: false,
				message: 'Cannot start when process is already running.',
			};

		this.start(startOrigin);
		return { succeed: true };
	}

	broadcastState() {
		this._Main.WebServer.socket.emit('unityRunnerState', this.getStatus());
	}

	setInfo(
		message: string,
		error: string,
		state: UnityRunnerState = 'PROBLEM',
	) {
		this.message = message;
		this.error = error;
		this.state = state;
		this._Main.Status.update(
			StatusType.UnityBuild,
			UnityBuildStateColors[state],
			message ?? error ?? '',
			{
				reboot: state === 'PROBLEM' || state === 'RUNNING',
				start: state === 'STOPPED',
				stop: state === 'RUNNING',
			},
		);
		this.broadcastState();

		if (error != null) this._Main.Twilio.sendError('UnityRunner', error);

		this.output.current.push(
			`[${new Date().toLocaleTimeString('nl-NL')}] [System] [${state}] ${
				message ?? error
			}`,
		);

		if (state == 'PROBLEM' || state == 'STOPPED')
			console.warn(PREFIX, message ?? error);
		else console.log(PREFIX, message ?? error);
	}

	async stop() {
		if (this.process == null) return;

		this.restartTriggered = true;

		if (this._Main.UnityWebSocket.state === 'CONNECTED') {
			this._Main.UnityWebSocket.quitApplication();

			this.setInfo(
				'Requested quit through WebSocket...',
				null,
				'STARTING',
			);
		} else {
			this.process.kill('SIGTERM');
			this.setInfo('Requested quit through SIGTERM...', null, 'STARTING');
		}

		const quitSucceeded = await new Promise((resolve) => {
			const timeout = setTimeout(() => {
				clearInterval(c);
				resolve(false);
			}, 5000);

			const c = setInterval(() => {
				if (
					this.process != null &&
					!this.process.killed &&
					this.process.exitCode == null
				)
					return;

				clearTimeout(timeout);
				clearInterval(c);
				resolve(true);
			});
		});

		if (!quitSucceeded) this.process.kill('SIGKILL');

		this.setInfo('Stopped', null, 'STOPPED');

		this._Main.UnityWebSocket.disconnect();

		this._Main.WebServer.Calibration.hasCalibrationImage = false;
		this.startTime = -1;
	}

	restartTriggered: boolean = false;
	async restart(origin: StartOrigin, instant: boolean = false) {
		if (this.restartTriggered) return;
		this.restartTriggered = true;

		this._Main.WebServer.Calibration.hasCalibrationImage = false;

		clearInterval(this.statusClock);

		this.startTime = -1;
		this.startOrigin = null;
		this.broadcastState();

		if (!instant) await delay(2000);

		if (this.output.current.length > 0) {
			this.output.last = [...this.output.current];
			this.output.current = [];
		}

		await this.stop();

		if (instant) {
			await delay(1000);
			this.setInfo('Process will restart shortly...', null, 'STOPPED');
			await delay(2000);
		} else {
			this.message = 'Reconnecting in 10 seconds...';
			this.broadcastState();

			await delay(10000);
		}
		await this.start(this.startOrigin);
	}

	async start(origin: StartOrigin) {
		if (this.output.current.length > 0) {
			this.output.last = [...this.output.current];
			this.output.current = [];
		}
		this.startTime = Date.now();
		this.restartTriggered = false;
		this.broadcastState();

		this._Main.WebServer.Calibration.hasCalibrationImage = false;

		clearInterval(this.statusClock);

		const path = this._Main.Config.unity.executable.path;
		if (path == null || !pathExistsSync(path)) {
			this.setInfo(
				'Executable problem',
				`Executable path is not set or does not exist: ${path}`,
			);
			return;
		}

		if (this._Main.CameraRunner.state !== 'CONNECTED')
			await new Promise<void>((resolve) => {
				this.setInfo(
					'Waiting for CameraRunner to connect...',
					null,
					'STARTING',
				);
				var c = setInterval(() => {
					if (this._Main.CameraRunner.state !== 'CONNECTED') return;

					clearInterval(c);
					resolve();
				}, 1000);
			});

		if (this._Main.CameraRunner.processStatus?.state !== 'RUNNING')
			await new Promise<void>((resolve) => {
				this.setInfo(
					'Waiting for CameraRunner process to start...',
					null,
					'STARTING',
				);
				var c = setInterval(() => {
					if (
						this._Main.CameraRunner.processStatus?.state !==
						'RUNNING'
					)
						return;

					clearInterval(c);
					resolve();
				}, 1000);
			});

		const fileName = path.split(/(\/|\\)/).pop();

		this.setInfo(`Starting executable: ${fileName}`, null, 'STARTING');

		this.process = spawn(
			path,
			this._Main.Config.unity.executable.arguments,
			{
				stdio: 'pipe',
			},
		);

		this.process.on('exit', (code, signal) => {
			if (this.restartTriggered) return;
			this.setInfo(
				'Process exited',
				`Process exited with code ${code} and signal ${signal}`,
				'PROBLEM',
			);
			this.restart(this.startOrigin);
		});
		this.process.on('error', (err) => {
			this.setInfo('Process error', err.message);
			this.restart(this.startOrigin);
		});
		this.process.stdout?.on('data', (data) => {
			const lines = data
				.toString()
				.trim()
				.split('\n')
				.filter((line) => line.length > 0);
			lines.forEach((line) => {
				const formattedLine = `[${new Date().toLocaleTimeString(
					'nl-NL',
				)}] [${fileName}] ${line}`;
				if (LOG_OUTPUT) console.log(PREFIX, formattedLine);
				this.output.current.push(formattedLine);
			});
		});
		this.process.stderr?.on('data', (data) => {
			const lines = data
				.toString()
				.trim()
				.split('\n')
				.filter((line) => line.length > 0);
			lines.forEach((line) => {
				const formattedLine = `[${new Date().toLocaleTimeString(
					'nl-NL',
				)}] [${fileName}] [ERROR] ${line}`;
				if (LOG_OUTPUT) console.error(PREFIX, formattedLine);
				this.output.current.push(formattedLine);
			});
		});
		this.startOrigin = origin;
		this.startStatusClock();
		setTimeout(() => {
			if (
				this.process == null ||
				this.process?.killed ||
				this.process?.exitCode != null ||
				this.restartTriggered
			)
				return;

			this._Main.Twilio.resetError('UnityRunner');
			this.setInfo('Running', '', 'RUNNING');
			this._Main.UnityWebSocket.connect();
		}, 5000);
	}

	getStatus(): UnityRunnerStatus {
		return {
			state: this.state,
			message: this.message,
			error: this.error,

			startTime: this.startTime,
			output: this.output,
		};
	}
}

interface UnityRunnerStatus {
	state: 'RUNNING' | 'STOPPED' | 'STARTING' | 'PROBLEM';
	message?: string;
	error?: string;

	startTime: number;
	output: { current: string[]; last: string[] };
}

enum State {
	Green = 'GREEN',
	Yellow = 'YELLOW',
	Red = 'RED',
	Gray = 'GRAY',
}

export const UnityBuildStateColors: Record<ProcessStatusState, State> = {
	RUNNING: State.Green,
	STOPPED: State.Gray,
	STARTING: State.Yellow,
	PROBLEM: State.Red,
};

export type UnityRunnerState = 'RUNNING' | 'STOPPED' | 'STARTING' | 'PROBLEM';

export enum StartOrigin {
	Timer = 'TIMER',
	Manual = 'MANUAL',
	Startup = 'STARTUP',
}
