package spec

import (
	"fmt"
	"sort"
)

// NormalizeDefinition returns a canonical, fully-detached copy of pkg in which
// every order-independent set and JSON container is in deterministic form.
//
// NormalizeDefinition is a pure canonicalization that SORTS ONLY — it never
// removes elements. Sorting is order-invariant (the semantic-digest projection
// applies the same ordering transforms), so NormalizeDefinition is
// digest-preserving: a package and its normalized form always share the same
// DefinitionDigest. De-duplication is deliberately NOT performed here; it is the
// VALIDATOR's job (ValidateDefinition/ValidateCatalog reject duplicate scopes,
// statuses, and content-types), so a normalizer that removed elements would be
// a normalizer that changes the digest of input it has not validated.
//
// Canonicalized sets and containers:
//   - actions are sorted by name;
//   - auth profiles are sorted by ID;
//   - per-action required_scopes, response success_statuses, and response
//     content_types are sorted (NOT de-duplicated; see above);
//   - input/output schemas and examples are normalized to the deterministic JSON
//     tree (map[string]any / []any / scalar leaves) via a JSON round-trip, which
//     fully detaches them from the caller's input including typed Go containers
//     (map[string]string, []string, ...) a programmatic registration may supply;
//   - static-headers maps and ordered sequences (parameters, body mappings)
//     are detached from the caller's input, preserving their declared order
//     (they are ordered, not unordered sets).
//
// NormalizeDefinition does not validate; it is a pure canonicalization. Callers
// that need validation must run ValidateDefinition (External) or ValidateCatalog
// (Builtin) separately, or — for the registry preparation path — use
// ValidateAndNormalizeDefinition, which validates, verifies the digest,
// normalizes, and post-verifies in one call.
func NormalizeDefinition(pkg Package) Package {
	actions := make([]Action, len(pkg.Actions))
	for i, a := range pkg.Actions {
		actions[i] = normalizeAction(a)
	}
	sort.Slice(actions, func(i, j int) bool { return actions[i].Name < actions[j].Name })

	return Package{
		Metadata: normalizeMetadata(pkg.Metadata),
		Actions:  actions,
		Digest:   pkg.Digest,
	}
}

// ValidateAndNormalizeDefinition is the single preparation path for an External
// Definition Package: it validates the full Definition contract
// (ValidateDefinition), re-verifies the recorded digest (VerifyDigest),
// canonicalizes via NormalizeDefinition, and post-verifies that normalization
// preserved the digest (VerifyDigest on the normalized package).
//
// Both RegisterExternal and the embedded batch bootstrap route through this
// helper so that two inputs with the same semantic content (same digest)
// produce a byte-identical canonical stored Definition and Binding regardless
// of the caller's field/element ordering. NormalizeDefinition is sort-only
// (no de-duplication; the validator rejects duplicates) and JSON-roundtrips
// schemas/examples for full detachment, so it is digest-preserving; the final
// post-verify confirms the normalized package still matches its recorded digest.
//
// The returned package is canonicalized and fully detached from the caller's
// input. This helper does NOT acquire any registry lock — atomicity of a batch
// is the caller's responsibility (validate-all → normalize-all → commit-all
// under one lock).
func ValidateAndNormalizeDefinition(pkg Package) (Package, error) {
	if err := ValidateDefinition(pkg); err != nil {
		return Package{}, err
	}
	if err := VerifyDigest(pkg); err != nil {
		return Package{}, err
	}
	normalized := NormalizeDefinition(pkg)
	if err := VerifyDigest(normalized); err != nil {
		return Package{}, fmt.Errorf("normalize definition %q: %w", pkg.Metadata.Domain, err)
	}
	return normalized, nil
}

// normalizeMetadata returns a detached copy of meta with auth profiles sorted by
// ID (an order-independent set).
func normalizeMetadata(meta Metadata) Metadata {
	cp := meta
	cp.Auth = normalizeAuth(meta.Auth)
	return cp
}

// normalizeAuth returns a detached copy of a with profiles sorted by ID.
func normalizeAuth(a Auth) Auth {
	ca := a
	if len(a.Profiles) > 0 {
		profiles := make([]AuthProfile, len(a.Profiles))
		copy(profiles, a.Profiles)
		sort.Slice(profiles, func(i, j int) bool { return profiles[i].ID < profiles[j].ID })
		ca.Profiles = profiles
	}
	return ca
}

