// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: MPL-2.0

package earlydecoder

import (
	"fmt"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/ext/typeexpr"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/terraform-schema/policy"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
)

// decodedPolicy is the type representing a decoded Terraform policy.
type decodedPolicy struct {
	RequiredCore     []string
	ResourcePolicies map[string]*policy.ResourcePolicy
	ProviderPolicies map[string]*policy.ProviderPolicy
	ModulePolicies   map[string]*policy.ModulePolicy
	Inputs           map[string]*policy.Input
}

func newDecodedPolicy() *decodedPolicy {
	return &decodedPolicy{
		RequiredCore:     make([]string, 0),
		ResourcePolicies: make(map[string]*policy.ResourcePolicy),
		ProviderPolicies: make(map[string]*policy.ProviderPolicy),
		ModulePolicies:   make(map[string]*policy.ModulePolicy),
		Inputs:           make(map[string]*policy.Input),
	}
}

// loadPolicyFromFile reads given file, interprets it and stores in given Policy
// This is useful for any caller which does tokenization/parsing on its own
// e.g. because it will reuse these parsed files later for more detailed
// interpretation.
func loadPolicyFromFile(file *hcl.File, decodedPolicy *decodedPolicy) hcl.Diagnostics {
	var diags hcl.Diagnostics
	content, _, contentDiags := file.Body.PartialContent(rootSchema)
	diags = append(diags, contentDiags...)

	for _, block := range content.Blocks {
		switch block.Type {

		case "policy":
			content, _, contentDiags := block.Body.PartialContent(policyBlockSchema)
			diags = append(diags, contentDiags...)

			for _, block := range content.Blocks {
				switch block.Type {
				case "terraform_config":
					content, _, contentDiags := block.Body.PartialContent(terraformConfigBlockSchema)
					diags = append(diags, contentDiags...)

					if attr, defined := content.Attributes["required_version"]; defined {
						var version string
						inputDiags := gohcl.DecodeExpression(attr.Expr, nil, &version)
						diags = append(diags, inputDiags...)
						if !inputDiags.HasErrors() {
							decodedPolicy.RequiredCore = append(decodedPolicy.RequiredCore, version)
						}
					}
				}
			}

		case "resource_policy":
			_, _, contentDiags := block.Body.PartialContent(resourcePolicyBlockSchema)
			diags = append(diags, contentDiags...)

			if len(block.Labels) != 2 || block.Labels[0] == "" || block.Labels[1] == "" {
				continue
			}

			body, ok := block.Body.(*hclsyntax.Body)
			if !ok {
				continue
			}
			resType := block.Labels[0]
			resName := block.Labels[1]
			key := resType + "." + resName

			decodedPolicy.ResourcePolicies[key] = &policy.ResourcePolicy{
				Type:  resType,
				Name:  resName,
				Range: body.SrcRange,
			}

		case "provider_policy":
			_, _, contentDiags := block.Body.PartialContent(providerPolicyBlockSchema)
			diags = append(diags, contentDiags...)

			if len(block.Labels) != 2 || block.Labels[0] == "" || block.Labels[1] == "" {
				continue
			}

			body, ok := block.Body.(*hclsyntax.Body)
			if !ok {
				continue
			}
			resType := block.Labels[0]
			resName := block.Labels[1]
			key := resType + "." + resName

			decodedPolicy.ProviderPolicies[key] = &policy.ProviderPolicy{
				Type:  resType,
				Name:  resName,
				Range: body.SrcRange,
			}

		case "module_policy":
			_, _, contentDiags := block.Body.PartialContent(modulePolicyBlockSchema)
			diags = append(diags, contentDiags...)

			if len(block.Labels) != 2 || block.Labels[0] == "" || block.Labels[1] == "" {
				continue
			}

			body, ok := block.Body.(*hclsyntax.Body)
			if !ok {
				continue
			}
			resType := block.Labels[0]
			resName := block.Labels[1]
			key := resType + "." + resName

			decodedPolicy.ModulePolicies[key] = &policy.ModulePolicy{
				Type:  resType,
				Name:  resName,
				Range: body.SrcRange,
			}

		case "input":
			content, _, contentDiags := block.Body.PartialContent(inputSchema)
			diags = append(diags, contentDiags...)

			if len(block.Labels) != 1 || block.Labels[0] == "" {
				continue
			}

			name := block.Labels[0]
			description := ""
			isSensitive := false
			var inputDiags hcl.Diagnostics

			if attr, defined := content.Attributes["description"]; defined {
				inputDiags = gohcl.DecodeExpression(attr.Expr, nil, &description)
				diags = append(diags, inputDiags...)
			}

			if attr, defined := content.Attributes["sensitive"]; defined {
				inputDiags = gohcl.DecodeExpression(attr.Expr, nil, &isSensitive)
				diags = append(diags, inputDiags...)
			}

			inputType := cty.DynamicPseudoType
			var defaults *typeexpr.Defaults
			if attr, defined := content.Attributes["type"]; defined {
				inputType, defaults, inputDiags = typeexpr.TypeConstraintWithDefaults(attr.Expr)
				diags = append(diags, inputDiags...)
			}

			defaultValue := cty.NilVal
			if attr, defined := content.Attributes["default"]; defined {
				val, vDiags := attr.Expr.Value(nil)
				diags = append(diags, vDiags...)
				if !vDiags.HasErrors() {
					if inputType != cty.NilType {
						var err error
						val, err = convert.Convert(val, inputType)
						if err != nil {
							diags = append(diags, &hcl.Diagnostic{
								Severity: hcl.DiagError,
								Summary:  "Invalid default value for input",
								Detail:   fmt.Sprintf("This default value is not compatible with the input's type constraint: %s.", err),
								Subject:  attr.Expr.Range().Ptr(),
							})
							val = cty.DynamicVal
						}
					}

					defaultValue = val
				}
			}

			decodedPolicy.Inputs[name] = &policy.Input{
				Type:         inputType,
				Description:  description,
				DefaultValue: defaultValue,
				TypeDefaults: defaults,
				IsSensitive:  isSensitive,
			}
		}

	}

	return diags
}
