package bootstrap

import (
	"context"
	"database/sql"
	_ "embed"
	"encoding/json"
	"fmt"
	"log"
	"strings"
	"time"

	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/pkg/utils"
)

//go:embed standard_field_mappings.json
var standardFieldMappingsJSON []byte

// FieldMappingGroup represents a set of field mappings between two objects for an action type.
type FieldMappingGroup struct {
	SourceObject        string            `json:"sourceObject"`
	TargetObject        string            `json:"targetObject"`
	ActionType          string            `json:"actionType"`
	IsCustomized        bool              `json:"isCustomized,omitempty"`
	RetiredSourceFields []string          `json:"retiredSourceFields,omitempty"`
	Mappings            []FieldMappingDef `json:"mappings"`
}

// FieldMappingDef represents a single source→target field mapping.
//
// SourceLookupField is the field on the target object that holds the source
// parent's Id at insert time. It is consumed only by the new
// insertInheritFromLookup ActionType's applier in features/records — legacy
// consumers (Lead Convert, Flow RecordCreate) pass their source record
// explicitly and ignore this field, matching its NULL shape in their rows.
type FieldMappingDef struct {
	SourceField       string `json:"sourceField"`
	TargetField       string `json:"targetField"`
	DefaultValue      string `json:"defaultValue,omitempty"`
	SourceLookupField string `json:"sourceLookupField,omitempty"`
	IsCustomized      *bool  `json:"isCustomized,omitempty"`
}

const (
	tableObjectFieldMapping         = "_System_ObjectFieldMapping"
	standardObjectFieldMappingOwner = "standard"
	legacyObjectFieldMappingOwner   = "legacy"
	maxObjectFieldMappingOwnerLen   = 64
)

// InitializeFieldMappings seeds standard field mappings for Lead Convert.
// Uses INSERT IGNORE to skip already-existing rows (idempotent on restart).
// Rows with IsCustomized=1 keep their editable payload columns.
func InitializeFieldMappings(rawDB *sql.DB) error {
	log.Println("🔧 Initializing standard field mappings...")
	var groups []FieldMappingGroup
	if err := json.Unmarshal(standardFieldMappingsJSON, &groups); err != nil {
		return apperrors.NewInternalError("failed to parse standard_field_mappings.json", err)
	}
	return UpsertFieldMappingGroups(context.Background(), rawDB, groups)
}

// UpsertFieldMappingGroups writes the platform-standard field-mapping seed.
// Standard rows are stamped with Source='standard' so their reconcile pass
// only touches standard-owned rows. Tenant overlays must call
// UpsertFieldMappingGroupsForOwner with a tenant-scoped owner instead of using
// IsCustomized as a pseudo-owner.
func UpsertFieldMappingGroups(ctx context.Context, rawDB *sql.DB, groups []FieldMappingGroup) error {
	return UpsertFieldMappingGroupsForOwner(ctx, rawDB, groups, standardObjectFieldMappingOwner)
}

