// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors

package middleware

import (
	"errors"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
)

// Example for `slog` https://pkg.go.dev/log/slog
// 	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
//	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
//		LogStatus:   true,
//		LogURI:      true,
//		LogError:    true,
//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
//			if v.Error == nil {
//				logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
//					slog.String("uri", v.URI),
//					slog.Int("status", v.Status),
//				)
//			} else {
//				logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
//					slog.String("uri", v.URI),
//					slog.Int("status", v.Status),
//					slog.String("err", v.Error.Error()),
//				)
//			}
//			return nil
//		},
//	}))
//
// Example for `fmt.Printf`
// 	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
//		LogStatus:   true,
//		LogURI:      true,
//		LogError:    true,
//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
//			if v.Error == nil {
//				fmt.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status)
//			} else {
//				fmt.Printf("REQUEST_ERROR: uri: %v, status: %v, err: %v\n", v.URI, v.Status, v.Error)
//			}
//			return nil
//		},
//	}))
//
// Example for Zerolog (https://github.com/rs/zerolog)
// 	logger := zerolog.New(os.Stdout)
//	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
//		LogURI:      true,
//		LogStatus:   true,
//		LogError:    true,
//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
//			if v.Error == nil {
//				logger.Info().
//					Str("URI", v.URI).
//					Int("status", v.Status).
//					Msg("request")
//			} else {
//				logger.Error().
//					Err(v.Error).
//					Str("URI", v.URI).
//					Int("status", v.Status).
//					Msg("request error")
//			}
//			return nil
//		},
//	}))
//
// Example for Zap (https://github.com/uber-go/zap)
// 	logger, _ := zap.NewProduction()
//	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
//		LogURI:      true,
//		LogStatus:   true,
//		LogError:    true,
//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
//			if v.Error == nil {
//				logger.Info("request",
//					zap.String("URI", v.URI),
//					zap.Int("status", v.Status),
//				)
//			} else {
//				logger.Error("request error",
//					zap.String("URI", v.URI),
//					zap.Int("status", v.Status),
//					zap.Error(v.Error),
//				)
//			}
//			return nil
//		},
//	}))
//
// Example for Logrus (https://github.com/sirupsen/logrus)
//  log := logrus.New()
//	e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
//		LogURI:      true,
//		LogStatus:   true,
//		LogError:    true,
//		HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
//		LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
//			if v.Error == nil {
//				log.WithFields(logrus.Fields{
//					"URI":    v.URI,
//					"status": v.Status,
//				}).Info("request")
//			} else {
//				log.WithFields(logrus.Fields{
//					"URI":    v.URI,
//					"status": v.Status,
//					"error":  v.Error,
//				}).Error("request error")
//			}
//			return nil
//		},
//	}))

