// Package actionvalidate holds shared validation primitives applied to
// action metadata across multiple writer paths. The Phase 2 single-
// runtime consolidation retired the action-level workflow.childCopy
// executor; this package now owns the symmetric authoring-time
// rejection so writers cannot accept a surface the runtime doesn't
// read.
//
// Placing the check here — outside controlplane/packs and outside
// features/metadata — lets every writer share the same primitive
// without circular imports (packs already imports metadata, so metadata
// cannot import packs).
package actionvalidate

import "fmt"

// StrictChildCopyInConfig rejects any authored content at
// Config.workflow.childCopy. Phase 2 retired the action-handler's
// workflow.childCopy executor; child cloning now lives exclusively on
// the object-level _System_Object.ChildCloneRules surface. Accepting
// any authored value at this path — regardless of shape — would
// create a Class-1 contract drift: writers / validators / runtime
// saying different things about the same surface (reviewer finding:
// fake metadata surface).
//
// Content model — what counts as "authored content":
//
//   - Missing key (workflow has no "childCopy" entry)            → OK
//   - nil value (`"childCopy": null`)                             → OK (explicit clear)
//   - Empty array (`"childCopy": []`)                             → OK (explicit clear,
//                                                                  matches overlay deep-merge
//                                                                  wholesale-array-replace)
//   - Non-empty array                                             → REJECT
//   - String / object / number / bool / any other non-empty shape → REJECT (also a
//                                                                  fake-surface violation —
//                                                                  the author is putting
//                                                                  meaning at a retired key)
//
// The non-array-shape rejection closes a gap the Phase 2c reviewer
// flagged: the previous version of this primitive returned nil on a
// failed []interface{} cast, assuming "schema validators elsewhere
// will flag it." Phase 2b deleted those validators as part of
// retiring the surface; with both the runtime AND the schema
// validator silent, a non-array authoring like
// `"childCopy": "bogus"` was being accepted and then dropped on
// decode — the exact fake-surface shape.
//
// Legacy residue in existing DB rows is handled separately:
//   - Runtime ignores it (the struct no longer has a ChildCopy field).
//   - The overlay writer's deep-merge propagates it through without
//     this primitive firing, because overlays submit only their
//     ConfigOverlay delta — this primitive runs on the delta, not on
//     the merged result. Residue is not re-authored, only preserved.
//   - Optional hygiene spec data-specs/2026-04-24-clear-residual-
//     workflow-childcopy.md removes residue on request.
//
// Callers pass the action's Name (for the error message) and the
// ENTITY BEING AUTHORED (e.g. the admin's new Config, or an overlay's
// ConfigOverlay, not the merged result).
func StrictChildCopyInConfig(actionName string, config map[string]interface{}) error {
	if config == nil {
		return nil
	}
	workflow, ok := config["workflow"].(map[string]interface{})
	if !ok {
		return nil
	}
	raw, present := workflow["childCopy"]
	if !present {
		return nil
	}
	if isClearedChildCopyValue(raw) {
		return nil
	}
	return fmt.Errorf(
		"action %q: workflow.childCopy is an authored value (%s) — this surface was retired "+
			"in the Phase 2 single-runtime consolidation (PR #1847). Move the rule to the "+
			"object-level authoring site _System_Object.ChildCloneRules (tenant "+
			"object_child_clone_rules.json / the admin schema edit surface). The runtime no "+
			"longer reads action-level workflow.childCopy, so accepting any content here "+
			"would be a fake metadata surface. Explicit clear lever: set workflow.childCopy "+
			"to null or [].",
		actionName, describeChildCopyShape(raw),
	)
}

// isClearedChildCopyValue returns true for the two values that count
// as "not authored": JSON null (decoded as nil interface) and an
// empty array. Every other shape — including empty strings, empty
// objects, and zero-valued scalars — counts as authored content and
// is rejected. The distinction is deliberate: the author put a key
// at this path; they meant something by it; the surface is retired;
// reject.
func isClearedChildCopyValue(v interface{}) bool {
	if v == nil {
		return true
	}
	if arr, ok := v.([]interface{}); ok {
		return len(arr) == 0
	}
	return false
}

// describeChildCopyShape returns a short human-readable description
// of what the author put at workflow.childCopy, for use in the
// rejection error. Enumerating the shape helps operators see exactly
// which mis-authoring they did — "bogus", [3 entries], {object}, etc.
func describeChildCopyShape(v interface{}) string {
	switch val := v.(type) {
	case nil:
		return "null"
	case []interface{}:
		return fmt.Sprintf("array with %d entrie(s)", len(val))
	case map[string]interface{}:
		return fmt.Sprintf("object with %d key(s)", len(val))
	case string:
		return fmt.Sprintf("string %q", val)
	case bool:
		return fmt.Sprintf("bool %v", val)
	case float64:
		return fmt.Sprintf("number %v", val)
	default:
		return fmt.Sprintf("%T", v)
	}
}