// UpsertFieldMappingGroupsForOwner is the shared writer used by the platform
// standard-pack loader and tenant overlays to add rows to
// _System_ObjectFieldMapping.
//
// owner is persisted in the Source column and is part of row identity plus
// reconcile scope.
// This is load-bearing for shared tuples such as Lead→Contact(convert): a
// partial tenant overlay can converge tenant-owned rows without soft-deleting
// standard Lead Convert rows owned by Source='standard'.
//
// IsCustomized remains the admin-override signal, not the writer-owner signal.
// Rows with IsCustomized=1 keep their editable payload columns and are skipped
// by reconcile. Source is not a mutable payload column; it names the writer
// owner and participates in uq_ofm_mapping so standard and tenant-owned rows
// cannot steal ownership from each other on key collisions.
//
// Convergent semantics (round-6 reviewer finding #3 fix): deployed state
// converges to the seed on every apply.
//
//  1. Inserts new rows.
//
//  2. Updates existing non-customized rows owned by the same writer — the
//     unique index
//     uq_ofm_mapping(Source, SourceObject, TargetObject, ActionType,
//     SourceField) collides on re-apply; ON DUPLICATE KEY UPDATE overwrites
//     TargetField, DefaultValue, SourceLookupField, SortOrder, IsActive,
//     IsDeleted, LastModifiedDate from the seed values. Before this
//     change the ON DUPLICATE KEY UPDATE was a true no-op and seed
//     edits never deployed to upgraded orgs.
//
//  3. Preserves IsCustomized=1 row payloads. Admin-edited mappings
//     (the IsCustomized flag is the admin-override signal) survive
//     pack upgrades by design — identical semantics to the
//     StandardAction / PageLayout overlays' IsCustomized contract.
//
//  4. Soft-deletes rows that the seed no longer declares, scoped
//     strictly to the writer owner plus (SourceObject, TargetObject,
//     ActionType) tuples present in this call. Owner scoping is
//     load-bearing: a tenant overlay can pass a partial Lead→Contact
//     convert tuple without soft-deleting standard Lead Convert rows
//     owned by the standard pack. Before reconcile, matching non-customized
//     legacy business keys — current mappings plus any retiredSourceFields
//     tombstones — are claimed by this owner so rows removed from a seed do not
//     remain active under Source='legacy'. The per-tuple NOT IN clause uses the
//     tuple's real business key SourceField, not the synthetic Id. This
//     is load-bearing because
//     the unique index is (Source, SourceObject, TargetObject, ActionType,
//     SourceField) while Id also encodes TargetField. When a seed edit changes
//     only TargetField, ON DUPLICATE KEY UPDATE mutates the row in place and
//     the primary key stays old; an Id-based reconcile would soft-delete the
//     freshly updated row on upgraded orgs.
//     Admin-customized rows (IsCustomized=1) are excluded from the
//     reconcile — deleting them would silently erase the admin
//     override.
//
// Empty groups is a legal no-op. Empty mappings within a group means
// the tuple's entire row set should be cleared; the reconcile step
// handles this correctly because the batch contributes zero SourceFields for
// that tuple and every non-customized row is soft-deleted.
func UpsertFieldMappingGroupsForOwner(ctx context.Context, rawDB *sql.DB, groups []FieldMappingGroup, owner string) error {
	owner = strings.TrimSpace(owner)
	if owner == "" {
		return apperrors.NewInternalError("field mapping owner must not be empty", nil)
	}
	if len(owner) > maxObjectFieldMappingOwnerLen {
		return apperrors.NewInternalError(
			fmt.Sprintf("field mapping owner %q exceeds %d characters", owner, maxObjectFieldMappingOwnerLen), nil)
	}

	start := time.Now()
	now := time.Now().Format("2006-01-02 15:04:05")
	if err := claimLegacyFieldMappingRowsForOwner(ctx, rawDB, groups, owner, now); err != nil {
		return err
	}
	if err := deactivateStaleTenantFieldMappingRowsForOwner(ctx, rawDB, groups, owner, now); err != nil {
		return err
	}

	// Build all mapping rows + track per-tuple SourceField keep-sets for
	// the reconcile pass. sourceFieldsByTuple is keyed by
	// (SourceObject, TargetObject, ActionType) joined on NUL so the tuple
	// signature is unambiguous.
	type tupleKey struct {
		SourceObject, TargetObject, ActionType string
	}
	sourceFieldsByTuple := map[tupleKey][]string{}

	var valueParts []string
	var params []interface{}
	sortOrder := 0

	// Pre-populate tuple keys from the input so tuples with empty
	// Mappings still trigger the reconcile pass (deliberate clear).
	for _, group := range groups {
		k := tupleKey{group.SourceObject, group.TargetObject, group.ActionType}
		if _, ok := sourceFieldsByTuple[k]; !ok {
			sourceFieldsByTuple[k] = nil
		}
		for _, m := range group.Mappings {
			id := fieldMappingID(owner, group, m)

			var defaultVal interface{} = nil
			if m.DefaultValue != "" {
				defaultVal = m.DefaultValue
			}
			var sourceLookupVal interface{} = nil
			if m.SourceLookupField != "" {
				sourceLookupVal = m.SourceLookupField
			}

			valueParts = append(valueParts, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?)")
			params = append(params,
				id,
				group.SourceObject,
				group.TargetObject,
				group.ActionType,
				m.SourceField,
				m.TargetField,
				sourceLookupVal,
				defaultVal,
				sortOrder,
				boolToInt(fieldMappingIsCustomized(group, m)),
				owner,
				now, now,
			)
			sourceFieldsByTuple[k] = append(sourceFieldsByTuple[k], m.SourceField)
			sortOrder++
		}
	}

	// ON DUPLICATE KEY UPDATE converges mutable columns for
	// non-customized rows. VALUES() reflects the row being inserted;
	// IF(IsCustomized=1, col, VALUES(col)) preserves admin overrides.
	// IsDeleted resets to 0 so a soft-deleted non-customized row that
	// re-enters the seed is revived — no "zombie IsDeleted=1 stuck
	// past a seed restore" state.
	if len(valueParts) > 0 {
		insertSQL := fmt.Sprintf(
			`INSERT INTO %s
			(Id, SourceObject, TargetObject, ActionType, SourceField, TargetField,
			 SourceLookupField, DefaultValue, SortOrder, IsCustomized, Source, IsActive,
			 IsDeleted, CreatedDate, LastModifiedDate)
			VALUES %s
			ON DUPLICATE KEY UPDATE
			  TargetField       = IF(IsCustomized=1, TargetField,       VALUES(TargetField)),
			  DefaultValue      = IF(IsCustomized=1, DefaultValue,      VALUES(DefaultValue)),
			  SourceLookupField = IF(IsCustomized=1, SourceLookupField, VALUES(SourceLookupField)),
			  SortOrder         = IF(IsCustomized=1, SortOrder,         VALUES(SortOrder)),
			  IsCustomized      = IF(IsCustomized=1, IsCustomized,      VALUES(IsCustomized)),
			  IsActive          = IF(IsCustomized=1, IsActive,          VALUES(IsActive)),
			  IsDeleted         = IF(IsCustomized=1, IsDeleted,         0),
			  LastModifiedDate  = IF(IsCustomized=1, LastModifiedDate,  VALUES(LastModifiedDate))`,
			tableObjectFieldMapping,
			strings.Join(valueParts, ", "),
		)

		result, err := rawDB.ExecContext(ctx, insertSQL, params...)
		if err != nil {
			return apperrors.NewInternalError("failed to seed field mappings", err)
		}
		inserted, _ := result.RowsAffected()
		log.Printf("✅ Field mappings upserted (%d rows affected, %d total, %v)",
			inserted, len(valueParts), time.Since(start).Round(time.Millisecond))
	} else {
		log.Println("   ↩ No field mappings to seed (reconcile-only pass)")
	}

	// Reconcile pass: per-tuple soft-delete for rows not in this batch.
	// Runs even when valueParts is empty so callers can explicitly
	// clear a tuple by passing an empty Mappings list. IsCustomized=0
	// guards admin overrides from accidental deletion. Source scopes the
	// delete to this writer's owner, so tenant overlays cannot delete
	// standard-pack rows for the same tuple.
	for k, sourceFields := range sourceFieldsByTuple {
		if err := reconcileTupleSoftDelete(ctx, rawDB, k.SourceObject, k.TargetObject, k.ActionType, owner, sourceFields, now); err != nil {
			return err
		}
	}
	return nil
}

