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

package configs

import (
	"fmt"
	"maps"
	"slices"

	"github.com/apparentlymart/go-versions/versions"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
	"github.com/zclconf/go-cty/cty"
)

// StateMigrationInstructions represents the sum of all state migration files within a
// configuration directory.
//
// A state migration file contains blocks that define how resource state has previously
// been stored for a given project. In combination with an updated Terraform configuration,
// the two pieces of information describe the source and destination of state that the user
// wishes to migrate.
//
// When creating a StateMigrationInstructions struct, calling code must ensure that there
// are no duplicated or mutually-exclusive pieces of information in the original file(s).
type StateMigrationInstructions struct {
	StateStoreProvider *StateStoreProviderRequirement
	StateStore         *StateStore

	Backend *Backend
}

// StateStoreProviderRequirement is very similar to RequiredProvider
// but contains data that's specific to state migration files, and how they're used in commands.
type StateStoreProviderRequirement struct {
	Type      addrs.Provider
	Source    string
	DeclRange hcl.Range

	// Requirement represents the single provider used for state storage.
	// By parsing data into this field it can easily be passed to functions for provider download, which expect a providerreqs.Requirements argument.
	Requirement providerreqs.Requirements
}

// StateMigrationFile represents a single state migration file within a configuration directory.
// A project can include multiple files of this type, and their contents is aggregated.
type StateMigrationFile struct {
	StateMigrationInstructions

	// fromBlockSource is the source range of the 'from' block in the HCL file,
	// intended to be used in error diagnostics from parsing,
	// e.g. multiple from blocks across multiple files.
	fromBlockSource *hcl.Range
}

func loadStateMigrationFile(body hcl.Body) (*StateMigrationFile, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	file := &StateMigrationFile{}

	content, contentDiags := body.Content(stateMigrationFileSchema)
	diags = append(diags, contentDiags...)

	for _, block := range content.Blocks {
		switch block.Type {
		case "state_store_provider":
			req, pDiags := decodeStateStoreProviderBlock(block)
			diags = diags.Extend(pDiags)

			if file.StateStoreProvider != nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  `Duplicate "state_store_provider" configuration block`,
					Detail:   `Only one "state_store_provider" block is allowed in a directory's .tfmigrate.hcl files.`,
					Subject:  block.DefRange.Ptr(),
				})
				continue // Keep file.StateStoreProvider as first parsed block in this scenario
			}

			if req != nil {
				file.StateStoreProvider = req
				file.fromBlockSource = &block.DefRange
			}
		case "from":
			if file.StateStore != nil || file.Backend != nil {
				// A from block has already been parsed.
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  `Duplicate "from" configuration block`,
					Detail:   `Only one "from" block is allowed in a directory's .tfmigrate.hcl files.`,
					Subject:  block.DefRange.Ptr(),
				})
				continue
			}

			// We're parsing the first encountered 'from' block.
			// There could still be duplications within that block, which is detected by the function.
			i, fromDiags := decodeFromBlock(block)
			diags = diags.Extend(fromDiags)

			if !fromDiags.HasErrors() {
				file.fromBlockSource = &block.DefRange

				// Only one of the below is non-nil
				file.StateStore = i.StateStore
				file.Backend = i.Backend
			}

		default:
			// We don't expect other block types in state migration files.
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid block type",
				Detail:   fmt.Sprintf("This block type is not valid within a state migration file: %s", block.Type),
				Subject:  block.DefRange.Ptr(),
			})
		}
	}

	// Check for mutually exclusive blocks, etc.

	// Defining two conflicting sources of state for migration.
	if file.Backend != nil && file.StateStore != nil {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Invalid combination of "backend" and "state_store"`,
			Detail:   `The "backend" and "state_store" blocks are mutually-exclusive inside a "from" block. Only one should be used in a directory's .tfmigrate.hcl files.`,
			Subject:  file.fromBlockSource, // We can blame the 'from' block as being invalid.
		})
	}
	// Unnecessary state store-related data supplied alongside description of a backend.
	if file.Backend != nil && file.StateStoreProvider != nil {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Invalid combination of "backend" and "state_store_provider"`,
			Detail:   `The "state_store_provider" block can only be used in combination with a "state_store" block. Either remove the unused "state_store_provider" block, or update your "from" block to contain a "state_store" block instead.`,
			// No Subject because we don't know which is correct or incorrect.
		})
	}

	return file, diags
}

// decodeFromBlock decodes a 'from' block that can only contain one of 'state_store' or 'backend' blocks.
func decodeFromBlock(block *hcl.Block) (*StateMigrationInstructions, hcl.Diagnostics) {
	var diags hcl.Diagnostics
	fromData := StateMigrationInstructions{}

	fromContent, fromContentDiags := block.Body.Content(fromBlockSchema)
	diags = diags.Extend(fromContentDiags)

	for _, block := range fromContent.Blocks {
		switch block.Type {
		case "state_store":
			ss, ssDiags := decodeStateStoreBlock(block)
			diags = diags.Extend(ssDiags)

			if fromData.StateStore != nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  `Duplicate "state_store" configuration block`,
					Detail:   `Only one "state_store" block, nested in a "from" block, is allowed in a directory's .tfmigrate.hcl files.`,
					Subject:  block.DefRange.Ptr(),
				})
				continue // Keep fromData.MigrateFromStateStore as first parsed block in this scenario
			}

			if ss != nil {
				fromData.StateStore = ss
			}
		case "backend":
			b, bDiags := decodeBackendBlock(block)
			diags = diags.Extend(bDiags)

			if fromData.Backend != nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  `Duplicate "backend" configuration block`,
					Detail:   `Only one "backend" block, nested in a "from" block, is allowed in a directory's .tfmigrate.hcl files.`,
					Subject:  block.DefRange.Ptr(),
				})
				continue // Keep fromData.MigrateFromBackend as first parsed block in this scenario
			}

			if b != nil {
				fromData.Backend = b
			}
		default:
			// We don't expect other block types nested inside from blocks.
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid block type",
				Detail:   fmt.Sprintf("This block type is not valid to be nested inside 'from' blocks within a state migration file: %s", block.Type),
				Subject:  block.DefRange.Ptr(),
			})
		}
	}

	return &fromData, diags
}

