package errors

import (
	"fmt"
	"net/http"
	"path"
	"strconv"
	"strings"
)

// CustomError implements the following interfaces:
//
//	error
//	github.com/pkg/errors: Cause
type CustomError struct {
	code           int
	message        string
	caller         Caller
	cause          error
	bypassRaygun   bool
	bypassSQSError bool
}

// implement interface error:
func (err *CustomError) Error() string {
	return err.Formatted(FormattingOptions{Causes: false})
}

func (err *CustomError) BypassRaygun() *CustomError {
	err.bypassRaygun = true
	return err
}

func (err *CustomError) ShouldBypassRaygun() bool {
	return err.bypassRaygun
}

func (err *CustomError) BypassSQSError() *CustomError {
	err.bypassSQSError = true
	return err
}

func (err *CustomError) ShouldBypassSQSError() bool {
	return err.bypassSQSError
}

func Is(e1, e2 error) bool {
	if e1WithIs, ok := e1.(ErrorWithIs); ok {
		return e1WithIs.Is(e2)
	}
	return e1.Error() == e2.Error()
}

// Is() compares the message string of this or any cause to match the specified error message
func (err *CustomError) Is(specificError error) bool {
	if err.message == specificError.Error() {
		return true
	}
	if err.cause != nil {
		if causeWithIs, ok := err.cause.(ErrorWithIs); ok {
			return causeWithIs.Is(specificError)
		}
	}
	return false
}

// implement github.com/pkg/errors: Cause
func (err *CustomError) Cause() error {
	return err.cause
}

func HTTPCode(err error) int {
	if errWithCode, ok := err.(ErrorWithCause); ok {
		return errWithCode.Code()
	}
	return 0
}

func (err *CustomError) Code() int {
	// find http error code - returning the smallest code in the stack of causes (excluding code==0)
	code := err.code
	if err.cause != nil {
		if causeWithCode, ok := err.cause.(ErrorWithCause); ok {
			causeCode := causeWithCode.Code()
			if code == 0 || (causeCode != 0 && causeCode < code) {
				code = causeCode
			}
		}
	}
	return code
}

func (err *CustomError) Description() Description {
	info := err.caller.Info()
	desc := &Description{
		Message: err.message,
		Source:  &info,
	}
	if err.cause != nil {
		causeWithStack, ok := err.cause.(*CustomError)
		if !ok {
			// external cause without our stack
			// if github.com/pkg/errors, we can still get caller reference
			desc.Cause = pkgDescription(0, err.cause)
		} else {
			// cause has our stack
			causeDesription := causeWithStack.Description()
			desc.Cause = &causeDesription
		}
	}
	return *desc
}

func (err *CustomError) Format(s fmt.State, v rune) {
	s.Write([]byte(
		err.Formatted(
			FormattingOptions{
				Causes:   (v == 'v' || v == 'c'), // include causes for %c and %v, s is only this error
				NewLines: v == 'v',               // use newlines only on v, c is more compact on single line
				Source:   s.Flag('+'),            // include source references when %+v or %+c
			},
		),
	))
}

type FormattingOptions struct {
	Causes   bool
	NewLines bool
	Source   bool
}

func (err *CustomError) Formatted(opts FormattingOptions) string {
	// start with this error
	thisError := ""
	if opts.Source {
		thisError += fmt.Sprintf("%s/%s(%d): %s() ",
			err.caller.Package(),
			path.Base(err.caller.File()),
			err.caller.Line(),
			err.caller.Function(),
		)
	}

	if err.cause == nil || !opts.Causes {
		return err.message
	}

	if err.cause.Error() != err.message {
		thisError += err.message
	}

	sep := ""
	if len(thisError) > 0 {
		sep = ", because"
		if opts.NewLines {
			sep += "\n\t"
		} else {
			sep += " "
		}
	}

	if causeWithStack, ok := err.cause.(*CustomError); ok {
		return thisError + sep + causeWithStack.Formatted(opts)
	}

	// this level does not have our own stack, but we can detect github.com/pkg/errors stack, then:
	// note: do not use fmt.Sprintf("%+v", err.cause) because it will repeat the stack if multiple were captured
	// note: do not use err.cause.Error() because it does not include any stack
	// instead, get first layer that implements it and log it
	return thisError + pkgStack(err.cause, opts)
}

