package logging

import (
	"context"
	"encoding/json"
	"os"
	"sort"
	"strconv"
	"strings"

	color "git.handmade.network/hmn/hmn/src/ansicolor"
	"git.handmade.network/hmn/hmn/src/config"
	"git.handmade.network/hmn/hmn/src/oops"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func init() {
	zerolog.ErrorStackMarshaler = oops.ZerologStackMarshaler
	log.Logger = log.Output(NewPrettyZerologWriter()).With().Stack().Logger()
	zerolog.SetGlobalLevel(config.Config.LogLevel)
}

func GlobalLogger() *zerolog.Logger {
	return &log.Logger
}

func Trace() *zerolog.Event {
	return log.Trace().Timestamp().Stack()
}

func Debug() *zerolog.Event {
	return log.Debug().Timestamp().Stack()
}

func Info() *zerolog.Event {
	return log.Info().Timestamp().Stack()
}

func Warn() *zerolog.Event {
	return log.Warn().Timestamp().Stack()
}

func Error() *zerolog.Event {
	return log.Error().Timestamp().Stack()
}

func Panic() *zerolog.Event {
	return log.Panic().Timestamp().Stack()
}

func Fatal() *zerolog.Event {
	return log.Fatal().Timestamp().Stack()
}

func With() zerolog.Context {
	return log.With().Stack()
}

type PrettyZerologWriter struct {
	wd                  string
	wasLastLogMultiline bool
}

type PrettyLogEntry struct {
	Timestamp  string
	Level      string
	Message    string
	Error      string
	StackTrace []interface{}

	OtherFields []PrettyField
}

type PrettyField struct {
	Name  string
	Value interface{}
}

var ColorFromLevel = map[string]string{
	"trace": color.Gray,
	"debug": color.Gray,
	"info":  color.BgBlue,
	"warn":  color.BgYellow,
	"error": color.BgRed,
	"fatal": color.BgRed,
	"panic": color.BgRed,
}

func NewPrettyZerologWriter() *PrettyZerologWriter {
	wd, _ := os.Getwd()
	return &PrettyZerologWriter{
		wd:                  wd,
		wasLastLogMultiline: false,
	}
}

func (w *PrettyZerologWriter) Write(p []byte) (int, error) {
	var fields map[string]interface{}
	err := json.Unmarshal(p, &fields)
	if err != nil {
		return os.Stderr.Write(p)
	}

	var pretty PrettyLogEntry
	for name, val := range fields {
		switch name {
		case zerolog.TimestampFieldName:
			pretty.Timestamp = val.(string)
		case zerolog.LevelFieldName:
			pretty.Level = val.(string)
		case zerolog.MessageFieldName:
			pretty.Message = val.(string)
		case zerolog.ErrorFieldName:
			pretty.Error = val.(string)
		case zerolog.ErrorStackFieldName:
			pretty.StackTrace = val.([]interface{})
		default:
			pretty.OtherFields = append(pretty.OtherFields, PrettyField{
				Name:  name,
				Value: val,
			})
		}
	}

	sort.Slice(pretty.OtherFields, func(i, j int) bool {
		return strings.Compare(pretty.OtherFields[i].Name, pretty.OtherFields[j].Name) < 0
	})

	isMultiline := (pretty.Error != "" || pretty.StackTrace != nil || pretty.OtherFields != nil)

	var b strings.Builder
	if isMultiline || w.wasLastLogMultiline {
		b.WriteString("---------------------------------------\n")
	}
	b.WriteString(pretty.Timestamp)
	b.WriteString(" ")
	if pretty.Level != "" {
		b.WriteString(ColorFromLevel[pretty.Level])
		b.WriteString(color.Bold)
		b.WriteString(strings.ToUpper(pretty.Level))
		b.WriteString(color.Reset)
		b.WriteString(": ")
	}
	b.WriteString(pretty.Message)
	b.WriteString("\n")
	if pretty.Error != "" {
		b.WriteString("  " + color.Bold + color.Red + "ERROR:" + color.Reset + " ")
		b.WriteString(pretty.Error)
		b.WriteString("\n")
	}
	if len(pretty.OtherFields) > 0 {
		b.WriteString("  " + color.Bold + color.Blue + "Fields:" + color.Reset + "\n")
		for _, field := range pretty.OtherFields {
			valuePretty, _ := json.MarshalIndent(field.Value, "    ", "  ")
			b.WriteString("    ")
			b.WriteString(field.Name)
			b.WriteString(": ")
			b.WriteString(string(valuePretty))
			b.WriteString("\n")
		}
	}
	if pretty.StackTrace != nil {
		b.WriteString("  " + color.Bold + color.Blue + "Stack trace:" + color.Reset + "\n")
		for _, frame := range pretty.StackTrace {
			frameMap := frame.(map[string]interface{})
			file := frameMap["file"].(string)
			file = strings.Replace(file, w.wd, ".", 1)

			b.WriteString("    ")
			b.WriteString(frameMap["function"].(string))
			b.WriteString(" (")
			b.WriteString(file)
			b.WriteString(":")
			b.WriteString(strconv.Itoa(int(frameMap["line"].(float64))))
			b.WriteString(")\n")
		}
	}

	w.wasLastLogMultiline = isMultiline

	return os.Stderr.Write([]byte(b.String()))
}

func LogPanics(logger *zerolog.Logger) {
	if r := recover(); r != nil {
		LogPanicValue(logger, r, "recovered from panic")
	}
}

func LogPanicValue(logger *zerolog.Logger, val interface{}, msg string) {
	if logger == nil {
		logger = GlobalLogger()
	}

	if err, ok := val.(error); ok {
		logger.Error().Err(err).Msg(msg)
	} else {
		logger.Error().
			Interface("recovered", val).
			Interface(zerolog.ErrorStackFieldName, oops.Trace()).
			Msg(msg)
	}
}

const LoggerContextKey = "logger"

func AttachLoggerToContext(logger *zerolog.Logger, ctx context.Context) context.Context {
	return context.WithValue(ctx, LoggerContextKey, logger)
}

func ExtractLogger(ctx context.Context) *zerolog.Logger {
	ilogger := ctx.Value(LoggerContextKey)
	if ilogger == nil {
		return GlobalLogger()
	}
	return ilogger.(*zerolog.Logger)
}
