package log

import (
	"context"
	"log"
	"os"

	"github.com/go-logr/logr"
	"github.com/go-logr/stdr"
	"github.com/go-logr/zapr"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"

	"github.com/tidbcloud/devops/pkg/infra/config"
)

var (
	Logger       logr.Logger = stdr.New(log.New(os.Stderr, "STD_LOG: ", log.LstdFlags))
	AccessLogger logr.Logger
)

func InitLogger(conf *config.LogConfig) error {
	cfg := zap.NewProductionConfig()

	cfg.Level = zap.NewAtomicLevelAt(zapcore.Level(-1 * conf.Level))
	cfg.Development = true
	cfg.Encoding = "console"
	cfg.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder
	cfg.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	opts := []zap.Option{}

	zapLog, err := cfg.Build(opts...)
	if err != nil {
		return err
	}

	Logger = zapr.NewLogger(zapLog)
	return nil
}

func InitAccessLogger(conf *config.LogConfig) error {
	accessCfg := zap.NewProductionEncoderConfig()
	accessCfg.EncodeTime = zapcore.RFC3339TimeEncoder

	fileLogger := &lumberjack.Logger{
		Filename:   conf.AccessLogPath,
		MaxSize:    conf.AccessLogMaxSize,
		MaxBackups: conf.AccessLogMaxBackups,
		MaxAge:     conf.AccessLogMaxAge,
		Compress:   false,
	}
	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(accessCfg),
		zapcore.AddSync(fileLogger),
		zap.InfoLevel,
	)

	AccessLogger = zapr.NewLogger(zap.New(core))

	return nil
}

// NewContext returns a new Context, derived from ctx, which carries the
// provided Logger.
// It is same as `logr.NewContext`.
func NewContext(ctx context.Context, logger logr.Logger, keysAndValues ...interface{}) context.Context {
	return logr.NewContext(ctx, logger.WithValues(keysAndValues...))
}

func WrapContext(ctx context.Context, keysAndValues ...interface{}) context.Context {
	logger := FromContext(ctx)
	return logr.NewContext(ctx, logger.WithValues(keysAndValues...))
}

// FromContextOrDef returns a Logger from ctx.  If no Logger is found, this
// returns the default Logger.
//
// `logr.FromContextOrDiscard` may return a zero logger, it is not we want.
func FromContextOrDef(ctx context.Context, def logr.Logger, keysAndValues ...interface{}) logr.Logger {
	logger := logr.FromContextOrDiscard(ctx)
	if logger.IsZero() {
		logger = def
	}

	return WithTraceID(ctx, logger).WithValues(keysAndValues...)
}

// FromContext returns a Logger from ctx.  If no Logger is found, this
// returns the global Logger.
//
// `logr.FromContextOrDiscard` may return a zero logger, it is not we want.
func FromContext(ctx context.Context, keysAndValues ...interface{}) logr.Logger {
	return FromContextOrDef(ctx, Logger, keysAndValues...)
}

// WithTraceID returns a Logger with trace_id field.
func WithTraceID(ctx context.Context, logger logr.Logger) logr.Logger {
	spanCtx := trace.SpanContextFromContext(ctx)
	if spanCtx.HasTraceID() {
		logger = logger.WithValues("trace_id", spanCtx.TraceID().String())
	} else {
		logger = logger.WithValues("trace_id", "none")
	}
	return logger
}

type LoggerToWriter struct {
	logger logr.Logger
}

func NewLogrToWriter(logger logr.Logger) *LoggerToWriter {
	return &LoggerToWriter{
		logger: logger,
	}
}

func (l *LoggerToWriter) Write(p []byte) (n int, err error) {
	l.logger.Info(string(p))
	return len(p), nil
}
