package interceptor

import (
	"context"
	"errors"
	"time"

	"github.com/pingcap/errcode"
	"github.com/tidbcloud/dataflow-service/app/config"
	"github.com/tidbcloud/dataflow-service/internal/bizutils"
	pkglog "github.com/tidbcloud/dataflow-service/pkg/log"
	"github.com/tidbcloud/dataflow-service/pkg/redact"
	"github.com/tidbcloud/pkgv2/bizerr"
	"go.uber.org/zap"
	"google.golang.org/grpc"
)

const requestTooLongTimeMS = 5000

func RequestLogUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(
		ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
	) (interface{}, error) {
		startTime := time.Now()
		printRequestInputLog(ctx, req, info)
		ret, err := handler(ctx, req)
		printRequestOutputLog(ctx, startTime, err, ret, info)
		return ret, err
	}
}

func printRequestInputLog(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo) {
	defer func() {
		if err := recover(); err != nil {
			pkglog.GetLogger(ctx).Error("print request input panic occurred", zap.Any("err", err))
			return
		}
	}()
	logger := pkglog.GetLogger(ctx)
	method := HttpMethod(ctx)
	fields := func() []zap.Field {
		redaction := redact.PasswordFields
		payload, reqErr := redact.RedactKeysToJSON(redaction, req)
		if reqErr != nil {
			logger.Error("error during request redaction", zap.Error(reqErr))
		}
		return []zap.Field{
			zap.String("http_method", method),
			zap.String("url", HttpURI(ctx)),
			zap.Any("user", getUserID(ctx)),
			zap.String("grpc_method", info.FullMethod),
			zap.String("request", string(payload)),
		}
	}
	if method != "" && method != "GET" && method != "OPTION" {
		logger.Info("mutation api call", fields()...)
	} else {
		logger.Info("query api call", fields()...)
	}
}

func getUserID(ctx context.Context) uint64 {
	user, _ := bizutils.GetUserFromContext(ctx)
	userID := uint64(0)
	if user != nil {
		userID = user.ID
	}
	return userID
}

func printRequestOutputLog(
	ctx context.Context, startTime time.Time, err error, ret interface{}, info *grpc.UnaryServerInfo,
) {
	defer func() {
		if err := recover(); err != nil {
			pkglog.GetLogger(ctx).Error("print request output panic occurred", zap.Any("err", err))
			return
		}
	}()
	method := HttpMethod(ctx)
	logger := pkglog.GetLogger(ctx)
	if time.Since(startTime).Milliseconds() > requestTooLongTimeMS {
		logger.Warn("slow api call",
			zap.String("method", method), zap.String("url", HttpURI(ctx)),
			zap.Int64("time", time.Since(startTime).Milliseconds()))
	} else {
		logger.Info("api call", zap.String("method", method), zap.String("url", HttpURI(ctx)))
	}

	fields := func() []zap.Field {
		redaction := redact.PasswordFields
		resp, respErr := redact.RedactKeysToJSON(redaction, ret)
		if respErr != nil {
			logger.Error("error during response redaction", zap.Error(respErr))
		}

		httpCode := 0
		if err != nil {
			httpCode = 500
			var ec errcode.ErrorCode
			if errors.As(err, &ec) {
				httpCode = ec.Code().HTTPCode()
			}
			var berr bizerr.BizError
			if errors.As(err, &berr) {
				httpCode = config.ConvertBizErrorToHTTPStatus(berr.ErrCode())
			}
		} else {
			httpCode = 200
		}

		return []zap.Field{
			zap.String("http_method", HttpMethod(ctx)),
			zap.Int("httpCode", httpCode),
			zap.String("url", HttpURI(ctx)),
			zap.Any("user", getUserID(ctx)),
			zap.String("grpc_method", info.FullMethod),
			zap.String("response", string(resp)),
		}
	}

	logger.Info("call API", append(fields(), zap.Error(err))...)
}
