package serverruntime

import (
	"context"
	"database/sql"
	"fmt"
	"path"
	"sort"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/auth"
	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/backend/internal/application/services"
	"github.com/nexuscrm/backend/internal/features/streaming"
	eventfeature "github.com/nexuscrm/events"
	"github.com/nexuscrm/files"
	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/approvalir"
	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

const (
	PackSourceKindOfficialRepo = "official_repo"
	PackSourceKindLocalPath    = "local_path"
)

type ActiveAppPack struct {
	Name               string
	SourceKind         string
	SourceURI          string
	SourceRevision     string
	AppliedPayloadHash string
	DesiredPayloadHash string
}

type HostPrimitive struct {
	Kind    string
	Name    string
	Version string
}

type DetachedReferenceResolverRegistration struct {
	SourceType     string
	Resolver       files.DetachedReferenceResolver
	OwningAppPacks []string

	// Filled from OwningAppPacks during composition; app-specific detached blob
	// formats are visible only while one of their owning packs is active.
	owningAppPacks []string
}

type OfficialRepoSourceRevision struct {
	Name     string
	Revision string
}

type RuntimeRouteSurfaceRegistration struct {
	Name                  string
	Register              func(RuntimeRouteSurfaceContext) error
	ReplacesDialectRoutes []DialectRouteReplacement

	// Filled from ProductHooks.OwningAppPacks during composition; runtime route
	// surfaces are visible only while one of their owning packs is active.
	owningAppPacks []string
}

type DialectRouteReplacement struct {
	DialectName string
	Method      string
	Path        string
	Handler     dialect.Handler
}

type RuntimeRouteSurfaceContext struct {
	Router                       *gin.Engine
	DB                           *sql.DB
	ServiceManager               *services.ServiceManager
	SessionManager               *streaming.SessionManager
	DataAPIMiddleware            []gin.HandlerFunc
	AuthenticatedScopeMiddleware []gin.HandlerFunc
}

type DynamicCORSRouteContext struct {
	Path            string
	FullPath        string
	RequestedMethod string
}

type DynamicCORSRouteFamilyRegistration struct {
	Name   string
	Allows func(DynamicCORSRouteContext) bool

	// Filled from ProductHooks.OwningAppPacks during composition; product-only
	// dynamic CORS route families are consulted only while one of their owning
	// packs is active.
	owningAppPacks []string
}

type OAuthScopeRouteContext struct {
	Scope    string
	Method   string
	FullPath string
}

type OAuthScopeRouteFamilyRegistration struct {
	Name   string
	Scopes []string
	Allows func(OAuthScopeRouteContext) bool

	// RequireActiveSourceScope marks a family whose scope(s) are source-backed
	// custom scopes: a family match is then honored only if the scope is still
	// active and owned by the caller's Connected App, failing closed if that
	// cannot be confirmed. Leave false for pure product/platform route scopes
	// (e.g. chatter_api, openid) that carry no ownership/active constraint.
	RequireActiveSourceScope bool

	// Filled from ProductHooks.OwningAppPacks during composition; product-only
	// OAuth scope route families are consulted only while one of their
	// owning packs is active.
	owningAppPacks []string
}

type OAuthRouteScopeRegistration struct {
	Scope string

	// Filled from ProductHooks.OwningAppPacks during composition; product-owned
	// OAuth route scope vocabulary is visible only while one of its owning packs
	// is active. Tenant/admin custom scopes remain metadata-owned by
	// _System_OAuthCustomScope and are not declared here.
	owningAppPacks []string
}

type AuthChallengeContext struct {
	Path         string
	MissingToken bool
}

type AuthChallenge struct {
	Status  int
	Code    string
	Message string
}

type AuthChallengeProfileRegistration struct {
	ProfileID string
	Build     func(AuthChallengeContext) (AuthChallenge, bool)

	// Filled from ProductHooks.OwningAppPacks during composition; product-only
	// auth challenge wire shapes apply only while one of their owning packs is
	// active.
	owningAppPacks []string
}

type PublicLoginProviderRegistration struct {
	ProviderID string
	Aliases    []string
	Build      func(auth.PublicLoginProviderContext) auth.PublicLoginProviderDescriptor

	// Filled from ProductHooks.OwningAppPacks during composition; product login
	// providers are advertised only while one of their owning packs is active.
	owningAppPacks []string
}

type OAuthUserInfoProfileRegistration struct {
	ProfileID string
	Applies   func(auth.UserInfoProfileContext) bool
	Build     func(auth.UserInfoProfileContext) map[string]any

	// Filled from ProductHooks.OwningAppPacks during composition; product OAuth
	// userinfo compatibility profiles are visible only while one of their owning
	// packs is active. Applies still performs request-level selection before a
	// profile can shape the shared OAuth userinfo response.
	owningAppPacks []string
}

type OAuthSessionIssuerClassRegistration struct {
	Class string

	// Filled from ProductHooks.OwningAppPacks during composition; product OAuth
	// session issuer classes are valid Connected App metadata only while one of
	// their owning packs is active.
	owningAppPacks []string
}

type OAuthTokenResponseProfileRegistration struct {
	ProfileID string
	Applies   func(auth.TokenResponseProfileContext) bool
	Build     func(auth.TokenResponseProfileContext) map[string]any

	// Filled from ProductHooks.OwningAppPacks during composition; product OAuth
	// token response compatibility profiles are visible only while one of their
	// owning packs is active. Applies still performs request-level selection
	// before a profile can shape the shared OAuth token response.
	owningAppPacks []string
}

// ProductCapabilities are host capabilities supplied by one L1 product. They
// are composed by ProductDistribution before being installed into process-local
// platform registries.
type ProductCapabilities struct {
	HostPrimitives []HostPrimitive

	// OfficialRepoSourceRevisions are per-product source-resolution locks
	// compiled into the distribution. They identify commits that the pack source
	// loader may resolve from the release's official-repository checkout. They
	// are not the release checkout revision and are not an installed-pack source
	// tuple; those authorities are owned by release metadata and
	// _System_InstalledPack respectively.
	OfficialRepoSourceRevisions []OfficialRepoSourceRevision

	// ComponentKindRegistrationProviders are L1-owned constructors for neutral
	// managed component registrations. Product composition evaluates them once
	// and owns the resulting immutable registration snapshot.
	ComponentKindRegistrationProviders []ComponentKindRegistrationProvider

	// ComponentKindManagedLifecycleRegistrationProviders are L1-owned
	// constructors for additive managed lifecycle registrations. Product
	// composition evaluates them once and binds them into the same immutable
	// registration snapshot as legacy-compatible registrations.
	ComponentKindManagedLifecycleRegistrationProviders []ComponentKindManagedLifecycleRegistrationProvider

	// ApprovalDefinitionDecoder lowers the product-owned approval grammar into
	// neutral approval semantics. Products without an approval grammar leave it
	// nil; composing more than one decoder is rejected as ambiguous.
	ApprovalDefinitionDecoder approvalir.DefinitionDecoder

	// ApprovalQueryOnlyOwnerPacks is the bounded subset of this product's
	// OwningAppPacks whose active approval topology is proven before serving.
	// Submit may retire compatibility materialization only for these owners.
	ApprovalQueryOnlyOwnerPacks []string

	// ConnectorInboundRemoteIdentityRecoveryValidator supplies the product-owned
	// source lifecycle policy used by the explicit recovery action and headless
	// maintenance command. L0 owns locking and durable identity; a product may
	// supply at most one pure validator for a composed distribution.
	ConnectorInboundRemoteIdentityRecoveryValidator automation.ConnectorInboundRemoteIdentityRecoveryLockedRecordValidator

	RuntimeRouteSurfaces []RuntimeRouteSurfaceRegistration

	DynamicCORSRouteFamilies []DynamicCORSRouteFamilyRegistration

	OAuthRouteScopes []OAuthRouteScopeRegistration

	OAuthScopeRouteFamilies []OAuthScopeRouteFamilyRegistration

	// OAuthRouteFamilyPrefix is the single app-blind prefix the host installs
	// into the OAuth scope readers (ScopeRegistry + PublicClientOriginRegistry)
	// to canonicalize and validate stored route patterns. A product that owns an
	// OAuth scope route family supplies it; the host fails closed at startup if a
	// route family is active but this is empty.
	OAuthRouteFamilyPrefix string

	AuthChallengeProfiles []AuthChallengeProfileRegistration

	PublicLoginProviders []PublicLoginProviderRegistration

	OAuthUserInfoProfiles []OAuthUserInfoProfileRegistration

	OAuthSessionIssuerClasses []OAuthSessionIssuerClassRegistration

	OAuthTokenResponseProfiles []OAuthTokenResponseProfileRegistration

	FlowActionCallHandlerImplementations []automation.FlowActionCallHandlerImplementation

	DetachedReferenceResolvers []DetachedReferenceResolverRegistration

	StreamingEventChannelMappings []streaming.EventChannelMapping

	NoSubscriberRecordEventObjects []string

	componentKindRegistrationSet           packs.ComponentKindRegistrationSet
	streamingEventChannelMappingsByProduct []productStreamingEventChannelMappings
}

type productStreamingEventChannelMappings struct {
	OwningAppPacks []string
	Mappings       []streaming.EventChannelMapping
}

type DialectRegistrationContext struct {
	DB                  *sql.DB
	HrefFieldsForObject func(objectAPIName string) []string
}

type ReportDefinitionDialectInstaller interface {
	SetReportDefinitionDialect(contract.ReportDefinitionDialect)
	RegisterVirtualReportSource(contract.VirtualReportSourceRegistration) error
}

type RecordLifecyclePersistence interface {
	Insert(ctx context.Context, objectName string, data models.SObject, currentUser *models.UserSession) (models.SObject, error)
	PhysicalDeleteRaw(ctx context.Context, objectName string, id string) error
	RetrieveByField(ctx context.Context, objectName string, fieldName string, fieldValue string, user *models.UserSession) (models.SObject, error)
}

type RecordLifecycleUserLookup interface {
	RetrieveByField(ctx context.Context, objectName string, fieldName string, fieldValue string, user *models.UserSession) (models.SObject, error)
}

type ProductLifecycle struct {
	ActiveAppPacks []ActiveAppPack
	DB             *sql.DB
	// TxManager exposes the transaction already owned by the records engine to
	// product lifecycle hooks. Product code must join that transaction; it must
	// never open a nested transaction for a record lifecycle side effect.
	TxManager             ports.TransactionManager
	AuthorizationGrants   ports.AuthorizationGrantRepository
	ReportDefinitions     ReportDefinitionDialectInstaller
	RecordLifecycle       contract.RecordLifecycleRegistrar
	RecordWriteValidation contract.RecordWriteValidatorRegistrar
	// AssignmentPolicyEligibilityMutations is the permissions-owned deep seam
	// for staging product eligibility transitions in the records transaction.
	// L1 does not receive the controlplane host, target, or attestation surface.
	AssignmentPolicyEligibilityMutations permissions.AssignmentPolicyEligibilityMutationBatchHandler
	// Query and Persistence remain populated for out-of-tree L1 compatibility.
	// New lifecycle writers must use OpenComponentMutationStore so the product
	// registration write set and caller-owned transaction remain authoritative.
	Query                      contract.Query
	Persistence                RecordLifecyclePersistence
	OpenComponentMutationStore ComponentMutationStoreOpener
	UserLookup                 RecordLifecycleUserLookup
}

type ProductHooks struct {
	// OwningAppPacks lists the app-pack names this product owns. Composition
	// validators are scoped to these packs when active app-packs are present.
	// Leave empty only for global host validators such as the app-blind default
	// public-core guard.
	OwningAppPacks []string

	// KnownProductAppPacks lists app-pack names that this L1 source family
	// recognizes as owning compiled product runtime surfaces. The composed
	// distribution uses the list to reject a partially-served install when a known
	// product pack is active but absent from the current binary. L0 treats the
	// values as L1-supplied facts.
	KnownProductAppPacks []string

	Capabilities ProductCapabilities

	Dialects func(DialectRegistrationContext) []dialect.Dialect

	ValidateComposition func(context.Context, *sql.DB, []ActiveAppPack) error

	RegisterLifecycle func(ProductLifecycle) error

	// AssignmentPolicyEligibilityProjection is the bounded legacy product
	// adapter paired with this product's managed lifecycle registration. The
	// independent Runtime instead compiles the same fact from Catalog metadata.
	AssignmentPolicyEligibilityProjection permissions.AssignmentPolicyEligibilityProjectionProvider

	// InternalErrorProjector supplies the wire body for a generic internal (5xx)
	// error on the routes this product owns. App-blind L0 middleware holds only this
	// neutral callback; the projector returns (0, nil) for requests it does not own,
	// so other products / legacy routes fall back to L0's neutral default. L1 supplies
	// the product-specific wire projection; nil = neutral default.
	InternalErrorProjector func(c *gin.Context) (status int, body any)

	// AuthFailureProjector supplies the wire body for an authentication failure
	// (e.g. missing/expired/invalid session). As with InternalErrorProjector,
	// app-blind L0 auth middleware holds only this neutral callback; L1 supplies
	// the product-specific wire projection (e.g. the Salesforce v63 error-array
	// shape). nil = L0's neutral auth-failure default.
	AuthFailureProjector func(c *gin.Context, status int, code, message string) (int, any)
}

// ProductDistribution is the ordered set of L1 product hooks compiled into a
// host binary. The order is significant for dialect registration, validation,
// and lifecycle hooks.
type ProductDistribution struct {
	Products []ProductHooks
}

// NewProductDistribution returns a distribution with a defensive copy of the
// supplied product hooks.
func NewProductDistribution(products ...ProductHooks) ProductDistribution {
	return ProductDistribution{Products: append([]ProductHooks(nil), products...)}
}

// NoopProductDistribution is the app-blind public-core distribution.
func NoopProductDistribution() ProductDistribution {
	return NewProductDistribution(NoopProductHooks())
}

func NoopProductHooks() ProductHooks {
	return ProductHooks{
		ValidateComposition: rejectActiveAppPacksForAppBlindBuild,
	}
}

func normalizeProductHooks(hooks ProductHooks) ProductHooks {
	if hooks.ValidateComposition == nil {
		hooks.ValidateComposition = func(context.Context, *sql.DB, []ActiveAppPack) error { return nil }
	}
	if hooks.RegisterLifecycle == nil {
		hooks.RegisterLifecycle = func(ProductLifecycle) error { return nil }
	}
	return hooks
}

// ComposeProductDistribution merges product hooks into a single runtime view.
// Host primitives dedupe by kind/name only when their versions agree; flow
// action-call handlerRefs must be unique. Dialect route conflicts remain
// fail-closed in the dialect transport binder after the ordered dialect set is
// registered. Detached reference resolvers are source-type scoped and
// app-pack gated so app-owned legacy blob formats do not become a global
// first-match parser.
func ComposeProductDistribution(distribution ProductDistribution) (ProductHooks, error) {
	products := append([]ProductHooks(nil), distribution.Products...)
	if len(products) == 0 {
		products = []ProductHooks{NoopProductHooks()}
	}
	normalized := make([]ProductHooks, 0, len(products))
	for _, product := range products {
		normalized = append(normalized, normalizeProductHooks(product))
	}
	return composeProductHooks(normalized)
}

// RegisterProductDistributionCapabilities installs the distribution-owned host
// primitives and flow action-call handler implementations into the process
// registries shared by setup and runtime.
func RegisterProductDistributionCapabilities(distribution ProductDistribution) (ProductHooks, error) {
	hooks, err := ComposeProductDistribution(distribution)
	if err != nil {
		return ProductHooks{}, err
	}
	registerProductHostPrimitives(hooks)
	registerProductOfficialRepoSourceRevisions(hooks)
	registerProductOAuthRouteScopes(hooks)
	registerProductOAuthSessionIssuerClasses(hooks)
	registerProductFlowActionCallHandlerImplementations(hooks)
	registerProductNoSubscriberRecordEventObjects(hooks)
	return hooks, nil
}

func composeProductHooks(products []ProductHooks) (ProductHooks, error) {
	if len(products) == 0 {
		return normalizeProductHooks(ProductHooks{}), nil
	}
	products = append([]ProductHooks(nil), products...)
	capabilities, err := composeProductCapabilities(products)
	if err != nil {
		return ProductHooks{}, err
	}
	owningAppPacks := composeOwningAppPacks(products)
	knownProductAppPacks := composeKnownProductAppPacks(products)
	assignmentPolicyEligibility, err := composeAssignmentPolicyEligibilityProjection(products)
	if err != nil {
		return ProductHooks{}, err
	}
	return ProductHooks{
		OwningAppPacks:                        owningAppPacks,
		KnownProductAppPacks:                  knownProductAppPacks,
		Capabilities:                          capabilities,
		AssignmentPolicyEligibilityProjection: assignmentPolicyEligibility,
		Dialects: func(ctx DialectRegistrationContext) []dialect.Dialect {
			var out []dialect.Dialect
			for productIndex, product := range products {
				if product.Dialects == nil {
					continue
				}
				declared := product.Dialects(ctx)
				mustValidateProductDialectOwnership(productIndex, product, declared)
				out = append(out, declared...)
			}
			return out
		},
		ValidateComposition: func(ctx context.Context, db *sql.DB, activeAppPacks []ActiveAppPack) error {
			if err := validateActiveProductPackCoverage(owningAppPacks, knownProductAppPacks, activeAppPacks); err != nil {
				return err
			}
			for i, product := range products {
				if !shouldRunProductCompositionValidator(product, activeAppPacks) {
					continue
				}
				if err := product.ValidateComposition(ctx, db, activeAppPacks); err != nil {
					return fmt.Errorf("product[%d] composition validation: %w", i, err)
				}
			}
			return nil
		},
		RegisterLifecycle: func(lifecycle ProductLifecycle) error {
			for i, product := range products {
				if err := product.RegisterLifecycle(lifecycle); err != nil {
					return fmt.Errorf("product[%d] lifecycle registration: %w", i, err)
				}
			}
			return nil
		},
	}, nil
}

func composeAssignmentPolicyEligibilityProjection(
	products []ProductHooks,
) (permissions.AssignmentPolicyEligibilityProjectionProvider, error) {
	var provider permissions.AssignmentPolicyEligibilityProjectionProvider
	for index, product := range products {
		if product.AssignmentPolicyEligibilityProjection == nil {
			continue
		}
		if provider != nil {
			return nil, fmt.Errorf("product[%d] declares a second assignment-policy eligibility projection provider", index)
		}
		provider = product.AssignmentPolicyEligibilityProjection
	}
	return provider, nil
}

func mustValidateProductDialectOwnership(productIndex int, product ProductHooks, declared []dialect.Dialect) {
	owningAppPacks := productOwningAppPackNames(product)
	for dialectIndex, d := range declared {
		if d == nil {
			continue
		}
		name := strings.TrimSpace(d.Name())
		if name == "" {
			panic(fmt.Sprintf("serverruntime: product[%d] dialect[%d] returned empty Name", productIndex, dialectIndex))
		}
		owner := strings.TrimSpace(d.OwningPack())
		if owner == "" {
			panic(fmt.Sprintf("serverruntime: product[%d] dialect %q requires non-empty owning pack", productIndex, name))
		}
		if len(owningAppPacks) == 0 {
			panic(fmt.Sprintf("serverruntime: product[%d] dialect %q declares owning pack %q but ProductHooks.OwningAppPacks is empty", productIndex, name, owner))
		}
		if _, ok := owningAppPacks[strings.ToLower(owner)]; !ok {
			panic(fmt.Sprintf("serverruntime: product[%d] dialect %q declares owning pack %q outside ProductHooks.OwningAppPacks %v", productIndex, name, owner, sortedNormalizedNames(product.OwningAppPacks)))
		}
	}
}

func composeKnownProductAppPacks(products []ProductHooks) []string {
	var out []string
	seen := map[string]struct{}{}
	for _, product := range products {
		for _, rawName := range product.KnownProductAppPacks {
			name := strings.TrimSpace(rawName)
			if name == "" {
				continue
			}
			key := strings.ToLower(name)
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			out = append(out, name)
		}
	}
	return out
}

func composeOwningAppPacks(products []ProductHooks) []string {
	var out []string
	seen := map[string]struct{}{}
	for _, product := range products {
		for _, rawName := range product.OwningAppPacks {
			name := strings.TrimSpace(rawName)
			if name == "" {
				continue
			}
			key := strings.ToLower(name)
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			out = append(out, name)
		}
	}
	return out
}

func shouldRunProductCompositionValidator(product ProductHooks, activeAppPacks []ActiveAppPack) bool {
	if len(activeAppPacks) == 0 {
		return true
	}
	ownedPackNames := productOwningAppPackNames(product)
	if len(ownedPackNames) == 0 {
		return true
	}
	for _, activePack := range activeAppPacks {
		name := strings.ToLower(strings.TrimSpace(activePack.Name))
		if name == "" {
			continue
		}
		if _, ok := ownedPackNames[name]; ok {
			return true
		}
	}
	return false
}

func validateActiveProductPackCoverage(owningAppPacks, knownProductAppPacks []string, activeAppPacks []ActiveAppPack) error {
	activeNames := sortedRuntimeActiveAppPackNames(activeAppPacks)
	if len(activeNames) == 0 || len(owningAppPacks) == 0 {
		return nil
	}

	owned := normalizedNameSet(owningAppPacks)
	var activeOwned bool
	for _, activeName := range activeNames {
		if _, ok := owned[strings.ToLower(activeName)]; ok {
			activeOwned = true
			break
		}
	}
	if !activeOwned {
		return fmt.Errorf(
			"product distribution bundles product app pack(s) %s, but active app pack(s) are installed: %s; start a distribution build that includes those packs or deactivate them",
			strings.Join(sortedNormalizedNames(owningAppPacks), ", "),
			strings.Join(activeNames, ", "),
		)
	}

	known := normalizedNameSet(knownProductAppPacks)
	if len(known) == 0 {
		return nil
	}
	var missing []string
	for _, activeName := range activeNames {
		key := strings.ToLower(activeName)
		if _, ok := known[key]; !ok {
			continue
		}
		if _, ok := owned[key]; ok {
			continue
		}
		missing = append(missing, activeName)
	}
	if len(missing) > 0 {
		sort.Strings(missing)
		return fmt.Errorf(
			"active first-party product app pack(s) %s are not compiled into this product distribution (compiled product app pack(s): %s); start a distribution build that includes those packs or deactivate them",
			strings.Join(missing, ", "),
			strings.Join(sortedNormalizedNames(owningAppPacks), ", "),
		)
	}
	return nil
}

func normalizedNameSet(names []string) map[string]struct{} {
	out := map[string]struct{}{}
	for _, rawName := range names {
		name := strings.ToLower(strings.TrimSpace(rawName))
		if name == "" {
			continue
		}
		out[name] = struct{}{}
	}
	return out
}

func sortedNormalizedNames(names []string) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(names))
	for _, rawName := range names {
		name := strings.TrimSpace(rawName)
		if name == "" {
			continue
		}
		key := strings.ToLower(name)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, name)
	}
	sort.Strings(out)
	return out
}

