package gcp

import (
	"context"
	// gcp uses MD5 hashes
	/* #nosec G501 */
	"crypto/md5"
	"fmt"
	"io"
	"os"

	"cloud.google.com/go/storage"
	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
)

const (
	// MetadataKeyImageName contains a key name used to store metadata on
	// a Storage object with the intended name of the image.
	// The metadata can be then used to associate the object with actual
	// image build using the image name.
	MetadataKeyImageName string = "osbuild-composer-image-name"
)

// StorageObjectUpload uploads an OS image to specified Cloud Storage bucket and object.
// The bucket must exist. MD5 sum of the image file and uploaded object is
// compared after the upload to verify the integrity of the uploaded image.
//
// The ObjectAttrs is returned if the object has been created.
//
// Uses:
//	- Storage API
func (g *GCP) StorageObjectUpload(ctx context.Context, filename, bucket, object string, metadata map[string]string) (*storage.ObjectAttrs, error) {
	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return nil, fmt.Errorf("failed to get Storage client: %v", err)
	}
	defer storageClient.Close()

	// Open the image file
	imageFile, err := os.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("cannot open the image: %v", err)
	}
	defer imageFile.Close()

	// Compute MD5 checksum of the image file for later verification
	// gcp uses MD5 hashes
	/* #nosec G401 */
	imageFileHash := md5.New()
	if _, err := io.Copy(imageFileHash, imageFile); err != nil {
		return nil, fmt.Errorf("cannot create md5 of the image: %v", err)
	}
	// Move the cursor of opened file back to the start
	if _, err := imageFile.Seek(0, 0); err != nil {
		return nil, fmt.Errorf("cannot seek the image: %v", err)
	}

	// Upload the image
	// The Bucket MUST exist and be of a STANDARD storage class
	obj := storageClient.Bucket(bucket).Object(object)
	wc := obj.NewWriter(ctx)

	// Uploaded data is rejected if its MD5 hash does not match the set value.
	wc.MD5 = imageFileHash.Sum(nil)

	if metadata != nil {
		wc.ObjectAttrs.Metadata = metadata
	}

	if _, err = io.Copy(wc, imageFile); err != nil {
		return nil, fmt.Errorf("uploading the image failed: %v", err)
	}

	// The object will not be available until Close has been called.
	if err := wc.Close(); err != nil {
		return nil, fmt.Errorf("Writer.Close: %v", err)
	}

	return wc.Attrs(), nil
}

// StorageObjectDelete deletes the given object from a bucket.
//
// Uses:
//	- Storage API
func (g *GCP) StorageObjectDelete(ctx context.Context, bucket, object string) error {
	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return fmt.Errorf("failed to get Storage client: %v", err)
	}
	defer storageClient.Close()

	objectHandle := storageClient.Bucket(bucket).Object(object)
	if err = objectHandle.Delete(ctx); err != nil {
		return fmt.Errorf("failed to delete image file object: %v", err)
	}

	return nil
}

// StorageListObjectsByMetadata searches specified Storage bucket for objects matching the provided
// metadata. The provided metadata is used for filtering the bucket's content. Therefore if the provided
// metadata is nil, then all objects present in the bucket will be returned.
//
// Matched objects are returned as a list of ObjectAttrs.
//
// Uses:
//	- Storage API
func (g *GCP) StorageListObjectsByMetadata(ctx context.Context, bucket string, metadata map[string]string) ([]*storage.ObjectAttrs, error) {
	var matchedObjectAttr []*storage.ObjectAttrs

	storageClient, err := storage.NewClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return nil, fmt.Errorf("failed to get Storage client: %v", err)
	}
	defer storageClient.Close()

	objects := storageClient.Bucket(bucket).Objects(ctx, nil)
	for {
		obj, err := objects.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("failure while iterating over bucket objects: %v", err)
		}

		// check if the object's Metadata match the provided values
		metadataMatch := true
		for key, value := range metadata {
			objMetadataValue, ok := obj.Metadata[key]
			if !ok {
				metadataMatch = false
				break
			}
			if objMetadataValue != value {
				metadataMatch = false
				break
			}
		}

		if metadataMatch {
			matchedObjectAttr = append(matchedObjectAttr, obj)
		}

	}

	return matchedObjectAttr, nil
}
