package ginext

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/rs/zerolog/log"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"gogs.mikescher.com/BlackForestBytes/goext/mathext"
	"gogs.mikescher.com/BlackForestBytes/goext/rext"
	"net"
	"net/http"
	"net/http/httptest"
	"regexp"
	"strings"
	"time"
)

type GinWrapper struct {
	engine          *gin.Engine
	suppressGinLogs bool

	opt                   Options
	allowCors             bool
	corsAllowHeader       []string
	corsExposeHeader      []string
	ginDebug              bool
	bufferBody            bool
	requestTimeout        time.Duration
	listenerBeforeRequest []func(g *gin.Context)
	listenerAfterRequest  []func(g *gin.Context, resp HTTPResponse)

	buildRequestBindError func(g *gin.Context, fieldtype string, err error) HTTPResponse

	routeSpecs []ginRouteSpec
}

type ginRouteSpec struct {
	Method      string
	URL         string
	Middlewares []string
	Handler     string
}

type Options struct {
	AllowCors                *bool                                                          // Add cors handler to allow all CORS requests on the default http methods
	CorsAllowHeader          *[]string                                                      // override the default values of Access-Control-Allow-Headers (AllowCors must be true)
	CorsExposeHeader         *[]string                                                      // return Access-Control-Expose-Headers (AllowCors must be true)
	GinDebug                 *bool                                                          // Set gin.debug to true (adds more logs)
	SuppressGinLogs          *bool                                                          // Suppress our custom gin logs (even if GinDebug == true)
	BufferBody               *bool                                                          // Buffers the input body stream, this way the ginext error handler can later include the whole request body
	Timeout                  *time.Duration                                                 // The default handler timeout
	ListenerBeforeRequest    []func(g *gin.Context)                                         // Register listener that are called before the handler method
	ListenerAfterRequest     []func(g *gin.Context, resp HTTPResponse)                      // Register listener that are called after the handler method
	DebugTrimHandlerPrefixes []string                                                       // Trim these prefixes from the handler names in the debug print
	DebugReplaceHandlerNames map[string]string                                              // Replace handler names in debug output
	BuildRequestBindError    func(g *gin.Context, fieldtype string, err error) HTTPResponse // Override function which generates the HTTPResponse errors that are returned by the preContext..Start() methids
}

// NewEngine creates a new (wrapped) ginEngine
func NewEngine(opt Options) *GinWrapper {
	ginDebug := langext.Coalesce(opt.GinDebug, true)
	if ginDebug {
		gin.SetMode(gin.DebugMode)

		// do not debug-print routes
		gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {}

	} else {
		gin.SetMode(gin.ReleaseMode)

		// do not debug-print routes
		gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {}
	}

	engine := gin.New()

	wrapper := &GinWrapper{
		engine:                engine,
		opt:                   opt,
		suppressGinLogs:       langext.Coalesce(opt.SuppressGinLogs, false),
		allowCors:             langext.Coalesce(opt.AllowCors, false),
		corsAllowHeader:       langext.Coalesce(opt.CorsAllowHeader, []string{"Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With"}),
		corsExposeHeader:      langext.Coalesce(opt.CorsExposeHeader, []string{}),
		ginDebug:              ginDebug,
		bufferBody:            langext.Coalesce(opt.BufferBody, false),
		requestTimeout:        langext.Coalesce(opt.Timeout, 24*time.Hour),
		listenerBeforeRequest: opt.ListenerBeforeRequest,
		listenerAfterRequest:  opt.ListenerAfterRequest,
		buildRequestBindError: langext.Conditional(opt.BuildRequestBindError == nil, defaultBuildRequestBindError, opt.BuildRequestBindError),
	}

	engine.RedirectFixedPath = false
	engine.RedirectTrailingSlash = false

	if wrapper.allowCors {
		engine.Use(CorsMiddleware(wrapper.corsAllowHeader, wrapper.corsExposeHeader))
	}

	if ginDebug && !wrapper.suppressGinLogs {
		ginlogger := gin.Logger()
		engine.Use(func(context *gin.Context) { ginlogger(context) })
	}
	return wrapper
}

