package logger

import (
	"encoding/json"
	"fmt"
	stdlog "log"
	"os"
	"runtime/debug"
	"sync"
	"time"

	"github.com/fatih/color"
	"github.com/getsentry/sentry-go"
)

var colorReset, colorGray, colorYellow, colorBlue, colorRed, colorMagenta, colorBlack, colorWhite *color.Color

// Log message structure.
type Log struct {
	Timestamp  string   `json:"timestamp"`
	Level      string   `json:"level"`
	Message    string   `json:"message"`
	Pid        int      `json:"pid"`
	Summary    string   `json:"summary,omitempty"`
	Caller     string   `json:"caller,omitempty"`
	StackTrace []string `json:"stack_trace,omitempty"`
}

// Logger instance
type Logger struct {
	Config
	mux sync.Mutex
}

// global logging configuration.
var logger = NewLogger()

// NewLogger creates a new logger instance
func NewLogger() *Logger {
	color.NoColor = false
	colorReset = color.New(color.Reset)
	colorGray = color.New(color.FgWhite)
	colorYellow = color.New(color.Bold, color.FgYellow)
	colorBlue = color.New(color.Bold, color.FgBlue)
	colorRed = color.New(color.Bold, color.FgRed)
	colorMagenta = color.New(color.Bold, color.FgMagenta)
	colorBlack = color.New(color.Bold, color.FgBlack)
	colorWhite = color.New(color.Bold, color.FgWhite)

	return &Logger{
		Config: NewConfig(),
	}
}

// NewConfig returns the default config
func NewConfig() Config {
	return Config{
		LogThreshold: InfoLevel,
		Formatter:    "json",
	}
}

// Configure logger and will return error if missing required fields.
func Configure(c *Config) error {
	return logger.Configure(c)
}

// GetLogLevel currently configured
func GetLogLevel() Level {
	return logger.GetLogLevel()
}

// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
func Debug(format string, args ...interface{}) {
	logger.Debug(format, args...)
}

// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
func Info(format string, args ...interface{}) {
	logger.Info(format, args...)
}

// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
func Warn(format string, args ...interface{}) {
	logger.Warn(format, args...)
}

// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
// Attempts to log to bugsang.
func Error(errorClass string, err error) {
	logger.Error(errorClass, err)
}

// Configure logger and will return error if missing required fields.
func (l *Logger) Configure(c *Config) error {
	// ensure updates to the config are atomic
	l.mux.Lock()
	defer l.mux.Unlock()

	if c == nil {
		return fmt.Errorf("a non nil Config is mandatory")
	}

	if err := c.LogThreshold.validate(); err != nil {
		return err
	}

	l.LogThreshold = c.LogThreshold
	l.Formatter = c.Formatter
	l.SentryConfig = c.SentryConfig

	if c.SentryConfig.Dsn != "" {
		if sentryErr := sentry.Init(c.SentryConfig); sentryErr != nil {
			fmt.Printf("Sentry initialization failed: %v\n", sentryErr)
		}
	}

	stdlog.SetFlags(0) // this removes timestamp prefixes from logs
	return nil
}

// validate the log level is in the accepted list.
func (l Level) validate() error {
	switch l {
	case DebugLevel, InfoLevel, WarnLevel, ErrorLevel:
		return nil
	default:
		return fmt.Errorf("invalid \"Level\" %d", l)
	}
}

var logLevels = map[Level]string{
	DebugLevel: "DEBUG",
	InfoLevel:  "INFO",
	WarnLevel:  "WARN",
	ErrorLevel: "ERROR",
}

func (l *Logger) logLevel(logLevel Level, format string, args ...interface{}) {
	if logLevel < l.LogThreshold {
		return
	}

	errorClass := ""
	if logLevel == ErrorLevel {
		// First arg is the errorClass
		errorClass = args[0].(string)
		if len(args) > 1 {
			args = args[1:]
		} else {
			args = []interface{}{}
		}
	}

	stringMessage := fmt.Sprintf(format, args...)

	if l.Formatter == "json" {
		// JSON Log Format
		jsonLog, _ := json.Marshal(
			Log{
				Timestamp: time.Now().Format(time.RFC3339Nano),
				Level:     logLevels[logLevel],
				Message:   stringMessage,
				Pid:       os.Getpid(),
			},
		)

		stdlog.Println(string(jsonLog))
	} else {
		// Nice Log Format
		var colorLevel *color.Color
		switch logLevel {
		case DebugLevel:
			colorLevel = colorMagenta
		case InfoLevel:
			colorLevel = colorBlue
		case WarnLevel:
			colorLevel = colorYellow
		case ErrorLevel:
			colorLevel = colorRed
			stringMessage = fmt.Sprintf("%s: %s", errorClass, stringMessage)
		}

		t := time.Now()
		stdlog.Println(
			colorBlack.Sprint("["),
			colorWhite.Sprint(t.Format("2006-01-02 15:04:05")),
			colorBlack.Sprint("] "),
			colorLevel.Sprintf("%-8v", logLevels[logLevel]),
			colorGray.Sprint(stringMessage),
			colorReset.Sprint(""),
		)

		if logLevel == ErrorLevel && l.LogThreshold == DebugLevel {
			// Print a stack trace too
			debug.PrintStack()
		}
	}
}

// GetLogLevel currently configured
func (l *Logger) GetLogLevel() Level {
	return l.LogThreshold
}

// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
func (l *Logger) Debug(format string, args ...interface{}) {
	l.logLevel(DebugLevel, format, args...)
}

// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
func (l *Logger) Info(format string, args ...interface{}) {
	l.logLevel(InfoLevel, format, args...)
}

// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
func (l *Logger) Warn(format string, args ...interface{}) {
	l.logLevel(WarnLevel, format, args...)
}

// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
// Attempts to log to bugsang.
func (l *Logger) Error(errorClass string, err error) {
	l.logLevel(ErrorLevel, err.Error(), errorClass)
	l.notifySentry(errorClass, err)
}

func (l *Logger) notifySentry(errorClass string, err error) {
	if l.SentryConfig.Dsn != "" && l.SentryConfig.Dsn != "-" {

		sentry.ConfigureScope(func(scope *sentry.Scope) {
			scope.SetLevel(sentry.LevelError)
			scope.SetTag("service", "backend")
			scope.SetTag("error_class", errorClass)
		})

		sentry.CaptureException(err)
		// Since sentry emits events in the background we need to make sure
		// they are sent before we shut down
		sentry.Flush(time.Second * 5)
	}
}