// RequestLoggerConfig is configuration for Request Logger middleware.
type RequestLoggerConfig struct {
	// Skipper defines a function to skip middleware.
	Skipper Skipper

	// BeforeNextFunc defines a function that is called before next middleware or handler is called in chain.
	BeforeNextFunc func(c echo.Context)
	// LogValuesFunc defines a function that is called with values extracted by logger from request/response.
	// Mandatory.
	LogValuesFunc func(c echo.Context, v RequestLoggerValues) error

	// HandleError instructs logger to call global error handler when next middleware/handler returns an error.
	// This is useful when you have custom error handler that can decide to use different status codes.
	//
	// A side-effect of calling global error handler is that now Response has been committed and sent to the client
	// and middlewares up in chain can not change Response status code or response body.
	HandleError bool

	// LogLatency instructs logger to record duration it took to execute rest of the handler chain (next(c) call).
	LogLatency bool
	// LogProtocol instructs logger to extract request protocol (i.e. `HTTP/1.1` or `HTTP/2`)
	LogProtocol bool
	// LogRemoteIP instructs logger to extract request remote IP. See `echo.Context.RealIP()` for implementation details.
	LogRemoteIP bool
	// LogHost instructs logger to extract request host value (i.e. `example.com`)
	LogHost bool
	// LogMethod instructs logger to extract request method value (i.e. `GET` etc)
	LogMethod bool
	// LogURI instructs logger to extract request URI (i.e. `/list?lang=en&page=1`)
	LogURI bool
	// LogURIPath instructs logger to extract request URI path part (i.e. `/list`)
	LogURIPath bool
	// LogRoutePath instructs logger to extract route path part to which request was matched to (i.e. `/user/:id`)
	LogRoutePath bool
	// LogRequestID instructs logger to extract request ID from request `X-Request-ID` header or response if request did not have value.
	LogRequestID bool
	// LogReferer instructs logger to extract request referer values.
	LogReferer bool
	// LogUserAgent instructs logger to extract request user agent values.
	LogUserAgent bool
	// LogStatus instructs logger to extract response status code. If handler chain returns an echo.HTTPError,
	// the status code is extracted from the echo.HTTPError returned
	LogStatus bool
	// LogError instructs logger to extract error returned from executed handler chain.
	LogError bool
	// LogContentLength instructs logger to extract content length header value. Note: this value could be different from
	// actual request body size as it could be spoofed etc.
	LogContentLength bool
	// LogResponseSize instructs logger to extract response content length value. Note: when used with Gzip middleware
	// this value may not be always correct.
	LogResponseSize bool
	// LogHeaders instructs logger to extract given list of headers from request. Note: request can contain more than
	// one header with same value so slice of values is been logger for each given header.
	//
	// Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header
	// names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding".
	LogHeaders []string
	// LogQueryParams instructs logger to extract given list of query parameters from request URI. Note: request can
	// contain more than one query parameter with same name so slice of values is been logger for each given query param name.
	LogQueryParams []string
	// LogFormValues instructs logger to extract given list of form values from request body+URI. Note: request can
	// contain more than one form value with same name so slice of values is been logger for each given form value name.
	LogFormValues []string

	timeNow func() time.Time
}

// RequestLoggerValues contains extracted values from logger.
type RequestLoggerValues struct {
	// StartTime is time recorded before next middleware/handler is executed.
	StartTime time.Time
	// Latency is duration it took to execute rest of the handler chain (next(c) call).
	Latency time.Duration
	// Protocol is request protocol (i.e. `HTTP/1.1` or `HTTP/2`)
	Protocol string
	// RemoteIP is request remote IP. See `echo.Context.RealIP()` for implementation details.
	RemoteIP string
	// Host is request host value (i.e. `example.com`)
	Host string
	// Method is request method value (i.e. `GET` etc)
	Method string
	// URI is request URI (i.e. `/list?lang=en&page=1`)
	URI string
	// URIPath is request URI path part (i.e. `/list`)
	URIPath string
	// RoutePath is route path part to which request was matched to (i.e. `/user/:id`)
	RoutePath string
	// RequestID is request ID from request `X-Request-ID` header or response if request did not have value.
	RequestID string
	// Referer is request referer values.
	Referer string
	// UserAgent is request user agent values.
	UserAgent string
	// Status is response status code. Then handler returns an echo.HTTPError then code from there.
	Status int
	// Error is error returned from executed handler chain.
	Error error
	// ContentLength is content length header value. Note: this value could be different from actual request body size
	// as it could be spoofed etc.
	ContentLength string
	// ResponseSize is response content length value. Note: when used with Gzip middleware this value may not be always correct.
	ResponseSize int64
	// Headers are list of headers from request. Note: request can contain more than one header with same value so slice
	// of values is been logger for each given header.
	// Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header
	// names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding".
	Headers map[string][]string
	// QueryParams are list of query parameters from request URI. Note: request can contain more than one query parameter
	// with same name so slice of values is been logger for each given query param name.
	QueryParams map[string][]string
	// FormValues are list of form values from request body+URI. Note: request can contain more than one form value with
	// same name so slice of values is been logger for each given form value name.
	FormValues map[string][]string
}

