package gcp

import (
	"context"
	"fmt"
	"strings"

	compute "cloud.google.com/go/compute/apiv1"
	"github.com/osbuild/osbuild-composer/internal/common"
	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
	computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
)

// Guest OS Features for RHEL8 images
var GuestOsFeaturesRHEL8 []*computepb.GuestOsFeature = []*computepb.GuestOsFeature{
	{Type: common.StringToPtr(computepb.GuestOsFeature_UEFI_COMPATIBLE.String())},
	{Type: common.StringToPtr(computepb.GuestOsFeature_VIRTIO_SCSI_MULTIQUEUE.String())},
	{Type: common.StringToPtr(computepb.GuestOsFeature_SEV_CAPABLE.String())},
}

// Guest OS Features for RHEL9 images
var GuestOsFeaturesRHEL9 []*computepb.GuestOsFeature = []*computepb.GuestOsFeature{
	{Type: common.StringToPtr(computepb.GuestOsFeature_UEFI_COMPATIBLE.String())},
	{Type: common.StringToPtr(computepb.GuestOsFeature_VIRTIO_SCSI_MULTIQUEUE.String())},
	{Type: common.StringToPtr(computepb.GuestOsFeature_SEV_CAPABLE.String())},
	{Type: common.StringToPtr(computepb.GuestOsFeature_GVNIC.String())},
}

// GuestOsFeaturesByDistro returns the the list of Guest OS Features, which
// should be used when importing an image of the specified distribution.
//
// In case the provided distribution does not have any specific Guest OS
// Features list defined, nil is returned.
func GuestOsFeaturesByDistro(distroName string) []*computepb.GuestOsFeature {
	switch {
	case strings.HasPrefix(distroName, "centos-8"):
		fallthrough
	case strings.HasPrefix(distroName, "rhel-8"):
		return GuestOsFeaturesRHEL8

	case strings.HasPrefix(distroName, "centos-9"):
		fallthrough
	case strings.HasPrefix(distroName, "rhel-9"):
		return GuestOsFeaturesRHEL9

	default:
		return nil
	}
}

// ComputeImageInsert imports a previously uploaded archive with raw image into Compute Engine.
//
// The image must be RAW image named 'disk.raw' inside a gzip-ed tarball.
//
// To delete the Storage object (image) used for the image import, use StorageObjectDelete().
//
// bucket - Google storage bucket name with the uploaded image archive
// object - Google storage object name of the uploaded image
// imageName - Desired image name after the import. This must be unique within the whole project.
// regions - A list of valid Google Storage regions where the resulting image should be located.
//           It is possible to specify multiple regions. Also multi and dual regions are allowed.
//           If not provided, the region of the used Storage object is used.
//           See: https://cloud.google.com/storage/docs/locations
// guestOsFeatures - A list of features supported by the Guest OS on the imported image.
//
// Uses:
//	- Compute Engine API
func (g *GCP) ComputeImageInsert(
	ctx context.Context,
	bucket, object, imageName string,
	regions []string,
	guestOsFeatures []*computepb.GuestOsFeature) (*computepb.Image, error) {
	imagesClient, err := compute.NewImagesRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return nil, fmt.Errorf("failed to get Compute Engine Images client: %v", err)
	}
	defer imagesClient.Close()

	operationsClient, err := compute.NewGlobalOperationsRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return nil, fmt.Errorf("failed to get Compute Engine Operations client: %v", err)
	}
	defer operationsClient.Close()

	imgInsertReq := &computepb.InsertImageRequest{
		Project: g.GetProjectID(),
		ImageResource: &computepb.Image{
			Name:             &imageName,
			StorageLocations: regions,
			GuestOsFeatures:  guestOsFeatures,
			RawDisk: &computepb.RawDisk{
				ContainerType: common.StringToPtr(computepb.RawDisk_TAR.String()),
				Source:        common.StringToPtr(fmt.Sprintf("https://storage.googleapis.com/%s/%s", bucket, object)),
			},
		},
	}

	operation, err := imagesClient.Insert(ctx, imgInsertReq)
	if err != nil {
		return nil, fmt.Errorf("failed to insert provided image into GCE: %v", err)
	}

	// wait for the operation to finish
	var operationResource *computepb.Operation
	for {
		waitOperationReq := &computepb.WaitGlobalOperationRequest{
			Operation: operation.Proto().GetName(),
			Project:   g.GetProjectID(),
		}

		operationResource, err = operationsClient.Wait(ctx, waitOperationReq)
		if err != nil {
			return nil, fmt.Errorf("failed to wait for an Image Import operation: %v", err)
		}

		// The operation finished
		if operationResource.GetStatus() != computepb.Operation_RUNNING && operationResource.GetStatus() != computepb.Operation_PENDING {
			break
		}
	}

	// If the operation failed, the HttpErrorStatusCode is set to a non-zero value
	if operationStatusCode := operationResource.GetHttpErrorStatusCode(); operationStatusCode != 0 {
		operationErrorMsg := operationResource.GetHttpErrorMessage()
		operationErrors := operationResource.GetError().GetErrors()
		return nil, fmt.Errorf("failed to insert image into GCE. HTTPErrorCode:%d HTTPErrorMsg:%v Errors:%v", operationStatusCode, operationErrorMsg, operationErrors)
	}

	getImageReq := &computepb.GetImageRequest{
		Image:   imageName,
		Project: g.GetProjectID(),
	}

	image, err := imagesClient.Get(ctx, getImageReq)
	if err != nil {
		return nil, fmt.Errorf("failed to get information about the imported Image: %v", err)
	}

	return image, nil
}

