package records

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// childCloneMaxRecords bounds the number of source rows a single child-clone
// rule may copy per parent insert. A ceiling exists purely to catch
// run-away tenant configurations; real Opportunity→Quote / Quote→Order
// product sets are single-digit to low-hundreds.
const childCloneMaxRecords = 500

// BuildChildCloneTarget assembles the target SObject for one source row
// under a ChildCloneRule-shaped spec. The action-handler's previous
// workflow.childCopy executor has been retired (Phase 2 single-runtime
// consolidation); this remains the sole authority for post-insert child
// cloning.
//
// Semantics (matches the deleted buildOrderProductClonePayload /
// buildQuoteProductClonePayload — parity gate: child_clone_parity_test.go):
//
//  1. Every key in fieldMappings is a flat target-field name; its value is a
//     flat source-field name. No dotted paths. Callers that wanted one-hop
//     resolution on the legacy action-level surface apply it themselves
//     (via applyFieldMapping) and are out of scope for this helper.
//  2. For each mapping, copy sourceRow[sourceField] to newChild[targetField]
//     only when the source value is non-blank (matches old copyField
//     helper). Blank-source keys are dropped.
//  3. targetLookupField is stamped as newChild[targetLookupField] =
//     targetParentID (always set, even if the FK key is also present in
//     fieldMappings — the parent Id wins).
//  4. sourceIdField, when non-empty, is stamped as
//     newChild[sourceIdField] = sourceRow.Id — the Order side's
//     Quote_Product__c back-reference. Skipped when the source row has
//     no Id (shouldn't happen in practice).
//  5. Target-schema capability checks: a target field is silently dropped
//     if it's missing from schema (unknown), marked non-createable,
//     system, formula, or lives in engineOwnedSet. Matches
//     canInheritTarget's target-field gating — so Name never overrides the
//     autonumber renderer even if a tenant accidentally includes "Name"
//     in fieldMappings.
//
// Returns an empty map when no field landed (caller can skip the Insert).
func BuildChildCloneTarget(
	sourceRow models.SObject,
	targetParentID string,
	targetLookupField string,
	sourceIdField string,
	fieldMappings map[string]string,
	targetSchema *models.ObjectMetadata,
	engineOwnedSet map[string]bool,
) models.SObject {
	if targetLookupField == "" || targetParentID == "" {
		return nil
	}

	targetFields := indexSchemaFields(targetSchema)

	newChild := models.SObject{
		targetLookupField: targetParentID,
	}

	// Back-reference FK (Order-side Quote_Product__c).
	if sourceIdField != "" {
		if sourceID, _ := sourceRow[constants.FieldID].(string); sourceID != "" {
			// Stamp even if the target field is missing from schema? No —
			// honor the same capability filter so a typo doesn't silently
			// write onto a surprise field. Validator rejects missing
			// target fields at bootstrap.
			if canCloneTargetField(sourceIdField, targetLookupField, targetFields, engineOwnedSet) {
				newChild[sourceIdField] = sourceID
			}
		}
	}

	for targetField, sourceField := range fieldMappings {
		if targetField == "" || sourceField == "" {
			continue
		}
		if !canCloneTargetField(targetField, targetLookupField, targetFields, engineOwnedSet) {
			continue
		}
		v, ok := sourceRow[sourceField]
		if !ok || isBlankInheritValue(v) {
			continue
		}
		newChild[targetField] = v
	}

	// Only the targetLookupField landed → nothing to copy, caller skips.
	if len(newChild) <= 1 {
		return nil
	}
	return newChild
}

// canCloneTargetField mirrors canInheritTarget's target-field gating with
// two deliberate differences:
//
//   - The blacklist does NOT include the rule's own back-reference
//     (sourceIdField can legitimately target an Id-holding lookup column).
//   - Engine-owned fields are NOT rejected. Child-clone's target is a
//     freshly inserted child record, so its own autonumber engine runs
//     under keep_existing semantics (PRD §3.3): a non-blank Name arriving
//     via fieldMappings correctly suppresses the autonumber rendering,
//     and a blank Name lets the engine fire. This preserves the
//     deleted buildQuoteProductClonePayload behaviour where Name was
//     copied from OpportunityLineItem to quote_product__c; dropping
//     engine-owned here would silently flip child.Name to QuotePro-NNN
//     whenever a seed lists Name — a behaviour narrowing the parity
//     test (with engineOwned=nil) would not catch. Inheritance is
//     different: it mutates the parent-being-inserted before that
//     parent's own autonumber render, so engine-owned targets must
//     still be rejected there — see canInheritTarget.
//
// Id / OwnerId are still rejected; system / formula /
// non-createable targets still fail.
//
// The `engineOwned` parameter is retained for symmetry with
// canInheritTarget and for possible future use; callers SHOULD pass nil
// for child-clone. Non-nil values are ignored by design.
func canCloneTargetField(
	targetField, targetLookupField string,
	targetFields map[string]*models.FieldMetadata,
	_ map[string]bool,
) bool {
	if targetField == "" {
		return false
	}
	switch {
	case strings.EqualFold(targetField, constants.FieldID),
		strings.EqualFold(targetField, constants.FieldOwnerID),
		strings.EqualFold(targetField, targetLookupField):
		return false
	}
	lc := strings.ToLower(targetField)
	fm, ok := targetFields[lc]
	if !ok || fm == nil {
		return false
	}
	if fm.IsSystem {
		return false
	}
	if fm.Formula != nil || fm.Type == constants.FieldTypeFormula {
		return false
	}
	if !fm.Createable {
		return false
	}
	return true
}

