package packs

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

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

// ValidateObjectChildCloneRules walks every ObjectMetadata's ChildCloneRules
// JSON column and verifies each rule references live metadata (source +
// target objects exist, lookup fields exist and point the right way,
// fieldMappings reference real source/target fields, no dotted paths).
// Returns a joined error listing every violation. Hard-fail posture — new
// metadata surface introduced by the Quote/Order cutover ships with zero
// legacy dirty rows to grandfather, so every violation is a real bug
// (plan §6.5).
//
// Called from platform-config pack Verify; also invokable by
// `cmd/setup verify` so operators can check tenant overlays before a
// rolling deploy.
func ValidateObjectChildCloneRules(ctx context.Context, meta schemaLookup, schemas []*models.ObjectMetadata) error {
	var errs []string
	for _, obj := range schemas {
		if obj == nil || len(obj.ChildCloneRules) == 0 {
			continue
		}
		for i, rule := range obj.ChildCloneRules {
			for _, msg := range validateChildCloneRule(ctx, meta, obj, i, rule) {
				errs = append(errs, msg)
			}
		}
	}
	if len(errs) == 0 {
		return nil
	}
	return fmt.Errorf("ChildCloneRules validation failed:\n  - %s", strings.Join(errs, "\n  - "))
}

func validateChildCloneRule(ctx context.Context, meta schemaLookup, parent *models.ObjectMetadata, idx int, rule models.ChildCloneRule) []string {
	prefix := fmt.Sprintf("%s.ChildCloneRules[%d]", parent.APIName, idx)
	var out []string

	if rule.ParentLookupField == "" {
		out = append(out, fmt.Sprintf("%s: parentLookupField is required", prefix))
	} else if !metadataFieldExists(parent, rule.ParentLookupField) {
		out = append(out, fmt.Sprintf("%s: parentLookupField %q does not exist on %s",
			prefix, rule.ParentLookupField, parent.APIName))
	}

	sourceSchema := meta.GetSchema(ctx, rule.SourceObject)
	if rule.SourceObject == "" {
		out = append(out, fmt.Sprintf("%s: sourceObject is required", prefix))
	} else if sourceSchema == nil {
		out = append(out, fmt.Sprintf("%s: sourceObject %q not found in metadata", prefix, rule.SourceObject))
	}

	if rule.SourceLookupField == "" {
		out = append(out, fmt.Sprintf("%s: sourceLookupField is required", prefix))
	} else if sourceSchema != nil && !metadataFieldExists(sourceSchema, rule.SourceLookupField) {
		out = append(out, fmt.Sprintf("%s: sourceLookupField %q does not exist on sourceObject %q",
			prefix, rule.SourceLookupField, rule.SourceObject))
	}

	// Cross-check: parentLookupField and sourceLookupField should share a
	// referenceTo target (the source parent). Skips cleanly when either
	// lookup has no ReferenceTo declared — some standard lookups don't.
	if rule.ParentLookupField != "" && rule.SourceLookupField != "" && sourceSchema != nil {
		pf := lookupField(parent, rule.ParentLookupField)
		sf := lookupField(sourceSchema, rule.SourceLookupField)
		if pf != nil && sf != nil && len(pf.ReferenceTo) > 0 && len(sf.ReferenceTo) > 0 {
			if !referenceToOverlap(pf.ReferenceTo, sf.ReferenceTo) {
				out = append(out, fmt.Sprintf(
					"%s: parentLookupField %q (referenceTo=%v) and sourceLookupField %q on %s (referenceTo=%v) do not share a referenced object",
					prefix, rule.ParentLookupField, pf.ReferenceTo,
					rule.SourceLookupField, rule.SourceObject, sf.ReferenceTo))
			}
		}
	}

	targetSchema := meta.GetSchema(ctx, rule.TargetObject)
	if rule.TargetObject == "" {
		out = append(out, fmt.Sprintf("%s: targetObject is required", prefix))
	} else if targetSchema == nil {
		out = append(out, fmt.Sprintf("%s: targetObject %q not found in metadata", prefix, rule.TargetObject))
	}

	if rule.TargetLookupField == "" {
		out = append(out, fmt.Sprintf("%s: targetLookupField is required", prefix))
	} else if targetSchema != nil {
		if !metadataFieldExists(targetSchema, rule.TargetLookupField) {
			out = append(out, fmt.Sprintf("%s: targetLookupField %q does not exist on targetObject %q",
				prefix, rule.TargetLookupField, rule.TargetObject))
		} else if tf := lookupField(targetSchema, rule.TargetLookupField); tf != nil && len(tf.ReferenceTo) > 0 {
			if !containsIgnoreCase(tf.ReferenceTo, parent.APIName) {
				out = append(out, fmt.Sprintf("%s: targetLookupField %q on %s does not reference parent object %q (referenceTo=%v)",
					prefix, rule.TargetLookupField, rule.TargetObject, parent.APIName, tf.ReferenceTo))
			}
		}
	}

	if rule.SourceIdField != "" && targetSchema != nil {
		if !metadataFieldExists(targetSchema, rule.SourceIdField) {
			out = append(out, fmt.Sprintf("%s: sourceIdField %q does not exist on targetObject %q",
				prefix, rule.SourceIdField, rule.TargetObject))
		} else {
			// sourceIdField stamps the source row's Id onto the child;
			// runtime's canCloneTargetField drops it silently when the
			// field is engine-owned / system / formula / non-createable.
			// Class-1 closure: validator rejects here so the silent-drop
			// never happens at runtime.
			for _, msg := range checkChildCloneTargetFieldCapability(prefix+".sourceIdField", rule.SourceIdField, rule.TargetLookupField, targetSchema) {
				out = append(out, msg)
			}
		}
	}

	for targetField, sourceField := range rule.FieldMappings {
		if strings.Contains(targetField, ".") {
			out = append(out, fmt.Sprintf("%s.fieldMappings: target key %q contains a dotted path — flat field names only",
				prefix, targetField))
			continue
		}
		if strings.Contains(sourceField, ".") {
			out = append(out, fmt.Sprintf("%s.fieldMappings[%q]: source value %q contains a dotted path — flat field names only",
				prefix, targetField, sourceField))
			continue
		}
		if targetSchema != nil && !metadataFieldExists(targetSchema, targetField) {
			out = append(out, fmt.Sprintf("%s.fieldMappings[%q]: target field does not exist on %q",
				prefix, targetField, rule.TargetObject))
		}
		if sourceSchema != nil && !metadataFieldExists(sourceSchema, sourceField) {
			out = append(out, fmt.Sprintf("%s.fieldMappings[%q]: source field %q does not exist on %q",
				prefix, targetField, sourceField, rule.SourceObject))
		}
		// Class-1 closure: runtime's canCloneTargetField
		// (features/records/child_clone.go) rejects Id / OwnerId /
		// targetLookupField as targets, plus any IsSystem
		// / Formula / !Createable field. Without mirroring those checks
		// here, a seeded rule like {"Id": "Id"} would pass validation
		// and be silently dropped at runtime — the exact contract drift
		// the reviewer flagged in round 4's "whack-a-mole vs root-cause"
		// audit. Engine-owned IS allowed on child-clone (Name copy via
		// autonumber keep_existing — see canCloneTargetField comment).
		if targetSchema != nil {
			for _, msg := range checkChildCloneTargetFieldCapability(prefix+fmt.Sprintf(".fieldMappings[%q]", targetField), targetField, rule.TargetLookupField, targetSchema) {
				out = append(out, msg)
			}
		}
	}

	return out
}

