import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto/index.js'
import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults'
import type { KeyPair } from '../Types'
import type { BinaryNode } from '../WABinary'
import { decodeBinaryNode } from '../WABinary'
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto'
import type { ILogger } from './logger'

const IV_LENGTH = 12

const EMPTY_BUFFER = Buffer.alloc(0)

const generateIV = (counter: number): Uint8Array => {
	const iv = new ArrayBuffer(IV_LENGTH)
	new DataView(iv).setUint32(8, counter)
	return new Uint8Array(iv)
}

class TransportState {
	private readCounter = 0
	private writeCounter = 0

	private readonly iv = new Uint8Array(IV_LENGTH)

	constructor(
		private readonly encKey: Uint8Array,
		private readonly decKey: Uint8Array
	) {}

	encrypt(plaintext: Uint8Array): Uint8Array {
		const c = this.writeCounter++
		this.iv[8] = (c >>> 24) & 0xff
		this.iv[9] = (c >>> 16) & 0xff
		this.iv[10] = (c >>> 8) & 0xff
		this.iv[11] = c & 0xff

		return aesEncryptGCM(plaintext, this.encKey, this.iv, EMPTY_BUFFER)
	}

	decrypt(ciphertext: Uint8Array): Buffer {
		const c = this.readCounter++
		this.iv[8] = (c >>> 24) & 0xff
		this.iv[9] = (c >>> 16) & 0xff
		this.iv[10] = (c >>> 8) & 0xff
		this.iv[11] = c & 0xff

		return aesDecryptGCM(ciphertext, this.decKey, this.iv, EMPTY_BUFFER) as Buffer
	}
}

