import * as CryptoJS from 'crypto-js'
import { ethers } from 'ethers'
import { deterministicStringify } from '../'
import { CONTRACT_CONSTANTS, CONTRACT_INTERFACES } from '../../'

const MAXIMUM_TIMESTAMP_DISTANCE = 400 // 200 ms
const MAXIMUM_PRICE_DISTANCE = 0.04 // 4%

const splitSignature = (
	signature: string,
): {
	r: string
	s: string
	v: number
} => {
	const r = signature.slice(0, 66)
	const s = '0x' + signature.slice(66, 130)
	const v = parseInt(signature.slice(130, 132), 16)
	return { r, s, v }
}

/*
 * Uses crypto-js to decrypt the price part
 * response is the encrypted price part as string
 * key is the key part as string
 * cryptography used function PBKDF2 with SHA-256
 * response is concatenation of, in order: salt, iv and ciphertext.
 */
const decryptPricePart = (
	response: string,
	password: string,
): CONTRACT_INTERFACES.IPriceOrErrorOrSkip => {
	const encrypted = CryptoJS.enc.Base64.parse(response)
	const salt_len = 16
	const iv_len = 16
	const salt = CryptoJS.lib.WordArray.create(
		encrypted.words.slice(0, salt_len / 4),
	)
	const iv = CryptoJS.lib.WordArray.create(
		encrypted.words.slice(0 + salt_len / 4, (salt_len + iv_len) / 4),
	)
	const key = CryptoJS.PBKDF2(password, salt, {
		keySize: 256 / 32,
		iterations: 10000,
		hasher: CryptoJS.algo.SHA256,
	})
	const decrypted = CryptoJS.AES.decrypt(
		CryptoJS.lib.WordArray.create(
			encrypted.words.slice((salt_len + iv_len) / 4),
		).toString(CryptoJS.enc.Base64),
		key,
		{ iv: iv },
	)
	return JSON.parse(
		decrypted.toString(CryptoJS.enc.Utf8),
	) as unknown as CONTRACT_INTERFACES.IPriceOrErrorOrSkip
}

const testForInvalidNoFetchNodes = (
	priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[],
	keys: [string, string][],
) => {
	for (const pricePartIndex in priceParts) {
		const pricePart = priceParts[pricePartIndex]
		// validate the skipped parts
		if ((pricePart as CONTRACT_INTERFACES.ISkipPrice).noFetch === true) {
			const skipped = pricePart as CONTRACT_INTERFACES.ISkipPrice
			if (skipped.ckWd === undefined) {
				throw new Error(
					'TradeIdeaPricingValidationError: noFetch ckWd undefined',
				)
			} else {
				if (skipped.ckWd !== keys[pricePartIndex][0]) {
					throw new Error(
						'TradeIdeaPricingValidationError: noFetch ckWd does not match',
					)
				}
			}
		}
	}
}

const testForDealtErrors = (
	priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[],
): undefined | CONTRACT_INTERFACES.IPriceError => {
	let listOfKnownErrors: CONTRACT_INTERFACES.IKnownFetchPriceError[] = []
	let atLeastOneUnknownError: string | undefined = undefined
	for (const pricePartIndex in priceParts) {
		const pricePart = priceParts[pricePartIndex]
		// validate the skipped parts
		if (
			(pricePart as CONTRACT_INTERFACES.IPriceError).fetchError === true
		) {
			const err = pricePart as CONTRACT_INTERFACES.IPriceError
			if (err.unknownError !== undefined) {
				atLeastOneUnknownError = err.unknownError
			} else {
				if (err.knownError === undefined) {
					throw new Error(
						'TradeIdeaPricingValidationError: fetchError knownError undefined when not expected',
					)
				}
				if (
					listOfKnownErrors.find((one) => one === err.knownError!) ===
					undefined
				) {
					listOfKnownErrors.push(err.knownError!)
				}
			}
		}
	}
	if (atLeastOneUnknownError !== undefined) {
		const pricingError: CONTRACT_INTERFACES.IPriceError = {
			fetchError: true,
			unknownError: atLeastOneUnknownError,
		}
		return pricingError
	} else {
		if (listOfKnownErrors.length === 0) {
			return undefined
		} else if (listOfKnownErrors.length === 1) {
			const pricingError: CONTRACT_INTERFACES.IPriceError = {
				fetchError: true,
				knownError: listOfKnownErrors[0],
			}
			return pricingError
		} else {
			const pricingError: CONTRACT_INTERFACES.IPriceError = {
				fetchError: true,
				unknownError: listOfKnownErrors.join(', '),
			}
			return pricingError
		}
	}
}

