package ginext

import (
	"bytes"
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"gogs.mikescher.com/BlackForestBytes/goext/dataext"
	"gogs.mikescher.com/BlackForestBytes/goext/exerr"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"io"
	"runtime/debug"
	"time"
)

type PreContext struct {
	ginCtx         *gin.Context
	wrapper        *GinWrapper
	uri            any
	query          any
	body           any
	rawbody        *[]byte
	form           any
	header         any
	timeout        *time.Duration
	persistantData *preContextData // must be a ptr, so that we can get the values back in out Wrap func
}

type preContextData struct {
	sessionObj SessionObject
}

func (pctx *PreContext) URI(uri any) *PreContext {
	pctx.uri = uri
	return pctx
}

func (pctx *PreContext) Query(query any) *PreContext {
	pctx.query = query
	return pctx
}

func (pctx *PreContext) Body(body any) *PreContext {
	pctx.body = body
	return pctx
}

func (pctx *PreContext) RawBody(rawbody *[]byte) *PreContext {
	pctx.rawbody = rawbody
	return pctx
}

func (pctx *PreContext) Form(form any) *PreContext {
	pctx.form = form
	return pctx
}

func (pctx *PreContext) Header(header any) *PreContext {
	pctx.header = header
	return pctx
}

func (pctx *PreContext) WithTimeout(to time.Duration) *PreContext {
	pctx.timeout = &to
	return pctx
}

func (pctx *PreContext) WithSession(sessionObj SessionObject) *PreContext {
	pctx.persistantData.sessionObj = sessionObj
	return pctx
}

func (pctx PreContext) Start() (*AppContext, *gin.Context, *HTTPResponse) {
	if pctx.uri != nil {
		if err := pctx.ginCtx.ShouldBindUri(pctx.uri); err != nil {
			err = exerr.Wrap(err, "Failed to read uri").
				WithType(exerr.TypeBindFailURI).
				Str("struct_type", fmt.Sprintf("%T", pctx.uri)).
				Build()
			return nil, nil, langext.Ptr(Error(err))
		}
	}

	if pctx.query != nil {
		if err := pctx.ginCtx.ShouldBindQuery(pctx.query); err != nil {
			err = exerr.Wrap(err, "Failed to read query").
				WithType(exerr.TypeBindFailQuery).
				Str("struct_type", fmt.Sprintf("%T", pctx.query)).
				Build()
			return nil, nil, langext.Ptr(Error(err))
		}
	}

	if pctx.body != nil {
		if pctx.ginCtx.ContentType() == "application/json" {
			if err := pctx.ginCtx.ShouldBindJSON(pctx.body); err != nil {
				err = exerr.Wrap(err, "Failed to read json-body").
					WithType(exerr.TypeBindFailJSON).
					Str("struct_type", fmt.Sprintf("%T", pctx.body)).
					Build()
				return nil, nil, langext.Ptr(Error(err))
			}
		} else {
			err := exerr.New(exerr.TypeBindFailJSON, "missing JSON body").
				Str("struct_type", fmt.Sprintf("%T", pctx.body)).
				Build()
			return nil, nil, langext.Ptr(Error(err))
		}
	}

	if pctx.rawbody != nil {
		if brc, ok := pctx.ginCtx.Request.Body.(dataext.BufferedReadCloser); ok {
			v, err := brc.BufferedAll()
			if err != nil {
				return nil, nil, langext.Ptr(Error(err))
			}
			*pctx.rawbody = v
		} else {
			buf := &bytes.Buffer{}
			_, err := io.Copy(buf, pctx.ginCtx.Request.Body)
			if err != nil {
				return nil, nil, langext.Ptr(Error(err))
			}
			*pctx.rawbody = buf.Bytes()
		}
	}

	if pctx.form != nil {
		if pctx.ginCtx.ContentType() == "multipart/form-data" {
			if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil {
				err = exerr.Wrap(err, "Failed to read multipart-form").
					WithType(exerr.TypeBindFailFormData).
					Str("struct_type", fmt.Sprintf("%T", pctx.form)).
					Build()
				return nil, nil, langext.Ptr(Error(err))
			}
		} else if pctx.ginCtx.ContentType() == "application/x-www-form-urlencoded" {
			if err := pctx.ginCtx.ShouldBindWith(pctx.form, binding.Form); err != nil {
				err = exerr.Wrap(err, "Failed to read urlencoded-form").
					WithType(exerr.TypeBindFailFormData).
					Str("struct_type", fmt.Sprintf("%T", pctx.form)).
					Build()
				return nil, nil, langext.Ptr(Error(err))
			}
		} else {
			err := exerr.New(exerr.TypeBindFailFormData, "missing form body").
				Str("struct_type", fmt.Sprintf("%T", pctx.form)).
				Build()
			return nil, nil, langext.Ptr(Error(err))
		}
	}

	if pctx.header != nil {
		if err := pctx.ginCtx.ShouldBindHeader(pctx.header); err != nil {
			err = exerr.Wrap(err, "Failed to read header").
				WithType(exerr.TypeBindFailHeader).
				Str("struct_type", fmt.Sprintf("%T", pctx.query)).
				Build()
			return nil, nil, langext.Ptr(Error(err))
		}
	}

	ictx, cancel := context.WithTimeout(context.Background(), langext.Coalesce(pctx.timeout, pctx.wrapper.requestTimeout))

	actx := CreateAppContext(pctx.ginCtx, ictx, cancel)

	if pctx.persistantData.sessionObj != nil {
		err := pctx.persistantData.sessionObj.Init(pctx.ginCtx, actx)
		if err != nil {
			actx.Cancel()
			return nil, nil, langext.Ptr(Error(exerr.Wrap(err, "Failed to init session").Build()))
		}
	}

	return actx, pctx.ginCtx, nil
}

func callPanicSafe(fn WHandlerFunc, pctx PreContext) (res HTTPResponse, stackTrace string, panicObj any) {
	defer func() {
		if rec := recover(); rec != nil {
			res = nil
			stackTrace = string(debug.Stack())
			panicObj = rec
		}
	}()

	res = fn(pctx)
	return res, "", nil
}