// Copyright IBM Corp. 2024, 2026
// SPDX-License-Identifier: BUSL-1.1

package packer

import (
	"context"
	"fmt"
	"log"
	"sync"

	hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
	"github.com/hashicorp/packer-plugin-sdk/common"
	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
	"github.com/hashicorp/packer-plugin-sdk/packerbuilderdata"
	"github.com/hashicorp/packer/version"
	"github.com/zclconf/go-cty/cty"
)

// A CoreBuild struct represents a single build job, the result of which should
// be a single machine image artifact. This artifact may be comprised of
// multiple files, of course, but it should be for only a single provider (such
// as VirtualBox, EC2, etc.).
type CoreBuild struct {
	BuildName string
	Type      string
	Builder   packersdk.Builder
	// BuilderConfig is the config for the builder.
	//
	// Is is deserialised directly from the JSON template,
	// and is only populated for legacy JSON templates.
	BuilderConfig interface{}
	// HCLConfig is the HCL config for the builder
	//
	// Its only use is for telemetry, since we use it to extract the
	// field names from it.
	HCLConfig          cty.Value
	BuilderType        string
	hooks              map[string][]packersdk.Hook
	Provisioners       []CoreBuildProvisioner
	PostProcessors     [][]CoreBuildPostProcessor
	CleanupProvisioner CoreBuildProvisioner
	TemplatePath       string
	Variables          map[string]string
	SensitiveVars      []string

	// Indicates whether the build is already initialized before calling Prepare(..)
	Prepared bool

	debug         bool
	force         bool
	onError       string
	l             sync.Mutex
	prepareCalled bool
	generatedVars []string

	SBOMs []SBOM
}

type SBOM struct {
	Name           string
	Format         hcpPackerModels.HashicorpCloudPacker20230101SbomFormat
	CompressedData []byte
}

type BuildMetadata struct {
	PackerVersion string
	Plugins       map[string]PluginDetails
	SBOMs         []SBOM
}

func (b *CoreBuild) getPluginsMetadata() map[string]PluginDetails {
	resp := map[string]PluginDetails{}

	builderPlugin, builderPluginOk := GlobalPluginsDetailsStore.GetBuilder(b.BuilderType)
	if builderPluginOk {
		resp[builderPlugin.Name] = builderPlugin
	}

	for _, pp := range b.PostProcessors {
		for _, p := range pp {
			postprocessorsPlugin, postprocessorsPluginOk := GlobalPluginsDetailsStore.GetPostProcessor(p.PType)
			if postprocessorsPluginOk {
				resp[postprocessorsPlugin.Name] = postprocessorsPlugin
			}
		}
	}

	for _, pv := range b.Provisioners {
		provisionerPlugin, provisionerPluginOk := GlobalPluginsDetailsStore.GetProvisioner(pv.PType)
		if provisionerPluginOk {
			resp[provisionerPlugin.Name] = provisionerPlugin
		}
	}

	return resp
}

func (b *CoreBuild) GetMetadata() BuildMetadata {
	metadata := BuildMetadata{
		PackerVersion: version.FormattedVersion(),
		Plugins:       b.getPluginsMetadata(),
		SBOMs:         b.SBOMs,
	}
	return metadata
}

// CoreBuildPostProcessor Keeps track of the post-processor and the
// configuration of the post-processor used within a build.
type CoreBuildPostProcessor struct {
	PostProcessor packersdk.PostProcessor
	PType         string
	PName         string
	// HCLConfig is the HCL config for the post-processor
	//
	// Its only use is for telemetry, since we use it to extract the
	// field names from it.
	HCLConfig cty.Value
	// config is JSON-specific, the configuration for the post-processor
	// deserialised directly from the JSON template
	config            map[string]interface{}
	KeepInputArtifact *bool
}

// CoreBuildProvisioner keeps track of the provisioner and the configuration of
// the provisioner within the build.
type CoreBuildProvisioner struct {
	PType       string
	PName       string
	Provisioner packersdk.Provisioner
	// HCLConfig is the HCL config for the provisioner
	//
	// Its only use is for telemetry, since we use it to extract the
	// field names from it.
	HCLConfig cty.Value
	// config is JSON-specific, and is the configuration of the
	// provisioner, with overrides
	config []interface{}
}

