package logger

import (
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"

	"github.com/influxdata/telegraf"
)

// Central handler for the logs used by the logger to actually output the logs.
// This is necessary to be able to dynamically switch the sink even though
// plugins already instantiated a logger _before_ the final sink is set up.
var (
	instance   *handler                // handler for the actual output
	callbacks  map[string]CallbackFunc // logging callback registry
	callbackMu sync.RWMutex
	once       sync.Once // once token to initialize the handler only once
)

// CallbackFunc is a function to be called when printing messages
type CallbackFunc func(
	level telegraf.LogLevel,
	timestamp time.Time,
	source string,
	attributes map[string]interface{},
	arguments ...interface{},
)

// sink interface that has to be implemented by a logging sink
type sink interface {
	Print(telegraf.LogLevel, time.Time, string, map[string]interface{}, ...interface{})
}

// logger is the actual implementation of the telegraf logger interface
type logger struct {
	level    *telegraf.LogLevel
	category string
	name     string
	alias    string

	source     string
	prefix     string
	onError    []func()
	attributes map[string]interface{}
}

// New creates a new logging instance to be used in models
func New(category, name, alias string) *logger {
	l := &logger{
		category:   category,
		name:       name,
		alias:      alias,
		attributes: map[string]interface{}{"category": category, "plugin": name},
	}
	if alias != "" {
		l.attributes["alias"] = alias
	}

	// Format the prefix
	l.source = l.category

	if l.source != "" && l.name != "" {
		l.source += "."
	}
	l.source += l.name

	if l.source != "" && l.alias != "" {
		l.source += "::"
	}
	l.source += l.alias
	if l.source != "" {
		l.prefix = "[" + l.source + "] "
	}

	return l
}

// Level returns the current log-level of the logger
func (l *logger) Level() telegraf.LogLevel {
	if l.level != nil {
		return *l.level
	}
	return instance.level
}

// AddAttribute allows to add a key-value attribute to the logging output
func (l *logger) AddAttribute(key string, value interface{}) {
	// Do not allow to overwrite general keys
	switch key {
	case "category", "plugin", "alias":
	default:
		l.attributes[key] = value
	}
}

// Error logging including callbacks
func (l *logger) Errorf(format string, args ...interface{}) {
	l.Error(fmt.Sprintf(format, args...))
}

func (l *logger) Error(args ...interface{}) {
	l.Print(telegraf.Error, time.Now(), args...)
	for _, f := range l.onError {
		f()
	}
}

// Warning logging
func (l *logger) Warnf(format string, args ...interface{}) {
	l.Warn(fmt.Sprintf(format, args...))
}

func (l *logger) Warn(args ...interface{}) {
	l.Print(telegraf.Warn, time.Now(), args...)
}

// Info logging
func (l *logger) Infof(format string, args ...interface{}) {
	l.Info(fmt.Sprintf(format, args...))
}

func (l *logger) Info(args ...interface{}) {
	l.Print(telegraf.Info, time.Now(), args...)
}

// Debug logging, this is suppressed on console
func (l *logger) Debugf(format string, args ...interface{}) {
	l.Debug(fmt.Sprintf(format, args...))
}

func (l *logger) Debug(args ...interface{}) {
	l.Print(telegraf.Debug, time.Now(), args...)
}

// Trace logging, this is suppressed on console
func (l *logger) Tracef(format string, args ...interface{}) {
	l.Trace(fmt.Sprintf(format, args...))
}

func (l *logger) Trace(args ...interface{}) {
	l.Print(telegraf.Trace, time.Now(), args...)
}

func (l *logger) Print(level telegraf.LogLevel, ts time.Time, args ...interface{}) {
	// Check if we are in early logging state and store the message in this case
	if instance.impl == nil {
		instance.add(level, ts, l.prefix, l.attributes, args...)
	}

	// Serve all registered callbacks before checking the log-level. This is
	// intentional to allow the callback to apply its own log-level filtering.
	callbackMu.RLock()
	for _, cb := range callbacks {
		cb(level, ts.UTC(), l.source, l.attributes, args...)
	}
	callbackMu.RUnlock()

	// Skip all messages with insufficient log-levels
	if l.level != nil && !l.level.Includes(level) || l.level == nil && !instance.level.Includes(level) {
		return
	}
	if instance.impl != nil {
		instance.impl.Print(level, ts.In(instance.timezone), l.prefix, l.attributes, args...)
	} else {
		msg := append([]interface{}{ts.In(instance.timezone).Format(time.RFC3339), " ", level.Indicator(), " ", l.prefix}, args...)
		instance.earlysink.Print(msg...)
	}
}

// SetLevel overrides the current log-level of the logger
func (l *logger) SetLevel(level telegraf.LogLevel) {
	l.level = &level
}

// SetLevel changes the log-level to the given one
func (l *logger) SetLogLevel(name string) error {
	if name == "" {
		return nil
	}
	level := telegraf.LogLevelFromString(name)
	if level == telegraf.None {
		return fmt.Errorf("invalid log-level %q", name)
	}
	l.SetLevel(level)
	return nil
}

