// automapper.go scans sql.Rows into Go structs using column name conventions.
//
// The standard library's sql.Rows.Scan requires you to pass a pointer for
// every column in the result set, in order, which is tedious and fragile.
// The automapper eliminates that by reflecting on the destination struct,
// building a map from column names to field index paths, and wiring up the
// scan targets automatically.
//
// # Column-to-field mapping
//
// The mapper inspects the destination struct and applies these rules in order:
//
//  1. A field with a `db:"col"` tag maps to the column named "col". When the
//     field lives inside a nested model struct, the column name is prefixed:
//     "prefix.col".
//
//  2. An anonymous (embedded) struct without a `db` tag is flattened into its
//     parent. Its fields are mapped as if they were declared directly on the
//     parent.
//
//  3. A named struct field without a `db` tag, whose type contains at least
//     one `db`-tagged field, is treated as a nested model. Its prefix is taken
//     from the field's `alias` tag if present, otherwise from the snake_case
//     of the field name. For example, a field named CreatedBy of type AppUser
//     gets the prefix "created_by", so its ID column maps to "created_by.id".
//
//  4. Everything else (unexported fields, fields without `db` tags whose types
//     have no `db`-tagged fields) is skipped.
//
// The mapping is computed once per destination type and cached with a
// sync.RWMutex for concurrent safety.
//
// # How it connects to the query builder
//
// The builder's Cols method generates SELECT expressions with aliases that
// match this mapping convention. For a table reference like
// T[models.AppUser]("au"), Cols produces:
//
//	au.id AS "app_user.id", au.username AS "app_user.username", ...
//
// When the automapper sees "app_user.id" in the result columns, it looks up
// that key and finds the field path into the AppUser nested struct in the
// destination DTO. This is what lets a single scan call populate a multi-model
// DTO from a JOIN query.
//
// For single-table queries where no prefix is needed, use ColsFlat instead.
// It produces bare expressions like "au.id, au.username, ..." which map
// directly to `db` tags without a prefix.
//
// # LEFT JOIN nil detection
//
// When a destination struct has a pointer-to-struct field (e.g. *AppUser),
// the mapper tracks it as a potential LEFT JOIN result. Before scanning, it
// allocates the pointed-to struct so the driver has somewhere to write values.
// After scanning, if every field in that struct is still its zero value, the
// mapper nils the pointer back out. This gives you a clean nil when the LEFT
// JOIN matched no rows, instead of a struct full of zero values.
//
// # Public API
//
//   - ScanOne scans a single row into a struct pointer. Returns sql.ErrNoRows
//     if no row is available.
//   - ScanAll scans all remaining rows into a slice of structs.
//   - QueryOne and QueryAll are convenience wrappers that execute a query and
//     scan in one call.
//   - QueryScalar scans a single scalar value (int, string, etc.) without
//     struct mapping.
//   - Columns generates the aliased SELECT expressions for a model type.
//   - DebugMapping returns the full column-to-field-path map for a type,
//     useful for troubleshooting mismatches.

package dbutil

import (
	"context"
	"database/sql"
	"fmt"
	"reflect"
	"strings"
	"sync"
	"unicode"
)

// Querier is the interface for types that can execute SQL queries.
// *sql.DB, *sql.Tx, and *sql.Conn all implement this.
type Querier interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}

var (
	cacheMu      sync.RWMutex
	mappingCache = make(map[reflect.Type]typeMapping)
)

type typeMapping struct {
	columns    map[string][]int // column name -> field index path
	ptrStructs [][]int          // field index paths of pointer-to-struct fields
}

// ScanOne scans the next row from rows into dest.
// dest must be a pointer to a struct. Does not close rows.
func ScanOne(rows *sql.Rows, dest any) error {
	dv := reflect.ValueOf(dest)
	if dv.Kind() != reflect.Ptr || dv.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("automapper: dest must be a pointer to a struct, got %T", dest)
	}

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

	tm := getMapping(dv.Elem().Type())

	if !rows.Next() {
		if err := rows.Err(); err != nil {
			return err
		}
		return sql.ErrNoRows
	}

	return scanRow(rows, columns, tm, dv.Elem())
}

