package sq

import (
	"errors"
	"fmt"
	"github.com/jmoiron/sqlx"
	"github.com/jmoiron/sqlx/reflectx"
	"reflect"
)

// 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
	columns []string
}

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

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))

	return nil
}

// StructScanExt forked from github.com/jmoiron/sqlx@v1.3.5/sqlx.go
// does also wok with nullabel structs (from LEFT JOIN's)
func (r *StructScanner) StructScanExt(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(v, r.fields, r.values)
	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()
		val3 := val2.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 {
			f.Set(val3)
		}

	}

	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(v reflect.Value, traversals [][]int, values []interface{}) 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)

		values[i] = reflect.New(reflect.PointerTo(f.Type())).Interface()
	}
	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
}
