import {
	CacheStorageModule,
	ICacheVia,
	PreCacheModule,
	deserializeDataTool,
	serializeDataTool,
} from '../../'
import { contractDbVersion } from '../../../version-contract-db.json'

const EXPIRATION_FOR_STRATEGY = 3600 * 24 * 100 // 100 days

// structure of cache
export const cachePlusUpsert = async (
	preCacheId: string,
	key: string,
	data: string,
	expiration?: number,
): Promise<void> => {
	PreCacheModule.getSlots(preCacheId).add(key, data)
	try {
		await CacheStorageModule.addData(key, data, expiration)
	} catch (err) {
		await CacheStorageModule.updateData(key, data, expiration)
	}
}

export const cachePlusRemove = async (key: string) => {
	await CacheStorageModule.removeData(key)
}

export const cachePlusRetrieve = async <T>(
	preCacheId: string,
	key: string,
): Promise<string | undefined> => {
	// console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
	// console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
	// console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
	// console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
	const preCached = PreCacheModule.getSlots(preCacheId).get<string>(key)
	// console.log('preCached:')
	// console.log(preCached)
	if (preCached !== undefined) {
		return preCached
	}
	const data = await CacheStorageModule.getData(key)
	// console.log('data:')
	// console.log(data)
	return data as string | undefined
	// return (await CacheStorageModule.removeData(key)) as unknown as string
}

const getCacheKeyFromParameters = (parameters: any[]): string => {
	const serialized = serializeDataTool([contractDbVersion, ...parameters])
	return serialized
}

const cacheIn = <U>(
	cachedFunctionName: string,
	argsToIndex: any[],
): ((cache: U) => Promise<void>) => {
	return async (cache: U): Promise<void> => {
		const cacheKey = getCacheKeyFromParameters([
			cachedFunctionName,
			argsToIndex,
		])
		// console.log('cache:')
		// console.log(cache)
		const cacheValue = serializeDataTool(cache)
		// console.log('cacheValue:')
		// console.log(cacheValue)
		await cachePlusUpsert(
			'strategyChain',
			cacheKey,
			cacheValue,
			EXPIRATION_FOR_STRATEGY,
		)
	}
}

const cacheOut = <U>(
	cachedFunctionName: string,
	argsToIndex: any[],
): (() => Promise<U | undefined>) => {
	return async (): Promise<U | undefined> => {
		// console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
		// console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
		// console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
		// console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
		const cacheKey = getCacheKeyFromParameters([
			cachedFunctionName,
			argsToIndex,
		])
		// console.log('cacheKey:')
		// console.log(cacheKey)
		const cacheValueStr = await cachePlusRetrieve('strategyChain', cacheKey)
		// console.log('cacheValueStr:')
		// console.log(cacheValueStr)
		if (cacheValueStr === undefined) {
			return undefined
		}
		const cacheValue = deserializeDataTool(cacheValueStr) as U
		return cacheValue
	}
}

// A => Arguments
// U => Cache or Result
const cacheVia = <U>(
	cachedFunctionName: string,
	argsToIndex: any[],
): ICacheVia<U> => {
	return {
		in: cacheIn<U>(cachedFunctionName, argsToIndex),
		out: cacheOut<U>(cachedFunctionName, argsToIndex),
	}
}

const recover = async <T>(via: ICacheVia<T>): Promise<T | undefined> => {
	return via.out()
}

// R => Result
// C => Cache
// U => Cache or Result
const resultAfterStore = async <R, C, U extends R>(
	via: ICacheVia<U>,
	resultCache:
		| R
		| {
				result: R
				cache: C
		  },
	prevCached: any,
): Promise<R> => {
	// console.log('---------------TO CACHE----------------')
	// console.log('resultCache:')
	// console.log((resultCache as unknown as any).cache.cachedStrategy)
	// throw new Error('check')
	const resultAsCache = resultCache as {
		result: R
		cache: U
	}
	if (
		resultAsCache.result !== undefined &&
		resultAsCache.cache !== undefined
	) {
		// console.log('resultAsCache.cache:')
		// console.log(resultAsCache.cache)
		const toGoCache = resultAsCache.cache
		if (serializeDataTool(toGoCache) !== serializeDataTool(prevCached)) {
			await via.in(toGoCache)
		}
		return resultAsCache.result as R
	} else {
		const resultAsResult = resultCache as R
		const toGoCache = resultAsResult as U
		if (serializeDataTool(toGoCache) !== serializeDataTool(prevCached)) {
			await via.in(toGoCache)
		}
		return resultAsResult as R
	}
}

