import type { Applog, IShare } from '@wovin/core'
import type { AgentStateClass } from './AgentState'
import { hkdf } from '@noble/hashes/hkdf.js'
import { sha256 } from '@noble/hashes/sha2.js'
import { utf8ToBytes } from '@noble/hashes/utils.js'
import { arrayBufferToBase64, assertCryptoKey, base64ToArray, base64ToArrayBuffer, DefaultFalse, randomBuf } from '@wovin/utils'
import { Logger } from 'besonders-logger'
import { base64pad } from 'iso-base/rfc4648'
import { untag } from 'iso-base/varint'
import { EdDSASigner } from 'iso-signatures/signers/eddsa.js'
import {
	createMnemonic,
	deriveAesViaHKDFKey,
	EdKeypairInstanceFromSecretKeyBytes,
	getECDHkeypairFromHashArray,
	getEdDSAkeypairFromHashArray,
	getHKDFkeyFromStrings,
	hashMnemonic,
} from 'mnemkey'
import { importPublicDerivationKey } from '../../ipfs/share-sync'
import { notifyToast, ToastVariant } from '../../ui/utils-ui'
import { stateDB } from '../local-state'
import { updateAgentState, useAgent } from './AgentState'

const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.DEBUG) // eslint-disable-line unused-imports/no-unused-vars

// export const configuration = {
// 	namespace: { creator: 'ztax', name: 'note3' },
// 	debug: true,
// 	fileSystem: {
// 		loadImmediately: false,
// 		//   version?: string
// 	},
// 	// permissions?: Permissions
// 	// userMessages?: UserMessages
// }

export const SymmAlg = {
	AES_CTR: 'AES-CTR',
	AES_CBC: 'AES-CBC',
	AES_GCM: 'AES-GCM',
} as const

type Base64String = string

export async function importPublicECDH64(derivationECDH: Base64String) {
	const publicECDHraw = base64ToArrayBuffer(derivationECDH)
	DEBUG({ derivationECDH, publicECDHraw })

	return await globalThis.crypto.subtle.importKey(
		'raw',
		publicECDHraw,
		{ name: 'ECDH', namedCurve: 'P-256' },
		true,
		[],
	)
}

export const decryptWithAesSharedKey = async function decryptWithAesSharedKey(
	encByteArray: Uint8Array,
	aesKey: CryptoKey,
	as: 'array' | 'parsed' | 'string' = 'array',
) {
	// const aesKey = currentPubSub.encryptedWith ?? (currentPubSub as IShare).sharedKey
	if (!aesKey) {
		throw ERROR('[decrypt] missing aesKey', { encByteArray })
		// throw new Error('[decrypt] missing info') // ERROR from logger is now throwable
	}

	// @expede advises against fixed iv https://github.com/fission-codes/keystore-idb/issues/70#issuecomment-1624377859
	// so i recreated the way @odd / keystoreIDB create random iv and join the iv unencrypted at the front of the encrypted payload
	let oddDecrypted: Uint8Array
	const dec = new TextDecoder('utf-8')

	const { crypto } = useAgent()
	VERBOSE('decryptWithAesSharedKey', { encByteArray, aesKey, crypto })
	try {
		oddDecrypted = await aesUtils.decrypt(encByteArray, aesKey, SymmAlg.AES_GCM)
	} catch (error) {
		ERROR('aes decrypt error', { error })
	}

	const oddDecoded = dec.decode(oddDecrypted)
	VERBOSE('decryptWithAesSharedKey', { oddDecrypted, oddDecoded })
	return as === 'array' ? oddDecrypted as Uint8Array : as === 'parsed' ? JSON.parse(oddDecoded) : oddDecoded
}

/**
 * @param privateKey
 * @param publicKey
 * @param exportable
 * @returns a shared AES key derived from the ECDH keys that are provided
 */
