// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package grapheval

import (
	"context"
	"sync"

	"github.com/apparentlymart/go-workgraph/workgraph"

	"github.com/opentofu/opentofu/internal/tfdiags"
)

// Once is a similar principle to the Go standard library's [sync.Once], but
// with some additions tailored for use in OpenTofu:
//
//   - The "Do" method returns a result of type T and diagnostics.
//   - If two Once instances attempt to depend on each other for resolution then
//     both will immediately fail with error diagnostics, rather than deadlocking.
//
// The typical way to use this type is to use is as a field of a type
// representing whatever object the operation conceptually belongs to, and
// then offer a method of that type which wraps the call to [Once.Do], thus
// ensuring that the first call to that method will cause the one-time operation
// to start and then subsequent calls will return the same result as that
// first call, without directly exposing this type in the public-facing
// signature.
type Once[T any] struct {
	mu        sync.Mutex
	promise   *workgraph.Promise[withDiagnostics[T]]
	requestID workgraph.RequestID
}

// The first time Do is called it runs the given function and returns its result
// once complete. Subsequent calls then just wait for the result of the function
// passed in the first call and return its result.
//
// The given context MUST have an associated [workgraph.Worker]. Typically
// the first worker should be established on entry to an internal callgraph
// that relies on this package, by calling [ContextWithNewWorker].
//
// The automatic deadlock detection relies on consistent use of
// [context.Context] values: any other calls to [once.Do]
// made directly or indirectly from the given callback function on any
// Once object in the program MUST pass a context derived from the one passed
// into the callback function, because it includes internal tracking
// information.
func (o *Once[T]) Do(ctx context.Context, f func(ctx context.Context) (T, tfdiags.Diagnostics)) (T, tfdiags.Diagnostics) {
	worker := WorkerFromContext(ctx)
	o.mu.Lock()
	if o.promise == nil {
		// This is the first call, so we'll establish the inner request and
		// start executing the function in a separate goroutine.
		resolver, promise := workgraph.NewRequest[withDiagnostics[T]](worker)
		o.promise = &promise
		o.requestID = resolver.RequestID()
		workgraph.WithNewAsyncWorker(func(w *workgraph.Worker) {
			ctx := ContextWithWorker(ctx, w)
			ret, diags := f(ctx)
			resolver.Report(w, withDiagnostics[T]{ret, diags}, nil)
		}, resolver)
	}
	o.mu.Unlock()

	withDiags, err := o.promise.Await(worker)
	if err != nil {
		// We return our own errors only as diagnostics, so any error here
		// must be one generated by the workgraph package itself in response
		// to a problem such as self-reference or a failure to resolve some
		// other request.
		var zero T
		return zero, DiagnosticsForWorkgraphError(ctx, err)
	}
	return withDiags.value, withDiags.diags
}

// RequestID returns the [workgraph.RequestID] associated with the inner
// request, or [workgraph.NoRequest] if Do has not been called yet.
func (o *Once[T]) RequestID() workgraph.RequestID {
	return o.requestID
}

type withDiagnostics[T any] struct {
	value T
	diags tfdiags.Diagnostics
}
