package middlewares

import (
	"fmt"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-logr/logr"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/logger"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/metrics"
)

type RequestLoggerOptions struct {
	// Logger is used for logging internal errors (like 5xx). It should not be nil.
	Logger logr.Logger
	// FilterFunc can be set to filter out some requests from logging and metrics. Return true to skip logging and metrics for the request.
	FilterFunc func(c *gin.Context) bool
}

type AccessLoggerOptions struct {
	// Logger is used for access log. If it's zero value, access log will be disabled.
	Logger logr.Logger
	// FilterFunc can be set to filter out some requests from access logging. Return true to skip access logging for the request.
	FilterFunc func(c *gin.Context) bool
	// ExtraFieldsFunc can be set to add extra fields to access log. It will be called only when AccessLogger is set and AccessLogFilterFunc returns false.
	ExtraFieldsFunc func(c *gin.Context) map[string]any
}

type LogMetricMiddlewareBuilder struct {
	Request RequestLoggerOptions
	Access  *AccessLoggerOptions
}

func (b *LogMetricMiddlewareBuilder) Build() gin.HandlerFunc {
	return func(c *gin.Context) {
		// Start timer
		start := time.Now()
		path := c.Request.URL.Path  // full path, like /api/v1/devops/1123
		matchedPath := c.FullPath() // registered path, like /api/v1/devops/:id
		rawQuery := c.Request.URL.RawQuery

		if b.Request.FilterFunc == nil || !b.Request.FilterFunc(c) {
			logger.FromContextOrDef(c, b.Request.Logger).V(4).Info("request started",
				"method", c.Request.Method,
				"path", path,
				"matched_path", matchedPath,
				"query", rawQuery,
				"client_ip", c.ClientIP(),
			)
		}

		// Process request
		c.Next()

		status := c.Writer.Status()
		statusStr := strconv.Itoa(c.Writer.Status())
		respSize := c.Writer.Size()
		latency := float64(time.Since(start)) / float64(time.Second)
		matchedPath = c.FullPath()

		// Filter log and metrics
		if b.Request.FilterFunc != nil && b.Request.FilterFunc(c) {
			return
		}

		// metrics
		if metrics.Gin != nil {
			metrics.Gin.ReqDur.WithLabelValues(
				statusStr,
				c.Request.Method,
				matchedPath,
			).Observe(latency)
			metrics.Gin.RespSize.Observe(float64(respSize))
		}

		// access log
		if b.Access != nil {
			if b.Access.FilterFunc == nil || !b.Access.FilterFunc(c) {
				fields := []any{
					"latency", latency,
					"method", c.Request.Method,
					"status_code", status,
					"body_size", respSize,
					"query", rawQuery,
				}

				if b.Access.ExtraFieldsFunc != nil {
					for k, v := range b.Access.ExtraFieldsFunc(c) {
						fields = append(fields, k, v)
					}
				}

				logger.WithTraceID(c, b.Access.Logger).Info(matchedPath, fields...)
			}
		}

		logger.FromContextOrDef(c, b.Request.Logger).V(4).Info("request completed",
			"latency", latency,
			"method", c.Request.Method,
			"path", path,
			"matched_path", matchedPath,
			"status_code", status,
			"body_size", respSize,
			"query", rawQuery,
			"client_ip", c.ClientIP(),
		)

		// log for internal server error
		if status >= 500 {
			err := fmt.Errorf("request failed but error in context is nil")
			if c.Errors.Last() != nil {
				err = c.Errors.Last().Unwrap()
			}
			logger.FromContextOrDef(c, b.Request.Logger).Error(err, path,
				"latency", latency,
				"method", c.Request.Method,
				"status_code", status,
				"query", rawQuery,
			)
		}
	}
}
