/*
 * Copyright 2018-2024 Open Networking Foundation (ONF) and the ONF Contributors

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

 * http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Package log provides a structured Logger interface implemented using zap logger. It provides the following capabilities:
// 1. Package level logging - a go package can register itself (AddPackage) and have a logger created for that package.
// 2. Dynamic log level change - for all registered packages (SetAllLogLevel)
// 3. Dynamic log level change - for a given package (SetPackageLogLevel)
// 4. Provides a default logger for unregistered packages (however avoid its usage)
// 5. Allow key-value pairs to be added to a logger(UpdateLogger) or all loggers (UpdateAllLoggers) at run time
// 6. Add to the log output the location where the log was invoked (filename.functionname.linenumber)
//
// Using package-level logging (recommended approach).  In the examples below, log refers to this log package.
//
// 1. In the appropriate package, add the following in the init section of the package (usually in a common.go file)
//    The log level can be changed and any number of default fields can be added as well. The log level specifies
//    the lowest log level that will be in the output while the fields will be automatically added to all log printouts.
//    However, as voltha components re-initialize the log level of each registered package to default initial loglevel
//    passed as CLI argument, the log level passed in RegisterPackage call effectively has no effect.
//
//    var logger log.CLogger
//    func init() {
//              logger, err = log.RegisterPackage(log.JSON, log.ErrorLevel, log.Fields{"key1": "value1"})
//    }
//
// 2. In the calling package, use any of the publicly available functions of local package-level logger instance created
//    in previous step.  Here is an example to write an Info log with additional fields:
//
//    logger.Infow("An example", mylog.Fields{"myStringOutput": "output", "myIntOutput": 2})
//
// 3. To dynamically change the log level, you can use
//          a) SetLogLevel from inside your package or
//          b) SetPackageLogLevel from anywhere or
//          c) SetAllLogLevel from anywhere.
//
//    Dynamic Loglevel configuration feature also uses SetPackageLogLevel method based on triggers received due to
//    Changes to configured loglevels

package log

import (
	"context"
	"errors"
	"fmt"
	"path"
	"runtime"
	"strings"

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

type LogLevel int8

const (
	// DebugLevel logs a message at debug level
	DebugLevel = LogLevel(iota)
	// InfoLevel logs a message at info level
	InfoLevel
	// WarnLevel logs a message at warning level
	WarnLevel
	// ErrorLevel logs a message at error level
	ErrorLevel
	// FatalLevel logs a message, then calls os.Exit(1).
	FatalLevel
)

// CONSOLE formats the log for the console, mostly used during development
const CONSOLE = "console"

// JSON formats the log using json format, mostly used by an automated logging system consumption
const JSON = "json"

// Context Aware Logger represents an abstract logging interface.  Any logging implementation used
// will need to abide by this interface
type CLogger interface {
	Debug(context.Context, ...interface{})
	Debugln(context.Context, ...interface{})
	Debugf(context.Context, string, ...interface{})
	Debugw(context.Context, string, Fields)

	Info(context.Context, ...interface{})
	Infoln(context.Context, ...interface{})
	Infof(context.Context, string, ...interface{})
	Infow(context.Context, string, Fields)

	Warn(context.Context, ...interface{})
	Warnln(context.Context, ...interface{})
	Warnf(context.Context, string, ...interface{})
	Warnw(context.Context, string, Fields)

	Error(context.Context, ...interface{})
	Errorln(context.Context, ...interface{})
	Errorf(context.Context, string, ...interface{})
	Errorw(context.Context, string, Fields)

	Fatal(context.Context, ...interface{})
	Fatalln(context.Context, ...interface{})
	Fatalf(context.Context, string, ...interface{})
	Fatalw(context.Context, string, Fields)

	With(Fields) CLogger

	// The following are added to be able to use this logger as a gRPC LoggerV2 if needed
	//
	Warning(context.Context, ...interface{})
	Warningln(context.Context, ...interface{})
	Warningf(context.Context, string, ...interface{})

	// V reports whether verbosity level l is at least the requested verbose level.
	V(l LogLevel) bool

	//Returns the log level of this specific logger
	GetLogLevel() LogLevel
}

// Fields is used as key-value pairs for structured logging
type Fields map[string]interface{}

var defaultLogger *clogger
var cfg zp.Config

var loggers map[string]*clogger
var cfgs map[string]zp.Config

type clogger struct {
	log         *zp.SugaredLogger
	parent      *zp.Logger
	packageName string
}

func logLevelToAtomicLevel(l LogLevel) zp.AtomicLevel {
	switch l {
	case DebugLevel:
		return zp.NewAtomicLevelAt(zc.DebugLevel)
	case InfoLevel:
		return zp.NewAtomicLevelAt(zc.InfoLevel)
	case WarnLevel:
		return zp.NewAtomicLevelAt(zc.WarnLevel)
	case ErrorLevel:
		return zp.NewAtomicLevelAt(zc.ErrorLevel)
	case FatalLevel:
		return zp.NewAtomicLevelAt(zc.FatalLevel)
	}
	return zp.NewAtomicLevelAt(zc.ErrorLevel)
}

func logLevelToLevel(l LogLevel) zc.Level {
	switch l {
	case DebugLevel:
		return zc.DebugLevel
	case InfoLevel:
		return zc.InfoLevel
	case WarnLevel:
		return zc.WarnLevel
	case ErrorLevel:
		return zc.ErrorLevel
	case FatalLevel:
		return zc.FatalLevel
	}
	return zc.ErrorLevel
}

func levelToLogLevel(l zc.Level) LogLevel {
	switch l {
	case zc.DebugLevel:
		return DebugLevel
	case zc.InfoLevel:
		return InfoLevel
	case zc.WarnLevel:
		return WarnLevel
	case zc.ErrorLevel:
		return ErrorLevel
	case zc.FatalLevel:
		return FatalLevel
	}
	return ErrorLevel
}

func StringToLogLevel(l string) (LogLevel, error) {
	switch strings.ToUpper(l) {
	case "DEBUG":
		return DebugLevel, nil
	case "INFO":
		return InfoLevel, nil
	case "WARN":
		return WarnLevel, nil
	case "ERROR":
		return ErrorLevel, nil
	case "FATAL":
		return FatalLevel, nil
	}
	return 0, errors.New("given LogLevel is invalid : " + l)
}

func LogLevelToString(l LogLevel) (string, error) {
	switch l {
	case DebugLevel:
		return "DEBUG", nil
	case InfoLevel:
		return "INFO", nil
	case WarnLevel:
		return "WARN", nil
	case ErrorLevel:
		return "ERROR", nil
	case FatalLevel:
		return "FATAL", nil
	}
	return "", fmt.Errorf("given LogLevel is invalid %d", l)
}

func getDefaultConfig(outputType string, level LogLevel, defaultFields Fields) zp.Config {
	return zp.Config{
		Level:            logLevelToAtomicLevel(level),
		Encoding:         outputType,
		Development:      true,
		OutputPaths:      []string{"stdout"},
		ErrorOutputPaths: []string{"stderr"},
		InitialFields:    defaultFields,
		EncoderConfig: zc.EncoderConfig{
			LevelKey:       "level",
			MessageKey:     "msg",
			TimeKey:        "ts",
			CallerKey:      "caller",
			StacktraceKey:  "stacktrace",
			LineEnding:     zc.DefaultLineEnding,
			EncodeLevel:    zc.LowercaseLevelEncoder,
			EncodeTime:     zc.ISO8601TimeEncoder,
			EncodeDuration: zc.SecondsDurationEncoder,
			EncodeCaller:   zc.ShortCallerEncoder,
		},
	}
}

func ConstructZapConfig(outputType string, level LogLevel, fields Fields) zp.Config {
	return getDefaultConfig(outputType, level, fields)
}

// SetLogger needs to be invoked before the logger API can be invoked.  This function
// initialize the default logger (zap's sugaredlogger)
func SetDefaultLogger(outputType string, level LogLevel, defaultFields Fields) (CLogger, error) {
	// Build a custom config using zap
	cfg = getDefaultConfig(outputType, level, defaultFields)

	l, err := cfg.Build(zp.AddCallerSkip(1))
	if err != nil {
		return nil, err
	}

	defaultLogger = &clogger{
		log:    l.Sugar(),
		parent: l,
	}

	return defaultLogger, nil
}

// AddPackage registers a package to the log map.  Each package gets its own logger which allows
// its config (loglevel) to be changed dynamically without interacting with the other packages.
// outputType is JSON, level is the lowest level log to output with this logger and defaultFields is a map of
// key-value pairs to always add to the output.
// Note: AddPackage also returns a reference to the actual logger.  If a calling package uses this reference directly
// instead of using the publicly available functions in this log package then a number of functionalities will not
// be available to it, notably log tracing with filename.functionname.linenumber annotation.
//
// pkgNames parameter should be used for testing only as this function detects the caller's package.
func RegisterPackage(outputType string, level LogLevel, defaultFields Fields, pkgNames ...string) (CLogger, error) {
	if cfgs == nil {
		cfgs = make(map[string]zp.Config)
	}
	if loggers == nil {
		loggers = make(map[string]*clogger)
	}

	var pkgName string
	for _, name := range pkgNames {
		pkgName = name
		break
	}
	if pkgName == "" {
		pkgName, _, _, _ = getCallerInfo()
	}

	if _, exist := loggers[pkgName]; exist {
		return loggers[pkgName], nil
	}

	cfgs[pkgName] = getDefaultConfig(outputType, level, defaultFields)

	l, err := cfgs[pkgName].Build(zp.AddCallerSkip(1))
	if err != nil {
		return nil, err
	}

	loggers[pkgName] = &clogger{
		log:         l.Sugar(),
		parent:      l,
		packageName: pkgName,
	}
	return loggers[pkgName], nil
}

// UpdateAllLoggers create new loggers for all registered pacakges with the defaultFields.
func UpdateAllLoggers(defaultFields Fields) error {
	for pkgName, cfg := range cfgs {
		for k, v := range defaultFields {
			if cfg.InitialFields == nil {
				cfg.InitialFields = make(map[string]interface{})
			}
			cfg.InitialFields[k] = v
		}
		l, err := cfg.Build(zp.AddCallerSkip(1))
		if err != nil {
			return err
		}

		// Update the existing zap logger instance
		loggers[pkgName].log = l.Sugar()
		loggers[pkgName].parent = l
	}
	return nil
}

// Return a list of all packages that have individually-configured loggers
func GetPackageNames() []string {
	i := 0
	keys := make([]string, len(loggers))
	for k := range loggers {
		keys[i] = k
		i++
	}
	return keys
}

// UpdateLogger updates the logger associated with a caller's package with supplied defaultFields
func UpdateLogger(defaultFields Fields) error {
	pkgName, _, _, _ := getCallerInfo()
	if _, exist := loggers[pkgName]; !exist {
		return fmt.Errorf("package-%s-not-registered", pkgName)
	}

	// Build a new logger
	if _, exist := cfgs[pkgName]; !exist {
		return fmt.Errorf("config-%s-not-registered", pkgName)
	}

	cfg := cfgs[pkgName]
	for k, v := range defaultFields {
		if cfg.InitialFields == nil {
			cfg.InitialFields = make(map[string]interface{})
		}
		cfg.InitialFields[k] = v
	}
	l, err := cfg.Build(zp.AddCallerSkip(1))
	if err != nil {
		return err
	}

	// Update the existing zap logger instance
	loggers[pkgName].log = l.Sugar()
	loggers[pkgName].parent = l

	return nil
}

func setLevel(cfg zp.Config, level LogLevel) {
	switch level {
	case DebugLevel:
		cfg.Level.SetLevel(zc.DebugLevel)
	case InfoLevel:
		cfg.Level.SetLevel(zc.InfoLevel)
	case WarnLevel:
		cfg.Level.SetLevel(zc.WarnLevel)
	case ErrorLevel:
		cfg.Level.SetLevel(zc.ErrorLevel)
	case FatalLevel:
		cfg.Level.SetLevel(zc.FatalLevel)
	default:
		cfg.Level.SetLevel(zc.ErrorLevel)
	}
}

// SetPackageLogLevel dynamically sets the log level of a given package to level.  This is typically invoked at an
// application level during debugging
func SetPackageLogLevel(packageName string, level LogLevel) {
	// Get proper config
	if cfg, ok := cfgs[packageName]; ok {
		setLevel(cfg, level)
	}
}

// SetAllLogLevel sets the log level of all registered packages to level
func SetAllLogLevel(level LogLevel) {
	// Get proper config
	for _, cfg := range cfgs {
		setLevel(cfg, level)
	}
}

// GetPackageLogLevel returns the current log level of a package.
func GetPackageLogLevel(packageName ...string) (LogLevel, error) {
	var name string
	if len(packageName) == 1 {
		name = packageName[0]
	} else {
		name, _, _, _ = getCallerInfo()
	}
	if cfg, ok := cfgs[name]; ok {
		return levelToLogLevel(cfg.Level.Level()), nil
	}
	return 0, fmt.Errorf("unknown-package-%s", name)
}

// GetDefaultLogLevel gets the log level used for packages that don't have specific loggers
func GetDefaultLogLevel() LogLevel {
	return levelToLogLevel(cfg.Level.Level())
}

// SetLogLevel sets the log level for the logger corresponding to the caller's package
func SetLogLevel(level LogLevel) error {
	pkgName, _, _, _ := getCallerInfo()
	if _, exist := cfgs[pkgName]; !exist {
		return fmt.Errorf("unregistered-package-%s", pkgName)
	}
	cfg := cfgs[pkgName]
	setLevel(cfg, level)
	return nil
}

// SetDefaultLogLevel sets the log level used for packages that don't have specific loggers
func SetDefaultLogLevel(level LogLevel) {
	setLevel(cfg, level)
}

// CleanUp flushed any buffered log entries. Applications should take care to call
// CleanUp before exiting.
func CleanUp() error {
	for _, logger := range loggers {
		if logger != nil {
			if logger.parent != nil {
				if err := logger.parent.Sync(); err != nil {
					return err
				}
			}
		}
	}
	if defaultLogger != nil {
		if defaultLogger.parent != nil {
			if err := defaultLogger.parent.Sync(); err != nil {
				return err
			}
		}
	}
	return nil
}

func getCallerInfo() (string, string, string, int) {
	// Since the caller of a log function is one stack frame before (in terms of stack higher level) the log.go
	// filename, then first look for the last log.go filename and then grab the caller info one level higher.
	maxLevel := 3
	skiplevel := 3 // Level with the most empirical success to see the last log.go stack frame.
	pc := make([]uintptr, maxLevel)
	n := runtime.Callers(skiplevel, pc)
	packageName := ""
	funcName := ""
	fileName := ""
	var line int
	if n == 0 {
		return packageName, fileName, funcName, line
	}
	frames := runtime.CallersFrames(pc[:n])
	var frame runtime.Frame
	var foundFrame runtime.Frame
	more := true
	for more {
		frame, more = frames.Next()
		_, fileName = path.Split(frame.File)
		if fileName != "log.go" {
			foundFrame = frame // First frame after log.go in the frame stack
			break
		}
	}
	parts := strings.Split(foundFrame.Function, ".")
	pl := len(parts)
	if pl >= 2 {
		funcName = parts[pl-1]
		if parts[pl-2][0] == '(' {
			packageName = strings.Join(parts[0:pl-2], ".")
		} else {
			packageName = strings.Join(parts[0:pl-1], ".")
		}
	}

	packageName = strings.TrimSuffix(packageName, ".init")

	fileName = strings.TrimSuffix(fileName, ".go")

	return packageName, fileName, funcName, foundFrame.Line
}

// With returns a logger initialized with the key-value pairs
func (l clogger) With(keysAndValues Fields) CLogger {
	return clogger{log: l.log.With(serializeMap(keysAndValues)...), parent: l.parent}
}

// Debug logs a message at level Debug on the standard logger.
func (l clogger) Debug(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Debug(args...)
}

// Debugln logs a message at level Debug on the standard logger with a line feed. Default in any case.
func (l clogger) Debugln(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Debug(args...)
}

// Debugw logs a message at level Debug on the standard logger.
func (l clogger) Debugf(ctx context.Context, format string, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Debugf(format, args...)
}

// Debugw logs a message with some additional context. The variadic key-value
// pairs are treated as they are in With.
func (l clogger) Debugw(ctx context.Context, msg string, keysAndValues Fields) {
	if l.V(DebugLevel) {
		l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Debugw(msg, serializeMap(keysAndValues)...)
	}
}

// Info logs a message at level Info on the standard logger.
func (l clogger) Info(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Info(args...)
}

// Infoln logs a message at level Info on the standard logger with a line feed. Default in any case.
func (l clogger) Infoln(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Info(args...)
	//msg := fmt.Sprintln(args...)
	//l.sourced().Info(msg[:len(msg)-1])
}

// Infof logs a message at level Info on the standard logger.
func (l clogger) Infof(ctx context.Context, format string, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Infof(format, args...)
}

// Infow logs a message with some additional context. The variadic key-value
// pairs are treated as they are in With.
func (l clogger) Infow(ctx context.Context, msg string, keysAndValues Fields) {
	if l.V(InfoLevel) {
		l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Infow(msg, serializeMap(keysAndValues)...)
	}
}

// Warn logs a message at level Warn on the standard logger.
func (l clogger) Warn(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Warn(args...)
}

// Warnln logs a message at level Warn on the standard logger with a line feed. Default in any case.
func (l clogger) Warnln(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Warn(args...)
}

// Warnf logs a message at level Warn on the standard logger.
func (l clogger) Warnf(ctx context.Context, format string, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Warnf(format, args...)
}

// Warnw logs a message with some additional context. The variadic key-value
// pairs are treated as they are in With.
func (l clogger) Warnw(ctx context.Context, msg string, keysAndValues Fields) {
	if l.V(WarnLevel) {
		l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Warnw(msg, serializeMap(keysAndValues)...)
	}
}

// Error logs a message at level Error on the standard logger.
func (l clogger) Error(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Error(args...)
}

// Errorln logs a message at level Error on the standard logger with a line feed. Default in any case.
func (l clogger) Errorln(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Error(args...)
}

// Errorf logs a message at level Error on the standard logger.
func (l clogger) Errorf(ctx context.Context, format string, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Errorf(format, args...)
}

// Errorw logs a message with some additional context. The variadic key-value
// pairs are treated as they are in With.
func (l clogger) Errorw(ctx context.Context, msg string, keysAndValues Fields) {
	if l.V(ErrorLevel) {
		l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Errorw(msg, serializeMap(keysAndValues)...)
	}
}

// Fatal logs a message at level Fatal on the standard logger.
func (l clogger) Fatal(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Fatal(args...)
}

// Fatalln logs a message at level Fatal on the standard logger with a line feed. Default in any case.
func (l clogger) Fatalln(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Fatal(args...)
}

// Fatalf logs a message at level Fatal on the standard logger.
func (l clogger) Fatalf(ctx context.Context, format string, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Fatalf(format, args...)
}

// Fatalw logs a message with some additional context. The variadic key-value
// pairs are treated as they are in With.
func (l clogger) Fatalw(ctx context.Context, msg string, keysAndValues Fields) {
	if l.V(FatalLevel) {
		l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Fatalw(msg, serializeMap(keysAndValues)...)
	}
}

// Warning logs a message at level Warn on the standard logger.
func (l clogger) Warning(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Warn(args...)
}

// Warningln logs a message at level Warn on the standard logger with a line feed. Default in any case.
func (l clogger) Warningln(ctx context.Context, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Warn(args...)
}

// Warningf logs a message at level Warn on the standard logger.
func (l clogger) Warningf(ctx context.Context, format string, args ...interface{}) {
	l.log.With(GetGlobalLFM().ExtractContextAttributes(ctx)...).Warnf(format, args...)
}

// V reports whether verbosity level l is at least the requested verbose level.
func (l clogger) V(level LogLevel) bool {
	return l.parent.Core().Enabled(logLevelToLevel(level))
}

// GetLogLevel returns the current level of the logger
func (l clogger) GetLogLevel() LogLevel {
	return levelToLogLevel(cfgs[l.packageName].Level.Level())
}

// UpdateCallerSkipLevel create new loggers for specified registered pacakges with the default updated caller skipltFields.
// This will enable to skip wrapper file caller in caller info and stacktrace
func UpdateCallerSkipLevel(skipLevel int) (CLogger, error) {
	pkgName, _, _, _ := getCallerInfo()
	if cfg, exist := cfgs[pkgName]; exist {
		l, err := cfg.Build(zp.AddCallerSkip(skipLevel))
		if err != nil {
			return loggers[pkgName], err
		}

		// Update the existing zap logger instance
		loggers[pkgName].log = l.Sugar()
		loggers[pkgName].parent = l

		return loggers[pkgName], nil
	}

	return loggers[pkgName], errors.New("package not found")
}

// UpdateAllCallerSkipLevel create new loggers for all registered pacakges with the default updated caller skipltFields.
// This will enable to skip wrapper file caller in caller info and stacktrace
func UpdateAllCallerSkipLevel(skipLevel int) error {
	for pkgName, cfg := range cfgs {
		l, err := cfg.Build(zp.AddCallerSkip(skipLevel))
		if err != nil {
			return err
		}

		// Update the existing zap logger instance
		loggers[pkgName].log = l.Sugar()
		loggers[pkgName].parent = l
	}
	return nil
}
