// Package registry is the immutable, digest-keyed registry of AgentTeam Extension
// Definitions.
//
// The registry holds immutable Extension Definitions keyed by (domain, version)
// identity and content-derived digest, serves deep-cloned Agent-visible
// projections on read, and never silently overwrites an existing Definition
// (spec §5.1).
//
// Kind model (spec §5.1: "Builtin|External is a server-local trusted
// registration kind — it is NOT a field in meta.yaml"):
//
// The common, kind-independent Definition (domain, version, digest, and the
// Agent-visible per-action Catalog) is shared by both kinds. A kind-specific
// union is stored alongside it:
//
//   - External — an OpenAPI-derived Package executed over HTTP. It
//     carries the HTTP binding (endpoint, auth profiles, per-action binding)
//     declared by the Package. Embedded External packages have NO code
//     executor.
//   - Builtin — a code-registered executor Extension. It carries a code
//     Executor plus a stable implementation revision, and NO HTTP binding. A
//     real Builtin REQUIRES a non-nil Executor and a non-empty revision; there
//     is no default/stub executor.
//
// Kind is immutable identity: re-registering the same (domain, version, digest)
// with a DIFFERENT Kind fails (no silent External<->Builtin switch).
//
// Boundary: Definition/Catalog reads are deep-cloned (clone-on-read). Executor
// access is SEPARATE — Lookup*/List return only Kind/Definition/Binding; the
// executor is reached via BuiltinExecutorFor, which shares the executor by
// reference (executors are caller-supplied behavior, not cloned data).
package registry

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"sort"
	"strconv"
	"sync"

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

// Kind is the server-local trusted registration kind of an Extension. It is NOT
// part of meta.yaml or the public Extension spec; it is a runtime registry
// concern (spec §5.1).
type Kind int

const (
	// KindUnknown is the zero value and is rejected at registration so an
	// uninitialised kind can never reach the registry.
	KindUnknown Kind = iota
	// External marks an OpenAPI-derived Package executed over HTTP.
	// Embedded Package Definitions are External.
	External
	// Builtin marks a code-registered executor Extension. A Builtin REQUIRES a
	// non-nil Executor and a non-empty revision; there is no default/stub
	// executor.
	Builtin
)

// String returns the stable lowercase label for the kind.
func (k Kind) String() string {
	switch k {
	case External:
		return "external"
	case Builtin:
		return "builtin"
	default:
		return "unknown"
	}
}

// ConnectionPolicy describes whether an App-hosted Builtin Extension requires
// a provider connection before it can be projected or invoked. The zero value
// deliberately preserves the existing behavior for every Builtin registered
// before connectionless Extensions were introduced.
type ConnectionPolicy uint8

const (
	// ConnectionRequired requires exactly one active App connection.
	ConnectionRequired ConnectionPolicy = iota
	// ConnectionNone requires exactly zero active App connections.
	ConnectionNone
)

// BuiltinOptions carries trusted server-local Builtin registration policy.
// It is intentionally not Agent-visible catalog metadata.
type BuiltinOptions struct {
	ConnectionPolicy ConnectionPolicy
	// CatalogPolicy optionally attaches the provider catalog facet in the
	// same atomic, immutable-by-digest registration as the executor.
	CatalogPolicy CatalogPolicy
}

// Executor is the contract for a code-registered Builtin executor. It is
// caller-supplied behavior, NOT cloned data: it is stored once and reached only
// via BuiltinExecutorFor (never through the clone-on-read Lookup*/List
// projections). External (HTTP) extensions carry no code executor.
type Executor interface {
	// Strategy returns a short stable label for the execution strategy (e.g.
	// "http", "native").
	Strategy() string
	// Invoke executes one Action after the authoritative runtime preflight has
	// resolved the authenticated Agent, host App Installation, and digest-pinned
	// connection policy. App identity is server-internal and is never supplied by
	// Desktop.
	Invoke(context.Context, BuiltinInvocation) (BuiltinResult, error)
}

