// Copyright 2017 HootSuite Media Inc.
//
// 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.
// Modified hereafter by contributors to runatlantis/atlantis.
//
// Package logging handles logging throughout Atlantis.
package logging

import (
	"bytes"
	"fmt"

	"github.com/pkg/errors"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"go.uber.org/zap/zaptest"
)

//go:generate pegomock generate --package mocks -o mocks/mock_simple_logging.go SimpleLogging

// SimpleLogging is the interface used for logging throughout the codebase.
type SimpleLogging interface {

	// These basically just fmt.Sprintf() the message and args.
	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{})
	SetLevel(lvl LogLevel)

	// With adds a variadic number of fields to the logging context. It accepts a
	// mix of strongly-typed Field objects and loosely-typed key-value pairs. When
	// processing pairs, the first element of the pair is used as the field key
	// and the second as the field value.
	With(a ...interface{}) SimpleLogging

	// Creates a new logger with history preserved . log storage + search strategies
	// should ideally be used instead of managing this ourselves.
	// keeping as a separate method to ensure that usage of history is completely intentional
	WithHistory(a ...interface{}) SimpleLogging

	// Fetches the history we've stored associated with the logging context
	GetHistory() string

	// Flushes anything left in the buffer
	Flush() error
}

type StructuredLogger struct {
	z           *zap.SugaredLogger
	level       zap.AtomicLevel
	keepHistory bool
	// History stores all log entries ever written using
	// this logger. This is safe for short-lived loggers
	// like those used during plan/apply commands.
	// TODO: Deprecate this
	// this is added here to maintain backwards compatibility
	// This doesn't really make sense to keep given that structured logging
	// gives us the ability to query our logs across multiple dimensions
	// I don't believe we should mix this in with atlantis commands and expose this to the user
	history bytes.Buffer
}

func NewStructuredLoggerFromLevel(lvl LogLevel) (SimpleLogging, error) {
	cfg := zap.NewProductionConfig()

	cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	cfg.Level = zap.NewAtomicLevelAt(lvl.zLevel)
	return newStructuredLogger(cfg)
}

func NewStructuredLogger() (SimpleLogging, error) {
	cfg := zap.NewProductionConfig()
	cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	return newStructuredLogger(cfg)
}

func newStructuredLogger(cfg zap.Config) (*StructuredLogger, error) {
	baseLogger, err := cfg.Build()

	baseLogger = baseLogger.
		// ensures that the caller doesn't just say logging/simple_logger each time
		WithOptions(zap.AddCallerSkip(1)).
		WithOptions(zap.AddStacktrace(zapcore.WarnLevel)).
		// creates isolated context for all future kv pairs, name can be flexible as needed
		With(zap.Namespace("json"))

	if err != nil {
		return nil, errors.Wrap(err, " initializing structured logger")
	}

	return &StructuredLogger{
		z:     baseLogger.Sugar(),
		level: cfg.Level,
	}, nil
}

func (l *StructuredLogger) With(a ...interface{}) SimpleLogging {
	return &StructuredLogger{
		z:     l.z.With(a...),
		level: l.level,
	}
}

func (l *StructuredLogger) WithHistory(a ...interface{}) SimpleLogging {
	logger := &StructuredLogger{
		z:     l.z.With(a...),
		level: l.level,
	}

	// ensure that the history is kept across loggers.
	logger.keepHistory = true
	logger.history = l.history

	return logger
}

func (l *StructuredLogger) GetHistory() string {
	return l.history.String()
}

func (l *StructuredLogger) Debug(format string, a ...interface{}) {
	l.z.Debugf(format, a...)
	l.saveToHistory(Debug, format, a...)
}

func (l *StructuredLogger) Info(format string, a ...interface{}) {
	l.z.Infof(format, a...)
	l.saveToHistory(Info, format, a...)
}

func (l *StructuredLogger) Warn(format string, a ...interface{}) {
	l.z.Warnf(format, a...)
	l.saveToHistory(Warn, format, a...)
}

func (l *StructuredLogger) Err(format string, a ...interface{}) {
	l.z.Errorf(format, a...)
	l.saveToHistory(Error, format, a...)
}

func (l *StructuredLogger) Log(level LogLevel, format string, a ...interface{}) {
	switch level {
	case Debug:
		l.z.Debugf(format, a...)
	case Info:
		l.z.Infof(format, a...)
	case Warn:
		l.z.Warnf(format, a...)
	case Error:
		l.z.Errorf(format, a...)
	}
}

func (l *StructuredLogger) SetLevel(lvl LogLevel) {
	if l != nil {
		l.level.SetLevel(lvl.zLevel)
	}
}

func (l *StructuredLogger) Flush() error {
	return l.z.Sync()
}

func (l *StructuredLogger) saveToHistory(lvl LogLevel, format string, a ...interface{}) {
	if !l.keepHistory {
		return
	}
	msg := fmt.Sprintf(format, a...)
	l.history.WriteString(fmt.Sprintf("[%s] %s\n", lvl.shortStr, msg))
}

// NewNoopLogger creates a logger instance that discards all logs and never
// writes them. Used for testing.
func NewNoopLogger(t zaptest.TestingT) SimpleLogging {
	level := zap.DebugLevel
	return &StructuredLogger{
		z:     zaptest.NewLogger(t, zaptest.Level(level)).Sugar(),
		level: zap.NewAtomicLevelAt(level),
	}
}

type LogLevel struct {
	zLevel   zapcore.Level
	shortStr string
}

var (
	Debug = LogLevel{
		zLevel:   zapcore.DebugLevel,
		shortStr: "DBUG",
	}
	Info = LogLevel{
		zLevel:   zapcore.InfoLevel,
		shortStr: "INFO",
	}
	Warn = LogLevel{
		zLevel:   zapcore.WarnLevel,
		shortStr: "WARN",
	}
	Error = LogLevel{
		zLevel:   zapcore.ErrorLevel,
		shortStr: "EROR",
	}
)