func claimLegacyFieldMappingRowsForOwner(ctx context.Context, rawDB *sql.DB, groups []FieldMappingGroup, owner string, now string) error {
	clauses, args := fieldMappingBusinessKeyClauses(groups)
	if len(clauses) == 0 || owner == legacyObjectFieldMappingOwner {
		return nil
	}

	query := fmt.Sprintf(
		`UPDATE %s
		 SET Source = ?, LastModifiedDate = ?
		 WHERE Source = ?
		   AND IsCustomized = 0
		   AND (%s)`,
		tableObjectFieldMapping,
		strings.Join(clauses, " OR "),
	)
	execArgs := []interface{}{owner, now, legacyObjectFieldMappingOwner}
	execArgs = append(execArgs, args...)

	result, err := rawDB.ExecContext(ctx, query, execArgs...)
	if err != nil {
		return apperrors.NewInternalError("failed to claim legacy field mapping owner rows", err)
	}
	claimed, _ := result.RowsAffected()
	if claimed > 0 {
		log.Printf("   ↪ Claimed %d legacy field mapping row(s) for owner %s", claimed, owner)
	}
	return nil
}

func deactivateStaleTenantFieldMappingRowsForOwner(ctx context.Context, rawDB *sql.DB, groups []FieldMappingGroup, owner string, now string) error {
	if !strings.HasPrefix(owner, "tenant:") {
		return nil
	}
	clauses, args := fieldMappingTupleClauses(groups)
	if len(clauses) == 0 {
		return nil
	}

	query := fmt.Sprintf(
		`UPDATE %s
		 SET IsDeleted = 1, LastModifiedDate = ?
		 WHERE Source LIKE 'tenant:%%'
		   AND Source <> ?
		   AND IsCustomized = 0
		   AND IsDeleted = 0
		   AND (%s)`,
		tableObjectFieldMapping,
		strings.Join(clauses, " OR "),
	)
	execArgs := []interface{}{now, owner}
	execArgs = append(execArgs, args...)

	result, err := rawDB.ExecContext(ctx, query, execArgs...)
	if err != nil {
		return apperrors.NewInternalError("failed to deactivate stale tenant field mapping owner rows", err)
	}
	deactivated, _ := result.RowsAffected()
	if deactivated > 0 {
		log.Printf("   🧹 Soft-deleted %d stale tenant field mapping row(s) for owner %s", deactivated, owner)
	}
	return nil
}

