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

package workdir

import (
	"encoding/json"
	"errors"
	"fmt"

	version "github.com/hashicorp/go-version"
	tfaddr "github.com/hashicorp/terraform-registry-address"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/getproviders"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/zclconf/go-cty/cty"
	ctyjson "github.com/zclconf/go-cty/cty/json"
)

var (
	_ ConfigState                        = &StateStoreConfigState{}
	_ DeepCopier[StateStoreConfigState]  = &StateStoreConfigState{}
	_ PlanDataProvider[plans.StateStore] = &StateStoreConfigState{}
)

// StateStoreConfigState describes the physical storage format for the state store
type StateStoreConfigState struct {
	Type               string                          `json:"type"`                 // State store type name
	Provider           *ProviderConfigState            `json:"provider"`             // Details about the state-storage provider
	ConfigRaw          json.RawMessage                 `json:"config"`               // state_store block raw config, barring provider details
	Hash               uint64                          `json:"hash"`                 // Hash of the state_store block's configuration, including the nested provider block
	ProviderSupplyMode getproviders.ProviderSupplyMode `json:"provider_supply_mode"` // How the provider was supplied to Terraform during the init operation that created this config state.
}

// Empty returns true if there is no active state store.
func (s *StateStoreConfigState) Empty() bool {
	return s == nil || s.Type == ""
}

// Validate returns true if there are no missing expected values, and
// important values have been validated, e.g. FQNs. When the config is
// invalid an error will be returned.
func (s *StateStoreConfigState) Validate() error {
	// Are any bits of data totally missing?
	if s.Empty() {
		return fmt.Errorf("attempted to encode a malformed backend state file; data is empty")
	}
	if s.Type == "" {
		return fmt.Errorf("attempted to encode a malformed backend state file; state store type is missing")
	}
	if s.Provider == nil {
		return fmt.Errorf("attempted to encode a malformed backend state file; provider data is missing")
	}
	if s.ConfigRaw == nil {
		return fmt.Errorf("attempted to encode a malformed backend state file; state_store configuration data is missing")
	}

	// Validity of data that is there
	err := s.Provider.Source.Validate()
	if err != nil {
		return fmt.Errorf("state store is not valid: %w", err)
	}

	// Version information is required if the provider isn't builtin or unmanaged by Terraform
	switch s.ProviderSupplyMode {
	case getproviders.BuiltIn, getproviders.Reattached, getproviders.DevOverride:
		// These modes do not require version information
	case getproviders.ManagedByTerraform:
		if s.Provider.Version == nil {
			return fmt.Errorf("state store is not valid: provider version data is missing despite provider %s being managed by Terraform.", s.Provider.Source.ForDisplay())
		}
	default:
		panic(fmt.Sprintf("State store provider %q (%s) has unknown supply mode %q. This is a bug in Terraform and should be reported.", s.Provider.Source.Type, s.Provider.Source.ForDisplay(), s.ProviderSupplyMode))
	}

	return nil
}

// Config decodes the type-specific configuration object using the provided
// schema and returns the result as a cty.Value.
//
// An error is returned if the stored configuration does not conform to the
// given schema, or is otherwise invalid.
func (s *StateStoreConfigState) Config(schema *configschema.Block) (cty.Value, error) {
	ty := schema.ImpliedType()
	if s == nil {
		return cty.NullVal(ty), nil
	}
	return ctyjson.Unmarshal(s.ConfigRaw, ty)
}

// SetConfig replaces (in-place) the type-specific configuration object using
// the provided value and associated schema.
//
// An error is returned if the given value does not conform to the implied
// type of the schema.
func (s *StateStoreConfigState) SetConfig(val cty.Value, schema *configschema.Block) error {
	if s == nil {
		return errors.New("SetConfig called on nil StateStoreConfigState receiver")
	}
	ty := schema.ImpliedType()
	buf, err := ctyjson.Marshal(val, ty)
	if err != nil {
		return err
	}
	s.ConfigRaw = buf
	return nil
}

