// Copyright 2016-2023, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package plugin

import (
	"context"
	"io"
	"sync"

	"github.com/opentracing/opentracing-go"
	"google.golang.org/grpc"

	"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)

// Context is used to group related operations together so that
// associated OS resources can be cached, shared, and reclaimed as
// appropriate. It also carries shared plugin configuration.
type Context struct {
	Diag       diag.Sink // the diagnostics sink to use for messages.
	StatusDiag diag.Sink // the diagnostics sink to use for status messages.
	Host       Host      // the host that can be used to fetch providers.
	Pwd        string    // the working directory to spawn all plugins in.
	Root       string    // the root directory of the context.

	// If non-nil, configures custom gRPC client options. Receives pluginInfo which is a JSON-serializable bit of
	// metadata describing the plugin.
	DialOptions func(pluginInfo interface{}) []grpc.DialOption

	DebugTraceMutex *sync.Mutex // used internally to syncronize debug tracing

	tracingSpan opentracing.Span // the OpenTracing span to parent requests within.

	cancelFuncs []context.CancelFunc
	cancelLock  *sync.Mutex // Guards cancelFuncs.
	baseContext context.Context
}

// NewContext allocates a new context with a given sink and host. Note
// that the host is "owned" by this context from here forwards, such
// that when the context's resources are reclaimed, so too are the
// host's.
func NewContext(d, statusD diag.Sink, host Host, _ ConfigSource,
	pwd string, runtimeOptions map[string]interface{}, disableProviderPreview bool,
	parentSpan opentracing.Span,
) (*Context, error) {
	// TODO: I think really this ought to just take plugins *workspace.Plugins as an arg, but yaml depends on
	// this function so *sigh*. For now just see if there's a project we should be using, and use it if there
	// is.
	projPath, err := workspace.DetectProjectPath()
	var plugins *workspace.Plugins
	if err == nil && projPath != "" {
		project, err := workspace.LoadProject(projPath)
		if err == nil {
			plugins = project.Plugins
		}
	}

	root := ""
	return NewContextWithRoot(d, statusD, host, pwd, root, runtimeOptions,
		disableProviderPreview, parentSpan, plugins, nil)
}

// NewContextWithRoot is a variation of NewContext that also sets known project Root. Additionally accepts Plugins
func NewContextWithRoot(d, statusD diag.Sink, host Host,
	pwd, root string, runtimeOptions map[string]interface{}, disableProviderPreview bool,
	parentSpan opentracing.Span, plugins *workspace.Plugins, config map[config.Key]string,
) (*Context, error) {
	return NewContextWithContext(
		context.Background(), d, statusD, host, pwd, root,
		runtimeOptions, disableProviderPreview, parentSpan, plugins, config)
}

// NewContextWithContext is a variation of NewContextWithRoot that also sets the base context.
func NewContextWithContext(
	ctx context.Context,
	d, statusD diag.Sink, host Host,
	pwd, root string, runtimeOptions map[string]interface{}, disableProviderPreview bool,
	parentSpan opentracing.Span, plugins *workspace.Plugins, config map[config.Key]string,
) (*Context, error) {
	if d == nil {
		d = diag.DefaultSink(io.Discard, io.Discard, diag.FormatOptions{Color: colors.Never})
	}
	if statusD == nil {
		statusD = diag.DefaultSink(io.Discard, io.Discard, diag.FormatOptions{Color: colors.Never})
	}

	pctx := &Context{
		Diag:            d,
		StatusDiag:      statusD,
		Host:            host,
		Pwd:             pwd,
		Root:            root,
		tracingSpan:     parentSpan,
		DebugTraceMutex: &sync.Mutex{},
		cancelLock:      &sync.Mutex{},
		baseContext:     ctx,
	}
	if host == nil {
		h, err := NewDefaultHost(pctx, runtimeOptions, disableProviderPreview, plugins, config)
		if err != nil {
			return nil, err
		}
		pctx.Host = h
	}
	return pctx, nil
}

// Request allocates a request sub-context.
func (ctx *Context) Request() context.Context {
	c := ctx.baseContext
	contract.Assertf(c != nil, "Context must have a base context")
	c = opentracing.ContextWithSpan(c, ctx.tracingSpan)
	c, cancel := context.WithCancel(c)
	ctx.cancelLock.Lock()
	ctx.cancelFuncs = append(ctx.cancelFuncs, cancel)
	ctx.cancelLock.Unlock()
	return c
}

// Close reclaims all resources associated with this context.
func (ctx *Context) Close() error {
	defer func() {
		// It is possible that cancelFuncs may be appended while this function is running.
		// Capture the current value of cancelFuncs and set cancelFuncs to nil to prevent cancelFuncs
		// from being appended to while we are iterating over it.
		ctx.cancelLock.Lock()
		cancelFuncs := ctx.cancelFuncs
		ctx.cancelFuncs = nil
		ctx.cancelLock.Unlock()
		for _, cancel := range cancelFuncs {
			cancel()
		}
	}()
	if ctx.tracingSpan != nil {
		ctx.tracingSpan.Finish()
	}
	err := ctx.Host.Close()
	if err != nil && !rpcutil.IsBenignCloseErr(err) {
		return err
	}
	return nil
}

// WithCancelChannel registers a close channel which will close the returned Context when
// the channel is closed.
//
// WARNING: Calling this function without ever closing `c` will leak go routines.
func (ctx *Context) WithCancelChannel(c <-chan struct{}) *Context {
	newCtx := *ctx
	go func() {
		<-c
		newCtx.Close()
	}()
	return &newCtx
}