package weldr

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"strings"
	"time"

	"github.com/osbuild/osbuild-composer/internal/common"
	"github.com/osbuild/osbuild-composer/internal/distro"
	"github.com/sirupsen/logrus"

	"github.com/google/uuid"
	"github.com/osbuild/osbuild-composer/internal/target"
)

type uploadResponse struct {
	UUID         uuid.UUID              `json:"uuid"`
	Status       common.ImageBuildState `json:"status"`
	ProviderName string                 `json:"provider_name"`
	ImageName    string                 `json:"image_name"`
	CreationTime float64                `json:"creation_time"`
	Settings     uploadSettings         `json:"settings"`
}

type uploadSettings interface {
	isUploadSettings()
}

type awsUploadSettings struct {
	Region          string `json:"region"`
	AccessKeyID     string `json:"accessKeyID,omitempty"`
	SecretAccessKey string `json:"secretAccessKey,omitempty"`
	SessionToken    string `json:"sessionToken,omitempty"`
	Bucket          string `json:"bucket"`
	Key             string `json:"key"`
}

func (awsUploadSettings) isUploadSettings() {}

type awsS3UploadSettings struct {
	Region              string `json:"region"`
	AccessKeyID         string `json:"accessKeyID,omitempty"`
	SecretAccessKey     string `json:"secretAccessKey,omitempty"`
	SessionToken        string `json:"sessionToken,omitempty"`
	Bucket              string `json:"bucket"`
	Key                 string `json:"key"`
	Endpoint            string `json:"endpoint"`
	CABundle            string `json:"ca_bundle"`
	SkipSSLVerification bool   `json:"skip_ssl_verification"`
}

func (awsS3UploadSettings) isUploadSettings() {}

type azureUploadSettings struct {
	StorageAccount   string `json:"storageAccount,omitempty"`
	StorageAccessKey string `json:"storageAccessKey,omitempty"`
	Container        string `json:"container"`
}

func (azureUploadSettings) isUploadSettings() {}

type gcpUploadSettings struct {
	Filename string `json:"filename"`
	Region   string `json:"region"`
	Bucket   string `json:"bucket"`
	Object   string `json:"object"`

	// base64 encoded GCP credentials JSON file
	Credentials string `json:"credentials,omitempty"`
}

func (gcpUploadSettings) isUploadSettings() {}

type vmwareUploadSettings struct {
	Host       string `json:"host"`
	Username   string `json:"username"`
	Password   string `json:"password"`
	Datacenter string `json:"datacenter"`
	Cluster    string `json:"cluster"`
	Datastore  string `json:"datastore"`
}

func (vmwareUploadSettings) isUploadSettings() {}

type ociUploadSettings struct {
	Tenancy     string `json:"tenancy"`
	Region      string `json:"region"`
	User        string `json:"user"`
	Bucket      string `json:"bucket"`
	Namespace   string `json:"namespace"`
	PrivateKey  string `json:"private_key"`
	Fingerprint string `json:"fingerprint"`
	Compartment string `json:"compartment"`
}

func (ociUploadSettings) isUploadSettings() {}

type containerUploadSettings struct {
	Username string `json:"username,omitempty"`
	Password string `json:"password,omitempty"`

	TlsVerify *bool `json:"tls_verify,omitempty"`
}

func (containerUploadSettings) isUploadSettings() {}

type uploadRequest struct {
	Provider  string         `json:"provider"`
	ImageName string         `json:"image_name"`
	Settings  uploadSettings `json:"settings"`
}

type rawUploadRequest struct {
	Provider  string          `json:"provider"`
	ImageName string          `json:"image_name"`
	Settings  json.RawMessage `json:"settings"`
}

func (u *uploadRequest) UnmarshalJSON(data []byte) error {
	var rawUploadRequest rawUploadRequest
	err := json.Unmarshal(data, &rawUploadRequest)
	if err != nil {
		return err
	}

	var settings uploadSettings
	switch rawUploadRequest.Provider {
	case "azure":
		settings = new(azureUploadSettings)
	case "aws":
		settings = new(awsUploadSettings)
	case "aws.s3":
		settings = new(awsS3UploadSettings)
	case "gcp":
		settings = new(gcpUploadSettings)
	case "vmware":
		settings = new(vmwareUploadSettings)
	case "oci":
		settings = new(ociUploadSettings)
	case "generic.s3":
		// While the API still accepts provider type "generic.s3", the request is handled
		// in the same way as for a request with provider type "aws.s3"
		settings = new(awsS3UploadSettings)
	case "container":
		settings = new(containerUploadSettings)
	default:
		return errors.New("unexpected provider name")
	}
	err = json.Unmarshal(rawUploadRequest.Settings, settings)
	if err != nil {
		return err
	}

	u.Provider = rawUploadRequest.Provider
	u.ImageName = rawUploadRequest.ImageName
	u.Settings = settings

	return err
}

