class DataSync {

	/** @type {Object.<string, DataSyncCategory>} */
	categories = {};

	constructor() {

	}

	/** @param {DataSyncCategory} category */
	registerCategory(category) {
		this.categories[category.category] = category;
		return this;
	}

	/** @return {DataSyncCategory} */
	category(category) {
		if(typeof this.categories[category] !== "undefined") {
			return this.categories[category];
		} else {
			throw new Error("Category " + category + " is not registered");
		}
	}

	async addItem(category, data, key = null, instantSave = true) {
		const $this = this;
		const cat = await this.category(category);
		const item = await cat.addItem(data, key, instantSave);
		return {category: cat, item: item};
	}

	async saveCategory(category) {
		await this.category(category).save();
	}

	async renderLists() {
		const $this = this;
		let $container = $('.offlineData-sync-list');
		

		let countAll = 0;
		let $allLists = [];
		for(let c in this.categories) {
			if(this.categories.hasOwnProperty(c)) {
				let cnt = count(await this.category(c).getItems())
				countAll += cnt;
				/*if(cnt > 0) {*/
				let $list = await this.category(c).renderList();
				// $container.append($list);
				$allLists.push($list);
				/*} //cnt*/
			}
		}

		$container.html('');
		for(let $list of $allLists) {
			$container.append($list);
		}
		$container.append('<div class="all-synced-info"><i class="fa-regular fa-check-circle text-color text-green"></i> alle Daten wurden hochgeladen.</div>');
		$container.append('<div class="not-synced-info"><div class="hidden-offline"><button type="button" class="control-btn control-btn-green small"><i class="fa-regular fa-cloud-arrow-up"></i> jetzt hochladen</button></div></div>');

		let $syncBtn = $container.find('.data-sync-all-lists-sync-btn');
		$syncBtn.on('click', function () {
			if(isOnline()) {
				$('.data-sync-all-lists-sync-btn, .data-sync-list-sync-btn').prop('disabled', true);
				$this.syncLists();
			}
		});
		if(countAll === 0) {
			// $('.offlineData-count-badge[data-category="total"]').html('');
			this.updateBadgeCounter('');
		} else {
			this.updateBadgeCounter(countAll);
			// $('.offlineData-count-badge[data-category="total"]').html(countAll);

		}
	}

	async updateBadgeCounter(num = null) {
		if(num === null) {
			num = await this.countItems();
		}
		let $allSyncedInfo = $('.offlineData-sync-list .all-synced-info');
		let $notSyncedInfo = $('.offlineData-sync-list .not-synced-info');
		let $badge = $('.offlineData-count-badge[data-category="total"]');
		let $navItem = $('#app-nav-offlineData');
		if(empty(num)) {
			$badge.html('');
			$allSyncedInfo.show();
			$notSyncedInfo.hide();
			$navItem.hide();

		} else {
			$badge.html(num);
			$allSyncedInfo.hide();
			$notSyncedInfo.show();
			$navItem.show();
		}
	}

	async countItems() {
		let countAll = 0;
		for(let c in this.categories) {
			if(this.categories.hasOwnProperty(c)) {
				countAll += count(await this.category(c).getItems())
			}
		}
		return countAll;
	}

	async syncLists() {
		// let $navItem = $('#app-nav-offlineData');
		// $navItem.addClass('uploading');
		for(let c in this.categories) {
			if(this.categories.hasOwnProperty(c)) {
				let catList = await this.category(c).syncList();
			}
		}
		// $navItem.removeClass('uploading');
		await this.renderLists();
		
	}

}

class DataSyncCategory {
	category;
	/** @type {function|string} */
	endpoint;
	/** @type {appStorage} */
	storage;
	loaded;
	syncListComplete = true;
	items = {};
	options = {
		categoryName: null
	};

	constructor(category, endpoint, options) {
		const $this = this;
		this.category = category;
		this.endpoint = endpoint;
		const storageName = DataSyncCategory.storageName(this.category);
		this.setOptions(options)
		this.loaded = new Promise(function (resolve, reject) {
			$this.storage = new appStorage(storageName);
			resolve(true);
		});
	}

	setOptions(options) {
		options = options || {};
		const $this = this;
		this.options = {...$this.options, ...options};
	}


	async save() {
		await this.loaded;
		await this.set('@lastupdated', date('Y-m-d H:i:s'));
		const save = this.storage.save();

		return this;
	}

	async clear() {
		await this.loaded;
		this.storage.clear();
		return this;
	}

