package sq

import (
	"errors"
	"fmt"
	"github.com/jmoiron/sqlx"
	"github.com/jmoiron/sqlx/reflectx"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"reflect"
	"strings"
)

// forked from sqlx, but added ability to unmarshal optional-nested structs

type StructScanner struct {
	rows   *sqlx.Rows
	Mapper *reflectx.Mapper
	unsafe bool

	fields    [][]int
	values    []any
	converter []ssConverter
	columns   []string
}

func NewStructScanner(rows *sqlx.Rows, unsafe bool) *StructScanner {
	return &StructScanner{
		rows:   rows,
		Mapper: reflectx.NewMapper("db"),
		unsafe: unsafe,
	}
}

type ssConverter struct {
	Converter DBTypeConverter
	RefCount  int
}

func (r *StructScanner) Start(dest any) error {
	v := reflect.ValueOf(dest)

	if v.Kind() != reflect.Ptr {
		return errors.New("must pass a pointer, not a value, to StructScan destination")
	}

	columns, err := r.rows.Columns()
	if err != nil {
		return err
	}

	r.columns = columns
	r.fields = r.Mapper.TraversalsByName(v.Type(), columns)
	// if we are not unsafe and are missing fields, return an error
	if f, err := missingFields(r.fields); err != nil && !r.unsafe {
		return fmt.Errorf("missing destination name %s in %T", columns[f], dest)
	}
	r.values = make([]interface{}, len(columns))
	r.converter = make([]ssConverter, len(columns))

	return nil
}

// StructScanExt forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
// does also work with nullabel structs (from LEFT JOIN's)
// does also work with custom value converters
func (r *StructScanner) StructScanExt(q Queryable, dest any) error {
	v := reflect.ValueOf(dest)

	if v.Kind() != reflect.Ptr {
		return errors.New("must pass a pointer, not a value, to StructScan destination")
	}

	// ========= STEP 1 ::  =========

	v = v.Elem()

	err := fieldsByTraversalExtended(q, v, r.fields, r.values, r.converter)
	if err != nil {
		return err
	}
	// scan into the struct field pointers and append to our results
	err = r.rows.Scan(r.values...)
	if err != nil {
		return err
	}

	nullStructs := make(map[string]bool)

	for i, traversal := range r.fields {
		if len(traversal) == 0 {
			continue
		}

		isnsil := reflect.ValueOf(r.values[i]).Elem().IsNil()

		for i := 1; i < len(traversal); i++ {

			canParentNil := reflectx.FieldByIndexes(v, traversal[0:i]).Kind() == reflect.Pointer

			k := fmt.Sprintf("%v", traversal[0:i])
			if v, ok := nullStructs[k]; ok {

				nullStructs[k] = canParentNil && v && isnsil

			} else {
				nullStructs[k] = canParentNil && isnsil
			}
		}

	}

	forcenulled := make(map[string]bool)

	for i, traversal := range r.fields {
		if len(traversal) == 0 {
			continue
		}

		anyparentnull := false
		for i := 1; i < len(traversal); i++ {
			k := fmt.Sprintf("%v", traversal[0:i])
			if nv, ok := nullStructs[k]; ok && nv {

				if _, ok := forcenulled[k]; !ok {
					f := reflectx.FieldByIndexes(v, traversal[0:i])
					f.Set(reflect.Zero(f.Type())) // set to nil
					forcenulled[k] = true
				}

				anyparentnull = true
				break

			}
		}

		if anyparentnull {
			continue
		}

		f := reflectx.FieldByIndexes(v, traversal)

		val1 := reflect.ValueOf(r.values[i])
		val2 := val1.Elem()

		if val2.IsNil() {
			if f.Kind() != reflect.Pointer {
				return errors.New(fmt.Sprintf("Cannot set field %v to NULL value from column '%s' (type: %s)", traversal, r.columns[i], f.Type().String()))
			}

			f.Set(reflect.Zero(f.Type())) // set to nil
		} else {
			if r.converter[i].Converter != nil {
				val3 := val2.Elem()
				conv3, err := r.converter[i].Converter.DBToModel(val3.Interface())
				if err != nil {
					return err
				}
				conv3RVal := reflect.ValueOf(conv3)
				for j := 0; j < r.converter[i].RefCount; j++ {
					newConv3Val := reflect.New(conv3RVal.Type())
					newConv3Val.Elem().Set(conv3RVal)
					conv3RVal = newConv3Val
				}
				f.Set(conv3RVal)
			} else {
				f.Set(val2.Elem())
			}
		}

	}

	return r.rows.Err()
}

// StructScanBase forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
// without (relevant) changes
func (r *StructScanner) StructScanBase(dest any) error {
	v := reflect.ValueOf(dest)

	if v.Kind() != reflect.Ptr {
		return errors.New("must pass a pointer, not a value, to StructScan destination")
	}

	v = v.Elem()

	err := fieldsByTraversalBase(v, r.fields, r.values, true)
	if err != nil {
		return err
	}
	// scan into the struct field pointers and append to our results
	err = r.rows.Scan(r.values...)
	if err != nil {
		return err
	}
	return r.rows.Err()
}

// fieldsByTraversal forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
func fieldsByTraversalExtended(q Queryable, v reflect.Value, traversals [][]int, values []interface{}, converter []ssConverter) error {
	v = reflect.Indirect(v)
	if v.Kind() != reflect.Struct {
		return errors.New("argument not a struct")
	}

	for i, traversal := range traversals {
		if len(traversal) == 0 {
			values[i] = new(interface{})
			continue
		}
		f := reflectx.FieldByIndexes(v, traversal)

		typeStr := f.Type().String()

		foundConverter := false
		for _, conv := range q.ListConverter() {
			if conv.ModelTypeString() == typeStr {
				_v := langext.Ptr[any](nil)
				values[i] = _v
				foundConverter = true
				converter[i] = ssConverter{Converter: conv, RefCount: 0}
				break
			}
		}
		if !foundConverter {
			// also allow non-pointer converter for pointer-types
			for _, conv := range q.ListConverter() {
				if conv.ModelTypeString() == strings.TrimLeft(typeStr, "*") {
					_v := langext.Ptr[any](nil)
					values[i] = _v
					foundConverter = true
					converter[i] = ssConverter{Converter: conv, RefCount: len(typeStr) - len(strings.TrimLeft(typeStr, "*"))} // kind hacky way to get the amount of ptr before <f>, but it works...
					break
				}
			}
		}

		if !foundConverter {
			values[i] = reflect.New(reflect.PointerTo(f.Type())).Interface()
			converter[i] = ssConverter{Converter: nil, RefCount: -1}
		}
	}
	return nil
}

// fieldsByTraversal forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
func fieldsByTraversalBase(v reflect.Value, traversals [][]int, values []interface{}, ptrs bool) error {
	v = reflect.Indirect(v)
	if v.Kind() != reflect.Struct {
		return errors.New("argument not a struct")
	}

	for i, traversal := range traversals {
		if len(traversal) == 0 {
			values[i] = new(interface{})
			continue
		}
		f := reflectx.FieldByIndexes(v, traversal)
		if ptrs {
			values[i] = f.Addr().Interface()
		} else {
			values[i] = f.Interface()
		}
	}
	return nil
}

// missingFields forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
func missingFields(transversals [][]int) (field int, err error) {
	for i, t := range transversals {
		if len(t) == 0 {
			return i, errors.New("missing field")
		}
	}
	return 0, nil
}