// Package logging handles logging throughout Atlantis.
package logging

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"unicode"
)

//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_simple_logging.go SimpleLogging

// SimpleLogging is the interface that our SimpleLogger implements.
// It's really only used for mocking when we need to test what's being logged.
type SimpleLogging interface {
	Debug(format string, a ...interface{})
	Info(format string, a ...interface{})
	Warn(format string, a ...interface{})
	Err(format string, a ...interface{})
	Log(level LogLevel, format string, a ...interface{})
	// Underlying returns the underlying logger.
	Underlying() *log.Logger
	// GetLevel returns the current log level.
	GetLevel() LogLevel
}

// SimpleLogger wraps the standard logger with leveled logging
// and the ability to store log history for later adding it
// to a VCS comment.
type SimpleLogger struct {
	// Source is added as a prefix to each log entry.
	// It's useful if you want to trace a log entry back to a
	// context, for example a pull request id.
	Source string
	// History stores all log entries ever written using
	// this logger. This is safe for short-lived loggers
	// like those used during plan/apply commands.
	History     bytes.Buffer
	Logger      *log.Logger
	KeepHistory bool
	Level       LogLevel
}

type LogLevel int

const (
	Debug LogLevel = iota
	Info
	Warn
	Error
)

// NewSimpleLogger creates a new logger.
// source is added as a prefix to each log entry. It's useful if you want to
// trace a log entry back to a specific context, for example a pull request id.
// logger is the underlying logger. If nil will create a logger from stdlib.
// keepHistory set to true will store all log entries written using this logger.
// level will set the level at which logs >= than that level will be written.
// If keepHistory is set to true, we'll store logs at all levels, regardless of
// what level is set to.
func NewSimpleLogger(source string, logger *log.Logger, keepHistory bool, level LogLevel) *SimpleLogger {
	if logger == nil {
		flags := log.LstdFlags
		if level == Debug {
			// If we're using debug logging, we also have the logger print the
			// filename the log comes from with log.Lshortfile.
			flags = flags | log.Lshortfile
		}
		logger = log.New(os.Stderr, "", flags)
	}
	return &SimpleLogger{
		Source:      source,
		Logger:      logger,
		Level:       level,
		KeepHistory: keepHistory,
	}
}

// NewNoopLogger creates a logger instance that discards all logs and never
// writes them. Used for testing.
func NewNoopLogger() *SimpleLogger {
	logger := log.New(os.Stderr, "", 0)
	logger.SetOutput(ioutil.Discard)
	return &SimpleLogger{
		Source:      "",
		Logger:      logger,
		Level:       Info,
		KeepHistory: false,
	}
}

// ToLogLevel converts a log level string to a valid LogLevel object.
// If the string doesn't match a level, it will return Info.
func ToLogLevel(levelStr string) LogLevel {
	switch levelStr {
	case "debug":
		return Debug
	case "info":
		return Info
	case "warn":
		return Warn
	case "error":
		return Error
	}
	return Info
}

// Debug logs at debug level.
func (l *SimpleLogger) Debug(format string, a ...interface{}) {
	l.Log(Debug, format, a...)
}

// Info logs at info level.
func (l *SimpleLogger) Info(format string, a ...interface{}) {
	l.Log(Info, format, a...)
}

// Warn logs at warn level.
func (l *SimpleLogger) Warn(format string, a ...interface{}) {
	l.Log(Warn, format, a...)
}

// Err logs at error level.
func (l *SimpleLogger) Err(format string, a ...interface{}) {
	l.Log(Error, format, a...)
}

// Log writes the log at level.
func (l *SimpleLogger) Log(level LogLevel, format string, a ...interface{}) {
	levelStr := l.levelToString(level)
	msg := l.capitalizeFirstLetter(fmt.Sprintf(format, a...))

	// Only log this message if configured to log at this level.
	if l.Level <= level {
		// Calling .Output instead of Printf so we can change the calldepth
		// param to 3. The default is 2 which would identify the log as coming
		// from this file and line every time instead of our caller's.
		l.Logger.Output(3, fmt.Sprintf("[%s] %s: %s\n", levelStr, l.Source, msg)) // nolint: errcheck
	}

	// Keep history at all log levels.
	if l.KeepHistory {
		l.saveToHistory(levelStr, msg)
	}
}

// Underlying returns the underlying logger.
func (l *SimpleLogger) Underlying() *log.Logger {
	return l.Logger
}

// GetLevel returns the current log level of the logger.
func (l *SimpleLogger) GetLevel() LogLevel {
	return l.Level
}

func (l *SimpleLogger) saveToHistory(level string, msg string) {
	l.History.WriteString(fmt.Sprintf("[%s] %s\n", level, msg))
}

func (l *SimpleLogger) capitalizeFirstLetter(s string) string {
	runes := []rune(s)
	runes[0] = unicode.ToUpper(runes[0])
	return string(runes)
}

func (l *SimpleLogger) levelToString(level LogLevel) string {
	switch level {
	case Debug:
		return "DEBUG"
	case Info:
		return "INFO"
	case Warn:
		return "WARN"
	case Error:
		return "ERROR"
	}
	return "NOLEVEL"
}
