"use strict"

globalThis.STORE = {local: {}, sync: {}}

class StorageInternal {
	constructor(type) {
		this._type = type
	}
	get(...keys) {
		return getObject(globalThis.STORE[this._type] || {}, ...keys)
	}
	gets(keyString) {
		return getObject(globalThis.STORE[this._type] || {}, ...keyString.split('.'))
	}
	set(...keysValue) {
		globalThis.STORE[this._type] = Object.assignDeep(globalThis.STORE[this._type], setObject(...keysValue))
		globalThis.STORE[this._type]._ = Date.now()
		if (arguments[0] !== 'stats') {
			this._log = true
		}
	}
	remove(...keys) {
		removeObject(globalThis.STORE[this._type] || {}, ...keys)
		globalThis.STORE[this._type]._ = Date.now()
	}
	updated() {
		globalThis.STORE[this._type]._ = Date.now()
	}
	setAll(store) {
		globalThis.STORE[this._type] = store
		this._saved = store._ || Date.now()
	}
	getAll() {
		if (this._saved < globalThis.STORE[this._type]._) {
			this._saved = globalThis.STORE[this._type]._
			return globalThis.STORE[this._type]
		}
		return null
	}
	send(win, key) {
		win.STORE[this._type][key] = globalThis.STORE[this._type][key]
	}
	static connect(win) {
		globalThis.STORE = (win && win.STORE) || globalThis.STORE
	}
	
	async getDirect(key) {
		return browser.storage[this._type].get(key)
	}
	setDirect(key) {
		browser.storage[this._type].set(key)
	}
	removeDirect(key) {
		browser.storage[this._type].remove(key)
	}
	onChanged(cb) {
		browser.storage.onChanged.addListener(cb)
	}

	async load() {
		let store
		try {
			store = await this.getDirect()
		} catch(ex) {
			log.error(`Error occured loading ${this._type} data from browser storage: ${ex.message}`)
			return
		}

		if (this._type === 'sync') {
			for (const entry of ['TRADINGVIEW', 'permissions']) {
				if (store[entry]) {
					delete store[entry]
					this.removeDirect(entry)
				}
			}
			if (store.alerts) {
				for (const alert of store.alerts) {
					store["alert_"+alert.id] = alert
				}
				store.alerts = 0
			}

			bg.GUIDS = {}
			const month = Date.now() - 31* d2ms
			for (const key in store) {
				if (isGUID(key)) {
					if (store[key].active > month) {
						bg.GUIDS[key] = store[key]
					}
					delete store[key]
				}
			}
			delete store.savedBy
			delete store.savedAt
			delete store.readBy
		} else {
			store.GUID = store.GUID || uniqueID()
			store.perms = store.perms || {}
			store.TRADINGVIEW = store.TRADINGVIEW || {}
			delete store.TRADINGVIEW.EVENT_ID
			bg.UUID = simpleHash(bg.UUIDD = store.TRADINGVIEW.USERNAME)
			store.lastAlert = store.lastAlert || {}
			store.lastPulse = store.lastPulse || {}
			store.balances = store.balances || {}
			store.idcache = store.idcache || {}
			store.tickers = store.tickers || {}
		}
		this.setAll(store)
		log.info((this._type == 'sync' ? "Synced" : "Local") +" data loaded!")
	}
	
	async save() {
		const store = this.getAll()
		if (store) {
			if (this._log) {
				log.info(`Saving ${this._type == 'sync' ? 'synced' : 'local'} data...`)
				this._log = false
			}
			try {
				this.setDirect(store)
				this._type === 'sync' && this.setDirect({savedBy: bg.GUID, savedAt: Date.now()})
			} catch(ex) {
				log.error(`Error occured saving ${this._type} data to browser storage: ${ex.message}`)
			}
		}
	}
}

const syncStore = new StorageInternal('sync')
const localStore = new StorageInternal('local')

function verify_checksum(salt, bacon) {
	let pepper = globalThis.spice = new Date(1970, 0, 1)
	let sauce = new Date()

	try {
		pepper.setTime(salt * 3600000)
		pepper.setTime(pepper.getTime() + (932614 - bacon) * 60000)
		return pepper > sauce || bacon === 0 || bacon === null
	} catch(ex) {}
}

