import { io, Socket } from 'socket.io-client';
import { Main } from './Main';
import { delay, ServiceState } from './Utils';
import { State, StatusType } from './Status';

const PREFIX = `[CameraRunner]`;
export class CameraRunner {
	private _Main: Main;

	socket: Socket;

	errorTriggerStartupDelay = 10000;
	bootTime = Date.now();

	state: ServiceState = 'DISCONNECTED';
	message?: string;
	error?: string;

	processStatus: ProcessStatus = {
		state: 'STOPPED',

		startTime: -1,
		output: { current: [], last: [] },
	};

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

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

				this.sendCommand(command, callback);
				break;
		}
	}

	sendCommand(
		command: 'reboot' | 'restart',
		callback: (response: { succeed: boolean; message?: string }) => void,
	) {
		if (this.socket == null || !this.socket.connected)
			return callback({
				succeed: false,
				message: 'Not connected to CameraRunner',
			});

		this.socket.emit(
			command,
			(response: { succeed: boolean; message?: string }) => {
				callback(response);
			},
		);
	}

	broadcastState() {
		this._Main.WebServer.socket.emit('cameraRunnerState', this.getState());
	}

	getState(): CameraRunnerStatus {
		return {
			state: this.state,
			message: this.message,
			error: this.error,

			processStatus: this.processStatus,
		};
	}

	setInfo(message: string, error: string, state: ServiceState = 'FAILED') {
		this.message = message;
		this.error = error;
		this.state = state;

		this._Main.Status.update(
			StatusType.CameraPC,
			CameraPCStateColors[this.state],
			message,
			{
				reboot:
					state === 'FAILED' ||
					state === 'DISCONNECTED' ||
					state == 'CONNECTED',
			},
		);
		this.broadcastState();

		if (
			error != null &&
			Date.now() - this.bootTime > this.errorTriggerStartupDelay
		)
			this._Main.Twilio.sendError('CameraRunner', error);

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

	private pollClock: NodeJS.Timeout;
	startPollClock() {
		const poll = async () => {
			const data: ProcessStatus = await new Promise((resolve) => {
				this.socket.emit('getStatus', (response: ProcessStatus) =>
					resolve(response),
				);
			});

			this.processStatus = data;
			this._Main.Status.update(
				StatusType.CameraProcess,
				CameraProcessStateColors[this.processStatus.state],
				this.processStatus.message,
				{
					reboot:
						this.processStatus.state === 'PROBLEM' ||
						this.processStatus.state === 'STOPPED' ||
						this.processStatus.state === 'RUNNING',
				},
			);

			this.broadcastState();
		};

		this.socket.on('simpleStatus', (simpleStatus: ProcessStatusSimple) => {
			this.processStatus.state = simpleStatus.state;
			this.processStatus.message = simpleStatus.message;
			this.processStatus.error = simpleStatus.error;
			this._Main.Status.update(
				StatusType.CameraProcess,
				CameraProcessStateColors[this.processStatus.state],
				this.processStatus.message,
				{
					reboot:
						this.processStatus.state === 'PROBLEM' ||
						this.processStatus.state === 'STOPPED' ||
						this.processStatus.state === 'RUNNING',
				},
			);
			this.broadcastState();
		});

		clearInterval(this.pollClock);
		this.pollClock = setInterval(
			() => poll(),
			this._Main.Config.cameraRunner.pollInterval,
		);
		poll();
	}

	async reconnect() {
		clearInterval(this.pollClock);
		if (this.socket != null) {
			this.socket.disconnect();
			this.socket.close();
			this.socket = null;
		}

		await delay(2000);

		this.message = 'Reconnecting in 10 seconds...';
		this.broadcastState();

		await delay(5000);
		await this.connect();
	}

	async connect() {
		this.setInfo('Connecting...', null, 'CONNECTING');

		await delay(1000);

		const ip = this._Main.Config.cameraRunner.webSocket.ip;
		const port = this._Main.Config.cameraRunner.webSocket.port;

		if (ip == null || port == null) {
			return this.setInfo(
				'Connection problem',
				'Camera Runner WebSocket IP or port is not configured.',
			);
		}

		this.socket = io(`ws://${ip}:${port}`, {
			reconnectionAttempts: 0,
			reconnectionDelay: 2000,
		});

		this.socket.on('connect', () => {
			this._Main.Twilio.resetError('CameraRunner');
			this.setInfo('Connected', null, 'CONNECTED');
			this.startPollClock();
		});

		this.socket.on('disconnect', () => {
			this.setInfo(
				'Disconnected',
				'Camera Runner was disconnected unexpectedly',
				'DISCONNECTED',
			);
			this.reconnect();
		});

		this.socket.on('reconnect', () => {
			this.setInfo('Connected (reconnection)', null, 'CONNECTED');
		});

		this.socket.on('connect_error', (err) => {
			this.setInfo('Connection problem', `Connection error: ${err}`);
			this.reconnect();
		});

		this.socket.on('error', (err) => {
			this.setInfo('Connection problem', `Socket error: ${err}`);
			this.reconnect();
		});

		this.socket.on('reconnect_attempt', () => {
			this.setInfo('Reconnecting', null, 'CONNECTING');
		});

		this.socket.on('reconnect_failed', () => {
			this.setInfo('Connection problem', 'Reconnection failed');
			this.reconnect();
		});

		this.socket.on('close', () => {
			this.setInfo('Disconnected', null, 'DISCONNECTED');
			this.reconnect();
		});
	}
}

interface CameraRunnerStatus {
	state: ServiceState;
	message?: string;
	error?: string;

	processStatus: ProcessStatus;
}

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

export const CameraPCStateColors: Record<ServiceState, State> = {
	CONNECTED: State.Green,
	DISCONNECTED: State.Gray,
	CONNECTING: State.Yellow,
	FAILED: State.Red,
};
export const CameraProcessStateColors: Record<ProcessStatusState, State> = {
	RUNNING: State.Green,
	STOPPED: State.Gray,
	STARTING: State.Yellow,
	PROBLEM: State.Red,
};

interface ProcessStatusSimple {
	state: ProcessStatusState;
	message?: string;
	error?: string;
}
interface ProcessStatus extends ProcessStatusSimple {
	startTime: number;
	output: { current: string[]; last: string[] };
}