// normalizeAction returns a detached copy of one action with its order-independent
// sets canonicalized. Parameters and body mappings keep their declared order.
func normalizeAction(a Action) Action {
	cp := a
	cp.Auth = normalizeActionAuth(a.Auth)
	cp.Catalog = normalizeCatalog(a.Catalog)
	cp.Binding = normalizeBinding(a.Binding)
	return cp
}

// normalizeActionAuth returns a detached copy of the per-action auth requirement
// with required_scopes sorted (NOT de-duplicated; duplicates are the validator's
// rejection, not the normalizer's job). A nil requirement (explicit no-auth)
// stays nil.
func normalizeActionAuth(auth *ActionAuthRequirement) *ActionAuthRequirement {
	if auth == nil {
		return nil
	}
	cp := *auth
	cp.RequiredScopes = sortStringsCopy(auth.RequiredScopes)
	return &cp
}

// normalizeCatalog returns a detached copy of the Agent-visible catalog with
// schemas and examples normalized to the deterministic JSON tree.
func normalizeCatalog(c Catalog) Catalog {
	cp := c
	cp.InputSchema = detachedJSONSchema(c.InputSchema)
	cp.OutputSchema = detachedJSONSchema(c.OutputSchema)
	cp.Example = detachedJSONValue(c.Example)
	return cp
}

// normalizeBinding returns a detached copy of the HTTP binding with its
// order-independent response sets canonicalized. Parameters and body mappings
// keep their declared order (they are sequences, not sets).
func normalizeBinding(b Binding) Binding {
	cb := b
	cb.Response = normalizeResponse(b.Response)
	cb.StaticHeaders = copyStringMap(b.StaticHeaders)
	if len(b.Parameters) > 0 {
		cb.Parameters = append([]Parameter(nil), b.Parameters...)
	}
	cb.Body = normalizeBody(b.Body)
	return cb
}

// normalizeBody returns a detached copy of the request body. Mappings keep their
// declared order.
func normalizeBody(body *Body) *Body {
	if body == nil {
		return nil
	}
	cp := *body
	if len(body.Mappings) > 0 {
		cp.Mappings = append([]BodyMapping(nil), body.Mappings...)
	}
	return &cp
}

// normalizeResponse returns a detached copy of the response policy with
// success_statuses (numeric) and content_types (lexicographic) sorted. They are
// NOT de-duplicated; duplicates are the validator's rejection, not the
// normalizer's job (removing elements would change the digest of unvalidated
// input).
func normalizeResponse(r Response) Response {
	cr := r
	cr.SuccessStatuses = sortIntsCopy(r.SuccessStatuses)
	cr.ContentTypes = sortStringsCopy(r.ContentTypes)
	return cr
}

// detachedJSONSchema normalizes a JSON schema map to the deterministic JSON tree
// (map[string]any / []any / scalar leaves) by round-tripping through
// encoding/json. A nil schema stays nil.
func detachedJSONSchema(schema map[string]any) map[string]any {
	if schema == nil {
		return nil
	}
	v := detachedJSONValue(schema)
	if m, ok := v.(map[string]any); ok {
		return m
	}
	return nil
}

// sortStringsCopy returns a sorted copy of s WITHOUT de-duplicating. Sorting is
// order-invariant, so it never changes the canonical digest; de-duplication is
// the validator's job (ValidateDefinition/ValidateCatalog reject duplicate
// scopes and content-types). A nil/empty input stays a detached nil/empty slice.
func sortStringsCopy(s []string) []string {
	if len(s) == 0 {
		return append([]string(nil), s...)
	}
	cp := append([]string(nil), s...)
	sort.Strings(cp)
	return cp
}

// sortIntsCopy returns a sorted copy of s WITHOUT de-duplicating. See
// sortStringsCopy for the digest rationale. A nil/empty input stays a detached
// nil/empty slice.
func sortIntsCopy(s []int) []int {
	if len(s) == 0 {
		return append([]int(nil), s...)
	}
	cp := append([]int(nil), s...)
	sort.Ints(cp)
	return cp
}