	async addItem(value, key = null, instantSave = true) {
		await this.loaded;
		key = key || unique_id(20);

		value = this.satinizeInput(value);
		let item = {
			'timestamp': date('Y-m-d H:i:s'),
			'key': key,
			'data': value
		};
		let resultKey = await this.storage.addItem(item, key, false);

		if(instantSave) {
			await this.save();
		}
		let $displayItem;
		$displayItem = this.$list().find('.data-sync-category-list-item[data-item_key="' + key + '"]');
		if($displayItem.length !== 0) {
			$displayItem.remove();
		}
		$displayItem = await this.renderListItem(item);
		this.$list().append($displayItem);
		this.updateBadgeCounter(); // no need to await

		// return this;
		return resultKey;
	}

	async removeItem(idx, instantSave = true) {
		await this.loaded;
		const success = this.storage.removeItem(idx);
		if(success === false) {
			// throw new Error("Item with index " + idx + " not found in " + this.category);
			return false; // no need for error
		}
		if(instantSave) {
			// this.storage.save();
			await this.save();
		}

		let $displayItem = this.$list().find('.data-sync-category-list-item[data-item_key="' + idx + '"]');
		$displayItem.remove();
		this.updateBadgeCounter(); // no need to await


		return this;
	}

	async getItem(idx) {
		await this.loaded;
		return this.storage.getItem(idx);
	}

	async getItems() {
		await this.loaded;
		/*let items = {};
		for(let i in this.storage?.data?.items || []) {
			if(this.storage.data.items.hasOwnProperty(i)) {
				// let k = 
			}
		}*/
		return this.storage?.data?.items ?? {};
	}

	async get(name) {
		await this.loaded;
		return this.storage.get(name);
	}

	async set(name, value) {
		await this.loaded;
		this.storage.set(name, value);
		return this;
	}

	async syncItem(itemKey, offlineFallback, notify = 'notification') {
		const $this = this;
		let $navItem = $('#app-nav-offlineData');
		offlineFallback = offlineFallback || function (a) {return a;};
		if(typeof itemKey !== 'string') {
			throw new Error("item Key must be a string");
		}
		let item = await this.getItem(itemKey);
		let data = item.data;
		if(!data) {
			throw new Error("item " + itemKey + " does not exist");
		}


		if(isOnline()) {
			let noti = false;
			/*if(notify === 'notification') {
				noti = this.syncNotification(data);
			}*/

			let $list = $this.$list();
			let $row = $list.find('.data-sync-category-list-item[data-item_key="' + item.key + '"]');
			let $progress = $row.find('.sync-progress');
			$progress.show();
			$progress.html('<div class="progress">'
				+ '<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">'
				+ '<span class="sr-only">wird übertragen...</span>'
				+ '</div>'
				+ '</div>'// /.progress
			);

			$navItem.addClass('uploading');


			return $this.#syncRequest(data)
				.then(function (result) {
					if(typeof result.syncSuccess === 'undefined') {
						throw new Error('syncSuccess must be defied in result');
					}
					if(result.syncSuccess === true) {
						$this.removeItem(itemKey, true);
						$progress.html('<div class="alert alert-success" ><i class="fa-light fa-check-circle"></i> Übertragung erfolgreich</div>');
						setTimeout(() => {
							$row.remove();
						}, 5000);
					} else {
						$progress.html('<span class="red">Fehler bei Übertragung.</span>');
					}
					/*if(noti) {
						noti.then(n => {
							n.notificationElement.find('.progress').hide();
							n.notificationElement.find('.no-sync-success-msg').show();
							setTimeout(async function () { n.close();}, 3000);
						});
					}*/
					$navItem.removeClass('uploading');
					return {...result, ...{notification: noti, item: item}};
				});
		} else {
			// return offlineFallback(data);
			return {
				syncSuccess: 'offline',
				json: await offlineFallback(data),
				notification: false,
				item: item
			};
		}
	}

	async syncNotification(item) {
		let previewText = await this.itemPreview(item);
		let content = '<div>'
			+ '<div>' + previewText.label + '</div>'
			+ '<div class="progress">'
			+ '<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">'
			+ '<span class="sr-only">wird übertragen...</span>'
			+ '</div>'
			+ '</div>'// /.progress
			+ '<div class="alert alert-success no-sync-success-msg" style="display:none;"><i class="fa-light fa-check-circle"></i> erfolgreich</div>'
			+ '</div>';
		let no = newNotification(content, this.categoryName(), false, 'info');
		let $no = $(no);
		return {
			notificationElement: $no,
			close: function () {
				let nr = $no.data('no_nr');
				hidenotification(nr);
			}
		};
	}

	async #syncRequest(data) {
		data = data || {};
		if(typeof this.options?.processUploadData === 'function') {
			data = this.options?.processUploadData(data);
		}
		// overwrite auth if old token
		/*data.app_user_id = app_user_id();
		data.api_token = userInfo('api_token');*/
		if(data?.app_user_id) {
			delete data.app_user_id;
		}
		if(data?.api_token) {
			delete(data.api_token);
		}
		// /overwrite auth if old token

