package goja_bindings

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"io"

	"github.com/dop251/goja"
)

type wordArray struct {
	vm   *goja.Runtime
	iv   []byte
	data []byte
}

func BindCrypto(vm *goja.Runtime) error {

	err := vm.Set("CryptoJS", map[string]interface{}{
		"AES": map[string]interface{}{
			"encrypt": cryptoAESEncryptFunc(vm),
			"decrypt": cryptoAESDecryptFunc(vm),
		},
		"enc": map[string]interface{}{
			"Utf8": map[string]interface{}{
				"parse":     cryptoEncUtf8ParseFunc(vm),
				"stringify": cryptoEncUtf8StringifyFunc(vm),
			},
			"Base64": map[string]interface{}{
				"parse":     cryptoEncBase64ParseFunc(vm),
				"stringify": cryptoEncBase64StringifyFunc(vm),
			},
			"Hex": map[string]interface{}{
				"parse":     cryptoEncHexParseFunc(vm),
				"stringify": cryptoEncHexStringifyFunc(vm),
			},
			"Latin1": map[string]interface{}{
				"parse":     cryptoEncLatin1ParseFunc(vm),
				"stringify": cryptoEncLatin1StringifyFunc(vm),
			},
			"Utf16": map[string]interface{}{
				"parse":     cryptoEncUtf16ParseFunc(vm),
				"stringify": cryptoEncUtf16StringifyFunc(vm),
			},
			"Utf16LE": map[string]interface{}{
				"parse":     cryptoEncUtf16LEParseFunc(vm),
				"stringify": cryptoEncUtf16LEStringifyFunc(vm),
			},
		},
	})
	if err != nil {
		return err
	}

	return nil
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

func newWordArrayGojaValue(vm *goja.Runtime, data []byte, iv []byte) goja.Value {
	wa := &wordArray{
		vm:   vm,
		iv:   iv,
		data: data,
	}
	// WordArray // Utf8
	// WordArray.toString(): string // Uses Base64.stringify
	// WordArray.toString(encoder: Encoder): string
	obj := vm.NewObject()
	obj.Prototype().Set("toString", wa.toStringFunc)
	obj.Set("toString", wa.toStringFunc)
	obj.Set("iv", iv)
	return obj
}

func (wa *wordArray) toStringFunc(call goja.FunctionCall) goja.Value {
	if len(call.Arguments) == 0 {
		return wa.vm.ToValue(base64Stringify(wa.data))
	}

	encoder, ok := call.Argument(0).Export().(map[string]interface{})
	if !ok {
		panic(wa.vm.ToValue("TypeError: encoder parameter must be a CryptoJS.enc object"))
	}

	var ret string
	if f, ok := encoder["stringify"]; ok {
		if stringify, ok := f.(func(functionCall goja.FunctionCall) goja.Value); ok {
			ret = stringify(goja.FunctionCall{Arguments: []goja.Value{wa.vm.ToValue(wa.data)}}).String()
		} else {
			panic(wa.vm.ToValue("TypeError: encoder.stringify must be a function"))
		}
	} else {
		ret = string(wa.data)
	}

	return wa.vm.ToValue(ret)
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// CryptoJS.AES.encrypt(message: string, key: string, cfg?: { iv: ArrayBuffer }): WordArray
func cryptoAESEncryptFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
	return func(call goja.FunctionCall) (ret goja.Value) {
		if len(call.Arguments) < 2 {
			panic(vm.ToValue("TypeError: AES.encrypt requires at least 2 arguments"))
		}

		message := call.Argument(0).String()

		var keyBytes []byte
		switch call.Argument(1).Export().(type) {
		case string:
			key := call.Argument(1).String()
			keyBytes = adjustKeyLength([]byte(key))
		case []byte:
			keyBytes = call.Argument(1).Export().([]byte)
			keyBytes = adjustKeyLength(keyBytes)
		default:
			panic(vm.ToValue("TypeError: key parameter must be a string or an ArrayBuffer"))
		}

		usedRandomIV := false
		// Check if IV is provided
		var ivBytes []byte
		if len(call.Arguments) > 2 {
			cfg := call.Argument(2).Export().(map[string]interface{})
			var ok bool
			iv, ok := cfg["iv"].([]byte)
			if !ok {
				panic(vm.ToValue("TypeError: iv parameter must be an ArrayBuffer"))
			}
			ivBytes = iv
			if len(ivBytes) != aes.BlockSize {
				panic(vm.ToValue("TypeError: IV length must be equal to block size (16 bytes for AES)"))
			}
		} else {
			// Generate a random IV
			ivBytes = make([]byte, aes.BlockSize)
			if _, err := io.ReadFull(rand.Reader, ivBytes); err != nil {
				panic(vm.ToValue(fmt.Sprintf("Failed to generate IV: %v", err)))
			}
			usedRandomIV = true
		}

		defer func() {
			if r := recover(); r != nil {
				ret = vm.ToValue(fmt.Sprintf("Encryption failed: %v", r))
			}
		}()

		// Encrypt the message
		encryptedMessage := encryptAES(vm, message, keyBytes, ivBytes)

		if usedRandomIV {
			// Prepend the IV to the encrypted message
			encryptedMessage = append(ivBytes, encryptedMessage...)
		}

		return newWordArrayGojaValue(vm, encryptedMessage, ivBytes)
	}
}

// CryptoJS.AES.decrypt(encryptedMessage: string | WordArray, key: string, cfg?: { iv: ArrayBuffer }): WordArray
func cryptoAESDecryptFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
	return func(call goja.FunctionCall) (ret goja.Value) {
		if len(call.Arguments) < 2 {
			panic(vm.ToValue("TypeError: AES.decrypt requires at least 2 arguments"))
		}

		// Can be string or WordArray
		// If WordArray, String() will call WordArray.toString() which will return the base64 encoded string
		encryptedMessage := call.Argument(0).String()

		var keyBytes []byte
		var originalPassword []byte
		switch call.Argument(1).Export().(type) {
		case string:
			key := call.Argument(1).String()
			originalPassword = []byte(key)
			keyBytes = adjustKeyLength([]byte(key))
		case []byte:
			keyBytes = call.Argument(1).Export().([]byte)
			originalPassword = keyBytes
			keyBytes = adjustKeyLength(keyBytes)
		default:
			panic(vm.ToValue("TypeError: key parameter must be a string or an ArrayBuffer"))
		}

		var ivBytes []byte
		var cipherText []byte

		// If IV is provided in the third argument
		if len(call.Arguments) > 2 {
			cfg := call.Argument(2).Export().(map[string]interface{})
			var ok bool
			iv, ok := cfg["iv"].([]byte)
			if !ok {
				panic(vm.ToValue("TypeError: iv parameter must be an ArrayBuffer"))
			}
			ivBytes = iv
			if len(ivBytes) != aes.BlockSize {
				panic(vm.ToValue("TypeError: IV length must be equal to block size (16 bytes for AES)"))
			}
			var err error
			decodedMessage, err := base64.StdEncoding.DecodeString(encryptedMessage)
			if err != nil {
				panic(vm.ToValue(fmt.Sprintf("Failed to decode ciphertext: %v", err)))
			}
			cipherText = decodedMessage
		} else {
			// Decode the base64 encoded string
			decodedMessage, err := base64.StdEncoding.DecodeString(encryptedMessage)
			if err != nil {
				panic(vm.ToValue(fmt.Sprintf("Failed to decode ciphertext: %v", err)))
			}

			// Check if openssl
			if len(decodedMessage) >= 16 && string(decodedMessage[:8]) == "Salted__" {
				salt := decodedMessage[8:16]
				cipherText = decodedMessage[16:]
				derivedKey, derivedIV := evpBytesToKey(originalPassword, salt, 32, aes.BlockSize)
				keyBytes = derivedKey
				ivBytes = derivedIV
			} else {
				// Extract the IV from the beginning of the message
				ivBytes = decodedMessage[:aes.BlockSize]
				cipherText = decodedMessage[aes.BlockSize:]
			}
		}

		// Decrypt the message
		decrypted := decryptAES(vm, cipherText, keyBytes, ivBytes)

		return newWordArrayGojaValue(vm, decrypted, ivBytes)
	}
}

