package toproto

import (
	"unicode/utf8"

	"github.com/hashicorp/terraform-plugin-go/tfprotov5"
	"github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5"
)

func Diagnostic(in *tfprotov5.Diagnostic) (*tfplugin5.Diagnostic, error) {
	diag := &tfplugin5.Diagnostic{
		Severity: Diagnostic_Severity(in.Severity),
		Summary:  forceValidUTF8(in.Summary),
		Detail:   forceValidUTF8(in.Detail),
	}
	if in.Attribute != nil {
		attr, err := AttributePath(in.Attribute)
		if err != nil {
			return diag, err
		}
		diag.Attribute = attr
	}
	return diag, nil
}

func Diagnostic_Severity(in tfprotov5.DiagnosticSeverity) tfplugin5.Diagnostic_Severity {
	return tfplugin5.Diagnostic_Severity(in)
}

func Diagnostics(in []*tfprotov5.Diagnostic) ([]*tfplugin5.Diagnostic, error) {
	diagnostics := make([]*tfplugin5.Diagnostic, 0, len(in))
	for _, diag := range in {
		if diag == nil {
			diagnostics = append(diagnostics, nil)
			continue
		}
		d, err := Diagnostic(diag)
		if err != nil {
			return diagnostics, err
		}
		diagnostics = append(diagnostics, d)
	}
	return diagnostics, nil
}

// forceValidUTF8 returns a string guaranteed to be valid UTF-8 even if the
// input isn't, by replacing any invalid bytes with a valid UTF-8 encoding of
// the Unicode Replacement Character (\uFFFD).
//
// The protobuf serialization library will reject invalid UTF-8 with an
// unhelpful error message:
//
//	string field contains invalid UTF-8
//
// Passing a string result through this function makes invalid UTF-8 instead
// emerge as placeholder characters on the other side of the wire protocol,
// giving a better chance of still returning a partially-legible message
// instead of a generic character encoding error.
//
// This is intended for user-facing messages such as diagnostic summary and
// detail messages, where Terraform will just treat the value as opaque and
// it's ultimately up to the user and their terminal or web browser to
// interpret the result. Don't use this for strings that have machine-readable
// meaning.
func forceValidUTF8(s string) string {
	// Most strings that pass through here will already be valid UTF-8 and
	// utf8.ValidString has a fast path which will beat our rune-by-rune
	// analysis below, so it's worth the cost of walking the string twice
	// in the rarer invalid case.
	if utf8.ValidString(s) {
		return s
	}

	// If we get down here then we know there's at least one invalid UTF-8
	// sequence in the string, so in this slow path we'll reconstruct the
	// string one rune at a time, guaranteeing that we'll only write valid
	// UTF-8 sequences into the resulting buffer.
	//
	// Any invalid string will grow at least a little larger as a result of
	// this operation because we'll be replacing each invalid byte with
	// the three-byte sequence \xEF\xBF\xBD, which is the UTF-8 encoding of
	// the replacement character \uFFFD. 9 is a magic number giving room for
	// three such expansions without any further allocation.
	ret := make([]byte, 0, len(s)+9)
	for {
		// If the first byte in s is not the start of a valid UTF-8 sequence
		// then the following will return utf8.RuneError, 1, where
		// utf8.RuneError is the unicode replacement character.
		r, advance := utf8.DecodeRuneInString(s)
		if advance == 0 {
			break
		}
		s = s[advance:]
		ret = utf8.AppendRune(ret, r)
	}
	return string(ret)
}

// we have to say this next thing to get golint to stop yelling at us about the
// underscores in the function names. We want the function names to match
// actually-generated code, so it feels like fair play. It's just a shame we
// lose golint for the entire file.
//
// This file is not actually generated. You can edit it. Ignore this next line.
// Code generated by hand ignore this next bit DO NOT EDIT.
