package log

import (
	"net/http"
	"os"
	"runtime"
	"time"

	"code.justin.tv/creator-collab/log/errors"
	"github.com/rollbar/rollbar-go"
)

type Fields map[string]interface{}

const (
	LevelError = "error"
	LevelInfo  = "info"
	LevelDebug = "debug"
)

type LogEvent struct {
	Message string
	Level   string
	Time    time.Time
	Error   error
	Fields  Fields
	Request *http.Request
}

type Logger interface {
	Debug(msg string, fields ...Fields)
	Info(msg string, fields ...Fields)
	Error(err error, fields ...Fields)
	Log(logEvent *LogEvent)
}

type logEventWriter interface {
	log(logEvent *LogEvent)
}

// NewProductionLogger creates a Logger that should be used when your service is running in AWS.
// It sends events to Rollbar and writes events to standard error as JSON objects.
func NewProductionLogger(rollbarToken, rollbarEnv string) Logger {
	var rollbarClient *rollbar.Client
	if rollbarToken != "" {
		rollbarClient = rollbar.NewAsync(rollbarToken, rollbarEnv, "", "", "")
	}

	return &logger{
		eventWriter: newProdLogEventWriter(os.Stderr, rollbarClient),
	}
}

// NewDevelopmentLogger creates a Logger that should be used when running your service locally.
// It writes events to standard error in a human readable format.
func NewDevelopmentLogger() Logger {
	return &logger{
		eventWriter: newDevLogEventWriter(os.Stderr),
	}
}

type logger struct {
	eventWriter logEventWriter
}

var _ Logger = &logger{}

func (l *logger) Debug(msg string, fields ...Fields) {
	logEvent := &LogEvent{
		Level:   LevelDebug,
		Message: msg,
		Time:    time.Now().UTC(),
		Fields:  merge(fields),
	}
	l.Log(logEvent)
}

func (l *logger) Info(msg string, fields ...Fields) {
	logEvent := &LogEvent{
		Level:   LevelInfo,
		Message: msg,
		Time:    time.Now().UTC(),
		Fields:  merge(fields),
	}
	l.Log(logEvent)
}

func (l *logger) Error(err error, fields ...Fields) {
	if err == nil {
		return
	}

	logEvent := &LogEvent{
		Level:  LevelError,
		Time:   time.Now().UTC(),
		Error:  err,
		Fields: merge(fields),
	}
	l.Log(logEvent)
}

func (l *logger) Log(logEvent *LogEvent) {
	if logEvent == nil {
		return
	}

	l.eventWriter.log(logEvent)
}

type logEventPayload struct {
	Message    string                 `json:"msg,omitempty"`
	Level      string                 `json:"level,omitempty"`
	Time       time.Time              `json:"time,omitempty"`
	ErrorChain []*errorPayload        `json:"errors,omitempty"`
	Fields     map[string]interface{} `json:"fields,omitempty"`
}

type errorPayload struct {
	Message string                 `json:"msg,omitempty"`
	Fields  map[string]interface{} `json:"fields,omitempty"`
	Stack   []*framePayload        `json:"stack,omitempty"`
}

type framePayload struct {
	Method string `json:"method,omitempty"`
	File   string `json:"file,omitempty"`
	Line   int    `json:"line,omitempty"`
}

func getLogEventPayload(logEvent *LogEvent) *logEventPayload {
	if logEvent == nil {
		return nil
	}
	return &logEventPayload{
		Message:    logEvent.Message,
		Level:      logEvent.Level,
		Time:       logEvent.Time,
		ErrorChain: getErrorPayloads(logEvent.Error),
		Fields:     logEvent.Fields,
	}
}

func getErrorPayloads(err error) []*errorPayload {
	payloads := make([]*errorPayload, 0, 0)

	for ; err != nil; err = errors.Cause(err) {
		frames := errors.Frames(err)

		message := errors.Message(err)
		if message == "" {
			message = err.Error()
		}

		payload := &errorPayload{
			Message: message,
			Stack:   framesToStackFrames(frames),
			Fields:  errors.GetFields(err),
		}
		payloads = append(payloads, payload)
	}

	return payloads
}

func framesToStackFrames(frames []runtime.Frame) []*framePayload {
	stackFrames := make([]*framePayload, 0, len(frames))
	for _, frame := range frames {
		s := &framePayload{
			Method: frame.Function,
			File:   frame.File,
			Line:   frame.Line,
		}
		stackFrames = append(stackFrames, s)
	}
	return stackFrames
}

func merge(fieldMaps []Fields) map[string]interface{} {
	if len(fieldMaps) == 0 {
		return nil
	} else if len(fieldMaps) == 1 {
		return fieldMaps[0]
	}

	merged := make(map[string]interface{})
	for _, fieldMap := range fieldMaps {
		for k, v := range fieldMap {
			merged[k] = v
		}
	}
	return merged
}