		if(typeof this.endpoint === 'function') {
			return this.endpoint(data);
		} else {
			let url = new URL(this.endpoint, appURL());
			return _fetch(url.toString(), {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
				},
				body: JSON.stringify(data),
				timeout: 15000,
			})
				.catch((whatever) => {
					console.log(whatever);
					return data;
				})
				.then(response => response.json())
				.then(json => {
					json = normalizeJSON(json);
					let syncSuccess = DataSyncCategory.hasSuccess(json);
					return {
						syncSuccess: syncSuccess,
						json: json
					}
				});
		}
	}

	async itemPreview(item) {
		if(typeof item === 'string') {
			item = this.getItem(item);
		}
		let data = item?.data;

		let label = typeof this.options?.itemPreviewLabel === 'function' ? await this.options?.itemPreviewLabel(item, this) : 'Datensatz';

		// return '1 '+this.category+' Datensatz';

		return {
			label: label
		};
	}

	/*async syncItem__v1(itemKey, callback, offlineFallbackReturn) {
		const $this = this;
		callback = typeof callback === 'function' ? callback : function(a) {return a; }
		offlineFallbackReturn = typeof offlineFallbackReturn === 'function' ? offlineFallbackReturn : function(a) {return a; }
		if(typeof itemKey !== 'string') {
			throw new Error("item Key must be a string");
		}
		let data = this.getItem(itemKey);
		if(!data) {
			throw new Error("item "+itemKey+" does not exist");
		}
		if(isOnline()) {
			// create notification with progressbar
			data = this.processSyncData(data);
			return this.upload(data).then(function(json) {
				let success = $this.constructor.hasSuccess(json);
				
				return callback(json);
			});
			
		} else {
			let json = typeof offlineFallbackReturn === 'function' ? offlineFallbackReturn(data) : offlineFallbackReturn ?? data;
			return callback(json);
		}
	}*/


	async syncAll() {

	}

	async syncList($list) {
		let $navItem = $('#app-nav-offlineData');
		if(isOnline()) {
			$navItem.addClass('uploading');
			let resolveSyncListComplete;
			this.syncListComplete = new Promise(function (resolve) {
				resolveSyncListComplete = resolve;
			});
			let items = await this.getItems();
			let numUploaded = 0;
			let numFailed = 0;
			if(count(items) > 0) {
				for(let i in items) {
					if(items.hasOwnProperty(i)) {
						let item = items[i];
						/*let $row = $list.find('.data-sync-category-list-item[data-item_key="' + item.key + '"]');
						let $progress = $row.find('.sync-progress');
						$progress.show();
						$progress.html('<div class="progress">'
							+ '<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">'
							+ '<span class="sr-only">wird übertragen...</span>'
							+ '</div>'
							+ '</div>'// /.progress
						);*/
						let sync = await this.syncItem(item.key, null, false)
							/*.then((result) => {
								if(result.syncSuccess) {
									$progress.html('<div class="alert alert-success" ><i class="fa-light fa-check-circle"></i> Übertragung erfolgreich</div>');
									setTimeout(() => {
										$row.remove();
									}, 5000);
									return true;
								} else {
									$progress.html('<span class="red">Fehler bei Übertragung.</span>');
									return false;
								}
							})*/
						;
						if(sync.syncSuccess === true) {
							numUploaded++;
						} else {
							numFailed++;
						}
					}
				}
				// $list.replaceWith(await this.renderList());
			}
			if(numUploaded > 0) {
				newNotification(numUploaded+' Datensätze erfolgreich hochgeladen', this.categoryName(), 4000, 'success');
			}
			if(numFailed > 0) {
				newNotification(numFailed+' Datensätze konnten nicht hochgeladen werden. Probiere es später nocheinmal', this.categoryName(), 4000, 'error');
			}
			$navItem.removeClass('uploading');
			resolveSyncListComplete(true);
			return true;
		} else {
			sweetAlert('Übertragung kann nur Online durchgefürht werden.');
			return false;
		}
	}

	async renderList($target = null) {
		await this.loaded;
		await this.syncListComplete;
		const $this = this;
		let items = await this.getItems();
		let $container = $('<div class="data-sync-category-container"></div>');
		$container.attr('data-category', $this.category);
		$container.html(
			'<h3>'
			+ $this.categoryName()
			+ ' <button type="button" class="control-btn control-btn-green small hidden-offline data-sync-list-sync-btn"><i class="fa-regular fa-cloud-arrow-up"></i></button>'
			+ '</h3>'
			+ '<div class="list-group data-sync-category-list"></div>'
		);
		let $list = $container.find('.data-sync-category-list').first();
		$list.attr('data-category', $this.category);
		let $syncBtn = $container.find('.data-sync-list-sync-btn');
		if(count(items) > 0) {

			// let $list = $('<div class="offline-donation-list"></div>');
			for(let i in items) {
				if(items.hasOwnProperty(i)) {
					let $row = await this.renderListItem(items[i]);
					$list.append($row);
				}
			}
		}
		$syncBtn.on('click', async function () {
			$syncBtn.prop('disabled', true);
			// $this.syncList($list);
			$this.syncList($this.$list()).then((synced) => {
				if(synced) {
					setTimeout(async () => {
						let $newList = await $this.renderList();
						if($newList) {
							$this.$container().replaceWith($newList);
						} else {
							$this.$container().remove();
						}
					}, 6000);
				}
			});
		});

		if($target) {
			$target.html('').append($container);
		}
		return $container;
		// return false;
	}

	async renderListItem(item) {
		let key = item.key;
		let preview = await this.itemPreview(item);

		let timestamp = item?.timestamp || date('Y-m-d H:i:s');
		let timestring = date('Ymd', timestamp) === date('Ymd') ? date('H:i', timestamp) : date('d.m. - H:i', timestamp);

		let $row = '<div class="list-group-item data-sync-category-list-item">'
			+ '<span class="small">' + timestring + '</span> - '
			+ preview.label
			+ '<div class="sync-progress" style="display:none;"></div>'
			+ '</div>';

		$row = $($row);
		$row.attr('data-item_key', key);

		return $row;
	}

	categoryName() {
		return this.options?.categoryName ?? this.category
	}

	$list() {
		return $('.data-sync-category-list[data-category="' + this.category + '"]');
	}

	$container() {
		return $('.data-sync-category-container[data-category="' + this.category + '"]');
	}

	appendToList($domElement) {
		let append = this.$list().append($domElement);
		this.updateBadgeCounter();
		return append;
	}

	async updateBadgeCounter(num = null, updateTotalCounter=true) {
		if(num === null) {
			num = await this.countItems();
		}
		let $badge = $('.offlineData-count-badge[data-category="' + this.category + '"]');
		if(empty(num)) {
			$badge.html('');
		} else {
			$badge.html(num);
		}

		if(offlineData && updateTotalCounter) {
			offlineData.updateBadgeCounter(); // not the best option but needed
		}

	}

	async countItems() {
		return count(await this.getItems())
	}


	static hasSuccess(data) {
		let success = false;
		success = success || data?.success === true;
		success = success || data?.real_success === true;
		success = success || data?.items?.[0]?.success === true;
		success = success || data?.items?.[0]?.real_success === true;

		return success;
	}

	static storageName(category) {
		return 'DataSync_' + category;
	}

	satinizeInput(data) {
		if(typeof data === 'object') {
			delete(data?.app_user_id);
			delete(data?.api_token);
		}
		if(typeof this.options.satinizeInput === 'function') {
			data = this.options.satinizeInput(data);
		}
		
		return data;
	}


}