func (w *GinWrapper) ListenAndServeHTTP(addr string, postInit func(port string)) (chan error, *http.Server) {

	w.DebugPrintRoutes()

	httpserver := &http.Server{
		Addr:    addr,
		Handler: w.engine,
	}

	errChan := make(chan error)

	go func() {

		ln, err := net.Listen("tcp", httpserver.Addr)
		if err != nil {
			errChan <- err
			return
		}

		_, port, err := net.SplitHostPort(ln.Addr().String())
		if err != nil {
			errChan <- err
			return
		}

		log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started on http://localhost:" + port)

		if postInit != nil {
			postInit(port) // the net.Listener a few lines above is at this point actually already buffering requests
		}

		errChan <- httpserver.Serve(ln)
	}()

	return errChan, httpserver
}

func (w *GinWrapper) DebugPrintRoutes() {
	if !w.ginDebug {
		return
	}

	lines := make([][4]string, 0)

	pad := [4]int{0, 0, 0, 0}

	for _, spec := range w.routeSpecs {

		line := [4]string{
			spec.Method,
			spec.URL,
			strings.Join(langext.ArrMap(spec.Middlewares, w.cleanMiddlewareName), " -> "),
			w.cleanMiddlewareName(spec.Handler),
		}

		lines = append(lines, line)

		pad[0] = mathext.Max(pad[0], len(line[0]))
		pad[1] = mathext.Max(pad[1], len(line[1]))
		pad[2] = mathext.Max(pad[2], len(line[2]))
		pad[3] = mathext.Max(pad[3], len(line[3]))
	}

	fmt.Printf("Gin-Routes:\n")
	fmt.Printf("{\n")
	for _, line := range lines {

		fmt.Printf(" %s  %s  -->  %s  -->  %s\n",
			langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2),
			langext.StrPadRight(line[1], " ", pad[1]),
			langext.StrPadRight(line[2], " ", pad[2]),
			langext.StrPadRight(line[3], " ", pad[3]))
	}
	fmt.Printf("}\n")
}

func (w *GinWrapper) cleanMiddlewareName(fname string) string {

	funcSuffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
	if match, ok := funcSuffix.MatchFirst(fname); ok {
		fname = fname[:len(fname)-match.FullMatch().Length()]
	}

	if strings.HasSuffix(fname, ".(*GinRoutesWrapper).WithJSONFilter") {
		fname = "[JSONFilter]"
	}

	if fname == "ginext.BodyBuffer" {
		fname = "[BodyBuffer]"
	}

	skipPrefixes := []string{"api.(*Handler).", "api.", "ginext.", "handler.", "admin-app.", "employee-app.", "employer-app."}
	for _, pfx := range skipPrefixes {
		if strings.HasPrefix(fname, pfx) {
			fname = fname[len(pfx):]
		}
	}

	for _, pfx := range w.opt.DebugTrimHandlerPrefixes {
		if strings.HasPrefix(fname, pfx) {
			fname = fname[len(pfx):]
		}
	}

	for k, v := range langext.ForceMap(w.opt.DebugReplaceHandlerNames) {
		if strings.EqualFold(fname, k) {
			fname = v
		}
	}

	return fname
}

// ServeHTTP only used for unit tests
func (w *GinWrapper) ServeHTTP(req *http.Request) *httptest.ResponseRecorder {
	respRec := httptest.NewRecorder()
	w.engine.ServeHTTP(respRec, req)
	return respRec
}

// ForwardRequest manually inserts a request into this router
// = behaves as if the request came from the outside (and writes the response to `writer`)
func (w *GinWrapper) ForwardRequest(writer http.ResponseWriter, req *http.Request) {
	w.engine.ServeHTTP(writer, req)
}

func (w *GinWrapper) ListRoutes() []gin.RouteInfo {
	return w.engine.Routes()
}

func defaultBuildRequestBindError(g *gin.Context, fieldtype string, err error) HTTPResponse {
	return Error(err)
}