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

package yandexexport

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/yandex-cloud/go-sdk/iamkey"

	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/hashicorp/packer/builder/yandex"
	"github.com/hashicorp/packer/common"
	"github.com/hashicorp/packer/helper/config"
	"github.com/hashicorp/packer/helper/multistep"
	"github.com/hashicorp/packer/packer"
	"github.com/hashicorp/packer/post-processor/artifice"
	"github.com/hashicorp/packer/template/interpolate"
)

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

	// Paths to Yandex Object Storage where exported image will be uploaded
	Paths []string `mapstructure:"paths" required:"true"`
	// The folder ID that will be used to launch a temporary instance.
	// Alternatively you may set value by environment variable YC_FOLDER_ID.
	FolderID string `mapstructure:"folder_id" required:"true"`
	// Service Account ID with proper permission to modify an instance, create and attach disk and
	// make upload to specific Yandex Object Storage paths
	ServiceAccountID string `mapstructure:"service_account_id" required:"true"`
	// The size of the disk in GB. This defaults to `100`, which is 100GB.
	DiskSizeGb int `mapstructure:"disk_size" required:"false"`
	// Specify disk type for the launched instance. Defaults to `network-ssd`.
	DiskType string `mapstructure:"disk_type" required:"false"`
	// Identifier of the hardware platform configuration for the instance. This defaults to `standard-v2`.
	PlatformID string `mapstructure:"platform_id" required:"false"`
	// The Yandex VPC subnet id to use for
	// the launched instance. Note, the zone of the subnet must match the
	// zone in which the VM is launched.
	SubnetID string `mapstructure:"subnet_id" required:"false"`
	// The name of the zone to launch the instance.  This defaults to `ru-central1-a`.
	Zone string `mapstructure:"zone" required:"false"`
	// OAuth token to use to authenticate to Yandex.Cloud. Alternatively you may set
	// value by environment variable YC_TOKEN.
	Token string `mapstructure:"token" required:"false"`
	// Path to file with Service Account key in json format. This
	// is an alternative method to authenticate to Yandex.Cloud. Alternatively you may set environment variable
	// YC_SERVICE_ACCOUNT_KEY_FILE.
	ServiceAccountKeyFile string `mapstructure:"service_account_key_file" required:"false"`

	ctx interpolate.Context
}

type PostProcessor struct {
	config Config
	runner multistep.Runner
}

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

func (p *PostProcessor) Configure(raws ...interface{}) error {
	err := config.Decode(&p.config, &config.DecodeOpts{
		Interpolate:        true,
		InterpolateContext: &p.config.ctx,
	}, raws...)
	if err != nil {
		return err
	}

	errs := new(packer.MultiError)

	if len(p.config.Paths) == 0 {
		errs = packer.MultiErrorAppend(
			errs, fmt.Errorf("paths must be specified"))
	}

	// provision config by OS environment variables
	if p.config.Token == "" {
		p.config.Token = os.Getenv("YC_TOKEN")
	}

	if p.config.ServiceAccountKeyFile == "" {
		p.config.ServiceAccountKeyFile = os.Getenv("YC_SERVICE_ACCOUNT_KEY_FILE")
	}

	if p.config.Token == "" && p.config.ServiceAccountKeyFile == "" {
		errs = packer.MultiErrorAppend(
			errs, fmt.Errorf("a token or service account key file must be specified"))
	}

	if p.config.Token != "" && p.config.ServiceAccountKeyFile != "" {
		errs = packer.MultiErrorAppend(
			errs, fmt.Errorf("one of token or service account key file must be specified, not both"))
	}

	if p.config.Token != "" {
		packer.LogSecretFilter.Set(p.config.Token)
	}

	if p.config.ServiceAccountKeyFile != "" {
		if _, err := iamkey.ReadFromJSONFile(p.config.ServiceAccountKeyFile); err != nil {
			errs = packer.MultiErrorAppend(
				errs, fmt.Errorf("fail to read service account key file: %s", err))
		}
	}

	if p.config.FolderID == "" {
		p.config.FolderID = os.Getenv("YC_FOLDER_ID")
	}

	// Set defaults.
	if p.config.DiskSizeGb == 0 {
		p.config.DiskSizeGb = 100
	}

	if p.config.DiskType == "" {
		p.config.DiskType = "network-ssd"
	}

	if p.config.PlatformID == "" {
		p.config.PlatformID = "standard-v2"
	}

	if p.config.Zone == "" {
		p.config.Zone = "ru-central1-a"
	}

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

	return nil
}

