package registry

import (
	"bytes"
	"encoding/json"

	"github.com/tidbcloud/AgentTeam/internal/extension/spec"
)

// cloneRegistered returns a fully detached deep copy of one cached entry's
// immutable Definition/Binding projection so a caller mutating a returned
// RegisteredExtension (its Definition, Binding, nested schemas, slices, maps)
// cannot affect cached registry state or subsequent reads.
//
// The Executor is intentionally NOT part of this clone. Executors are
// caller-supplied behavior that cannot be meaningfully deep-cloned; they are
// reached only via BuiltinExecutorFor (a separate, by-reference lookup).
// Keeping the executor out of the clone-on-read return upholds the contract
// that EVERY value returned by Lookup*/List is independent of cached state.
func cloneRegistered(e *registeredEntry) RegisteredExtension {
	return RegisteredExtension{
		Kind:             e.kind,
		ConnectionPolicy: e.connectionPolicy,
		Definition:       cloneDefinition(e.definition),
		Binding:          cloneExternalBinding(e.binding),
	}
}

// cloneDefinition returns a fully detached deep copy of def. Nested schemas and
// examples are deep-copied so a caller cannot mutate cached state through any
// reachable reference.
func cloneDefinition(def Definition) Definition {
	cp := def
	cp.Actions = cloneDefinitionActions(def.Actions)
	return cp
}

func cloneDefinitionActions(actions []DefinitionAction) []DefinitionAction {
	if actions == nil {
		return nil
	}
	out := make([]DefinitionAction, len(actions))
	for i, a := range actions {
		out[i] = cloneDefinitionAction(a)
	}
	return out
}

func cloneDefinitionAction(a DefinitionAction) DefinitionAction {
	cp := a
	cp.RequiredScopes = append([]string(nil), a.RequiredScopes...)
	cp.InputSchema = cloneSchema(a.InputSchema)
	cp.OutputSchema = cloneSchema(a.OutputSchema)
	cp.Example = cloneValue(a.Example)
	return cp
}

// cloneExternalBinding returns a fully detached deep copy of the kind-specific
// HTTP binding. It is the zero value for a Builtin (no binding), which clones
// safely to another zero value.
func cloneExternalBinding(b ExternalBinding) ExternalBinding {
	cp := b
	if len(b.Auth.Profiles) > 0 {
		cp.Auth.Profiles = make([]spec.AuthProfile, len(b.Auth.Profiles))
		copy(cp.Auth.Profiles, b.Auth.Profiles)
	}
	cp.Actions = cloneExternalBindingActions(b.Actions)
	return cp
}

func cloneExternalBindingActions(actions []ExternalBindingAction) []ExternalBindingAction {
	if actions == nil {
		return nil
	}
	out := make([]ExternalBindingAction, len(actions))
	for i, a := range actions {
		out[i] = ExternalBindingAction{
			Name:    a.Name,
			Auth:    cloneActionAuth(a.Auth),
			Binding: cloneBinding(a.Binding),
		}
	}
	return out
}

func cloneActionAuth(auth *spec.ActionAuthRequirement) *spec.ActionAuthRequirement {
	if auth == nil {
		return nil
	}
	cp := *auth
	if len(auth.RequiredScopes) > 0 {
		cp.RequiredScopes = append([]string(nil), auth.RequiredScopes...)
	}
	return &cp
}

// cloneBinding deep-copies an HTTP binding. Nested static-headers, parameters,
// body mappings, and response sets are detached from the source.
func cloneBinding(b spec.Binding) spec.Binding {
	cb := b
	cb.StaticHeaders = cloneStringStringMap(b.StaticHeaders)
	if len(b.Parameters) > 0 {
		cb.Parameters = make([]spec.Parameter, len(b.Parameters))
		copy(cb.Parameters, b.Parameters)
	}
	cb.Body = cloneBody(b.Body)
	cb.Response = cloneResponse(b.Response)
	return cb
}

func cloneBody(body *spec.Body) *spec.Body {
	if body == nil {
		return nil
	}
	cp := *body
	if len(body.Mappings) > 0 {
		cp.Mappings = make([]spec.BodyMapping, len(body.Mappings))
		copy(cp.Mappings, body.Mappings)
	}
	return &cp
}