// BuiltinInvocation is the server-internal execution envelope handed to a
// trusted code-registered Extension executor after Catalog, selection, App
// authorization, connection-policy check, Definition, Action, and input
// validation succeed.
type BuiltinInvocation struct {
	WorkspaceID                    string
	AgentID                        string
	AgentMemberID                  string
	ServiceInstallationID          string
	ServiceConnectionID            string
	ServiceConnectionGeneration    string
	ServiceInstallationExtensionID string
	AuthorizedByMemberID           string
	ExtensionName                  string
	Action                         string
	DefinitionVersion              int
	DefinitionDigest               string
	CatalogVersion                 int64
	RequestHash                    string
	Input                          json.RawMessage
}

// BuiltinResult is the strategy-neutral bounded payload returned by a trusted
// code-registered Extension executor.
type BuiltinResult struct {
	Data                 json.RawMessage
	StatusClass          string
	ResultClassification string
	// AuditFailureData is an optional conservative result that may be returned
	// when the Builtin completed a provider mutation but the mandatory audit
	// write failed afterward. It must never claim success or invite an
	// automatic retry.
	AuditFailureData json.RawMessage
}

// isNilExecutor reports whether exec is a real, usable Executor. It rejects both
// an unset interface (exec == nil) and a typed-nil value (e.g. (*myExec)(nil)
// wrapped in the Executor interface), which is non-nil to a plain != nil check
// but nil in practice. A Builtin is defined by its executor, so a typed-nil
// executor is rejected as missing rather than stored as a latent nil-pointer
// dereference.
func isNilExecutor(exec Executor) bool {
	if exec == nil {
		return true
	}
	v := reflect.ValueOf(exec)
	switch v.Kind() {
	case reflect.Pointer, reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Slice:
		return v.IsNil()
	}
	return false
}

// Definition is the COMMON, kind-independent immutable identity plus the
// Agent-visible Catalog of one Extension. It is shared by External and Builtin.
// It deliberately carries NO HTTP binding and NO executor: those are
// kind-specific and live in the kind union (ExternalBinding / Executor+revision).
// The Digest is content-derived — for External it is the Package Spec digest
// (which covers the HTTP binding); for Builtin it is the
// registry's catalog digest (see computeBuiltinDigest).
type Definition struct {
	Domain      string
	Version     int
	Digest      string
	Name        string
	Description string
	Actions     []DefinitionAction
}

// DefinitionAction is the Agent-visible Catalog for one action (spec §6.3). It
// intentionally omits the HTTP binding, auth profile/scope requirement, and
// source operation id, which are server-only. Nested schemas and examples are
// deep-copied on every read.
type DefinitionAction struct {
	Name        string
	Summary     string
	Description string
	Safety      spec.Safety
	// RequiredScopes is the server-owned all-of App authorization scope set
	// for this Action. It is never accepted from Agent input. Empty preserves
	// the existing Extension behavior for Definitions without per-Action App
	// scopes.
	RequiredScopes []string
	InputSchema    map[string]any
	OutputSchema   map[string]any
	Example        any
}

// ExternalBinding is the kind-specific HTTP binding for an External Extension,
// derived from its Package. A Builtin carries the zero value
// (it has no HTTP binding). All nested slices and maps are deep-copied on read.
type ExternalBinding struct {
	Endpoint spec.Endpoint
	Auth     spec.Auth
	Actions  []ExternalBindingAction
}

// ExternalBindingAction pairs an action name with its server-only HTTP binding
// and per-action auth requirement.
type ExternalBindingAction struct {
	Name    string
	Auth    *spec.ActionAuthRequirement
	Binding spec.Binding
}

// RegisteredExtension is the clone-on-read projection of one registered
// Extension: its Kind, the common Definition, and (for External) the HTTP
// binding. The binding is the zero value for a Builtin. Values returned from
// the registry are deep clones — mutating a returned Definition or Binding does
// not affect cached registry state.
//
// The Executor is intentionally NOT part of this projection. Executors are
// caller-supplied behavior that cannot be meaningfully deep-cloned; access them
// via BuiltinExecutorFor (a separate, by-reference lookup).
type RegisteredExtension struct {
	Kind             Kind
	ConnectionPolicy ConnectionPolicy
	Definition       Definition
	Binding          ExternalBinding
}

// ConflictError is returned when a different digest is registered at an existing
// (domain, version) identity. Registration is fail-closed: the existing record
// is preserved unchanged and the conflicting payload is rejected. Use
// errors.As to detect this case at call sites.
type ConflictError struct {
	Domain   string
	Version  int
	Existing string // digest already registered under (domain, version)
	Incoming string // digest of the rejected payload
}