// Adjusts the key length to match AES key length requirements (16, 24, or 32 bytes).
// If the key length is not 16, 24, or 32 bytes, it is hashed using SHA-256 and truncated to 32 bytes (AES-256).
func adjustKeyLength(keyBytes []byte) []byte {
	switch len(keyBytes) {
	case 16, 24, 32:
		// Valid AES key lengths: 16 bytes (AES-128), 24 bytes (AES-192), 32 bytes (AES-256)
		return keyBytes
	default:
		// Hash the key to 32 bytes (AES-256)
		hash := sha256.Sum256(keyBytes)
		return hash[:]
	}
}

func encryptAES(vm *goja.Runtime, message string, key []byte, iv []byte) (ret []byte) {
	defer func() {
		if r := recover(); r != nil {
			ret = nil
		}
	}()

	block, err := aes.NewCipher(key)
	if err != nil {
		panic(vm.ToValue(fmt.Sprintf("%v", err)))
	}

	messageBytes := []byte(message)
	messageBytes = pkcs7Padding(messageBytes, aes.BlockSize)

	cipherText := make([]byte, len(messageBytes))

	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(cipherText, messageBytes)

	return cipherText
}

func decryptAES(vm *goja.Runtime, cipherText []byte, key []byte, iv []byte) (ret []byte) {
	defer func() {
		if r := recover(); r != nil {
			ret = nil
		}
	}()

	block, err := aes.NewCipher(key)
	if err != nil {
		panic(vm.ToValue(fmt.Sprintf("%v", err)))
	}

	plainText := make([]byte, len(cipherText))

	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(plainText, cipherText)

	plainText = pkcs7Trimming(plainText)

	return plainText
}

func pkcs7Padding(data []byte, blockSize int) []byte {
	padding := blockSize - len(data)%blockSize
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(data, padText...)
}

func pkcs7Trimming(data []byte) []byte {
	length := len(data)
	up := int(data[length-1])
	return data[:(length - up)]
}

func evpBytesToKey(password []byte, salt []byte, keyLen, ivLen int) ([]byte, []byte) {
	d := make([]byte, 0)
	dI := make([]byte, 0)

	for len(d) < (keyLen + ivLen) {
		h := md5.New()
		h.Write(dI)
		h.Write(password)
		h.Write(salt)
		dI = h.Sum(nil)
		d = append(d, dI...)
	}

	return d[:keyLen], d[keyLen : keyLen+ivLen]
}