func cloneResponse(r spec.Response) spec.Response {
	cr := r
	if len(r.SuccessStatuses) > 0 {
		cr.SuccessStatuses = append([]int(nil), r.SuccessStatuses...)
	}
	if len(r.ContentTypes) > 0 {
		cr.ContentTypes = append([]string(nil), r.ContentTypes...)
	}
	return cr
}

func cloneStringStringMap(m map[string]string) map[string]string {
	if m == nil {
		return nil
	}
	out := make(map[string]string, len(m))
	for k, v := range m {
		out[k] = v
	}
	return out
}

// cloneSchema deep-copies a JSON schema map. Schemas nest map[string]any and
// []any arbitrarily; cloneValue walks them so a returned schema is fully
// detached from cached state. A nil map stays nil.
func cloneSchema(m map[string]any) map[string]any {
	if m == nil {
		return nil
	}
	out := make(map[string]any, len(m))
	for k, v := range m {
		out[k] = cloneValue(v)
	}
	return out
}

// cloneValue returns a deep copy of an arbitrary JSON-derived value. It recurses
// into every container type that can reach a Definition catalog: map[string]any
// and []any (the canonical encoding/json / YAML shapes) PLUS the typed slices
// and maps a hand-constructed package may carry ([]string, []int, []float64,
// []bool, map[string]string). This is defense-in-depth: at registration every
// Example is also normalized to the canonical tree (see detachedJSONValue), but
// cloning defensively closes the aliasing window for any typed slice that
// reaches a cached entry by another path.
//
// Scalar Go values (string, int, float64, bool, ...) are immutable and are
// returned unchanged. A nil value stays nil.
func cloneValue(v any) any {
	switch val := v.(type) {
	case nil:
		return nil
	case map[string]any:
		out := make(map[string]any, len(val))
		for k, sub := range val {
			out[k] = cloneValue(sub)
		}
		return out
	case []any:
		out := make([]any, len(val))
		for i, sub := range val {
			out[i] = cloneValue(sub)
		}
		return out
	case []string:
		out := append([]string(nil), val...)
		return out
	case []int:
		out := append([]int(nil), val...)
		return out
	case []float64:
		out := append([]float64(nil), val...)
		return out
	case []bool:
		out := append([]bool(nil), val...)
		return out
	case map[string]string:
		out := make(map[string]string, len(val))
		for k, sub := range val {
			out[k] = sub
		}
		return out
	default:
		// Scalars (string, int, float64, bool, ...) are immutable; structs or
		// other reference types do not occur in a valid Definition catalog.
		return v
	}
}

// detachedJSONValue normalizes an arbitrary value into the deterministic JSON tree
// (map[string]any, []any, and scalar leaves) by round-tripping through
// encoding/json. This guarantees every Example stored in a cached Definition is
// a JSON-derived value whose only container types are map[string]any / []any,
// so cloneValue's recursion fully detaches it on read regardless of the typed
// slice a caller may have supplied (e.g. []string) at registration. A nil value
// stays nil. A value that is not JSON-encodable (which cannot occur in a valid
// catalog) is returned unchanged.
//
// The decode uses json.Decoder.UseNumber so JSON numbers unmarshal as
// json.Number (a string-backed type) instead of float64. float64 silently
// rounds integers beyond 2^53, which would COLLIDE the Builtin runtime digest
// of distinct Agent-visible example values (e.g. 9007199254740992 vs
// 9007199254740993 both round to the same float64). json.Number preserves the
// exact numeric text losslessly and is a leaf in the canonical tree (marshaled
// verbatim), so this MUST mirror the internal/extensions/spec detachedJSONValue: both
// paths share one lossless numeric model so an External digest and a Builtin
// digest never diverge on large-integer examples, and for integers within
// float64's exact range the canonical bytes are unchanged.
func detachedJSONValue(v any) any {
	if v == nil {
		return nil
	}
	b, err := json.Marshal(v)
	if err != nil {
		return v
	}
	dec := json.NewDecoder(bytes.NewReader(b))
	dec.UseNumber()
	var out any
	if err := dec.Decode(&out); err != nil {
		return v
	}
	return out
}