func fieldMappingTupleClauses(groups []FieldMappingGroup) ([]string, []interface{}) {
	var clauses []string
	var args []interface{}
	seen := map[string]struct{}{}
	for _, group := range groups {
		key := group.SourceObject + "\x00" + group.TargetObject + "\x00" + group.ActionType
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		clauses = append(clauses, "(SourceObject = ? AND TargetObject = ? AND ActionType = ?)")
		args = append(args, group.SourceObject, group.TargetObject, group.ActionType)
	}
	return clauses, args
}

func fieldMappingBusinessKeyClauses(groups []FieldMappingGroup) ([]string, []interface{}) {
	var clauses []string
	var args []interface{}
	for _, group := range groups {
		if len(group.Mappings) == 0 {
			clauses = append(clauses, "(SourceObject = ? AND TargetObject = ? AND ActionType = ?)")
			args = append(args, group.SourceObject, group.TargetObject, group.ActionType)
			continue
		}
		sourceFields := make([]string, 0, len(group.Mappings)+len(group.RetiredSourceFields))
		seen := map[string]bool{}
		for _, mapping := range group.Mappings {
			sourceField := strings.TrimSpace(mapping.SourceField)
			if seen[sourceField] {
				continue
			}
			seen[sourceField] = true
			sourceFields = append(sourceFields, sourceField)
		}
		for _, sourceField := range group.RetiredSourceFields {
			sourceField = strings.TrimSpace(sourceField)
			if sourceField == "" || seen[sourceField] {
				continue
			}
			seen[sourceField] = true
			sourceFields = append(sourceFields, sourceField)
		}
		if len(sourceFields) == 0 {
			continue
		}

		placeholders := strings.TrimRight(strings.Repeat("?,", len(sourceFields)), ",")
		clauses = append(clauses,
			fmt.Sprintf("(SourceObject = ? AND TargetObject = ? AND ActionType = ? AND SourceField IN (%s))", placeholders))
		args = append(args, group.SourceObject, group.TargetObject, group.ActionType)
		for _, sourceField := range sourceFields {
			args = append(args, sourceField)
		}
	}
	return clauses, args
}

func fieldMappingID(owner string, group FieldMappingGroup, mapping FieldMappingDef) string {
	id := fmt.Sprintf("ofm_%s_%s_%s_%s_%s_%s",
		strings.ToLower(owner),
		strings.ToLower(group.SourceObject),
		strings.ToLower(group.TargetObject),
		strings.ToLower(group.ActionType),
		strings.ToLower(mapping.SourceField),
		strings.ToLower(mapping.TargetField),
	)
	// Truncate ID if too long (VARCHAR 255 limit). The unique key still
	// provides deterministic convergence for re-apply.
	if len(id) > 255 {
		return utils.GenerateID()
	}
	return id
}

func fieldMappingIsCustomized(group FieldMappingGroup, mapping FieldMappingDef) bool {
	if mapping.IsCustomized != nil {
		return *mapping.IsCustomized
	}
	return group.IsCustomized
}

