package exerr

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/rs/zerolog"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"gogs.mikescher.com/BlackForestBytes/goext/dataext"
	"gogs.mikescher.com/BlackForestBytes/goext/enums"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"net/http"
	"os"
	"runtime/debug"
	"strings"
	"time"
)

//
// ==== USAGE =====
//
// If some method returns an error _always wrap it into an exerror:
//     value, err := do_something(..)
//     if err != nil {
//         return nil, exerror.Wrap(err, "do something failed").Build()
//     }
//
// If possible add metadata to the error (eg the id that was not found, ...), the methods are the same as in zerolog
//     return nil, exerror.Wrap(err, "do something failed").Str("someid", id).Int("count", in.Count).Build()
//
// You can change the errortype with `.User()` and `.System()` (User-errors are 400 and System-errors 500)
// You can also manually set the statuscode with `.WithStatuscode(http.NotFound)`
// You can set the type with `WithType(..)`
//
// New Errors (that don't wrap an existing err object) are created with New
//     return nil, exerror.New(exerror.TypeInternal, "womethign wen horrible wrong").Build()
// You can eitehr use an existing ErrorType, the "catch-all" ErrInternal, or add you own ErrType in consts.go
//
// All errors should be handled one of the following four ways:
//   - return the error to the caller and let him handle it:
//     (also auto-prints the error to the log)
//     => Wrap/New + Build
//   - Print the error
//     (also auto-sends it to the error-service)
//     This is useful for errors that happen asynchron or are non-fatal for the current request
//     => Wrap/New + Print
//   - Return the error to the Rest-API caller
//     (also auto-prints the error to the log)
//     (also auto-sends it to the error-service)
//     => Wrap/New + Output
//   - Print and stop the service
//     (also auto-sends it to the error-service)
//     => Wrap/New + Fatal
//

var stackSkipLogger zerolog.Logger

func init() {
	cw := zerolog.ConsoleWriter{
		Out:        os.Stdout,
		TimeFormat: "2006-01-02 15:04:05 Z07:00",
	}

	multi := zerolog.MultiLevelWriter(cw)
	stackSkipLogger = zerolog.New(multi).With().Timestamp().CallerWithSkipFrameCount(4).Logger()
}

type Builder struct {
	wrappedErr      error
	errorData       *ExErr
	containsGinData bool
	noLog           bool
}

func Get(err error) *Builder {
	return &Builder{errorData: FromError(err)}
}

func New(t ErrorType, msg string) *Builder {
	return &Builder{errorData: newExErr(CatSystem, t, msg)}
}

func Wrap(err error, msg string) *Builder {
	if err == nil {
		return &Builder{errorData: newExErr(CatSystem, TypeInternal, msg)} // prevent NPE if we call Wrap with err==nil
	}

	if !pkgconfig.RecursiveErrors {
		v := FromError(err)
		v.Message = msg
		return &Builder{wrappedErr: err, errorData: v}
	}
	return &Builder{wrappedErr: err, errorData: wrapExErr(FromError(err), msg, CatWrap, 1)}
}

// ----------------------------------------------------------------------------

func (b *Builder) WithType(t ErrorType) *Builder {
	b.errorData.Type = t
	return b
}

func (b *Builder) WithStatuscode(status int) *Builder {
	b.errorData.StatusCode = &status
	return b
}

func (b *Builder) WithMessage(msg string) *Builder {
	b.errorData.Message = msg
	return b
}

// ----------------------------------------------------------------------------

// Err changes the Severity to ERROR (default)
// The error will be:
//
//   - On Build():
//
//   - Short-Logged as Err
//
//   - On Print():
//
//   - Logged as Err
//
//   - Send to the error-service
//
//   - On Output():
//
//   - Logged as Err
//
//   - Send to the error-service
func (b *Builder) Err() *Builder {
	b.errorData.Severity = SevErr
	return b
}

// Warn changes the Severity to WARN
// The error will be:
//
//   - On Build():
//
//   - -(nothing)-
//
//   - On Print():
//
//   - Short-Logged as Warn
//
//   - On Output():
//
//   - Logged as Warn
func (b *Builder) Warn() *Builder {
	b.errorData.Severity = SevWarn
	return b
}

// Info changes the Severity to INFO
// The error will be:
//
//   - On Build():
//
//   - -(nothing)-
//
//   - On Print():
//
//   - -(nothing)-
//
//   - On Output():
//
//   - -(nothing)-
func (b *Builder) Info() *Builder {
	b.errorData.Severity = SevInfo
	return b
}

// ----------------------------------------------------------------------------

// User sets the Category to CatUser
//
// Errors with category
func (b *Builder) User() *Builder {
	b.errorData.Category = CatUser
	return b
}

func (b *Builder) System() *Builder {
	b.errorData.Category = CatSystem
	return b
}

// ----------------------------------------------------------------------------

