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

package tfdiags

import (
	"bytes"
	"errors"
	"fmt"
	"path/filepath"
	"sort"
	"strings"

	"github.com/hashicorp/hcl/v2"
)

// Diagnostics is a list of diagnostics. Diagnostics is intended to be used
// where a Go "error" might normally be used, allowing richer information
// to be conveyed (more context, support for warnings).
//
// A nil Diagnostics is a valid, empty diagnostics list, thus allowing
// heap allocation to be avoided in the common case where there are no
// diagnostics to report at all.
type Diagnostics []Diagnostic

// Append is the main interface for constructing Diagnostics lists, taking
// an existing list (which may be nil) and appending the new objects to it
// after normalizing them to be implementations of Diagnostic.
//
// The usual pattern for a function that natively "speaks" diagnostics is:
//
//	// Create a nil Diagnostics at the start of the function
//	var diags diag.Diagnostics
//
//	// At later points, build on it if errors / warnings occur:
//	foo, err := DoSomethingRisky()
//	if err != nil {
//	    diags = diags.Append(err)
//	}
//
//	// Eventually return the result and diagnostics in place of error
//	return result, diags
//
// Append accepts a variety of different diagnostic-like types, including
// native Go errors and HCL diagnostics. It also knows how to unwrap
// a multierror.Error into separate error diagnostics. It can be passed
// another Diagnostics to concatenate the two lists. If given something
// it cannot handle, this function will panic.
func (diags Diagnostics) Append(new ...interface{}) Diagnostics {
	for _, item := range new {
		if item == nil {
			continue
		}

		switch ti := item.(type) {
		case Diagnostic:
			diags = append(diags, ti)
		case Diagnostics:
			diags = append(diags, ti...) // flatten
		case DiagnosticsAsError:
			diags = diags.Append(ti.Diagnostics) // unwrap
		case NonFatalError:
			diags = diags.Append(ti.Diagnostics) // unwrap
		case hcl.Diagnostics:
			for _, hclDiag := range ti {
				diags = append(diags, hclDiagnostic{hclDiag})
			}
		case *hcl.Diagnostic:
			diags = append(diags, hclDiagnostic{ti})
		case error:
			diags = append(diags, diagnosticsForError(ti)...)
		default:
			panic(fmt.Errorf("can't construct diagnostic(s) from %T", item))
		}
	}

	// Given the above, we should never end up with a non-nil empty slice
	// here, but we'll make sure of that so callers can rely on empty == nil
	if len(diags) == 0 {
		return nil
	}

	return diags
}

// ContainsDiagnostic returns true of a given diagnostic is contained
// within the Diagnostics slice.
// Comparisons are done via [ComparableDiagnostic].
func (diags Diagnostics) ContainsDiagnostic(diag ComparableDiagnostic) bool {
	for _, d := range diags {
		if cd, ok := d.(ComparableDiagnostic); ok && diag.Equals(cd) {
			return true
		}
	}
	return false
}

// AppendWithoutDuplicates appends a Diagnostic unless one is already contained
// according to [ContainsDiagnostic], i.e. based on [ComparableDiagnostic].
func (diags Diagnostics) AppendWithoutDuplicates(newDiags ...Diagnostic) Diagnostics {
	for _, newItem := range newDiags {
		if newItem == nil {
			continue
		}

		cd, ok := newItem.(ComparableDiagnostic)
		if !ok {
			// append what we cannot compare
			diags = diags.Append(newItem)
			continue
		}
		if diags.ContainsDiagnostic(cd) {
			continue
		}

		diags = diags.Append(newItem)
	}

	if len(diags) == 0 {
		return nil
	}

	return diags
}

func diagnosticsForError(err error) []Diagnostic {
	if err == nil {
		return nil
	}

	// This is the interface implemented by the result of the
	// standard library errors.Join function, which combines
	// multiple errors together into a single error value.
	type UnwrapJoined interface {
		Unwrap() []error
	}
	if err, ok := err.(UnwrapJoined); ok {
		errs := err.Unwrap()
		if len(errs) == 0 { // weird, but harmless!
			return nil
		}
		// We'll start with the assumption of 1:1 relationship between
		// errors and diagnostics, but we'll grow this if one of
		// the wrapped errors becomes multiple diagnostics itself.
		ret := make([]Diagnostic, 0, len(errs))
		for _, err := range errs {
			ret = append(ret, diagnosticsForError(err)...)
		}
		return ret
	}

	// If we've wrapped a Diagnostics in an error then we'll unwrap
	// it and add it directly.
	var asErr DiagnosticsAsError
	if errors.As(err, &asErr) {
		return asErr.Diagnostics
	}

	// We also support wrapping diagnostics in a special kind of error
	// that might contain only warnings, in special cases where the
	// caller and callee are both aware of that convention.
	var asErrWithWarnings NonFatalError
	if errors.As(err, &asErrWithWarnings) {
		return asErrWithWarnings.Diagnostics
	}

	// Finally, HCL's own Diagnostics type implements error and so we
	// might have been given HCL diagnostics directly.
	var asHCLDiags hcl.Diagnostics
	if errors.As(err, &asHCLDiags) {
		ret := make([]Diagnostic, len(asHCLDiags))
		for i, hclDiag := range asHCLDiags {
			ret[i] = hclDiagnostic{hclDiag}
		}
		return ret
	}

	// If none of the special treatments above applied then we'll just
	// wrap the given error as a single (low-quality) diagnostic.
	return []Diagnostic{
		nativeError{err},
	}
}