// Converts a `Target` to a serializable `uploadResponse`.
//
// This ignore the status in `targets`, because that's never set correctly.
// Instead, it sets each target's status to the ImageBuildState equivalent of
// `state`.
//
// This also ignores any sensitive data passed into targets. Access keys may
// be passed as input to composer, but should not be possible to be queried.
func targetsToUploadResponses(targets []*target.Target, state ComposeState) []uploadResponse {
	var uploads []uploadResponse
	for _, t := range targets {
		upload := uploadResponse{
			UUID:         t.Uuid,
			ImageName:    t.ImageName,
			CreationTime: float64(t.Created.UnixNano()) / 1000000000,
		}

		switch state {
		case ComposeWaiting:
			upload.Status = common.IBWaiting
		case ComposeRunning:
			upload.Status = common.IBRunning
		case ComposeFinished:
			upload.Status = common.IBFinished
		case ComposeFailed:
			upload.Status = common.IBFailed
		}

		switch options := t.Options.(type) {
		case *target.AWSTargetOptions:
			upload.ProviderName = "aws"
			upload.Settings = &awsUploadSettings{
				Region: options.Region,
				Bucket: options.Bucket,
				Key:    options.Key,
				// AccessKeyID and SecretAccessKey are intentionally not included.
			}
			uploads = append(uploads, upload)
		case *target.AzureTargetOptions:
			upload.ProviderName = "azure"
			upload.Settings = &azureUploadSettings{
				Container: options.Container,
				// StorageAccount and StorageAccessKey are intentionally not included.
			}
			uploads = append(uploads, upload)
		case *target.GCPTargetOptions:
			upload.ProviderName = "gcp"
			upload.Settings = &gcpUploadSettings{
				Filename: options.Filename,
				Region:   options.Region,
				Bucket:   options.Bucket,
				Object:   options.Object,
				// Credentials are intentionally not included.
			}
			uploads = append(uploads, upload)
		case *target.VMWareTargetOptions:
			upload.ProviderName = "vmware"
			upload.Settings = &vmwareUploadSettings{
				Host:       options.Host,
				Cluster:    options.Cluster,
				Datacenter: options.Datacenter,
				Datastore:  options.Datastore,
				// Username and Password are intentionally not included.
			}
			uploads = append(uploads, upload)
		case *target.AWSS3TargetOptions:
			upload.ProviderName = "aws.s3"
			upload.Settings = &awsS3UploadSettings{
				Region: options.Region,
				Bucket: options.Bucket,
				Key:    options.Key,
				// AccessKeyID and SecretAccessKey are intentionally not included.
			}
			uploads = append(uploads, upload)
		}
	}

	return uploads
}

func uploadRequestToTarget(u uploadRequest, imageType distro.ImageType) *target.Target {
	var t target.Target

	t.Uuid = uuid.New()
	t.ImageName = u.ImageName
	t.Status = common.IBWaiting
	t.Created = time.Now()

	switch options := u.Settings.(type) {
	case *awsUploadSettings:
		t.Name = "org.osbuild.aws"
		t.Options = &target.AWSTargetOptions{
			Filename:        imageType.Filename(),
			Region:          options.Region,
			AccessKeyID:     options.AccessKeyID,
			SecretAccessKey: options.SecretAccessKey,
			SessionToken:    options.SessionToken,
			Bucket:          options.Bucket,
			Key:             options.Key,
		}
	case *awsS3UploadSettings:
		t.Name = "org.osbuild.aws.s3"
		t.Options = &target.AWSS3TargetOptions{
			Filename:            imageType.Filename(),
			Region:              options.Region,
			AccessKeyID:         options.AccessKeyID,
			SecretAccessKey:     options.SecretAccessKey,
			SessionToken:        options.SessionToken,
			Bucket:              options.Bucket,
			Key:                 options.Key,
			Endpoint:            options.Endpoint,
			CABundle:            options.CABundle,
			SkipSSLVerification: options.SkipSSLVerification,
		}
	case *azureUploadSettings:
		t.Name = "org.osbuild.azure"
		t.Options = &target.AzureTargetOptions{
			Filename:         imageType.Filename(),
			StorageAccount:   options.StorageAccount,
			StorageAccessKey: options.StorageAccessKey,
			Container:        options.Container,
		}
	case *gcpUploadSettings:
		t.Name = "org.osbuild.gcp"

		var gcpCredentials []byte
		var err error
		if options.Credentials != "" {
			gcpCredentials, err = base64.StdEncoding.DecodeString(options.Credentials)
			if err != nil {
				panic(err)
			}
		}

		// The uploaded image object name must have 'tar.gz' suffix to be imported
		objectName := options.Object
		if !strings.HasSuffix(objectName, ".tar.gz") {
			objectName = objectName + ".tar.gz"
			logrus.Infof("[GCP] object name must end with '.tar.gz', using %q as the object name", objectName)
		}

		t.Options = &target.GCPTargetOptions{
			Filename:    imageType.Filename(),
			Region:      options.Region,
			Os:          imageType.Arch().Distro().Name(),
			Bucket:      options.Bucket,
			Object:      objectName,
			Credentials: gcpCredentials,
		}
	case *vmwareUploadSettings:
		t.Name = "org.osbuild.vmware"
		t.Options = &target.VMWareTargetOptions{
			Filename:   imageType.Filename(),
			Username:   options.Username,
			Password:   options.Password,
			Host:       options.Host,
			Cluster:    options.Cluster,
			Datacenter: options.Datacenter,
			Datastore:  options.Datastore,
		}
	case *ociUploadSettings:
		t.Name = "org.osbuild.oci"
		t.Options = &target.OCITargetOptions{
			User:        options.User,
			Tenancy:     options.Tenancy,
			Region:      options.Region,
			FileName:    imageType.Filename(),
			PrivateKey:  options.PrivateKey,
			Fingerprint: options.Fingerprint,
			Bucket:      options.Bucket,
			Namespace:   options.Namespace,
			Compartment: options.Compartment,
		}
	case *containerUploadSettings:
		t.Name = "org.osbuild.container"
		t.Options = &target.ContainerTargetOptions{
			Username: options.Username,
			Password: options.Password,

			Filename:  imageType.Filename(),
			TlsVerify: options.TlsVerify,
		}
	}

	return &t
}
