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

package stackeval

import (
	"context"
	"fmt"

	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/instances"
	"github.com/hashicorp/terraform/internal/promising"
	"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
	"github.com/hashicorp/terraform/internal/stacks/stackplan"
	"github.com/hashicorp/terraform/internal/stacks/stackstate"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

// Provider represents a provider configuration in a particular stack config.
type Provider struct {
	addr   stackaddrs.AbsProviderConfig
	config *ProviderConfig
	stack  *Stack

	main *Main

	forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]]
	instances    perEvalPhase[promising.Once[withDiagnostics[instancesResult[*ProviderInstance]]]]
}

func newProvider(main *Main, addr stackaddrs.AbsProviderConfig, stack *Stack, config *ProviderConfig) *Provider {
	return &Provider{
		addr:   addr,
		stack:  stack,
		config: config,
		main:   main,
	}
}

func (p *Provider) ProviderType() *ProviderType {
	return p.main.ProviderType(p.addr.Item.Provider)
}

// InstRefValueType returns the type of any values that represent references to
// instances of this provider configuration.
//
// All configurations for the same provider share the same type.
func (p *Provider) InstRefValueType() cty.Type {
	decl := p.config.config
	return providerInstanceRefType(decl.ProviderAddr)
}

// ForEachValue returns the result of evaluating the "for_each" expression
// for this provider configuration, with the following exceptions:
//   - If the provider config doesn't use "for_each" at all, returns [cty.NilVal].
//   - If the for_each expression is present but too invalid to evaluate,
//     returns [cty.DynamicVal] to represent that the for_each value cannot
//     be determined.
//
// A present and valid "for_each" expression produces a result that's
// guaranteed to be:
// - Either a set of strings, a map of any element type, or an object type
// - Known and not null (only the top-level value)
// - Not sensitive (only the top-level value)
func (p *Provider) ForEachValue(ctx context.Context, phase EvalPhase) cty.Value {
	ret, _ := p.CheckForEachValue(ctx, phase)
	return ret
}

// CheckForEachValue evaluates the "for_each" expression if present, validates
// that its value is valid, and then returns that value.
//
// If this call does not use "for_each" then this immediately returns cty.NilVal
// representing the absense of the value.
//
// If the diagnostics does not include errors and the result is not cty.NilVal
// then callers can assume that the result value will be:
// - Either a set of strings, a map of any element type, or an object type
// - Known and not null (except for nested map/object element values)
// - Not sensitive (only the top-level value)
//
// If the diagnostics _does_ include errors then the result might be
// [cty.DynamicVal], which represents that the for_each expression was so invalid
// that we cannot know the for_each value.
func (p *Provider) CheckForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) {
	val, diags := doOnceWithDiags(
		ctx, p.tracingName()+" for_each", p.forEachValue.For(phase),
		func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
			var diags tfdiags.Diagnostics
			cfg := p.config.config

			switch {

			case cfg.ForEach != nil:
				result, moreDiags := evaluateForEachExpr(ctx, cfg.ForEach, phase, p.stack, "provider")
				diags = diags.Append(moreDiags)
				if diags.HasErrors() {
					return cty.DynamicVal, diags
				}
				return result.Value, diags

			default:
				// This stack config doesn't use for_each at all
				return cty.NilVal, diags
			}
		},
	)
	if val == cty.NilVal && diags.HasErrors() {
		// We use cty.DynamicVal as the placeholder for an invalid for_each,
		// to represent "unknown for_each value" as distinct from "no for_each
		// expression at all".
		val = cty.DynamicVal
	}
	return val, diags
}

// Instances returns all of the instances of the provider config known to be
// declared by the configuration.
//
// Calcluating this involves evaluating the call's for_each expression if any,
// and so this call may block on evaluation of other objects in the
// configuration.
//
// If the configuration has an invalid definition of the instances then the
// result will be nil. Callers that need to distinguish between invalid
// definitions and valid definitions of zero instances can rely on the
// result being a non-nil zero-length map in the latter case.
//
// This function doesn't return any diagnostics describing ways in which the
// for_each expression is invalid because we assume that the main plan walk
// will visit the stack call directly and ask it to check itself, and that
// call will be the one responsible for returning any diagnostics.
func (p *Provider) Instances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ProviderInstance, bool) {
	ret, unknown, _ := p.CheckInstances(ctx, phase)
	return ret, unknown
}