// checkInheritanceTargetFieldCapability mirrors canInheritTarget in
// features/records/insert_inheritance.go. Runtime silently drops
// Id / OwnerId / the rule's own SourceLookupField, plus
// any IsSystem / Formula / !Createable field. Validator must reject
// here so admin authoring never "works in metadata, silently does
// nothing at runtime."
func checkInheritanceTargetFieldCapability(prefix, targetField, sourceLookupField string, targetSchema *models.ObjectMetadata) []string {
	var msgs []string
	if strings.EqualFold(targetField, "Id") || strings.EqualFold(targetField, "OwnerId") {
		msgs = append(msgs, fmt.Sprintf("%s: TargetField %q is on the inheritance blacklist (Id / OwnerId) — runtime silently drops",
			prefix, targetField))
		return msgs
	}
	if sourceLookupField != "" && strings.EqualFold(targetField, sourceLookupField) {
		msgs = append(msgs, fmt.Sprintf("%s: TargetField %q collides with the rule's own SourceLookupField — runtime silently drops to avoid clobbering the FK",
			prefix, targetField))
		return msgs
	}
	for i := range targetSchema.Fields {
		f := &targetSchema.Fields[i]
		if !strings.EqualFold(f.APIName, targetField) {
			continue
		}
		if f.IsSystem {
			msgs = append(msgs, fmt.Sprintf("%s: TargetField %q is IsSystem — runtime silently drops", prefix, targetField))
		}
		if f.Formula != nil || f.Type == constants.FieldTypeFormula {
			// Runtime rejects both shapes — see canInheritTarget in
			// features/records/insert_inheritance.go. Mirror here so a
			// Type=Formula field without a Formula string still fails
			// at validation, not silently at runtime.
			msgs = append(msgs, fmt.Sprintf("%s: TargetField %q is a formula field (Formula set or Type=Formula) — runtime silently drops", prefix, targetField))
		}
		if !f.Createable {
			msgs = append(msgs, fmt.Sprintf("%s: TargetField %q is !Createable — runtime silently drops", prefix, targetField))
		}
		break
	}
	return msgs
}

