package flume

import (
	"fmt"
	"go.uber.org/multierr"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"runtime"
	"sync/atomic"
	"time"
)

var _ Logger = (*Core)(nil)

type atomicInnerCore struct {
	innerLoggerPtr atomic.Value
}

func (af *atomicInnerCore) get() *innerCore {
	return af.innerLoggerPtr.Load().(*innerCore)
}

func (af *atomicInnerCore) set(ic *innerCore) {
	af.innerLoggerPtr.Store(ic)
}

// innerCore holds state which can be reconfigured at the factory level.
// if these settings are changed in the factory, the factory builds new
// innerCore instances, and atomically injects them into all existing loggers.
type innerCore struct {
	name string
	zapcore.Core
	addCaller   bool
	errorOutput zapcore.WriteSyncer
	hooks       []HookFunc
}

// Core is the concrete implementation of Logger.  It has some additional
// lower-level methods which can be used by other logging packages which wrap
// flume, to build alternate logging interfaces.
type Core struct {
	*atomicInnerCore
	context    []zap.Field
	callerSkip int
	// these are logger-scoped hooks, which only hook into this particular logger
	hooks []HookFunc
}

// Log is the core logging method, used by the convenience methods Debug(), Info(), and Error().
//
// Returns true if the log was actually logged.
//
// AddCaller option will report the caller of this method.  If wrapping this, be sure to
// use the AddCallerSkip option.
func (l *Core) Log(lvl Level, template string, fmtArgs, context []interface{}) bool {
	// call another method, just to add a caller to the call stack, so the
	// add caller option resolves the right caller in the stack
	return l.log(lvl, template, fmtArgs, context)
}

// log must be called directly from one of the public methods to make the addcaller
// resolution resolve the caller of the public method.
func (l *Core) log(lvl Level, template string, fmtArgs, context []interface{}) bool {
	c := l.get()

	if !c.Enabled(zapcore.Level(lvl)) {
		return false
	}

	msg := template
	if msg == "" && len(fmtArgs) > 0 {
		msg = fmt.Sprint(fmtArgs...)
	} else if msg != "" && len(fmtArgs) > 0 {
		msg = fmt.Sprintf(template, fmtArgs...)
	}

	// check must always be called directly by a method in the Logger interface
	// (e.g., Log, Info, Debug).
	const callerSkipOffset = 2

	// Create basic checked entry thru the core; this will be non-nil if the
	// log message will actually be written somewhere.
	ent := zapcore.Entry{
		LoggerName: c.name,
		Time:       time.Now(),
		Level:      zapcore.Level(lvl),
		Message:    msg,
	}
	ce := c.Check(ent, nil)
	if ce == nil {
		return false
	}

	// Thread the error output through to the CheckedEntry.
	ce.ErrorOutput = c.errorOutput
	if c.addCaller {
		ce.Entry.Caller = zapcore.NewEntryCaller(runtime.Caller(l.callerSkip + callerSkipOffset))
		if !ce.Entry.Caller.Defined {
			_, _ = fmt.Fprintf(c.errorOutput, "%v Logger.check error: failed to get caller\n", time.Now().UTC())
			_ = ce.ErrorOutput.Sync()
		}
	}

	fields := append(l.context, l.sweetenFields(context)...) //nolint:gocritic

	// execute global hooks, which might modify the fields
	for i := range c.hooks {
		if f := c.hooks[i](ce, fields); f != nil {
			fields = f
		}
	}

	// execute logger hooks
	for i := range l.hooks {
		if f := l.hooks[i](ce, fields); f != nil {
			fields = f
		}
	}

	ce.Write(fields...)
	return true
}

// IsEnabled returns true if the specified level is enabled.
func (l *Core) IsEnabled(lvl Level) bool {
	return l.get().Enabled(zapcore.Level(lvl))
}

const (
	_oddNumberErrMsg    = "Ignored key without a value."
	_nonStringKeyErrMsg = "Ignored key-value pairs with non-string keys."
)

