//go:build loghttp

package middleware

import (
	"bytes"
	"io"
	"net/http"
	"os"

	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/detector"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/xmlutils"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

type (
	LogHTTPSettings interface {
		LogHTTPConfig() LogHTTPConfig
	}
	LogHTTPConfig struct {
		Enabled    bool
		MaxBody    int64
		MaxLogSize int
		OutputPath string
		UseGzip    bool
		log        *httpLogger
	}
	httpLogger struct {
		*zap.Logger
		logRoller *lumberjack.Logger
	}
	// responseReadWriter helps read http response body.
	responseReadWriter struct {
		http.ResponseWriter
		response   *bytes.Buffer
		statusCode int
	}
)

const (
	payloadLabel  = "payload"
	responseLabel = "response"
)

func (lc *LogHTTPConfig) InitHTTPLogger(log *zap.Logger) {
	if err := lc.initHTTPLogger(); err != nil {
		log.Error(logs.FailedToInitializeHTTPLogger, zap.Error(err))
	}
}

// initHTTPLogger returns registers zap sink and returns new httpLogger.
func (lc *LogHTTPConfig) initHTTPLogger() (err error) {
	lc.log = &httpLogger{
		Logger:    zap.NewNop(),
		logRoller: &lumberjack.Logger{},
	}
	c := newLoggerConfig()
	lc.log.Logger, err = c.Build()
	if err != nil {
		return err
	}
	lc.setLogOutput()

	return nil
}

// newLoggerConfig creates new zap.Config with disabled base fields.
func newLoggerConfig() zap.Config {
	c := zap.NewProductionConfig()
	c.DisableCaller = true
	c.DisableStacktrace = true
	c.EncoderConfig = newEncoderConfig()
	c.Sampling = nil

	return c
}

func (lc *LogHTTPConfig) setLogOutput() {
	var output zapcore.WriteSyncer
	switch lc.OutputPath {
	case "", StdoutPath:
		output = zapcore.AddSync(os.Stdout)
	case StderrPath:
		output = zapcore.AddSync(os.Stderr)
	default:
		output = zapcore.AddSync(&lumberjack.Logger{
			Filename: lc.OutputPath,
			MaxSize:  lc.MaxLogSize,
			Compress: lc.UseGzip,
		})
	}

	// create logger with new sync
	lc.log.Logger = lc.log.Logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
		return zapcore.NewCore(zapcore.NewJSONEncoder(newEncoderConfig()), output, zapcore.InfoLevel)
	}))
}

func newEncoderConfig() zapcore.EncoderConfig {
	c := zap.NewProductionEncoderConfig()
	c.MessageKey = zapcore.OmitKey
	c.LevelKey = zapcore.OmitKey
	c.TimeKey = zapcore.OmitKey
	c.FunctionKey = zapcore.OmitKey

	return c
}

func (ww *responseReadWriter) Write(data []byte) (int, error) {
	ww.response.Write(data)
	return ww.ResponseWriter.Write(data)
}

func (ww *responseReadWriter) WriteHeader(code int) {
	ww.statusCode = code
	ww.ResponseWriter.WriteHeader(code)
}

func (ww *responseReadWriter) Flush() {
	if f, ok := ww.ResponseWriter.(http.Flusher); ok {
		f.Flush()
	}
}

// LogHTTP logs http parameters from s3 request.
func LogHTTP(l *zap.Logger, settings LogHTTPSettings) Func {
	return func(h http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			config := settings.LogHTTPConfig()
			if !config.Enabled || config.log == nil {
				h.ServeHTTP(w, r)
				return
			}

			httplog := config.log.getHTTPLogger(r).
				withFieldIfExist("query", r.URL.Query()).
				withFieldIfExist("headers", r.Header)

			payload := getBody(r.Body, l)
			r.Body = io.NopCloser(bytes.NewReader(payload))

			payloadReader := io.LimitReader(bytes.NewReader(payload), config.MaxBody)
			httplog = httplog.withProcessedBody(payloadLabel, payloadReader, l)

			wr := newResponseReadWriter(w)
			h.ServeHTTP(wr, r)

			respReader := io.LimitReader(wr.response, config.MaxBody)
			httplog = httplog.withProcessedBody(responseLabel, respReader, l)
			httplog = httplog.with(zap.Int("status", wr.statusCode))

			httplog.Info(logs.LogHTTP)
		})
	}
}

// withFieldIfExist checks whether data is not empty and attach it to log output.
func (lg *httpLogger) withFieldIfExist(label string, data map[string][]string) *httpLogger {
	if len(data) != 0 {
		return lg.with(zap.Any(label, data))
	}
	return lg
}

func (lg *httpLogger) with(fields ...zap.Field) *httpLogger {
	return &httpLogger{
		Logger:    lg.Logger.With(fields...),
		logRoller: lg.logRoller,
	}
}

func (lg *httpLogger) getHTTPLogger(r *http.Request) *httpLogger {
	return lg.with(
		zap.String("from", r.RemoteAddr),
		zap.String("URI", r.RequestURI),
		zap.String("method", r.Method),
		zap.String("protocol", r.Proto),
	)
}

func (lg *httpLogger) withProcessedBody(label string, bodyReader io.Reader, l *zap.Logger) *httpLogger {
	resp, err := processBody(bodyReader)
	if err != nil {
		l.Error(logs.FailedToProcessHTTPBody,
			zap.Error(err),
			zap.String("body type", payloadLabel))
		return lg
	}

	return lg.with(zap.ByteString(label, resp))
}

func newResponseReadWriter(w http.ResponseWriter) *responseReadWriter {
	return &responseReadWriter{
		ResponseWriter: w,
		response:       &bytes.Buffer{},
	}
}

func getBody(httpBody io.ReadCloser, l *zap.Logger) []byte {
	defer func(httpBody io.ReadCloser) {
		if err := httpBody.Close(); err != nil {
			l.Error(logs.FailedToCloseHTTPBody, zap.Error(err))
		}
	}(httpBody)

	body, err := io.ReadAll(httpBody)
	if err != nil {
		l.Error(logs.FailedToReadHTTPBody,
			zap.Error(err),
			zap.String("body type", payloadLabel))
		return nil
	}
	return body
}

// processBody reads body and base64 encode it if it's not XML.
func processBody(bodyReader io.Reader) ([]byte, error) {
	resultBody := &bytes.Buffer{}
	detect := detector.NewDetector(bodyReader, xmlutils.DetectXML)
	dataType, err := detect.Detect()
	if err != nil {
		return nil, err
	}
	writer := xmlutils.ChooseWriter(dataType, resultBody)
	if _, err = io.Copy(writer, detect.RestoredReader()); err != nil {
		return nil, err
	}
	if err = writer.Close(); err != nil {
		return nil, err
	}

	return resultBody.Bytes(), nil
}