package s3

import (
	"bytes"
	"fmt"
	"io"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/porter-dev/porter/internal/encryption"
	"github.com/porter-dev/porter/internal/models"
	"github.com/porter-dev/porter/provisioner/integrations/storage"
)

type S3StorageClient struct {
	client        *s3.S3
	bucket        string
	encryptionKey *[32]byte
}
type S3Options struct {
	AWSRegion      string
	AWSAccessKeyID string
	AWSSecretKey   string
	AWSBucketName  string
	EncryptionKey  *[32]byte
}

func NewS3StorageClient(opts *S3Options) (*S3StorageClient, error) {
	var sess *session.Session
	var err error

	awsConf := &aws.Config{
		Credentials: credentials.NewStaticCredentials(
			opts.AWSAccessKeyID,
			opts.AWSSecretKey,
			"",
		),
		Region: &opts.AWSRegion,
	}

	sess, err = session.NewSessionWithOptions(session.Options{
		SharedConfigState: session.SharedConfigEnable,
		Config:            *awsConf,
	})

	if err != nil {
		return nil, fmt.Errorf("cannot create AWS session: %v", err)
	}

	return &S3StorageClient{
		bucket:        opts.AWSBucketName,
		encryptionKey: opts.EncryptionKey,
		client:        s3.New(sess),
	}, nil
}

func (s *S3StorageClient) WriteFile(infra *models.Infra, name string, fileBytes []byte, shouldEncrypt bool) error {
	body := fileBytes
	var err error
	if shouldEncrypt {
		body, err = encryption.Encrypt(fileBytes, s.encryptionKey)
		if err != nil {
			return err
		}
	}

	_, err = s.client.PutObject(&s3.PutObjectInput{
		Body:   aws.ReadSeekCloser(bytes.NewReader(body)),
		Bucket: &s.bucket,
		Key:    aws.String(getKeyFromInfra(infra, name)),
	})
	return err
}

func (s *S3StorageClient) WriteFileWithKey(fileBytes []byte, shouldEncrypt bool, key string) error {
	body := fileBytes
	var err error
	if shouldEncrypt {
		body, err = encryption.Encrypt(fileBytes, s.encryptionKey)
		if err != nil {
			return err
		}
	}

	_, err = s.client.PutObject(&s3.PutObjectInput{
		Body:   aws.ReadSeekCloser(bytes.NewReader(body)),
		Bucket: &s.bucket,
		Key:    aws.String(key),
	})
	return err
}

func (s *S3StorageClient) ReadFile(infra *models.Infra, name string, shouldDecrypt bool) ([]byte, error) {
	output, err := s.client.GetObject(&s3.GetObjectInput{
		Bucket: &s.bucket,
		Key:    aws.String(getKeyFromInfra(infra, name)),
	})
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case s3.ErrCodeNoSuchKey:
				return nil, storage.FileDoesNotExist
			default:
				return nil, err
			}
		}

		return nil, err
	}

	if shouldDecrypt {
		var encryptedData bytes.Buffer
		_, err = encryptedData.ReadFrom(output.Body)
		if err != nil {
			return nil, err
		}
		data, err := encryption.Decrypt(encryptedData.Bytes(), s.encryptionKey)
		if err != nil {
			return nil, err
		}
		return data, nil
	} else {
		return io.ReadAll(output.Body)
	}
}

func (s *S3StorageClient) DeleteFile(infra *models.Infra, name string) error {
	_, err := s.client.DeleteObject(&s3.DeleteObjectInput{
		Bucket: &s.bucket,
		Key:    aws.String(getKeyFromInfra(infra, name)),
	})
	if err != nil {
		return err
	}
	return nil
}

func getKeyFromInfra(infra *models.Infra, name string) string {
	return fmt.Sprintf("%s/%s", infra.GetUniqueName(), name)
}