func indexSchemaFields(schema *models.ObjectMetadata) map[string]*models.FieldMetadata {
	if schema == nil {
		return nil
	}
	out := make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		f := &schema.Fields[i]
		out[strings.ToLower(f.APIName)] = f
	}
	return out
}

// applyChildCloneRules is the persistence-layer executor: iterates the
// rules declared on the target schema's ChildCloneRules column, resolves
// each rule's source parent via parentLookupField on the new record, loads
// source children via repo.GetChildren (Id-only), optionally hydrates via
// Retrieve so formula/computed fields are populated before copy, and
// inserts each clone through ps.Insert so post-save hooks fire (outbox,
// rollup, validators).
//
// Fires post-repo.Insert inside the caller's transaction — so any clone
// failure aborts the parent insert. Matches the atomicity guarantee of
// the deleted hardcoded Quote→Order clone (persistence_insert.go:219-241).
//
// No-op when the target schema carries zero rules (greenfield) or when
// the new record's parentLookupField resolves to a blank value (user
// created a parent without linking it to a source).
func (ps *PersistenceService) applyChildCloneRules(
	ctx context.Context,
	schema *models.ObjectMetadata,
	newRecord models.SObject,
	currentUser *models.UserSession,
) error {
	if schema == nil || len(schema.ChildCloneRules) == 0 {
		return nil
	}
	newRecordID, _ := newRecord[constants.FieldID].(string)
	if newRecordID == "" {
		return nil
	}

	for _, rule := range schema.ChildCloneRules {
		if err := ps.runChildCloneRule(ctx, rule, newRecord, newRecordID, currentUser); err != nil {
			return err
		}
	}
	return nil
}

func (ps *PersistenceService) runChildCloneRule(
	ctx context.Context,
	rule models.ChildCloneRule,
	newRecord models.SObject,
	newRecordID string,
	currentUser *models.UserSession,
) error {
	if rule.ParentLookupField == "" || rule.SourceObject == "" || rule.SourceLookupField == "" ||
		rule.TargetObject == "" || rule.TargetLookupField == "" {
		// Malformed rule. Validator rejects these at bootstrap, but a
		// runtime no-op is the right posture for defense-in-depth.
		return nil
	}

	sourceParentID := inheritLookupString(newRecord[rule.ParentLookupField])
	if sourceParentID == "" {
		// New parent has no source-parent FK set → nothing to clone. This
		// is the greenfield-path / direct-POST-without-source path.
		return nil
	}

	sourceStubs, err := ps.repo.GetChildren(ctx, rule.SourceObject, rule.SourceLookupField, sourceParentID)
	if err != nil {
		return fmt.Errorf("child-clone load %s.%s=%q: %w",
			rule.SourceObject, rule.SourceLookupField, sourceParentID, err)
	}
	if len(sourceStubs) == 0 {
		return nil
	}
	if len(sourceStubs) > childCloneMaxRecords {
		return fmt.Errorf("child-clone rule %s→%s loaded %d rows, exceeds cap %d — aborting rather than silently truncate",
			rule.SourceObject, rule.TargetObject, len(sourceStubs), childCloneMaxRecords)
	}

	targetSchema, _ := ps.GetObjectSchema(ctx, rule.TargetObject)
	if targetSchema == nil {
		// Validator catches this at bootstrap; defensive runtime skip.
		return nil
	}

	for _, stub := range sourceStubs {
		source := stub
		if rule.HydrateSource {
			sourceID, _ := stub[constants.FieldID].(string)
			if sourceID != "" {
				if hydrated, retrieveErr := ps.Retrieve(ctx, rule.SourceObject, sourceID, currentUser); retrieveErr == nil && len(hydrated) > 0 {
					source = hydrated
				}
			}
		}
		newChild := BuildChildCloneTarget(
			source,
			newRecordID,
			rule.TargetLookupField,
			rule.SourceIdField,
			rule.FieldMappings,
			targetSchema,
			nil, // engineOwned intentionally unused: see canCloneTargetField rationale
		)
		if len(newChild) == 0 {
			continue
		}
		if _, err := ps.Insert(ctx, rule.TargetObject, newChild, currentUser); err != nil {
			return fmt.Errorf("child-clone insert %s from %s: %w",
				rule.TargetObject, rule.SourceObject, err)
		}
	}

	return nil
}