func (b *Builder) NoLog() *Builder {
	b.noLog = true
	return b
}

// ----------------------------------------------------------------------------

func (b *Builder) Id(key string, val fmt.Stringer) *Builder {
	return b.addMeta(key, MDTID, newIDWrap(val))
}

func (b *Builder) StrPtr(key string, val *string) *Builder {
	return b.addMeta(key, MDTStringPtr, val)
}

func (b *Builder) Str(key string, val string) *Builder {
	return b.addMeta(key, MDTString, val)
}

func (b *Builder) Int(key string, val int) *Builder {
	return b.addMeta(key, MDTInt, val)
}

func (b *Builder) Int8(key string, val int8) *Builder {
	return b.addMeta(key, MDTInt8, val)
}

func (b *Builder) Int16(key string, val int16) *Builder {
	return b.addMeta(key, MDTInt16, val)
}

func (b *Builder) Int32(key string, val int32) *Builder {
	return b.addMeta(key, MDTInt32, val)
}

func (b *Builder) Int64(key string, val int64) *Builder {
	return b.addMeta(key, MDTInt64, val)
}

func (b *Builder) Float32(key string, val float32) *Builder {
	return b.addMeta(key, MDTFloat32, val)
}

func (b *Builder) Float64(key string, val float64) *Builder {
	return b.addMeta(key, MDTFloat64, val)
}

func (b *Builder) Bool(key string, val bool) *Builder {
	return b.addMeta(key, MDTBool, val)
}

func (b *Builder) Bytes(key string, val []byte) *Builder {
	return b.addMeta(key, MDTBytes, val)
}

func (b *Builder) ObjectID(key string, val primitive.ObjectID) *Builder {
	return b.addMeta(key, MDTObjectID, val)
}

func (b *Builder) Time(key string, val time.Time) *Builder {
	return b.addMeta(key, MDTTime, val)
}

func (b *Builder) Dur(key string, val time.Duration) *Builder {
	return b.addMeta(key, MDTDuration, val)
}

func (b *Builder) Strs(key string, val []string) *Builder {
	return b.addMeta(key, MDTStringArray, val)
}

func (b *Builder) Ints(key string, val []int) *Builder {
	return b.addMeta(key, MDTIntArray, val)
}

func (b *Builder) Ints32(key string, val []int32) *Builder {
	return b.addMeta(key, MDTInt32Array, val)
}

func (b *Builder) Type(key string, cls interface{}) *Builder {
	return b.addMeta(key, MDTString, fmt.Sprintf("%T", cls))
}

func (b *Builder) Interface(key string, val interface{}) *Builder {
	return b.addMeta(key, MDTAny, newAnyWrap(val))
}

func (b *Builder) Any(key string, val any) *Builder {
	return b.addMeta(key, MDTAny, newAnyWrap(val))
}

func (b *Builder) Stringer(key string, val fmt.Stringer) *Builder {
	if langext.IsNil(val) {
		return b.addMeta(key, MDTString, "(!nil)")
	} else {
		return b.addMeta(key, MDTString, val.String())
	}
}

func (b *Builder) Enum(key string, val enums.Enum) *Builder {
	return b.addMeta(key, MDTEnum, newEnumWrap(val))
}

func (b *Builder) Stack() *Builder {
	return b.addMeta("@Stack", MDTString, string(debug.Stack()))
}

func (b *Builder) Errs(key string, val []error) *Builder {
	for i, valerr := range val {
		b.addMeta(fmt.Sprintf("%v[%v]", key, i), MDTString, Get(valerr).errorData.FormatLog(LogPrintFull))
	}
	return b
}

func (b *Builder) GinReq(ctx context.Context, g *gin.Context, req *http.Request) *Builder {
	if v := ctx.Value("start_timestamp"); v != nil {
		if t, ok := v.(time.Time); ok {
			b.Time("ctx.startTimestamp", t)
			b.Time("ctx.endTimestamp", time.Now())
		}
	}
	b.Str("gin.method", req.Method)
	b.Str("gin.path", g.FullPath())
	b.Strs("gin.header", extractHeader(g.Request.Header))
	if req.URL != nil {
		b.Str("gin.url", req.URL.String())
	}
	if ctxVal := g.GetString("apiversion"); ctxVal != "" {
		b.Str("gin.context.apiversion", ctxVal)
	}
	if ctxVal := g.GetString("uid"); ctxVal != "" {
		b.Str("gin.context.uid", ctxVal)
	}
	if ctxVal := g.GetString("fcmId"); ctxVal != "" {
		b.Str("gin.context.fcmid", ctxVal)
	}
	if ctxVal := g.GetString("reqid"); ctxVal != "" {
		b.Str("gin.context.reqid", ctxVal)
	}
	if req.Method != "GET" && req.Body != nil {

		if req.Header.Get("Content-Type") == "application/json" {
			if brc, ok := req.Body.(dataext.BufferedReadCloser); ok {
				if bin, err := brc.BufferedAll(); err == nil {
					if len(bin) < 16*1024 {
						var prettyJSON bytes.Buffer
						err = json.Indent(&prettyJSON, bin, "", "  ")
						if err == nil {
							b.Str("gin.body", string(prettyJSON.Bytes()))
						} else {
							b.Bytes("gin.body", bin)
						}
					} else {
						b.Str("gin.body", fmt.Sprintf("[[%v bytes | %s]]", len(bin), req.Header.Get("Content-Type")))
					}
				}
			}
		}

		if req.Header.Get("Content-Type") == "multipart/form-data" || req.Header.Get("Content-Type") == "x-www-form-urlencoded" {
			if brc, ok := req.Body.(dataext.BufferedReadCloser); ok {
				if bin, err := brc.BufferedAll(); err == nil {
					if len(bin) < 16*1024 {
						b.Bytes("gin.body", bin)
					} else {
						b.Str("gin.body", fmt.Sprintf("[[%v bytes | %s]]", len(bin), req.Header.Get("Content-Type")))
					}
				}
			}
		}

	}

	b.containsGinData = true
	return b
}