// Returns the name of the build.
func (b *CoreBuild) Name() string {
	if b.BuildName != "" {
		return b.BuildName + "." + b.Type
	}
	return b.Type
}

func (b *CoreBuild) packerConfig() map[string]interface{} {
	return map[string]interface{}{
		common.BuildNameConfigKey:     b.Type,
		common.BuilderTypeConfigKey:   b.BuilderType,
		common.CoreVersionConfigKey:   version.FormattedVersion(),
		common.DebugConfigKey:         b.debug,
		common.ForceConfigKey:         b.force,
		common.OnErrorConfigKey:       b.onError,
		common.TemplatePathKey:        b.TemplatePath,
		common.UserVariablesConfigKey: b.Variables,
		common.SensitiveVarsConfigKey: b.SensitiveVars,
	}
}

func (b *CoreBuild) prepareProvisioners(provisioners []CoreBuildProvisioner, packerConfig map[string]interface{}, generatedVars []string) error {
	generatedPlaceholderMap := placeholderDataFromGeneratedVars(generatedVars)

	for _, coreProv := range provisioners {
		configs := make([]interface{}, len(coreProv.config), len(coreProv.config)+2)
		copy(configs, coreProv.config)
		configs = append(configs, packerConfig, generatedPlaceholderMap)

		if err := coreProv.Provisioner.Prepare(configs...); err != nil {
			return err
		}
	}

	return nil
}

func placeholderDataFromGeneratedVars(generatedVars []string) map[string]string {
	generatedPlaceholderMap := BasicPlaceholderData()
	for _, k := range generatedVars {
		generatedPlaceholderMap[k] = fmt.Sprintf("Build_%s. "+
			packerbuilderdata.PlaceholderMsg, k)
	}

	return generatedPlaceholderMap
}

// SetGeneratedVars stores the builder-generated variables from the initial
// builder preparation so late-injected provisioners can reuse them without
// invoking Builder.Prepare again.
func (b *CoreBuild) SetGeneratedVars(generatedVars []string) {
	b.generatedVars = append([]string(nil), generatedVars...)
}

// PrepareProvisioners prepares provisioners injected after the build itself has already been prepared.
func (b *CoreBuild) PrepareProvisioners(provisioners ...CoreBuildProvisioner) error {
	if !b.prepareCalled {
		return fmt.Errorf("Prepare must be called first")
	}

	packerConfig := b.packerConfig()
	return b.prepareProvisioners(provisioners, packerConfig, b.generatedVars)
}

// Prepare prepares the build by doing some initialization for the builder
// and any hooks. This _must_ be called prior to Run. The parameter is the
// overrides for the variables within the template (if any).
func (b *CoreBuild) Prepare() (warn []string, err error) {
	// For HCL2 templates, the builder and hooks are initialized when the
	// template is parsed. Calling Prepare(...) is not necessary
	if b.Prepared {
		b.prepareCalled = true
		return
	}

	b.l.Lock()
	defer b.l.Unlock()

	if b.prepareCalled {
		panic("prepare already called")
	}

	// Templates loaded from HCL2 will never get here. TODO: move this code into
	// a custom json area instead of just aborting early for HCL.
	b.prepareCalled = true

	packerConfig := b.packerConfig()

	// Prepare the builder
	generatedVars, warn, err := b.Builder.Prepare(b.BuilderConfig, packerConfig)
	if err != nil {
		log.Printf("Build '%s' prepare failure: %s\n", b.Type, err)
		return
	}
	b.SetGeneratedVars(generatedVars)

	if err = b.prepareProvisioners(b.Provisioners, packerConfig, generatedVars); err != nil {
		return
	}

	generatedPlaceholderMap := placeholderDataFromGeneratedVars(generatedVars)

	// Prepare the on-error-cleanup provisioner
	if b.CleanupProvisioner.PType != "" {
		configs := make([]interface{}, len(b.CleanupProvisioner.config), len(b.CleanupProvisioner.config)+2)
		copy(configs, b.CleanupProvisioner.config)
		configs = append(configs, packerConfig, generatedPlaceholderMap)
		err = b.CleanupProvisioner.Provisioner.Prepare(configs...)
		if err != nil {
			return
		}
	}

	// Prepare the post-processors
	for _, ppSeq := range b.PostProcessors {
		for _, corePP := range ppSeq {
			err = corePP.PostProcessor.Configure(corePP.config, packerConfig, generatedPlaceholderMap)
			if err != nil {
				return
			}
		}
	}

	return
}

