// Copyright IBM Corp. 2015, 2025
// SPDX-License-Identifier: BUSL-1.1

package taskrunner

import (
	"bytes"
	"context"
	"fmt"
	"sync"

	"github.com/hashicorp/consul-template/renderer"
	"github.com/hashicorp/go-envparse"
	log "github.com/hashicorp/go-hclog"
	"github.com/hashicorp/go-multierror"
	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
	ti "github.com/hashicorp/nomad/client/allocrunner/taskrunner/interfaces"
	"github.com/hashicorp/nomad/client/allocrunner/taskrunner/secrets"
	"github.com/hashicorp/nomad/client/allocrunner/taskrunner/template"
	"github.com/hashicorp/nomad/client/commonplugins"
	"github.com/hashicorp/nomad/client/config"
	"github.com/hashicorp/nomad/client/taskenv"
	"github.com/hashicorp/nomad/nomad/structs"
)

type TemplateProvider interface {
	BuildTemplate() *structs.Template
}

type PluginProvider interface {
	Fetch(context.Context) (map[string]string, error)
}

type secretsHookConfig struct {
	// logger is used to log
	logger log.Logger

	// lifecycle is used to interact with the task's lifecycle
	lifecycle ti.TaskLifecycle

	// events is used to emit events
	events ti.EventEmitter

	// clientConfig is the Nomad Client configuration
	clientConfig *config.Config

	// envBuilder is the environment variable builder for the task.
	envBuilder *taskenv.Builder

	// nomadNamespace is the job's Nomad namespace
	nomadNamespace string

	// jobId is the ID of the job
	jobId string
}

type secretsHook struct {
	// logger is used to log
	logger log.Logger

	// lifecycle is used to interact with the task's lifecycle
	lifecycle ti.TaskLifecycle

	// events is used to emit events
	events ti.EventEmitter

	// clientConfig is the Nomad Client configuration
	clientConfig *config.Config

	// envBuilder is the environment variable builder for the task
	envBuilder *taskenv.Builder

	// nomadNamespace is the job's Nomad namespace
	nomadNamespace string

	// jobId is the nomad job's ID
	jobId string

	// secrets to be fetched and populated for interpolation
	secrets []*structs.Secret
}

func newSecretsHook(conf *secretsHookConfig, secrets []*structs.Secret) *secretsHook {
	return &secretsHook{
		logger:         conf.logger,
		lifecycle:      conf.lifecycle,
		events:         conf.events,
		clientConfig:   conf.clientConfig,
		envBuilder:     conf.envBuilder,
		nomadNamespace: conf.nomadNamespace,
		jobId:          conf.jobId,
		secrets:        secrets,
	}
}

func (h *secretsHook) Name() string {
	return "secrets"
}

func (h *secretsHook) Prestart(ctx context.Context, req *interfaces.TaskPrestartRequest, resp *interfaces.TaskPrestartResponse) error {
	tmplProvider, pluginProvider, err := h.buildSecretProviders(req.TaskDir.SecretsDir)
	if err != nil {
		return err
	}

	templates := []*structs.Template{}
	for _, p := range tmplProvider {
		templates = append(templates, p.BuildTemplate())
	}

	vaultCluster := req.Task.GetVaultClusterName()
	vaultConfig := h.clientConfig.GetVaultConfigs(h.logger)[vaultCluster]

	mu := &sync.Mutex{}
	contents := []byte{}
	unblock := make(chan struct{})
	tm, err := template.NewTaskTemplateManager(&template.TaskTemplateManagerConfig{
		UnblockCh:            unblock,
		Lifecycle:            h.lifecycle,
		Events:               h.events,
		Templates:            templates,
		ClientConfig:         h.clientConfig,
		VaultToken:           req.VaultToken,
		VaultConfig:          vaultConfig,
		VaultNamespace:       req.Alloc.Job.VaultNamespace,
		TaskDir:              req.TaskDir.Dir,
		EnvBuilder:           h.envBuilder,
		MaxTemplateEventRate: template.DefaultMaxTemplateEventRate,
		NomadNamespace:       h.nomadNamespace,
		NomadToken:           req.NomadToken,
		TaskID:               req.Alloc.ID + "-" + req.Task.Name,
		Logger:               h.logger,

		// This RenderFunc is used to keep any secret data from being written to disk.
		RenderFunc: func(ri *renderer.RenderInput) (*renderer.RenderResult, error) {
			// This RenderFunc is called by a single goroutine synchronously, but we
			// lock the append in the event this behavior changes without us knowing.
			mu.Lock()
			defer mu.Unlock()
			contents = append(contents, ri.Contents...)
			return &renderer.RenderResult{
				DidRender:   true,
				WouldRender: true,
				Contents:    ri.Contents,
			}, nil
		},
	})
	if err != nil {
		return err
	}

	go tm.Run()

	// Safeguard against the template manager continuing to run.
	defer tm.Stop()

	select {
	case <-ctx.Done():
		return nil
	case <-unblock:
	}

	// Set secrets from templates
	m, err := envparse.Parse(bytes.NewBuffer(contents))
	if err != nil {
		return err
	}
	h.envBuilder.SetSecrets(m)

	taskEnv := h.envBuilder.Build()

	for _, p := range pluginProvider {
		if ep, ok := p.(*secrets.ExternalPluginProvider); ok {
			ep.InterpolateEnv(taskEnv.ReplaceEnv)
		}
		vars, err := p.Fetch(ctx)
		if err != nil {
			return err
		}
		h.envBuilder.SetSecrets(vars)
	}

	resp.Done = true
	return nil
}

func (h *secretsHook) buildSecretProviders(secretDir string) ([]TemplateProvider, []PluginProvider, error) {
	// Any configuration errors will be found when calling the secret providers constructor,
	// so use a multierror to collect all errors and return them to the user at the same time.
	tmplProvider, pluginProvider, mErr := []TemplateProvider{}, []PluginProvider{}, new(multierror.Error)

	for idx, s := range h.secrets {
		if s == nil {
			continue
		}

		tmplFile := fmt.Sprintf("temp-%d", idx)
		switch s.Provider {
		case secrets.SecretProviderNomad:
			if p, err := secrets.NewNomadProvider(s, secretDir, tmplFile, h.nomadNamespace); err != nil {
				multierror.Append(mErr, err)
			} else {
				tmplProvider = append(tmplProvider, p)
			}
		case secrets.SecretProviderVault:
			if p, err := secrets.NewVaultProvider(s, secretDir, tmplFile); err != nil {
				multierror.Append(mErr, err)
			} else {
				tmplProvider = append(tmplProvider, p)
			}
		default:
			plug, err := commonplugins.NewExternalSecretsPlugin(h.clientConfig.CommonPluginDir, s.Provider)
			if err != nil {
				multierror.Append(mErr, err)
				continue
			}
			// Add/overwrite the nomad namespace and jobID envVars
			s.Env = h.setupPluginEnv(s.Env)
			pluginProvider = append(pluginProvider, secrets.NewExternalPluginProvider(plug, s.Provider, s.Name, s.Path, s.Env))
		}
	}

	return tmplProvider, pluginProvider, mErr.ErrorOrNil()
}

func (h *secretsHook) setupPluginEnv(env map[string]string) map[string]string {
	if env == nil {
		env = make(map[string]string)
	}

	// set jobID and namespace, overwriting anything already set
	env[taskenv.JobID] = h.jobId
	env[taskenv.Namespace] = h.nomadNamespace

	return env
}
