// @ts-ignore[untyped-import]
import { x25519 } from "../internal/noble-curves-1.3.0.js"
import { random } from "../random/Randomizer.js"

export type X25519PrivateKey = Uint8Array
export type X25519PublicKey = Uint8Array

/**
 * Contains a public key and its corresponding private key
 *
 * NOTE: Keys should be cleared from memory once they are no longer needed!
 */
export type X25519KeyPair = {
	publicKey: X25519PublicKey
	privateKey: X25519PrivateKey
}

/**
 * Contains all information for deriving AES keys
 *
 * The shared secret MUST NEVER be used as a key directly as it is a biased (some bits are more likely to be set than others).
 * The involved public keys should also be included when deriving an AES key from these shared secrets.
 */
export type X25519SharedSecrets = {
	ephemeralSharedSecret: Uint8Array
	authSharedSecret: Uint8Array
}

// The number of bytes for a private key in the curve
// the byte length of the modulus
const X25519_N_BYTE_LENGTH = 32

/**
 * @return randomly generated X25519 key pair
 */
export function generateX25519KeyPair(): X25519KeyPair {
	// noble-curves appears to clamp the private key when using it, but not when generating it, so for safety,
	// we do not want to store it un-clamped in case we use a different implementation later
	const privateKey = clampPrivateKey(random.generateRandomData(X25519_N_BYTE_LENGTH))
	const publicKey = deriveX25519PublicKey(privateKey)
	return {
		privateKey,
		publicKey,
	}
}

/**
 * Derive a shared secret from the sender's private key and the recipient's public key to encrypt a message
 * @param senderIdentityPrivateKey    the sender's private identity key
 * @param ephemeralPrivateKey  the ephemeral private key generated by the sender for just one message (to one or more recipients)
 * @param recipientIdentityPublicKey the recipient's public identity key
 * @return the shared secrets
 */
export function x25519Encapsulate(
	senderIdentityPrivateKey: X25519PrivateKey,
	ephemeralPrivateKey: X25519PrivateKey,
	recipientIdentityPublicKey: X25519PublicKey,
): X25519SharedSecrets {
	const ephemeralSharedSecret = generateSharedSecret(ephemeralPrivateKey, recipientIdentityPublicKey)
	const authSharedSecret = generateSharedSecret(senderIdentityPrivateKey, recipientIdentityPublicKey)
	return { ephemeralSharedSecret, authSharedSecret }
}

/**
 * Derive a shared secret from the recipient's private key and the sender's public key to decrypt a message
 * @param senderIdentityPublicKey    the sender's public identity key
 * @param ephemeralPublicKey  the ephemeral public key generated by the sender for just one message (to one or more recipients)
 * @param recipientIdentityPrivateKey the recipient's private identity key
 * @return shared secret and the sender's public key
 */
export function x25519Decapsulate(
	senderIdentityPublicKey: X25519PublicKey,
	ephemeralPublicKey: X25519PublicKey,
	recipientIdentityPrivateKey: X25519PrivateKey,
): X25519SharedSecrets {
	const ephemeralSharedSecret = generateSharedSecret(recipientIdentityPrivateKey, ephemeralPublicKey)
	const authSharedSecret = generateSharedSecret(recipientIdentityPrivateKey, senderIdentityPublicKey)
	return { ephemeralSharedSecret, authSharedSecret }
}

/**
 * Diffie-Hellman key exchange; works by combining one party's private key and the other party's public key to form a shared secret between both parties
 */
function generateSharedSecret(localPrivateKey: X25519PrivateKey, remotePublicKey: X25519PublicKey): Uint8Array {
	const sharedSecret = x25519.getSharedSecret(localPrivateKey, remotePublicKey)

	// if every byte somehow happens to be 0, we can't use this as a secret; this is astronomically unlikely to happen by chance
	if (sharedSecret.every((val: number) => val === 0)) {
		throw new Error("can't get shared secret: bad key inputs")
	}

	return sharedSecret
}

// see https://www.jcraige.com/an-explainer-on-ed25519-clamping for an explanation on why we do this
function clampPrivateKey(privateKey: X25519PrivateKey): X25519PrivateKey {
	// First, we want to unset the highest bit but set the second-highest bit to 1. This prevents potential timing and brute-force attacks, respectively.
	privateKey[privateKey.length - 1] = (privateKey[privateKey.length - 1] & 0b01111111) | 0b01000000

	// Then, we want to guarantee our scalar is a multiple of 8, our cofactor, to protect against small-subgroup attacks per RFC 2785 which could leak key data!
	privateKey[0] &= 0b11111000

	return privateKey
}

/**
 * Derives the public key from the private key, without any integrity checks.
 * @param privateKey a valid clamped private key
 */
export function deriveX25519PublicKey(privateKey: X25519PrivateKey): X25519PublicKey {
	return x25519.getPublicKey(privateKey)
}
