package dbutil

import (
	"context"
	"regexp"
	"strings"
	"time"

	"github.com/rs/zerolog"
	"maunium.net/go/maulogger/v2"
)

type DatabaseLogger interface {
	QueryTiming(ctx context.Context, method, query string, args []interface{}, nrows int, duration time.Duration)
	WarnUnsupportedVersion(current, latest int)
	PrepareUpgrade(current, latest int)
	DoUpgrade(from, to int, message string, txn bool)
	// Deprecated: legacy warning method, return errors instead
	Warn(msg string, args ...interface{})
}

type noopLogger struct{}

var NoopLogger DatabaseLogger = &noopLogger{}

func (n noopLogger) WarnUnsupportedVersion(_, _ int)      {}
func (n noopLogger) PrepareUpgrade(_, _ int)              {}
func (n noopLogger) DoUpgrade(_, _ int, _ string, _ bool) {}
func (n noopLogger) Warn(msg string, args ...interface{}) {}

func (n noopLogger) QueryTiming(_ context.Context, _, _ string, _ []interface{}, _ int, _ time.Duration) {
}

type mauLogger struct {
	l maulogger.Logger
}

func MauLogger(log maulogger.Logger) DatabaseLogger {
	return &mauLogger{l: log}
}

func (m mauLogger) WarnUnsupportedVersion(current, latest int) {
	m.l.Warnfln("Unsupported database schema version: currently on v%d, latest known: v%d - continuing anyway", current, latest)
}

func (m mauLogger) PrepareUpgrade(current, latest int) {
	m.l.Infofln("Database currently on v%d, latest: v%d", current, latest)
}

func (m mauLogger) DoUpgrade(from, to int, message string, _ bool) {
	m.l.Infofln("Upgrading database from v%d to v%d: %s", from, to, message)
}

func (m mauLogger) QueryTiming(_ context.Context, method, query string, _ []interface{}, _ int, duration time.Duration) {
	if duration > 1*time.Second {
		m.l.Warnfln("%s(%s) took %.3f seconds", method, query, duration.Seconds())
	}
}

func (m mauLogger) Warn(msg string, args ...interface{}) {
	m.l.Warnfln(msg, args...)
}

type zeroLogger struct {
	l *zerolog.Logger
}

func ZeroLogger(log zerolog.Logger) DatabaseLogger {
	return ZeroLoggerPtr(&log)
}

func ZeroLoggerPtr(log *zerolog.Logger) DatabaseLogger {
	return &zeroLogger{l: log}
}

func (z zeroLogger) WarnUnsupportedVersion(current, latest int) {
	z.l.Warn().
		Int("current_db_version", current).
		Int("latest_known_version", latest).
		Msg("Unsupported database schema version, continuing anyway")
}

func (z zeroLogger) PrepareUpgrade(current, latest int) {
	evt := z.l.Info().
		Int("current_db_version", current).
		Int("latest_known_version", latest)
	if current == latest {
		evt.Msg("Database is up to date")
	} else {
		evt.Msg("Preparing to update database schema")
	}
}

func (z zeroLogger) DoUpgrade(from, to int, message string, txn bool) {
	z.l.Info().
		Int("from", from).
		Int("to", to).
		Bool("single_txn", txn).
		Str("description", message).
		Msg("Upgrading database")
}

var whitespaceRegex = regexp.MustCompile(`\s+`)

func (z zeroLogger) QueryTiming(ctx context.Context, method, query string, args []interface{}, nrows int, duration time.Duration) {
	log := zerolog.Ctx(ctx)
	if log.GetLevel() == zerolog.Disabled {
		log = z.l
	}
	if log.GetLevel() != zerolog.TraceLevel && duration < 1*time.Second {
		return
	}
	if nrows > -1 {
		rowLog := log.With().Int("rows", nrows).Logger()
		log = &rowLog
	}
	query = strings.TrimSpace(whitespaceRegex.ReplaceAllLiteralString(query, " "))
	log.Trace().
		Int64("duration_µs", duration.Microseconds()).
		Str("method", method).
		Str("query", query).
		Interface("query_args", args).
		Msg("Query")
	if duration >= 1*time.Second {
		log.Warn().
			Float64("duration_seconds", duration.Seconds()).
			Str("method", method).
			Str("query", query).
			Msg("Query took long")
	}
}

func (z zeroLogger) Warn(msg string, args ...interface{}) {
	z.l.Warn().Msgf(msg, args...)
}