func (e *ConflictError) Error() string {
	return fmt.Sprintf("registry: definition %q version %d already registered with digest %s; cannot register conflicting digest %s",
		e.Domain, e.Version, e.Existing, e.Incoming)
}

// Is makes ConflictError match itself via errors.Is/As.
func (e *ConflictError) Is(target error) bool {
	_, ok := target.(*ConflictError)
	return ok
}

// KindMismatchError is returned when the same (domain, version, digest) is
// re-registered with a different Kind. Kind is immutable identity; an
// already-registered Definition cannot switch between External and Builtin.
type KindMismatchError struct {
	Domain   string
	Version  int
	Digest   string
	Existing Kind
	Incoming Kind
}

func (e *KindMismatchError) Error() string {
	return fmt.Sprintf("registry: definition %q version %d digest %s already registered as %s; cannot re-register as %s",
		e.Domain, e.Version, e.Digest, e.Existing, e.Incoming)
}

// Is makes KindMismatchError match itself via errors.Is/As.
func (e *KindMismatchError) Is(target error) bool {
	_, ok := target.(*KindMismatchError)
	return ok
}

// RevisionMismatchError is returned when a Builtin is re-registered at the same
// (domain, version, digest) identity but with a DIFFERENT implementation
// revision. Since the revision is part of the runtime digest, a revision change
// produces a different digest and surfaces as a *ConflictError at the same
// (domain, version). This error is retained as defense-in-depth for the case
// where two different revisions somehow produce the same digest; under the
// current digest algorithm it is unreachable.
type RevisionMismatchError struct {
	Domain   string
	Version  int
	Digest   string
	Existing string // revision already registered
	Incoming string // revision of the rejected payload
}

func (e *RevisionMismatchError) Error() string {
	return fmt.Sprintf("registry: builtin %q version %d digest %s already registered with revision %q; cannot re-register with revision %q",
		e.Domain, e.Version, e.Digest, e.Existing, e.Incoming)
}

// Is makes RevisionMismatchError match itself via errors.Is/As.
func (e *RevisionMismatchError) Is(target error) bool {
	_, ok := target.(*RevisionMismatchError)
	return ok
}

// ErrMissingDigest is returned when RegisterExternal is called with a package
// carrying no digest in either Package.Digest or Package.Metadata.Digest.
var ErrMissingDigest = errors.New("registry: package digest is required")

// ErrDigestMismatch is returned when Package.Digest and Package.Metadata.Digest
// are both present but disagree. The registry keys on a single consistent
// digest; the two digest fields must carry the same value.
var ErrDigestMismatch = errors.New("registry: package digest fields disagree")

// ErrMissingDomain is returned when Register is called with an empty domain.
var ErrMissingDomain = errors.New("registry: definition domain is required")

// ErrBuiltinMissingExecutor is returned when a Builtin is registered without a
// real Executor. There is no default/stub executor; a Builtin is defined by its
// code-registered executor.
var ErrBuiltinMissingExecutor = errors.New("registry: builtin registration requires a non-nil executor")

// ErrBuiltinMissingRevision is returned when a Builtin is registered with an
// empty revision. The revision is the stable implementation identity and is
// required so a catalog digest alone cannot authorize an executor swap.
var ErrBuiltinMissingRevision = errors.New("registry: builtin registration requires a non-empty revision")

// ErrInvalidConnectionPolicy is returned when a Builtin is registered with an
// unknown connection policy.
var ErrInvalidConnectionPolicy = errors.New("registry: invalid builtin connection policy")

// registeredEntry is the internal cached record. It holds the canonical cloned
// record; reads clone it again before returning so callers cannot mutate cached
// state through any reachable reference (nested slices, maps, pointers). The
// executor is shared by reference and reached only via BuiltinExecutorFor.
type registeredEntry struct {
	kind             Kind
	connectionPolicy ConnectionPolicy
	definition       Definition
	binding          ExternalBinding // External only; zero for Builtin
	executor         Executor        // Builtin only; nil for External
	revision         string          // Builtin only; "" for External
	catalogPolicy    CatalogPolicy   // Builtin only; optional provider facet
}