func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, bool, error) {
	switch artifact.BuilderId() {
	case yandex.BuilderID, artifice.BuilderId:
		break
	default:
		err := fmt.Errorf(
			"Unknown artifact type: %s\nCan only export from Yandex Cloud builder artifact or Artifice post-processor artifact.",
			artifact.BuilderId())
		return nil, false, false, err
	}

	builderID := artifact.State("ImageID").(string)

	ui.Say(fmt.Sprintf("Exporting image %v to destination: %v", builderID, p.config.Paths))

	// Set up exporter instance configuration.
	exporterName := fmt.Sprintf("%s-exporter", artifact.Id())
	exporterMetadata := map[string]string{
		"image_id":  builderID,
		"name":      exporterName,
		"paths":     strings.Join(p.config.Paths, " "),
		"user-data": CloudInitScript,
		"zone":      p.config.Zone,
	}

	yandexConfig := ycSaneDefaults()
	yandexConfig.Token = p.config.Token
	yandexConfig.ServiceAccountKeyFile = p.config.ServiceAccountKeyFile
	yandexConfig.DiskName = exporterName
	yandexConfig.InstanceName = exporterName
	yandexConfig.DiskSizeGb = p.config.DiskSizeGb
	yandexConfig.Metadata = exporterMetadata
	yandexConfig.SubnetID = p.config.SubnetID
	yandexConfig.FolderID = p.config.FolderID
	yandexConfig.Zone = p.config.Zone

	if p.config.ServiceAccountID != "" {
		yandexConfig.ServiceAccountID = p.config.ServiceAccountID
	}

	if p.config.PlatformID != "" {
		yandexConfig.PlatformID = p.config.PlatformID
	}

	driver, err := yandex.NewDriverYC(ui, &yandexConfig)
	if err != nil {
		return nil, false, false, err
	}

	// Set up the state.
	state := new(multistep.BasicStateBag)
	state.Put("config", &yandexConfig)
	state.Put("driver", driver)
	state.Put("sdk", driver.SDK())
	state.Put("ui", ui)

	// Build the steps.
	steps := []multistep.Step{
		&yandex.StepCreateSSHKey{
			Debug:        p.config.PackerDebug,
			DebugKeyPath: fmt.Sprintf("yc_pp_%s.pem", p.config.PackerBuildName),
		},
		&yandex.StepCreateInstance{
			Debug: p.config.PackerDebug,
		},
		new(yandex.StepWaitCloudInitScript),
		new(yandex.StepTeardownInstance),
	}

	// Run the steps.
	p.runner = common.NewRunner(steps, p.config.PackerConfig, ui)
	p.runner.Run(ctx, state)

	result := &Artifact{paths: p.config.Paths}

	return result, false, false, nil
}

func ycSaneDefaults() yandex.Config {
	return yandex.Config{
		DiskType:       "network-ssd",
		InstanceCores:  2,
		InstanceMemory: 2,
		Labels: map[string]string{
			"role":   "exporter",
			"target": "object-storage",
		},
		PlatformID:          "standard-v2",
		Preemptible:         true,
		SourceImageFamily:   "ubuntu-1604-lts",
		SourceImageFolderID: yandex.StandardImagesFolderID,
		UseIPv4Nat:          true,
		Zone:                "ru-central1-a",
		StateTimeout:        3 * time.Minute,
	}
}
