package bizerr

import (
	"bytes"
	"cmp"
	"fmt"
	"net/http"
	"runtime"
	"runtime/debug"
	"slices"
	"strings"

	"github.com/DataDog/gostackparse"
	"github.com/samber/lo"
	"gopkg.in/yaml.v3"
)

type BizError interface {
	error

	// Chain appends a new message to the error msg.
	// It won't change any other fields of the error.
	Chain(format string, args ...any) BizError

	OverwriteMessage(format string, args ...any) BizError

	// OverwriteCode allows you to set a new error code for the BizErr instance.
	// This method creates a clone of the current BizErr, updates the error code,
	// and returns the modified instance. This is useful for changing the error
	// code in scenarios where the original error code needs to be replaced
	// with a more specific or relevant one based on the context of the error.
	OverwriteCode(code ErrCode) *BizErr

	// IsException returns true if the error is an exception.
	// Exception indicates that the error is critical, we need to be notified and take some actions.
	IsException() bool

	// IsError returns true if the error is an error.
	// Error indicates that there are something goes wrong and need to break process of the request,
	// but that don't matter and not harmful to our system, just log it is fine.
	IsError() bool

	// Redact returns a redacted version of the error. stack information is removed,
	// and the message is replaced with the "Internal Error" if the error is internal-error.
	Redact() BizError

	// ErrCode returns the error code of the error.
	ErrCode() ErrCode

	// ErrMsg returns the error message of the error.
	ErrMsg() string

	// RawErrCode returns the error code has been converted to error if the error is exception.
	RawErrCode() ErrCode

	// Stack returns the stack trace of the error.
	Stack() string

	// HttpStatus returns the http status code of the error.
	HttpStatus() int
}

// BizErr is immutable, implement the BizError interface.
type BizErr struct {
	code    ErrCode
	message string

	stackTrace []byte

	filename   string
	fileno     int
	methodName string

	chainedMsg []string
}

func New(code ErrCode, format string, args ...any) BizError {
	ret := &BizErr{
		code:       code,
		message:    fmt.Errorf(format, args...).Error(),
		stackTrace: []byte(""),
	}
	ret = ret.withCallerInfo()
	return ret.withStack()
}

func (e *BizErr) clone() *BizErr {
	return &BizErr{
		code:       e.code,
		message:    e.message,
		stackTrace: e.stackTrace,
		filename:   e.filename,
		fileno:     e.fileno,
		methodName: e.methodName,
		chainedMsg: append([]string(nil), e.chainedMsg...),
	}
}

// Error returns business error message and debug chained message.
func (e *BizErr) Error() string {
	reversed := append([]string{}, e.chainedMsg...) // clone
	slices.SortFunc(reversed, func(a, b string) int {
		return cmp.Compare(b, a)
	})
	msg := strings.Join(reversed, " -> ")

	return fmt.Sprintf(
		"[%s:%d] [%s] code=%d, msg=%s, chain=%s",
		e.filename,
		e.fileno,
		e.methodName,
		e.code,
		e.message,
		msg,
	)
}

func (e *BizErr) ErrCode() ErrCode {
	return e.code
}

// ErrMsg returns the business error message.
func (e *BizErr) ErrMsg() string {
	return e.message
}

func (e *BizErr) RawErrCode() ErrCode {
	return convert2Error(e.code)
}

func (e *BizErr) Stack() string {
	goroutines, errs := gostackparse.Parse(bytes.NewReader(e.stackTrace))
	if errs != nil {
		return string(e.stackTrace)
	}
	for _, goroutine := range goroutines {
		goroutine.Stack = lo.Filter(goroutine.Stack, func(frame *gostackparse.Frame, _ int) bool {
			return strings.Contains(frame.Func, "/tidb-management-service/") && !strings.Contains(frame.File, "/bizerr")
		})
	}
	s, err := yaml.Marshal(goroutines)
	if err != nil {
		return string(e.stackTrace)
	}
	return string(s)
}

func (e *BizErr) Chain(format string, args ...any) BizError {
	cloned := e.clone()
	msg := fmt.Errorf(format, args...).Error()
	cloned.chainedMsg = append(cloned.chainedMsg, msg)

	return cloned
}

func (e *BizErr) OverwriteMessage(format string, args ...any) BizError {
	cloned := e.clone()
	msg := fmt.Errorf(format, args...).Error()
	cloned.message = msg

	return cloned.Chain("%s", e.message)
}

func (e *BizErr) IsException() bool {
	return IsException(e.code)
}

func (e *BizErr) IsError() bool {
	return !IsException(e.code)
}

func (e *BizErr) Redact() BizError {
	cloned := e.clone()
	if IsInternalErr(cloned.code) {
		cloned.message = "Internal Error"
	}
	cloned.stackTrace = []byte("")
	cloned.filename = ""
	cloned.methodName = ""
	cloned.fileno = 0

	return cloned
}

func (e *BizErr) HttpStatus() int {
	errCode := convert2Error(e.code)
	status, ok := ErrCode2HttpStatus[errCode]
	if ok {
		return status
	}

	switch {
	case IsRequestErr(errCode):
		return http.StatusBadRequest
	case IsInternalErr(errCode):
		return http.StatusInternalServerError
	case IsBusinessErr(errCode):
		return http.StatusBadRequest
	}
	return http.StatusInternalServerError
}

func (e *BizErr) withStack() BizError {
	cloned := e.clone()
	if len(cloned.stackTrace) == 0 {
		cloned.stackTrace = debug.Stack()
	}
	return cloned
}

func (e *BizErr) withCallerInfo() *BizErr {
	cloned := e.clone()
	pc := make([]uintptr, 10)
	n := runtime.Callers(1, pc) // skip runtime/extern.go
	if n == 0 {
		return cloned
	}
	pc = pc[:n] // pass only valid pcs to runtime.CallersFrames
	frames := runtime.CallersFrames(pc)

	// Loop to get frames.
	// A fixed number of PCs can expand to an indefinite number of Frames.
	for {
		frame, more := frames.Next()
		if !strings.Contains(frame.File, "/bizerr/") {
			cloned.filename = frame.File
			cloned.fileno = frame.Line
			cloned.methodName = frame.Function
			return cloned
		}

		// Check whether there are more frames to process after this one.
		if !more {
			break
		}
	}
	return cloned
}

func (e *BizErr) OverwriteCode(code ErrCode) *BizErr {
	cloned := e.clone()
	cloned.code = code
	return cloned
}