// Warnings returns a Diagnostics list containing only diagnostics with a severity of Warning.
func (diags Diagnostics) Warnings() Diagnostics {
	var warns = Diagnostics{}
	for _, diag := range diags {
		if diag.Severity() == Warning {
			warns = append(warns, diag)
		}
	}
	return warns
}

// ErrorsOnly returns a Diagnostics list containing only diagnostics with a severity of Error.
func (diags Diagnostics) ErrorsOnly() Diagnostics {
	var errorsOnly = Diagnostics{}
	for _, diag := range diags {
		if diag.Severity() == Error {
			errorsOnly = append(errorsOnly, diag)
		}
	}
	return errorsOnly
}

// HasErrors returns true if any of the diagnostics in the list have
// a severity of Error.
func (diags Diagnostics) HasErrors() bool {
	for _, diag := range diags {
		if diag.Severity() == Error {
			return true
		}
	}
	return false
}

// HasWarnings returns true if any of the diagnostics in the list have
// a severity of Warning.
func (diags Diagnostics) HasWarnings() bool {
	for _, diag := range diags {
		if diag.Severity() == Warning {
			return true
		}
	}
	return false
}

// ForRPC returns a version of the receiver that has been simplified so that
// it is friendly to RPC protocols.
//
// Currently this means that it can be serialized with encoding/gob and
// subsequently re-inflated. It may later grow to include other serialization
// formats.
//
// Note that this loses information about the original objects used to
// construct the diagnostics, so e.g. the errwrap API will not work as
// expected on an error-wrapped Diagnostics that came from ForRPC.
func (diags Diagnostics) ForRPC() Diagnostics {
	ret := make(Diagnostics, len(diags))
	for i := range diags {
		ret[i] = makeRPCFriendlyDiag(diags[i])
	}
	return ret
}

// Err flattens a diagnostics list into a single Go error, or to nil
// if the diagnostics list does not include any error-level diagnostics.
//
// This can be used to smuggle diagnostics through an API that deals in
// native errors, but unfortunately it will lose any warnings that aren't
// accompanied by at least one error since such APIs have no mechanism through
// which to report those.
//
//	return result, diags.Error()
func (diags Diagnostics) Err() error {
	if !diags.HasErrors() {
		return nil
	}
	return DiagnosticsAsError{diags}
}

// ErrWithWarnings is similar to Err except that it will also return a non-nil
// error if the receiver contains only warnings.
//
// In the warnings-only situation, the result is guaranteed to be of dynamic
// type NonFatalError, allowing diagnostics-aware callers to type-assert
// and unwrap it, treating it as non-fatal.
//
// This should be used only in contexts where the caller is able to recognize
// and handle NonFatalError. For normal callers that expect a lack of errors
// to be signaled by nil, use just Diagnostics.Err.
func (diags Diagnostics) ErrWithWarnings() error {
	if len(diags) == 0 {
		return nil
	}
	if diags.HasErrors() {
		return diags.Err()
	}
	return NonFatalError{diags}
}

// NonFatalErr is similar to Err except that it always returns either nil
// (if there are no diagnostics at all) or NonFatalError.
//
// This allows diagnostics to be returned over an error return channel while
// being explicit that the diagnostics should not halt processing.
//
// This should be used only in contexts where the caller is able to recognize
// and handle NonFatalError. For normal callers that expect a lack of errors
// to be signaled by nil, use just Diagnostics.Err.
func (diags Diagnostics) NonFatalErr() error {
	if len(diags) == 0 {
		return nil
	}
	return NonFatalError{diags}
}

// Sort applies an ordering to the diagnostics in the receiver in-place.
//
// The ordering is: warnings before errors, sourceless before sourced,
// short source paths before long source paths, and then ordering by
// position within each file.
//
// Diagnostics that do not differ by any of these sortable characteristics
// will remain in the same relative order after this method returns.
func (diags Diagnostics) Sort() {
	sort.Stable(sortDiagnostics(diags))
}