// Runs the actual build. Prepare must be called prior to running this.
func (b *CoreBuild) Run(ctx context.Context, originalUi packersdk.Ui) ([]packersdk.Artifact, error) {
	if !b.prepareCalled {
		panic("Prepare must be called first")
	}

	// Copy the hooks
	hooks := make(map[string][]packersdk.Hook)
	for hookName, hookList := range b.hooks {
		hooks[hookName] = make([]packersdk.Hook, len(hookList))
		copy(hooks[hookName], hookList)
	}

	// Add a hook for the provisioners if we have provisioners
	if len(b.Provisioners) > 0 {
		hookedProvisioners := make([]*HookedProvisioner, len(b.Provisioners))
		for i, p := range b.Provisioners {
			var pConfig interface{}
			if len(p.config) > 0 {
				pConfig = p.config[0]
			} else {
				pConfig = p.HCLConfig
			}
			if b.debug {
				hookedProvisioners[i] = &HookedProvisioner{
					&DebuggedProvisioner{Provisioner: p.Provisioner},
					pConfig,
					p.PType,
				}
			} else {
				hookedProvisioners[i] = &HookedProvisioner{
					p.Provisioner,
					pConfig,
					p.PType,
				}
			}
		}

		if _, ok := hooks[packersdk.HookProvision]; !ok {
			hooks[packersdk.HookProvision] = make([]packersdk.Hook, 0, 1)
		}

		hooks[packersdk.HookProvision] = append(hooks[packersdk.HookProvision], &ProvisionHook{
			Provisioners: hookedProvisioners,
		})
	}

	if b.CleanupProvisioner.PType != "" {
		hookedCleanupProvisioner := &HookedProvisioner{
			b.CleanupProvisioner.Provisioner,
			b.CleanupProvisioner.config,
			b.CleanupProvisioner.PType,
		}
		hooks[packersdk.HookCleanupProvision] = []packersdk.Hook{&ProvisionHook{
			Provisioners: []*HookedProvisioner{hookedCleanupProvisioner},
		}}
	}

	hook := &packersdk.DispatchHook{Mapping: hooks}
	artifacts := make([]packersdk.Artifact, 0, 1)

	// The builder just has a normal Ui, but targeted
	builderUi := &TargetedUI{
		Target: b.Name(),
		Ui:     originalUi,
	}

	var ts *TelemetrySpan
	log.Printf("Running builder: %s", b.BuilderType)
	if b.BuilderConfig != nil {
		ts = CheckpointReporter.AddSpan(b.Type, "builder", b.BuilderConfig)
	} else {
		ts = CheckpointReporter.AddSpan(b.Type, "builder", b.HCLConfig)
	}
	builderArtifact, err := b.Builder.Run(ctx, builderUi, hook)
	ts.End(err)
	if err != nil {
		return nil, err
	}

	for _, p := range b.Provisioners {
		sbomInternalProvisioner, ok := p.Provisioner.(*SBOMInternalProvisioner)
		if ok {
			sbom := SBOM{
				Name:           sbomInternalProvisioner.SBOMName,
				Format:         sbomInternalProvisioner.SBOMFormat,
				CompressedData: sbomInternalProvisioner.CompressedData,
			}
			b.SBOMs = append(b.SBOMs, sbom)
		}
	}

	// If there was no result, don't worry about running post-processors
	// because there is nothing they can do, just return.
	if builderArtifact == nil {
		return nil, nil
	}

	errors := make([]error, 0)
	keepOriginalArtifact := len(b.PostProcessors) == 0

	select {
	case <-ctx.Done():
		log.Println("Build was cancelled. Skipping post-processors.")
		return nil, ctx.Err()
	default:
	}

	// Run the post-processors
PostProcessorRunSeqLoop:
	for _, ppSeq := range b.PostProcessors {
		priorArtifact := builderArtifact
		for i, corePP := range ppSeq {
			ppUi := &TargetedUI{
				Target: fmt.Sprintf("%s (%s)", b.Name(), corePP.PType),
				Ui:     originalUi,
			}

			if corePP.PName == corePP.PType {
				builderUi.Say(fmt.Sprintf("Running post-processor: %s", corePP.PType))
			} else {
				builderUi.Say(fmt.Sprintf("Running post-processor: %s (type %s)", corePP.PName, corePP.PType))
			}
			var ts *TelemetrySpan
			if corePP.config != nil {
				ts = CheckpointReporter.AddSpan(corePP.PType, "post-processor", corePP.config)
			} else {
				ts = CheckpointReporter.AddSpan(corePP.PType, "post-processor", corePP.HCLConfig)
			}
			artifact, defaultKeep, forceOverride, err := corePP.PostProcessor.PostProcess(ctx, ppUi, priorArtifact)
			ts.End(err)
			if err != nil {
				errors = append(errors, fmt.Errorf("Post-processor failed: %s", err))
				continue PostProcessorRunSeqLoop
			}

			if artifact == nil {
				log.Println("Nil artifact, halting post-processor chain.")
				continue PostProcessorRunSeqLoop
			}

			keep := defaultKeep
			// When user has not set keep_input_artifact
			// corePP.keepInputArtifact is nil.
			// In this case, use the keepDefault provided by the postprocessor.
			// When user _has_ set keep_input_artifact, go with that instead.
			// Exception: for postprocessors that will fail/become
			// useless if keep isn't true, heed forceOverride and keep the
			// input artifact regardless of user preference.
			if corePP.KeepInputArtifact != nil {
				if defaultKeep && *corePP.KeepInputArtifact == false && forceOverride {
					log.Printf("The %s post-processor forces "+
						"keep_input_artifact=true to preserve integrity of the"+
						"build chain. User-set keep_input_artifact=false will be"+
						"ignored.", corePP.PType)
				} else {
					// User overrides default.
					keep = *corePP.KeepInputArtifact
				}
			}
			if i == 0 {
				// This is the first post-processor. We handle deleting
				// previous artifacts a bit different because multiple
				// post-processors may be using the original and need it.
				if !keepOriginalArtifact && keep {
					log.Printf(
						"Flagging to keep original artifact from post-processor '%s'",
						corePP.PType)
					keepOriginalArtifact = true
				}
			} else {
				// We have a prior artifact. If we want to keep it, we append
				// it to the results list. Otherwise, we destroy it.
				if keep {
					artifacts = append(artifacts, priorArtifact)
				} else {
					log.Printf("Deleting prior artifact from post-processor '%s'", corePP.PType)
					if err := priorArtifact.Destroy(); err != nil {
						log.Printf("Error is %#v", err)
						errors = append(errors, fmt.Errorf("Failed cleaning up prior artifact: %s; pp is %s", err, corePP.PType))
					}
				}
			}

			priorArtifact = artifact
		}

		// Add on the last artifact to the results
		if priorArtifact != nil {
			artifacts = append(artifacts, priorArtifact)
		}
	}

	if keepOriginalArtifact {
		artifacts = append(artifacts, nil)
		copy(artifacts[1:], artifacts)
		artifacts[0] = builderArtifact
	} else {
		log.Printf("Deleting original artifact for build '%s'", b.Type)
		if err := builderArtifact.Destroy(); err != nil {
			errors = append(errors, fmt.Errorf("Error destroying builder artifact: %s; bad artifact: %#v", err, builderArtifact.Files()))
		}
	}

	if len(errors) > 0 {
		err = &packersdk.MultiError{Errors: errors}
		return artifacts, err
	}

	return artifacts, nil
}

func (b *CoreBuild) SetDebug(val bool) {
	if b.prepareCalled {
		panic("prepare has already been called")
	}

	b.debug = val
}

func (b *CoreBuild) SetForce(val bool) {
	if b.prepareCalled {
		panic("prepare has already been called")
	}

	b.force = val
}

func (b *CoreBuild) SetOnError(val string) {
	if b.prepareCalled {
		panic("prepare has already been called")
	}

	b.onError = val
}