export function deriveSharedEncryptionKey(privateKey: CryptoKey, publicKey: CryptoKey, exportable = DefaultFalse) {
	if (exportable) ERROR('I hope you know what you be up to', { exportable })
	if (
		!(privateKey.type === 'private' && privateKey.algorithm.name === 'ECDH' && publicKey.type === 'public'
			&& publicKey.algorithm.name === 'ECDH')
	) {
		throw ERROR('invalid keys (should be ECDH and private/public', { privateKey, publicKey })
	}
	return globalThis.crypto.subtle.deriveKey(
		{
			name: 'ECDH',
			public: publicKey,
		},
		privateKey,
		{
			name: 'AES-GCM',
			length: 256,
		},
		exportable, // ! for testing/comparison
		['encrypt', 'decrypt'],
	)
}
export async function deriveSecretBitsFromECDH(
	{ privateKey, publicKey }: CryptoKeyPair,
	SEEDLENGTH = 32 * 8,
) {
	const personalSecret = await globalThis.crypto.subtle.deriveBits(
		{
			name: 'ECDH',
			namedCurve: 'P-256',
			public: publicKey,
		} as EcdhKeyDeriveParams,
		privateKey,
		SEEDLENGTH,
	)
	const secretAsUint8Array = new Uint8Array(personalSecret, 0, SEEDLENGTH / 16)
	DEBUG({ secretAsUint8Array })
	return secretAsUint8Array
}

/**
 * @param privateKey CryptoKey
 * @param publicKey CryptoKey
 * @param derivationCounter number
 * @param exportable boolean (default False)
 * @returns Uint8Array a seed suitable for key creation, derived from the ECDH keys that are provided
 */
export async function derivePublicationKeySeed(
	{ privateKey, publicKey }: CryptoKeyPair,
	derivationCounter: number,
	exportable = DefaultFalse,
	SEEDLENGTH = 32 * 8,
) {
	if (exportable) ERROR('I hope you know what you be up to', { exportable })
	if (
		!(privateKey.type === 'private' && privateKey.algorithm.name === 'ECDH' && publicKey.type === 'public'
			&& publicKey.algorithm.name === 'ECDH')
	) {
		throw ERROR('invalid keys (should be ECDH and private/public', { privateKey, publicKey })
	}
	const secretAsUint8Array = await deriveSecretBitsFromECDH({ privateKey, publicKey }, SEEDLENGTH)
	DEBUG('derived secret from keys', { privateKey, publicKey, secretAsUint8Array }) // in this case from agent ecdh matching public and private

	const hashedSecret = hkdf(sha256, secretAsUint8Array, utf8ToBytes(`note3-pubcounter:${derivationCounter}`), utf8ToBytes('note3-pub'), 32)
	DEBUG({ hashedSecret })
	return hashedSecret
}

// personal means only for the current agent derived using the public and private keys from the same keypair
// apparently its fine: https://crypto.stackexchange.com/a/26343/104653
export type ECDHKeyPair = CryptoKeyPair /* TODO strongly type & { privateKey:{type:'ECDH'}} */
export const getAESkeyPersonal = async function getAESkeyForPersonalEncryption(keypair?: ECDHKeyPair) {
	// TODO review when and where this is appropriate
	// subsequent derivations for each context may prove more secure
	const { privateKey, publicKey } = keypair ?? await useAgent().getDerivationKeypair()
	const aesEncryptionKey = deriveSharedEncryptionKey(privateKey, publicKey, false)
	VERBOSE({ privateKey, publicKey, aesEncryptionKey })
	return aesEncryptionKey
}

