package api

import (
	"encoding/base64"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/didip/tollbooth/v7"
	"github.com/gorilla/mux"

	"zotregistry.dev/zot/pkg/extensions/monitoring"
	"zotregistry.dev/zot/pkg/log"
)

type statusWriter struct {
	http.ResponseWriter
	status int
	length int
}

func (w *statusWriter) WriteHeader(status int) {
	w.status = status
	w.ResponseWriter.WriteHeader(status)
}

func (w *statusWriter) Write(b []byte) (int, error) {
	if w.status == 0 {
		w.status = http.StatusOK
	}

	n, err := w.ResponseWriter.Write(b)
	w.length += n

	return n, err
}

// RateLimiter limits handling of incoming requests.
func RateLimiter(ctlr *Controller, rate int) mux.MiddlewareFunc {
	ctlr.Log.Info().Int("rate", rate).Msg("ratelimiter enabled")

	limiter := tollbooth.NewLimiter(float64(rate), nil)
	limiter.SetMessage(http.StatusText(http.StatusTooManyRequests)).
		SetStatusCode(http.StatusTooManyRequests).
		SetOnLimitReached(nil)

	return func(next http.Handler) http.Handler {
		return tollbooth.LimitHandler(limiter, next)
	}
}

// MethodRateLimiter limits handling of incoming requests.
func MethodRateLimiter(ctlr *Controller, method string, rate int) mux.MiddlewareFunc {
	ctlr.Log.Info().Str("method", method).Int("rate", rate).Msg("per-method ratelimiter enabled")

	limiter := tollbooth.NewLimiter(float64(rate), nil)
	limiter.SetMethods([]string{method}).
		SetMessage(http.StatusText(http.StatusTooManyRequests)).
		SetStatusCode(http.StatusTooManyRequests).
		SetOnLimitReached(nil)

	return func(next http.Handler) http.Handler {
		return tollbooth.LimitHandler(limiter, next)
	}
}

// SessionLogger logs session details.
func SessionLogger(ctlr *Controller) mux.MiddlewareFunc {
	logger := ctlr.Log.With().Str("module", "http").Logger()

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
			// Start timer
			start := time.Now()
			path := request.URL.Path
			raw := request.URL.RawQuery

			stwr := statusWriter{ResponseWriter: response}

			// Process request
			next.ServeHTTP(&stwr, request)

			// Stop timer
			end := time.Now()
			latency := end.Sub(start)
			latency = latency.Truncate(time.Second)

			clientIP := request.RemoteAddr
			method := request.Method
			headers := map[string][]string{}
			log := logger.Info() //nolint: zerologlint // false positive, the Msg call is below

			for key, value := range request.Header {
				if key == "Authorization" { // anonymize from logs
					s := strings.SplitN(value[0], " ", 2) //nolint:mnd
					if len(s) == 2 && strings.EqualFold(s[0], "basic") {
						b, err := base64.StdEncoding.DecodeString(s[1])
						if err == nil {
							pair := strings.SplitN(string(b), ":", 2) //nolint:mnd
							//nolint:mnd
							if len(pair) == 2 {
								log = log.Str("username", pair[0])
							}
						}
					}

					value = []string{"******"}
				}

				headers[key] = value
			}

			statusCode := stwr.status
			bodySize := stwr.length

			if raw != "" {
				path = path + "?" + raw
			}

			if path != "/metrics" {
				// In order to test metrics feture,the instrumentation related to node exporter
				// should be handled by node exporter itself (ex: latency)
				monitoring.IncHTTPConnRequests(ctlr.Metrics, method, strconv.Itoa(statusCode))
				monitoring.ObserveHTTPRepoLatency(ctlr.Metrics, path, latency)     // summary
				monitoring.ObserveHTTPMethodLatency(ctlr.Metrics, method, latency) // histogram
			}

			log.Str("component", "session").
				Str("clientIP", clientIP).
				Str("method", method).
				Str("path", path).
				Int("statusCode", statusCode).
				Str("latency", latency.String()).
				Int("bodySize", bodySize).
				Interface("headers", headers).
				Msg("HTTP API")
		})
	}
}

func SessionAuditLogger(audit *log.Logger) mux.MiddlewareFunc {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
			path := request.URL.Path
			raw := request.URL.RawQuery

			statusWr := statusWriter{ResponseWriter: response}

			// Process request
			next.ServeHTTP(&statusWr, request)

			clientIP := request.RemoteAddr
			method := request.Method
			username := ""

			for key, value := range request.Header {
				if key == "Authorization" { // anonymize from logs
					s := strings.SplitN(value[0], " ", 2) //nolint:mnd
					if len(s) == 2 && strings.EqualFold(s[0], "basic") {
						b, err := base64.StdEncoding.DecodeString(s[1])
						if err == nil {
							pair := strings.SplitN(string(b), ":", 2) //nolint:mnd
							if len(pair) == 2 {                       //nolint:mnd
								username = pair[0]
							}
						}
					}
				}
			}

			statusCode := statusWr.status

			if raw != "" {
				path = path + "?" + raw
			}

			if (method == http.MethodPost || method == http.MethodPut ||
				method == http.MethodPatch || method == http.MethodDelete) &&
				(statusCode == http.StatusOK || statusCode == http.StatusCreated || statusCode == http.StatusAccepted) {
				audit.Info().
					Str("component", "session").
					Str("clientIP", clientIP).
					Str("subject", username).
					Str("action", method).
					Str("object", path).
					Int("status", statusCode).
					Msg("HTTP API Audit")
			}
		})
	}
}