const restorePricePartsFromKeys = (
	responses: any[],
	keys: [string, string][],
): CONTRACT_INTERFACES.IPriceOrErrorOrSkip[] => {
	const priceParts: any[] = []
	for (let i = 0; i < responses.length; i++) {
		const response = responses[i]
		const key = keys[i]
		const pricePart = decryptPricePart(response, key[1])
		// log.dev('pricePart')
		// log.dev(pricePart)
		priceParts.push(pricePart)
	}
	return priceParts
}

const getValidPriceFromPartsAndKeys = (
	priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[],
	keys: [string, string][],
): CONTRACT_INTERFACES.IPrice | CONTRACT_INTERFACES.IPriceError => {
	// console.log('priceParts')
	// console.log(priceParts)
	// console.log('keys')
	// console.log(keys)

	testForInvalidNoFetchNodes(priceParts, keys)

	const pricingError = testForDealtErrors(priceParts)
	// console.log('pricingError')
	// console.log(pricingError)
	if (pricingError !== undefined) {
		return pricingError
	}

	let provider: CONTRACT_INTERFACES.IPricingProvider | undefined // required
	let symbol: string | undefined // required
	let company: string | undefined
	const globalPrices: number[] = [] // required
	let globalPrice: number = 0 // required
	let asks: number[] | undefined = undefined
	let ask: number | undefined = undefined
	let bids: number[] | undefined = undefined
	let bid: number | undefined = undefined
	const timestamps: number[] = [] // required
	let timestamp: number = 0 // required
	let fromProxy: string | undefined = undefined
	for (const pricePartIndex in priceParts) {
		const pricePart = priceParts[pricePartIndex]
		if (!((pricePart as CONTRACT_INTERFACES.ISkipPrice).noFetch === true)) {
			const _price = pricePart as CONTRACT_INTERFACES.IPrice
			// validate provider
			if (_price.provider === undefined) {
				throw new Error(
					'TradeIdeaPricingValidationError: provider undefined',
				)
			}
			if (typeof _price.provider !== 'string') {
				throw new Error(
					'TradeIdeaPricingValidationError: provider not string',
				)
			}
			if (provider === undefined) {
				provider = _price.provider
			} else {
				if (provider !== _price.provider) {
					throw new Error(
						'TradeIdeaPricingValidationError: provider does not match',
					)
				}
			}
			// validate symbol
			if (_price.symbol === undefined) {
				throw new Error(
					'TradeIdeaPricingValidationError: symbol undefined',
				)
			}
			if (typeof _price.symbol !== 'string') {
				throw new Error(
					'TradeIdeaPricingValidationError: symbol not string',
				)
			}
			if (symbol === undefined) {
				symbol = _price.symbol
			} else {
				if (symbol !== _price.symbol) {
					throw new Error(
						'TradeIdeaPricingValidationError: symbol does not match',
					)
				}
			}
			// validate company
			if (_price.company !== undefined) {
				if (typeof _price.company !== 'string') {
					throw new Error(
						'TradeIdeaPricingValidationError: company not string',
					)
				}
				if (company === undefined) {
					company = _price.company
				} else {
					if (company !== _price.company) {
						throw new Error(
							'TradeIdeaPricingValidationError: company does not match',
						)
					}
				}
			} else {
				if (company !== undefined) {
					throw new Error(
						'TradeIdeaPricingValidationError: company does not match',
					)
				}
			}
			// validate globalPrice
			if (_price.globalPrice === undefined) {
				throw new Error(
					'TradeIdeaPricingValidationError: globalPrice undefined',
				)
			}
			if (typeof _price.globalPrice !== 'number') {
				throw new Error(
					'TradeIdeaPricingValidationError: globalPrice not number',
				)
			}
			globalPrices.push(_price.globalPrice)
			// validate ask
			if (_price.ask !== undefined) {
				if (typeof _price.ask !== 'number') {
					throw new Error(
						'TradeIdeaPricingValidationError: ask not number',
					)
				}
				if (asks === undefined) {
					asks = []
				}
				asks.push(_price.ask)
			}
			// validate bid
			if (_price.bid !== undefined) {
				if (typeof _price.bid !== 'number') {
					throw new Error(
						'TradeIdeaPricingValidationError: bid not number',
					)
				}
				if (bids === undefined) {
					bids = []
				}
				bids.push(_price.bid)
			}
			// validate timestamp
			if (_price.timestamp === undefined) {
				throw new Error(
					'TradeIdeaPricingValidationError: timestamp undefined',
				)
			}
			if (typeof _price.timestamp !== 'number') {
				throw new Error(
					'TradeIdeaPricingValidationError: timestamp not number',
				)
			}
			timestamps.push(_price.timestamp)
			// validate origin
			if (_price.fromProxy !== undefined) {
				if (fromProxy === undefined) {
					fromProxy = _price.fromProxy
				} else {
					if (fromProxy !== _price.fromProxy) {
						throw new Error(
							'TradeIdeaPricingValidationError: fromProxy does not match',
						)
					}
				}
			}
		}
	}
	// calculate globalPrice
	// console.log('globalPrices')
	// console.log(globalPrices)
	let maxDecimalsGlobalPrice = 0
	for (const _globalPrice of globalPrices) {
		const preDecimals = _globalPrice.toString().split('.')
		const decimals = preDecimals.length > 1 ? preDecimals[1].length : 0
		if (decimals > maxDecimalsGlobalPrice) {
			maxDecimalsGlobalPrice = decimals
		}
		globalPrice += _globalPrice
	}
	globalPrice = globalPrice / globalPrices.length
	// restore globalPrice to its maximum decimals
	globalPrice = parseFloat(globalPrice.toFixed(maxDecimalsGlobalPrice))
	// console.log('globalPrice')
	// console.log(globalPrice)
	// validate that globalPrices are not too far apart in a distance of constant
	// MAXIMUM_PRICE_DISTANCE, in percentage difference from average
	for (const _globalPrice of globalPrices) {
		const difference = Math.abs(_globalPrice - globalPrice)
		const percentageDifference = difference / globalPrice
		if (percentageDifference > MAXIMUM_PRICE_DISTANCE) {
			throw new Error(
				'TradeIdeaPricingValidationError: globalPrice too far apart',
			)
		}
	}

	// calculate average timestamp
	for (const _timestamp of timestamps) {
		timestamp += _timestamp
	}
	timestamp = Math.floor(timestamp / timestamps.length)
	// validate that timestamps are not too far apart in a distance of constant
	// MAXIMUM_TIMESTAMP_DISTANCE, in milliseconds
	for (const _timestamp of timestamps) {
		if (Math.abs(_timestamp - timestamp) > MAXIMUM_TIMESTAMP_DISTANCE) {
			throw new Error(
				'TradeIdeaPricingValidationError: timestamp too far apart',
			)
		}
	}
	// if applicable, calculate average ask and bid
	if (asks !== undefined) {
		ask = 0
		let maxDecimalsAsk = 0
		for (const _ask of asks) {
			const preDecimals = _ask.toString().split('.')
			const decimals = preDecimals.length > 1 ? preDecimals[1].length : 0
			if (decimals > maxDecimalsAsk) {
				maxDecimalsAsk = decimals
			}
			ask += _ask
		}
		ask = ask / asks.length
		ask = parseFloat(ask.toFixed(maxDecimalsAsk))

		// validate that ask prices are not too far apart in a distance of constant
		// MAXIMUM_PRICE_DISTANCE, in percentage difference from average
		for (const _ask of asks) {
			const difference = Math.abs(_ask - ask)
			const percentageDifference = difference / ask
			if (percentageDifference > MAXIMUM_PRICE_DISTANCE) {
				throw new Error(
					'TradeIdeaPricingValidationError: ask too far apart',
				)
			}
		}
	}
	if (bids !== undefined) {
		bid = 0
		let maxDecimalsBid = 0
		for (const _bid of bids) {
			const preDecimals = _bid.toString().split('.')
			const decimals = preDecimals.length > 1 ? preDecimals[1].length : 0
			if (decimals > maxDecimalsBid) {
				maxDecimalsBid = decimals
			}
			bid += _bid
		}
		bid = bid / bids.length
		bid = parseFloat(bid.toFixed(maxDecimalsBid))
		// validate that bid prices are not too far apart in a distance of constant
		// MAXIMUM_PRICE_DISTANCE, in percentage difference from average
		for (const _bid of bids) {
			const difference = Math.abs(_bid - bid)
			const percentageDifference = difference / bid
			if (percentageDifference > MAXIMUM_PRICE_DISTANCE) {
				throw new Error(
					'TradeIdeaPricingValidationError: bid too far apart',
				)
			}
		}
	}
	if (provider === undefined) {
		throw new Error('TradeIdeaPricingValidationError: provider undefined')
	}
	if (symbol === undefined) {
		throw new Error('TradeIdeaPricingValidationError: symbol undefined')
	}
	if (timestamp === 0) {
		throw new Error('TradeIdeaPricingValidationError: timestamp undefined')
	}
	if (globalPrice === 0) {
		throw new Error(
			'TradeIdeaPricingValidationError: globalPrice undefined',
		)
	}

	return {
		provider: provider!,
		symbol: symbol!,
		company,
		globalPrice,
		ask,
		bid,
		fromProxy,
		timestamp,
	}
}