export const getAESkeyForEncryptedApplogs = async function getAESkeyForEncryptedApplogs(nonEncryptedLogs: Applog[]) {
	const agent = useAgent()
	const { ag, getDerivationKeypair, getSharedKeyApplogForCurrentAgent } = agent
	const { privateKey: priDerivationKey } = await getDerivationKeypair()
	const sharedKeyApplog = getSharedKeyApplogForCurrentAgent(nonEncryptedLogs)

	if (!sharedKeyApplog) return ERROR('no sharedKey Found for current agent', { nonEncryptedLogs, priDerivationKey, ag })

	const { vl: encryptedSharedKey, ag: publisherAg } = sharedKeyApplog as Applog

	const publicKeyForPublisher = (nonEncryptedLogs as Applog[]).find(eachLog =>
		(eachLog?.at as string) === 'agent/jwkd'
		&& (eachLog?.ag as string) === publisherAg,
	)?.vl

	const publicKeyECDHforPublisher = (nonEncryptedLogs as Applog[]).find(eachLog =>
		(eachLog?.at as string) === 'agent/ecdh'
		&& (eachLog?.ag as string) === publisherAg,
	)?.vl

	const publicKeyFromApplogs = await importPublicDerivationKey(publicKeyForPublisher as string) // TODO discuss depricate jwkd
	const derivedKeyDecryptionKey = await deriveSharedEncryptionKey(priDerivationKey, publicKeyFromApplogs)
	const encryptedKeyUintArr = base64ToArray(encryptedSharedKey as string)
	DEBUG({ publicKeyFromApplogs, publicKeyECDHforPublisher, derivedKeyDecryptionKey, encryptedSharedKey })

	const decryptedSharedKeyBase64 = arrayBufferToBase64(await decryptWithAesSharedKey(encryptedKeyUintArr, derivedKeyDecryptionKey))
	const decryptedSharedKeyUintArrForImport = base64ToArrayBuffer(decryptedSharedKeyBase64)
	DEBUG({ decryptedSharedKeyBase64, decryptedSharedKeyUintArrForImport })

	const aesKey = await globalThis.crypto.subtle.importKey(
		'raw',
		decryptedSharedKeyUintArrForImport, // this has been converted from b64 to uint8[] then decrypted then converted again to uint and then imported
		{
			name: 'AES-GCM',
			length: 256,
		},
		false,
		['encrypt', 'decrypt'],
	)
	DEBUG('shared enc', { aesKey, decryptedSharedKeyBase64, encryptedSharedKey })
	return { aesKey, publisherAg }
}

export const createAESkeyForShare = async function createAESkeyForShare(share: IShare) {
	const agent = useAgent()
	const { ag, did, getHKDFkey } = agent
	const rootHKDFderivationKey = await getHKDFkey()
	const salt = share.id
	const info = `note3share-ag:${ag}-${did}-shareCounter:${share.pubCounter}`

	const aesKey = await deriveAesViaHKDFKey(rootHKDFderivationKey, info, salt, true) // needs to be extractable for distribution and will only be stored encrypted
	DEBUG('shared enc', { aesKey, rootHKDFderivationKey, info, salt })
	return aesKey
}

export interface CryptoKeys {
	personalAes?: CryptoKey
	hkdf?: CryptoKey
	ecdh?: Partial<CryptoKeyPair>
	ecdsa?: Partial<CryptoKeyPair>
	eddsa?: EdDSASigner
}

export const DEFAULT_CTR_LEN = 64

export const aesUtils = {
	decrypt: aesDecrypt,
	encrypt: aesEncrypt,
}
export async function aesDecrypt(cipherArray, key, alg, iv?: ArrayBufferLike) {
	assertCryptoKey(key)
	// the keystore version prefixes the `iv` into the cipher text
	// : await keystoreAES.decryptBytes(encrypted, key, { alg });
	const cipherBufferFull = (new Uint8Array(cipherArray)).buffer // normalizeBase64ToBuf(cipherArray)
	// const importedKey = typeof key === 'string' ? await keys.importKey(key, opts) : key;
	type AesParams = AesCtrParams & AesCbcParams & AesGcmParams
	const decryptOptions: Partial<AesParams> = {
		name: alg,
	}
	// AES-CTR uses a counter, AES-GCM/AES-CBC use an initialization vector
	if (alg !== SymmAlg.AES_CTR) {
		decryptOptions.iv = iv ?? cipherBufferFull.slice(0, 16)
	} else {
		if (!iv) throw ERROR('iv is needed for AES_CTR')
		decryptOptions.counter = new Uint8Array(iv)
		decryptOptions.length = DEFAULT_CTR_LEN
	}

	const cipherBuffer = cipherBufferFull.slice(16)
	DEBUG('aesDecrypt', { cipherBuffer, decryptOptions, key })
	try {
		const msgBuff = await globalThis.crypto.subtle.decrypt(
			decryptOptions as AesParams,
			key,
			cipherBuffer,
		)
		DEBUG('aesDecrypt', { msgBuff })
		return new Uint8Array(msgBuff)
	} catch (error) {
		throw ERROR('aesDecrypt', { decryptOptions, key, cipherBuffer: cipherBufferFull, error })
	}
}
export function joinBufs(fst, snd) {
	const view1 = new Uint8Array(fst)
	const view2 = new Uint8Array(snd)
	const joined = new Uint8Array(view1.length + view2.length)
	joined.set(view1)
	joined.set(view2, view1.length)
	return joined.buffer
}

