package observability

import (
	"code.justin.tv/eventbus/controlplane/internal/db"
	"code.justin.tv/eventbus/controlplane/internal/twirperr"
	"github.com/pkg/errors"
	"go.uber.org/zap"
)

// layout the expectations for what is able to log
// using this middleware (mainly useful for testing)
type obsLogger interface {
	Debug(string, ...zap.Field)
	Error(string, ...zap.Field)
	Warn(string, ...zap.Field)
}

type DBError interface {
	NotFound() bool
}

// Given some observabilityData for a given DB request, logs the information using
// the logger provided in the obsData
func log(obsData *observabilityData) {
	fields := make([]zap.Field, 0, len(obsData.fields)+6)
	fields = append(fields, zap.Namespace("query")) // namespace all log details under this key
	fields = append(fields, zap.String("action", obsData.action))
	fields = append(fields, obsData.fields...) // include returned operation fields
	fields = append(fields, zap.Time("start", obsData.start))
	fields = append(fields, zap.Time("end", obsData.end))
	fields = append(fields, zap.Duration("duration", obsData.end.Sub(obsData.start)))
	fields = append(fields, zap.Error(obsData.err)) // will noop if err is nil

	// choose a logging function and base message
	var logFn func(string, ...zap.Field)
	var msg string
	if obsData.err == db.ErrResourceNotFound { // old 'not found' method
		// It's common to have NotFound-type errors from time to time,
		// but still make sure they are logged for debugging purposes
		logFn = obsData.logger.Warn
		msg = db.ErrResourceNotFound.Error()
	} else if _, ok := obsData.err.(DBError); ok {
		logFn = obsData.logger.Warn
		msg = "resource not found"
	} else if dbErr, ok := errors.Cause(obsData.err).(twirperr.DBError); ok { // if this is an expected DB-type error, don't log ERROR
		logFn = obsData.logger.Warn
		msg = twirperr.Convert(dbErr).Error()
	} else if obsData.err != nil {
		logFn = obsData.logger.Error
		msg = "db operation error"
	} else {
		logFn = obsData.logger.Debug
		msg = "db operation"
	}
	logFn(msg, fields...)
}
