// Copyright 2022 The TVL Contributors
// SPDX-License-Identifier: Apache-2.0
package logs

// This file configures different log formatters via logrus. The
// standard formatter uses a structured JSON format that is compatible
// with Stackdriver Error Reporting.
//
// https://cloud.google.com/error-reporting/docs/formatting-error-messages

import (
	"bytes"
	"encoding/json"
	log "github.com/sirupsen/logrus"
)

type stackdriverFormatter struct{}

type serviceContext struct {
	Service string `json:"service"`
	Version string `json:"version"`
}

type reportLocation struct {
	FilePath     string `json:"filePath"`
	LineNumber   int    `json:"lineNumber"`
	FunctionName string `json:"functionName"`
}

var nixeryContext = serviceContext{
	Service: "nixery",
}

// isError determines whether an entry should be logged as an error
// (i.e. with attached `context`).
//
// This requires the caller information to be present on the log
// entry, as stacktraces are not available currently.
func isError(e *log.Entry) bool {
	l := e.Level
	return (l == log.ErrorLevel || l == log.FatalLevel || l == log.PanicLevel) &&
		e.HasCaller()
}

// logSeverity formats the entry's severity into a format compatible
// with Stackdriver Logging.
//
// The two formats that are being mapped do not have an equivalent set
// of severities/levels, so the mapping is somewhat arbitrary for a
// handful of them.
//
// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
func logSeverity(l log.Level) string {
	switch l {
	case log.TraceLevel:
		return "DEBUG"
	case log.DebugLevel:
		return "DEBUG"
	case log.InfoLevel:
		return "INFO"
	case log.WarnLevel:
		return "WARNING"
	case log.ErrorLevel:
		return "ERROR"
	case log.FatalLevel:
		return "CRITICAL"
	case log.PanicLevel:
		return "EMERGENCY"
	default:
		return "DEFAULT"
	}
}

func (f stackdriverFormatter) Format(e *log.Entry) ([]byte, error) {
	msg := e.Data
	msg["serviceContext"] = &nixeryContext
	msg["message"] = &e.Message
	msg["eventTime"] = &e.Time
	msg["severity"] = logSeverity(e.Level)

	if e, ok := msg[log.ErrorKey]; ok {
		if err, isError := e.(error); isError {
			msg[log.ErrorKey] = err.Error()
		} else {
			delete(msg, log.ErrorKey)
		}
	}

	if isError(e) {
		loc := reportLocation{
			FilePath:     e.Caller.File,
			LineNumber:   e.Caller.Line,
			FunctionName: e.Caller.Function,
		}
		msg["context"] = &loc
	}

	b := new(bytes.Buffer)
	err := json.NewEncoder(b).Encode(&msg)

	return b.Bytes(), err
}

func Init(version string) {
	nixeryContext.Version = version
	log.SetReportCaller(true)
	log.SetFormatter(stackdriverFormatter{})
}
