package sq

import (
	"errors"
	"fmt"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"reflect"
)

type DBTypeConverter interface {
	ModelTypeString() string
	DBTypeString() string
	ModelToDB(v any) (any, error)
	DBToModel(v any) (any, error)
}

type DBDataConstraint interface {
	string | langext.NumberConstraint | []byte
}

type DatabaseConvertible[TModelData any, TDBData DBDataConstraint] interface {
	MarshalToDB(v TModelData) (TDBData, error)
	UnmarshalToModel(v TDBData) (TModelData, error)
}

type dbTypeConverterImpl[TModelData any, TDBData DBDataConstraint] struct {
	dbTypeString    string
	modelTypeString string
	todb            func(v TModelData) (TDBData, error)
	tomodel         func(v TDBData) (TModelData, error)
}

func (t *dbTypeConverterImpl[TModelData, TDBData]) ModelTypeString() string {
	return t.modelTypeString
}

func (t *dbTypeConverterImpl[TModelData, TDBData]) DBTypeString() string {
	return t.dbTypeString
}

func (t *dbTypeConverterImpl[TModelData, TDBData]) ModelToDB(v any) (any, error) {
	if vv, ok := v.(TModelData); ok {
		return t.todb(vv)
	}
	return nil, errors.New(fmt.Sprintf("Unexpected value in DBTypeConverter, expected '%s', found '%T'", t.modelTypeString, v))
}

func (t *dbTypeConverterImpl[TModelData, TDBData]) DBToModel(v any) (any, error) {
	if vv, ok := v.(TDBData); ok {
		return t.tomodel(vv)
	}
	return nil, errors.New(fmt.Sprintf("Unexpected value in DBTypeConverter, expected '%s', found '%T'", t.dbTypeString, v))
}

func NewDBTypeConverter[TModelData any, TDBData DBDataConstraint](todb func(v TModelData) (TDBData, error), tomodel func(v TDBData) (TModelData, error)) DBTypeConverter {
	return &dbTypeConverterImpl[TModelData, TDBData]{
		dbTypeString:    fmt.Sprintf("%T", *new(TDBData)),
		modelTypeString: fmt.Sprintf("%T", *new(TModelData)),
		todb:            todb,
		tomodel:         tomodel,
	}
}

func NewAutoDBTypeConverter[TDBData DBDataConstraint, TModelData DatabaseConvertible[TModelData, TDBData]](obj TModelData) DBTypeConverter {
	return &dbTypeConverterImpl[TModelData, TDBData]{
		dbTypeString:    fmt.Sprintf("%T", *new(TDBData)),
		modelTypeString: fmt.Sprintf("%T", *new(TModelData)),
		todb:            obj.MarshalToDB,
		tomodel:         obj.UnmarshalToModel,
	}
}

func convertValueToDB(q Queryable, value any) (any, error) {
	modelTypeStr := fmt.Sprintf("%T", value)

	for _, conv := range q.ListConverter() {
		if conv.ModelTypeString() == modelTypeStr {
			return conv.ModelToDB(value)
		}
	}

	if value != nil && reflect.TypeOf(value).Kind() == reflect.Ptr {
		vof := reflect.ValueOf(value)
		if vof.IsNil() {
			return nil, nil
		} else {
			return convertValueToDB(q, vof.Elem().Interface())
		}
	}

	return value, nil
}

func convertValueToModel(q Queryable, value any, destinationType string) (any, error) {
	dbTypeString := fmt.Sprintf("%T", value)

	for _, conv := range q.ListConverter() {
		if conv.ModelTypeString() == destinationType && conv.DBTypeString() == dbTypeString {
			return conv.DBToModel(value)
		}
	}

	return value, nil
}