class API {
	constructor(address) {
		this.base = '/api';
		this.address = address;
		this.token = '';

		this.cache = new Map();
	}

	_debug(message) {
		//console.log(`[CoreAPI] ${message}`);
	}

	_error(message) {
		console.error(`[CoreAPI] Error: ${message}`);
	}

	async _GET(path, options) {
		const key = path + JSON.stringify(options);

		const data = this.cache.get(key);
		if (data !== undefined) {
			const millis = Date.now() - data.ts;

			if (millis < 950) {
				return JSON.parse(JSON.stringify(data.payload));
			}
		}

		const ts = Date.now();

		const res = await this._call('GET', path, options);

		this.cache.set(key, {
			payload: JSON.parse(JSON.stringify(res)),
			ts: ts,
		});

		return res;
	}

	async _HEAD(path, options) {
		return await this._call('HEAD', path, options);
	}

	async _POST(path, options) {
		return await this._call('POST', path, options);
	}

	async _PUT(path, options) {
		return await this._call('PUT', path, options);
	}

	async _DELETE(path, options) {
		return await this._call('DELETE', path, options);
	}

	async _PATCH(path, options) {
		return await this._call('PATCH', path, options);
	}

	async _call(method, path, options = {}) {
		options = {
			method: method.toUpperCase(),
			expect: 'any',
			headers: {},
			token: '',
			...options,
		};

		path = this.base + path;
		if (path !== '/') {
			// remove slash at the end of the path
			if (path[path.length - 1] === '/') {
				path = path.substring(0, path.length - 1);
			}
		}

		let token = '';

		if (options.token.length !== 0) {
			token = options.token;
		} else {
			if (typeof this.token === 'function') {
				token = await this.token();
			} else {
				token = this.token;
			}
		}

		if (token.length !== 0) {
			options.headers.Authorization = 'Bearer ' + token;
		}

		this._debug(`calling ${options.method} ${this.address + path}`);

		const res = {
			err: null,
			val: null,
		};

		let response = null;

		try {
			response = await fetch(this.address + path, options);
		} catch (err) {
			res.err = {
				code: -1,
				message: err.message,
			};

			this._error(res.err.message);

			return res;
		}

		const contentType = response.headers.get('Content-Type');
		let isJSON = false;

		if (contentType != null) {
			isJSON = contentType.indexOf('application/json') !== -1;
		}

		if (response.ok === false) {
			res.err = {
				code: response.status,
				message: response.statusText,
			};

			if (isJSON === true) {
				const body = await response.json();

				if ('code' in body && 'message' in body) {
					res.err.message = body.message;
				} else {
					res.err.message = body;
				}
			} else {
				const body = await response.text();
				if (body.length > 0) {
					res.err.message = body;
				}
			}

			this._error(res.err.message);

			return res;
		}

		if (isJSON === true) {
			res.val = await response.json();
		} else {
			res.val = await response.text();
		}

		if (options.expect === 'json') {
			if (isJSON === false) {
				res.val = null;
				res.err = {
					code: -2,
					message: `The response is not JSON as expected (${contentType})`,
				};

				this._error(res.err.message);
			}
		}

		return res;
	}

	SetAddress(address) {
		this.address = address;
	}

	SetToken(token) {
		this.token = token;
		this.cache = new Map();
	}

	async Login(username, password) {
		return await this._POST('/login', {
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify({
				username: username,
				password: password,
			}),
			expect: 'json',
		});
	}

	async LoginWithToken(token) {
		return await this._POST('/login', {
			headers: {
				'Content-Type': 'application/json',
			},
			expect: 'json',
			token: token,
		});
	}

	async RefreshToken(refresh_token) {
		return await this._GET('/login/refresh', {
			expect: 'json',
			token: refresh_token,
		});
	}

	async About() {
		return await this._GET('/', {
			expect: 'json',
		});
	}

	async Skills() {
		return await this._GET('/v3/skills', {
			expect: 'json',
		});
	}

	async SkillsReload() {
		return await this._GET('/v3/skills/reload', {
			expect: 'json',
		});
	}

	async Config(type) {
		return await this._GET('/v3/config', {
			expect: 'json',
		});
	}

	async ConfigSet(config) {
		return await this._PUT('/v3/config', {
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(config),
			expect: 'json',
		});
	}

	async ConfigReload(type) {
		return await this._GET('/v3/config/reload');
	}

	async Log() {
		return await this._GET('/v3/log', {
			expect: 'json',
		});
	}

