package logging

import (
	"fmt"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"os"
	"sync"
	"time"
)

const (
	CONSOLE = "console"
	JOURNAL = "systemd-journald"
)

// defaultEncConfig defines the default zapcore.EncoderConfig for the logging package.
var defaultEncConfig = zapcore.EncoderConfig{
	TimeKey:        "ts",
	LevelKey:       "level",
	NameKey:        "logger",
	CallerKey:      "caller",
	MessageKey:     "msg",
	StacktraceKey:  "stacktrace",
	LineEnding:     zapcore.DefaultLineEnding,
	EncodeLevel:    zapcore.CapitalLevelEncoder,
	EncodeTime:     zapcore.ISO8601TimeEncoder,
	EncodeDuration: zapcore.StringDurationEncoder,
	EncodeCaller:   zapcore.ShortCallerEncoder,
}

// Options define child loggers with their desired log level.
type Options map[string]zapcore.Level

// Logging implements access to a default logger and named child loggers.
// Log levels can be configured per named child via Options which, if not configured,
// fall back on a default log level.
// Logs either to the console or to systemd-journald.
type Logging struct {
	logger    *Logger
	output    string
	verbosity zap.AtomicLevel
	interval  time.Duration

	// coreFactory creates zapcore.Core based on the log level and the log output.
	coreFactory func(zap.AtomicLevel) zapcore.Core

	mu      sync.Mutex
	loggers map[string]*Logger

	options Options
}

// NewLogging takes the name and log level for the default logger,
// output where log messages are written to,
// options having log levels for named child loggers
// and returns a new Logging.
func NewLogging(name string, level zapcore.Level, output string, options Options, interval time.Duration) (*Logging, error) {
	verbosity := zap.NewAtomicLevelAt(level)

	var coreFactory func(zap.AtomicLevel) zapcore.Core
	switch output {
	case CONSOLE:
		enc := zapcore.NewConsoleEncoder(defaultEncConfig)
		ws := zapcore.Lock(os.Stderr)
		coreFactory = func(verbosity zap.AtomicLevel) zapcore.Core {
			return zapcore.NewCore(enc, ws, verbosity)
		}
	case JOURNAL:
		coreFactory = func(verbosity zap.AtomicLevel) zapcore.Core {
			return NewJournaldCore(name, verbosity)
		}
	default:
		return nil, invalidOutput(output)
	}

	logger := NewLogger(zap.New(coreFactory(verbosity)).Named(name).Sugar(), interval)

	return &Logging{
			logger:      logger,
			output:      output,
			verbosity:   verbosity,
			interval:    interval,
			coreFactory: coreFactory,
			loggers:     make(map[string]*Logger),
			options:     options,
		},
		nil
}

// GetChildLogger returns a named child logger.
// Log levels for named child loggers are obtained from the logging options and, if not found,
// set to the default log level.
func (l *Logging) GetChildLogger(name string) *Logger {
	l.mu.Lock()
	defer l.mu.Unlock()

	if logger, ok := l.loggers[name]; ok {
		return logger
	}

	var verbosity zap.AtomicLevel
	if level, found := l.options[name]; found {
		verbosity = zap.NewAtomicLevelAt(level)
	} else {
		verbosity = l.verbosity
	}

	logger := NewLogger(zap.New(l.coreFactory(verbosity)).Named(name).Sugar(), l.interval)
	l.loggers[name] = logger

	return logger
}

// GetLogger returns the default logger.
func (l *Logging) GetLogger() *Logger {
	return l.logger
}

// AssertOutput returns an error if output is not a valid logger output.
func AssertOutput(o string) error {
	if o == CONSOLE || o == JOURNAL {
		return nil
	}

	return invalidOutput(o)
}

func invalidOutput(o string) error {
	return fmt.Errorf("%s is not a valid logger output. Must be either %q or %q", o, CONSOLE, JOURNAL)
}
