package connector

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
)

// 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. Two consumers read this column:
//
//  1. The `insertInheritFromLookup` ActionType's applier in features/records
//     (insert_inheritance.go) — the original consumer; copies fields from the
//     parent record at insert time. Reads rows scoped to that ActionType.
//
//  2. The create-from-source bypass guard in features/records / bootstrap
//     (create_from_source_guard.go) introduced for issue #1234 — uses the
//     column ActionType-agnostically to identify which (TargetObject,
//     SourceFKField) positions are owned by a configured standard action's
//     Flow fan-out. Setting `SourceLookupField` on non-`insertInheritFromLookup`
//     rows (e.g. `createQuote`, `createOrder`) is safe: the
//     `insertInheritFromLookup` applier scopes by ActionType so non-matching
//     rows are not processed by it.
//
// Legacy consumers (Lead Convert, Flow RecordCreate via ApplyFieldMappings
// / GetFieldMappings) 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"`
}

// FieldMappingDefV2 adds the authored value-translation policy without
// changing the published FieldMappingDef shape. FieldMappingDef has external
// unkeyed-literal consumers, so additions belong in this versioned envelope.
type FieldMappingDefV2 struct {
	FormatVersion   uint32
	Mapping         FieldMappingDef
	ValueMap        string `json:"valueMap,omitempty"`
	OnUnmappedValue string `json:"onUnmappedValue,omitempty"`
}

// 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
		}
	}
}

// ApplyFieldMappingsV2 applies the same copy/default behavior plus the V2
// value-translation policy. All writes are staged so invalid authored policy
// cannot partially mutate the target.
func ApplyFieldMappingsV2(mappings []FieldMappingDefV2, source map[string]interface{}, target map[string]interface{}) error {
	pending := make(map[string]interface{}, len(mappings))
	for _, m := range mappings {
		if m.FormatVersion != 2 {
			return fmt.Errorf("field mapping format version %d is not supported", m.FormatVersion)
		}
		mapping := m.Mapping
		var value interface{}
		hasValue := false
		if v, ok := source[mapping.SourceField]; ok && v != nil {
			value, hasValue = v, true
		} else if mapping.DefaultValue != "" {
			value, hasValue = mapping.DefaultValue, true
		}
		if !hasValue {
			continue
		}
		if strings.TrimSpace(m.ValueMap) != "" {
			translated, include, _, err := TranslateMappedValue(m.ValueMap, m.OnUnmappedValue, value)
			if err != nil {
				return fmt.Errorf("field mapping %s to %s: %w", mapping.SourceField, mapping.TargetField, err)
			}
			if !include {
				continue
			}
			value = translated
		}
		pending[mapping.TargetField] = value
	}
	for field, value := range pending {
		target[field] = value
	}
	return nil
}

// TranslateMappedValue applies the neutral ValueMap / OnUnmappedValue policy
// shared by connector and legacy-direction field mappings. include=false means
// the caller must omit the target field, allowing the next defaulting stage to
// remain its sole owner. matched distinguishes an authored translation from a
// passthrough so callers that model explicit blank clears preserve that fact.
func TranslateMappedValue(rawValueMap, onUnmappedValue string, value interface{}) (translated interface{}, include bool, matched bool, err error) {
	policy := strings.TrimSpace(onUnmappedValue)
	switch policy {
	case "", OnUnmappedValueError, OnUnmappedValueSkip, OnUnmappedValuePassthrough:
	default:
		return nil, false, false, fmt.Errorf("OnUnmappedValue %q is not valid", onUnmappedValue)
	}
	if strings.TrimSpace(rawValueMap) == "" {
		return value, true, false, nil
	}
	valueMap, err := ParseConnectorValueMap(rawValueMap)
	if err != nil {
		return nil, false, false, fmt.Errorf("ValueMap is invalid: %w", err)
	}
	key, scalar, keyEligible := mappedValueSourceKey(value)
	if !scalar {
		return nil, false, false, fmt.Errorf("ValueMap received non-scalar source value %T", value)
	}
	if keyEligible {
		if translated, found := valueMap[key]; found {
			return mappedValueTargetValue(translated), true, true, nil
		}
	}
	switch policy {
	case OnUnmappedValueSkip:
		return nil, false, false, nil
	case OnUnmappedValuePassthrough:
		return value, true, false, nil
	case "", OnUnmappedValueError:
		return nil, false, false, fmt.Errorf("ValueMap has no translation for source value %q", key)
	}
	return nil, false, false, fmt.Errorf("OnUnmappedValue %q is not valid", onUnmappedValue)
}

func mappedValueSourceKey(value interface{}) (key string, scalar bool, keyEligible bool) {
	switch value := value.(type) {
	case string:
		key := strings.TrimSpace(value)
		return key, true, key != ""
	case bool:
		return strconv.FormatBool(value), true, true
	case json.Number:
		return value.String(), true, value.String() != ""
	case int:
		return strconv.Itoa(value), true, true
	case int8:
		return strconv.FormatInt(int64(value), 10), true, true
	case int16:
		return strconv.FormatInt(int64(value), 10), true, true
	case int32:
		return strconv.FormatInt(int64(value), 10), true, true
	case int64:
		return strconv.FormatInt(value, 10), true, true
	case uint:
		return strconv.FormatUint(uint64(value), 10), true, true
	case uint8:
		return strconv.FormatUint(uint64(value), 10), true, true
	case uint16:
		return strconv.FormatUint(uint64(value), 10), true, true
	case uint32:
		return strconv.FormatUint(uint64(value), 10), true, true
	case uint64:
		return strconv.FormatUint(value, 10), true, true
	case float32:
		return strconv.FormatFloat(float64(value), 'f', -1, 32), true, true
	case float64:
		return strconv.FormatFloat(value, 'f', -1, 64), true, true
	default:
		return "", false, false
	}
}

func mappedValueTargetValue(value interface{}) interface{} {
	// json.Number preserves the exact authored decimal text. Converting it to
	// float64 here would round values before the target field's schema-aware
	// write path has a chance to validate and persist them exactly.
	return value
}

// 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
}
