package logs

import (
	"fmt"
	golog "log"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

const (
	initialSampleSize = 100
	sampleRate        = 100
)

type Logger interface {
	Error(string, ...zapcore.Field)
	Fatal(string, ...zapcore.Field)
	Debug(string, ...zapcore.Field)
	Info(string, ...zapcore.Field)
	Warn(string, ...zapcore.Field)
	Log(string, ...interface{})
	NewStdLogger() *golog.Logger
	With(...zap.Field) Logger
}

type logger struct {
	*zap.Logger
}

func New(isLocalDev bool) (*logger, error) {
	var log *zap.Logger
	var err error

	if isLocalDev {
		log, err = zap.NewDevelopment()
	} else {
		// Assume deployed in some environment like staging, canary, or production.
		config := zap.Config{
			Level:             zap.NewAtomicLevelAt(zap.InfoLevel),
			Encoding:          "json",
			DisableCaller:     true,
			DisableStacktrace: true,
			EncoderConfig:     zap.NewProductionEncoderConfig(),
			OutputPaths:       []string{"stderr"},
			ErrorOutputPaths:  []string{"stderr"},
			// Zap samples by logging the first N entries with a given level and
			// message each tick. If more Entries with the same level and message
			// are seen during the same interval, every Mth message is logged and the
			// rest are dropped.
			Sampling: &zap.SamplingConfig{
				Initial:    initialSampleSize, // accept the first `Initial` logs each second...
				Thereafter: sampleRate,        // and one log in every `Thearafter` after that...
			},
		}
		log, err = config.Build()
	}

	if err != nil {
		return nil, err
	}

	return &logger{Logger: log}, nil
}

func (l *logger) With(fields ...zap.Field) Logger {
	return &logger{Logger: l.Logger.With(fields...)}
}

func (l *logger) NewStdLogger() *golog.Logger {
	return zap.NewStdLog(l.Logger)
}

// Log allows zap logger to meet the Fulton TwitchLogging interface: code.justin.tv/amzn/TwitchLogging
// Log does a best effort logging of the given args using zap as an underlying logger
// The fulton logger expects a main message, then a series of keys and values as
// followup arguments for structured logging, like
//     fultonlogger.Log("my main message!", "KEY1", "VALUE1", "KEY2", "VALUE2")
//
// Here, we will usher this into something that zap likes by taking those keypairs
// and shoving them into zap.String() calls
//
// Implementation heavily adapted from JSON fulton logger:
// https://git-aws.internal.justin.tv/amzn/TwitchLoggingCommonLoggers/blob/mainline/json_logger.go#L35-L54
func (l *logger) Log(msg string, keyvals ...interface{}) {
	numFields := ((len(keyvals) + (len(keyvals) % 2)) / 2)
	fields := make([]zapcore.Field, numFields)
	for i := 0; i < len(keyvals); i += 2 {
		var key, value interface{}
		if i == len(keyvals)-1 {
			key, value = "UNKEYED_VALUE", keyvals[i]
		} else {
			key, value = keyvals[i], keyvals[i+1]
		}
		fields[i/2] = zap.String(fmt.Sprint(key), fmt.Sprint(value))
	}
	l.Info(msg, fields...)
}

// Noop is a Logger implementation which does nothing.
// Useful when you need to satisfy an interface in unit testing, but do not care
// to inspect what was logged.
type Noop struct{}

type NoopWriter struct{}

func (*NoopWriter) Write(p []byte) (int, error) {
	return len(p), nil
}

func (*Noop) Error(string, ...zapcore.Field) {}
func (*Noop) Debug(string, ...zapcore.Field) {}
func (*Noop) Fatal(string, ...zapcore.Field) {}
func (*Noop) Info(string, ...zapcore.Field)  {}
func (*Noop) Warn(string, ...zapcore.Field)  {}
func (*Noop) Log(string, ...interface{})     {}
func (*Noop) NewStdLogger() *golog.Logger {
	return golog.New(&NoopWriter{}, "", 0)
}
func (n *Noop) With(...zap.Field) Logger { return n }