const hashString = (valueStr: string) => {
	return ethers.utils.keccak256(ethers.utils.toUtf8Bytes(valueStr))
}

const getHashedMessage = (
	originalMessage: string,
): CONTRACT_INTERFACES.IHashedMessage => {
	const hashedMessage = hashString(originalMessage)
	return {
		originalMessage,
		hashedMessage,
	}
}

const validateHashedSignedMessage = async (
	hashedSignedMessage: CONTRACT_INTERFACES.IHashedSignedMessage,
	expectedAddress?: string,
): Promise<string> => {
	const probeHashed = getHashedMessage(hashedSignedMessage.originalMessage)
	if (probeHashed.originalMessage !== hashedSignedMessage.originalMessage) {
		throw new Error('The hashed message does not match original message')
	}
	const signerAddr = await ethers.utils.verifyMessage(
		hashedSignedMessage.hashedMessage,
		hashedSignedMessage.signature,
	)
	if (expectedAddress !== undefined) {
		if (signerAddr !== expectedAddress) {
			throw new Error(
				`Signature address mismatch. Expected ${expectedAddress}, got ${signerAddr}`,
			)
		}
	}

	return signerAddr
}

const checkIfPriceSignedResponseIsValid = async (
	pricingDetails: CONTRACT_INTERFACES.IValidPrice,
	cachedStrategyState: CONTRACT_INTERFACES.IStrategyState,
	mockValidation: boolean = false,
): Promise<{ valid: boolean; price: CONTRACT_INTERFACES.IValidPrice }> => {
	if (mockValidation) {
		return {
			valid: true,
			price: pricingDetails,
		}
	}
	const hashedMessage = getHashedMessage(
		deterministicStringify(pricingDetails.response)!,
	)
	// log.dev('pricingDetails.signature.signature:')
	// log.dev(pricingDetails.signature.signature)
	const signature = pricingDetails.signature.signature
	const splitedSignature = splitSignature(signature)
	const hashedSignedMessage: CONTRACT_INTERFACES.IHashedSignedMessage = {
		...hashedMessage,
		signature,
		...splitedSignature,
	}

	if (pricingDetails.price === undefined) {
		return {
			valid: false,
			price: pricingDetails,
		}
	}

	let expectedAddress: undefined | string = undefined
	if (pricingDetails.price!.provider === 'Binance') {
		expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.BINANCE_V2
	} else if (pricingDetails.price!.provider === 'IEX') {
		expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.IEX_V2
	} else if (pricingDetails.price!.provider === 'IG Group') {
		expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.IG_V2
	} else if (pricingDetails.price!.provider === 'Alpaca') {
		expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.ALPACA_V2
	} else if (pricingDetails.price!.provider === 'Tradestation') {
		expectedAddress =
			CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.TRADESTATION_V2
	} else if (pricingDetails.price!.provider === 'CoinMarketCap') {
		expectedAddress =
			CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.COINMARKETCAP_V2
	} else if (pricingDetails.price!.provider === 'CryptoCompare') {
		expectedAddress =
			CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.CRYPTOCOMPARE_V2
	} else {
		throw new Error(
			'TradeIdeaPricingValidationError: Invalid pricing provider',
		)
	}

	const verification = await validateHashedSignedMessage(hashedSignedMessage)

	// console.log('verification:', verification)
	// we let off while not debugged
	if (verification !== expectedAddress) {
		return {
			valid: false,
			price: pricingDetails,
		}
	} else {
		return {
			valid: true,
			price: pricingDetails,
		}
	}
}

