package http

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"

	"github.com/awnumar/memguard"

	"github.com/influxdata/telegraf/config"
)

type AesEncryptor struct {
	Variant []string      `toml:"-"`
	Key     config.Secret `toml:"key"`
	Vec     config.Secret `toml:"init_vector"`
	KDFConfig

	mode string
	trim func([]byte) ([]byte, error)
}

func (a *AesEncryptor) Init() error {
	var cipherName, mode, padding string

	switch len(a.Variant) {
	case 3:
		padding = strings.ToLower(a.Variant[2])
		fallthrough
	case 2:
		mode = strings.ToLower(a.Variant[1])
		cipherName = strings.ToLower(a.Variant[0])
		if !strings.HasPrefix(cipherName, "aes") {
			return fmt.Errorf("requested AES but specified %q", cipherName)
		}
	case 1:
		return errors.New("please specify cipher mode")
	case 0:
		return errors.New("please specify cipher")
	default:
		return errors.New("too many variant elements")
	}

	var keylen int
	switch cipherName {
	case "aes128":
		keylen = 16
	case "aes192":
		keylen = 24
	case "aes256":
		keylen = 32
	default:
		return fmt.Errorf("unsupported AES cipher %q", cipherName)
	}

	if mode != "cbc" {
		return fmt.Errorf("unsupported cipher mode %q", a.Variant[1])
	}
	a.mode = mode

	// Setup the trimming function to revert padding
	switch padding {
	case "", "none":
		// identity, no padding
		a.trim = func(in []byte) ([]byte, error) { return in, nil }
	case "pkcs#5", "pkcs#7":
		a.trim = PKCS5or7Trimming
	default:
		return fmt.Errorf("unsupported padding %q", padding)
	}

	// Generate the key using password-based-keys
	if a.Key.Empty() {
		if a.Passwd.Empty() {
			return errors.New("either key or password has to be specified")
		}
		if a.Salt.Empty() || a.Iterations == 0 {
			return errors.New("salt and iterations required for password-based-keys")
		}

		key, iv, err := a.KDFConfig.NewKey(keylen)
		if err != nil {
			return fmt.Errorf("generating key failed: %w", err)
		}
		a.Key.Destroy()
		a.Key = key

		if a.Vec.Empty() && !iv.Empty() {
			a.Vec.Destroy()
			a.Vec = iv
		}
	} else {
		encodedKey, err := a.Key.Get()
		if err != nil {
			return fmt.Errorf("getting key failed: %w", err)
		}
		key := make([]byte, hex.DecodedLen(len(encodedKey.Bytes())))
		_, err = hex.Decode(key, encodedKey.Bytes())
		encodedKey.Destroy()
		if err != nil {
			return fmt.Errorf("decoding key failed: %w", err)
		}
		actuallen := len(key)
		memguard.WipeBytes(key)

		if actuallen != keylen {
			return fmt.Errorf("key length (%d bit) does not match cipher (%d bit)", actuallen*8, keylen*8)
		}
	}

	if a.Vec.Empty() {
		return errors.New("'init_vector' has to be specified or derived from password")
	}

	encodedIV, err := a.Vec.Get()
	if err != nil {
		return fmt.Errorf("getting IV failed: %w", err)
	}
	ivlen := len(encodedIV.Bytes())
	encodedIV.Destroy()
	if ivlen != 2*aes.BlockSize {
		return errors.New("init vector size must match block size")
	}

	return nil
}

func (a *AesEncryptor) Decrypt(data []byte) ([]byte, error) {
	if len(data)%aes.BlockSize != 0 {
		return nil, fmt.Errorf("invalid data size %d", len(data))
	}
	if a.mode != "cbc" {
		return nil, fmt.Errorf("unsupported cipher mode %q", a.mode)
	}

	// Setup the cipher and return the decoded data
	encodedKey, err := a.Key.Get()
	if err != nil {
		return nil, fmt.Errorf("getting key failed: %w", err)
	}
	key := make([]byte, hex.DecodedLen(len(encodedKey.Bytes())))
	_, err = hex.Decode(key, encodedKey.Bytes())
	encodedKey.Destroy()
	if err != nil {
		return nil, fmt.Errorf("decoding key failed: %w", err)
	}

	// Setup AES
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("creating AES cipher failed: %w", err)
	}

	// Setup the block/stream cipher and decode the data
	encodedIV, err := a.Vec.Get()
	if err != nil {
		return nil, fmt.Errorf("getting initialization-vector failed: %w", err)
	}
	iv := make([]byte, hex.DecodedLen(len(encodedIV.Bytes())))
	_, err = hex.Decode(iv, encodedIV.Bytes())
	encodedIV.Destroy()
	if err != nil {
		memguard.WipeBytes(iv)
		return nil, fmt.Errorf("decoding init vector failed: %w", err)
	}

	cipher.NewCBCDecrypter(block, iv).CryptBlocks(data, data)
	return a.trim(data)
}