func formatHeader(header map[string][]string) string {
	ml := 1
	for k, _ := range header {
		if len(k) > ml {
			ml = len(k)
		}
	}
	r := ""
	for k, v := range header {
		if r != "" {
			r += "\n"
		}
		for _, hval := range v {
			value := hval
			value = strings.ReplaceAll(value, "\n", "\\n")
			value = strings.ReplaceAll(value, "\r", "\\r")
			value = strings.ReplaceAll(value, "\t", "\\t")
			r += langext.StrPadRight(k, " ", ml) + " := " + value
		}
	}
	return r
}

func extractHeader(header map[string][]string) []string {
	r := make([]string, 0, len(header))
	for k, v := range header {
		for _, hval := range v {
			value := hval
			value = strings.ReplaceAll(value, "\n", "\\n")
			value = strings.ReplaceAll(value, "\r", "\\r")
			value = strings.ReplaceAll(value, "\t", "\\t")
			r = append(r, k+": "+value)
		}
	}
	return r
}

// ----------------------------------------------------------------------------

// Build creates a new error, ready to pass up the stack
// If the errors is not SevWarn or SevInfo it gets also logged (in short form, without stacktrace) onto stdout
// Can be gloablly configured with ZeroLogErrTraces and ZeroLogAllTraces
// Can be locally suppressed with Builder.NoLog()
func (b *Builder) Build() error {
	warnOnPkgConfigNotInitialized()

	if pkgconfig.DisableErrorWrapping && b.wrappedErr != nil {
		return b.wrappedErr
	}

	if pkgconfig.ZeroLogErrTraces && !b.noLog && (b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal) {
		b.errorData.ShortLog(stackSkipLogger.Error())
	} else if pkgconfig.ZeroLogAllTraces && !b.noLog {
		b.errorData.ShortLog(stackSkipLogger.Error())
	}

	b.errorData.CallListener(MethodBuild)

	return b.errorData
}

// Output prints the error onto the gin stdout.
// The error also gets printed to stdout/stderr
// If the error is SevErr|SevFatal we also send it to the error-service
func (b *Builder) Output(ctx context.Context, g *gin.Context) {
	if !b.containsGinData && g.Request != nil {
		// Auto-Add gin metadata if the caller hasn't already done it
		b.GinReq(ctx, g, g.Request)
	}

	b.errorData.Output(g)

	if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
		b.errorData.Log(stackSkipLogger.Error())
	} else if b.errorData.Severity == SevWarn {
		b.errorData.Log(stackSkipLogger.Warn())
	}

	b.errorData.CallListener(MethodOutput)
}

// Print prints the error
// If the error is SevErr we also send it to the error-service
func (b *Builder) Print() {
	if b.errorData.Severity == SevErr || b.errorData.Severity == SevFatal {
		b.errorData.Log(stackSkipLogger.Error())
	} else if b.errorData.Severity == SevWarn {
		b.errorData.ShortLog(stackSkipLogger.Warn())
	}

	b.errorData.CallListener(MethodPrint)
}

func (b *Builder) Format(level LogPrintLevel) string {
	return b.errorData.FormatLog(level)
}

// Fatal prints the error and terminates the program
// If the error is SevErr we also send it to the error-service
func (b *Builder) Fatal() {
	b.errorData.Severity = SevFatal
	b.errorData.Log(stackSkipLogger.WithLevel(zerolog.FatalLevel))

	b.errorData.CallListener(MethodFatal)

	os.Exit(1)
}

// ----------------------------------------------------------------------------

func (b *Builder) addMeta(key string, mdtype metaDataType, val interface{}) *Builder {
	b.errorData.Meta.add(key, mdtype, val)
	return b
}