class Permissions {
	constructor(own) {
		this._own = Permissions.normalize(own)
	}
	static normalize(perm) {
		if (perm && perm.get) {
			return perm.get()
		}
		perm = Array.isArray(perm) ? {permissions: perm} : (perm || {})
		perm.origins = perm.origins || []
		perm.permissions = perm.permissions || []
		return perm
	}
	get() {
		return this._own
	}
	set(other) {
		other = Permissions.normalize(other)
		this._own.origins = other.origins
		this._own.permissions = other.permissions
		return this._own
	}
	origins() {
		return this._own.origins.join(', ')
	}
	length() {
		return this._own.origins.length + this._own.permissions.length
	}
	grant(other, dropOrigins) {
		other = Permissions.normalize(other)
		!dropOrigins && this._own.origins.push(...other.origins.without(this._own.origins))
		this._own.permissions.push(...other.permissions.without(this._own.permissions))
		return this
	}
	hasAll(other) {
		other = Permissions.normalize(other)
		return !other.origins.without(this._own.origins).length && !other.permissions.without(this._own.permissions).length
	}
	hasAny(other) {
		other = Permissions.normalize(other)
		return this._own.origins.includesAny(other.origins) || this._own.permissions.includesAny(other.permissions)
	}
	revoke(other) {
		other = Permissions.normalize(other)
		this._own.origins = this._own.origins.without(other.origins)
		this._own.permissions = this._own.permissions.without(other.permissions)
		return this
	}
	static defaultFilter(perm) {
		return !perm.match(/\d/)
	}
	filter(check = Permissions.defaultFilter) {
		this._own.permissions = this._own.permissions.filter(check)
		return this
	}
	count(check = Permissions.defaultFilter) {
		return this._own.permissions.length - this._own.permissions.filter(check).length
	}
	save(clearFirst) {
		clearFirst && localStore.remove('perms')
		localStore.set('perms', this._own)
		if (bg !== window) {
			Permissions.send(bg)
			localStore.send(bg, 'perms')
		}
		return this
	}
	static send(win) {
		win.perms = globalThis.perms
	}
	static connect(win) {
		globalThis.perms = (win && win.perms) || globalThis.perms
	}
}

globalThis.perms = new Permissions()

class Packages {
	static #pkg = {
		MODULES: {
			permissions: [
				"e3fdd77de3a682bfcfd8dc3000a25a5b",
				"x28vizh6hn8i8usnc22atdw12u57g435",
				"h24dwc0qi188he96xduakfc0kr4jqjaf",
				"d8zwmh92wfj8ge3yzgyxbttf5zhaa3zh",
				"HYvYqTSNBB7AEVfLG7csDUHnLmBnzfe7",
				"t7vu9cbx85sezba9zct9dgmb4yrqw5xf",
			],
		},
		ADDONS: {
			permissions: [
				"c9wrao5iy3d8ebh2lfcgxq184w6zfxrq",
				"0g5t2xydnwdm90xj5nkup2cqr8rwrji9",
				"3zmru2ftaql6wbxefsymng4njx4803mv",
				"7ayhnl3nda5wtvu86cn38nujpsn5ub5h",
				"dbvs8tvdbaqppmtu27g94hfs4m2rj8i1",
				"ckdj0m7v4l5d364jraugs5evoo0b3mju",
			],
		},
		NONE: {
			permissions: [],
		},
	}

	static get(name) {
		if (!name || !(name = _(name))) {
			throw new SyntaxError("Package Name is required!")
		}
		if (!Packages.#pkg[name]) {
			throw new ReferenceError("Invalid Package Name provided!")
		}
		return Packages.#pkg[name]
	}
}

class VarStore {
	static prefix = 'var.'
	constructor() {
		addRelayHandler(this)
	}
	normalize(name, keepCase) {
		if (name[0] === '$') name = name.slice(1)
		return keepCase ? name : name.toLowerCase()
	}
	set(key, value, ev = {}, cmd = {}) {
		if (!key) return
		key = this.normalize(key)

		const meta = localStore.get('vars', key) || {}
		if (meta.value === value) return
		meta.value = value
		meta[meta.created ? 'updated' : 'created'] = {_: Date.now(), id: ev.id||'', name: ev.name||'', alert: ev.alert||0, line: cmd._line||0}
		localStore.set('vars', key, meta)
		this.updated()
	}
	get(key) {
		if (!key) return
		key = this.normalize(key)
		const meta = localStore.get('vars', key)
		return meta && meta.value
	}
	getNewName() {
		let key, i = 0
		while (localStore.get('vars', (key = `new${i||''}`))) ++i
		return key
	}
	remove(keys) {
		if (!keys || !keys.length) return
		for (let key of keys) {
			key = this.normalize(key, true)
			localStore.remove('vars', key)
			localStore.remove('vars', key.toLowerCase())
		}
		this.updated(true)
	}
	rename(keyOld, keyNew) {
		keyOld = this.normalize(keyOld), keyNew = this.normalize(keyNew)
		if (keyNew === keyOld) return
		const meta = localStore.get('vars', keyOld)
		if (meta) {
			localStore.remove('vars', keyOld)
			localStore.set('vars', keyNew, meta)
		}
		//this.updated(true)
	}
	setAll(vars) {
		localStore.set('vars', vars)
		this.updated(true)
	}
	getAll() {
		return localStore.get('vars')
	}
	updated(refresh = false) {
		localStore.set('vars', '_', refresh ? -Date.now() : Date.now())
	}
	getUpdated() {
		const ts = localStore.get('vars', '_')
		const updated = Math.abs(ts)
		const refresh = ts < 0
		if (refresh) localStore.set('vars', '_', updated)
		return {updated, refresh}
	}
	ohlc(key, data, ohlc, noCase) {
		if (!key) return
		let value
		if (key[0] === '$') {
			value = this.get(key)
		} else {
			value = noCase ? data && data.getProp(key) : data && data.hasOwnProperty(key) ? data[key] : ohlc && ohlc[key.toLowerCase()]
			// $$$$$ if missing '_' (custom/local prefix) try w '_' (local) first ???
			if (value === null || value === undefined) {
				value = this.get(key[0] === '_' ? key.slice(1) : key)
			}
		}
		return value === null ? undefined : value
	}
}