// checkChildCloneTargetFieldCapability mirrors canCloneTargetField in
// features/records/child_clone.go. Used for both fieldMappings keys and
// sourceIdField. Returns descriptive messages for any violation (caller
// appends them to the aggregate error list).
func checkChildCloneTargetFieldCapability(prefix, targetField, targetLookupField string, targetSchema *models.ObjectMetadata) []string {
	var msgs []string
	// Blacklist set — these are never valid clone targets regardless
	// of schema capability flags. targetLookupField is rejected
	// separately because it's always overwritten by the parent Id
	// stamping step; accepting a mapping that would set it is
	// misleading.
	if strings.EqualFold(targetField, "Id") || strings.EqualFold(targetField, "OwnerId") {
		msgs = append(msgs, fmt.Sprintf("%s: %q is on the child-clone blacklist (Id / OwnerId) — runtime silently drops; move or remove the mapping",
			prefix, targetField))
		return msgs
	}
	if targetLookupField != "" && strings.EqualFold(targetField, targetLookupField) {
		msgs = append(msgs, fmt.Sprintf("%s: target %q collides with targetLookupField — the parent Id stamping overwrites this at runtime; remove the mapping",
			prefix, targetField))
		return msgs
	}
	// Capability checks against schema. Missing field already reported
	// elsewhere; this function only runs when the field exists.
	for i := range targetSchema.Fields {
		f := &targetSchema.Fields[i]
		if !strings.EqualFold(f.APIName, targetField) {
			continue
		}
		if f.IsSystem {
			msgs = append(msgs, fmt.Sprintf("%s: target %q is IsSystem — runtime silently drops",
				prefix, targetField))
		}
		if f.Formula != nil || f.Type == constants.FieldTypeFormula {
			// Runtime rejects both shapes — features/records/child_clone.go:152 /
			// insert_inheritance.go:224. A field with Type=Formula but
			// Formula==nil (unusual but valid metadata) would otherwise
			// pass this validator and be silently dropped at runtime.
			msgs = append(msgs, fmt.Sprintf("%s: target %q is a formula field (Formula set or Type=Formula) — runtime silently drops",
				prefix, targetField))
		}
		if !f.Createable {
			msgs = append(msgs, fmt.Sprintf("%s: target %q is !Createable — runtime silently drops",
				prefix, targetField))
		}
		break
	}
	return msgs
}

