package main

import (
	"fmt"
	"os"

	"git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/logs"
	"git.frostfs.info/TrueCloudLab/zapjournald"
	"github.com/spf13/viper"
	"github.com/ssgreg/journald"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

func getLogLevel(v *viper.Viper) (zapcore.Level, error) {
	var lvl zapcore.Level
	lvlStr := v.GetString(cfgLoggerLevel)
	err := lvl.UnmarshalText([]byte(lvlStr))
	if err != nil {
		return lvl, fmt.Errorf("incorrect logger level configuration %s (%v), "+
			"value should be one of %v", lvlStr, err, [...]zapcore.Level{
			zapcore.DebugLevel,
			zapcore.InfoLevel,
			zapcore.WarnLevel,
			zapcore.ErrorLevel,
			zapcore.DPanicLevel,
			zapcore.PanicLevel,
			zapcore.FatalLevel,
		})
	}
	return lvl, nil
}

var _ zapcore.Core = (*zapCoreTagFilterWrapper)(nil)

type zapCoreTagFilterWrapper struct {
	core     zapcore.Core
	settings TagFilterSettings
	maxLvl   MaxLevelSetting
	extra    []zap.Field
}

type TagFilterSettings interface {
	LevelEnabled(tag string, lvl zapcore.Level) bool
}

type MaxLevelSetting interface {
	MaxLevel() zap.AtomicLevel
}

func (c *zapCoreTagFilterWrapper) Enabled(level zapcore.Level) bool {
	return c.core.Enabled(level)
}

func (c *zapCoreTagFilterWrapper) With(fields []zapcore.Field) zapcore.Core {
	return &zapCoreTagFilterWrapper{
		core:     c.core.With(fields),
		settings: c.settings,
		maxLvl:   c.maxLvl,
		extra:    append(c.extra, fields...),
	}
}

func (c *zapCoreTagFilterWrapper) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
	if c.core.Enabled(entry.Level) {
		return checked.AddCore(entry, c)
	}
	return checked
}

func (c *zapCoreTagFilterWrapper) Write(entry zapcore.Entry, fields []zapcore.Field) error {
	if c.shouldSkip(entry, fields) || c.shouldSkip(entry, c.extra) {
		return nil
	}

	return c.core.Write(entry, fields)
}

func (c *zapCoreTagFilterWrapper) shouldSkip(entry zapcore.Entry, fields []zap.Field) bool {
	for _, field := range fields {
		if field.Key == logs.TagFieldName && field.Type == zapcore.StringType {
			return !c.settings.LevelEnabled(field.String, entry.Level)
		}
	}
	return !c.maxLvl.MaxLevel().Enabled(entry.Level)
}

func (c *zapCoreTagFilterWrapper) Sync() error {
	return c.core.Sync()
}

func applyZapCoreMiddlewares(core zapcore.Core, v *viper.Viper, loggerSettings LoggerAppSettings, tagSetting TagFilterSettings) zapcore.Core {
	core = &zapCoreTagFilterWrapper{
		core:     core,
		settings: tagSetting,
	}

	if v.GetBool(cfgLoggerSamplingEnabled) {
		core = zapcore.NewSamplerWithOptions(core,
			v.GetDuration(cfgLoggerSamplingInterval),
			v.GetInt(cfgLoggerSamplingInitial),
			v.GetInt(cfgLoggerSamplingThereafter),
			zapcore.SamplerHook(func(_ zapcore.Entry, dec zapcore.SamplingDecision) {
				if dec&zapcore.LogDropped > 0 {
					loggerSettings.DroppedLogsInc()
				}
			}))
	}

	return core
}

func newLogEncoder() zapcore.Encoder {
	c := zap.NewProductionEncoderConfig()
	c.EncodeTime = zapcore.ISO8601TimeEncoder

	return zapcore.NewConsoleEncoder(c)
}

// newStdoutLogger constructs a zap.Logger instance for current application.
// Panics on failure.
//
// Logger is built from zap's production logging configuration with:
//   - parameterized level (debug by default)
//   - console encoding
//   - ISO8601 time encoding
//
// Logger records a stack trace for all messages at or above fatal level.
//
// See also zapcore.Level, zap.NewProductionConfig, zap.AddStacktrace.
func newStdoutLogger(v *viper.Viper, lvl zap.AtomicLevel, loggerSettings LoggerAppSettings, tagSetting TagFilterSettings) *Logger {
	stdout := zapcore.AddSync(os.Stderr)

	consoleOutCore := zapcore.NewCore(newLogEncoder(), stdout, lvl)
	consoleOutCore = applyZapCoreMiddlewares(consoleOutCore, v, loggerSettings, tagSetting)

	return &Logger{
		logger: zap.New(consoleOutCore, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel))),
		lvl:    lvl,
	}
}

func newJournaldLogger(v *viper.Viper, lvl zap.AtomicLevel, loggerSettings LoggerAppSettings, tagSetting TagFilterSettings) *Logger {
	encoder := zapjournald.NewPartialEncoder(newLogEncoder(), zapjournald.SyslogFields)

	core := zapjournald.NewCore(lvl, encoder, &journald.Journal{}, zapjournald.SyslogFields)
	coreWithContext := core.With([]zapcore.Field{
		zapjournald.SyslogFacility(zapjournald.LogDaemon),
		zapjournald.SyslogIdentifier(),
		zapjournald.SyslogPid(),
	})

	coreWithContext = applyZapCoreMiddlewares(coreWithContext, v, loggerSettings, tagSetting)

	return &Logger{
		logger: zap.New(coreWithContext, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel))),
		lvl:    lvl,
	}
}

type LoggerAppSettings interface {
	DroppedLogsInc()
}

func pickLogger(v *viper.Viper, lvl zap.AtomicLevel, loggerSettings LoggerAppSettings, tagSettings TagFilterSettings) *Logger {
	dest := v.GetString(cfgLoggerDestination)

	switch dest {
	case destinationStdout:
		return newStdoutLogger(v, lvl, loggerSettings, tagSettings)
	case destinationJournald:
		return newJournaldLogger(v, lvl, loggerSettings, tagSettings)
	default:
		panic(fmt.Sprintf("wrong destination for logger: %s", dest))
	}
}