class DataSyncStorage {
	storageName;
	/** @type Promise */
	loaded;
	#_data = {};

	constructor(storageName) {
		this.storageName = storageName;
		this.init();
	}

	async init() {
		const $this = this;
		this.loaded = new Promise(async function (resolve, reject) {
			let data = localStorage.getItem($this.storageName);
			if(data == '' || data == null || data == 'null' || typeof data == 'undefined') {
				data = '{"items":{}}';
			}
			if(typeof data === 'string') {
				data = JSON.parse(data);
			}
			if(data) {
				$this.#_data = data;
			}
			resolve(true);
		});
		return this.loaded;
	}

	async save() {
		const $this = this;
		let data = {
			...{},
			...$this.#_data,
			...{
				'@lastupdate': date('Y-m-d H:i:s'),
			}
		};
		const json = JSON.stringify(data);
		if(json) {
			localStorage.setItem($this.storageName, json);
		} else {
			return false;
		}

		return this;
	}

	data() {
		return this.#_data;
	}


}

/*class DataSyncItemHandler {
	constructor(request, fallback) {

	}

	callback() {

	}

	offline(data) {
		return {
			syncSuccess: false,
			json: data
		};
	}
}*/

/*
const catSurvey = new DataSyncCategory('surveys', (function() {
	const url = new URL(appURL() + 'loader/surveyComplete.php', appURL());
	return url.toString();
})());
ds.registerCategory(catSurvey);
*/


async function DataSyncTest() {
	let data = {
		foo: 'bar',
		cat: 'miau',
		dog: 'woff',
		random: unique_id(6)
	};

	let save = await offlineData.addItem('test', data);
	console.log(save.category.storage, localStorage.getItem('DataSync_test'));
	return save.category.syncItem(save.item, function (data) {
		return {
			success: true,
			online: false,
			data: data
		}
	}).then(result => {
		console.log('yayy sync success', result.json);
	});

}