func decodeStateStoreProviderBlock(block *hcl.Block) (*StateStoreProviderRequirement, hcl.Diagnostics) {
	// state_store_provider blocks are similar to required_provider blocks but different, so we need logic
	// similar to that in decodeProviderRequirementsBlock but distinct. E.g. version constraints must be
	// exact versions, not a range.

	var diags hcl.Diagnostics
	attrs, hclDiags := block.Body.JustAttributes()
	diags = diags.Extend(hclDiags)

	// Only one provider should be in the block
	localNames := slices.Collect(maps.Keys(attrs))
	if len(localNames) != 1 {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Unexpected number of providers described in "state_store_provider" configuration block.`,
			Detail:   fmt.Sprintf(`The "state_store_provider" block is only expected to include a single provider, but %d were found.`, len(localNames)),
			Subject:  block.DefRange.Ptr(),
		})
		return nil, diags
	}
	localName := localNames[0] // Local name
	attr := attrs[localName]   // Block containing source and version info

	// verify that the local name is already localized or produce an error.
	nameDiags := checkProviderNameNormalized(localName, attr.Expr.Range())
	if nameDiags.HasErrors() {
		diags = append(diags, nameDiags...)
		return nil, diags
	}

	kvs, mapDiags := hcl.ExprMap(attr.Expr)
	if mapDiags.HasErrors() {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  `Invalid "state_store_provider" object`,
			Detail:   "The provider described inside state_store_provider must be an object",
			Subject:  attr.Expr.Range().Ptr(),
		})
		return nil, diags
	}

	// Process the data inside the object describing the provider
	ssProvider := StateStoreProviderRequirement{
		DeclRange: attr.Range,
	}
	var constraints providerreqs.VersionConstraints
	for _, kv := range kvs {
		key, keyDiags := kv.Key.Value(nil)
		if keyDiags.HasErrors() {
			diags = append(diags, keyDiags...)
			return nil, diags
		}

		if key.Type() != cty.String {
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid Attribute",
				Detail:   fmt.Sprintf("Invalid attribute value for provider requirement described by state_store_provider block: %#v", key),
				Subject:  kv.Key.Range().Ptr(),
			})
			return nil, diags
		}

		switch key.AsString() {
		case "version":
			versionString, valDiags := kv.Value.Value(nil)
			if valDiags.HasErrors() || !versionString.Type().Equals(cty.String) {
				diags = append(diags, badVersionConstraintErrorDiag("", kv.Value.Range().Ptr()))
				return nil, diags
			}

			// This means it's only valid to specify a single version in the format "1.0.0",
			// but disallows using the equivalent "=1.0.0"
			v, err := versions.ParseVersion(versionString.AsString())
			if err != nil {
				diags = append(diags, badVersionConstraintErrorDiag(versionString.AsString(), kv.Value.Range().Ptr()))
				return nil, diags
			}

			constraints, err = providerreqs.ParseVersionConstraints(v.String())
			if err != nil {
				// ParseVersionConstraints doesn't return user-friendly errors, so we'll just
				// ignore the provided error and produce our own generic one.
				diags = append(diags, badVersionConstraintErrorDiag(versionString.AsString(), kv.Value.Range().Ptr()))
				return nil, diags
			}

			// All ok with the parsed version constraint. It's used after this switch statement.

		case "source":
			source, err := kv.Value.Value(nil)
			if err != nil || !source.Type().Equals(cty.String) {
				diags = append(diags, &hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  `Invalid source in "state_store_provider" configuration block`,
					Detail:   "Source must be specified as a string.",
					Subject:  kv.Value.Range().Ptr(),
				})
				return nil, diags
			}

			fqn, sourceDiags := addrs.ParseProviderSourceString(source.AsString())
			if sourceDiags.HasErrors() {
				hclDiags := sourceDiags.ToHCL()
				// The diagnostics from ParseProviderSourceString don't contain
				// source location information because it has no context to compute
				// them from, and so we'll add those in quickly here before we
				// return.
				for _, diag := range hclDiags {
					if diag.Subject == nil {
						diag.Subject = kv.Value.Range().Ptr()
					}
				}
				diags = append(diags, hclDiags...)
				return nil, diags
			}

			ssProvider.Source = source.AsString()
			ssProvider.Type = fqn
		default:
			diags = append(diags, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid state_store_provider object",
				Detail:   `state_store_provider objects can only contain "version" and "source" attributes.`,
				Subject:  kv.Key.Range().Ptr(),
			})
			return nil, diags
		}
	}

	// Now both the source and version have been parsed, we can populate
	// the Requirement field with a single provider requirement.
	req := make(providerreqs.Requirements, 1)
	req[ssProvider.Type] = constraints
	ssProvider.Requirement = req

	return &ssProvider, diags
}

// stateMigrationFileSchema is the schema for a .tfmigrate.hcl file, for use with
// the `state migrate` command.
// Whereas the current Terraform config (.tf) defines the destination that state should
// be migrated to, these files define how a backend or state store was previously configured.
// Due to this, these files define the source where migrated state is copied from.
var stateMigrationFileSchema = &hcl.BodySchema{
	Blocks: []hcl.BlockHeaderSchema{
		{
			Type: "state_store_provider",
		},
		{
			Type: "from",
		},
	},
}

// fromBlockSchema is the schema for 'from' blocks within .tfmigrate.hcl files.
var fromBlockSchema = &hcl.BodySchema{
	Blocks: []hcl.BlockHeaderSchema{
		{
			Type:       "state_store",
			LabelNames: []string{"type"},
		},
		{
			Type:       "backend",
			LabelNames: []string{"type"},
		},
	},
}

func badVersionConstraintErrorDiag(version string, subject *hcl.Range) *hcl.Diagnostic {
	return &hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  `Invalid provider version in "state_store_provider" configuration block`,
		Detail:   fmt.Sprintf("Version must be a string, specifying a single version, but got: %s. Please ensure the version string is valid and specifies a single version in format \"1.0.0\"", version),
		Subject:  subject,
	}
}
