//go:generate mapstructure-to-hcl2 -type Config

package exoscaleimport

import (
	"context"
	"crypto/md5"
	"encoding/base64"
	"fmt"
	"io"
	"os"
	"path/filepath"

	"github.com/aws/aws-sdk-go/aws"
	"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/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/exoscale/egoscale"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/hashicorp/packer/builder/file"
	"github.com/hashicorp/packer/builder/qemu"
	"github.com/hashicorp/packer/packer-plugin-sdk/common"
	packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
	"github.com/hashicorp/packer/packer-plugin-sdk/template/config"
	"github.com/hashicorp/packer/post-processor/artifice"
	"github.com/hashicorp/packer/post-processor/exoscale-import/version"
)

var (
	defaultTemplateZone = "ch-gva-2"
	defaultAPIEndpoint  = "https://api.exoscale.com/compute"
	defaultSOSEndpoint  = "https://sos-" + defaultTemplateZone + ".exo.io"
)

type Config struct {
	common.PackerConfig `mapstructure:",squash"`
	SkipClean           bool `mapstructure:"skip_clean"`

	SOSEndpoint             string `mapstructure:"sos_endpoint"`
	APIEndpoint             string `mapstructure:"api_endpoint"`
	APIKey                  string `mapstructure:"api_key"`
	APISecret               string `mapstructure:"api_secret"`
	ImageBucket             string `mapstructure:"image_bucket"`
	TemplateZone            string `mapstructure:"template_zone"`
	TemplateName            string `mapstructure:"template_name"`
	TemplateDescription     string `mapstructure:"template_description"`
	TemplateUsername        string `mapstructure:"template_username"`
	TemplateDisablePassword bool   `mapstructure:"template_disable_password"`
	TemplateDisableSSHKey   bool   `mapstructure:"template_disable_sshkey"`
}

func init() {
	egoscale.UserAgent = "Packer-Exoscale/" + version.ExoscaleImportPluginVersion.FormattedVersion() + " " + egoscale.UserAgent
}

type PostProcessor struct {
	config Config
}

func (p *PostProcessor) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }

func (p *PostProcessor) Configure(raws ...interface{}) error {
	p.config.TemplateZone = defaultTemplateZone
	p.config.APIEndpoint = defaultAPIEndpoint
	p.config.SOSEndpoint = defaultSOSEndpoint

	if err := config.Decode(&p.config, nil, raws...); err != nil {
		return err
	}

	if p.config.APIKey == "" {
		p.config.APIKey = os.Getenv("EXOSCALE_API_KEY")
	}

	if p.config.APISecret == "" {
		p.config.APISecret = os.Getenv("EXOSCALE_API_SECRET")
	}

	requiredArgs := map[string]*string{
		"api_key":              &p.config.APIKey,
		"api_secret":           &p.config.APISecret,
		"api_endpoint":         &p.config.APIEndpoint,
		"sos_endpoint":         &p.config.SOSEndpoint,
		"image_bucket":         &p.config.ImageBucket,
		"template_zone":        &p.config.TemplateZone,
		"template_name":        &p.config.TemplateName,
		"template_description": &p.config.TemplateDescription,
	}

	errs := new(packersdk.MultiError)
	for k, v := range requiredArgs {
		if *v == "" {
			errs = packersdk.MultiErrorAppend(
				errs, fmt.Errorf("%s must be set", k))
		}
	}

	if len(errs.Errors) > 0 {
		return errs
	}

	packersdk.LogSecretFilter.Set(p.config.APIKey, p.config.APISecret)

	return nil
}

func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, a packersdk.Artifact) (packersdk.Artifact, bool, bool, error) {
	switch a.BuilderId() {
	case qemu.BuilderId, file.BuilderId, artifice.BuilderId:
		break

	default:
		err := fmt.Errorf(
			"Unknown artifact type: %s\nCan only import from QEMU/file builders and Artifice post-processor artifacts.",
			a.BuilderId())
		return nil, false, false, err
	}

	ui.Message("Uploading template image")
	url, md5sum, err := p.uploadImage(ctx, ui, a)
	if err != nil {
		return nil, false, false, fmt.Errorf("unable to upload image: %s", err)
	}

	ui.Message("Registering template")
	id, err := p.registerTemplate(ctx, ui, url, md5sum)
	if err != nil {
		return nil, false, false, fmt.Errorf("unable to register template: %s", err)
	}

	if !p.config.SkipClean {
		ui.Message("Deleting uploaded template image")
		if err = p.deleteImage(ctx, ui, a); err != nil {
			return nil, false, false, fmt.Errorf("unable to delete uploaded template image: %s", err)
		}
	}

	return &Artifact{id}, false, false, nil
}

