// Copyright 2024 The Prometheus Authors
// 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 promslog defines standardised ways to initialize the Go standard
// library's log/slog logger.
// It should typically only ever be imported by main packages.

package promslog

import (
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

// LogStyle represents the common logging formats in the Prometheus ecosystem.
type LogStyle string

const (
	SlogStyle  LogStyle = "slog"
	GoKitStyle LogStyle = "go-kit"

	reservedKeyPrefix = "logged_"
)

var (
	// LevelFlagOptions represents allowed logging levels.
	LevelFlagOptions = []string{"debug", "info", "warn", "error"}
	// FormatFlagOptions represents allowed formats.
	FormatFlagOptions = []string{"logfmt", "json"}

	defaultWriter = os.Stderr
)

// Level controls a logging level, with an info default.
// It wraps slog.LevelVar with string-based level control.
// Level is safe to be used concurrently.
type Level struct {
	lvl *slog.LevelVar
}

// NewLevel returns a new Level.
func NewLevel() *Level {
	return &Level{
		lvl: &slog.LevelVar{},
	}
}

func (l *Level) UnmarshalYAML(unmarshal func(any) error) error {
	var s string
	type plain string
	if err := unmarshal((*plain)(&s)); err != nil {
		return err
	}
	if s == "" {
		return nil
	}
	if err := l.Set(s); err != nil {
		return err
	}
	return nil
}

// Level returns the value of the logging level as an slog.Level.
func (l *Level) Level() slog.Level {
	return l.lvl.Level()
}

// String returns the current level.
func (l *Level) String() string {
	switch l.lvl.Level() {
	case slog.LevelDebug:
		return "debug"
	case slog.LevelInfo:
		return "info"
	case slog.LevelWarn:
		return "warn"
	case slog.LevelError:
		return "error"
	default:
		return ""
	}
}

// Set updates the logging level with the validation.
func (l *Level) Set(s string) error {
	switch strings.ToLower(s) {
	case "debug":
		l.lvl.Set(slog.LevelDebug)
	case "info":
		l.lvl.Set(slog.LevelInfo)
	case "warn":
		l.lvl.Set(slog.LevelWarn)
	case "error":
		l.lvl.Set(slog.LevelError)
	default:
		return fmt.Errorf("unrecognized log level %s", s)
	}
	return nil
}

// Format controls a logging output format.
// Not concurrency-safe.
type Format struct {
	s string
}

// NewFormat creates a new Format.
func NewFormat() *Format { return &Format{} }

func (f *Format) String() string {
	return f.s
}

// Set updates the value of the allowed format.
func (f *Format) Set(s string) error {
	switch s {
	case "logfmt", "json":
		f.s = s
	default:
		return fmt.Errorf("unrecognized log format %s", s)
	}
	return nil
}

// Config is a struct containing configurable settings for the logger.
type Config struct {
	Level  *Level
	Format *Format
	Style  LogStyle
	Writer io.Writer
}

func newGoKitStyleReplaceAttrFunc(lvl *Level) func(groups []string, a slog.Attr) slog.Attr {
	return func(_ []string, a slog.Attr) slog.Attr {
		key := a.Key
		switch key {
		case slog.TimeKey, "ts":
			if t, ok := a.Value.Any().(time.Time); ok {
				a.Key = "ts"

				// This timestamp format differs from RFC3339Nano by using .000 instead
				// of .999999999 which changes the timestamp from 9 variable to 3 fixed
				// decimals (.130 instead of .130987456).
				a.Value = slog.StringValue(t.UTC().Format("2006-01-02T15:04:05.000Z07:00"))
			} else {
				// If we can't cast the any from the value to a
				// time.Time, it means the caller logged
				// another attribute with a key of `ts`.
				// Prevent duplicate keys (necessary for proper
				// JSON) by renaming the key to `logged_ts`.
				a.Key = reservedKeyPrefix + key
			}
		case slog.SourceKey, "caller":
			if src, ok := a.Value.Any().(*slog.Source); ok {
				a.Key = "caller"
				switch lvl.String() {
				case "debug":
					a.Value = slog.StringValue(filepath.Base(src.File) + "(" + filepath.Base(src.Function) + "):" + strconv.Itoa(src.Line))
				default:
					a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
				}
			} else {
				// If we can't cast the any from the value to
				// an *slog.Source, it means the caller logged
				// another attribute with a key of `caller`.
				// Prevent duplicate keys (necessary for proper
				// JSON) by renaming the key to
				// `logged_caller`.
				a.Key = reservedKeyPrefix + key
			}
		case slog.LevelKey:
			if lvl, ok := a.Value.Any().(slog.Level); ok {
				a.Value = slog.StringValue(strings.ToLower(lvl.String()))
			} else {
				// If we can't cast the any from the value to
				// an slog.Level, it means the caller logged
				// another attribute with a key of `level`.
				// Prevent duplicate keys (necessary for proper
				// JSON) by renaming the key to `logged_level`.
				a.Key = reservedKeyPrefix + key
			}
		default:
		}

		// Ensure time.Duration values are _always_ formatted as a Go
		// duration string (ie, "1d2h3m").
		if v, ok := a.Value.Any().(time.Duration); ok {
			a.Value = slog.StringValue(v.String())
		}

		return a
	}
}

func defaultReplaceAttr(_ []string, a slog.Attr) slog.Attr {
	key := a.Key
	switch key {
	case slog.TimeKey:
		// Note that we do not change the timezone to UTC anymore.
		if _, ok := a.Value.Any().(time.Time); !ok {
			// If we can't cast the any from the value to a
			// time.Time, it means the caller logged
			// another attribute with a key of `time`.
			// Prevent duplicate keys (necessary for proper
			// JSON) by renaming the key to `logged_time`.
			a.Key = reservedKeyPrefix + key
		}
	case slog.SourceKey:
		if src, ok := a.Value.Any().(*slog.Source); ok {
			a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
		} else {
			// If we can't cast the any from the value to
			// an *slog.Source, it means the caller logged
			// another attribute with a key of `source`.
			// Prevent duplicate keys (necessary for proper
			// JSON) by renaming the key to
			// `logged_source`.
			a.Key = reservedKeyPrefix + key
		}
	case slog.LevelKey:
		if _, ok := a.Value.Any().(slog.Level); !ok {
			// If we can't cast the any from the value to
			// an slog.Level, it means the caller logged
			// another attribute with a key of `level`.
			// Prevent duplicate keys (necessary for proper
			// JSON) by renaming the key to
			// `logged_level`.
			a.Key = reservedKeyPrefix + key
		}
	default:
	}

	// Ensure time.Duration values are _always_ formatted as a Go duration
	// string (ie, "1d2h3m").
	if v, ok := a.Value.Any().(time.Duration); ok {
		a.Value = slog.StringValue(v.String())
	}

	return a
}

// New returns a new slog.Logger. Each logged line will be annotated
// with a timestamp. The output always goes to stderr.
func New(config *Config) *slog.Logger {
	if config.Level == nil {
		config.Level = NewLevel()
	}

	if config.Writer == nil {
		config.Writer = defaultWriter
	}

	logHandlerOpts := &slog.HandlerOptions{
		Level:       config.Level.lvl,
		AddSource:   true,
		ReplaceAttr: defaultReplaceAttr,
	}

	if config.Style == GoKitStyle {
		logHandlerOpts.ReplaceAttr = newGoKitStyleReplaceAttrFunc(config.Level)
	}

	if config.Format != nil && config.Format.s == "json" {
		return slog.New(slog.NewJSONHandler(config.Writer, logHandlerOpts))
	}
	return slog.New(slog.NewTextHandler(config.Writer, logHandlerOpts))
}

// NewNopLogger is a convenience function to return an slog.Logger that writes
// to io.Discard.
func NewNopLogger() *slog.Logger {
	return New(&Config{Writer: io.Discard})
}