// ValidateInsertInheritFromLookupRules loads every _System_ObjectFieldMapping
// row with ActionType='insertInheritFromLookup' and validates each against
// live metadata. Hard-fail — plan §6.5.
//
// Additional constraint (beyond flat-field existence): the TargetField must
// not be in the object's engine-owned set (autonumber-rendered fields). The
// engine-owned set is passed in as a map per object; callers pre-compute
// it so this function stays pure.
func ValidateInsertInheritFromLookupRules(
	ctx context.Context,
	db *sql.DB,
	meta schemaLookup,
	engineOwnedByObject func(objectName string) map[string]bool,
) error {
	if db == nil {
		return nil
	}
	const actionType = "insertInheritFromLookup"
	rows, err := db.Query(`
		SELECT SourceObject, TargetObject, SourceField, TargetField, DefaultValue, SourceLookupField
		FROM _System_ObjectFieldMapping
		WHERE ActionType = ? AND IsActive = 1 AND IsDeleted = 0`, actionType)
	if err != nil {
		return fmt.Errorf("load insertInheritFromLookup rules for validation: %w", err)
	}
	defer rows.Close()

	type ruleRow struct {
		SourceObject, TargetObject, SourceField, TargetField string
		DefaultValue, SourceLookupField                      sql.NullString
	}
	var all []ruleRow
	for rows.Next() {
		var r ruleRow
		if err := rows.Scan(&r.SourceObject, &r.TargetObject, &r.SourceField, &r.TargetField, &r.DefaultValue, &r.SourceLookupField); err != nil {
			return fmt.Errorf("scan insertInheritFromLookup row: %w", err)
		}
		all = append(all, r)
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate insertInheritFromLookup rows: %w", err)
	}

	var errs []string
	for _, r := range all {
		prefix := fmt.Sprintf("insertInheritFromLookup(%s→%s,%q→%q)",
			r.SourceObject, r.TargetObject, r.SourceField, r.TargetField)

		if r.TargetObject == "" {
			errs = append(errs, fmt.Sprintf("%s: TargetObject is empty", prefix))
			continue
		}
		if r.SourceObject == "" {
			errs = append(errs, fmt.Sprintf("%s: SourceObject is empty", prefix))
			continue
		}
		if !r.SourceLookupField.Valid || r.SourceLookupField.String == "" {
			errs = append(errs, fmt.Sprintf("%s: SourceLookupField is required for this ActionType", prefix))
			continue
		}
		sourceLookup := r.SourceLookupField.String
		if strings.Contains(sourceLookup, ".") {
			errs = append(errs, fmt.Sprintf("%s: SourceLookupField %q contains a dotted path — flat field names only", prefix, sourceLookup))
			continue
		}
		if strings.Contains(r.SourceField, ".") {
			errs = append(errs, fmt.Sprintf("%s: SourceField %q contains a dotted path — flat field names only", prefix, r.SourceField))
			continue
		}
		if strings.Contains(r.TargetField, ".") {
			errs = append(errs, fmt.Sprintf("%s: TargetField %q contains a dotted path — flat field names only", prefix, r.TargetField))
			continue
		}

		targetSchema := meta.GetSchema(ctx, r.TargetObject)
		sourceSchema := meta.GetSchema(ctx, r.SourceObject)
		if targetSchema == nil {
			errs = append(errs, fmt.Sprintf("%s: targetObject not found in metadata", prefix))
			continue
		}
		if sourceSchema == nil {
			errs = append(errs, fmt.Sprintf("%s: sourceObject not found in metadata", prefix))
			continue
		}

		// TargetField must always exist on target (even for constant-default rows).
		if r.TargetField == "" {
			errs = append(errs, fmt.Sprintf("%s: TargetField is empty", prefix))
		} else if !metadataFieldExists(targetSchema, r.TargetField) {
			errs = append(errs, fmt.Sprintf("%s: TargetField does not exist on %q", prefix, r.TargetObject))
		} else {
			// Mirror canInheritTarget's runtime rejections —
			// features/records/insert_inheritance.go. Blacklist fields
			// and capability flags are silently dropped at runtime;
			// validator must reject to prevent the authoring/runtime
			// drift the reviewer called out in round 4's whack-a-mole
			// audit. Engine-owned is checked separately below because
			// the engineOwnedByObject parameter is optional.
			for _, msg := range checkInheritanceTargetFieldCapability(prefix, r.TargetField, sourceLookup, targetSchema) {
				errs = append(errs, msg)
			}
		}

		// SourceLookupField must exist on target, point at source.
		if !metadataFieldExists(targetSchema, sourceLookup) {
			errs = append(errs, fmt.Sprintf("%s: SourceLookupField %q does not exist on target %q", prefix, sourceLookup, r.TargetObject))
		} else if lf := lookupField(targetSchema, sourceLookup); lf != nil && len(lf.ReferenceTo) > 0 {
			if !containsIgnoreCase(lf.ReferenceTo, r.SourceObject) {
				errs = append(errs, fmt.Sprintf("%s: SourceLookupField %q does not reference sourceObject (referenceTo=%v)",
					prefix, sourceLookup, lf.ReferenceTo))
			}
		}

		// SourceField (when non-empty) must exist on source.
		if r.SourceField != "" && !metadataFieldExists(sourceSchema, r.SourceField) {
			errs = append(errs, fmt.Sprintf("%s: SourceField does not exist on %q", prefix, r.SourceObject))
		}

		// Engine-owned TargetField is forbidden (fail at validation, not runtime).
		if engineOwnedByObject != nil {
			owned := engineOwnedByObject(r.TargetObject)
			if owned != nil && r.TargetField != "" && owned[strings.ToLower(r.TargetField)] {
				errs = append(errs, fmt.Sprintf("%s: TargetField is engine-owned (autonumber/formula-rendered) and cannot be inheritance target",
					prefix))
			}
		}
	}

	if len(errs) == 0 {
		return nil
	}
	return fmt.Errorf("insertInheritFromLookup validation failed:\n  - %s", strings.Join(errs, "\n  - "))
}

func lookupField(schema *models.ObjectMetadata, apiName string) *models.FieldMetadata {
	if schema == nil {
		return nil
	}
	for i := range schema.Fields {
		if strings.EqualFold(schema.Fields[i].APIName, apiName) {
			return &schema.Fields[i]
		}
	}
	return nil
}

func containsIgnoreCase(hay []string, needle string) bool {
	for _, h := range hay {
		if strings.EqualFold(h, needle) {
			return true
		}
	}
	return false
}

func referenceToOverlap(a, b []string) bool {
	for _, x := range a {
		if containsIgnoreCase(b, x) {
			return true
		}
	}
	return false
}