// ScanAll scans all remaining rows into dest.
// dest must be a pointer to a slice of structs (or pointer-to-structs).
// Does not close rows.
func ScanAll(rows *sql.Rows, dest any) error {
	dv := reflect.ValueOf(dest)
	if dv.Kind() != reflect.Ptr || dv.Elem().Kind() != reflect.Slice {
		return fmt.Errorf("automapper: dest must be a pointer to a slice, got %T", dest)
	}

	sliceVal := dv.Elem()
	elemType := sliceVal.Type().Elem()
	isPtr := elemType.Kind() == reflect.Ptr
	if isPtr {
		elemType = elemType.Elem()
	}
	if elemType.Kind() != reflect.Struct {
		return fmt.Errorf("automapper: slice element must be a struct or *struct, got %s", elemType.Kind())
	}

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

	tm := getMapping(elemType)

	for rows.Next() {
		elem := reflect.New(elemType).Elem()
		if err := scanRow(rows, columns, tm, elem); err != nil {
			return err
		}
		if isPtr {
			ptr := reflect.New(elemType)
			ptr.Elem().Set(elem)
			sliceVal.Set(reflect.Append(sliceVal, ptr))
		} else {
			sliceVal.Set(reflect.Append(sliceVal, elem))
		}
	}

	return rows.Err()
}

// QueryOne executes query and scans a single row into dest.
// dest must be a pointer to a struct.
func QueryOne(ctx context.Context, db Querier, dest any, query string, args ...any) error {
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return err
	}
	defer rows.Close()
	return ScanOne(rows, dest)
}

// QueryAll executes query and scans all rows into dest.
// dest must be a pointer to a slice of structs.
func QueryAll(ctx context.Context, db Querier, dest any, query string, args ...any) error {
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return err
	}
	defer rows.Close()
	return ScanAll(rows, dest)
}

// QueryScalar executes query and scans a single scalar value.
func QueryScalar[T any](ctx context.Context, db Querier, query string, args ...any) (T, error) {
	var result T
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return result, err
	}
	defer rows.Close()

	if !rows.Next() {
		if err := rows.Err(); err != nil {
			return result, err
		}
		return result, sql.ErrNoRows
	}
	err = rows.Scan(&result)
	return result, err
}

// Columns generates SELECT column expressions for a model struct, aliased for
// the automapper to route into nested destination structs.
//
// For a type like model.AppUser with db tags, and tableAlias "au":
//
//	Columns(model.AppUser{}, "au")
//	-> au.id AS "app_user.id", au.username AS "app_user.username", ...
//
// The mapping prefix defaults to the snake_case of the type name. Override it
// with a third argument to match an `alias` tag on the destination field:
//
//	Columns(model.AppUser{}, "cb", "created_by")
//	-> cb.id AS "created_by.id", cb.username AS "created_by.username", ...
func Columns(model any, tableAlias string, mappingPrefix ...string) string {
	t := reflect.TypeOf(model)
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	prefix := toSnakeCase(t.Name())
	if len(mappingPrefix) > 0 && mappingPrefix[0] != "" {
		prefix = mappingPrefix[0]
	}
	cols := collectColumns(t, tableAlias, prefix)
	return strings.Join(cols, ", ")
}

// DebugMapping returns the column-to-field mapping for a given struct type.
// Useful for verifying that your SQL column aliases match the expected mapping.
func DebugMapping(dest any) map[string]string {
	t := reflect.TypeOf(dest)
	if t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	tm := getMapping(t)
	result := make(map[string]string, len(tm.columns))
	for col, idx := range tm.columns {
		result[col] = fieldPathString(t, idx)
	}
	return result
}

// internal

func scanRow(rows *sql.Rows, columns []string, tm typeMapping, dest reflect.Value) error {
	// Allocate pointer-to-struct fields so we can scan into them
	for _, idx := range tm.ptrStructs {
		f := dest.FieldByIndex(idx)
		if f.IsNil() {
			f.Set(reflect.New(f.Type().Elem()))
		}
	}

	// Build scan targets
	targets := make([]any, len(columns))
	for i, col := range columns {
		if idx, ok := tm.columns[col]; ok {
			f := fieldByIndex(dest, idx)
			if isInsidePtrStruct(idx, tm.ptrStructs) && f.Kind() != reflect.Ptr {
				// Non-pointer field inside a LEFT JOIN struct.
				// database/sql cannot scan NULL into non-pointer types,
				// so wrap in a nullSafeScanner that absorbs NULLs.
				targets[i] = &nullSafeScanner{field: f}
			} else {
				targets[i] = f.Addr().Interface()
			}
		} else {
			// Unmapped column -- discard
			targets[i] = new(sql.RawBytes)
		}
	}

	if err := rows.Scan(targets...); err != nil {
		return err
	}

	// For LEFT JOINs: nil out pointer-to-struct fields where every column
	// scanned to its zero value (the entire joined row was NULL).
	for _, idx := range tm.ptrStructs {
		f := dest.FieldByIndex(idx)
		if f.Elem().IsZero() {
			f.Set(reflect.Zero(f.Type()))
		}
	}

	return nil
}