// ComputeImageURL returns an image's URL to Google Cloud Console. The method does
// not check at all, if the image actually exists or not.
func (g *GCP) ComputeImageURL(imageName string) string {
	return fmt.Sprintf("https://console.cloud.google.com/compute/imagesDetail/projects/%s/global/images/%s", g.GetProjectID(), imageName)
}

// ComputeImageShare shares the specified Compute Engine image with list of accounts.
//
// "shareWith" is a list of accounts to share the image with. Items can be one
// of the following options:
//
// - `user:{emailid}`: An email address that represents a specific
//	 Google account. For example, `alice@example.com`.
//
// - `serviceAccount:{emailid}`: An email address that represents a
//   service account. For example, `my-other-app@appspot.gserviceaccount.com`.
//
// - `group:{emailid}`: An email address that represents a Google group.
//   For example, `admins@example.com`.
//
// - `domain:{domain}`: The G Suite domain (primary) that represents all
//   the users of that domain. For example, `google.com` or `example.com`.
//
// Uses:
//	- Compute Engine API
func (g *GCP) ComputeImageShare(ctx context.Context, imageName string, shareWith []string) error {
	imagesClient, err := compute.NewImagesRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return fmt.Errorf("failed to get Compute Engine Images client: %v", err)
	}
	defer imagesClient.Close()

	// Standard role to enable account to view and use a specific Image
	imageDesiredRole := "roles/compute.imageUser"

	// Get the current Policy set on the Image
	getIamPolicyReq := &computepb.GetIamPolicyImageRequest{
		Project:  g.GetProjectID(),
		Resource: imageName,
	}
	policy, err := imagesClient.GetIamPolicy(ctx, getIamPolicyReq)
	if err != nil {
		return fmt.Errorf("failed to get image's policy: %v", err)
	}

	// Add new members, who can use the image
	// Completely override the old policy
	userBinding := &computepb.Binding{
		Members: shareWith,
		Role:    common.StringToPtr(imageDesiredRole),
	}
	newPolicy := &computepb.Policy{
		Bindings: []*computepb.Binding{userBinding},
		Etag:     policy.Etag,
	}
	setIamPolicyReq := &computepb.SetIamPolicyImageRequest{
		Project:  g.GetProjectID(),
		Resource: imageName,
		GlobalSetPolicyRequestResource: &computepb.GlobalSetPolicyRequest{
			Policy: newPolicy,
		},
	}
	_, err = imagesClient.SetIamPolicy(ctx, setIamPolicyReq)
	if err != nil {
		return fmt.Errorf("failed to set new image policy: %v", err)
	}

	// Users won't see the shared image in their images.list requests, unless
	// they are also granted a specific "imagesList" role on the project. If you
	// don't need users to be able to view the list of shared images, this
	// step can be skipped.
	//
	// Downside of granting the "imagesList" role to a project is that the user
	// will be able to list all available images in the project, even those that
	// they can't use because of insufficient permissions.
	//
	// Even without the ability to view / list shared images, the user can still
	// create a Compute Engine instance using the image via API or 'gcloud' tool.
	//
	// Custom role to enable account to only list images in the project.
	// Without this role, the account won't be able to list and see the image
	// in the GCP Web UI.

	// For now, the decision is that the account should not get any role to the
	// project, where the image has been imported.

	return nil
}

// ComputeImageDelete deletes a Compute Engine image with the given name. If the
// image existed and was successfully deleted, no error is returned.
//
// Uses:
//	- Compute Engine API
func (g *GCP) ComputeImageDelete(ctx context.Context, name string) error {
	imagesClient, err := compute.NewImagesRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return fmt.Errorf("failed to get Compute Engine Images client: %v", err)
	}
	defer imagesClient.Close()

	req := &computepb.DeleteImageRequest{
		Project: g.GetProjectID(),
		Image:   name,
	}
	_, err = imagesClient.Delete(ctx, req)

	return err
}