class OptStore {
	static prefix = 'opt.'
	constructor() {
		addRelayHandler(this)
	}
	getAll(ts) {
		if (ts && opt?._ <= ts) return null
		return opt
	}
	setAll(newOpt) {
		Object.assign(opt, newOpt)
		this.save()
	}
	save() {
		opt._ = Date.now()
		localStore.set('options', Object.without(opt, ['crc', 'sum']))
		updateIcon()
	}
	load() {
		const stored = localStore.get('options')
		if (stored) {
			Object.assign(opt, stored)
		} else {
			this.migrate()
		}
	}
	migrate() {
		try {
			if (localStorage.opt !== undefined) {
				const legacy = JSON.parse(localStorage.opt)
				Object.assign(opt, legacy)
				this.save()
				delete localStorage.opt, delete localStorage.opt_
			}
		} catch(ex) {}
	}
}

class OptRelay {
	async load(useFallback) {
		const ts = opt?._ || 0
		const newOpt = await relayMsg('opt.getAll', ts)
		if (newOpt) {
			opt = newOpt
		} else if (useFallback) {
			opt = (await localStore.getDirect('options'))?.options || {}
		}
		return opt?._ > ts
	}
	save() {
		opt && relayMsg('opt.setAll', opt)
	}
	saveDirect() {
		opt && localStore.setDirect({options: opt})
	}
}