// nullSafeScanner implements sql.Scanner for non-pointer struct fields that
// live inside a LEFT JOIN (pointer-to-struct) target. When the LEFT JOIN
// produces no match, every column is NULL. database/sql can't scan NULL into
// non-pointer types like string or int32, so this wrapper absorbs NULLs
// (leaving the field at its zero value) and delegates non-NULL values to the
// field's own Scanner or to database/sql's built-in conversion.
type nullSafeScanner struct {
	field reflect.Value
}

func (n *nullSafeScanner) Scan(src any) error {
	if src == nil {
		return nil
	}
	// If the field's address implements sql.Scanner, delegate to it.
	addr := n.field.Addr().Interface()
	if scanner, ok := addr.(sql.Scanner); ok {
		return scanner.Scan(src)
	}
	// Otherwise, use reflect to assign compatible types directly.
	sv := reflect.ValueOf(src)
	ft := n.field.Type()
	if sv.Type().AssignableTo(ft) {
		n.field.Set(sv)
		return nil
	}
	if sv.Type().ConvertibleTo(ft) {
		n.field.Set(sv.Convert(ft))
		return nil
	}
	return fmt.Errorf("automapper: cannot convert %T to %s", src, ft)
}

// isInsidePtrStruct reports whether the field at idx is a descendant of any
// pointer-to-struct field tracked for LEFT JOIN nil detection.
func isInsidePtrStruct(idx []int, ptrStructs [][]int) bool {
	for _, ps := range ptrStructs {
		if len(idx) > len(ps) {
			match := true
			for i, v := range ps {
				if idx[i] != v {
					match = false
					break
				}
			}
			if match {
				return true
			}
		}
	}
	return false
}

// fieldByIndex walks a field index path, dereferencing pointers along the way.
func fieldByIndex(v reflect.Value, index []int) reflect.Value {
	for _, i := range index {
		if v.Kind() == reflect.Ptr {
			v = v.Elem()
		}
		v = v.Field(i)
	}
	return v
}

func getMapping(t reflect.Type) typeMapping {
	cacheMu.RLock()
	if tm, ok := mappingCache[t]; ok {
		cacheMu.RUnlock()
		return tm
	}
	cacheMu.RUnlock()

	cacheMu.Lock()
	defer cacheMu.Unlock()

	// Double-check after write lock
	if tm, ok := mappingCache[t]; ok {
		return tm
	}

	tm := typeMapping{columns: make(map[string][]int)}
	buildColumns(t, "", nil, tm.columns)
	tm.ptrStructs = findPtrStructs(t, nil)
	mappingCache[t] = tm
	return tm
}

// buildColumns recursively maps column names -> field index paths.
//
// Mapping rules:
//  1. Field has `db:"col"` tag -> maps to "col" (or "prefix.col" when nested)
//  2. Anonymous (embedded) struct without `db` tag -> flattened into parent
//  3. Named struct field without `db` tag, whose type has db-tagged fields ->
//     nested model. Prefix comes from `alias` tag or snake_case of field name.
//  4. Everything else is skipped.
func buildColumns(t reflect.Type, prefix string, parent []int, columns map[string][]int) {
	for i := range t.NumField() {
		field := t.Field(i)
		if !field.IsExported() {
			continue
		}

		idx := appendIndex(parent, i)

		// Rule 1: leaf column
		if dbTag := field.Tag.Get("db"); dbTag != "" && dbTag != "-" {
			col := dbTag
			if prefix != "" {
				col = prefix + "." + dbTag
			}
			columns[col] = idx
			continue
		}

		// Struct field without db tag -- check for nested model or embedding
		ft := derefType(field.Type)
		if ft.Kind() != reflect.Struct || !isModelStruct(ft) {
			continue
		}

		if field.Anonymous {
			// Rule 2: embedded -- flatten
			buildColumns(ft, prefix, idx, columns)
		} else {
			// Rule 3: named nested model
			nestedPrefix := field.Tag.Get("alias")
			if nestedPrefix == "" {
				nestedPrefix = toSnakeCase(field.Name)
			}
			if prefix != "" {
				nestedPrefix = prefix + "." + nestedPrefix
			}
			buildColumns(ft, nestedPrefix, idx, columns)
		}
	}
}