	async ActiveSessions(collectors) {
		return await this._GET('/v3/session/active?collectors=' + encodeURIComponent(collectors.join(',')), {
			expect: 'json',
		});
	}

	async SetMetadata(key, data) {
		return await this._PUT('/v3/metadata/' + encodeURIComponent(key), {
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(data),
			expect: 'json',
		});
	}

	async GetMetadata(key) {
		return await this._GET('/v3/metadata/' + encodeURIComponent(key), {
			expect: 'json',
		});
	}

	async DataHasFile(path) {
		return await this._HEAD('/v3/fs/disk' + path);
	}

	async DataPutFile(path, data) {
		return await this._PUT('/v3/fs/disk' + path, {
			headers: {
				'Content-Type': 'application/data',
			},
			body: data,
		});
	}

	async DataGetFile(path) {
		return await this._GET('/v3/fs/disk' + path);
	}

	async DataListFiles(pathPattern) {
		return await this._GET('/v3/fs/disk?glob=' + encodeURIComponent(pathPattern), {
			expect: 'json',
		});
	}

	async DataDeleteFile(path) {
		return await this._DELETE('/v3/fs/disk' + path);
	}

	async MemFSListFiles(pathPattern) {
		return await this._GET('/v3/fs/mem?glob=' + encodeURIComponent(pathPattern), {
			expect: 'json',
		});
	}

	async MemFSHasFile(path) {
		return await this._HEAD('/v3/fs/mem' + path);
	}

	async MemFSDeleteFile(path) {
		return await this._DELETE('/v3/fs/mem' + path);
	}

	async MemFSLinkFile(path, linkto) {
		return await this._PATCH('/v3/fs/mem/' + path, {
			headers: {
				'Content-Type': 'application/data',
			},
			body: linkto,
		});
	}

	async Processes(reference = '', ids = [], filter = []) {
		let url = '/v3/process';
		let params = [];

		if (reference.length !== 0) {
			params.push('reference=' + encodeURIComponent(reference));
		}

		if (ids.length !== 0) {
			params.push('id=' + encodeURIComponent(ids.join(',')));
		}

		if (filter.length !== 0) {
			params.push('filter=' + encodeURIComponent(filter.join(',')));
		}

		if (params.length !== 0) {
			url = url + '?' + params.join('&');
		}

		return await this._GET(url, {
			expect: 'json',
		});
	}

	async Process(name, filter = []) {
		let url = '/v3/process/' + name;
		if (filter.length !== 0) {
			url = url + '?filter=' + encodeURIComponent(filter.join(','));
		}

		return await this._GET(url, {
			expect: 'json',
		});
	}

	async ProcessConfig(name) {
		return await this._GET('/v3/process/' + encodeURIComponent(name) + '/config', {
			expect: 'json',
		});
	}

	async ProcessState(name) {
		return await this._GET('/v3/process/' + encodeURIComponent(name) + '/state', {
			expect: 'json',
		});
	}

	async ProcessReport(name) {
		return await this._GET('/v3/process/' + encodeURIComponent(name) + '/report', {
			expect: 'json',
		});
	}

	async ProcessCommand(name, command) {
		return await this._PUT('/v3/process/' + encodeURIComponent(name) + '/command', {
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify({
				command: command,
			}),
			expect: 'json',
		});
	}

	async ProcessDelete(name) {
		return await this._DELETE('/v3/process/' + encodeURIComponent(name));
	}

	async ProcessUpdate(name, config) {
		return await this._PUT('/v3/process/' + encodeURIComponent(name), {
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(config),
		});
	}

	async ProcessSetMetadata(name, key, data) {
		return await this._PUT('/v3/process/' + encodeURIComponent(name) + '/metadata/' + encodeURIComponent(key), {
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(data),
			expect: 'json',
		});
	}

	async ProcessGetMetadata(name, key) {
		return await this._GET('/v3/process/' + encodeURIComponent(name) + '/metadata/' + encodeURIComponent(key), {
			expect: 'json',
		});
	}

	async ProcessAdd(config) {
		return await this._POST('/v3/process', {
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(config),
			expect: 'json',
		});
	}

	async ProcessProbe(name) {
		return await this._GET('/v3/process/' + encodeURIComponent(name) + '/probe', {
			expect: 'json',
		});
	}

	async Metrics(query) {
		return await this._POST('/v3/metrics', {
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify(query),
			expect: 'json',
		});
	}
}

export default API;
