package models

import "strings"

// CloneWithAdditionalFields returns a shallow copy of schema whose Fields
// slice contains the original fields with the supplied additions
// appended; when an addition collides with an existing field (case-
// insensitive APIName match), the addition REPLACES the original entry
// both in the Fields slice and in the fresh FieldsMap. Safe to call on
// a cached/shared schema pointer — the caller receives a schema whose
// Fields and FieldsMap mutations are isolated from the source.
//
// Nested pointer/slice/map values on FieldMetadata entries (Options,
// ReferenceTo, PicklistDependency, etc.) are shared between the source
// and the clone — callers must treat them as immutable. This matches
// the invariant every downstream consumer already relies on for cached
// metadata. If a future caller needs to mutate a nested field they must
// deep-copy that field themselves.
//
// Contract used by schema decorators:
//   - schema must be non-nil; panics on nil to surface misuse early.
//   - additions may be nil or empty; the result is still a clone so
//     decorators can always return `CloneWithAdditionalFields(s, nil)`
//     for the no-op case without branching at the call site.
//   - Duplicate APIName collisions between existing Fields and additions
//     are resolved in favor of the addition: the collided existing entry
//     is dropped from the slice AND from FieldsMap so downstream
//     consumers that iterate schema.Fields cannot observe the legacy
//     value. Without this dedupe, describe handlers would emit the same
//     field twice on upgraded clusters whose legacy _System_ObjectField
//     rows still describe fields the decorator is now synthesizing.
//   - Case comparison is lowercased — matches every other FieldsMap
//     lookup in the platform. A schema whose Fields slice legitimately
//     contains two entries under different casing (not observed in the
//     current schema.go / metadata_schema loader, but worth being
//     explicit about) would collapse to one here.
func CloneWithAdditionalFields(schema *ObjectMetadata, additions []FieldMetadata) *ObjectMetadata {
	if schema == nil {
		panic("models.CloneWithAdditionalFields: nil schema")
	}
	out := *schema

	replacedKeys := make(map[string]struct{}, len(additions))
	for i := range additions {
		replacedKeys[strings.ToLower(additions[i].APIName)] = struct{}{}
	}

	combined := make([]FieldMetadata, 0, len(schema.Fields)+len(additions))
	for i := range schema.Fields {
		if _, collides := replacedKeys[strings.ToLower(schema.Fields[i].APIName)]; collides {
			continue
		}
		combined = append(combined, schema.Fields[i])
	}
	combined = append(combined, additions...)
	out.Fields = combined

	fmap := make(map[string]*FieldMetadata, len(combined))
	for i := range combined {
		fmap[strings.ToLower(combined[i].APIName)] = &combined[i]
	}
	out.FieldsMap = fmap
	return &out
}