func (p *PostProcessor) uploadImage(ctx context.Context, ui packersdk.Ui, a packersdk.Artifact) (string, string, error) {
	var (
		imageFile  = a.Files()[0]
		bucketFile = filepath.Base(imageFile)
	)

	f, err := os.Open(imageFile)
	if err != nil {
		return "", "", err
	}
	defer f.Close()

	fileInfo, err := f.Stat()
	if err != nil {
		return "", "", err
	}

	// For tracking image file upload progress
	pf := ui.TrackProgress(imageFile, 0, fileInfo.Size(), f)
	defer pf.Close()

	hash := md5.New()
	if _, err := io.Copy(hash, f); err != nil {
		return "", "", fmt.Errorf("image checksumming failed: %s", err)
	}
	if _, err := f.Seek(0, 0); err != nil {
		return "", "", err
	}

	sess := session.Must(session.NewSessionWithOptions(session.Options{Config: aws.Config{
		Region:      aws.String(p.config.TemplateZone),
		Endpoint:    aws.String(p.config.SOSEndpoint),
		Credentials: credentials.NewStaticCredentials(p.config.APIKey, p.config.APISecret, "")}}))

	uploader := s3manager.NewUploader(sess)
	output, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
		Body:       pf,
		Bucket:     aws.String(p.config.ImageBucket),
		Key:        aws.String(bucketFile),
		ContentMD5: aws.String(base64.StdEncoding.EncodeToString(hash.Sum(nil))),
		ACL:        aws.String("public-read"),
	})
	if err != nil {
		return "", "", err
	}

	return output.Location, fmt.Sprintf("%x", hash.Sum(nil)), nil
}

func (p *PostProcessor) deleteImage(ctx context.Context, ui packersdk.Ui, a packersdk.Artifact) error {
	var (
		imageFile  = a.Files()[0]
		bucketFile = filepath.Base(imageFile)
	)

	sess := session.Must(session.NewSessionWithOptions(session.Options{Config: aws.Config{
		Region:      aws.String(p.config.TemplateZone),
		Endpoint:    aws.String(p.config.SOSEndpoint),
		Credentials: credentials.NewStaticCredentials(p.config.APIKey, p.config.APISecret, "")}}))

	svc := s3.New(sess)
	if _, err := svc.DeleteObject(&s3.DeleteObjectInput{
		Bucket: aws.String(p.config.ImageBucket),
		Key:    aws.String(bucketFile),
	}); err != nil {
		return err
	}

	return nil
}

func (p *PostProcessor) registerTemplate(ctx context.Context, ui packersdk.Ui, url, md5sum string) (string, error) {
	var (
		passwordEnabled = !p.config.TemplateDisablePassword
		sshkeyEnabled   = !p.config.TemplateDisableSSHKey
		regErr          error
	)

	exo := egoscale.NewClient(p.config.APIEndpoint, p.config.APIKey, p.config.APISecret)
	exo.RetryStrategy = egoscale.FibonacciRetryStrategy

	zone := egoscale.Zone{Name: p.config.TemplateZone}
	if resp, err := exo.GetWithContext(ctx, &zone); err != nil {
		return "", fmt.Errorf("template zone lookup failed: %s", err)
	} else {
		zone.ID = resp.(*egoscale.Zone).ID
	}

	req := egoscale.RegisterCustomTemplate{
		URL:             url,
		ZoneID:          zone.ID,
		Name:            p.config.TemplateName,
		Displaytext:     p.config.TemplateDescription,
		PasswordEnabled: &passwordEnabled,
		SSHKeyEnabled:   &sshkeyEnabled,
		Details:         map[string]string{"username": p.config.TemplateUsername},
		Checksum:        md5sum,
	}

	res := make([]egoscale.Template, 0)

	exo.AsyncRequestWithContext(ctx, req, func(jobRes *egoscale.AsyncJobResult, err error) bool {
		if err != nil {
			regErr = fmt.Errorf("request failed: %s", err)
			return false
		} else if jobRes.JobStatus == egoscale.Pending {
			// Job is not completed yet
			ui.Message("template registration in progress")
			return true
		}

		if err := jobRes.Result(&res); err != nil {
			regErr = err
			return false
		}

		if len(res) != 1 {
			regErr = fmt.Errorf("unexpected response from API (expected 1 item, got %d)", len(res))
			return false
		}

		return false
	})
	if regErr != nil {
		return "", regErr
	}

	return res[0].ID.String(), nil
}
