package mapper

// Package mapper provides utilities to map fields from one struct to another
// by matching field names (case-insensitive). Unmatched fields are left as
// their zero values.

import (
	"fmt"
	"reflect"
	"strings"
)

// Map copies field values from src into dst by matching field names
// (case-insensitive). Fields in dst that have no counterpart in src
// are left at their zero value.
//
// Both dst and src must be pointers to structs.
// Returns an error if the types do not satisfy those constraints.
func Map(dst, src any) error {
	dstVal := reflect.ValueOf(dst)
	srcVal := reflect.ValueOf(src)

	if dstVal.Kind() != reflect.Ptr || dstVal.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("mapper: dst must be a pointer to a struct, got %T", dst)
	}
	if srcVal.Kind() != reflect.Ptr || srcVal.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("mapper: src must be a pointer to a struct, got %T", src)
	}

	dstElem := dstVal.Elem()
	srcElem := srcVal.Elem()

	// Build a lookup map of src fields: lowercase name -> field value
	srcFields := make(map[string]reflect.Value)
	for i := 0; i < srcElem.NumField(); i++ {
		f := srcElem.Type().Field(i)
		if !f.IsExported() {
			continue
		}
		srcFields[strings.ToLower(f.Name)] = srcElem.Field(i)
	}

	// Iterate over dst fields and copy matching src values
	for i := 0; i < dstElem.NumField(); i++ {
		dstField := dstElem.Field(i)
		dstType := dstElem.Type().Field(i)

		if !dstType.IsExported() || !dstField.CanSet() {
			continue
		}

		srcField, ok := srcFields[strings.ToLower(dstType.Name)]
		if !ok {
			// No matching src field – leave zero value in place
			continue
		}

		if srcField.Type().AssignableTo(dstField.Type()) {
			dstField.Set(srcField)
		} else if srcField.Type().ConvertibleTo(dstField.Type()) {
			dstField.Set(srcField.Convert(dstField.Type()))
		}
		// If neither assignable nor convertible, the dst field keeps its zero value
	}

	return nil
}

// MustMap is like Map but panics on error.
func MustMap(dst, src any) {
	if err := Map(dst, src); err != nil {
		panic(err)
	}
}