// ComputeExecuteFunctionForImages will pass all the compute images in the account to a function,
// which is able to iterate over the images. Useful if something needs to be execute for each image.
// Uses:
//	- Compute Engine API
func (g *GCP) ComputeExecuteFunctionForImages(ctx context.Context, f func(*compute.ImageIterator) error) error {
	imagesClient, err := compute.NewImagesRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return fmt.Errorf("failed to get Compute Engine Images client: %v", err)
	}
	defer imagesClient.Close()

	req := &computepb.ListImagesRequest{
		Project: g.GetProjectID(),
	}
	imagesIterator := imagesClient.List(ctx, req)
	return f(imagesIterator)
}

// ComputeInstanceDelete deletes a Compute Engine instance with the given name and
// running in the given zone. If the instance existed and was successfully deleted,
// no error is returned.
//
// Uses:
//	- Compute Engine API
func (g *GCP) ComputeInstanceDelete(ctx context.Context, zone, instance string) error {
	instancesClient, err := compute.NewInstancesRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return fmt.Errorf("failed to get Compute Engine Instances client: %v", err)
	}
	defer instancesClient.Close()

	req := &computepb.DeleteInstanceRequest{
		Project:  g.GetProjectID(),
		Zone:     zone,
		Instance: instance,
	}
	_, err = instancesClient.Delete(ctx, req)

	return err
}

// ComputeInstanceGet fetches a Compute Engine instance information. If fetching the information
// was successful, it is returned to the caller, otherwise <nil> is returned with a proper error.
//
// Uses:
//	- Compute Engine API
func (g *GCP) ComputeInstanceGet(ctx context.Context, zone, instance string) (*computepb.Instance, error) {
	instancesClient, err := compute.NewInstancesRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return nil, fmt.Errorf("failed to get Compute Engine Instances client: %v", err)
	}
	defer instancesClient.Close()

	req := &computepb.GetInstanceRequest{
		Project:  g.GetProjectID(),
		Instance: instance,
		Zone:     zone,
	}
	resp, err := instancesClient.Get(ctx, req)

	return resp, err
}

// ComputeDiskDelete deletes a Compute Engine disk with the given name and
// running in the given zone. If the disk existed and was successfully deleted,
// no error is returned.
//
// Uses:
//	- Compute Engine API
func (g *GCP) ComputeDiskDelete(ctx context.Context, zone, disk string) error {
	disksClient, err := compute.NewDisksRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return fmt.Errorf("failed to get Compute Engine Disks client: %v", err)
	}
	defer disksClient.Close()

	req := &computepb.DeleteDiskRequest{
		Project: g.GetProjectID(),
		Disk:    disk,
		Zone:    zone,
	}
	_, err = disksClient.Delete(ctx, req)

	return err
}

// ComputeUpZonesInRegion returns list of zones within the given GCE Region, which are "UP".
//
// Uses:
//  - Compute Engine API
func (g *GCP) ComputeUpZonesInRegion(ctx context.Context, region *computepb.Region) ([]*computepb.Zone, error) {
	var zones []*computepb.Zone

	zonesClient, err := compute.NewZonesRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return nil, fmt.Errorf("failed to get Compute Engine Zones client: %v", err)
	}
	defer zonesClient.Close()

	for _, zoneURL := range region.Zones {
		// zone URL example - "https://www.googleapis.com/compute/v1/projects/<PROJECT_ID>/zones/us-central1-a"
		zoneNameSs := strings.Split(zoneURL, "/")
		zoneName := zoneNameSs[len(zoneNameSs)-1]

		getZoneReq := &computepb.GetZoneRequest{
			Project: g.GetProjectID(),
			Zone:    zoneName,
		}
		zone, err := zonesClient.Get(ctx, getZoneReq)
		if err != nil {
			return nil, fmt.Errorf("failed to get information about Compute Engine zone '%s': %v", zoneName, err)
		}

		// Make sure to return only Zones, which can be used
		if zone.GetStatus() == computepb.Zone_UP.String() {
			zones = append(zones, zone)
		}
	}

	return zones, nil
}

// ComputeRegionsList returns list of GCE regions based on the provided filter.
//
// Uses:
//  - Compute Engine API
func (g *GCP) ComputeRegionsList(ctx context.Context, filter string) ([]*computepb.Region, error) {
	var regions []*computepb.Region

	regionsClient, err := compute.NewRegionsRESTClient(ctx, option.WithCredentials(g.creds))
	if err != nil {
		return nil, fmt.Errorf("failed to get GCE regions client: %v", err)
	}
	defer regionsClient.Close()

	listRegionsReq := &computepb.ListRegionsRequest{
		Project: g.GetProjectID(),
		Filter:  common.StringToPtr(filter),
	}
	regionsIter := regionsClient.List(ctx, listRegionsReq)

	for {
		resp, err := regionsIter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("error while iterating over GCE regions: %v", err)
		}

		regions = append(regions, resp)
	}

	return regions, nil
}