// Register a callback triggered when errors are about to be written to the log
func (l *logger) RegisterErrorCallback(f func()) {
	l.onError = append(l.onError, f)
}

type Config struct {
	// will set the log level to DEBUG
	Debug bool
	// will set the log level to ERROR
	Quiet bool
	// format and target of log messages
	LogTarget string
	LogFormat string
	Logfile   string
	// will rotate when current file at the specified time interval
	RotationInterval time.Duration
	// will rotate when current file size exceeds this parameter.
	RotationMaxSize int64
	// maximum rotated files to keep (older ones will be deleted)
	RotationMaxArchives int
	// pick a timezone to use when logging. or type 'local' for local time.
	LogWithTimezone string
	// Logger instance name
	InstanceName string
	// Structured logging message key
	StructuredLogMessageKey string

	// internal  log-level
	logLevel telegraf.LogLevel
}

// SetupLogging configures the logging output.
func SetupLogging(cfg *Config) error {
	// Issue deprecation warning for option
	switch cfg.LogTarget {
	case "":
		// Best-case no target set or file already migrated...
	case "stderr":
		msg := "Agent setting %q is deprecated, please leave %q empty and remove this setting!"
		deprecation := "The setting will be removed in v1.40.0."
		log.Printf("W! "+msg+" "+deprecation, "logtarget", "logfile")
		cfg.Logfile = ""
	case "file":
		msg := "Agent setting %q is deprecated, please just set %q and remove this setting!"
		deprecation := "The setting will be removed in v1.40.0."
		log.Printf("W! "+msg+" "+deprecation, "logtarget", "logfile")
	case "eventlog":
		msg := "Agent setting %q is deprecated, please set %q to %q and remove this setting!"
		deprecation := "The setting will be removed in v1.40.0."
		log.Printf("W! "+msg+" "+deprecation, "logtarget", "logformat", "eventlog")
		if cfg.LogFormat != "" && cfg.LogFormat != "eventlog" {
			return errors.New("contradicting setting between 'logtarget' and 'logformat'")
		}
		cfg.LogFormat = "eventlog"
	default:
		return fmt.Errorf("invalid deprecated 'logtarget' setting %q", cfg.LogTarget)
	}

	if cfg.LogFormat == "" {
		cfg.LogFormat = "text"
	}

	if cfg.Debug {
		cfg.logLevel = telegraf.Debug
	}
	if cfg.Quiet {
		cfg.logLevel = telegraf.Error
	}
	if !cfg.Debug && !cfg.Quiet {
		cfg.logLevel = telegraf.Info
	}

	if cfg.InstanceName == "" {
		cfg.InstanceName = "telegraf"
	}

	if cfg.LogFormat == "" {
		cfg.LogFormat = "text"
	}

	// Get configured timezone
	timezoneName := cfg.LogWithTimezone
	if strings.EqualFold(timezoneName, "local") {
		timezoneName = "Local"
	}
	tz, err := time.LoadLocation(timezoneName)
	if err != nil {
		return fmt.Errorf("setting logging timezone failed: %w", err)
	}

	// Get the logging factory and create the root instance
	creator, found := registry[cfg.LogFormat]
	if !found {
		return fmt.Errorf("unsupported log-format: %s", cfg.LogFormat)
	}

	l, err := creator(cfg)
	if err != nil {
		return err
	}

	// Close the previous logger if possible
	if err := CloseLogging(); err != nil {
		return err
	}

	// Update the logging instance
	skipEarlyLogs := cfg.LogFormat == "text" && cfg.Logfile == ""
	instance.switchSink(l, cfg.logLevel, tz, skipEarlyLogs)

	return nil
}

func RedirectLogging(w io.Writer) {
	instance = redirectHandler(w)
}

func CloseLogging() error {
	if instance == nil {
		return nil
	}

	if err := instance.close(); err != nil && !errors.Is(err, os.ErrClosed) {
		return err
	}

	return nil
}

// AddCallback adds the given callback function to the registry and returns an
// ID that can be used for removing the callback later. Callback functions must
// not block or take a lot of time!
func AddCallback(callback CallbackFunc) (string, error) {
	// Create a cookie to be returned to the caller in order to be able to
	// remove the callback later
	rawid, err := uuid.NewRandom()
	if err != nil {
		return "", err
	}
	id := rawid.String()

	callbackMu.Lock()
	callbacks[id] = callback
	callbackMu.Unlock()

	return id, nil
}

// RemoveCallback removes the callback function with the given ID from the registry
func RemoveCallback(id string) {
	callbackMu.Lock()
	defer callbackMu.Unlock()
	delete(callbacks, id)
}

func init() {
	once.Do(func() {
		// Create a special logging instance that additionally buffers all
		// messages logged before the final logger is up.
		instance = defaultHandler()

		// Setup callback registry
		callbacks = make(map[string]CallbackFunc)

		// Redirect the standard logger output to our logger instance
		log.SetFlags(0)
		log.SetOutput(&stdlogRedirector{})
	})
}
