// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package stackruntime

import (
	"context"
	"fmt"
	"sync/atomic"

	"google.golang.org/protobuf/types/known/anypb"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/stacks/stackconfig"
	"github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval"
	"github.com/hashicorp/terraform/internal/stacks/stackstate"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// Apply performs the changes described in a previously-generated plan,
// aiming to make the real system converge with the desired state and
// then emit a series of patches that the caller must make to the
// current state to represent what has changed.
//
// Apply does not return a result directly because it emits results in a
// streaming fashion using channels provided in the given [ApplyResponse].
//
// Callers must not modify any values reachable directly or indirectly
// through resp after passing it to this function, aside from the implicit
// modifications to the internal state of channels caused by reading them.
func Apply(ctx context.Context, req *ApplyRequest, resp *ApplyResponse) {
	resp.Complete = false // We'll reset this to true only if we actually succeed

	var seenAnyErrors atomic.Bool
	outp := stackeval.ApplyOutput{
		AnnounceAppliedChange: func(ctx context.Context, change stackstate.AppliedChange) {
			resp.AppliedChanges <- change
		},
		AnnounceDiagnostics: func(ctx context.Context, diags tfdiags.Diagnostics) {
			for _, diag := range diags {
				if diag.Severity() == tfdiags.Error {
					seenAnyErrors.Store(true) // never becomes false again
				}
				resp.Diagnostics <- diag
			}
		},
	}

	// Whatever return path we take, we must close our channels to allow
	// a caller to see that the operation is complete.
	defer func() {
		close(resp.Diagnostics)
		close(resp.AppliedChanges) // MUST be the last channel to close
	}()

	main, err := stackeval.ApplyPlan(
		ctx,
		req.Config,
		req.RawPlan,
		stackeval.ApplyOpts{
			ProviderFactories:  req.ProviderFactories,
			ExperimentsAllowed: req.ExperimentsAllowed,
		},
		outp,
	)
	if err != nil {
		// An error here means that the apply wasn't even able to _start_,
		// typically because the request itself was invalid. We'll announce
		// that as a diagnostic and then halt, though if we get here then
		// it's most likely a bug in the caller rather than end-user error.
		resp.Diagnostics <- tfdiags.Sourceless(
			tfdiags.Error,
			"Invalid apply request",
			fmt.Sprintf("Cannot begin the apply phase: %s.", err),
		)
		return
	}

	if !seenAnyErrors.Load() {
		resp.Complete = true
	}

	cleanupDiags := main.DoCleanup(ctx)
	for _, diag := range cleanupDiags {
		// cleanup diagnostics don't stop the apply from being "complete",
		// since this should include only transient operational errors such
		// as failing to terminate a provider plugin.
		resp.Diagnostics <- diag
	}

}

// ApplyRequest represents the inputs to an [Apply] call.
type ApplyRequest struct {
	Config  *stackconfig.Config
	RawPlan []*anypb.Any

	ProviderFactories map[addrs.Provider]providers.Factory

	ExperimentsAllowed bool
}

// ApplyResponse is used by [Apply] to describe the results of applying.
//
// [Apply] produces streaming results throughout its execution, and so it
// communicates with the caller by writing to provided channels during its work
// and then modifying other fields in this structure before returning. Callers
// MUST NOT access any non-channel fields of ApplyResponse until the
// AppliedChanges channel has been closed to signal the completion of the
// apply process.
type ApplyResponse struct {
	// [Apply] will set this field to true if the apply ran to completion
	// without encountering any errors, or set this to false if not.
	//
	// A caller might react to Complete: true by creating one follow-up plan
	// just to confirm that everything has converged and then, if so, consider
	// all of the configuration versions that contributed to this plan to now
	// be converged. If unsuccessful, none of the contributing configurations
	// are known to be converged and the operator will need to decide whether
	// to immediately try creating a new plan (if they think the error was
	// transient) or push a new configuration update to correct the problem.
	//
	// If this field is false after applying is complete then it's likely that
	// at least some of the planned side-effects already occurred, and so
	// it's important to still handle anything that was written to the
	// AppliedChanges channel to partially update the state with the subset
	// of changes that were completed.
	//
	// The initial value of this field is ignored; there's no reason to set
	// it to anything other than the zero value.
	Complete bool

	// AppliedChanges is the channel that will be sent each individual
	// applied change, in no predictable order, during the apply
	// operation.
	//
	// Callers MUST provide a non-nil channel and read from it from
	// another Goroutine throughout the apply operation, or apply
	// progress will be blocked. Callers that read slowly should provide
	// a buffered channel to reduce the backpressure they exert on the
	// apply process.
	//
	// The apply operation will close this channel before it returns.
	// AppliedChanges is guaranteed to be the last channel to close
	// (i.e. after Diagnostics is closed) so callers can use the close
	// signal of this channel alone to mark that the apply process is
	// over, but if Diagnostics is a buffered channel they must take
	// care to deplete its buffer afterwards to avoid losing diagnostics
	// delivered near the end of the apply process.
	AppliedChanges chan<- stackstate.AppliedChange

	// Diagnostics is the channel that will be sent any diagnostics
	// that arise during the apply process, in no particular order.
	//
	// In particular note that there's no guarantee that the diagnostics
	// for applying changes to a particular object will be emitted in close
	// proximity to an AppliedChanges write for that same object. Diagnostics
	// and applied changes are totally decoupled, since diagnostics might be
	// collected up and emitted later as a large batch if the runtime
	// needs to perform aggregate operations such as deduplication on
	// the diagnostics before exposing them.
	//
	// Callers MUST provide a non-nil channel and read from it from
	// another Goroutine throughout the plan operation, or apply
	// progress will be blocked. Callers that read slowly should provide
	// a buffered channel to reduce the backpressure they exert on the
	// apply process.
	//
	// The apply operation will close this channel before it returns, but
	// callers should use the close event of AppliedChanges as the definitive
	// signal that planning is complete.
	Diagnostics chan<- tfdiags.Diagnostic
}