func (l *Core) sweetenFields(args []interface{}) []zap.Field {
	if len(args) == 0 {
		return nil
	}

	// Allocate enough space for the worst case; if users pass only structured
	// fields, we shouldn't penalize them with extra allocations.
	fields := make([]zap.Field, 0, len(args))
	var invalid invalidPairs

	for i := 0; i < len(args); {
		// This is a strongly-typed field. Consume it and move on.
		if f, ok := args[i].(zap.Field); ok {
			fields = append(fields, f)
			i++
			continue
		}

		if len(args) == 1 {
			// passed a bare arg with no key.  We'll handle this
			// as a special case
			if err, ok := args[0].(error); ok {
				return append(fields, zap.Error(err))
			}
			return append(fields, zap.Any("", args[0]))
		}

		// Make sure this element isn't a dangling key.
		if i == len(args)-1 {
			l.Error(_oddNumberErrMsg, zap.Any("ignored", args[i]))
			break
		}

		// Consume this value and the next, treating them as a key-value pair. If the
		// key isn't a string, add this pair to the slice of invalid pairs.
		key, val := args[i], args[i+1]
		if keyStr, ok := key.(string); !ok {
			// Subsequent errors are likely, so allocate once up front.
			if cap(invalid) == 0 {
				invalid = make(invalidPairs, 0, len(args)/2)
			}
			invalid = append(invalid, invalidPair{i, key, val})
		} else {
			fields = append(fields, zap.Any(keyStr, val))
		}
		i += 2
	}

	// If we encountered any invalid key-value pairs, log an error.
	if len(invalid) > 0 {
		l.Error(_nonStringKeyErrMsg, zap.Array("invalid", invalid))
	}
	return fields
}

type invalidPair struct {
	position   int
	key, value interface{}
}

func (p invalidPair) MarshalLogObject(enc zapcore.ObjectEncoder) error {
	enc.AddInt64("position", int64(p.position))
	zap.Any("key", p.key).AddTo(enc)
	zap.Any("value", p.value).AddTo(enc)
	return nil
}

type invalidPairs []invalidPair

func (ps invalidPairs) MarshalLogArray(enc zapcore.ArrayEncoder) error {
	var err error
	for i := range ps {
		err = multierr.Append(err, enc.AppendObject(ps[i]))
	}
	return err
}

// Debug logs at DBG level.  args should be alternative keys and values.  keys should be strings.
func (l *Core) Debug(msg string, args ...interface{}) {
	l.log(DebugLevel, msg, nil, args)
}

// Info logs at INF level. args should be alternative keys and values.  keys should be strings.
func (l *Core) Info(msg string, args ...interface{}) {
	l.log(InfoLevel, msg, nil, args)
}

// Error logs at ERR level.  args should be alternative keys and values.  keys should be strings.
func (l *Core) Error(msg string, args ...interface{}) {
	l.log(ErrorLevel, msg, nil, args)
}

// IsDebug returns true if DBG level is enabled.
func (l *Core) IsDebug() bool {
	return l.IsEnabled(DebugLevel)
}

// IsDebug returns true if INF level is enabled
func (l *Core) IsInfo() bool {
	return l.IsEnabled(InfoLevel)
}

// With returns a new Logger with some context baked in.  All entries
// logged with the new logger will include this context.
//
// args should be alternative keys and values.  keys should be strings.
//
//     reqLogger := l.With("requestID", reqID)
//
func (l *Core) With(args ...interface{}) Logger {
	return l.WithArgs(args...)
}

// WithArgs is the same as With() but returns the concrete type.  Useful
// for other logging packages which wrap this one.
func (l *Core) WithArgs(args ...interface{}) *Core {
	l2 := l.clone()
	switch len(args) {
	case 0:
	default:
		l2.context = append(l2.context, l.sweetenFields(args)...)
	}
	return l2
}

func (l *Core) clone() *Core {
	l2 := *l
	l2.context = nil
	if len(l.context) > 0 {
		l2.context = append(l2.context, l.context...)
	}
	return &l2
}