export const validatePricingSignature = async (
	ideaIdea: CONTRACT_INTERFACES.ITradeIdeaIdea,
	cachedStrategyState: CONTRACT_INTERFACES.IStrategyState,
	mockValidation: boolean = false,
) => {
	const pricingDetails = ideaIdea.priceInfo!
	// console.log('pricingDetails:')
	// console.log(pricingDetails)
	if (mockValidation) {
		let priceIn: CONTRACT_INTERFACES.IPrice
		try {
			priceIn = JSON.parse(pricingDetails.response[0])
		} catch (e) {
			priceIn = {
				globalPrice: 1,
				provider: 'Binance',
				symbol: 'BTCUSDT',
				timestamp: 1,
			}
		}
		return {
			valid: true,
			price: {
				...pricingDetails,
				priceParts:
					pricingDetails.response as unknown as CONTRACT_INTERFACES.IPriceOrErrorOrSkip[],
				price: priceIn,
			},
		}
	}

	if (ideaIdea.priceInfo === undefined) {
		// console.log('TradeIdeaPricingValidationError: priceInfo undefined')
		return {
			valid: false,
			price: pricingDetails,
		}
	}

	// First we restore the price parts from key parts
	let priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[] = []
	try {
		priceParts = restorePricePartsFromKeys(
			pricingDetails.response,
			pricingDetails.keys,
		)
	} catch (e) {
		// console.log('Error Validating Prices:')
		// console.log(e)
		return {
			valid: false,
			price: pricingDetails,
		}
	}
	if (
		deterministicStringify(priceParts) !==
		deterministicStringify(pricingDetails.priceParts)
	) {
		// console.log('TradeIdeaPricingValidationError: priceParts do not match')
		return {
			valid: false,
			price: pricingDetails,
		}
	}
	// Now we validate the got prices and reduce to one price
	let price: CONTRACT_INTERFACES.IPrice | CONTRACT_INTERFACES.IPriceError
	try {
		price = getValidPriceFromPartsAndKeys(
			pricingDetails.priceParts!,
			pricingDetails.keys,
		)
	} catch (e) {
		// console.log('Error Validating Prices:')
		// console.log(e)
		return {
			valid: false,
			price: pricingDetails,
		}
	}

	if ((price as CONTRACT_INTERFACES.IPriceError).fetchError === true) {
		return {
			valid: false,
			price: pricingDetails,
		}
	}

	// console.log('price:')
	// console.log(price)
	// console.log('pricingDetails.price:')
	// console.log(pricingDetails.price)
	if (
		deterministicStringify(price) !==
		deterministicStringify(pricingDetails.price)
	) {
		// console.log('TradeIdeaPricingValidationError: price do not match')
		return {
			valid: false,
			price: pricingDetails,
		}
	}

	// console.log('price')
	// console.log(price)
	// console.log('pricingDetails')
	// console.log(pricingDetails.price)
	// For last, we validate the original responseArray signature
	return checkIfPriceSignedResponseIsValid(
		pricingDetails,
		cachedStrategyState,
		mockValidation,
	)
}