export async function aesEncrypt(data, key, alg, iv?: BufferSource) {
	assertCryptoKey(key)
	if (!iv) iv = randomBuf(16)
	const encrypted = await globalThis.crypto.subtle.encrypt({ name: alg, iv }, key, data)

	return new Uint8Array(joinBufs(iv, encrypted))
}
async function createECDHderivationKeyPair(newMnemonic?: string[]) {
	// notifyToast(`mnemonic \n${newMnemonic}`, 'success', 300000) // TODO more elegant ux needed

	const hashedMnemonic = hashMnemonic(newMnemonic)
	const encHashedMnu = (new TextEncoder()).encode(hashedMnemonic)

	const keypairFromMnemonic = await getECDHkeypairFromHashArray(encHashedMnu)
	DEBUG('[createECDHderivationKeyPair]', { newMnemonic, keypairFromMnemonic })
	return keypairFromMnemonic
}
async function createEdDSAkeypair(newMnemonic: string[], agentStringNoDID: string) {
	DEBUG.force({ agentStringNoDID })
	const tweekedHashedMnem = hashMnemonic([...newMnemonic, agentStringNoDID])
	const tweekedencHashedMnu = (new TextEncoder()).encode(tweekedHashedMnem)
	const edDSAKeypairFromMnemonic = await getEdDSAkeypairFromHashArray(tweekedencHashedMnu) // const edDSAKeypairFromNonTweekedMnemonic = await getEdDSAkeypairFromHashArray(encHashedMnu)

	return edDSAKeypairFromMnemonic
}
async function createHKDFkey(newMnemonic: string[], entropy = 'base-entropy-for-hkdf') {
	const tweekedHashedMnem = hashMnemonic([...newMnemonic, entropy])
	const HKDFkeyFromMnemonic = await getHKDFkeyFromStrings(tweekedHashedMnem)

	return HKDFkeyFromMnemonic
}
export async function getDerivationKeypairFromIDB(agent: AgentStateClass) {
	const { expectedAgentHash /* , ag */ } = agent
	// if (expectedAgentHash !== ag) WARN('divergent expected and ag in getDerivationKeypairFromIDB', { expectedAgentHash, ag })
	return (await stateDB.cryptokeys.get(
		[expectedAgentHash, 'derivation'], // ? using expected here so that the indexdb can hold agent info for more than one agent
	))?.keys as CryptoKeyPair | undefined
}
export async function getDecryptedSecretFromIDB(agent: AgentStateClass, crypto?: CryptoKeys) {
	crypto = crypto ?? agent.crypto
	const fromIDB = (await stateDB.cryptokeys.get([agent.expectedAgentHash, 'eddsa']))?.keys as { secretKey: Uint8Array }
	const encryptedfromIDB = fromIDB?.secretKey
	DEBUG({ fromIDB })
	if (encryptedfromIDB) {
		const personalEncryptionKey = await getAESkeyPersonal(crypto.ecdh as CryptoKeyPair)
		VERBOSE('Attempting Decryption', { encryptedfromIDB, personalEncryptionKey })
		const decryptedFromIDB = await aesUtils.decrypt(encryptedfromIDB, personalEncryptionKey, SymmAlg.AES_GCM)
		DEBUG({ fromIDB, decryptedFromIDB })
		return decryptedFromIDB
	} else {
		WARN('decrypted Secret Not Found')
	}
	return null
}
export async function getEdDSAfromIDB(agent: AgentStateClass, crypto?: CryptoKeys) {
	crypto = crypto ?? agent.crypto
	const decryptedFromIDB = await getDecryptedSecretFromIDB(agent, crypto)
	if (decryptedFromIDB) {
		const edKeypairInstance = EdKeypairInstanceFromSecretKeyBytes(decryptedFromIDB)
		DEBUG({ edKeypairInstance })
		return edKeypairInstance
	}
	return null
}
export async function getMnemonicFromIDB(agent: AgentStateClass, agOveride?: string, crypto?: CryptoKeys) {
	crypto = crypto ?? agent.crypto
	const fromIDB = (await stateDB.cryptokeys.get([agOveride ?? agent.expectedAgentHash ?? agent.ag, 'mnemonic']))?.keys

	// @ts-expect-error
	const encryptedfromIDB = fromIDB?.encryptedMnemonic
	DEBUG({ fromIDB })
	if (encryptedfromIDB) {
		const personalEncryptionKey = await getAESkeyPersonal(crypto.ecdh as CryptoKeyPair)
		const decryptedFromIDB = await aesUtils.decrypt(encryptedfromIDB, personalEncryptionKey, SymmAlg.AES_GCM)
		return new TextDecoder('utf-8').decode(decryptedFromIDB)
	}
}
export const initializeCryptoKeypairs = async function initializeCryptoKeypairs(
	agent: AgentStateClass,
	newMnemonic?: string[],
	skipWarning = DefaultFalse,
) {
	const crypto: CryptoKeys = agent.crypto ?? {}
	DEBUG('[CryptoKeys] default', { crypto })

	if (newMnemonic?.length > 10) {
		crypto.ecdh = await createECDHderivationKeyPair(newMnemonic)
		crypto.eddsa = await createEdDSAkeypair(newMnemonic, agent.agentStringNoDID)
		VERBOSE('created ecdh and EdDSAkeypair from passed "valid" mneumonic', { crypto })
	} else {
		crypto.ecdh = await getDerivationKeypairFromIDB(agent)
		DEBUG('[CryptoKeys] ecdh?', { crypto })

		if (!crypto.ecdh) {
			newMnemonic = createMnemonic(24)
			crypto.ecdh = await createECDHderivationKeyPair(newMnemonic)
			crypto.eddsa = await createEdDSAkeypair(newMnemonic, agent.agentStringNoDID) // TODO derive from ecdh instead maybe
			VERBOSE('created ecdh and EdDSAkeypair from new mneumonic', { crypto })
		} else {
			crypto.eddsa = await getEdDSAfromIDB(agent, crypto)
			VERBOSE('createdEdDSAkeypair from ecdh in IDB', { crypto })
		}
	}

	const personalEncryptionKey = await getAESkeyPersonal(crypto.ecdh as CryptoKeyPair) // crypto.ecdh could be from idb or fresh
	// const personalHKDFkey = await createHKDFkey(newMnemonic)
	// crypto.hkdf = personalHKDFkey
	let encryptedEdSecretKey
	if (crypto.eddsa) {
		// HACK: we need raw private key
		const eddsaPkEncoded = crypto.eddsa.export()
		const eddsaPk = untag(EdDSASigner.code, base64pad.decode(eddsaPkEncoded))
		encryptedEdSecretKey = await aesUtils.encrypt(
			eddsaPk,
			personalEncryptionKey,
			SymmAlg.AES_GCM,
		)
	}
	// ready in case its needed: ecdsa
	// const signingKeypairFromMnemonic = await getECDSAkeypairFromHashArray(encHashedMnu)
	// crypto.ecdsa = signingKeypairFromMnemonic

	const updated = { ...crypto }

	updateAgentState({ crypto: updated })

	const didAfter = agent.did
	const ag = agent.ag
	const expectedBefor = agent.expectedAgentHash
	agent.expectedAgentHash = ag
	DEBUG('init Keys', { ag, didAfter, agExpected: agent.expectedAgentHash })
	if (!skipWarning && expectedBefor && expectedBefor !== ag) {
		notifyToast(
			`non matching expected and resulting ag \n (ag has changed since last app load from ${expectedBefor} -> ${ag})`,
			ToastVariant.warning,
			90000,
		)
	}
	// ? check this idempotent put approach
	if (crypto.eddsa) await stateDB.cryptokeys.put({ ag, type: 'eddsa', keys: { secretKey: encryptedEdSecretKey } })
	if (crypto.ecdh) await stateDB.cryptokeys.put({ ag, type: 'derivation', keys: crypto.ecdh })
	// await stateDB.cryptokeys.put({ ag, type: 'signing-ecdsa', keys: crypto.eddsa }) // keep around

	if (newMnemonic?.length) {
		const encryptedMnemonic = await aesUtils.encrypt(
			new TextEncoder().encode(newMnemonic.toString()),
			personalEncryptionKey,
			SymmAlg.AES_GCM,
		)
		await stateDB.cryptokeys.put({ ag, type: 'mnemonic', keys: { encryptedMnemonic } })
	}

	DEBUG('initialized', { crypto })
	await agent.ensureAgentAtoms()
	return crypto.ecdh
}
