package spec

import (
	"errors"
	"fmt"
	"net/url"
	"regexp"
	"sort"
	"strings"
)

// Exported sentinel errors used by the shared catalog validation. Both
// ValidateDefinition (External) and ValidateCatalog (Builtin) wrap these so
// callers can errors.Is against a stable identity regardless of the kind.
var (
	// ErrInvalidSafetyClass is returned when an action's safety class is outside
	// the closed V1 set (read|mutation|destructive) or is internally
	// inconsistent (a read action marked destructive).
	ErrInvalidSafetyClass = errors.New("extension: action has invalid or inconsistent safety class")

	// ErrDuplicateAction is returned when two actions in one package share a
	// name.
	ErrDuplicateAction = errors.New("extension: duplicate action name")
)

// Closed, versioned policy sets. Expanding the reserved domain set or the
// supported serialization surface is a Package Spec change and must be
// intentional.
var (
	kebabNameRe   = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`)
	placeholderRe = regexp.MustCompile(`\{([A-Za-z0-9_.-]+)\}`)
	headerTokenRe = regexp.MustCompile(`^[A-Za-z0-9!#$%&'*+\-.^_` + "`" + `|~]+$`)
	pathVarRe     = regexp.MustCompile(`[{}]`)

	reservedDomainWords = map[string]struct{}{
		"list": {}, "describe": {}, "add": {}, "remove": {}, "compile": {}, "check": {},
	}

	reservedHeaders = map[string]struct{}{
		"authorization":       {},
		"proxy-authorization": {},
		"cookie":              {},
		"host":                {},
		"content-length":      {},
		"transfer-encoding":   {},
		"connection":          {},
		"proxy-connection":    {},
		"keep-alive":          {},
		"te":                  {},
		"trailer":             {},
		"upgrade":             {},
	}

	validSafetyClasses = map[string]struct{}{
		"read": {}, "mutation": {}, "destructive": {},
	}

	// validMethods is the V1 Package Spec closed-set. HEAD and OPTIONS (and
	// TRACE, which is excluded a fortiori) produce no useful Agent data action
	// in V1 — HEAD omits the response body, OPTIONS is a CORS preflight, TRACE
	// is a diagnostic loopback — so a Package carrying one is rejected at
	// LoadDefinition.
	validMethods = map[string]struct{}{
		"GET": {}, "POST": {}, "PUT": {}, "PATCH": {}, "DELETE": {},
	}

	// V1 supports application/json request bodies only.
	validBodyContentTypes = map[string]struct{}{"application/json": {}}

	// streamingResponseMediaTypes are text/* types that represent unbounded
	// event streams rather than bounded payloads. They must fail validation
	// even though they match the text/* response allowance.
	streamingResponseMediaTypes = map[string]struct{}{
		"text/event-stream": {},
	}
)

// validateMetadataCommon validates the metadata identity fields shared by both
// External and Builtin Definitions: schema_version, domain (kebab-case, not a
// reserved management word), version, and name. Endpoint and auth-profile
// validation are External-only and are added by validateMetadata.
func validateMetadataCommon(meta Metadata) error {
	if meta.SchemaVersion != 1 {
		return fmt.Errorf("unsupported schema_version %d (want 1)", meta.SchemaVersion)
	}
	if !kebabNameRe.MatchString(meta.Domain) {
		return fmt.Errorf("domain %q is not lowercase kebab-case", meta.Domain)
	}
	if _, reserved := reservedDomainWords[meta.Domain]; reserved {
		return fmt.Errorf("domain %q uses a reserved management word", meta.Domain)
	}
	if meta.Version < 1 {
		return fmt.Errorf("version must be >= 1, got %d", meta.Version)
	}
	if strings.TrimSpace(meta.Name) == "" {
		return fmt.Errorf("metadata name must be set")
	}
	return nil
}

// validateMetadata checks the full package-level scalar contract (everything
// except the action reference list, which is validated by validateActionRefs):
// the common identity fields plus the External-only endpoint and auth profiles.
func validateMetadata(meta Metadata) error {
	if err := validateMetadataCommon(meta); err != nil {
		return err
	}
	if err := validateEndpoint(meta.Endpoint); err != nil {
		return err
	}
	if err := validateAuth(meta.Auth); err != nil {
		return err
	}
	return nil
}

// validateActionRefs checks the meta.yaml actions list: kebab-case names, clean
// confined paths, basename == action name, and no duplicate names or files.
func validateActionRefs(refs []ActionRef) error {
	seenName := make(map[string]struct{})
	seenFile := make(map[string]struct{})
	for _, ref := range refs {
		if !kebabNameRe.MatchString(ref.Name) {
			return fmt.Errorf("action name %q is not lowercase kebab-case", ref.Name)
		}
		if _, dup := seenName[ref.Name]; dup {
			return fmt.Errorf("duplicate action name %q", ref.Name)
		}
		seenName[ref.Name] = struct{}{}
		clean, err := confinePath(ref.File)
		if err != nil {
			return fmt.Errorf("action %q file path: %w", ref.Name, err)
		}
		if clean != ref.File {
			return fmt.Errorf("action %q file path %q is not clean", ref.Name, ref.File)
		}
		if _, dup := seenFile[ref.File]; dup {
			return fmt.Errorf("duplicate action file %q", ref.File)
		}
		seenFile[ref.File] = struct{}{}
		base := fileBase(ref.File)
		if base != ref.Name {
			return fmt.Errorf("action %q file basename %q does not match the action name", ref.Name, base)
		}
	}
	return nil
}

// ActionRef is the meta.yaml {name, file} reference pair. It is internal to
// loading/encoding; the full Action is stored in Package.Actions.
type ActionRef struct {
	Name string `yaml:"name"`
	File string `yaml:"file"`
}

func validateEndpoint(ep Endpoint) error {
	u, err := url.Parse(ep.BaseURL)
	if err != nil {
		return fmt.Errorf("endpoint.base_url %q is not a valid URL: %w", ep.BaseURL, err)
	}
	if u.Scheme != "https" {
		return fmt.Errorf("endpoint.base_url must be https, got scheme %q", u.Scheme)
	}
	if u.Host == "" {
		return fmt.Errorf("endpoint.base_url must have a host")
	}
	if u.User != nil {
		return fmt.Errorf("endpoint.base_url must not carry credentials")
	}
	if u.RawQuery != "" {
		return fmt.Errorf("endpoint.base_url must not carry a query string")
	}
	if u.Fragment != "" {
		return fmt.Errorf("endpoint.base_url must not carry a fragment")
	}
	if pathVarRe.MatchString(u.Host) || pathVarRe.MatchString(u.Path) {
		return fmt.Errorf("endpoint.base_url must not contain server variables ({})")
	}
	return nil
}

// maxAuthProfileIDLength bounds an exact auth-profile identity authored into a
// Definition. App-hosted selections may map this identity to an App connection
// slot, but the generic service_connections table does not duplicate it.
const maxAuthProfileIDLength = 255

// validateAuthProfileID enforces the auth-profile id contract (stage-5 round-3
// hardening): non-empty, no surrounding whitespace (it is an exact identity,
// never trimmed/normalized), and within the package-format bound. Each rule is
// fail-closed.
func validateAuthProfileID(id string) error {
	if id == "" {
		return fmt.Errorf("auth profile id must be set")
	}
	if strings.TrimSpace(id) != id {
		return fmt.Errorf("auth profile id %q must not have surrounding whitespace", id)
	}
	if len(id) > maxAuthProfileIDLength {
		return fmt.Errorf("auth profile id %q exceeds the %d-byte limit", id, maxAuthProfileIDLength)
	}
	return nil
}

func validateAuth(auth Auth) error {
	ids := make(map[string]struct{}, len(auth.Profiles))
	for _, p := range auth.Profiles {
		if err := validateAuthProfileID(p.ID); err != nil {
			return err
		}
		if strings.TrimSpace(p.Type) == "" {
			return fmt.Errorf("auth profile %q type must be set", p.ID)
		}
		// Closed-set type validation: only oauth2_bearer and api_key are V1
		// credential slot types.
		switch p.Type {
		case "oauth2_bearer":
			// oauth2_bearer is a bearer token; in/name are irrelevant and must
			// not be set (they would mislead the install UI).
			if p.In != "" || p.Name != "" {
				return fmt.Errorf("auth profile %q: oauth2_bearer must not set in or name", p.ID)
			}
		case "api_key":
			// api_key requires a location (header/query only — cookie is rejected)
			// and a header/parameter name.
			switch p.In {
			case "header", "query":
			case "":
				return fmt.Errorf("auth profile %q: api_key requires in (header or query)", p.ID)
			default:
				return fmt.Errorf("auth profile %q: api_key in %q is not supported in V1 (header or query only, not cookie)", p.ID, p.In)
			}
			if strings.TrimSpace(p.Name) == "" {
				return fmt.Errorf("auth profile %q: api_key requires name", p.ID)
			}
			if p.In == "header" && isReservedHeader(p.Name) {
				return fmt.Errorf("auth profile %q: api_key uses reserved header %q", p.ID, p.Name)
			}
		default:
			return fmt.Errorf("auth profile %q: type %q is not supported in V1 (oauth2_bearer or api_key)", p.ID, p.Type)
		}
		if _, dup := ids[p.ID]; dup {
			return fmt.Errorf("duplicate auth profile id %q", p.ID)
		}
		ids[p.ID] = struct{}{}
	}
	if auth.Default != "" {
		if _, ok := ids[auth.Default]; !ok {
			return fmt.Errorf("auth default %q references no declared profile", auth.Default)
		}
	}
	return nil
}

// validateSafety checks the closed safety-class set and the internal
// consistency of the safety contract. The destructive flag must agree with the
// class: class=destructive REQUIRES destructive=true, and every other class
// (read, mutation) REQUIRES destructive=false. Any mismatch is contradictory
// (a conforming Package never carries one and the authorization layer cannot
// interpret one) and is rejected. This check is shared by External and Builtin
// so neither kind can declare an inconsistent safety contract.
func validateSafety(actionName string, safety Safety) error {
	if _, ok := validSafetyClasses[safety.Class]; !ok {
		return fmt.Errorf("%w: action %q has class %q (want read|mutation|destructive)", ErrInvalidSafetyClass, actionName, safety.Class)
	}
	wantDestructive := safety.Class == "destructive"
	if safety.Destructive != wantDestructive {
		return fmt.Errorf("%w: action %q has class %q but destructive=%v (destructive must be true only for class=destructive and false for every other class)", ErrInvalidSafetyClass, actionName, safety.Class, safety.Destructive)
	}
	return nil
}

// validateCatalogAction validates the common catalog fields of an action that
// both External and Builtin share: kebab-case name, a closed-set and internally
// consistent safety class, and a non-nil input schema. It does NOT validate the
// HTTP binding or auth profile references (those are External-only, validated by
// validateAction).
func validateCatalogAction(act Action) error {
	if !kebabNameRe.MatchString(act.Name) {
		return fmt.Errorf("action name %q is not lowercase kebab-case", act.Name)
	}
	if err := validateSafety(act.Name, act.Catalog.Safety); err != nil {
		return err
	}
	if act.Catalog.InputSchema == nil {
		return fmt.Errorf("action %q must declare an input_schema", act.Name)
	}
	return nil
}

// validateAction validates one loaded action against the package metadata. It
// runs the common catalog validation (validateCatalogAction) plus the
// External-only auth-profile reference check and HTTP binding validation.
func validateAction(act Action, auth Auth) error {
	if err := validateCatalogAction(act); err != nil {
		return err
	}
	if act.Auth != nil {
		prof := findProfile(auth, act.Auth.Profile)
		if prof == nil {
			return fmt.Errorf("action %q auth profile %q references no declared profile", act.Name, act.Auth.Profile)
		}
		// required_scopes are only meaningful on oauth2_bearer profiles. An
		// api_key credential is a fixed bearer/key; per-call scopes do not
		// apply.
		if prof.Type != "oauth2_bearer" && len(act.Auth.RequiredScopes) > 0 {
			return fmt.Errorf("action %q: required_scopes are only allowed on oauth2_bearer profiles, profile %q is %s", act.Name, act.Auth.Profile, prof.Type)
		}
		if err := validateScopes(act.Name, act.Auth.RequiredScopes); err != nil {
			return err
		}
	}
	if err := validateBinding(act.Name, act.Binding, act.Catalog.InputSchema); err != nil {
		return err
	}
	return nil
}

// validateScopes canonical-sorts the scopes in place is NOT done here (the
// caller keeps the original order); instead we reject duplicate scopes so a
// "set with dup" cannot form a different canonical digest.
func validateScopes(actionName string, scopes []string) error {
	seen := make(map[string]struct{}, len(scopes))
	for _, s := range scopes {
		if _, dup := seen[s]; dup {
			return fmt.Errorf("action %q: duplicate required_scope %q", actionName, s)
		}
		seen[s] = struct{}{}
	}
	return nil
}

func findProfile(auth Auth, id string) *AuthProfile {
	for i := range auth.Profiles {
		if auth.Profiles[i].ID == id {
			return &auth.Profiles[i]
		}
	}
	return nil
}

func validateBinding(name string, b Binding, inputSchema map[string]any) error {
	if _, ok := validMethods[b.Method]; !ok {
		return fmt.Errorf("action %q has invalid HTTP method %q (V1 supports GET, POST, PUT, PATCH, DELETE only; HEAD, OPTIONS, and TRACE are rejected)", name, b.Method)
	}
	if !strings.HasPrefix(b.Path, "/") {
		return fmt.Errorf("action %q binding path %q must start with /", name, b.Path)
	}
	placeholders := placeholderRe.FindAllStringSubmatch(b.Path, -1)
	boundPath := make(map[string]int)
	for _, p := range b.Parameters {
		if p.In != "path" && p.In != "query" && p.In != "header" {
			return fmt.Errorf("action %q parameter %q has invalid in %q (want path|query|header)", name, p.Name, p.In)
		}
		if p.Name == "" {
			return fmt.Errorf("action %q parameter has empty name", name)
		}
		if p.In == "header" {
			if isReservedHeader(p.Name) {
				return fmt.Errorf("action %q binds reserved header %q", name, p.Name)
			}
			if !headerTokenRe.MatchString(p.Name) {
				return fmt.Errorf("action %q header parameter %q is not a valid header token", name, p.Name)
			}
		}
		tokens, err := ParseJSONPointer(p.From)
		if err != nil {
			return fmt.Errorf("action %q parameter %q from: %w", name, p.Name, err)
		}
		// The source pointer must address a property declared by the input
		// schema; the root pointer ("") addresses the whole input and is valid.
		if !pointerExists(inputSchema, tokens) {
			return fmt.Errorf("action %q parameter %q from %q does not exist in input_schema", name, p.Name, p.From)
		}
		if p.In == "path" {
			// A path placeholder is always sent on the wire, so its source must
			// be required at EVERY level along the pointer chain (a child of an
			// optional parent would not be reached, and an optional child of a
			// required parent would leave the placeholder unbound).
			if err := validateRequiredChain(name, inputSchema, tokens); err != nil {
				return err
			}
		}
		// Non-body parameters (path/query/header) cannot serialize null. The
		// compiled source schema must definitively reject null per §4.4; if
		// null is admissible or cannot be statically determined, fail-closed.
		if leaf, ok := schemaAtPointer(inputSchema, tokens); ok {
			rejects, err := schemaRejectsNull(leaf)
			if err != nil {
				return fmt.Errorf("action %q parameter %q source schema null-permissibility cannot be determined (non-body parameters cannot serialize null per §4.4): %w", name, p.Name, err)
			}
			if !rejects {
				return fmt.Errorf("action %q parameter %q source schema allows null (non-body parameters cannot serialize null per §4.4)", name, p.Name)
			}
		}
		if err := validateParamStyle(p); err != nil {
			return fmt.Errorf("action %q parameter %q: %w", name, p.Name, err)
		}
		if p.In == "path" {
			boundPath[p.Name]++
		}
	}
	// Every path placeholder must be bound exactly once.
	want := make(map[string]int, len(placeholders))
	for _, m := range placeholders {
		want[m[1]]++
	}
	for ph, n := range want {
		if boundPath[ph] != n {
			return fmt.Errorf("action %q path placeholder {%s} must be bound exactly once", name, ph)
		}
	}
	for ph, n := range boundPath {
		if n > 1 {
			return fmt.Errorf("action %q path placeholder {%s} bound %d times", name, ph, n)
		}
		if _, ok := want[ph]; !ok {
			return fmt.Errorf("action %q path parameter {%s} has no matching placeholder", name, ph)
		}
	}
	// Static headers must not be reserved and must not collide with header params.
	for h := range b.StaticHeaders {
		if isReservedHeader(h) {
			return fmt.Errorf("action %q sets reserved header %q", name, h)
		}
		if !headerTokenRe.MatchString(h) {
			return fmt.Errorf("action %q static header %q is not a valid header token", name, h)
		}
	}
	if err := validateTargetUniqueness(name, b); err != nil {
		return err
	}
	if b.Body != nil {
		if err := validateBody(name, b.Body, inputSchema); err != nil {
			return err
		}
	}
	if err := validateResponse(name, b.Response); err != nil {
		return err
	}
	return nil
}

func validateParamStyle(p Parameter) error {
	switch p.In {
	case "path":
		if p.Style != "" && p.Style != "simple" {
			return fmt.Errorf("unsupported path style %q (V1 supports simple)", p.Style)
		}
	case "query":
		if p.Style != "" && p.Style != "form" {
			return fmt.Errorf("unsupported query style %q (V1 supports form)", p.Style)
		}
	case "header":
		if p.Style != "" && p.Style != "simple" {
			return fmt.Errorf("unsupported header style %q (V1 supports simple)", p.Style)
		}
	}
	// allowReserved only applies to query parameters per OpenAPI; reject it on
	// other locations as an unsupported serialization option.
	if p.AllowReserved && p.In != "query" {
		return fmt.Errorf("allow_reserved is only supported on query parameters, not %q", p.In)
	}
	return nil
}

// validateTargetUniqueness rejects two parameters (or a parameter and a static
// header) targeting the same HTTP location. Header names are compared
// case-insensitively (HTTP headers are case-insensitive, so X-Foo and x-foo
// collide). One source may fan out to several targets through explicit mappings,
// but two sources may not claim one target.
func validateTargetUniqueness(name string, b Binding) error {
	type loc struct{ in, n string }
	keyFor := func(in, name string) loc {
		if in == "header" {
			name = strings.ToLower(name)
		}
		return loc{in, name}
	}
	seen := make(map[loc]struct{})
	for _, p := range b.Parameters {
		key := keyFor(p.In, p.Name)
		if _, ok := seen[key]; ok {
			return fmt.Errorf("action %q has conflicting binding targets for %s %q", name, p.In, p.Name)
		}
		seen[key] = struct{}{}
	}
	for h := range b.StaticHeaders {
		key := keyFor("header", h)
		if _, ok := seen[key]; ok {
			return fmt.Errorf("action %q static header %q conflicts with a header parameter", name, h)
		}
		seen[key] = struct{}{}
	}
	return nil
}

func validateBody(name string, body *Body, inputSchema map[string]any) error {
	if _, ok := validBodyContentTypes[body.ContentType]; !ok {
		return fmt.Errorf("action %q body content_type %q is not supported (V1: application/json)", name, body.ContentType)
	}
	if len(body.Mappings) == 0 {
		return fmt.Errorf("action %q body must declare at least one mapping", name)
	}
	for _, m := range body.Mappings {
		fromTokens, err := ParseJSONPointer(m.From)
		if err != nil {
			return fmt.Errorf("action %q body mapping from: %w", name, err)
		}
		if _, err := ParseJSONPointer(m.To); err != nil {
			return fmt.Errorf("action %q body mapping to: %w", name, err)
		}
		// The source pointer must address a property declared by the input
		// schema (or be the root, which addresses the whole input).
		if !pointerExists(inputSchema, fromTokens) {
			return fmt.Errorf("action %q body mapping from %q does not exist in input_schema", name, m.From)
		}
	}
	if err := validateBodyTargetOverlap(name, body.Mappings); err != nil {
		return err
	}
	return nil
}

// validateBodyTargetOverlap rejects body target pointers that overlap
// ambiguously: two pointers may not be equal nor ancestor/descendant of each
// other, so each mapping writes a disjoint region of the body. Pointers are
// parsed with the shared RFC 6901 parser so escaped tokens compare correctly.
func validateBodyTargetOverlap(name string, mappings []BodyMapping) error {
	parsed := make([]struct {
		raw    string
		tokens []string
	}, 0, len(mappings))
	for _, m := range mappings {
		tokens, err := ParseJSONPointer(m.To)
		if err != nil {
			// Already validated above; treat as a hard error defensively.
			return fmt.Errorf("action %q body mapping to: %w", name, err)
		}
		parsed = append(parsed, struct {
			raw    string
			tokens []string
		}{m.To, tokens})
	}
	sort.Slice(parsed, func(i, j int) bool { return pointerLess(parsed[i].tokens, parsed[j].tokens) })
	for i := 1; i < len(parsed); i++ {
		if pointerPrefix(parsed[i-1].tokens, parsed[i].tokens) {
			return fmt.Errorf("action %q body target pointer %q overlaps %q ambiguously", name, parsed[i-1].raw, parsed[i].raw)
		}
	}
	return nil
}

func validateResponse(name string, r Response) error {
	if r.Mode != "passthrough" {
		return fmt.Errorf("action %q response mode %q is not supported (V1: passthrough)", name, r.Mode)
	}
	if len(r.SuccessStatuses) == 0 {
		return fmt.Errorf("action %q response must declare at least one success status", name)
	}
	seenStatus := make(map[int]struct{}, len(r.SuccessStatuses))
	for _, s := range r.SuccessStatuses {
		if s < 100 || s > 599 {
			return fmt.Errorf("action %q response success status %d is out of range", name, s)
		}
		if _, dup := seenStatus[s]; dup {
			return fmt.Errorf("action %q response has duplicate success status %d", name, s)
		}
		seenStatus[s] = struct{}{}
	}
	if len(r.ContentTypes) == 0 {
		return fmt.Errorf("action %q response must declare at least one content type", name)
	}
	seenCT := make(map[string]struct{}, len(r.ContentTypes))
	for _, c := range r.ContentTypes {
		if !strings.Contains(c, "/") {
			return fmt.Errorf("action %q response content type %q is not a valid media type", name, c)
		}
		if !allowedResponseMediaType(c) {
			return fmt.Errorf("action %q response content type %q is not supported in V1 (JSON, application/*+json, or bounded text only; binary/streaming/multipart fail)", name, c)
		}
		if _, dup := seenCT[c]; dup {
			return fmt.Errorf("action %q response has duplicate content type %q", name, c)
		}
		seenCT[c] = struct{}{}
	}
	return nil
}

// allowedResponseMediaType reports whether mt is an accepted V1 response type
// per §13.4: application/json, application/*+json, or bounded text (text/*).
// Streaming types (text/event-stream) are rejected even though they match text/*
// and therefore forbidden by the Package Spec even when OpenAPI describes
// them.
func allowedResponseMediaType(mt string) bool {
	mt = strings.ToLower(strings.TrimSpace(mt))
	if i := strings.IndexByte(mt, ';'); i >= 0 {
		mt = strings.TrimSpace(mt[:i]) // drop parameters like "; charset=utf-8"
	}
	if _, ok := streamingResponseMediaTypes[mt]; ok {
		return false
	}
	switch {
	case mt == "application/json":
		return true
	case strings.HasPrefix(mt, "application/") && strings.HasSuffix(mt, "+json"):
		return true
	case strings.HasPrefix(mt, "text/"):
		return true
	default:
		return false
	}
}

func isReservedHeader(h string) bool {
	_, ok := reservedHeaders[strings.ToLower(h)]
	return ok
}

// validateRequiredChain checks that every token along the pointer chain is a
// required property of its parent schema. A path placeholder is always sent on
// the wire, so its source must be reachable at every level: a child of an
// optional parent would never be reached, and an optional child of a required
// parent would leave the placeholder unbound.
func validateRequiredChain(name string, schema map[string]any, tokens []string) error {
	if len(tokens) == 0 {
		return nil // root pointer — the whole input is always sent
	}
	current := schema
	for i, tok := range tokens {
		if !schemaRequires(current, tok) {
			partial := EncodeJSONPointer(tokens[:i+1])
			return fmt.Errorf("action %q path parameter source %q must be a required property at every level (%q is not required)", name, partial, tok)
		}
		// Descend for the next token.
		next, ok := schemaAtPointer(current, []string{tok})
		if !ok {
			return nil // non-object leaf — no further required check possible
		}
		current = next
	}
	return nil
}

// pointerPrefix reports whether short is a component-wise prefix of long
// (including equality), i.e. the two pointers overlap. Tokens must already be
// RFC 6901 decoded.
func pointerPrefix(short, long []string) bool {
	if len(short) > len(long) {
		return false
	}
	for i := range short {
		if short[i] != long[i] {
			return false
		}
	}
	return true
}

func pointerLess(a, b []string) bool {
	for i := 0; i < len(a) && i < len(b); i++ {
		if a[i] != b[i] {
			return a[i] < b[i]
		}
	}
	return len(a) < len(b)
}

// ---- Exported shared validation API ----

// ValidateDefinition runs the FULL in-memory Definition validation (metadata,
// endpoint, auth profiles, per-action binding/schema/example/safety) that
// LoadDefinition uses, WITHOUT any file-system round-trip. It is the single
// shared validation entry point for an already-assembled External Package — a
// registry (or any in-memory caller) calls this instead of re-encoding and
// re-loading through an fsys. A self-signed package whose CONTENT is invalid for
// V1 — e.g. an unsupported binding.method (TRACE/HEAD/OPTIONS), a bad
// endpoint or auth profile, a malformed schema, or an example that fails its
// schema — is rejected here regardless of its digest.
//
// This does NOT duplicate or diverge from the loader: LoadDefinition builds a
// Package from files and then applies the same validators (validateMetadata +
// validatePackageActions). The file-level guards (symlink confinement,
// unlisted-action detection, action-ref path cleanliness) are load-time-only and
// are intentionally NOT part of this in-memory check.
func ValidateDefinition(pkg Package) error {
	if err := validateMetadata(pkg.Metadata); err != nil {
		return err
	}
	return validatePackageActions(pkg.Actions, pkg.Metadata.Auth)
}

// ValidateCatalog validates the COMMON catalog fields shared by External and
// Builtin Definitions: metadata identity (schema_version, domain kebab-case +
// not reserved, version, name) and per-action name/safety/schema/example. It
// does NOT validate endpoint, auth profiles, or HTTP binding — a Builtin carries
// none of those (spec §5.1). A code-registered Builtin is thus held to the SAME
// catalog contract as an OpenAPI-derived External, so an invalid domain, a
// non-kebab action name, an uncompilable schema, an example that violates its
// schema, or a contradictory safety contract (destructive flag disagreeing with
// the class) is rejected at registration.
//
// meta.SchemaVersion must be 1. Each action's Catalog.InputSchema and
// Catalog.OutputSchema (if non-nil) are compiled as Draft 2020-12, and
// Catalog.Example (if non-nil) is validated against the compiled input schema.
// The action Binding and Auth fields are ignored (a Builtin has no HTTP
// binding).
func ValidateCatalog(meta Metadata, actions []Action) error {
	if err := validateMetadataCommon(meta); err != nil {
		return err
	}
	seen := make(map[string]struct{}, len(actions))
	for _, act := range actions {
		if _, dup := seen[act.Name]; dup {
			return fmt.Errorf("%w: %q", ErrDuplicateAction, act.Name)
		}
		seen[act.Name] = struct{}{}
		if err := validateCatalogAction(act); err != nil {
			return err
		}
		compiled, err := compileInputSchema(act.Name, act.Catalog.InputSchema)
		if err != nil {
			return err
		}
		if act.Catalog.OutputSchema != nil {
			if _, err := compileInputSchema(act.Name, act.Catalog.OutputSchema); err != nil {
				return err
			}
		}
		if err := validateExample(act.Name, compiled, act.Catalog.Example); err != nil {
			return err
		}
	}
	return nil
}

// validatePackageActions validates the action list of a Package: duplicate
// names, schema compilation (Draft 2020-12), per-action binding/auth/safety, and
// example-against-schema checks. It is shared by ValidateDefinition and is the
// in-memory mirror of the per-action agentteam in LoadDefinition.
func validatePackageActions(actions []Action, auth Auth) error {
	seen := make(map[string]struct{}, len(actions))
	for _, act := range actions {
		if _, dup := seen[act.Name]; dup {
			return fmt.Errorf("%w: %q", ErrDuplicateAction, act.Name)
		}
		seen[act.Name] = struct{}{}
		compiled, err := compileInputSchema(act.Name, act.Catalog.InputSchema)
		if err != nil {
			return err
		}
		if act.Catalog.OutputSchema != nil {
			if _, err := compileInputSchema(act.Name, act.Catalog.OutputSchema); err != nil {
				return err
			}
		}
		if err := validateAction(act, auth); err != nil {
			return err
		}
		if err := validateExample(act.Name, compiled, act.Catalog.Example); err != nil {
			return err
		}
	}
	return nil
}