func pkgStack(err error, opts FormattingOptions) string {
	s := ""
	e := err
	for e != nil {
		if errWithStackTracer, ok := e.(stackTracer); ok {
			st := errWithStackTracer.StackTrace()
			for _, f := range st {
				// source := fmt.Sprintf("%n %s %d", f, f, f) - this shows only package name, not fully qualified package name :-(
				sources := strings.SplitN(fmt.Sprintf("%+s(%d)\n%n", f, f, f), "\n", 3)
				// package            <-- sources[0]
				// full filename:line <-- source[1]
				// function name      <-- source[2]

				// skip runtime packages
				if strings.HasPrefix(sources[0], "runtime") {
					break
				}
				if opts.NewLines {
					s += ", because \n\t"
				} else {
					s += ", because "
				}
				if opts.Source {
					s += sources[0] + "/" + path.Base(sources[1]) + ": " + sources[2] + "(): "
				}
				s += fmt.Sprintf("%s", e)
			}
			return s
		} else {
			// no stack tracer...
			if opts.NewLines {
				s += ", because \n\t"
			} else {
				s += ", because "
			}
			s += e.Error()
		}
		errWithCause, ok := e.(ErrorWithCause)
		if !ok {
			break
		}
		e = errWithCause.Cause()
	}
	return s
}

func pkgDescription(level int, err error) *Description {
	desc := &Description{
		Message: fmt.Sprintf("%s", err),
		Source:  nil,
		Cause:   nil,
	}

	// recursively fill causes first
	if errWithCause, ok := err.(ErrorWithCause); ok {
		causeErr := errWithCause.Cause()
		if causeErr != nil {
			desc.Cause = pkgDescription(level+1, causeErr)
		}
	}

	if errWithStackTracer, ok := err.(stackTracer); ok {
		tempDesc := desc
		st := errWithStackTracer.StackTrace()
		for _, f := range st {
			if tempDesc == nil {
				break // stop if no more causes populated
			}

			// source := fmt.Sprintf("%n %s %d", f, f, f) - this shows only package name, not fully qualified package name :-(
			sources := strings.SplitN(fmt.Sprintf("%+s\n%d\n%n", f, f, f), "\n", 4)
			// package            <-- sources[0]
			// full filename      <-- source[1]
			//line               <-- source[2]
			// function name      <-- source[3]

			// stop if entering runtime part of the stack
			if strings.HasPrefix(sources[0], "runtime") {
				break
			}
			tempDesc.Source = &CallerInfo{
				Package: sources[0],
				File:    path.Base(sources[1]),
				// Line:     sources[2],
				Function: sources[3],
			}
			if i64, err := strconv.ParseInt(sources[2], 10, 64); err == nil {
				tempDesc.Source.Line = int(i64)
			}
			tempDesc = tempDesc.Cause
		} // for each stack level
	} // if has stack
	return desc
}

func IsRetryableError(err error) bool {

	code := HTTPCode(err)

	if code == 0 {
		return false
	}

	// 429 should always retry
	if code == http.StatusTooManyRequests {
		return true
	}

	// Retry for all except 200s and 400s
	if code < 200 || (code >= 300 && code < 400) || code >= 500 {
		return true
	}

	return false
}

func IsRetryableErrorOrShouldFail(err error) (shouldRetry bool, shouldFail bool) {
	if err == nil {
		return false, false
	}

	// Check for retryable
	if IsRetryableError(err) {
		return true, false
	}

	code := HTTPCode(err)

	// If no HTTP code in the error, just fail but don't retry
	if code == 0 {
		return false, true
	}

	// Explicitly check for 400s, and don't retry or fail
	if code >= 400 && code < 500 {
		return false, false
	}

	// Explicitly check for 200s, for a fail-safe
	if code >= 200 && code < 300 {
		return false, false
	}

	// If we got here, we should fail normally
	return false, true
}