package bizerr

import (
	"fmt"
	"net/http"
	"runtime"
	"runtime/debug"
	"sort"
	"strings"
)

type BizError interface {
	error
	// Escalate Error is used to escalate an error to exception that will be logged with error-level and trigger an alert.
	// for example, a RecordNotFoundError is needed to be escalated if we can't find cluster's profile by cluster.ProfileId.
	// we need to call this method in this case.
	// in another case, a RecordNotFoundError just need log something with info-level if user request to delete a NOT existing cluster.
	Escalate() BizError

	// Degrade Error is used to degrade an error to error that will be logged with info-level.
	// for example, a TimeoutException has been raised from the underlying function, but we can retry without any side effect.
	// we need to call this method in this case.
	Degrade() BizError

	// Chain Error is used to chain an error with some messages.
	Chain(format string, args ...interface{}) BizError

	// 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
	WithStack() BizError
}

// BizErr is immutable, implement the BizError interface.
type BizErr struct {
	Code       ErrCode
	Message    string
	StackTrace string

	filename   string
	fileno     int
	methodName string

	chainedMsg []string
}

func New(code ErrCode, format string, args ...interface{}) BizError {
	msg := format
	if len(args) > 0 {
		msg = fmt.Sprintf(format, args...)
	}

	ret := &BizErr{
		Code:       code,
		Message:    msg,
		StackTrace: "",
	}
	ret = ret.withCallerInfo()
	// auto append stack and escalate the error
	if IsInternalErr(code) {
		return ret.WithStack().Escalate()
	}
	return ret
}

func NewWithStack(code ErrCode, format string, args ...interface{}) BizError {
	return New(code, format, args...).WithStack()
}

func Restore(code ErrCode, msg string) BizError {
	return &BizErr{
		Code:    code,
		Message: msg,
	}
}

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...),
	}
}

func (e *BizErr) Error() string {
	reversed := append([]string{}, e.chainedMsg...) // clone
	sort.Sort(sort.Reverse(sort.StringSlice(reversed)))
	reversed = append(reversed, e.Message)
	msg := strings.Join(reversed, " -> ")

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

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

func (e *BizErr) ErrMsg() string {
	return e.Message
}

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

func (e *BizErr) Stack() string {
	return e.StackTrace
}

func (e *BizErr) Escalate() BizError {
	cloned := e.clone()
	cloned.Code = convert2Exception(cloned.Code)
	return cloned
}

func (e *BizErr) Degrade() BizError {
	cloned := e.clone()
	cloned.Code = convert2Error(cloned.Code)
	return cloned
}

func (e *BizErr) Chain(format string, args ...interface{}) BizError {
	cloned := e.clone()
	msg := format
	if len(args) > 0 {
		msg = fmt.Sprintf(format, args...)
	}
	cloned.chainedMsg = append(cloned.chainedMsg, msg)

	return cloned
}

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 = ""
	cloned.filename = ""
	cloned.methodName = ""
	cloned.fileno = 0

	return cloned
}

func (e *BizErr) HttpStatus() int {
	if useHttpOK4all {
		return http.StatusOK
	}
	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 cloned.StackTrace == "" {
		cloned.StackTrace = string(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
}
