package log

import (
	"fmt"
	"io"
	"os"
	"time"

	"github.com/sirupsen/logrus"
)

const (
	iso8601TimestampFormat = "2006-01-02T15:04:05.000Z07:00"
	iso8601TimestampEnvKey = "GITLAB_ISO8601_LOG_TIMESTAMP"
)

type loggerConfig struct {
	logger     *logrus.Logger
	level      logrus.Level
	formatter  logrus.Formatter
	location   *time.Location
	outputPath string
	writer     io.Writer

	// A list of warnings that will be emitted once the logger is configured
	warnings []string
}

type timezoneFormatter struct {
	formatter logrus.Formatter
	location  *time.Location
}

// LoggerOption will configure a new logrus Logger.
type LoggerOption func(*loggerConfig)

// We default to time.RFC3339 (to match the Logrus default) for the timestamp format for backward compatibility reasons.
// Optionally, users can opt in for ISO8601 with millisecond precision by setting (to any value) the
// iso8601TimestampEnvKey environment variable.
func timestampFormat() string {
	if _, exists := os.LookupEnv(iso8601TimestampEnvKey); exists {
		return iso8601TimestampFormat
	}
	return time.RFC3339
}

func applyLoggerOptions(opts []LoggerOption) *loggerConfig {
	conf := loggerConfig{
		logger:    logger,
		level:     logrus.InfoLevel,
		formatter: &logrus.TextFormatter{TimestampFormat: timestampFormat()},
		writer:    os.Stdout,
	}

	for _, v := range opts {
		v(&conf)
	}

	return &conf
}

func (l *loggerConfig) buildFormatter() logrus.Formatter {
	out := l.formatter

	if l.location != nil {
		out = &timezoneFormatter{formatter: out, location: l.location}
	}

	return out
}

func (f *timezoneFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	// Avoid mutating the passed-in entry as callers may retain a reference to
	// it. Since we don't change the `Data` field, a shallow copy is sufficient.
	if entry != nil {
		entryCopy := *entry
		entryCopy.Time = entryCopy.Time.In(f.location)

		entry = &entryCopy
	}

	return f.formatter.Format(entry)
}

// WithFormatter allows setting the format to `text`, `json`, `color` or `combined`. In case
// the input is not recognized it defaults to text with a warning.
// More details of these formats:
// * `text` - human readable.
// * `json` - computer readable, new-line delimited JSON.
// * `color` - human readable, in color. Useful for development.
// * `combined` - httpd access logs. Good for legacy access log parsers.
func WithFormatter(format string) LoggerOption {
	return func(conf *loggerConfig) {
		timestampFormat := timestampFormat()
		switch format {
		case "text":
			conf.formatter = &logrus.TextFormatter{TimestampFormat: timestampFormat}
		case "color":
			conf.formatter = &logrus.TextFormatter{TimestampFormat: timestampFormat, ForceColors: true, EnvironmentOverrideColors: true}
		case "json":
			conf.formatter = &logrus.JSONFormatter{TimestampFormat: timestampFormat}
		case "combined":
			conf.formatter = newCombinedcombinedAccessLogFormatter()
		default:
			conf.warnings = append(conf.warnings, fmt.Sprintf("unknown logging format %s, ignoring option", format))
		}
	}
}

// WithTimezone allows setting the timezone that will be used for log messages.
// The default behaviour is to use the local timezone, but a specific timezone
// (such as time.UTC) may be preferred.
func WithTimezone(location *time.Location) LoggerOption {
	return func(conf *loggerConfig) {
		conf.location = location
	}
}

// WithLogLevel is used to set the log level when defaulting to `info` is not
// wanted. Other options are: `debug`, `warn`, `error`, `fatal`, and `panic`.
func WithLogLevel(level string) LoggerOption {
	return func(conf *loggerConfig) {
		logrusLevel, err := logrus.ParseLevel(level)
		if err != nil {
			conf.warnings = append(conf.warnings, fmt.Sprintf("unknown log level, ignoring option: %v", err))
		} else {
			conf.level = logrusLevel
		}
	}
}

// WithOutputName allows customization of the sink of the logger. Output is either:
// `stdout`, `stderr`, or a path to a file.
func WithOutputName(outputName string) LoggerOption {
	return func(conf *loggerConfig) {
		switch outputName {
		case "stdout":
			conf.writer = os.Stdout
		case "stderr":
			conf.writer = os.Stderr
		default:
			conf.writer = nil
			conf.outputPath = outputName
		}
	}
}

// WithWriter allows the writer to be customized. The application is responsible for closing the writer manually.
func WithWriter(writer io.Writer) LoggerOption {
	return func(conf *loggerConfig) {
		conf.writer = writer
	}
}

// WithLogger allows you to configure a proprietary logger using the `Initialize` method.
func WithLogger(logger *logrus.Logger) LoggerOption {
	return func(conf *loggerConfig) {
		conf.logger = logger
	}
}