// reconcileTupleSoftDelete soft-deletes every non-customized, non-deleted
// row in _System_ObjectFieldMapping for the given tuple whose
// business-key SourceField is not in `keepSourceFields`. Emits zero rows
// when the seed matches the DB exactly, which is the steady-state case.
func reconcileTupleSoftDelete(ctx context.Context, rawDB *sql.DB, sourceObject, targetObject, actionType, owner string, keepSourceFields []string, now string) error {
	// Empty keepSourceFields — the entire tuple's non-customized surface
	// is being cleared. The NOT IN (...) clause can't take an empty list in
	// standard SQL, so drop it and rely on the tuple + customized +
	// deleted filter.
	var query string
	args := []interface{}{now, sourceObject, targetObject, actionType, owner}
	if len(keepSourceFields) == 0 {
		query = fmt.Sprintf(
			`UPDATE %s
			 SET IsDeleted = 1, LastModifiedDate = ?
			 WHERE SourceObject = ? AND TargetObject = ? AND ActionType = ?
			   AND Source = ?
			   AND IsCustomized = 0 AND IsDeleted = 0`,
			tableObjectFieldMapping,
		)
	} else {
		placeholders := strings.Repeat("?,", len(keepSourceFields))
		placeholders = placeholders[:len(placeholders)-1]
		query = fmt.Sprintf(
			`UPDATE %s
			 SET IsDeleted = 1, LastModifiedDate = ?
			 WHERE SourceObject = ? AND TargetObject = ? AND ActionType = ?
			   AND Source = ?
			   AND IsCustomized = 0 AND IsDeleted = 0
			   AND SourceField NOT IN (%s)`,
			tableObjectFieldMapping, placeholders,
		)
		for _, sourceField := range keepSourceFields {
			args = append(args, sourceField)
		}
	}
	res, err := rawDB.ExecContext(ctx, query, args...)
	if err != nil {
		return apperrors.NewInternalError(
			fmt.Sprintf("reconcile soft-delete for %s→%s(%s)", sourceObject, targetObject, actionType), err)
	}
	affected, _ := res.RowsAffected()
	if affected > 0 {
		log.Printf("   🧹 Soft-deleted %d stale row(s) for %s→%s(%s)",
			affected, sourceObject, targetObject, actionType)
	}
	return nil
}

// GetFieldMappings retrieves active field mappings for a given conversion.
// Used by BootstrapFieldMappingApplier during flow-driven recordCreate to
// copy source-record field values into a newly created target record.
func GetFieldMappings(db *sql.DB, sourceObject, targetObject, actionType string) ([]FieldMappingDef, error) {
	query := fmt.Sprintf(
		`SELECT Source, SourceField, TargetField, DefaultValue, SourceLookupField
		 FROM %s
		 WHERE SourceObject = ? AND TargetObject = ? AND ActionType = ?
		   AND IsActive = 1 AND IsDeleted = 0
		 ORDER BY %s, SortOrder ASC, SourceField ASC, TargetField ASC`,
		tableObjectFieldMapping,
		fieldMappingSourcePrecedenceSQL(),
	)

	rows, err := db.Query(query, sourceObject, targetObject, actionType)
	if err != nil {
		return nil, fmt.Errorf("failed to query field mappings: %w", err)
	}
	defer rows.Close()

	var mappings []FieldMappingDef
	seen := map[string]bool{}
	for rows.Next() {
		var m FieldMappingDef
		var source string
		var defaultVal, sourceLookup sql.NullString
		if err := rows.Scan(&source, &m.SourceField, &m.TargetField, &defaultVal, &sourceLookup); err != nil {
			return nil, fmt.Errorf("failed to scan field mapping: %w", err)
		}
		if seen[m.SourceField] {
			continue
		}
		seen[m.SourceField] = true
		if defaultVal.Valid {
			m.DefaultValue = defaultVal.String
		}
		if sourceLookup.Valid {
			m.SourceLookupField = sourceLookup.String
		}
		mappings = append(mappings, m)
	}

	return mappings, nil
}