// findPtrStructs returns field index paths of all pointer-to-struct fields
// whose pointed-to type is a model struct. Used for LEFT JOIN nil detection.
func findPtrStructs(t reflect.Type, parent []int) [][]int {
	var result [][]int
	for i := range t.NumField() {
		field := t.Field(i)
		if !field.IsExported() {
			continue
		}

		idx := appendIndex(parent, i)

		if field.Type.Kind() == reflect.Ptr {
			inner := field.Type.Elem()
			if inner.Kind() == reflect.Struct && isModelStruct(inner) {
				result = append(result, idx)
				result = append(result, findPtrStructs(inner, idx)...)
				continue
			}
		}

		// Recurse into non-pointer nested model structs
		ft := derefType(field.Type)
		if field.Tag.Get("db") == "" && ft.Kind() == reflect.Struct && isModelStruct(ft) {
			result = append(result, findPtrStructs(ft, idx)...)
		}
	}
	return result
}

// isModelStruct reports whether t (or any of its embedded structs) contains
// at least one field with a "db" tag. This distinguishes model structs
// (e.g. model.AppUser) from value types (e.g. time.Time, uuid.UUID).
func isModelStruct(t reflect.Type) bool {
	for i := range t.NumField() {
		f := t.Field(i)
		if tag := f.Tag.Get("db"); tag != "" && tag != "-" {
			return true
		}
		if f.Anonymous {
			ft := derefType(f.Type)
			if ft.Kind() == reflect.Struct && isModelStruct(ft) {
				return true
			}
		}
	}
	return false
}

func collectColumns(t reflect.Type, tableAlias, prefix string) []string {
	var cols []string
	for i := range t.NumField() {
		f := t.Field(i)
		if !f.IsExported() {
			continue
		}
		if dbTag := f.Tag.Get("db"); dbTag != "" && dbTag != "-" {
			if tableAlias != "" {
				cols = append(cols, fmt.Sprintf(`%s.%s AS "%s.%s"`, tableAlias, dbTag, prefix, dbTag))
			} else {
				cols = append(cols, fmt.Sprintf(`%s AS "%s.%s"`, dbTag, prefix, dbTag))
			}
			continue
		}
		// Flatten embedded structs
		if f.Anonymous {
			ft := derefType(f.Type)
			if ft.Kind() == reflect.Struct {
				cols = append(cols, collectColumns(ft, tableAlias, prefix)...)
			}
		}
	}
	return cols
}

func fieldPathString(t reflect.Type, index []int) string {
	var parts []string
	for _, i := range index {
		if t.Kind() == reflect.Ptr {
			t = t.Elem()
		}
		f := t.Field(i)
		parts = append(parts, f.Name)
		t = f.Type
	}
	return strings.Join(parts, ".")
}

func derefType(t reflect.Type) reflect.Type {
	for t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	return t
}

func appendIndex(parent []int, i int) []int {
	idx := make([]int, len(parent)+1)
	copy(idx, parent)
	idx[len(parent)] = i
	return idx
}

// toSnakeCase converts CamelCase/PascalCase to snake_case.
// Handles consecutive uppercase correctly: "IPAddr" -> "ip_addr", "OrgUserDTO" -> "org_user_dto".
func toSnakeCase(s string) string {
	var b strings.Builder
	runes := []rune(s)
	for i, r := range runes {
		if unicode.IsUpper(r) {
			if i > 0 {
				prev := runes[i-1]
				if !unicode.IsUpper(prev) || (i+1 < len(runes) && unicode.IsLower(runes[i+1])) {
					b.WriteRune('_')
				}
			}
			b.WriteRune(unicode.ToLower(r))
		} else {
			b.WriteRune(r)
		}
	}
	return b.String()
}
