package controller

import (
	"context"
	"dataservices/commons/config"
	"dataservices/commons/contextvar"
	dto "dataservices/internal/dto/dataapi_gateway"
	"dataservices/internal/entity"
	"fmt"
	"net/http"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	jsoniter "github.com/json-iterator/go"
	"github.com/pingcap/log"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
)

const userRequestIDHeader = "X-Custom-Requestid"

type customLogger struct {
	*zap.Logger
}

var defaultCustomLogger customLogger

func InitCustomLogger(cnf config.CustomLogWriteConfig) error {
	pingcapLogCfg := log.Config{
		Level:  "info",
		Format: "json",
		File: log.FileLogConfig{
			Filename:   filepath.Join(cnf.LogDir, cnf.FileName),
			MaxSize:    cnf.MaxSize,
			MaxDays:    cnf.MaxDays,
			MaxBackups: cnf.MaxBackups,
		},
		DisableCaller:       true,
		DisableStacktrace:   true,
		DisableErrorVerbose: true,
	}
	l, _, err := log.InitLogger(&pingcapLogCfg)
	if err != nil {
		return err
	}
	defaultCustomLogger.Logger = l
	return nil
}

func (l *customLogger) getTraceId(ctx context.Context) string {
	spanCtx := trace.SpanContextFromContext(ctx)
	tID := ""
	if spanCtx.HasTraceID() {
		tID = spanCtx.TraceID().String()
	}
	return tID
}

func (l *customLogger) getReqId(gc *gin.Context) string {
	reqId := gc.GetHeader(userRequestIDHeader)
	if len(reqId) == 0 {
		reqId = l.getTraceId(gc.Request.Context())
	}
	return reqId
}

func (l *customLogger) traceIDFromContext(ctx context.Context) zap.Field {
	return zap.String("trace_id", l.getTraceId(ctx))
}

func (l *customLogger) userRequestIDFromContext(gc *gin.Context) zap.Field {
	return zap.String("req_id", l.getReqId(gc))
}

type ClientInfo struct {
	Ip string `json:"ip"`
}

func (l *customLogger) fillFields(gc *gin.Context, result *dto.AdvancedRes, endpoint *entity.Endpoint, dataapp *entity.DataApp, body string) ([]zap.Field, []zap.Field) {
	fields := make([]zap.Field, 0, 16)
	metricsFields := make([]zap.Field, 0, 4)
	// fetch trace_id
	fields = append(fields, l.traceIDFromContext(gc.Request.Context()))
	// fetch req_id
	fields = append(fields, l.userRequestIDFromContext(gc))
	// fetch org info
	fields = append(fields, zap.Uint64("org", dataapp.OrgId))
	metricsFields = append(metricsFields, zap.Uint64("org", dataapp.OrgId))
	// fetch dataapp info
	fields = append(fields, zap.Uint64("dataapp", endpoint.DataappId))
	metricsFields = append(metricsFields, zap.String("dataapp", dataapp.ExternalAppID))
	// fetch cluster info
	fields = append(fields, zap.String("cluster", strconv.FormatUint(endpoint.ClusterId, 10)))
	metricsFields = append(metricsFields, zap.String("cluster", "0"))
	// fetch endpoint info
	fields = append(fields, zap.String("endpoint_id", strconv.FormatUint(endpoint.ID, 10)), zap.String("endpoint_name", fmt.Sprintf("%s (%s)", endpoint.Name, endpoint.Method)))
	metricsFields = append(metricsFields, zap.String("endpoint_id", strconv.FormatUint(endpoint.ID, 10)))
	fields = append(fields, zap.String("type", "endpoints"))
	// fetch latency
	if result != nil {
		latency, err := time.ParseDuration(result.Data.Result.Latency)
		if err != nil {
			latency = time.Duration(result.Data.Result.EndMs-result.Data.Result.StartMs) * time.Millisecond
		}
		fields = append(fields, zap.String("latency", fmt.Sprintf("%.2fms", float64(latency.Microseconds())/1000.0)))
		metricsFields = append(metricsFields, zap.Float64("latency", float64(latency.Microseconds())/1000.0))
	} else {
		latency := time.Since(contextvar.RequestBeginTime(gc.Request.Context()))
		fields = append(fields, zap.String("latency", fmt.Sprintf("%.2fms", float64(latency.Microseconds())/1000.0)))
		metricsFields = append(metricsFields, zap.Float64("latency", float64(latency.Microseconds())/1000.0))
	}
	bodySize := 0.0
	if gc.Writer.Written() {
		bodySize = float64(gc.Writer.Size()) / 1024.0
	}
	fields = append(fields, zap.String("rsp_size", fmt.Sprintf("%.2fKiB", bodySize)))
	fields = append(fields, zap.String("method", gc.Request.Method))
	if result == nil {
		fields = append(fields, zap.Int("http_code", gc.Writer.Status()))
		fields = append(fields, zap.String("http_msg", http.StatusText(gc.Writer.Status())))
	} else {
		fields = append(fields, zap.Int("http_code", int(result.Data.Result.Code)))
		fields = append(fields, zap.String("http_msg", result.Data.Result.Message))
	}

	fields = append(fields, zap.String("url", fmt.Sprintf("https://%s%s", gc.Request.Host, gc.Request.URL.String())))
	qr := gc.Request.URL.Query()
	qrStr, _ := jsoniter.MarshalToString(qr)
	fields = append(fields, zap.String("query_param", qrStr))
	headers := make(map[string]string)
	for k, v := range gc.Request.Header {
		headers[k] = strings.Join(v, ";")
	}
	headersJson, _ := jsoniter.MarshalToString(headers)
	fields = append(fields, zap.String("req_header", headersJson))
	if gc.Request.Method == http.MethodPut || gc.Request.Method == http.MethodPost {
		fields = append(fields, zap.String("req_body", body))
	}
	if result != nil {
		fields = append(fields, zap.Int64("row_cnt", result.Data.Result.RowCount))
	} else {
		fields = append(fields, zap.Int64("row_cnt", 0))
	}

	var client ClientInfo
	client.Ip = gc.ClientIP()
	clstr, _ := jsoniter.MarshalToString(&client)
	fields = append(fields, zap.String("client", clstr))

	return fields, metricsFields
}

func (l *customLogger) LogExecuteSuccess(gc *gin.Context, result *dto.AdvancedRes, endpoint *entity.Endpoint, dataapp *entity.DataApp, cached bool, body string) {
	if endpoint == nil || dataapp == nil {
		return
	}
	fields, metricsFields := l.fillFields(gc, result, endpoint, dataapp, body)
	fields = append(fields, zap.Bool("cache", cached))
	message := ""
	if result != nil && (result.Data.Result.Code != 200 || dataapp.Settings.LogExecResult) {
		message, _ = jsoniter.MarshalToString(result)
	}
	if result == nil || result.Data.Result.Code == 200 {
		l.Info(message, fields...)
		l.Info("", metricsFields...)
	} else {
		l.Error(message, fields...)
		l.Error("", metricsFields...)
	}
}

func (l *customLogger) LogExecuteError(gc *gin.Context, result *dto.AdvancedRes, endpoint *entity.Endpoint, dataapp *entity.DataApp, body string) {
	if endpoint == nil || result == nil || dataapp == nil {
		return
	}
	fields, metricsFields := l.fillFields(gc, result, endpoint, dataapp, body)
	fields = append(fields, zap.Bool("cache", false))
	message, _ := jsoniter.MarshalToString(result)
	l.Error(message, fields...)
	l.Error("", metricsFields...)
}
