package s3crypto

import (
	"fmt"
	"io"

	"github.com/aws/aws-sdk-go/aws"
)

const (
	gcmKeySize   = 32
	gcmNonceSize = 12
)

// AESGCMContentCipherBuilder returns a new encryption only AES/GCM mode structure with a specific cipher data generator
// that will provide keys to be used for content encryption.
//
// Note: This uses the Go stdlib AEAD implementation for AES/GCM. Due to this objects to be encrypted or decrypted
// will be fully loaded into memory before encryption or decryption can occur. Caution must be taken to avoid memory
// allocation failures.
//
// Deprecated: This feature is in maintenance mode, no new updates will be released. Please see https://docs.aws.amazon.com/general/latest/gr/aws_sdk_cryptography.html for more information.
func AESGCMContentCipherBuilder(generator CipherDataGenerator) ContentCipherBuilder {
	return gcmContentCipherBuilder{generator}
}

// AESGCMContentCipherBuilderV2 returns a new encryption only AES/GCM mode structure with a specific cipher data generator
// that will provide keys to be used for content encryption. This type is compatible with the V2 encryption client.
//
// Note: This uses the Go stdlib AEAD implementation for AES/GCM. Due to this objects to be encrypted or decrypted
// will be fully loaded into memory before encryption or decryption can occur. Caution must be taken to avoid memory
// allocation failures.
func AESGCMContentCipherBuilderV2(generator CipherDataGeneratorWithCEKAlg) ContentCipherBuilder {
	return gcmContentCipherBuilderV2{generator}
}

// RegisterAESGCMContentCipher registers the AES/GCM content cipher algorithm with the provided CryptoRegistry.
//
// Example:
//
//	cr := s3crypto.NewCryptoRegistry()
//	if err := s3crypto.RegisterAESGCMContentCipher(cr); err != nil {
//		panic(err) // handle error
//	}
func RegisterAESGCMContentCipher(registry *CryptoRegistry) error {
	if registry == nil {
		return errNilCryptoRegistry
	}

	err := registry.AddCEK(AESGCMNoPadding, newAESGCMContentCipher)
	if err != nil {
		return err
	}

	// NoPadder is generic but required by this algorithm, so if it is already registered and is the expected implementation
	// don't error.
	padderName := NoPadder.Name()
	if v, ok := registry.GetPadder(padderName); !ok {
		if err := registry.AddPadder(padderName, NoPadder); err != nil {
			return err
		}
	} else if _, ok := v.(noPadder); !ok {
		return fmt.Errorf("%s is already registred but does not match expected type %T", padderName, NoPadder)
	}
	return nil
}

// gcmContentCipherBuilder is a AES/GCM content cipher to be used with the V1 client CipherDataGenerator interface
type gcmContentCipherBuilder struct {
	generator CipherDataGenerator
}

func (builder gcmContentCipherBuilder) ContentCipher() (ContentCipher, error) {
	return builder.ContentCipherWithContext(aws.BackgroundContext())
}

func (builder gcmContentCipherBuilder) ContentCipherWithContext(ctx aws.Context) (ContentCipher, error) {
	var cd CipherData
	var err error

	switch v := builder.generator.(type) {
	case CipherDataGeneratorWithContext:
		cd, err = v.GenerateCipherDataWithContext(ctx, gcmKeySize, gcmNonceSize)
	default:
		cd, err = builder.generator.GenerateCipherData(gcmKeySize, gcmNonceSize)
	}
	if err != nil {
		return nil, err
	}

	return newAESGCMContentCipher(cd)
}

// isFixtureEncryptionCompatible will ensure that this type may only be used with the V1 client
func (builder gcmContentCipherBuilder) isEncryptionVersionCompatible(version clientVersion) error {
	if version != v1ClientVersion {
		return errDeprecatedIncompatibleCipherBuilder
	}
	return nil
}

func (builder gcmContentCipherBuilder) isAWSFixture() bool {
	return true
}

// gcmContentCipherBuilderV2 return a new builder for encryption content using AES/GCM/NoPadding. This type is meant
// to be used with key wrapping implementations that allow the cek algorithm to be provided when calling the
// cipher data generator.
type gcmContentCipherBuilderV2 struct {
	generator CipherDataGeneratorWithCEKAlg
}

func (builder gcmContentCipherBuilderV2) ContentCipher() (ContentCipher, error) {
	return builder.ContentCipherWithContext(aws.BackgroundContext())
}

func (builder gcmContentCipherBuilderV2) ContentCipherWithContext(ctx aws.Context) (ContentCipher, error) {
	cd, err := builder.generator.GenerateCipherDataWithCEKAlg(ctx, gcmKeySize, gcmNonceSize, AESGCMNoPadding)
	if err != nil {
		return nil, err
	}

	return newAESGCMContentCipher(cd)
}

// isFixtureEncryptionCompatible will ensure that this type may only be used with the V2 client
func (builder gcmContentCipherBuilderV2) isEncryptionVersionCompatible(version clientVersion) error {
	if version != v2ClientVersion {
		return errDeprecatedIncompatibleCipherBuilder
	}
	return nil
}

// isAWSFixture will return whether this type was constructed with an AWS provided CipherDataGenerator
func (builder gcmContentCipherBuilderV2) isAWSFixture() bool {
	v, ok := builder.generator.(awsFixture)
	return ok && v.isAWSFixture()
}

func newAESGCMContentCipher(cd CipherData) (ContentCipher, error) {
	cd.CEKAlgorithm = AESGCMNoPadding
	cd.TagLength = "128"

	cipher, err := newAESGCM(cd)
	if err != nil {
		return nil, err
	}

	return &aesGCMContentCipher{
		CipherData: cd,
		Cipher:     cipher,
	}, nil
}

// AESGCMContentCipher will use AES GCM for the main cipher.
type aesGCMContentCipher struct {
	CipherData CipherData
	Cipher     Cipher
}

// EncryptContents will generate a random key and iv and encrypt the data using cbc
func (cc *aesGCMContentCipher) EncryptContents(src io.Reader) (io.Reader, error) {
	return cc.Cipher.Encrypt(src), nil
}

// DecryptContents will use the symmetric key provider to instantiate a new GCM cipher.
// We grab a decrypt reader from gcm and wrap it in a CryptoReadCloser. The only error
// expected here is when the key or iv is of invalid length.
func (cc *aesGCMContentCipher) DecryptContents(src io.ReadCloser) (io.ReadCloser, error) {
	reader := cc.Cipher.Decrypt(src)
	return &CryptoReadCloser{Body: src, Decrypter: reader}, nil
}

// GetCipherData returns cipher data
func (cc aesGCMContentCipher) GetCipherData() CipherData {
	return cc.CipherData
}

// assert ContentCipherBuilder implementations
var (
	_ ContentCipherBuilder = (*gcmContentCipherBuilder)(nil)
	_ ContentCipherBuilder = (*gcmContentCipherBuilderV2)(nil)
)

// assert ContentCipherBuilderWithContext implementations
var (
	_ ContentCipherBuilderWithContext = (*gcmContentCipherBuilder)(nil)
	_ ContentCipherBuilderWithContext = (*gcmContentCipherBuilderV2)(nil)
)

// assert ContentCipher implementations
var (
	_ ContentCipher = (*aesGCMContentCipher)(nil)
)

// assert awsFixture implementations
var (
	_ awsFixture = (*gcmContentCipherBuilderV2)(nil)
)