func productOwningAppPackNames(product ProductHooks) map[string]struct{} {
	out := map[string]struct{}{}
	for _, rawName := range product.OwningAppPacks {
		name := strings.ToLower(strings.TrimSpace(rawName))
		if name == "" {
			continue
		}
		out[name] = struct{}{}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func composeProductCapabilities(products []ProductHooks) (ProductCapabilities, error) {
	hostPrimitives, err := composeHostPrimitives(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	componentKindRegistrationSet, err := composeProductComponentKindRegistrationSet(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	approvalDefinitionDecoder, approvalQueryOnlyOwnerPacks, err := composeApprovalDefinitionDecoder(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	connectorInboundRecoveryValidator, err := composeConnectorInboundRemoteIdentityRecoveryValidator(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	flowHandlers, err := composeFlowActionCallHandlerImplementations(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	detachedResolvers, err := composeDetachedReferenceResolvers(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	officialRepoSourceRevisions, err := composeOfficialRepoSourceRevisions(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	runtimeRouteSurfaces, err := composeRuntimeRouteSurfaces(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	dynamicCORSRouteFamilies, err := composeDynamicCORSRouteFamilies(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	oauthRouteScopes, oauthRouteScopeProductIndexes, err := composeOAuthRouteScopes(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	oauthScopeRouteFamilies, err := composeOAuthScopeRouteFamilies(products, oauthRouteScopeProductIndexes)
	if err != nil {
		return ProductCapabilities{}, err
	}
	oauthRouteFamilyPrefix, err := composeOAuthRouteFamilyPrefix(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	authChallengeProfiles, err := composeAuthChallengeProfiles(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	publicLoginProviders, err := composePublicLoginProviders(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	oauthUserInfoProfiles, err := composeOAuthUserInfoProfiles(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	oauthSessionIssuerClasses, err := composeOAuthSessionIssuerClasses(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	oauthTokenResponseProfiles, err := composeOAuthTokenResponseProfiles(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	noSubscriberRecordEventObjects, err := composeNoSubscriberRecordEventObjects(products)
	if err != nil {
		return ProductCapabilities{}, err
	}
	streamingMappings, streamingMappingsByProduct := composeStreamingEventChannelMappings(products)
	return ProductCapabilities{
		HostPrimitives:                                  hostPrimitives,
		OfficialRepoSourceRevisions:                     officialRepoSourceRevisions,
		componentKindRegistrationSet:                    componentKindRegistrationSet,
		ApprovalDefinitionDecoder:                       approvalDefinitionDecoder,
		ApprovalQueryOnlyOwnerPacks:                     approvalQueryOnlyOwnerPacks,
		ConnectorInboundRemoteIdentityRecoveryValidator: connectorInboundRecoveryValidator,
		RuntimeRouteSurfaces:                            runtimeRouteSurfaces,
		DynamicCORSRouteFamilies:                        dynamicCORSRouteFamilies,
		OAuthRouteScopes:                                oauthRouteScopes,
		OAuthScopeRouteFamilies:                         oauthScopeRouteFamilies,
		OAuthRouteFamilyPrefix:                          oauthRouteFamilyPrefix,
		AuthChallengeProfiles:                           authChallengeProfiles,
		PublicLoginProviders:                            publicLoginProviders,
		OAuthUserInfoProfiles:                           oauthUserInfoProfiles,
		OAuthSessionIssuerClasses:                       oauthSessionIssuerClasses,
		OAuthTokenResponseProfiles:                      oauthTokenResponseProfiles,
		FlowActionCallHandlerImplementations:            flowHandlers,
		DetachedReferenceResolvers:                      detachedResolvers,
		StreamingEventChannelMappings:                   streamingMappings,
		NoSubscriberRecordEventObjects:                  noSubscriberRecordEventObjects,
		streamingEventChannelMappingsByProduct:          streamingMappingsByProduct,
	}, nil
}

func composeConnectorInboundRemoteIdentityRecoveryValidator(
	products []ProductHooks,
) (automation.ConnectorInboundRemoteIdentityRecoveryLockedRecordValidator, error) {
	var validator automation.ConnectorInboundRemoteIdentityRecoveryLockedRecordValidator
	validatorProductIndex := -1
	for productIndex, product := range products {
		candidate := product.Capabilities.ConnectorInboundRemoteIdentityRecoveryValidator
		if candidate == nil {
			continue
		}
		if validator != nil {
			return nil, fmt.Errorf(
				"multiple connector inbound remote identity recovery validators across composed products: product[%d] and product[%d]",
				validatorProductIndex,
				productIndex,
			)
		}
		validator = candidate
		validatorProductIndex = productIndex
	}
	return validator, nil
}

func composeApprovalDefinitionDecoder(products []ProductHooks) (approvalir.DefinitionDecoder, []string, error) {
	var decoder approvalir.DefinitionDecoder
	var queryOnlyOwnerPacks []string
	decoderProductIndex := -1
	for productIndex, product := range products {
		candidate := product.Capabilities.ApprovalDefinitionDecoder
		candidateQueryOnlyOwners := product.Capabilities.ApprovalQueryOnlyOwnerPacks
		if candidate == nil && len(candidateQueryOnlyOwners) > 0 {
			return nil, nil, fmt.Errorf("product[%d] declares approval query-only owner packs without an approval definition decoder", productIndex)
		}
		if candidate == nil {
			continue
		}
		if decoder != nil {
			return nil, nil, fmt.Errorf(
				"multiple approval definition decoders across composed products: product[%d] and product[%d]",
				decoderProductIndex,
				productIndex,
			)
		}
		owners, err := validateApprovalQueryOnlyOwnerPacks(productIndex, product, candidateQueryOnlyOwners)
		if err != nil {
			return nil, nil, err
		}
		decoder = candidate
		queryOnlyOwnerPacks = owners
		decoderProductIndex = productIndex
	}
	return decoder, queryOnlyOwnerPacks, nil
}

func validateApprovalQueryOnlyOwnerPacks(productIndex int, product ProductHooks, declared []string) ([]string, error) {
	if len(declared) == 0 {
		return nil, nil
	}
	productOwners := productOwningAppPackNames(product)
	seen := make(map[string]struct{}, len(declared))
	owners := make([]string, 0, len(declared))
	for ownerIndex, rawOwner := range declared {
		owner := strings.TrimSpace(rawOwner)
		if owner == "" || owner != rawOwner {
			return nil, fmt.Errorf("product[%d] approval query-only owner pack[%d] %q is not canonical", productIndex, ownerIndex, rawOwner)
		}
		key := strings.ToLower(owner)
		if _, owned := productOwners[key]; !owned {
			return nil, fmt.Errorf("product[%d] approval query-only owner pack %q is outside ProductHooks.OwningAppPacks", productIndex, owner)
		}
		if _, duplicate := seen[key]; duplicate {
			return nil, fmt.Errorf("product[%d] approval query-only owner pack %q is duplicated", productIndex, owner)
		}
		seen[key] = struct{}{}
		owners = append(owners, owner)
	}
	sort.Strings(owners)
	return owners, nil
}

// composeOAuthRouteFamilyPrefix selects the single app-blind route-family prefix
// the host installs into the OAuth scope readers. At most one product may own
// the route family, so all non-empty declarations must agree; two different
// non-empty prefixes are a composition error.
func composeOAuthRouteFamilyPrefix(products []ProductHooks) (string, error) {
	prefix := ""
	for _, product := range products {
		candidate := strings.TrimSpace(product.Capabilities.OAuthRouteFamilyPrefix)
		if candidate == "" {
			continue
		}
		if prefix != "" && prefix != candidate {
			return "", fmt.Errorf("conflicting OAuthRouteFamilyPrefix across composed products: %q vs %q", prefix, candidate)
		}
		prefix = candidate
	}
	return prefix, nil
}

func composeOAuthUserInfoProfiles(products []ProductHooks) ([]OAuthUserInfoProfileRegistration, error) {
	var out []OAuthUserInfoProfileRegistration
	seen := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.OAuthUserInfoProfiles {
			profileID := auth.NormalizeProductCapabilityToken(registration.ProfileID)
			switch {
			case profileID == "":
				return nil, fmt.Errorf("product[%d] OAuth userinfo profile requires id", productIndex)
			case registration.Applies == nil:
				return nil, fmt.Errorf("product[%d] OAuth userinfo profile %q requires request selector", productIndex, registration.ProfileID)
			case registration.Build == nil:
				return nil, fmt.Errorf("product[%d] OAuth userinfo profile %q requires builder", productIndex, registration.ProfileID)
			case len(owningAppPacks) == 0:
				return nil, fmt.Errorf("product[%d] OAuth userinfo profile %q requires owning app pack", productIndex, registration.ProfileID)
			}
			if earlier, ok := seen[profileID]; ok {
				return nil, fmt.Errorf("product[%d] OAuth userinfo profile %q duplicates product[%d]", productIndex, registration.ProfileID, earlier)
			}
			seen[profileID] = productIndex
			registration.ProfileID = profileID
			registration.owningAppPacks = owningAppPacks
			out = append(out, registration)
		}
	}
	return out, nil
}

func composeOAuthSessionIssuerClasses(products []ProductHooks) ([]OAuthSessionIssuerClassRegistration, error) {
	type pendingOAuthSessionIssuerClass struct {
		productIndex   int
		registration   OAuthSessionIssuerClassRegistration
		owningAppPacks []string
	}

	var pending []pendingOAuthSessionIssuerClass
	owners := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.OAuthSessionIssuerClasses {
			class := auth.NormalizeProductCapabilityToken(registration.Class)
			if class == "" {
				return nil, fmt.Errorf("product[%d] OAuth session issuer class requires class", productIndex)
			}
			if class == auth.OAuthSessionClassPlatformBrowser {
				return nil, fmt.Errorf("product[%d] OAuth session issuer class %q conflicts with platform session issuer class", productIndex, registration.Class)
			}
			if len(owningAppPacks) == 0 {
				return nil, fmt.Errorf("product[%d] OAuth session issuer class %q requires owning app pack", productIndex, registration.Class)
			}
			if earlier, ok := owners[class]; ok {
				return nil, fmt.Errorf("product[%d] OAuth session issuer class %q duplicates product[%d]", productIndex, registration.Class, earlier)
			}
			owners[class] = productIndex
			registration.Class = class
			pending = append(pending, pendingOAuthSessionIssuerClass{
				productIndex:   productIndex,
				registration:   registration,
				owningAppPacks: owningAppPacks,
			})
		}
	}

	out := make([]OAuthSessionIssuerClassRegistration, 0, len(pending))
	for _, item := range pending {
		registration := item.registration
		registration.owningAppPacks = item.owningAppPacks
		out = append(out, registration)
	}
	return out, nil
}

func composeOAuthTokenResponseProfiles(products []ProductHooks) ([]OAuthTokenResponseProfileRegistration, error) {
	var out []OAuthTokenResponseProfileRegistration
	seen := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.OAuthTokenResponseProfiles {
			profileID := auth.NormalizeProductCapabilityToken(registration.ProfileID)
			switch {
			case profileID == "":
				return nil, fmt.Errorf("product[%d] OAuth token response profile requires id", productIndex)
			case registration.Applies == nil:
				return nil, fmt.Errorf("product[%d] OAuth token response profile %q requires request selector", productIndex, registration.ProfileID)
			case registration.Build == nil:
				return nil, fmt.Errorf("product[%d] OAuth token response profile %q requires builder", productIndex, registration.ProfileID)
			case len(owningAppPacks) == 0:
				return nil, fmt.Errorf("product[%d] OAuth token response profile %q requires owning app pack", productIndex, registration.ProfileID)
			}
			if earlier, ok := seen[profileID]; ok {
				return nil, fmt.Errorf("product[%d] OAuth token response profile %q duplicates product[%d]", productIndex, registration.ProfileID, earlier)
			}
			seen[profileID] = productIndex
			registration.ProfileID = profileID
			registration.owningAppPacks = owningAppPacks
			out = append(out, registration)
		}
	}
	return out, nil
}

func composePublicLoginProviders(products []ProductHooks) ([]PublicLoginProviderRegistration, error) {
	type pendingPublicLoginProvider struct {
		productIndex   int
		registration   PublicLoginProviderRegistration
		owningAppPacks []string
	}

	var pending []pendingPublicLoginProvider
	providerOwners := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.PublicLoginProviders {
			providerID := auth.NormalizePublicLoginProviderToken(registration.ProviderID)
			if providerID == "" {
				return nil, fmt.Errorf("product[%d] public login provider requires id", productIndex)
			}
			if builtin := auth.NormalizeBuiltinPublicLoginProviderID(providerID); builtin != "" {
				return nil, fmt.Errorf("product[%d] public login provider %q conflicts with built-in login provider %q", productIndex, registration.ProviderID, builtin)
			}
			if registration.Build == nil {
				return nil, fmt.Errorf("product[%d] public login provider %q requires descriptor builder", productIndex, registration.ProviderID)
			}
			if len(owningAppPacks) == 0 {
				return nil, fmt.Errorf("product[%d] public login provider %q requires owning app pack", productIndex, registration.ProviderID)
			}
			if earlier, ok := providerOwners[providerID]; ok {
				return nil, fmt.Errorf("product[%d] public login provider %q duplicates product[%d]", productIndex, registration.ProviderID, earlier)
			}
			providerOwners[providerID] = productIndex
			registration.ProviderID = providerID
			pending = append(pending, pendingPublicLoginProvider{
				productIndex:   productIndex,
				registration:   registration,
				owningAppPacks: owningAppPacks,
			})
		}
	}

	var out []PublicLoginProviderRegistration
	aliasOwners := map[string]int{}
	for _, item := range pending {
		registration := item.registration
		aliases := normalizedPublicLoginProviderAliases(registration.ProviderID, registration.Aliases)
		for _, alias := range aliases {
			if builtin := auth.NormalizeBuiltinPublicLoginProviderID(alias); builtin != "" {
				return nil, fmt.Errorf("product[%d] public login provider %q alias %q conflicts with built-in login provider %q", item.productIndex, registration.ProviderID, alias, builtin)
			}
			if earlier, ok := providerOwners[alias]; ok {
				return nil, fmt.Errorf("product[%d] public login provider %q alias %q conflicts with product[%d] provider id", item.productIndex, registration.ProviderID, alias, earlier)
			}
			if earlier, ok := aliasOwners[alias]; ok {
				return nil, fmt.Errorf("product[%d] public login provider %q alias %q duplicates product[%d] alias", item.productIndex, registration.ProviderID, alias, earlier)
			}
			aliasOwners[alias] = item.productIndex
		}
		registration.Aliases = aliases
		registration.owningAppPacks = item.owningAppPacks
		out = append(out, registration)
	}
	return out, nil
}

func normalizedPublicLoginProviderAliases(providerID string, aliases []string) []string {
	seen := map[string]struct{}{providerID: {}}
	out := make([]string, 0, len(aliases))
	for _, rawAlias := range aliases {
		alias := auth.NormalizePublicLoginProviderToken(rawAlias)
		if alias == "" {
			continue
		}
		if _, ok := seen[alias]; ok {
			continue
		}
		seen[alias] = struct{}{}
		out = append(out, alias)
	}
	sort.Strings(out)
	return out
}

func composeRuntimeRouteSurfaces(products []ProductHooks) ([]RuntimeRouteSurfaceRegistration, error) {
	var out []RuntimeRouteSurfaceRegistration
	seen := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.RuntimeRouteSurfaces {
			name := strings.ToLower(strings.TrimSpace(registration.Name))
			replacements, err := normalizeRuntimeRouteSurfaceDialectRouteReplacements(productIndex, registration.Name, registration.ReplacesDialectRoutes)
			if err != nil {
				return nil, err
			}
			switch {
			case name == "":
				return nil, fmt.Errorf("product[%d] runtime route surface requires name", productIndex)
			case registration.Register == nil && len(replacements) == 0:
				return nil, fmt.Errorf("product[%d] runtime route surface %q requires register function or dialect route replacements", productIndex, registration.Name)
			case registration.Register != nil && len(replacements) > 0:
				return nil, fmt.Errorf("product[%d] runtime route surface %q cannot mix register function with dialect route replacements", productIndex, registration.Name)
			case len(owningAppPacks) == 0:
				return nil, fmt.Errorf("product[%d] runtime route surface %q requires owning app pack", productIndex, registration.Name)
			}
			if earlier, ok := seen[name]; ok {
				return nil, fmt.Errorf("product[%d] runtime route surface %q duplicates product[%d]", productIndex, registration.Name, earlier)
			}
			seen[name] = productIndex
			registration.Name = name
			registration.ReplacesDialectRoutes = replacements
			registration.owningAppPacks = owningAppPacks
			out = append(out, registration)
		}
	}
	return out, nil
}

func normalizeRuntimeRouteSurfaceDialectRouteReplacements(productIndex int, surfaceName string, replacements []DialectRouteReplacement) ([]DialectRouteReplacement, error) {
	out := make([]DialectRouteReplacement, 0, len(replacements))
	seen := map[string]struct{}{}
	for replacementIndex, replacement := range replacements {
		dialectName := strings.TrimSpace(replacement.DialectName)
		method := strings.ToUpper(strings.TrimSpace(replacement.Method))
		routePath := strings.TrimSpace(replacement.Path)
		switch {
		case dialectName == "":
			return nil, fmt.Errorf("product[%d] runtime route surface %q replacement[%d] requires dialect name", productIndex, surfaceName, replacementIndex)
		case method == "":
			return nil, fmt.Errorf("product[%d] runtime route surface %q replacement[%d] requires method", productIndex, surfaceName, replacementIndex)
		case routePath == "" || !strings.HasPrefix(routePath, "/") || routePath != path.Clean(routePath):
			return nil, fmt.Errorf("product[%d] runtime route surface %q replacement[%d] declares invalid path %q", productIndex, surfaceName, replacementIndex, replacement.Path)
		case replacement.Handler == nil:
			return nil, fmt.Errorf("product[%d] runtime route surface %q replacement[%d] requires handler", productIndex, surfaceName, replacementIndex)
		}
		key := strings.ToLower(dialectName) + "\x00" + method + "\x00" + routePath
		if _, ok := seen[key]; ok {
			return nil, fmt.Errorf("product[%d] runtime route surface %q replacement[%d] duplicates dialect route %s %s", productIndex, surfaceName, replacementIndex, method, routePath)
		}
		seen[key] = struct{}{}
		out = append(out, DialectRouteReplacement{
			DialectName: dialectName,
			Method:      method,
			Path:        routePath,
			Handler:     replacement.Handler,
		})
	}
	return out, nil
}

func composeDynamicCORSRouteFamilies(products []ProductHooks) ([]DynamicCORSRouteFamilyRegistration, error) {
	var out []DynamicCORSRouteFamilyRegistration
	seen := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.DynamicCORSRouteFamilies {
			name := strings.ToLower(strings.TrimSpace(registration.Name))
			switch {
			case name == "":
				return nil, fmt.Errorf("product[%d] dynamic CORS route family requires name", productIndex)
			case registration.Allows == nil:
				return nil, fmt.Errorf("product[%d] dynamic CORS route family %q requires route predicate", productIndex, registration.Name)
			case len(owningAppPacks) == 0:
				return nil, fmt.Errorf("product[%d] dynamic CORS route family %q requires owning app pack", productIndex, registration.Name)
			}
			if earlier, ok := seen[name]; ok {
				return nil, fmt.Errorf("product[%d] dynamic CORS route family %q duplicates product[%d]", productIndex, registration.Name, earlier)
			}
			seen[name] = productIndex
			registration.Name = name
			registration.owningAppPacks = owningAppPacks
			out = append(out, registration)
		}
	}
	return out, nil
}

func composeOAuthRouteScopes(products []ProductHooks) ([]OAuthRouteScopeRegistration, map[string]int, error) {
	var out []OAuthRouteScopeRegistration
	seen := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.OAuthRouteScopes {
			scope := canonicalOAuthScopeName(registration.Scope)
			switch {
			case scope == "":
				return nil, nil, fmt.Errorf("product[%d] OAuth route scope requires scope", productIndex)
			case containsPlatformBroadOAuthScope([]string{scope}):
				return nil, nil, fmt.Errorf("product[%d] OAuth route scope %q must not declare platform broad OAuth scope", productIndex, registration.Scope)
			case len(owningAppPacks) == 0:
				return nil, nil, fmt.Errorf("product[%d] OAuth route scope %q requires owning app pack", productIndex, registration.Scope)
			}
			if earlier, ok := seen[scope]; ok {
				return nil, nil, fmt.Errorf("product[%d] OAuth route scope %q duplicates product[%d]", productIndex, registration.Scope, earlier)
			}
			seen[scope] = productIndex
			registration.Scope = scope
			registration.owningAppPacks = owningAppPacks
			out = append(out, registration)
		}
	}
	return out, seen, nil
}

func composeOAuthScopeRouteFamilies(products []ProductHooks, oauthRouteScopeProductIndexes map[string]int) ([]OAuthScopeRouteFamilyRegistration, error) {
	var out []OAuthScopeRouteFamilyRegistration
	seen := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.OAuthScopeRouteFamilies {
			name := strings.ToLower(strings.TrimSpace(registration.Name))
			scopes := normalizedOAuthScopeNames(registration.Scopes)
			undeclaredScopes := undeclaredOAuthScopeRouteScopes(scopes, productIndex, oauthRouteScopeProductIndexes)
			nonSourceBackedScopes := nonSourceBackedOAuthScopeRouteScopes(scopes, oauthRouteScopeProductIndexes)
			switch {
			case name == "":
				return nil, fmt.Errorf("product[%d] OAuth scope route family requires name", productIndex)
			case len(scopes) == 0:
				return nil, fmt.Errorf("product[%d] OAuth scope route family %q requires at least one scope", productIndex, registration.Name)
			case containsPlatformBroadOAuthScope(scopes):
				return nil, fmt.Errorf("product[%d] OAuth scope route family %q must not claim platform broad OAuth scope", productIndex, registration.Name)
			case registration.RequireActiveSourceScope && len(nonSourceBackedScopes) > 0:
				return nil, fmt.Errorf("product[%d] OAuth scope route family %q marks source-backed scopes but also claims platform/product route scope(s) %v; source-backed scopes must stay metadata-owned and must not be declared in OAuthRouteScopes", productIndex, registration.Name, nonSourceBackedScopes)
			case !registration.RequireActiveSourceScope && len(undeclaredScopes) > 0:
				return nil, fmt.Errorf("product[%d] OAuth scope route family %q uses undeclared product OAuth route scope(s) %v; declare product-owned route scopes in OAuthRouteScopes, while custom scopes must be backed by _System_OAuthCustomScope", productIndex, registration.Name, undeclaredScopes)
			case registration.Allows == nil:
				return nil, fmt.Errorf("product[%d] OAuth scope route family %q requires route predicate", productIndex, registration.Name)
			case len(owningAppPacks) == 0:
				return nil, fmt.Errorf("product[%d] OAuth scope route family %q requires owning app pack", productIndex, registration.Name)
			}
			if earlier, ok := seen[name]; ok {
				return nil, fmt.Errorf("product[%d] OAuth scope route family %q duplicates product[%d]", productIndex, registration.Name, earlier)
			}
			seen[name] = productIndex
			registration.Name = name
			registration.Scopes = scopes
			registration.owningAppPacks = owningAppPacks
			out = append(out, registration)
		}
	}
	return out, nil
}

func containsPlatformBroadOAuthScope(scopes []string) bool {
	for _, scope := range scopes {
		switch strings.ToLower(strings.TrimSpace(scope)) {
		case "api", "full", "api.read":
			return true
		}
	}
	return false
}

func undeclaredOAuthScopeRouteScopes(scopes []string, productIndex int, oauthRouteScopeProductIndexes map[string]int) []string {
	var out []string
	for _, scope := range scopes {
		scope = canonicalOAuthScopeName(scope)
		if auth.IsPlatformOAuthRouteScope(scope) {
			continue
		}
		ownerProductIndex, ok := oauthRouteScopeProductIndexes[scope]
		if !ok || ownerProductIndex != productIndex {
			out = append(out, scope)
		}
	}
	return out
}

func nonSourceBackedOAuthScopeRouteScopes(scopes []string, oauthRouteScopeProductIndexes map[string]int) []string {
	var out []string
	for _, scope := range scopes {
		scope = canonicalOAuthScopeName(scope)
		if auth.IsPlatformOAuthRouteScope(scope) {
			out = append(out, scope)
			continue
		}
		if _, ok := oauthRouteScopeProductIndexes[scope]; ok {
			out = append(out, scope)
		}
	}
	return out
}

func normalizedOAuthScopeNames(scopes []string) []string {
	seen := map[string]struct{}{}
	var out []string
	for _, raw := range scopes {
		scope := canonicalOAuthScopeName(raw)
		if scope == "" {
			continue
		}
		if _, ok := seen[scope]; ok {
			continue
		}
		seen[scope] = struct{}{}
		out = append(out, scope)
	}
	sort.Strings(out)
	return out
}

func canonicalOAuthScopeName(scope string) string {
	return strings.ToLower(strings.TrimSpace(scope))
}

func composeAuthChallengeProfiles(products []ProductHooks) ([]AuthChallengeProfileRegistration, error) {
	var out []AuthChallengeProfileRegistration
	seen := map[string]int{}
	for productIndex, product := range products {
		owningAppPacks := productCapabilityOwningAppPacks(product)
		for _, registration := range product.Capabilities.AuthChallengeProfiles {
			profileID := auth.NormalizeProductCapabilityToken(registration.ProfileID)
			switch {
			case profileID == "":
				return nil, fmt.Errorf("product[%d] auth challenge profile requires id", productIndex)
			case registration.Build == nil:
				return nil, fmt.Errorf("product[%d] auth challenge profile %q requires builder", productIndex, registration.ProfileID)
			case len(owningAppPacks) == 0:
				return nil, fmt.Errorf("product[%d] auth challenge profile %q requires owning app pack", productIndex, registration.ProfileID)
			}
			if earlier, ok := seen[profileID]; ok {
				return nil, fmt.Errorf("product[%d] auth challenge profile %q duplicates product[%d]", productIndex, registration.ProfileID, earlier)
			}
			seen[profileID] = productIndex
			registration.ProfileID = profileID
			registration.owningAppPacks = owningAppPacks
			out = append(out, registration)
		}
	}
	return out, nil
}

func composeNoSubscriberRecordEventObjects(products []ProductHooks) ([]string, error) {
	var out []string
	seen := map[string]struct{}{}
	for productIndex, product := range products {
		for _, rawObject := range product.Capabilities.NoSubscriberRecordEventObjects {
			object := strings.TrimSpace(rawObject)
			if object == "" {
				return nil, fmt.Errorf("product[%d] no-subscriber record event object requires object API name", productIndex)
			}
			key := strings.ToLower(object)
			if _, ok := seen[key]; ok {
				continue
			}
			seen[key] = struct{}{}
			out = append(out, object)
		}
	}
	return out, nil
}

func productCapabilityOwningAppPacks(product ProductHooks) []string {
	return sortedNormalizedNames(product.OwningAppPacks)
}

func composeStreamingEventChannelMappings(products []ProductHooks) ([]streaming.EventChannelMapping, []productStreamingEventChannelMappings) {
	var out []streaming.EventChannelMapping
	var byProduct []productStreamingEventChannelMappings
	for _, product := range products {
		mappings := append([]streaming.EventChannelMapping(nil), product.Capabilities.StreamingEventChannelMappings...)
		out = append(out, mappings...)
		if len(mappings) == 0 {
			continue
		}
		byProduct = append(byProduct, productStreamingEventChannelMappings{
			OwningAppPacks: append([]string(nil), product.OwningAppPacks...),
			Mappings:       mappings,
		})
	}
	return out, byProduct
}

func composeOfficialRepoSourceRevisions(products []ProductHooks) ([]OfficialRepoSourceRevision, error) {
	var out []OfficialRepoSourceRevision
	seen := map[string]OfficialRepoSourceRevision{}
	for productIndex, product := range products {
		for _, registration := range product.Capabilities.OfficialRepoSourceRevisions {
			name := strings.TrimSpace(registration.Name)
			revision := strings.TrimSpace(registration.Revision)
			switch {
			case name == "":
				return nil, fmt.Errorf("product[%d] official_repo source revision requires name", productIndex)
			case revision == "":
				return nil, fmt.Errorf("product[%d] official_repo source revision for %q requires revision", productIndex, registration.Name)
			}
			key := strings.ToLower(name)
			if existing, ok := seen[key]; ok {
				if existing.Revision != revision {
					return nil, fmt.Errorf("product[%d] official_repo source revision for %q is %q but an earlier product declared %q", productIndex, name, revision, existing.Revision)
				}
				continue
			}
			entry := OfficialRepoSourceRevision{Name: name, Revision: revision}
			seen[key] = entry
			out = append(out, entry)
		}
	}
	return out, nil
}

func composeDetachedReferenceResolvers(products []ProductHooks) ([]DetachedReferenceResolverRegistration, error) {
	var out []DetachedReferenceResolverRegistration
	seen := map[string]int{}
	for productIndex, product := range products {
		productOwners := normalizedNameSet(product.OwningAppPacks)
		for _, registration := range product.Capabilities.DetachedReferenceResolvers {
			sourceType := strings.ToLower(strings.TrimSpace(registration.SourceType))
			owningAppPacks := sortedNormalizedNames(registration.OwningAppPacks)
			switch {
			case sourceType == "":
				return nil, fmt.Errorf("product[%d] detached reference resolver requires sourceType", productIndex)
			case registration.Resolver == nil:
				return nil, fmt.Errorf("product[%d] detached reference resolver for sourceType %q requires resolver", productIndex, registration.SourceType)
			case len(owningAppPacks) == 0:
				return nil, fmt.Errorf("product[%d] detached reference resolver for sourceType %q requires owning app pack", productIndex, registration.SourceType)
			}
			if earlier, ok := seen[sourceType]; ok {
				return nil, fmt.Errorf("product[%d] detached reference sourceType %q duplicates product[%d]", productIndex, registration.SourceType, earlier)
			}
			for _, owner := range owningAppPacks {
				if _, ok := productOwners[strings.ToLower(owner)]; !ok {
					return nil, fmt.Errorf("product[%d] detached reference resolver for sourceType %q owns app pack %q outside ProductHooks.OwningAppPacks", productIndex, registration.SourceType, owner)
				}
			}
			seen[sourceType] = productIndex
			registration.SourceType = sourceType
			registration.OwningAppPacks = append([]string(nil), owningAppPacks...)
			registration.owningAppPacks = owningAppPacks
			out = append(out, registration)
		}
	}
	return out, nil
}

func composeHostPrimitives(products []ProductHooks) ([]HostPrimitive, error) {
	var out []HostPrimitive
	seen := map[string]HostPrimitive{}
	for productIndex, product := range products {
		for _, primitive := range product.Capabilities.HostPrimitives {
			primitive = normalizeHostPrimitive(primitive)
			if primitive.Kind == "" || primitive.Name == "" {
				return nil, fmt.Errorf("product[%d] host primitive requires non-empty kind and name", productIndex)
			}
			key := hostPrimitiveKey(primitive.Kind, primitive.Name)
			if existing, ok := seen[key]; ok {
				if existing.Version != primitive.Version {
					return nil, fmt.Errorf("product[%d] host primitive %s/%s has version %q but an earlier product declared version %q", productIndex, primitive.Kind, primitive.Name, primitive.Version, existing.Version)
				}
				continue
			}
			seen[key] = primitive
			out = append(out, primitive)
		}
	}
	return out, nil
}

func composeFlowActionCallHandlerImplementations(products []ProductHooks) ([]automation.FlowActionCallHandlerImplementation, error) {
	var out []automation.FlowActionCallHandlerImplementation
	seen := map[string]int{}
	for productIndex, product := range products {
		for _, implementation := range product.Capabilities.FlowActionCallHandlerImplementations {
			handlerRef := strings.TrimSpace(implementation.HandlerRef)
			switch {
			case handlerRef == "":
				return nil, fmt.Errorf("product[%d] flow action-call handler implementation requires handlerRef", productIndex)
			case implementation.Build == nil:
				return nil, fmt.Errorf("product[%d] flow action-call handler implementation %q requires builder", productIndex, implementation.HandlerRef)
			}
			if earlier, ok := seen[handlerRef]; ok {
				return nil, fmt.Errorf("product[%d] flow action-call handlerRef %q duplicates product[%d]", productIndex, implementation.HandlerRef, earlier)
			}
			seen[handlerRef] = productIndex
			implementation.HandlerRef = handlerRef
			out = append(out, implementation)
		}
	}
	return out, nil
}

func normalizeHostPrimitive(primitive HostPrimitive) HostPrimitive {
	primitive.Kind = strings.ToLower(strings.TrimSpace(primitive.Kind))
	primitive.Name = strings.TrimSpace(primitive.Name)
	primitive.Version = strings.TrimSpace(primitive.Version)
	return primitive
}

func hostPrimitiveKey(kind, name string) string {
	return strings.ToLower(strings.TrimSpace(kind)) + "\x00" + strings.TrimSpace(name)
}

func rejectActiveAppPacksForAppBlindBuild(_ context.Context, _ *sql.DB, activeAppPacks []ActiveAppPack) error {
	names := sortedRuntimeActiveAppPackNames(activeAppPacks)
	if len(names) == 0 {
		return nil
	}
	return fmt.Errorf(
		"cmd/server default public-core build has no product dialects, but active app pack(s) are installed: %s; start a matching product distribution build or deactivate the app pack(s)",
		strings.Join(names, ", "),
	)
}

func sortedRuntimeActiveAppPackNames(rows []ActiveAppPack) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(rows))
	for _, row := range rows {
		name := strings.TrimSpace(row.Name)
		if name == "" {
			continue
		}
		key := strings.ToLower(name)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, name)
	}
	sort.Strings(out)
	return out
}

func registerProductHostPrimitives(hooks ProductHooks) {
	primitives := productHostPrimitives(hooks, nil)
	if len(primitives) == 0 {
		return
	}
	packs.RegisterDefaultHostPrimitives(primitives...)
}

func registerProductOfficialRepoSourceRevisions(hooks ProductHooks) {
	for _, registration := range hooks.Capabilities.OfficialRepoSourceRevisions {
		packs.RegisterDefaultOfficialRepoSourceRevision(registration.Name, registration.Revision)
	}
}

func registerProductOAuthSessionIssuerClasses(hooks ProductHooks) {
	owners := make([]auth.ProductOAuthSessionIssuerClassOwner, 0, len(hooks.Capabilities.OAuthSessionIssuerClasses))
	for _, registration := range hooks.Capabilities.OAuthSessionIssuerClasses {
		owners = append(owners, auth.ProductOAuthSessionIssuerClassOwner{
			Class:          registration.Class,
			OwningAppPacks: append([]string(nil), registration.owningAppPacks...),
		})
	}
	auth.SetProductOAuthSessionIssuerClassOwners(owners)
}

func registerProductOAuthRouteScopes(hooks ProductHooks) {
	owners := make([]auth.ProductOAuthRouteScopeOwner, 0, len(hooks.Capabilities.OAuthRouteScopes))
	for _, registration := range hooks.Capabilities.OAuthRouteScopes {
		owners = append(owners, auth.ProductOAuthRouteScopeOwner{
			Scope:          registration.Scope,
			OwningAppPacks: append([]string(nil), registration.owningAppPacks...),
		})
	}
	auth.SetProductOAuthRouteScopeOwners(owners)
}

func productHostPrimitives(hooks ProductHooks, db *sql.DB) []packs.HostPrimitive {
	primitives := make([]packs.HostPrimitive, 0, len(hooks.Capabilities.HostPrimitives))
	for _, primitive := range hooks.Capabilities.HostPrimitives {
		primitives = append(primitives, packs.HostPrimitive{
			Kind:    strings.TrimSpace(primitive.Kind),
			Name:    strings.TrimSpace(primitive.Name),
			Version: strings.TrimSpace(primitive.Version),
		})
	}
	primitives = append(primitives, productDialectHostPrimitives(hooks, db)...)
	return primitives
}

func productDialectHostPrimitives(hooks ProductHooks, db *sql.DB) []packs.HostPrimitive {
	if hooks.Dialects == nil {
		return nil
	}
	declared := hooks.Dialects(DialectRegistrationContext{
		DB:                  db,
		HrefFieldsForObject: files.FieldsToRewriteForObject,
	})
	mustValidateProductDialectOwnership(0, hooks, declared)
	out := make([]packs.HostPrimitive, 0, len(declared))
	seen := map[string]struct{}{}
	for _, d := range declared {
		if d == nil {
			continue
		}
		name := strings.TrimSpace(d.Name())
		if name == "" {
			panic("serverruntime: product hook returned a dialect with empty Name")
		}
		key := strings.ToLower(name)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, packs.HostPrimitive{
			Kind: packs.HostPrimitiveKindProductDialect,
			Name: name,
		})
	}
	return out
}

func registerProductFlowActionCallHandlerImplementations(hooks ProductHooks) {
	if len(hooks.Capabilities.FlowActionCallHandlerImplementations) == 0 {
		return
	}
	automation.RegisterDefaultAppFlowActionCallHandlerImplementations(hooks.Capabilities.FlowActionCallHandlerImplementations...)
}

func registerProductNoSubscriberRecordEventObjects(hooks ProductHooks) {
	if len(hooks.Capabilities.NoSubscriberRecordEventObjects) == 0 {
		return
	}
	eventfeature.RegisterDefaultNoSubscriberRecordEventObjects(hooks.Capabilities.NoSubscriberRecordEventObjects...)
}

// ValidateProductComposition runs the same product/active-pack compatibility
// guard used by server startup and distribution-aware setup commands.
func ValidateProductComposition(ctx context.Context, hooks ProductHooks, db *sql.DB, activeAppPacks []packs.ActiveInstalledAppPack) error {
	hooks = normalizeProductHooks(hooks)
	return hooks.ValidateComposition(ctx, db, runtimeActiveAppPacks(activeAppPacks))
}

func runtimeActiveAppPacks(rows []packs.ActiveInstalledAppPack) []ActiveAppPack {
	out := make([]ActiveAppPack, 0, len(rows))
	for _, row := range rows {
		out = append(out, ActiveAppPack{
			Name:               row.Name,
			SourceKind:         row.SourceKind,
			SourceURI:          row.SourceURI,
			SourceRevision:     row.SourceRevision,
			AppliedPayloadHash: row.AppliedPayloadHash,
			DesiredPayloadHash: row.DesiredPayloadHash,
		})
	}
	return out
}
