package logger

import (
	"fmt"

	"git.frostfs.info/TrueCloudLab/zapjournald"
	"github.com/ssgreg/journald"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

// Logger represents a component
// for writing messages to log.
type Logger struct {
	z   *zap.Logger
	lvl zap.AtomicLevel
}

// Prm groups Logger's parameters.
// Successful passing non-nil parameters to the NewLogger (if returned
// error is nil) connects the parameters with the returned Logger.
// Parameters that have been connected to the Logger support its
// configuration changing.
//
// Passing Prm after a successful connection via the NewLogger, connects
// the Prm to a new instance of the Logger.
//
// See also Reload, SetLevelString.
type Prm struct {
	// link to the created Logger
	// instance; used for a runtime
	// reconfiguration
	_log *Logger

	// support runtime rereading
	level zapcore.Level

	// SamplingHook hook for the zap.Logger
	SamplingHook func(e zapcore.Entry, sd zapcore.SamplingDecision)

	// do not support runtime rereading
	dest string

	// PrependTimestamp specifies whether to prepend a timestamp in the log
	PrependTimestamp bool
}

const (
	DestinationUndefined = ""
	DestinationStdout    = "stdout"
	DestinationJournald  = "journald"
)

// SetLevelString sets the minimum logging level. Default is
// "info".
//
// Returns an error if s is not a string representation of a
// supporting logging level.
//
// Supports runtime rereading.
func (p *Prm) SetLevelString(s string) error {
	return p.level.UnmarshalText([]byte(s))
}

func (p *Prm) SetDestination(d string) error {
	if d != DestinationStdout && d != DestinationJournald {
		return fmt.Errorf("invalid logger destination %s", d)
	}
	if p != nil {
		p.dest = d
	}
	return nil
}

// Reload reloads configuration of a connected instance of the Logger.
// Returns ErrLoggerNotConnected if no connection has been performed.
// Returns any reconfiguration error from the Logger directly.
func (p Prm) Reload() error {
	if p._log == nil {
		// incorrect logger usage
		panic("parameters are not connected to any Logger")
	}

	return p._log.reload(p)
}

func defaultPrm() *Prm {
	return new(Prm)
}

// NewLogger constructs a new zap logger instance. Constructing with nil
// parameters is safe: default values will be used then.
// Passing non-nil parameters after a successful creation (non-error) allows
// runtime reconfiguration.
//
// Logger is built from production logging configuration with:
//   - parameterized level;
//   - console encoding;
//   - ISO8601 time encoding.
//
// Logger records a stack trace for all messages at or above fatal level.
func NewLogger(prm *Prm) (*Logger, error) {
	if prm == nil {
		prm = defaultPrm()
	}
	switch prm.dest {
	case DestinationUndefined, DestinationStdout:
		return newConsoleLogger(prm)
	case DestinationJournald:
		return newJournaldLogger(prm)
	default:
		return nil, fmt.Errorf("unknown destination %s", prm.dest)
	}
}

func newConsoleLogger(prm *Prm) (*Logger, error) {
	lvl := zap.NewAtomicLevelAt(prm.level)

	c := zap.NewProductionConfig()
	c.Level = lvl
	c.Encoding = "console"
	if prm.SamplingHook != nil {
		c.Sampling.Hook = prm.SamplingHook
	}

	if prm.PrependTimestamp {
		c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	} else {
		c.EncoderConfig.TimeKey = ""
	}

	lZap, err := c.Build(
		zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel)),
		zap.AddCallerSkip(1),
	)
	if err != nil {
		return nil, err
	}

	l := &Logger{z: lZap, lvl: lvl}
	prm._log = l

	return l, nil
}

func newJournaldLogger(prm *Prm) (*Logger, error) {
	lvl := zap.NewAtomicLevelAt(prm.level)

	c := zap.NewProductionConfig()
	c.Level = lvl
	c.Encoding = "console"
	if prm.SamplingHook != nil {
		c.Sampling.Hook = prm.SamplingHook
	}

	if prm.PrependTimestamp {
		c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	} else {
		c.EncoderConfig.TimeKey = ""
	}

	encoder := zapjournald.NewPartialEncoder(zapcore.NewConsoleEncoder(c.EncoderConfig), zapjournald.SyslogFields)

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

	lZap := zap.New(coreWithContext, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel)), zap.AddCallerSkip(1))

	l := &Logger{z: lZap, lvl: lvl}
	prm._log = l

	return l, nil
}

func (l *Logger) reload(prm Prm) error {
	l.lvl.SetLevel(prm.level)
	return nil
}

func (l *Logger) WithOptions(options ...zap.Option) {
	l.z = l.z.WithOptions(options...)
}

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

func NewLoggerWrapper(z *zap.Logger) *Logger {
	return &Logger{
		z: z.WithOptions(zap.AddCallerSkip(1)),
	}
}