func (p *Provider) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ProviderInstance, bool, tfdiags.Diagnostics) {
	result, diags := doOnceWithDiags(
		ctx, p.tracingName()+" instances", p.instances.For(phase),
		func(ctx context.Context) (instancesResult[*ProviderInstance], tfdiags.Diagnostics) {
			forEachVal, diags := p.CheckForEachValue(ctx, phase)
			if diags.HasErrors() {
				return instancesResult[*ProviderInstance]{}, diags
			}

			return instancesMap(forEachVal, func(ik addrs.InstanceKey, rd instances.RepetitionData) *ProviderInstance {
				return newProviderInstance(p, stackaddrs.AbsProviderToInstance(p.addr, ik), rd)
			}), diags
		},
	)
	return result.insts, result.unknown, diags
}

// ExprReferenceValue implements Referenceable, returning a value containing
// one or more values that act as references to instances of the provider.
func (p *Provider) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value {
	decl := p.config.config
	insts, unknown := p.Instances(ctx, phase)
	refType := p.InstRefValueType()

	switch {
	case decl.ForEach != nil:
		if unknown {
			return cty.UnknownVal(cty.Map(refType))
		}

		if insts == nil {
			// Then we errored during instance calculation, this should have
			// been caught before we got here.
			return cty.NilVal
		}
		elems := make(map[string]cty.Value, len(insts))
		for instKey := range insts {
			k, ok := instKey.(addrs.StringKey)
			if !ok {
				panic(fmt.Sprintf("provider config with for_each has invalid instance key of type %T", instKey))
			}
			elems[string(k)] = cty.CapsuleVal(refType, &stackaddrs.AbsProviderConfigInstance{
				Stack: p.addr.Stack,
				Item: stackaddrs.ProviderConfigInstance{
					ProviderConfig: p.addr.Item,
					Key:            instKey,
				},
			})
		}
		if len(elems) == 0 {
			return cty.MapValEmpty(refType)
		}
		return cty.MapVal(elems)
	default:
		if insts == nil {
			return cty.UnknownVal(refType)
		}
		return cty.CapsuleVal(refType, &stackaddrs.AbsProviderConfigInstance{
			Stack: p.addr.Stack,
			Item: stackaddrs.ProviderConfigInstance{
				ProviderConfig: p.addr.Item,
				Key:            addrs.NoKey,
			},
		})
	}
}

func (p *Provider) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics

	_, moreDiags := p.CheckForEachValue(ctx, phase)
	diags = diags.Append(moreDiags)
	_, _, moreDiags = p.CheckInstances(ctx, phase)
	diags = diags.Append(moreDiags)
	// Everything else is instance-specific and so the plan walk driver must
	// call p.Instances and ask each instance to plan itself.

	return diags
}

// PlanChanges implements Plannable.
func (p *Provider) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
	return nil, p.checkValid(ctx, PlanPhase)
}

// References implements Referrer
func (p *Provider) References(ctx context.Context) []stackaddrs.AbsReference {
	cfg := p.config.config
	var ret []stackaddrs.Reference
	ret = append(ret, ReferencesInExpr(cfg.ForEach)...)
	if schema, err := p.ProviderType().Schema(ctx); err == nil {
		ret = append(ret, ReferencesInBody(cfg.Config, schema.Provider.Body.DecoderSpec())...)
	}
	return makeReferencesAbsolute(ret, p.addr.Stack)
}

// CheckApply implements ApplyChecker.
func (p *Provider) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) {
	return nil, p.checkValid(ctx, ApplyPhase)
}

// tracingName implements Plannable.
func (p *Provider) tracingName() string {
	return p.addr.String()
}