// R => Result
// C => Cache
// U => Cache or Result
const resultAfterStoreWithCache = async <R, C>(
	via: ICacheVia<C>,
	resultCache: {
		result: R
		cache: C
	},
	prevCached: any,
): Promise<{
	result: R
	cache: C
}> => {
	// console.log('---------------TO CACHE----------------')
	// console.log('resultCache.cache:')
	// console.log(resultCache.cache)
	const toGoCache = resultCache.cache
	if (serializeDataTool(toGoCache) !== serializeDataTool(prevCached)) {
		await via.in(toGoCache)
	}
	return resultCache
}

// R => Result
// C => Cache
// A => Arguments
const dynamicCached = async <R, C, A extends any[], U extends R>(
	functionCachedIdentifier: string,
	functionToBeCalled: (...args: A[number][any]) => Promise<
		| R
		| {
				result: R
				cache: C
		  }
	>,
	argumentsForGettingData: A,
	argsToIndex: any[],
): Promise<R> => {
	const via = cacheVia<U>(functionCachedIdentifier, argsToIndex)
	const cached = await recover(via)
	// console.log('---------------FROM CACHE----------------')
	// console.log('cached:')
	// console.log(cached)
	// if (cached !== undefined) {
	// 	throw new Error('check')
	// }
	const argumentsForGettingDataPlusCache = [
		...argumentsForGettingData,
		cached,
	]
	const resultCache = await functionToBeCalled(
		...argumentsForGettingDataPlusCache,
	)
	return resultAfterStore<R, C, U>(via, resultCache, cached)
}

// R => Result
// C => Cache
// A => Arguments
const dynamicCachedWithCache = async <R, C, A extends any[]>(
	functionCachedIdentifier: string,
	functionToBeCalled: (...args: A[number][any]) => Promise<{
		result: R
		cache: C
	}>,
	argumentsForGettingData: A,
	argsToIndex: any[],
): Promise<{
	result: R
	cache: C
}> => {
	const via = cacheVia<C>(functionCachedIdentifier, argsToIndex)
	const cached = await recover(via)
	// console.log('---------------FROM CACHE----------------')
	// console.log('cached:')
	// console.log(cached)
	// if (cached !== undefined) {
	// 	throw new Error('check')
	// }
	const argumentsForGettingDataPlusCache = [
		...argumentsForGettingData,
		cached,
	]
	const resultCache = await functionToBeCalled(
		...argumentsForGettingDataPlusCache,
	)
	return resultAfterStoreWithCache<R, C>(via, resultCache, cached)
}

/*
const STRIP_COMMENTS =
	/(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/gm
const ARGUMENT_NAMES = /([^\s,]+)/g
type FunctionType = (...args: any[]) => Promise<unknown>
function getParamNames<F extends FunctionType>(func: F): string[] {
	const fnStr = func.toString().replace(STRIP_COMMENTS, '')
	let result: string[] | null = fnStr
		.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')'))
		.match(ARGUMENT_NAMES)
	if (result === null) result = [] as string[]
	return result as string[]
}
*/

export const dynamicCachedFunction = <R, C, A extends any[]>(
	functionToBeCached: (...args: A[number][any]) => Promise<
		| R
		| {
				result: R
				cache: C
		  }
	>,
	argsToIndex: any[],
): ((...args: A[number][any]) => Promise<R>) => {
	const fName = functionToBeCached.name
	return async (...args: A[number][any]): Promise<R> => {
		return dynamicCached(fName, functionToBeCached, args, argsToIndex)
	}
}

export const dynamicCachedFunctionWithCache = <R, C, A extends any[]>(
	functionToBeCached: (...args: A[number][any]) => Promise<{
		result: R
		cache: C
	}>,
	argsToIndex: any[],
): ((...args: A[number][any]) => Promise<{
	result: R
	cache: C
}>) => {
	const fName = functionToBeCached.name
	return async (
		...args: A[number][any]
	): Promise<{
		result: R
		cache: C
	}> => {
		return dynamicCachedWithCache(
			fName,
			functionToBeCached,
			args,
			argsToIndex,
		)
	}
}
