package logger

import (
	"strings"

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

type (
	Option func(o *options)

	options struct {
		Options []zap.Option

		SamplingInitial    int
		SamplingThereafter int

		Format     string
		Level      string
		TraceLevel string

		NoCaller     bool
		NoDisclaimer bool

		AppName    string
		AppVersion string
	}
)

const (
	formatJSON    = "json"
	formatConsole = "console"

	defaultSamplingInitial    = 100
	defaultSamplingThereafter = 100

	lvlInfo  = "info"
	lvlWarn  = "warn"
	lvlDebug = "debug"
	lvlError = "error"
	lvlFatal = "fatal"
	lvlPanic = "panic"
)

func safeLevel(lvl string) zap.AtomicLevel {
	switch strings.ToLower(lvl) {
	case lvlDebug:
		return zap.NewAtomicLevelAt(zap.DebugLevel)
	case lvlWarn:
		return zap.NewAtomicLevelAt(zap.WarnLevel)
	case lvlError:
		return zap.NewAtomicLevelAt(zap.ErrorLevel)
	case lvlFatal:
		return zap.NewAtomicLevelAt(zap.FatalLevel)
	case lvlPanic:
		return zap.NewAtomicLevelAt(zap.PanicLevel)
	default:
		return zap.NewAtomicLevelAt(zap.InfoLevel)
	}
}

func defaults() *options {
	return &options{
		SamplingInitial:    defaultSamplingInitial,
		SamplingThereafter: defaultSamplingThereafter,

		Format:     formatConsole,
		Level:      lvlDebug,
		TraceLevel: lvlInfo,

		NoCaller:     false,
		NoDisclaimer: false,

		AppName:    "",
		AppVersion: "",
	}
}

func New(opts ...Option) (*zap.Logger, error) {
	o := defaults()
	c := zap.NewProductionConfig()

	c.OutputPaths = []string{"stdout"}
	c.ErrorOutputPaths = []string{"stdout"}

	for _, opt := range opts {
		opt(o)
	}

	// set sampling
	c.Sampling = &zap.SamplingConfig{
		Initial:    o.SamplingInitial,
		Thereafter: o.SamplingThereafter,
	}

	// logger level
	c.Level = safeLevel(o.Level)
	traceLvl := safeLevel(o.TraceLevel)

	// logger format
	switch f := o.Format; strings.ToLower(f) {
	case formatConsole:
		c.Encoding = formatConsole
	default:
		c.Encoding = formatJSON
	}

	// logger time
	c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder

	if o.NoCaller {
		c.EncoderConfig.EncodeCaller = nil
	}

	// enable trace only for current log-level
	o.Options = append(o.Options, zap.AddStacktrace(traceLvl))

	l, err := c.Build(o.Options...)
	if err != nil {
		return nil, err
	}

	if o.NoDisclaimer {
		return l, nil
	}

	return l.With(
		zap.String("app_name", o.AppName),
		zap.String("app_version", o.AppVersion)), nil
}