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

package policy

import (
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/terraform/internal/policy/proto"
	"github.com/hashicorp/terraform/internal/tfdiags"
	"github.com/zclconf/go-cty/cty"
)

// PolicyExtra is a wrapper for policy information that can be used as a diagnostic extra.
// It also combines all the different extra information in the original proto diagnostic.
type PolicyExtra struct {
	Policy
	Severity hcl.DiagnosticSeverity
	Result   EvaluateResult

	// EnforceIndex is the index of the enforce block that generated this diagnostic.
	// This is only set if the diagnostic was generated by an enforce block.
	EnforceIndex *int32

	Snippet          *proto.Snippet
	Range            *proto.RangeExtra
	ExpressionValues []*proto.ExpressionValue
	Attribute        cty.Path
}

// DiagsFromProto converts a slice of proto.Diagnostic to tfdiags.Diagnostics, while wrapping
// the policy information in the diagnostic extra info.
func DiagsFromProto(protoDiags []*proto.Diagnostic, policy *Policy) Diagnostics {
	var ret Diagnostics
	if len(protoDiags) == 0 {
		return ret
	}
	diags := proto.ToHCLDiagnostics(protoDiags)
	for _, diag := range diags {
		ret = append(ret, newPolicyDiagnostic(diag, policy))
	}

	return ret
}

func newPolicyDiagnostic(diag *hcl.Diagnostic, policy *Policy) Diagnostic {
	// policy diags are represented as hcl diagnostics, with the diagnostic containing
	// source information related to the policy files. We convert them to a terraform diag here.
	hclDiag := tfdiags.FromHCL(diag)
	return Diagnostic{original: hclDiag, extra: policyExtra(diag, policy)}
}

func policyExtra(diag *hcl.Diagnostic, policy *Policy) *PolicyExtra {
	diagExtra := &PolicyExtra{Severity: diag.Severity}
	if policy != nil {
		diagExtra.Policy = *policy
	}
	if extra := extraInfo[*proto.EvaluateResultExtra](diag.Extra); extra != nil {
		diagExtra.Result = ResultFromProto(extra.EvaluateResult)
	}
	if extra := extraInfo[*proto.SnippetExtra](diag.Extra); extra != nil {
		diagExtra.Snippet = extra.Snippet
	}
	if extra := extraInfo[*proto.RangeExtra](diag.Extra); extra != nil {
		diagExtra.Range = extra
	}
	if extra := extraInfo[*proto.ExpressionValuesExtra](diag.Extra); extra != nil {
		diagExtra.ExpressionValues = extra.ExpressionValues
	}
	if extra := extraInfo[*proto.AttributeExtra](diag.Extra); extra != nil {
		diagExtra.Attribute = extra.Attribute
	}

	// if we have no policy information, we can use the little we have within the diagnostic itself.
	if extra := extraInfo[*proto.PolicyExtra](diag.Extra); extra != nil && policy == nil {
		diagExtra.Policy = Policy{
			PolicySetName: extra.PolicySet.Name,
			Directory:     extra.PolicySet.Path,
		}
	}

	return diagExtra
}

func extraInfo[T any](extra any) T {
	if extra, ok := extra.(T); ok {
		return extra
	}
	return tfdiags.ExtraInfoNext[T](extra)
}

// Diagnostic contains the diagnostic produced by the policy engine, as well as
// extra information within it.
type Diagnostic struct {
	original tfdiags.Diagnostic
	extra    *PolicyExtra

	// localRange is the range of the terraform object related to the policy being evaluated
	localRange *hcl.Range
}

type Diagnostics []Diagnostic

func (d Diagnostics) HasErrors() bool {
	for _, diag := range d {
		if diag.Severity() == tfdiags.Error {
			return true
		}
	}
	return false
}

// WithLocalRange returns a copy of the response with the local range set to the given value.
// This value typically comes from the range of the terraform object being evaluated.
func (d Diagnostics) WithLocalRange(localRange *hcl.Range) Diagnostics {
	if len(d) == 0 || localRange == nil {
		return d
	}
	for i := range d {
		d[i].localRange = localRange
	}
	return d
}

func (o Diagnostic) Severity() tfdiags.Severity {
	return o.original.Severity()
}

func (o Diagnostic) Description() tfdiags.Description {
	return o.original.Description()
}

// Source returns the local terraform source information.
// The policy source information should be obtained separately from the policy extra information.
func (o Diagnostic) Source() tfdiags.Source {
	var ret tfdiags.Source
	if o.localRange != nil {
		rng := tfdiags.SourceRangeFromHCL(*o.localRange)
		ret.Subject = &rng
		// TODO: get exact context
		ret.Context = &rng
	}
	return ret
}

func (o Diagnostic) FromExpr() *tfdiags.FromExpr {
	return o.original.FromExpr()
}

func (o Diagnostic) ExtraInfo() any {
	return o.extra
}

// WithLocalRange sets the local terraform range, which will be used as the diagnostic's source information
func (o Diagnostic) WithLocalRange(rng *hcl.Range) Diagnostic {
	o.localRange = rng
	return o
}

func NewErrorDiagnostic(summary, detail string, result EvaluateResult) Diagnostic {
	return Diagnostic{
		original: tfdiags.Sourceless(tfdiags.Error, summary, detail),
		extra:    &PolicyExtra{Severity: hcl.DiagError, Result: result},
	}
}

func (diags Diagnostics) AsTerraformDiags() tfdiags.Diagnostics {
	var ret tfdiags.Diagnostics
	for _, diag := range diags {
		ret = ret.Append(diag)
	}
	return ret
}