// GetFieldMappingSourceForTarget returns the active source field that maps to
// a target field for a conversion-style mapping tuple. It shares the same owner
// precedence as GetFieldMappings so standard-action discovery and Flow runtime
// describe the same metadata fact.
func GetFieldMappingSourceForTarget(db *sql.DB, sourceObject, targetObject, actionType, targetField string) (string, error) {
	query := fmt.Sprintf(
		`SELECT SourceField
		 FROM %s
		 WHERE SourceObject = ? AND TargetObject = ? AND ActionType = ?
		   AND TargetField = ?
		   AND IsActive = 1 AND IsDeleted = 0
		 ORDER BY %s, SortOrder ASC, SourceField ASC
		 LIMIT 1`,
		tableObjectFieldMapping,
		fieldMappingSourcePrecedenceSQL(),
	)
	var sourceField string
	err := db.QueryRow(query, sourceObject, targetObject, actionType, targetField).Scan(&sourceField)
	switch {
	case err == sql.ErrNoRows:
		return "", nil
	case err != nil:
		return "", fmt.Errorf("failed to query field mapping source for %s→%s.%s(%s): %w",
			sourceObject, targetObject, targetField, actionType, err)
	default:
		return sourceField, nil
	}
}

func fieldMappingSourcePrecedenceSQL() string {
	// IsCustomized is the admin override signal. Upgraded admin-authored rows
	// may still carry Source='legacy', so runtime readers must prefer them over
	// seed-owned rows instead of relying on owner alone.
	return `CASE
		   WHEN IsCustomized = 1 THEN 0
		   WHEN Source LIKE 'tenant:%' THEN 1
		   WHEN Source = 'standard' THEN 2
		   WHEN Source = 'legacy' THEN 3
		   ELSE 4
		 END, Source ASC`
}

// ApplyFieldMappings applies field mappings from a source record to a target SObject.
// If a source field has a value, it's copied. If not and a DefaultValue exists, the default is used.
//
// Used by Lead Convert and Flow RecordCreate. Semantics: unconditionally
// overwrite target on source hit. For the insert-inheritance applier (fill
// blank only, skip engine-owned, etc.), see
// backend/internal/features/records/insert_inheritance.go — deliberately a
// separate applier consuming the same registry with a different
// ActionType filter.
func ApplyFieldMappings(mappings []FieldMappingDef, source map[string]interface{}, target map[string]interface{}) {
	for _, m := range mappings {
		if v, ok := source[m.SourceField]; ok && v != nil {
			target[m.TargetField] = v
		} else if m.DefaultValue != "" {
			target[m.TargetField] = m.DefaultValue
		}
	}
}

// FieldMappingWithSource carries a FieldMappingDef enriched with its
// SourceObject — needed by the insert-inheritance applier, which looks up
// rules by (targetObject, actionType) and dispatches per SourceObject. The
// existing GetFieldMappings is keyed by (sourceObject, targetObject, actionType)
// because its Lead-Convert consumer already knows the source.
type FieldMappingWithSource struct {
	SourceObject string
	FieldMappingDef
}

// GetFieldMappingsByTarget returns every active row in
// _System_ObjectFieldMapping for (targetObject, actionType), preserving
// SourceObject on each row. Consumers group rows by SourceObject +
// SourceLookupField to decide which parent record to retrieve per group.
func GetFieldMappingsByTarget(db *sql.DB, targetObject, actionType string) ([]FieldMappingWithSource, error) {
	query := fmt.Sprintf(
		`SELECT Source, SourceObject, SourceField, TargetField, DefaultValue, SourceLookupField
		 FROM %s
		 WHERE TargetObject = ? AND ActionType = ?
		   AND IsActive = 1 AND IsDeleted = 0
		 ORDER BY %s, SortOrder ASC, SourceObject ASC, SourceField ASC, TargetField ASC`,
		tableObjectFieldMapping,
		fieldMappingSourcePrecedenceSQL(),
	)

	rows, err := db.Query(query, targetObject, actionType)
	if err != nil {
		return nil, fmt.Errorf("failed to query field mappings by target: %w", err)
	}
	defer rows.Close()

	var mappings []FieldMappingWithSource
	seen := map[string]bool{}
	for rows.Next() {
		var m FieldMappingWithSource
		var source string
		var defaultVal, sourceLookup sql.NullString
		if err := rows.Scan(&source, &m.SourceObject, &m.SourceField, &m.TargetField, &defaultVal, &sourceLookup); err != nil {
			return nil, fmt.Errorf("failed to scan field mapping by target: %w", err)
		}
		key := m.SourceObject + "\x00" + m.SourceField
		if seen[key] {
			continue
		}
		seen[key] = true
		if defaultVal.Valid {
			m.DefaultValue = defaultVal.String
		}
		if sourceLookup.Valid {
			m.SourceLookupField = sourceLookup.String
		}
		mappings = append(mappings, m)
	}

	return mappings, nil
}