// RequestLoggerWithConfig returns a RequestLogger middleware with config.
func RequestLoggerWithConfig(config RequestLoggerConfig) echo.MiddlewareFunc {
	mw, err := config.ToMiddleware()
	if err != nil {
		panic(err)
	}
	return mw
}

// ToMiddleware converts RequestLoggerConfig into middleware or returns an error for invalid configuration.
func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
	if config.Skipper == nil {
		config.Skipper = DefaultSkipper
	}
	now := time.Now
	if config.timeNow != nil {
		now = config.timeNow
	}

	if config.LogValuesFunc == nil {
		return nil, errors.New("missing LogValuesFunc callback function for request logger middleware")
	}

	logHeaders := len(config.LogHeaders) > 0
	headers := append([]string(nil), config.LogHeaders...)
	for i, v := range headers {
		headers[i] = http.CanonicalHeaderKey(v)
	}

	logQueryParams := len(config.LogQueryParams) > 0
	logFormValues := len(config.LogFormValues) > 0

	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if config.Skipper(c) {
				return next(c)
			}

			req := c.Request()
			res := c.Response()
			start := now()

			if config.BeforeNextFunc != nil {
				config.BeforeNextFunc(c)
			}
			err := next(c)
			if err != nil && config.HandleError {
				c.Error(err)
			}

			v := RequestLoggerValues{
				StartTime: start,
			}
			if config.LogLatency {
				v.Latency = now().Sub(start)
			}
			if config.LogProtocol {
				v.Protocol = req.Proto
			}
			if config.LogRemoteIP {
				v.RemoteIP = c.RealIP()
			}
			if config.LogHost {
				v.Host = req.Host
			}
			if config.LogMethod {
				v.Method = req.Method
			}
			if config.LogURI {
				v.URI = req.RequestURI
			}
			if config.LogURIPath {
				p := req.URL.Path
				if p == "" {
					p = "/"
				}
				v.URIPath = p
			}
			if config.LogRoutePath {
				v.RoutePath = c.Path()
			}
			if config.LogRequestID {
				id := req.Header.Get(echo.HeaderXRequestID)
				if id == "" {
					id = res.Header().Get(echo.HeaderXRequestID)
				}
				v.RequestID = id
			}
			if config.LogReferer {
				v.Referer = req.Referer()
			}
			if config.LogUserAgent {
				v.UserAgent = req.UserAgent()
			}
			if config.LogStatus {
				v.Status = res.Status
				if err != nil && !config.HandleError {
					//  this block should not be executed in case of HandleError=true as the global error handler will decide
					//  the status code. In that case status code could be different from what err contains.
					var httpErr *echo.HTTPError
					if errors.As(err, &httpErr) {
						v.Status = httpErr.Code
					}
				}
			}
			if config.LogError && err != nil {
				v.Error = err
			}
			if config.LogContentLength {
				v.ContentLength = req.Header.Get(echo.HeaderContentLength)
			}
			if config.LogResponseSize {
				v.ResponseSize = res.Size
			}
			if logHeaders {
				v.Headers = map[string][]string{}
				for _, header := range headers {
					if values, ok := req.Header[header]; ok {
						v.Headers[header] = values
					}
				}
			}
			if logQueryParams {
				queryParams := c.QueryParams()
				v.QueryParams = map[string][]string{}
				for _, param := range config.LogQueryParams {
					if values, ok := queryParams[param]; ok {
						v.QueryParams[param] = values
					}
				}
			}
			if logFormValues {
				v.FormValues = map[string][]string{}
				for _, formValue := range config.LogFormValues {
					if values, ok := req.Form[formValue]; ok {
						v.FormValues[formValue] = values
					}
				}
			}

			if errOnLog := config.LogValuesFunc(c, v); errOnLog != nil {
				return errOnLog
			}

			// in case of HandleError=true we are returning the error that we already have handled with global error handler
			// this is deliberate as this error could be useful for upstream middlewares and default global error handler
			// will ignore that error when it bubbles up in middleware chain.
			return err
		}
	}, nil
}