export const makeNoiseHandler = ({
	keyPair: { private: privateKey, public: publicKey },
	NOISE_HEADER,
	logger,
	routingInfo
}: {
	keyPair: KeyPair
	NOISE_HEADER: Uint8Array
	logger: ILogger
	routingInfo?: Buffer | undefined
}) => {
	logger = logger.child({ class: 'ns' })

	const data = Buffer.from(NOISE_MODE)
	let hash = data.byteLength === 32 ? data : sha256(data)
	let salt: Buffer = hash
	let encKey: Buffer = hash
	let decKey: Buffer = hash
	let counter = 0
	let sentIntro = false

	let inBytes: Buffer = Buffer.alloc(0)

	let transport: TransportState | null = null
	let isWaitingForTransport = false
	let pendingOnFrame: ((buff: Uint8Array | BinaryNode) => void) | null = null

	let introHeader: Buffer
	if (routingInfo) {
		introHeader = Buffer.alloc(7 + routingInfo.byteLength + NOISE_HEADER.length)
		introHeader.write('ED', 0, 'utf8')
		introHeader.writeUint8(0, 2)
		introHeader.writeUint8(1, 3)
		introHeader.writeUint8(routingInfo.byteLength >> 16, 4)
		introHeader.writeUint16BE(routingInfo.byteLength & 65535, 5)
		introHeader.set(routingInfo, 7)
		introHeader.set(NOISE_HEADER, 7 + routingInfo.byteLength)
	} else {
		introHeader = Buffer.from(NOISE_HEADER)
	}

	const authenticate = (data: Uint8Array) => {
		if (!transport) {
			hash = sha256(Buffer.concat([hash, data]))
		}
	}

	const encrypt = (plaintext: Uint8Array): Uint8Array => {
		if (transport) {
			return transport.encrypt(plaintext)
		}

		const result = aesEncryptGCM(plaintext, encKey, generateIV(counter++), hash)
		authenticate(result)
		return result
	}

	const decrypt = (ciphertext: Uint8Array): Uint8Array => {
		if (transport) {
			return transport.decrypt(ciphertext)
		}

		const result = aesDecryptGCM(ciphertext, decKey, generateIV(counter++), hash)
		authenticate(ciphertext)
		return result
	}

	const localHKDF = (data: Uint8Array): [Uint8Array, Uint8Array] => {
		const key = hkdf(Buffer.from(data), 64, { salt, info: '' })
		return [key.subarray(0, 32), key.subarray(32)]
	}

	const mixIntoKey = (data: Uint8Array) => {
		const [write, read] = localHKDF(data)
		salt = Buffer.from(write)
		encKey = Buffer.from(read)
		decKey = Buffer.from(read)
		counter = 0
	}

	const finishInit = async () => {
		isWaitingForTransport = true
		const [write, read] = localHKDF(new Uint8Array(0))
		transport = new TransportState(write, read)
		isWaitingForTransport = false

		logger.trace('Noise handler transitioned to Transport state')

		if (pendingOnFrame) {
			logger.trace({ length: inBytes.length }, 'Flushing buffered frames after transport ready')
			await processData(pendingOnFrame)
			pendingOnFrame = null
		}
	}

	const processData = async (onFrame: (buff: Uint8Array | BinaryNode) => void) => {
		let size: number | undefined

		while (true) {
			if (inBytes.length < 3) return

			size = (inBytes[0]! << 16) | (inBytes[1]! << 8) | inBytes[2]!

			if (inBytes.length < size + 3) return

			let frame: Uint8Array | BinaryNode = inBytes.subarray(3, size + 3)
			inBytes = inBytes.subarray(size + 3)

			if (transport) {
				const result = transport.decrypt(frame)
				frame = await decodeBinaryNode(result)
			}

			if (logger.level === 'trace') {
				logger.trace({ msg: (frame as BinaryNode)?.attrs?.id }, 'recv frame')
			}

			onFrame(frame)
		}
	}

	authenticate(NOISE_HEADER)
	authenticate(publicKey)

	return {
		encrypt,
		decrypt,
		authenticate,
		mixIntoKey,
		finishInit,
		processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
			if (!serverHello?.ephemeral) {
				throw new Boom('Missing server hello ephemeral', { statusCode: 500 })
			}

			if (!serverHello?.static) {
				throw new Boom('Missing server hello static', { statusCode: 500 })
			}

			if (!serverHello?.payload) {
				throw new Boom('Missing server hello payload', { statusCode: 500 })
			}

			authenticate(serverHello.ephemeral)
			mixIntoKey(Curve.sharedKey(privateKey, serverHello.ephemeral))

			const decStaticContent = decrypt(serverHello.static)
			mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))

			const certDecoded = decrypt(serverHello.payload)

			const { intermediate: certIntermediate, leaf } = proto.CertChain.decode(certDecoded)
			// leaf
			if (!leaf?.details || !leaf?.signature) {
				throw new Boom('invalid noise leaf certificate', { statusCode: 400 })
			}

			if (!certIntermediate?.details || !certIntermediate?.signature) {
				throw new Boom('invalid noise intermediate certificate', { statusCode: 400 })
			}

			const details = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate.details)

			const { issuerSerial } = details

			if (!details.key) {
				throw new Boom('Missing certificate key', { statusCode: 500 })
			}

			const verify = Curve.verify(details.key, leaf.details, leaf.signature)

			const verifyIntermediate = Curve.verify(
				WA_CERT_DETAILS.PUBLIC_KEY,
				certIntermediate.details,
				certIntermediate.signature
			)

			if (!verify) {
				throw new Boom('noise certificate signature invalid', { statusCode: 400 })
			}

			if (!verifyIntermediate) {
				throw new Boom('noise intermediate certificate signature invalid', { statusCode: 400 })
			}

			if (issuerSerial !== WA_CERT_DETAILS.SERIAL) {
				throw new Boom('certification match failed', { statusCode: 400 })
			}

			const keyEnc = encrypt(noiseKey.public)
			mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello.ephemeral))

			return keyEnc
		},
		encodeFrame: (data: Buffer | Uint8Array) => {
			if (transport) {
				data = transport.encrypt(data)
			}

			const dataLen = data.byteLength
			const introSize = sentIntro ? 0 : introHeader.length
			const frame = Buffer.allocUnsafe(introSize + 3 + dataLen)

			if (!sentIntro) {
				frame.set(introHeader)
				sentIntro = true
			}

			frame[introSize] = (dataLen >>> 16) & 0xff
			frame[introSize + 1] = (dataLen >>> 8) & 0xff
			frame[introSize + 2] = dataLen & 0xff

			frame.set(data, introSize + 3)

			return frame
		},
		decodeFrame: async (newData: Buffer | Uint8Array, onFrame: (buff: Uint8Array | BinaryNode) => void) => {
			if (isWaitingForTransport) {
				inBytes = Buffer.concat([inBytes, newData])
				pendingOnFrame = onFrame
				return
			}

			if (inBytes.length === 0) {
				inBytes = Buffer.from(newData)
			} else {
				inBytes = Buffer.concat([inBytes, newData])
			}

			await processData(onFrame)
		}
	}
}