// Registry is a concurrency-safe, immutable-by-digest registry of Extension
// Definitions. Identity is (domain, version); one identity maps to exactly one
// digest and therefore one immutable Definition. Registering the same
// (domain, version, digest) (and, for Builtin, the same revision) is idempotent;
// registering a different digest at the same (domain, version) is a conflict
// and fails closed. The zero value is NOT usable — use New.
type Registry struct {
	mu         sync.RWMutex
	byDigest   map[string]*registeredEntry
	byIdentity map[string]string // "domain\x00version" -> digest
}

// New returns an empty, ready-to-use Registry.
func New() *Registry {
	return &Registry{
		byDigest:   make(map[string]*registeredEntry),
		byIdentity: make(map[string]string),
	}
}

// RegisterExternal validates and records a Spec-conforming Definition Package
// as an External Extension under its (domain, version, digest).
//
// Preparation runs through the SAME spec.ValidateAndNormalizeDefinition
// helper the embedded batch bootstrap uses (validate via the shared in-memory
// ValidateDefinition — the same validator the loader uses — then re-verify the
// digest, then NormalizeDefinition, then a digest post-verify). Any payload
// whose CONTENT is invalid for V1 — e.g. an unsupported binding.method
// (TRACE/HEAD/OPTIONS), a bad endpoint/auth, a malformed schema, or an example
// that fails its schema — is rejected EVEN IF the caller recomputed a
// self-consistent digest over the invalid content. Because both registration
// paths share one preparation route, two inputs differing only in the order of
// unordered sets share a digest AND a byte-identical stored Definition/Binding.
//
//   - The same (domain, version, digest) with the same Kind is idempotent and
//     returns the registered record (a deep clone).
//   - The same (domain, version, digest) with Kind Builtin returns
//     *KindMismatchError.
//   - A different digest at the same (domain, version) returns *ConflictError
//     and does NOT mutate the registry (fail-closed, no silent overwrite).
//   - A new (domain, version) registers the Definition.
//
// External carries no code executor; there is no executor parameter (an
// External executes via its HTTP binding, not a code object).
func (r *Registry) RegisterExternal(pkg spec.Package) (RegisteredExtension, error) {
	// Fast-fail the cheap presence/consistency guards with clean sentinel
	// errors before the (more expensive) full validation.
	if pkg.Metadata.Domain == "" {
		return RegisteredExtension{}, ErrMissingDomain
	}
	if pkg.Digest == "" || pkg.Metadata.Digest == "" {
		return RegisteredExtension{}, ErrMissingDigest
	}
	if pkg.Digest != pkg.Metadata.Digest {
		return RegisteredExtension{}, ErrDigestMismatch
	}
	// Shared preparation path: validate -> verify digest -> normalize -> post-verify.
	// This is the SAME helper the embedded batch bootstrap uses
	// (spec.ValidateAndNormalizeDefinition), so a direct RegisterExternal and
	// the embedded path produce byte-identical stored Definitions/Bindings for
	// the same semantic content (same digest) regardless of the caller's
	// field/element ordering. ValidateDefinition rejects a self-signed package
	// whose CONTENT is invalid for V1 regardless of its digest; VerifyDigest
	// re-checks the recorded digest; NormalizeDefinition canonicalizes (sort-only,
	// JSON-roundtrip detach) and is digest-preserving, so the verified digest
	// still identifies the stored record.
	canon, err := spec.ValidateAndNormalizeDefinition(pkg)
	if err != nil {
		return RegisteredExtension{}, err
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	digest := pkg.Digest
	key := identityKey(pkg.Metadata.Domain, pkg.Metadata.Version)
	existing, ok := r.byIdentity[key]
	if ok {
		return r.resolveIdentityLocked(existing, digest, External, "", pkg.Metadata.Domain, pkg.Metadata.Version)
	}
	if _, exists := r.byDigest[digest]; exists {
		return RegisteredExtension{}, fmt.Errorf(
			"registry: digest %s already registered under a different (domain, version) identity", digest)
	}

	entry := &registeredEntry{
		kind:       External,
		definition: buildDefinitionFromPackage(canon),
		binding:    buildExternalBindingFromPackage(canon),
	}
	r.byDigest[digest] = entry
	r.byIdentity[key] = digest
	return cloneRegistered(entry), nil
}

// resolveIdentityLocked handles the case where (domain, version) is already
// registered. The caller MUST hold r.mu exclusively. For a Builtin, revision is
// the implementation identity and is mismatch-checked; for External it is "".
//
// Resolution order:
//  1. Kind is immutable identity: a different Kind at the same (domain, version)
//     is *KindMismatchError regardless of digest (no silent External<->Builtin
//     switch). This is checked BEFORE the digest so a kind swap is always
//     reported as such, even though External and Builtin digests are computed
//     differently and would otherwise diverge into a ConflictError.
//  2. A different digest at the same (domain, version) is *ConflictError.
//  3. Same (domain, version, digest) and same Kind: for Builtin the revision is
//     a trusted-identity dimension, so a different revision is
//     *RevisionMismatchError; otherwise the registration is idempotent.
func (r *Registry) resolveIdentityLocked(existingDigest, incomingDigest string, kind Kind, revision, domain string, version int) (RegisteredExtension, error) {
	existing := r.byDigest[existingDigest]
	if existing.kind != kind {
		return RegisteredExtension{}, &KindMismatchError{
			Domain: domain, Version: version, Digest: existingDigest,
			Existing: existing.kind, Incoming: kind,
		}
	}
	if existingDigest != incomingDigest {
		return RegisteredExtension{}, &ConflictError{
			Domain: domain, Version: version, Existing: existingDigest, Incoming: incomingDigest,
		}
	}
	// Same (domain, version, digest) and same kind. For Builtin, the revision
	// is a trusted-identity dimension: the catalog digest alone does not
	// authorize swapping the executor. A different revision at the same catalog
	// digest is a RevisionMismatchError, not a silent overwrite.
	if kind == Builtin && existing.revision != revision {
		return RegisteredExtension{}, &RevisionMismatchError{
			Domain: domain, Version: version, Digest: existingDigest,
			Existing: existing.revision, Incoming: revision,
		}
	}
	return cloneRegistered(existing), nil
}

// RegisterBuiltin validates and records a code-registered executor as a Builtin
// Extension. def carries the COMMON Definition (domain, version, Agent-visible
// catalog) and NO HTTP binding — a Builtin must not construct a fake HTTP
// binding. exec is the code executor (required, non-nil). revision is the
// stable implementation identity (required, non-empty); it is INCLUDED in the
// runtime digest so a revision change is digest-visible.
//
// The Builtin digest is content-derived by the registry over the common Catalog
// PLUS the revision (computeBuiltinDigest); any caller-supplied def.Digest is
// ignored. The full Builtin identity is (domain, version, revision-inclusive
// digest). Because the revision is part of the digest:
//
//   - The same (domain, version) with the same catalog AND the same revision
//     produce the same digest and are idempotent.
//   - The same catalog but a DIFFERENT revision produces a DIFFERENT digest,
//     which is a *ConflictError at the same (domain, version) (the revision
//     change is visible as a new identity, not a silent executor swap).
//   - A different catalog at the same (domain, version) is also a
//     *ConflictError.
//   - The same (domain, version) already registered as External returns
//     *ConflictError or *KindMismatchError (kind is immutable identity).
func (r *Registry) RegisterBuiltin(def Definition, exec Executor, revision string) (RegisteredExtension, error) {
	return r.RegisterBuiltinWithOptions(def, exec, revision, BuiltinOptions{
		ConnectionPolicy: ConnectionRequired,
	})
}

// RegisterBuiltinWithOptions is RegisterBuiltin with explicit trusted runtime
// policy. ConnectionRequired uses the original v1 digest projection so adding
// this API does not invalidate any existing Builtin digest. ConnectionNone
// uses a policy-bearing v2 projection so connection semantics are digest-pinned
// and cannot change without producing a new identity.
func (r *Registry) RegisterBuiltinWithOptions(
	def Definition,
	exec Executor,
	revision string,
	options BuiltinOptions,
) (RegisteredExtension, error) {
	if def.Domain == "" {
		return RegisteredExtension{}, ErrMissingDomain
	}
	// Full shared catalog validation: the SAME catalog validator an External
	// passes through, scoped to the fields a Builtin carries.
	if err := validateBuiltinCatalog(def); err != nil {
		return RegisteredExtension{}, err
	}
	if isNilExecutor(exec) {
		return RegisteredExtension{}, ErrBuiltinMissingExecutor
	}
	if revision == "" {
		return RegisteredExtension{}, ErrBuiltinMissingRevision
	}
	if options.ConnectionPolicy != ConnectionRequired &&
		options.ConnectionPolicy != ConnectionNone {
		return RegisteredExtension{}, ErrInvalidConnectionPolicy
	}
	if options.CatalogPolicy != nil && isNilCatalogPolicy(options.CatalogPolicy) {
		return RegisteredExtension{}, ErrInvalidCatalogPolicy
	}
	digest, err := computeBuiltinDigestWithPolicy(
		def,
		revision,
		options.ConnectionPolicy,
	)
	if err != nil {
		return RegisteredExtension{}, fmt.Errorf("registry: compute builtin digest for %q: %w", def.Domain, err)
	}
	def.Digest = digest

	r.mu.Lock()
	defer r.mu.Unlock()

	key := identityKey(def.Domain, def.Version)
	existing, ok := r.byIdentity[key]
	if ok {
		resolved, err := r.resolveIdentityLocked(
			existing, digest, Builtin, revision, def.Domain, def.Version)
		if err != nil {
			return RegisteredExtension{}, err
		}
		// The catalog policy facet is trusted authority behavior under the
		// same immutable-by-digest contract as the executor and revision:
		// re-registration may not introduce or swap it.
		if catalogPolicyType(
			r.byDigest[existing].catalogPolicy,
		) != catalogPolicyType(options.CatalogPolicy) {
			return RegisteredExtension{}, fmt.Errorf(
				"%w: %s",
				ErrCatalogPolicyMismatch,
				digest,
			)
		}
		return resolved, nil
	}
	if _, exists := r.byDigest[digest]; exists {
		return RegisteredExtension{}, fmt.Errorf(
			"registry: digest %s already registered under a different (domain, version) identity", digest)
	}

	entry := &registeredEntry{
		kind:             Builtin,
		connectionPolicy: options.ConnectionPolicy,
		catalogPolicy:    options.CatalogPolicy,
		definition:       buildStoredDefinition(def),
		executor:         exec,
		revision:         revision,
	}
	r.byDigest[digest] = entry
	r.byIdentity[key] = digest
	return cloneRegistered(entry), nil
}

// LookupByDigest returns a deep clone of the Extension registered under digest,
// or ok=false if no such digest is registered. It never returns a stale pointer
// on a miss.
func (r *Registry) LookupByDigest(digest string) (RegisteredExtension, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	entry, ok := r.byDigest[digest]
	if !ok {
		return RegisteredExtension{}, false
	}
	return cloneRegistered(entry), true
}

// LookupByIdentity returns a deep clone of the Extension registered under
// (domain, version), or ok=false if no such identity is registered.
func (r *Registry) LookupByIdentity(domain string, version int) (RegisteredExtension, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	digest, ok := r.byIdentity[identityKey(domain, version)]
	if !ok {
		return RegisteredExtension{}, false
	}
	return cloneRegistered(r.byDigest[digest]), true
}

// List returns deep clones of every registered Extension, sorted by domain then
// version (ascending). The returned slice and every element are independent of
// cached state.
func (r *Registry) List() []RegisteredExtension {
	r.mu.RLock()
	entries := make([]*registeredEntry, 0, len(r.byDigest))
	for _, e := range r.byDigest {
		entries = append(entries, e)
	}
	r.mu.RUnlock()

	sort.Slice(entries, func(i, j int) bool {
		di, dj := entries[i].definition.Domain, entries[j].definition.Domain
		if di != dj {
			return di < dj
		}
		return entries[i].definition.Version < entries[j].definition.Version
	})

	out := make([]RegisteredExtension, len(entries))
	for i, e := range entries {
		out[i] = cloneRegistered(e)
	}
	return out
}

// Len reports the number of registered Extensions (distinct digests).
func (r *Registry) Len() int {
	r.mu.RLock()
	defer r.mu.RUnlock()
	return len(r.byDigest)
}

// BuiltinExecutorFor returns the Executor and revision registered under digest,
// or ok=false if the digest is not registered OR is registered as an External
// (External has no code executor). This is the ONLY executor access path:
// executors are shared by reference (they are caller-supplied behavior, not
// cloned data), so they are deliberately excluded from the clone-on-read
// Lookup*/List projections. ok=true means the digest is a registered Builtin;
// the returned Executor is always non-nil in that case.
func (r *Registry) BuiltinExecutorFor(digest string) (Executor, string, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	entry, ok := r.byDigest[digest]
	if !ok || entry.kind != Builtin {
		return nil, "", false
	}
	return entry.executor, entry.revision, true
}

// CatalogPolicy is an optional provider-owned facet consulted during App
// catalog projection: it filters the scope-derived action set against live
// connection capability state so the catalog never advertises actions the
// current Connection cannot serve. It travels with the Extension composite
// through bootstrap registration and is keyed by definition digest.
type CatalogPolicy interface {
	// FilterActions returns the capability-filtered action subset. available
	// reports whether the Extension is usable at all under the given
	// connections.
	FilterActions(input CatalogPolicyInput) (actions []string, available bool, err error)
}

// CatalogPolicyInput carries the server-resolved projection state one
// provider facet may filter on. Connections are the secret-free identities
// of the selection's active App connections (never credentials); providers
// decode their own Config payloads and enforce their connection contract
// (count, subject identity, credential-generation digest).
type CatalogPolicyInput struct {
	WorkspaceID           string
	ServiceDefinitionID   string
	ServiceInstallationID string
	DefinitionDomain      string
	DefinitionVersion     int
	DefinitionDigest      string
	Connections           []CatalogConnection
	GrantedScopes         []string
	// ExplicitAuthorization reports whether an explicit per-principal
	// authorization (not the App's default-allow policy) granted this
	// surface. Providers with an explicit-only product contract must treat
	// false as unavailable regardless of the durable capability mode.
	ExplicitAuthorization bool
	Actions               []string
}

// CatalogConnection is the secret-free identity of one active App connection.
type CatalogConnection struct {
	ID                      string
	WorkspaceID             string
	ServiceDefinitionID     string
	ServiceInstallationID   string
	ServiceKind             string
	Status                  string
	ExternalSubjectRef      string
	CurrentCredentialDigest string
	Config                  []byte
}

// ErrCatalogPolicyMismatch rejects a Builtin re-registration that carries a
// different catalog policy facet: provider authority behavior is pinned by
// the same immutable-by-digest contract as the executor and revision, and
// cannot be introduced or swapped after the first registration.
var ErrCatalogPolicyMismatch = errors.New(
	"registry: catalog policy mismatch for an already-registered definition",
)

// isNilCatalogPolicy rejects untyped AND typed nils: a typed nil (for
// example a nil *T stored in the interface) is not a registrable facet —
// it would later panic at FilterActions — and is treated as absent, the
// same fail-closed guard the Executor registration applies.
func isNilCatalogPolicy(policy CatalogPolicy) bool {
	if policy == nil {
		return true
	}
	value := reflect.ValueOf(policy)
	switch value.Kind() {
	case reflect.Chan, reflect.Func, reflect.Interface,
		reflect.Map, reflect.Pointer, reflect.Slice, reflect.UnsafePointer:
		return value.IsNil()
	default:
		return false
	}
}

// catalogPolicyType reports the dynamic type of a policy facet so
// re-registration can compare authority behavior identity without
// requiring stateless facets to be pointer-equal singletons. Typed nils
// are absent facets.
func catalogPolicyType(policy CatalogPolicy) reflect.Type {
	if isNilCatalogPolicy(policy) {
		return nil
	}
	return reflect.TypeOf(policy)
}

// ErrInvalidCatalogPolicy rejects a registration attempt that carries a
// typed-nil catalog policy: an explicitly-provided facet must be callable.
var ErrInvalidCatalogPolicy = errors.New(
	"registry: catalog policy must not be a nil pointer",
)

// CatalogPolicyFor resolves the provider catalog policy facet pinned to a
// definition digest, if the provider registered one.
func (r *Registry) CatalogPolicyFor(digest string) (CatalogPolicy, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	entry, ok := r.byDigest[digest]
	if !ok || entry.kind != Builtin || entry.catalogPolicy == nil {
		return nil, false
	}
	return entry.catalogPolicy, true
}

// identityKey is the registry's identity index key for (domain, version). The
// NUL separator prevents any collision between a domain containing the
// separator character and a version prefix (domains are kebab-case, so this is
// defense-in-depth).
func identityKey(domain string, version int) string {
	return domain + "\x00" + strconv.Itoa(version)
}
