package schema

import (
	"context"
	"database/sql"
	"fmt"
	"time"

	"github.com/sirupsen/logrus"
	"xorm.io/xorm"
	"xorm.io/xorm/log"
	"xorm.io/xorm/names"

	_ "github.com/lib/pq"           // PostgreSQL support
	_ "github.com/mattn/go-sqlite3" // SQLite support
)

// Logger used by this package
var Logger = logrus.New()

var xormEngine *xorm.Engine

type engineContextKeyType struct{}

var engineContextKey = engineContextKeyType{}

// Engine represents a xorm engine or session.
type Engine interface {
	Table(tableNameOrBean any) *xorm.Session
	Count(...any) (int64, error)
	Decr(column string, arg ...any) *xorm.Session
	Delete(...any) (int64, error)
	Truncate(...any) (int64, error)
	Exec(...any) (sql.Result, error)
	Find(any, ...any) error
	Get(beans ...any) (bool, error)
	ID(any) *xorm.Session
	In(string, ...any) *xorm.Session
	Incr(column string, arg ...any) *xorm.Session
	Insert(...any) (int64, error)
	Iterate(any, xorm.IterFunc) error
	Join(joinOperator string, tablename, condition any, args ...any) *xorm.Session
	SQL(any, ...any) *xorm.Session
	Where(any, ...any) *xorm.Session
	Asc(colNames ...string) *xorm.Session
	Desc(colNames ...string) *xorm.Session
	Limit(limit int, start ...int) *xorm.Session
	NoAutoTime() *xorm.Session
	SumInt(bean any, columnName string) (res int64, err error)
	Sync(...any) error
	Select(string) *xorm.Session
	SetExpr(string, any) *xorm.Session
	NotIn(string, ...any) *xorm.Session
	OrderBy(any, ...any) *xorm.Session
	Exist(...any) (bool, error)
	Distinct(...string) *xorm.Session
	Query(...any) ([]map[string][]byte, error)
	Cols(...string) *xorm.Session
	Context(ctx context.Context) *xorm.Session
	Ping() error
	IsTableExist(tableNameOrBean any) (bool, error)
	Begin() error
	Rollback() error
	Commit() error
}

// Query the engine from the context.
func Query(ctx context.Context) Engine {
	if engine, ok := ctx.Value(engineContextKey).(Engine); ok {
		return engine
	}
	return xormEngine.Context(ctx)
}

// Open a database connection.
func Open(driver, config string) error {
	var err error
	if xormEngine, err = xorm.NewEngine(driver, config); err != nil {
		return err
	}

	gonicNames := []string{
		"ID",
		"SSL", "UID",
		"SNR", "RSSI",
		"APRS", "MeshCore",
		"LoRa",
	}
	for _, name := range gonicNames {
		names.LintGonicMapper[name] = true
	}
	xormEngine.SetMapper(names.GonicMapper{})

	logger := &xormLogger{}
	//logger.SetLevel(log.LOG_DEBUG)
	xormEngine.SetLogger(logger)
	xormEngine.ShowSQL(true)

	for _, model := range registeredModels {
		Logger.Debugf("schema: sync schema %T", model)
		if err = xormEngine.Sync(model); err != nil {
			_ = xormEngine.Close()
			xormEngine = nil
			return err
		}
	}

	return nil
}

var (
	registeredModels    []any
	registeredInitFuncs []func() error
)

func RegisterModel(model any, initFuncs ...func() error) {
	registeredModels = append(registeredModels, model)
	if len(initFuncs) > 0 && initFuncs[0] != nil {
		registeredInitFuncs = append(registeredInitFuncs, initFuncs...)
	}
}

func NULLFloat64(v float64) *float64 {
	if v == 0 {
		return nil
	}
	return &v
}

func NULLString(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}

func NULLTime(t time.Time) *time.Time {
	if t.Equal(time.Time{}) {
		return nil
	}
	return &t
}

type xormLogger struct {
	showSQL bool
}

func (l xormLogger) BeforeSQL(ctx log.LogContext) {
	var sessionPart string
	v := ctx.Ctx.Value(log.SessionIDKey)
	if key, ok := v.(string); ok {
		sessionPart = fmt.Sprintf(" [%s]", key)
	}
	Logger.Debugf("[SQL (before)]%s %s %v", sessionPart, ctx.SQL, ctx.Args)
}

func (l xormLogger) AfterSQL(ctx log.LogContext) {
	var sessionPart string
	v := ctx.Ctx.Value(log.SessionIDKey)
	if key, ok := v.(string); ok {
		sessionPart = fmt.Sprintf(" [%s]", key)
	}
	if ctx.ExecuteTime > 0 {
		Logger.Infof("[SQL (after)]%s %s %v - %v", sessionPart, ctx.SQL, ctx.Args, ctx.ExecuteTime)
	} else {
		Logger.Infof("[SQL (after)]%s %s %v", sessionPart, ctx.SQL, ctx.Args)
	}
} // only invoked when IsShowSQL is true

func (l xormLogger) Debug(args ...any)                 { Logger.Debug(append([]any{"engine: "}, args...)...) }
func (l xormLogger) Debugf(format string, args ...any) { Logger.Debugf("engine: "+format, args...) }
func (l xormLogger) Error(args ...any)                 { Logger.Error(append([]any{"engine: "}, args...)...) }
func (l xormLogger) Errorf(format string, args ...any) { Logger.Errorf("engine: "+format, args...) }
func (l xormLogger) Info(args ...any)                  { Logger.Info(append([]any{"engine: "}, args...)...) }
func (l xormLogger) Infof(format string, args ...any)  { Logger.Infof("engine: "+format, args...) }
func (l xormLogger) Warn(args ...any)                  { Logger.Warn(append([]any{"engine: "}, args...)...) }
func (l xormLogger) Warnf(format string, args ...any)  { Logger.Warnf("engine: "+format, args...) }

func (l xormLogger) Level() log.LogLevel {
	switch Logger.Level {
	case logrus.TraceLevel:
		return log.LOG_DEBUG
	case logrus.DebugLevel:
		return log.LOG_DEBUG
	case logrus.InfoLevel:
		return log.LOG_INFO
	case logrus.ErrorLevel:
		return log.LOG_ERR
	case logrus.WarnLevel:
		return log.LOG_WARNING
	case logrus.FatalLevel:
		return log.LOG_OFF
	default:
		return log.LOG_UNKNOWN
	}
}

func (l xormLogger) SetLevel(level log.LogLevel) {
	switch level {
	case log.LOG_DEBUG:
		Logger.SetLevel(logrus.DebugLevel)
	case log.LOG_INFO:
		Logger.SetLevel(logrus.InfoLevel)
	case log.LOG_ERR:
		Logger.SetLevel(logrus.ErrorLevel)
	case log.LOG_OFF:
		Logger.SetLevel(logrus.FatalLevel)
	}
}

func (l *xormLogger) ShowSQL(show ...bool) {
	if len(show) > 0 {
		l.showSQL = show[0]
	}
}

func (l xormLogger) IsShowSQL() bool {
	return l.showSQL
}

var _ log.ContextLogger = (*xormLogger)(nil)