class StatsStore {
	static prefix = 'stats.'
	constructor() {
		addRelayHandler(this)
	}
	get(key) {
		return localStore.get('stats', key)
	}
	getAll() {
		return localStore.get('stats') || {}
	}
	set(key, value) {
		localStore.set('stats', key, value)
		return true
	}
	delete(key) {
		localStore.remove('stats', key)
		return true
	}
	inc(name, add = 1, last, msg) {
		this.set('session'+name, (this.get('session'+name) || 0) + add)
		this.set('total'+name, (this.get('total'+name) || 0) + add)
		last && this.set('last'+name, Date.now())
		msg && this.set(`last${name}Msg`, msg)
	}
	inc1(name, add = 1) {
		this.set(name, (this.get(name) || 0) + add)
	}
	incAlt(name, add = 1) {
		const lc = lcfirst(name)
		this.set(lc, (this.get(lc) || 0) + add)
		this.set('total'+name, (this.get('total'+name) || 0) + add)
		this.set(lc+'Last', Date.now())
	}
	minMax(name, value, last) {
		this.set('session'+name+'Min', Math.min((this.get('session'+name+'Min') || Number.MAX_VALUE), value))
		this.set('session'+name+'Max', Math.max((this.get('session'+name+'Max') || 0), value))
		this.set('session'+name+'Avg', ((this.get('session'+name+'Avg') || value) + value) / 2)
		this.set('total'+name+'Min', Math.min((this.get('total'+name+'Min') || Number.MAX_VALUE), value))
		this.set('total'+name+'Max', Math.max((this.get('total'+name+'Max') || 0), value))
		this.set('total'+name+'Avg', ((this.get('total'+name+'Avg') || value) + value) / 2)
		last && this.set('last'+name, value)
	}
	migrate() {
		if (localStore.get('stats')) return
		const stats = {}
		const vars = ['badgeRcv', 'badgeRun', 'badgeErr', 'badgeEvt', 'badgeEvtLast', 'lastError', 'lastSuccess', 'authToken']
		try {
			for (const key in localStorage) {
				if (key.startsWith('stat') || vars.includes(key)) {
					const newKey = vars.includes(key) ? key : key[4].toLowerCase() + key.slice(5)
					const val = localStorage[key]
					stats[newKey] = key === 'lastError' || key === 'lastSuccess' ? safeJSON(val) : isNaN(val) ? val : Number(val)
					delete localStorage[key]
				}
			}
		} catch(ex) {}
		if (!Object.isEmpty(stats)) {
			localStore.set('stats', stats)
		}
	}
	rollover() {
		this.migrate()
		this.delete('prevHeart')
		Object.each(this.getAll(), key => key.startsWith('session') && this.delete(key))

		;['bootup', 'heartbeat', 'emailLast', 'discordLast', 'telegramLast', 'twilioLast', 'iFTTTLast']
			.forEach(key => { this.set('prev'+ucfirst(key), this.get(key) || this.get('prev'+ucfirst(key)) || 0); this.delete('prev'+key) })
		;['Cmd', 'Ping', 'Event', 'Sync', 'Timeout', 'Stall', 'Rest', 'Down', 'StreamToken', 'StreamStall', 'StreamRest', 'StreamDown', 'TelebotStart', 'TelebotStop', 'TelebotMsg', 'TelebotErr']
			.forEach(key => this.set('prev'+key, this.get('last'+key) || this.get('prev'+key) || 0))
		;['Cmd', 'Timeout', 'Stall', 'Rest', 'Down', 'StreamToken', 'StreamStall', 'StreamRest', 'StreamDown', 'TelebotStart', 'TelebotStop', 'TelebotMsg', 'TelebotErr']
			.forEach(key => this.delete('last'+key))
		;['emailLast', 'discordLast', 'telegramLast', 'twilioLast', 'iFTTTLast', 'email', 'emailAuth', 'emailErr', 'discord', 'discordErr', 'telegram', 'telegramErr', 'twilio', 'twilioErr', 'iFTTT', 'iFTTTErr']
			.forEach(key => { this.delete(key); this.delete(ucfirst(key)) })

		this.set('downtimeLast', this.get('downtime') || 0)
		this.set('downtime', Math.max((Date.now() - (this.get('heartbeat') || Date.now())) / 1000, 0))
		if (this.get('downtime')) {
			this.set('downtimeTotal', (this.get('downtimeTotal') || 0) + this.get('downtime'))
			this.set('downtimeAvg', ((this.get('downtimeAvg') || this.get('downtimeLast') || this.get('downtime')) + this.get('downtime')) / 2)
		}
		this.set('downtimeMax', Math.max(this.get('downtimeMax') || 0, this.get('downtime') || 0))
		this.set('downtimeMin', Math.min(Math.max(this.get('downtimeMin'), 0) || Number.MAX_VALUE, this.get('downtime') || Number.MAX_VALUE))
		
		this.set('uptimeLast', this.get('uptime') || (Date.now() - (this.get('bootup') || Date.now())) / 1000)
		if (this.get('uptimeLast')) {
			this.set('uptimeTotal', (this.get('uptimeTotal') || 0) + this.get('uptimeLast'))
			this.set('uptimeAvg', ((this.get('uptimeAvg') || this.get('uptimeLast')) + this.get('uptimeLast')) / 2)
		}
		this.set('uptimeMax', Math.max(this.get('uptimeMax') || 0, this.get('uptime') || 0))
		this.set('uptimeMin', Math.min(this.get('uptimeMin') || Number.MAX_VALUE, this.get('uptime') || Number.MAX_VALUE))
		
		this.set('uptime', 0)
		this.set('bootup', Date.now())
		this.set('bootups', (this.get('bootups') || 0) + 1)
		this.set('bootupFirst', this.get('bootupFirst') || this.get('bootup'))
		this.set('instances', Object.keys(bg.GUIDS).length)
	}
	resetBadge() {
		//stats.badgeEvt && (stats.badgeEvtLast = stats.badgeEvt)
		;['badgeRcv', 'badgeRun', 'badgeErr'/*, 'badgeEvt'*/].forEach(key => this.delete(key))
	}
	resetTotals() {
		Object.each(this.getAll(), key => key.startsWith('total') && this.delete(key))
		;['bootups', 'restarts', 'autoRestarts', 'uptimeTotal', 'uptimeMin', 'uptimeMax', 'uptimeAvg', 'downtimeTotal', 'downtimeMin', 'downtimeMax', 'downtimeAvg'].forEach(key => this.delete(key))
	}
}

class StatsRelay {
	constructor() {
		this._cache = {}
	}
	async load() {
		this._cache = (await relayMsg('stats.getAll')) || this._cache
		return this._cache
	}
	get(key) {
		return this._cache[key]
	}
	getAll() {
		return this._cache
	}
	set(key, value) {
		relayMsg('stats.set', key, value)
		this._cache[key] = value
		return true
	}
	delete(key) {
		relayMsg('stats.delete', key)
		delete this._cache[key]
		return true
	}
	incAlt(name, add = 1) {
		relayMsg('stats.incAlt', name, add)
	}
	resetBadge() {
		relayMsg('stats.resetBadge')
		// $$$ reload?!
	}
	resetTotals() {
		relayMsg('stats.resetTotals')
		// $$$ reload?!
	}
}
