// Package tracerr provides a custom error type that includes a stack trace
package tracerr

import (
	"errors"
	"fmt"
	"runtime"
	"strings"
)

// StackTraceError is a custom error type that includes a stack trace
type StackTraceError struct {
	Err   error
	Stack string
}

// Newf creates a new error with stack trace
func New(msg string) *StackTraceError {
	return &StackTraceError{
		Err:   fmt.Errorf(msg, nil),
		Stack: GetStackTrace(3),
	}
}

// Errorf creates a new formated error with stack trace
func Errorf(format string, a ...any) *StackTraceError {
	var e error
	if a == nil {
		e = errors.New(format)
	} else {
		e = fmt.Errorf(format, a...)
	}

	return &StackTraceError{
		Err:   e,
		Stack: GetStackTrace(3),
	}
}

func (e *StackTraceError) Error() string {
	return fmt.Sprintf("%s\nStack Trace:\n%s", e.Err, e.Stack)
}

// Unwrap implements the errors.Unwrap interface
func (e *StackTraceError) Unwrap() error {
	return e.Err
}

// Is implements the errors.Is interface
func (e *StackTraceError) Is(target error) bool {
	if target == nil {
		return false
	}

	// Check if the target is also a StackTraceError
	t, ok := target.(*StackTraceError)
	if !ok {
		return errors.Is(e.Err, target)
	}

	return errors.Is(e.Err, t.Err)
}

// As implements the errors.As interface
func (e *StackTraceError) As(target any) bool {
	// Try to convert the target to *StackTraceError
	if target, ok := target.(*StackTraceError); ok {
		*target = *e

		return true
	}

	// If target is not *StackTraceError, delegate to the wrapped error
	return errors.As(e.Err, target)
}

// Join creates a new error that wraps the given errors.
// If there is only one error, it returns that error.
// If there are no errors, it returns nil.
func Join(errs ...error) error {
	// Filter out nil errors
	nonNil := make([]error, 0, len(errs))
	for _, err := range errs {
		if err != nil {
			nonNil = append(nonNil, err)
		}
	}

	if len(nonNil) == 0 {
		return nil
	}
	if len(nonNil) == 1 {
		return nonNil[0]
	}

	// Create a new StackTraceError with joined errors
	msg := make([]string, len(nonNil))
	for i, err := range nonNil {
		msg[i] = err.Error()
	}

	return &StackTraceError{
		Err:   errors.Join(nonNil...),
		Stack: GetStackTrace(3),
	}
}

// GetStackTrace returns a formatted stack trace similar to gopkg.in/src-d/go-errors.v1
// It skips the first 'skip' number of frames from the stack trace
func GetStackTrace(skip int) string {
	// Allocate space for 32 PCs
	pc := make([]uintptr, 32)

	// Get the program counters for the stack trace
	n := runtime.Callers(skip, pc)
	if n == 0 {
		return ""
	}

	pc = pc[:n] // Only use PCs that were actually filled

	// Create frames from program counters
	frames := runtime.CallersFrames(pc)

	// Build the stack trace string
	var builder strings.Builder

	// Process frames until we run out
	for {
		frame, more := frames.Next()

		// Format this frame according to the specified format
		//revive:disable:unhandled-error  No need to handle errors here WriteString always returns error nil
		builder.WriteString(frame.Function + "\n")
		builder.WriteString(fmt.Sprintf("\t%s:%d\n", frame.File, frame.Line))
		//revive:enable

		if !more {
			break
		}
	}

	return builder.String()
}