// PlanData produces an alternative representation of the receiver that is
// suitable for storing in a plan. The current workspace must additionally
// be provided, to be stored alongside the state store configuration.
//
// The state_store configuration schema is required in order to properly
// encode the state store-specific configuration settings.
func (s *StateStoreConfigState) PlanData(storeSchema *configschema.Block, providerSchema *configschema.Block, workspaceName string) (*plans.StateStore, error) {
	if s == nil {
		panic("PlanData called on a nil *StateStoreConfigState receiver. This is a bug in Terraform and should be reported.")
	}

	if err := s.Validate(); err != nil {
		return nil, fmt.Errorf("error when preparing state store config for planfile: %s", err)
	}

	storeConfigVal, err := s.Config(storeSchema)
	if err != nil {
		return nil, fmt.Errorf("failed to decode state_store config: %w", err)
	}
	providerConfigVal, err := s.Provider.Config(providerSchema)
	if err != nil {
		return nil, fmt.Errorf("failed to decode state_store's nested provider config: %w", err)
	}

	var providerVersion *version.Version
	switch s.ProviderSupplyMode {
	case getproviders.BuiltIn, getproviders.Reattached, getproviders.DevOverride:
		// For built-in providers, reattached providers, and developer overrides, we don't require version information to be present in the state file, so we should be tolerant of it being missing.
		// In this case we can just use a placeholder version that will never actually be used for anything, but allows us to avoid returning an error when trying to save state store data to a plan file.
		providerVersion = version.Must(version.NewVersion("0.0.0"))
	case getproviders.ManagedByTerraform:
		providerVersion = s.Provider.Version
	default:
		panic(fmt.Sprintf("State store provider %q (%s) has unknown supply mode %q. This is a bug in Terraform and should be reported.", s.Provider.Source.Type, s.Provider.Source.ForDisplay(), s.ProviderSupplyMode))
	}

	return plans.NewStateStore(s.Type, providerVersion, s.Provider.Source, storeConfigVal, storeSchema, providerConfigVal, providerSchema, workspaceName)
}

func (s *StateStoreConfigState) DeepCopy() *StateStoreConfigState {
	if s == nil {
		return nil
	}
	provider := &ProviderConfigState{
		Version: s.Provider.Version,
		Source:  s.Provider.Source,
	}
	if s.Provider.ConfigRaw != nil {
		provider.ConfigRaw = make([]byte, len(s.Provider.ConfigRaw))
		copy(provider.ConfigRaw, s.Provider.ConfigRaw)
	}

	ret := &StateStoreConfigState{
		Type:               s.Type,
		Provider:           provider,
		ProviderSupplyMode: s.ProviderSupplyMode,
		Hash:               s.Hash,
	}

	if s.ConfigRaw != nil {
		ret.ConfigRaw = make([]byte, len(s.ConfigRaw))
		copy(ret.ConfigRaw, s.ConfigRaw)
	}
	return ret
}

var _ ConfigState = &ProviderConfigState{}

// ProviderConfigState is used in the StateStoreConfigState struct to describe the provider that's used for pluggable
// state storage. The version and source data inside should mirror an entry in the dependency lock file.
// The configuration recorded here is the provider block nested inside state_store block.
type ProviderConfigState struct {
	Version   *version.Version `json:"version"` // The specific provider version used for the state store. Should be set using a getproviders.Version, etc.
	Source    *tfaddr.Provider `json:"source"`  // The FQN/fully-qualified name of the provider.
	ConfigRaw json.RawMessage  `json:"config"`  // state_store block raw config, barring provider details
}

// Empty returns true if there is no provider config state data.
func (s *ProviderConfigState) Empty() bool {
	return s == nil || s.Version == nil || s.ConfigRaw == nil
}

// Config decodes the type-specific configuration object using the provided
// schema and returns the result as a cty.Value.
//
// An error is returned if the stored configuration does not conform to the
// given schema, or is otherwise invalid.
func (s *ProviderConfigState) Config(schema *configschema.Block) (cty.Value, error) {
	ty := schema.ImpliedType()
	if s == nil {
		return cty.NullVal(ty), nil
	}
	return ctyjson.Unmarshal(s.ConfigRaw, ty)
}

// SetConfig replaces (in-place) the type-specific configuration object using
// the provided value and associated schema.
//
// An error is returned if the given value does not conform to the implied
// type of the schema.
func (s *ProviderConfigState) SetConfig(val cty.Value, schema *configschema.Block) error {
	if s == nil {
		return errors.New("SetConfig called on nil ProviderConfigState receiver")
	}
	ty := schema.ImpliedType()
	buf, err := ctyjson.Marshal(val, ty)
	if err != nil {
		return err
	}
	s.ConfigRaw = buf
	return nil
}
