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

package rpcapi

import (
	"bytes"
	"errors"
	"fmt"

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

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/policy"
	"github.com/hashicorp/terraform/internal/policy/proto"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1"
	"github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks"
	"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
	"github.com/hashicorp/terraform/internal/stacks/stackruntime"
	"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

func diagnosticsToProto(diags tfdiags.Diagnostics) []*terraform1.Diagnostic {
	if len(diags) == 0 {
		return nil
	}

	ret := make([]*terraform1.Diagnostic, len(diags))
	for i, diag := range diags {
		ret[i] = diagnosticToProto(diag)
	}
	return ret
}

func diagnosticToProto(diag tfdiags.Diagnostic) *terraform1.Diagnostic {
	protoDiag := &terraform1.Diagnostic{}

	switch diag.Severity() {
	case tfdiags.Error:
		protoDiag.Severity = terraform1.Diagnostic_ERROR
	case tfdiags.Warning:
		protoDiag.Severity = terraform1.Diagnostic_WARNING
	default:
		protoDiag.Severity = terraform1.Diagnostic_INVALID
	}

	desc := diag.Description()
	protoDiag.Summary = desc.Summary
	protoDiag.Detail = desc.Detail

	srcRngs := diag.Source()
	if srcRngs.Subject != nil {
		protoDiag.Subject = sourceRangeToProto(*srcRngs.Subject)
	}
	if srcRngs.Context != nil {
		protoDiag.Context = sourceRangeToProto(*srcRngs.Context)
	}

	return protoDiag
}

func sourceRangeToProto(rng tfdiags.SourceRange) *terraform1.SourceRange {
	return &terraform1.SourceRange{
		// RPC API operations use source address syntax for "filename" by
		// convention, because the physical filesystem layout is an
		// implementation detail.
		SourceAddr: rng.Filename,

		Start: sourcePosToProto(rng.Start),
		End:   sourcePosToProto(rng.End),
	}
}

func sourceRangeFromProto(protoRng *terraform1.SourceRange) tfdiags.SourceRange {
	return tfdiags.SourceRange{
		Filename: protoRng.SourceAddr,
		Start:    sourcePosFromProto(protoRng.Start),
		End:      sourcePosFromProto(protoRng.End),
	}
}

func sourceRangeFromHCL(rng hcl.Range) tfdiags.SourceRange {
	return tfdiags.SourceRange{
		Filename: rng.Filename,
		Start: tfdiags.SourcePos{
			Line:   rng.Start.Line,
			Column: rng.Start.Column,
			Byte:   rng.Start.Byte,
		},
		End: tfdiags.SourcePos{
			Line:   rng.End.Line,
			Column: rng.End.Column,
			Byte:   rng.End.Byte,
		},
	}
}

func sourcePosToProto(pos tfdiags.SourcePos) *terraform1.SourcePos {
	return &terraform1.SourcePos{
		Byte:   int64(pos.Byte),
		Line:   int64(pos.Line),
		Column: int64(pos.Column),
	}
}

func sourcePosFromProto(protoPos *terraform1.SourcePos) tfdiags.SourcePos {
	return tfdiags.SourcePos{
		Byte:   int(protoPos.Byte),
		Line:   int(protoPos.Line),
		Column: int(protoPos.Column),
	}
}

func dynamicTypedValueFromProto(protoVal *stacks.DynamicValue) (cty.Value, error) {
	if len(protoVal.Msgpack) == 0 {
		return cty.DynamicVal, fmt.Errorf("uses unsupported serialization format (only MessagePack is supported)")
	}
	v, err := msgpack.Unmarshal(protoVal.Msgpack, cty.DynamicPseudoType)
	if err != nil {
		return cty.DynamicVal, fmt.Errorf("invalid serialization: %w", err)
	}
	// FIXME: Incredibly imprecise handling of sensitive values. We should
	// actually decode the attribute paths and mark individual leaf attributes
	// that are sensitive, but for now we'll just mark the whole thing as
	// sensitive if any part of it is sensitive.
	if len(protoVal.Sensitive) != 0 {
		v = v.Mark(marks.Sensitive)
	}
	return v, nil
}

func externalInputValuesFromProto(protoVals map[string]*stacks.DynamicValueWithSource) (map[stackaddrs.InputVariable]stackruntime.ExternalInputValue, error) {
	if len(protoVals) == 0 {
		return nil, nil
	}
	var err error
	ret := make(map[stackaddrs.InputVariable]stackruntime.ExternalInputValue, len(protoVals))
	for name, protoVal := range protoVals {
		v, moreErr := externalInputValueFromProto(protoVal)
		if moreErr != nil {
			err = errors.Join(err, fmt.Errorf("%s: %w", name, moreErr))
		}
		ret[stackaddrs.InputVariable{Name: name}] = v
	}
	return ret, err
}

func externalInputValueFromProto(protoVal *stacks.DynamicValueWithSource) (stackruntime.ExternalInputValue, error) {
	v, err := dynamicTypedValueFromProto(protoVal.Value)
	if err != nil {
		return stackruntime.ExternalInputValue{}, nil
	}
	rng := sourceRangeFromProto(protoVal.SourceRange)
	return stackruntime.ExternalInputValue{
		Value:    v,
		DefRange: rng,
	}, nil
}

func componentInstancePolicyEvaluationProto(componentInstanceAddr stackaddrs.AbsComponentInstance, policyResults map[string]policy.EvaluationResponse) *stacks.ComponentInstancePolicyEvaluation {
	results := make([]*stacks.PolicyResult, 0)
	infos := make([]*stacks.PolicyInfo, 0)
	policyDiags := make([]*stacks.PolicyDiagnostic, 0)

	for addr, result := range policyResults {
		results = append(results, policyResultsToProto(addr, result.Policies)...)
		infos = append(infos, policyInfosToProto(addr, result.Enforcements)...)
		policyDiags = append(policyDiags, policyDiagsToProto(addr, result.Diagnostics)...)
	}

	return &stacks.ComponentInstancePolicyEvaluation{
		Addr: &stacks.ComponentInstanceInStackAddr{
			ComponentAddr:         stackaddrs.ConfigComponentForAbsInstance(componentInstanceAddr).String(),
			ComponentInstanceAddr: componentInstanceAddr.String(),
		},
		Results:     results,
		Infos:       infos,
		Diagnostics: policyDiags,
	}
}

func providerInstancePolicyEvaluationProto(result *hooks.ProviderInstancePolicyResults) *stacks.ProviderInstancePolicyEvaluation {
	resp := result.Result
	results := policyResultsToProto(result.ProviderAddr, resp.Policies)
	infos := policyInfosToProto(result.ProviderAddr, resp.Enforcements)
	policyDiags := policyDiagsToProto(result.ProviderAddr, resp.Diagnostics)

	return &stacks.ProviderInstancePolicyEvaluation{
		Addr: &stacks.ProviderInstanceInStackAddr{
			ProviderAddr:         stackaddrs.ConfigProviderConfigForAbsInstance(result.Addr).String(),
			ProviderInstanceAddr: result.Addr.String(),
		},
		Results:     results,
		Infos:       infos,
		Diagnostics: policyDiags,
	}
}

func policyEvaluateResultToProto(result policy.EvaluateResult) stacks.EvaluateResult {
	switch result {
	case policy.InvalidResult:
		return stacks.EvaluateResult_INVALID_EVALUATE_RESULT
	case policy.UnknownResult:
		return stacks.EvaluateResult_UNKNOWN_EVALUATE_RESULT
	case policy.PolicyErrorResult:
		return stacks.EvaluateResult_ERROR_EVALUATE_RESULT
	case policy.AllowResult:
		return stacks.EvaluateResult_ALLOW_EVALUATE_RESULT
	case policy.DenyResult:
		return stacks.EvaluateResult_DENY_EVALUATE_RESULT
	case policy.SetupErrorResult:
		return stacks.EvaluateResult_SETUP_ERROR_EVALUATE_RESULT
	default:
		// should be exhaustive
		panic(fmt.Errorf("unhandled policy.EvaluateResult type: %T", result))
	}
}

func policyResultsToProto(addr string, policies []*policy.Policy) []*stacks.PolicyResult {
	protoPolicyResults := make([]*stacks.PolicyResult, len(policies))
	for i, policy := range policies {
		result := stacks.PolicyResult{
			TargetAddress:  addr,
			PolicyMetadata: policyMetadataToProto(policy, nil),
			Result:         policyEvaluateResultToProto(policy.Result),
		}
		protoPolicyResults[i] = &result
	}
	return protoPolicyResults
}

func policyMetadataToProto(policyObj *policy.Policy, enforceIndex *int32) *stacks.PolicyMetaData {
	if policyObj == nil {
		return nil
	}
	metadata := &stacks.PolicyMetaData{
		PolicySetName:    policyObj.PolicySetName,
		PolicyName:       policyObj.Address,
		FileName:         policyObj.Filename,
		EnforcementLevel: policyObj.EnforcementLevel,
	}

	if enforceIndex != nil {
		metadata.EnforceIndex = *enforceIndex
	}

	return metadata
}

func policyInfosToProto(addr string, enforcements []policy.EnforcementResult) []*stacks.PolicyInfo {
	protoPolicyInfos := make([]*stacks.PolicyInfo, 0)

	for _, enforcement := range enforcements {
		if enforcement.Message == "" {
			continue
		}
		protoPolicyMetadata := policyMetadataToProto(enforcement.Policy, &enforcement.BlockIndex)

		var protoPolicySnippet *stacks.PolicySnippet
		if snippet := enforcement.Snippet; snippet != nil {
			protoPolicySnippet = &stacks.PolicySnippet{
				Code:                 snippet.Code,
				StartLine:            snippet.StartLine,
				HighlightStartOffset: snippet.HighlightStartOffset,
				HighlightEndOffset:   snippet.HighlightEndOffset,
			}
			if snippet.Context != nil {
				protoPolicySnippet.Context = *snippet.Context
			}
		}

		var protoPolicyRange *terraform1.SourceRange
		if enforcement.Range != nil {
			rng := sourceRangeFromHCL(*enforcement.Range)
			protoPolicyRange = &terraform1.SourceRange{
				SourceAddr: enforcement.Range.Filename,
				Start:      sourcePosToProto(rng.Start),
				End:        sourcePosToProto(rng.End),
			}
		}

		protoPolicyInfos = append(protoPolicyInfos, &stacks.PolicyInfo{
			TargetAddress:  addr,
			Result:         policyEvaluateResultToProto(enforcement.Result),
			Message:        enforcement.Message,
			PolicySnippet:  protoPolicySnippet,
			PolicyMetadata: protoPolicyMetadata,
			PolicyRange:    protoPolicyRange,
		})
	}

	return protoPolicyInfos
}

func policyDiagsToProto(addr string, policyDiags policy.Diagnostics) []*stacks.PolicyDiagnostic {
	protoPolicyDiags := make([]*stacks.PolicyDiagnostic, len(policyDiags))

	for i, diag := range policyDiags {
		desc := diag.Description()
		extra := tfdiags.ExtraInfo[*policy.PolicyExtra](diag)

		policyDiag := stacks.PolicyDiagnostic{
			TargetAddress: addr,
			Diagnostic: &terraform1.Diagnostic{
				Severity: terraform1.Diagnostic_ERROR,
				Summary:  desc.Summary,
				Detail:   desc.Detail,
			},
		}

		if extra != nil {
			if extra.Severity == hcl.DiagWarning {
				policyDiag.Diagnostic.Severity = terraform1.Diagnostic_WARNING
			}

			policyDiag.Result = policyEvaluateResultToProto(extra.Result)
			policyDiag.PolicyMetadata = policyMetadataToProto(&extra.Policy, extra.EnforceIndex)

			if snippet := extra.Snippet; snippet != nil {
				policyDiag.PolicySnippet = &stacks.PolicySnippet{
					Code:                 snippet.Code,
					StartLine:            snippet.StartLine,
					HighlightStartOffset: snippet.HighlightStartOffset,
					HighlightEndOffset:   snippet.HighlightEndOffset,
				}
				if snippet.Context != nil {
					policyDiag.PolicySnippet.Context = *snippet.Context
				}
			}

			if rng := extra.Range; rng != nil && rng.Subject != nil {
				policyDiag.PolicyRange = &terraform1.SourceRange{
					SourceAddr: rng.Subject.Filename,
				}
				if start := rng.Subject.Start; start != nil {
					policyDiag.PolicyRange.Start = &terraform1.SourcePos{
						Line:   start.Line,
						Column: start.Column,
						Byte:   start.Byte,
					}
				}
				if end := rng.Subject.End; end != nil {
					policyDiag.PolicyRange.End = &terraform1.SourcePos{
						Line:   end.Line,
						Column: end.Column,
						Byte:   end.Byte,
					}
				}
			}

			policyDiag.ExpressionValues = policyExpressionValuesToProto(extra.ExpressionValues)
		}

		if src := diag.Source(); src.Subject != nil {
			policyDiag.Diagnostic.Subject = sourceRangeToProto(*src.Subject)
		}
		if src := diag.Source(); src.Context != nil {
			policyDiag.Diagnostic.Context = sourceRangeToProto(*src.Context)
		}

		protoPolicyDiags[i] = &policyDiag
	}

	return protoPolicyDiags
}

func policyExpressionValuesToProto(policyExpressionValues []*proto.ExpressionValue) []*stacks.ExpressionValue {
	if len(policyExpressionValues) == 0 {
		return nil
	}

	expressionValues := make([]*stacks.ExpressionValue, 0, len(policyExpressionValues))
	seen := make(map[string]struct{}, len(policyExpressionValues))

	for _, val := range policyExpressionValues {
		path, err := val.Traversal.ToCtyPath()
		if err != nil {
			continue // then we can't display this value
		}

		exprValue := &stacks.ExpressionValue{
			Traversal: stacks.NewAttributePath(path),
		}

		strPath := ctyPathStr(path)
		if _, exists := seen[strPath]; exists {
			continue
		}
		seen[strPath] = struct{}{}

		exprValue.Value = val.Value
		expressionValues = append(expressionValues, exprValue)
	}

	return expressionValues
}

func ctyPathStr(path cty.Path) string {
	// This is a specialized subset of traversal rendering tailored to
	// producing a string that can be used to detect duplicate cty paths.
	// It is not comprehensive nor intended to be used for other purposes.

	var buf bytes.Buffer
	first := true
	for _, step := range path {
		switch tStep := step.(type) {
		case cty.GetAttrStep:
			if !first {
				buf.WriteByte('.')
			}
			buf.WriteString(tStep.Name)
		case cty.IndexStep:
			buf.WriteByte('[')
			if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {
				buf.WriteString(tfdiags.CompactValueStr(tStep.Key))
			} else {
				// We'll just use a placeholder for more complex values,
				// since otherwise our result could grow ridiculously long.
				buf.WriteString("...")
			}
			buf.WriteByte(']')
		}
		first = false
	}
	return buf.String()
}
