package unknown

import (
	"errors"
	"fmt"
	"slices"
	"strings"

	"github.com/anchore/syft/internal/log"
	"github.com/anchore/syft/syft/file"
)

type hasCoordinates interface {
	GetCoordinates() file.Coordinates
}

type CoordinateError struct {
	Coordinates file.Coordinates
	Reason      error
}

var _ error = (*CoordinateError)(nil)

func (u *CoordinateError) Error() string {
	if u.Coordinates.FileSystemID == "" {
		return fmt.Sprintf("%s: %v", u.Coordinates.RealPath, u.Reason)
	}
	return fmt.Sprintf("%s (%s): %v", u.Coordinates.RealPath, u.Coordinates.FileSystemID, u.Reason)
}

// New returns a new CoordinateError unless the reason is a CoordinateError itself, in which case
// reason will be returned directly or if reason is nil, nil will be returned
func New(coords hasCoordinates, reason error) *CoordinateError {
	if reason == nil {
		return nil
	}
	coordinates := coords.GetCoordinates()
	reasonCoordinateError := &CoordinateError{}
	if errors.As(reason, &reasonCoordinateError) {
		// if the reason is already a coordinate error, it is potentially for a different location,
		// so we do not want to surface this location having an error
		return reasonCoordinateError
	}
	return &CoordinateError{
		Coordinates: coordinates,
		Reason:      reason,
	}
}

// Newf returns a new CoordinateError with a reason of an error created from given format and args
func Newf(coords hasCoordinates, format string, args ...any) *CoordinateError {
	return New(coords, fmt.Errorf(format, args...))
}

// Append returns an error joined to the first error/set of errors, with a new CoordinateError appended to the end
func Append(errs error, coords hasCoordinates, reason error) error {
	return Join(errs, New(coords, reason))
}

// Appendf returns an error joined to the first error/set of errors, with a new CoordinateError appended to the end,
// created from the given reason and args
func Appendf(errs error, coords hasCoordinates, format string, args ...any) error {
	return Append(errs, coords, fmt.Errorf(format, args...))
}

// Join joins the provided sets of errors together in a flattened manner, taking into account nested errors created
// from other sources, including errors.Join, multierror.Append, and unknown.Join
func Join(errs ...error) error {
	var out []error
	for _, err := range errs {
		// append errors, de-duplicated
		for _, e := range flatten(err) {
			if containsErr(out, e) {
				continue
			}
			out = append(out, e)
		}
	}
	if len(out) == 1 {
		return out[0]
	}
	if len(out) == 0 {
		return nil
	}
	return errors.Join(out...)
}

// Joinf joins the provided sets of errors together in a flattened manner, taking into account nested errors created
// from other sources, including errors.Join, multierror.Append, and unknown.Join and appending a new error,
// created from the format and args provided -- the error is NOT a CoordinateError
func Joinf(errs error, format string, args ...any) error {
	return Join(errs, fmt.Errorf(format, args...))
}

// IfEmptyf returns a new Errorf-formatted error, only when the provided slice is empty or nil when
// the slice has entries
func IfEmptyf[T any](emptyTest []T, format string, args ...any) error {
	if len(emptyTest) == 0 {
		return fmt.Errorf(format, args...)
	}
	return nil
}

// ExtractCoordinateErrors extracts all coordinate errors returned, and any _additional_ errors in the graph
// are encapsulated in the second, error return parameter
func ExtractCoordinateErrors(err error) (coordinateErrors []CoordinateError, remainingErrors error) {
	remainingErrors = visitErrors(err, func(e error) error {
		if coordinateError, _ := e.(*CoordinateError); coordinateError != nil {
			coordinateErrors = append(coordinateErrors, *coordinateError)
			return nil
		}
		return e
	})
	return coordinateErrors, remainingErrors
}

func flatten(errs ...error) []error {
	var out []error
	for _, err := range errs {
		if err == nil {
			continue
		}
		// turn all errors nested under a coordinate error to individual coordinate errors
		if e, ok := err.(*CoordinateError); ok {
			if e == nil {
				continue
			}
			for _, r := range flatten(e.Reason) {
				out = append(out, New(e.Coordinates, r))
			}
		} else
		// from multierror.Append
		if e, ok := err.(interface{ WrappedErrors() []error }); ok {
			if e == nil {
				continue
			}
			out = append(out, flatten(e.WrappedErrors()...)...)
		} else
		// from errors.Join
		if e, ok := err.(interface{ Unwrap() []error }); ok {
			if e == nil {
				continue
			}
			out = append(out, flatten(e.Unwrap()...)...)
		} else {
			out = append(out, err)
		}
	}
	return out
}

// containsErr returns true if a duplicate error is found
func containsErr(out []error, err error) bool {
	defer func() {
		if err := recover(); err != nil {
			log.Tracef("error comparing errors: %v", err)
		}
	}()
	return slices.Contains(out, err)
}

// visitErrors visits every wrapped error. the returned error replaces the provided error, null errors are omitted from
// the object graph
func visitErrors(err error, fn func(error) error) error {
	// unwrap errors from errors.Join
	if errs, ok := err.(interface{ Unwrap() []error }); ok {
		var out []error
		for _, e := range errs.Unwrap() {
			out = append(out, visitErrors(e, fn))
		}
		// errors.Join omits nil errors and will return nil if all passed errors are nil
		return errors.Join(out...)
	}
	// unwrap errors from multierror.Append -- these also implement Unwrap() error, so check this first
	if errs, ok := err.(interface{ WrappedErrors() []error }); ok {
		var out []error
		for _, e := range errs.WrappedErrors() {
			out = append(out, visitErrors(e, fn))
		}
		// errors.Join omits nil errors and will return nil if all passed errors are nil
		return errors.Join(out...)
	}
	// unwrap singly wrapped errors
	if e, ok := err.(interface{ Unwrap() error }); ok {
		wrapped := e.Unwrap()
		got := visitErrors(wrapped, fn)
		if got == nil {
			return nil
		}
		if wrapped.Error() != got.Error() {
			prefix := strings.TrimSuffix(err.Error(), wrapped.Error())
			return fmt.Errorf("%s%w", prefix, got)
		}
		return err
	}
	return fn(err)
}