// DiagnosticsAsError embeds diagnostics, and satisfies the error interface.
type DiagnosticsAsError struct {
	Diagnostics
}

func (dae DiagnosticsAsError) Error() string {
	diags := dae.Diagnostics
	switch {
	case len(diags) == 0:
		// should never happen, since we don't create this wrapper if
		// there are no diagnostics in the list.
		return "no errors"
	case len(diags) == 1:
		desc := diags[0].Description()
		if desc.Detail == "" {
			return desc.Summary
		}
		return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail)
	default:
		var ret bytes.Buffer
		fmt.Fprintf(&ret, "%d problems:\n", len(diags))
		for _, diag := range dae.Diagnostics {
			desc := diag.Description()
			if desc.Detail == "" {
				fmt.Fprintf(&ret, "\n- %s", desc.Summary)
			} else {
				fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail)
			}
		}
		return ret.String()
	}
}

// WrappedErrors is an implementation of errwrap.Wrapper so that an error-wrapped
// diagnostics object can be picked apart by errwrap-aware code.
func (dae DiagnosticsAsError) WrappedErrors() []error {
	var errs []error
	for _, diag := range dae.Diagnostics {
		if wrapper, isErr := diag.(nativeError); isErr {
			errs = append(errs, wrapper.err)
		}
	}
	return errs
}

// NonFatalError is a special error type, returned by
// Diagnostics.ErrWithWarnings and Diagnostics.NonFatalErr,
// that indicates that the wrapped diagnostics should be treated as non-fatal.
// Callers can conditionally type-assert an error to this type in order to
// detect the non-fatal scenario and handle it in a different way.
type NonFatalError struct {
	Diagnostics
}

func (woe NonFatalError) Error() string {
	diags := woe.Diagnostics
	switch {
	case len(diags) == 0:
		// should never happen, since we don't create this wrapper if
		// there are no diagnostics in the list.
		return "no errors or warnings"
	case len(diags) == 1:
		desc := diags[0].Description()
		if desc.Detail == "" {
			return desc.Summary
		}
		return fmt.Sprintf("%s: %s", desc.Summary, desc.Detail)
	default:
		var ret bytes.Buffer
		if diags.HasErrors() {
			fmt.Fprintf(&ret, "%d problems:\n", len(diags))
		} else {
			fmt.Fprintf(&ret, "%d warnings:\n", len(diags))
		}
		for _, diag := range woe.Diagnostics {
			desc := diag.Description()
			if desc.Detail == "" {
				fmt.Fprintf(&ret, "\n- %s", desc.Summary)
			} else {
				fmt.Fprintf(&ret, "\n- %s: %s", desc.Summary, desc.Detail)
			}
		}
		return ret.String()
	}
}

// sortDiagnostics is an implementation of sort.Interface
type sortDiagnostics []Diagnostic

var _ sort.Interface = sortDiagnostics(nil)

func (sd sortDiagnostics) Len() int {
	return len(sd)
}

func (sd sortDiagnostics) Less(i, j int) bool {
	iD, jD := sd[i], sd[j]
	iSev, jSev := iD.Severity(), jD.Severity()
	iSrc, jSrc := iD.Source(), jD.Source()

	switch {

	case iSev != jSev:
		return iSev == Warning

	case (iSrc.Subject == nil) != (jSrc.Subject == nil):
		return iSrc.Subject == nil

	case iSrc.Subject != nil && *iSrc.Subject != *jSrc.Subject:
		iSubj := iSrc.Subject
		jSubj := jSrc.Subject
		switch {
		case iSubj.Filename != jSubj.Filename:
			// Path with fewer segments goes first if they are different lengths
			sep := string(filepath.Separator)
			iCount := strings.Count(iSubj.Filename, sep)
			jCount := strings.Count(jSubj.Filename, sep)
			if iCount != jCount {
				return iCount < jCount
			}
			return iSubj.Filename < jSubj.Filename
		case iSubj.Start.Byte != jSubj.Start.Byte:
			return iSubj.Start.Byte < jSubj.Start.Byte
		case iSubj.End.Byte != jSubj.End.Byte:
			return iSubj.End.Byte < jSubj.End.Byte
		}
		fallthrough

	default:
		// The remaining properties do not have a defined ordering, so
		// we'll leave it unspecified. Since we use sort.Stable in
		// the caller of this, the ordering of remaining items will
		// be preserved.
		return false
	}
}

func (sd sortDiagnostics) Swap(i, j int) {
	sd[i], sd[j] = sd[j], sd[i]
}
