package catalogreconcile

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"slices"
	"strings"
	"time"

	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	consumerv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	layoutauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/layoutauthoringv1"
	pagelayoutresolutionv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/pagelayoutresolutionv1"
	permissionauthoringv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionauthoringv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/catalogapp"
	"github.com/pingcap-inc/nexuscrm-controlplane/releaseartifact"
	"github.com/pingcap-inc/nexuscrm-controlplane/strictjson"
)

const (
	ConfigSchemaV1 = "nexuscrm.catalog-reconcile-config/v1"
	ConfigSchemaV2 = "nexuscrm.catalog-reconcile-config/v2"
	ConfigSchemaV3 = "nexuscrm.catalog-reconcile-config/v3"
	ConfigSchemaV4 = "nexuscrm.catalog-reconcile-config/v4"
)

type Config struct {
	// HandlerWorkloadCapabilities comes from the deployment grants for WorkloadID.
	// It names RPCs used by composite handlers without Catalog serving bindings.
	HandlerWorkloadCapabilities []string `json:"handlerWorkloadCapabilities,omitempty"`

	Schema                   string                          `json:"schema"`
	OrgID                    string                          `json:"orgId"`
	AppID                    string                          `json:"appId"`
	ExpectedAppReleaseDigest string                          `json:"expectedAppReleaseDigest"`
	MetadataSourceDigest     string                          `json:"metadataSourceDigest,omitempty"`
	ApprovalPublication      *ApprovalPublicationConfig      `json:"approvalPublication,omitempty"`
	ReleaseImport            *ReleaseImportConfig            `json:"releaseImport,omitempty"`
	ArtifactStoreRoot        string                          `json:"artifactStoreRoot"`
	HostCompatibility        HostCompatibilityConfig         `json:"hostCompatibility"`
	IdempotencyKey           string                          `json:"idempotencyKey"`
	OperationKind            catalogapp.OperationKind        `json:"operationKind"`
	RefreshActiveRelease     bool                            `json:"refreshActiveRelease,omitempty"`
	AppInstallationID        string                          `json:"appInstallationId"`
	ExpectedCatalogRevision  *uint64                         `json:"expectedCatalogRevision"`
	ExpectedSecurityEpoch    *uint64                         `json:"expectedSecurityEpoch"`
	RuntimeAudience          string                          `json:"runtimeAudience"`
	WorkloadID               string                          `json:"workloadId"`
	AuthorizedWorkloadIDs    []string                        `json:"authorizedWorkloadIds,omitempty"`
	AppCapabilityID          string                          `json:"appCapabilityId"`
	HandlerRef               string                          `json:"handlerRef"`
	RuntimeCapabilityID      string                          `json:"runtimeCapabilityId"`
	Capabilities             []CapabilityConfig              `json:"capabilities,omitempty"`
	ServiceReleaseDigest     string                          `json:"serviceReleaseDigest"`
	Frontend                 *FrontendBindingConfig          `json:"frontend"`
	SchemaGeneration         uint64                          `json:"schemaGeneration"`
	EngineGeneration         uint64                          `json:"engineGeneration"`
	ResolvedDependencies     []catalogapp.ResolvedDependency `json:"resolvedDependencies"`
	ConsumerGrants           []ConsumerGrantConfig           `json:"consumerGrants,omitempty"`
	IntegrationBindings      []IntegrationBindingConfig      `json:"integrationBindings,omitempty"`
	OutboundMailRoutes       []OutboundMailRouteConfig       `json:"outboundMailRoutes,omitempty"`
	PhaseOnePublication      *PhaseOnePublicationConfig      `json:"phaseOnePublication,omitempty"`
}

// OutboundMailRouteConfig is the secret-free L1/L2 publication input for one
// provider-neutral SEND_MAIL route. The callout profile selector is resolved
// to the exact active Integration binding before Catalog compilation.
type OutboundMailRouteConfig struct {
	AuthorizedWorkloadID      string                                           `json:"authorizedWorkloadId"`
	ConnectorOwnerKey         string                                           `json:"connectorOwnerKey"`
	FlowDeveloperName         string                                           `json:"flowDeveloperName"`
	FlowActionName            string                                           `json:"flowActionName"`
	SourceObject              string                                           `json:"sourceObject"`
	ActionSlotID              string                                           `json:"actionSlotId"`
	DestinationSlotID         string                                           `json:"destinationSlotId"`
	CalloutProfileOwnerPack   string                                           `json:"calloutProfileOwnerPack"`
	CalloutProfileBusinessKey string                                           `json:"calloutProfileBusinessKey"`
	MailCompletion            catalogsnapshot.OutboundMailCompletionProjection `json:"mailCompletion"`
}

// ApprovalPublicationConfig selects one immutable neutral bundle produced by
// the L1 Approval compiler from the exact L2 metadata tree. It is deployment
// input only; Catalog validates and revision-binds it before Runtime can serve
// any definition.
type ApprovalPublicationConfig struct {
	BundlePath     string `json:"bundlePath"`
	ExpectedDigest string `json:"expectedDigest"`
}

// PhaseOnePublicationConfig explicitly enables the stopped-deployment
// publication of the neutral consumer lineage produced by this reconcile.
// Layout bundles remain disabled; this is not a second layout authoring path.
type PhaseOnePublicationConfig struct {
	CommandID string `json:"commandId"`
}

// CapabilityConfig selects one release-owned App handler and the one neutral
// Runtime capability it invokes. Schema v3/v4 commits the complete list as
// one Catalog operation; it is not a second capability registry.
type CapabilityConfig struct {
	AppCapabilityID     string `json:"appCapabilityId"`
	HandlerRef          string `json:"handlerRef"`
	RuntimeCapabilityID string `json:"runtimeCapabilityId"`
}

// IntegrationBindingConfig selects one exact installed-pack reference for one
// authorized Integration workload. It carries no connector payload or secret;
// Catalog validates the exact generation before activating the reference set.
type IntegrationBindingConfig struct {
	AuthorizedWorkloadID string                            `json:"authorizedWorkloadId"`
	Kind                 consumerv1.IntegrationBindingKind `json:"kind"`
	OwnerPack            string                            `json:"ownerPack"`
	PackInstallationID   string                            `json:"packInstallationId"`
	PackPayloadDigest    string                            `json:"packPayloadDigest"`
	BusinessKey          string                            `json:"businessKey"`
	DefinitionVersion    uint64                            `json:"definitionVersion"`
	CredentialRefs       []string                          `json:"credentialRefs"`
	RuntimeCommand       *RuntimeCommandBindingConfig      `json:"runtimeCommand,omitempty"`
}

// RuntimeCommandBindingConfig binds one disclosed Integration route to the
// exact neutral Runtime command and immutable definition that consume it.
// Dynamic provider payload remains Integration-owned and is never Catalog
// configuration.
type RuntimeCommandBindingConfig struct {
	CommandKind          string `json:"commandKind"`
	PayloadFormatVersion uint32 `json:"payloadFormatVersion"`
	DefinitionRef        string `json:"definitionRef"`
	DefinitionVersion    string `json:"definitionVersion"`
}

type ConsumerGrantConfig struct {
	AuthorizedWorkloadID string `json:"authorizedWorkloadId"`
	CapabilityID         string `json:"capabilityId"`
}

// SelectsRuntimeMetadataCapability applies the same workload and capability
// rules used by Catalog compilation. Runtime composition uses this instead of
// maintaining a second, weaker interpretation of consumer grants.
func (c Config) SelectsRuntimeMetadataCapability(capabilityID string) bool {
	for _, capability := range c.Capabilities {
		if capability.RuntimeCapabilityID == capabilityID {
			return true
		}
	}
	for _, grant := range c.ConsumerGrants {
		if consumerGrantSelectsRuntimeMetadataCapability(c.WorkloadID, capabilityID, catalogapp.ConsumerGrant{
			AuthorizedWorkloadID: grant.AuthorizedWorkloadID,
			CapabilityID:         grant.CapabilityID,
		}) {
			return true
		}
	}
	return false
}

func consumerGrantSelectsRuntimeMetadataCapability(
	workloadID string,
	capabilityID string,
	grant catalogapp.ConsumerGrant,
) bool {
	return grant.AuthorizedWorkloadID == workloadID && grant.CapabilityID == capabilityID &&
		(capabilityID == pagelayoutresolutionv1.CapabilityName ||
			capabilityID == layoutauthoringv1.CapabilityName)
}

// ReleaseImportConfig is optional bootstrap material for a release that is not
// yet present in Catalog. It is deliberately separate from per-org lifecycle
// input: once registered, Catalog owns the immutable release fact.
type ReleaseImportConfig struct {
	PackagePath     string          `json:"packagePath"`
	AttestationPath string          `json:"attestationPath"`
	Publisher       PublisherConfig `json:"publisher"`
}

type PublisherConfig struct {
	PublisherID string               `json:"publisherId"`
	Keys        []PublisherKeyConfig `json:"keys"`
}

type PublisherKeyConfig struct {
	KeyID           string    `json:"keyId"`
	Algorithm       string    `json:"algorithm"`
	PublicKeyBase64 string    `json:"publicKeyBase64"`
	ValidFrom       time.Time `json:"validFrom"`
	ValidUntil      time.Time `json:"validUntil"`
}

type HostCompatibilityConfig struct {
	Protocols          []SupportedProtocolConfig      `json:"protocols"`
	EngineCapabilities []string                       `json:"engineCapabilities"`
	ComponentKinds     []SupportedComponentKindConfig `json:"componentKinds"`
}

type SupportedProtocolConfig struct {
	ProtocolID string `json:"protocolId"`
	Major      uint32 `json:"major"`
	Minor      uint32 `json:"minor"`
}

type SupportedComponentKindConfig struct {
	ComponentKind          string `json:"componentKind"`
	ComponentFormatVersion uint32 `json:"componentFormatVersion"`
}

type FrontendBindingConfig struct {
	ReleaseDigest              string `json:"releaseDigest"`
	CompatibilityReceiptDigest string `json:"compatibilityReceiptDigest"`
}

func ParseConfig(raw []byte) (Config, error) {
	if err := strictjson.RejectDuplicateKeys(raw, "catalog reconcile config"); err != nil {
		return Config{}, fmt.Errorf("decode Catalog reconcile config: %w", err)
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var config Config
	if err := decoder.Decode(&config); err != nil {
		return Config{}, fmt.Errorf("decode Catalog reconcile config: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		return Config{}, fmt.Errorf("Catalog reconcile config contains trailing content")
	}
	if config.Schema != ConfigSchemaV1 && config.Schema != ConfigSchemaV2 &&
		config.Schema != ConfigSchemaV3 && config.Schema != ConfigSchemaV4 {
		return Config{}, fmt.Errorf("unsupported Catalog reconcile config schema %q", config.Schema)
	}
	if config.Schema == ConfigSchemaV2 || config.Schema == ConfigSchemaV3 ||
		config.Schema == ConfigSchemaV4 {
		if err := appauthority.ValidateDigest(config.MetadataSourceDigest); err != nil {
			return Config{}, fmt.Errorf("Catalog reconcile metadata source digest: %w", err)
		}
	} else if config.MetadataSourceDigest != "" {
		return Config{}, fmt.Errorf("Catalog reconcile v1 config cannot declare metadataSourceDigest")
	}
	if config.Schema != ConfigSchemaV4 && len(config.IntegrationBindings) != 0 {
		return Config{}, fmt.Errorf("Catalog reconcile integrationBindings require config schema v4")
	}
	if config.Schema != ConfigSchemaV4 && len(config.OutboundMailRoutes) != 0 {
		return Config{}, fmt.Errorf("Catalog reconcile outboundMailRoutes require config schema v4")
	}
	if config.Schema != ConfigSchemaV4 && len(config.HandlerWorkloadCapabilities) != 0 {
		return Config{}, fmt.Errorf("Catalog reconcile handlerWorkloadCapabilities require config schema v4")
	}
	if config.Schema != ConfigSchemaV4 && len(config.ConsumerGrants) != 0 {
		return Config{}, fmt.Errorf("Catalog reconcile consumerGrants require config schema v4")
	}
	if config.Schema != ConfigSchemaV4 && len(config.AuthorizedWorkloadIDs) != 0 {
		return Config{}, fmt.Errorf("Catalog reconcile authorizedWorkloadIds require config schema v4")
	}
	if config.Schema != ConfigSchemaV4 && config.PhaseOnePublication != nil {
		return Config{}, fmt.Errorf("Catalog reconcile phaseOnePublication requires config schema v4")
	}
	if config.Schema != ConfigSchemaV4 && config.RefreshActiveRelease {
		return Config{}, fmt.Errorf("Catalog reconcile refreshActiveRelease requires config schema v4")
	}
	if config.Schema != ConfigSchemaV4 && config.ApprovalPublication != nil {
		return Config{}, fmt.Errorf("Catalog reconcile approvalPublication requires config schema v4")
	}
	if config.ApprovalPublication != nil {
		if strings.TrimSpace(config.ApprovalPublication.BundlePath) == "" {
			return Config{}, fmt.Errorf("Catalog reconcile approvalPublication bundlePath is required")
		}
		if err := appauthority.ValidateDigest(config.ApprovalPublication.ExpectedDigest); err != nil {
			return Config{}, fmt.Errorf("Catalog reconcile approval publication digest: %w", err)
		}
	}
	if config.RefreshActiveRelease && config.OperationKind != catalogapp.OperationUpgrade {
		return Config{}, fmt.Errorf("Catalog reconcile refreshActiveRelease requires operationKind UPGRADE")
	}
	if config.Schema == ConfigSchemaV4 {
		if len(config.AuthorizedWorkloadIDs) == 0 ||
			!slices.Contains(config.AuthorizedWorkloadIDs, config.WorkloadID) {
			return Config{}, fmt.Errorf(
				"Catalog reconcile v4 authorizedWorkloadIds must include the handler workloadId",
			)
		}
	}
	if err := validateOutboundMailRouteConfigs(config); err != nil {
		return Config{}, err
	}
	if config.PhaseOnePublication != nil &&
		!isCanonicalUUIDv7(config.PhaseOnePublication.CommandID) {
		return Config{}, fmt.Errorf(
			"Catalog reconcile phaseOnePublication commandId must be a lowercase UUIDv7",
		)
	}
	for name, value := range map[string]string{
		"orgId": config.OrgID, "appId": config.AppID,
		"expectedAppReleaseDigest": config.ExpectedAppReleaseDigest,
		"artifactStoreRoot":        config.ArtifactStoreRoot, "idempotencyKey": config.IdempotencyKey,
		"runtimeAudience": config.RuntimeAudience, "workloadId": config.WorkloadID,
		"serviceReleaseDigest": config.ServiceReleaseDigest,
	} {
		if strings.TrimSpace(value) == "" {
			return Config{}, fmt.Errorf("Catalog reconcile config %s is required", name)
		}
	}
	if _, err := config.capabilitySelections(); err != nil {
		return Config{}, err
	}
	if config.SchemaGeneration == 0 || config.EngineGeneration == 0 {
		return Config{}, fmt.Errorf("Catalog reconcile schema and engine generations must be non-zero")
	}
	if config.ExpectedCatalogRevision == nil || config.ExpectedSecurityEpoch == nil {
		return Config{}, fmt.Errorf("Catalog reconcile expected Catalog revision and security epoch are required")
	}
	switch config.OperationKind {
	case catalogapp.OperationPrepare:
		if config.AppInstallationID != "" {
			return Config{}, fmt.Errorf("Catalog reconcile PREPARE installation ID must be empty")
		}
	case catalogapp.OperationUpgrade, catalogapp.OperationRollback:
		if err := appauthority.ValidateAppInstallationID(config.AppInstallationID); err != nil {
			return Config{}, fmt.Errorf("Catalog reconcile AppInstallation ID: %w", err)
		}
	default:
		return Config{}, fmt.Errorf("Catalog reconcile operation kind %q is unsupported", config.OperationKind)
	}
	if config.Frontend != nil {
		if err := appauthority.ValidateDigest(config.Frontend.ReleaseDigest); err != nil {
			return Config{}, fmt.Errorf("Catalog reconcile frontend release digest: %w", err)
		}
		if err := appauthority.ValidateDigest(config.Frontend.CompatibilityReceiptDigest); err != nil {
			return Config{}, fmt.Errorf("Catalog reconcile frontend compatibility receipt: %w", err)
		}
	}
	if config.ReleaseImport != nil {
		if strings.TrimSpace(config.ReleaseImport.PackagePath) == "" || strings.TrimSpace(config.ReleaseImport.AttestationPath) == "" {
			return Config{}, fmt.Errorf("Catalog reconcile releaseImport packagePath and attestationPath are required")
		}
		if _, err := config.ReleaseImport.PublisherTrust(); err != nil {
			return Config{}, err
		}
	}
	return config, nil
}

// BuildRequest closes the imported release shape and the operator-selected
// lifecycle transition before Catalog creates an operation. It accepts only
// the bounded serving capabilities for which Catalog owns an explicit
// metadata projection. It is not a generic release controller. Unsupported or
// partial release shapes fail before any installation state is mutated.
func (c Config) BuildRequest(release catalogapp.RegisteredRelease) (Request, error) {
	if release.AppID != c.AppID || release.AppReleaseDigest != c.ExpectedAppReleaseDigest {
		return Request{}, fmt.Errorf("verified AppRelease identity does not match the pinned reconcile input")
	}
	manifest := release.Package.Manifest
	selections, err := c.capabilitySelections()
	if err != nil {
		return Request{}, err
	}
	consumerGrants := c.consumerGrantSelections()
	if c.Schema == ConfigSchemaV4 {
		if err := validateReleaseCapabilityClosureV4(
			manifest.ProvidedCapabilities,
			manifest.RequiredEngineCapabilities,
			selections, c.HandlerWorkloadCapabilities...,
		); err != nil {
			return Request{}, err
		}
	} else if err := validateReleaseCapabilitySelection(
		manifest.ProvidedCapabilities,
		manifest.RequiredEngineCapabilities,
		selections,
	); err != nil {
		return Request{}, err
	}
	var frontend *FrontendSelection
	switch manifest.FrontendCompatibility.Mode {
	case appauthority.FrontendModeRequired:
		if c.Frontend == nil {
			return Request{}, fmt.Errorf("Catalog reconcile verified AppRelease requires a frontend binding")
		}
		frontend = &FrontendSelection{
			ReleaseDigest:              c.Frontend.ReleaseDigest,
			CompatibilityReceiptDigest: c.Frontend.CompatibilityReceiptDigest,
		}
	case appauthority.FrontendModeNone:
		if c.Frontend != nil {
			return Request{}, fmt.Errorf("Catalog reconcile verified AppRelease does not accept a frontend binding")
		}
	default:
		return Request{}, fmt.Errorf("Catalog reconcile AppRelease frontend mode %q is unsupported", manifest.FrontendCompatibility.Mode)
	}
	// App capability selections are canonically ordered by AppCapabilityID for
	// binding/output identity. Runtime grants have their own canonical order,
	// owned by the verified AppRelease. Do not derive one ordering from the
	// other: the two identifier namespaces need not sort the same way.
	granted := slices.Clone(manifest.RequiredEngineCapabilities)
	authorizedWorkloads := []string{c.WorkloadID}
	if c.Schema == ConfigSchemaV4 {
		authorizedWorkloads = slices.Clone(c.AuthorizedWorkloadIDs)
	}
	request := Request{
		Operation: catalogapp.BeginOperationRequest{
			Kind: c.OperationKind, OrgID: c.OrgID, AppInstallationID: c.AppInstallationID, AppID: c.AppID,
			TargetAppReleaseDigest: release.AppReleaseDigest,
			IdempotencyKey:         c.IdempotencyKey, RuntimeAudience: c.RuntimeAudience,
			ExpectedCatalogRevision: *c.ExpectedCatalogRevision,
			ExpectedSecurityEpoch:   *c.ExpectedSecurityEpoch,
			RefreshActiveRelease:    c.RefreshActiveRelease,
			AuthorizedWorkloads:     authorizedWorkloads,
			GrantedCapabilities:     granted,
			HandlerWorkloadID:       c.WorkloadID,
			SelectedHandlerCapabilities: durableSelectedHandlerCapabilityIDs(
				manifest.ProvidedCapabilities, selections,
			),
			WorkloadHandlerAdmissions: durableWorkloadHandlerAdmissions(
				manifest.ProvidedCapabilities, selections, c.WorkloadID,
			),
			ResolvedDependencies: append([]catalogapp.ResolvedDependency(nil), c.ResolvedDependencies...),
			ConsumerGrants:       consumerGrants,
			IntegrationBindings:  c.integrationBindingSelections(),
		},
		WorkloadID:           c.WorkloadID,
		ServiceReleaseDigest: catalogsnapshot.Digest(c.ServiceReleaseDigest), Frontend: frontend,
		MetadataSourceDigest: catalogsnapshot.Digest(c.MetadataSourceDigest),
		SchemaGeneration:     c.SchemaGeneration, EngineGeneration: c.EngineGeneration,
		outboundMailRoutes: append([]OutboundMailRouteConfig(nil), c.OutboundMailRoutes...),
	}
	if c.Schema == ConfigSchemaV4 {
		request.capabilityClosure = ConfigSchemaV4
		request.handlerWorkloadCapabilities = slices.Clone(c.HandlerWorkloadCapabilities)
	}
	if c.Schema == ConfigSchemaV3 || c.Schema == ConfigSchemaV4 {
		request.Capabilities = selections
	} else {
		request.AppCapabilityID = selections[0].AppCapabilityID
		request.HandlerRef = selections[0].HandlerRef
		request.RuntimeCapabilityID = selections[0].RuntimeCapabilityID
	}
	return request, nil
}

func capabilitySelectionIDs(selections []CapabilitySelection) []string {
	result := make([]string, len(selections))
	for index, selection := range selections {
		result[index] = selection.AppCapabilityID
	}
	return result
}

// durableSelectedHandlerCapabilityIDs preserves the legacy empty-selection
// request identity when every verified service handler is selected. Only an
// explicit subset needs to be persisted to keep retry/resume bound to the
// workload-versus-serving split introduced by newer manifests.
func durableSelectedHandlerCapabilityIDs(
	provided []appauthority.ProvidedCapability,
	selections []CapabilitySelection,
) []string {
	selected := capabilitySelectionIDs(selections)
	available := make([]string, 0, len(provided))
	for _, capability := range provided {
		if capability.Kind == appauthority.CapabilityKindServiceHandler {
			available = append(available, capability.CapabilityID)
		}
	}
	slices.Sort(available)
	if slices.Equal(selected, available) {
		return nil
	}
	return selected
}

func durableWorkloadHandlerAdmissions(
	provided []appauthority.ProvidedCapability,
	selections []CapabilitySelection,
	workloadID string,
) []catalogapp.WorkloadHandlerAdmission {
	selected := capabilitySelectionIDs(selections)
	available := make([]string, 0, len(provided))
	providedByID := make(map[string]appauthority.ProvidedCapability, len(provided))
	for _, capability := range provided {
		if capability.Kind == appauthority.CapabilityKindServiceHandler {
			available = append(available, capability.CapabilityID)
			providedByID[capability.CapabilityID] = capability
		}
	}
	slices.Sort(available)
	if slices.Equal(selected, available) {
		return nil
	}
	result := make([]catalogapp.WorkloadHandlerAdmission, 0, len(available)-len(selected))
	for _, capabilityID := range available {
		if slices.Contains(selected, capabilityID) {
			continue
		}
		capability := providedByID[capabilityID]
		result = append(result, catalogapp.WorkloadHandlerAdmission{
			CapabilityID: capabilityID, AuthorizedWorkloadID: workloadID,
			RequiredRuntimeCapabilities: append(
				[]string(nil), capability.RequiredRuntimeCapabilities...,
			),
		})
	}
	return result
}

func (c Config) capabilitySelections() ([]CapabilitySelection, error) {
	if c.Schema == ConfigSchemaV3 || c.Schema == ConfigSchemaV4 {
		if c.AppCapabilityID != "" || c.HandlerRef != "" || c.RuntimeCapabilityID != "" {
			return nil, fmt.Errorf("Catalog reconcile v3/v4 config cannot mix legacy singular capability fields with capabilities")
		}
		if c.Schema == ConfigSchemaV3 && len(c.Capabilities) < 2 {
			return nil, fmt.Errorf("Catalog reconcile v3 config requires at least two capabilities")
		}
		if c.Schema == ConfigSchemaV4 && len(c.Capabilities) == 0 {
			return nil, fmt.Errorf("Catalog reconcile v4 config requires at least one capability")
		}
		selections := make([]CapabilitySelection, 0, len(c.Capabilities))
		for _, capability := range c.Capabilities {
			selections = append(selections, CapabilitySelection{
				AppCapabilityID:     capability.AppCapabilityID,
				HandlerRef:          capability.HandlerRef,
				RuntimeCapabilityID: capability.RuntimeCapabilityID,
			})
		}
		return normalizeCapabilitySelections(selections)
	}
	if len(c.Capabilities) != 0 {
		return nil, fmt.Errorf("Catalog reconcile v1/v2 config cannot declare capabilities")
	}
	if c.RuntimeCapabilityID != recordaccessv1.CapabilityName {
		return nil, fmt.Errorf("Catalog reconcile does not support Runtime capability %q in v1/v2; only %q is allowed", c.RuntimeCapabilityID, recordaccessv1.CapabilityName)
	}
	for name, value := range map[string]string{
		"appCapabilityId":     c.AppCapabilityID,
		"handlerRef":          c.HandlerRef,
		"runtimeCapabilityId": c.RuntimeCapabilityID,
	} {
		if strings.TrimSpace(value) == "" {
			return nil, fmt.Errorf("Catalog reconcile config %s is required", name)
		}
	}
	return normalizeCapabilitySelections([]CapabilitySelection{{
		AppCapabilityID:     c.AppCapabilityID,
		HandlerRef:          c.HandlerRef,
		RuntimeCapabilityID: c.RuntimeCapabilityID,
	}})
}

func (c Config) consumerGrantSelections() []catalogapp.ConsumerGrant {
	result := make([]catalogapp.ConsumerGrant, len(c.ConsumerGrants))
	for index, value := range c.ConsumerGrants {
		result[index] = catalogapp.ConsumerGrant{
			AuthorizedWorkloadID: value.AuthorizedWorkloadID,
			CapabilityID:         value.CapabilityID,
		}
	}
	return result
}

func (c Config) integrationBindingSelections() []catalogapp.IntegrationBindingSelection {
	result := make([]catalogapp.IntegrationBindingSelection, len(c.IntegrationBindings))
	for index, value := range c.IntegrationBindings {
		var runtimeCommand *consumerv1.RuntimeCommandBinding
		if value.RuntimeCommand != nil {
			runtimeCommand = &consumerv1.RuntimeCommandBinding{
				CommandKind:          value.RuntimeCommand.CommandKind,
				PayloadFormatVersion: value.RuntimeCommand.PayloadFormatVersion,
				DefinitionRef:        value.RuntimeCommand.DefinitionRef,
				DefinitionVersion:    value.RuntimeCommand.DefinitionVersion,
			}
		}
		result[index] = catalogapp.IntegrationBindingSelection{
			AuthorizedWorkloadID: value.AuthorizedWorkloadID,
			Binding: consumerv1.IntegrationBinding{
				Kind:               value.Kind,
				OwnerPack:          value.OwnerPack,
				PackInstallationID: value.PackInstallationID,
				PackPayloadDigest:  value.PackPayloadDigest,
				BusinessKey:        value.BusinessKey,
				DefinitionVersion:  value.DefinitionVersion,
				CredentialRefs:     append([]string(nil), value.CredentialRefs...),
				RuntimeCommand:     runtimeCommand,
			},
		}
	}
	return result
}

func validateReleaseCapabilitySelection(provided []appauthority.ProvidedCapability, required []string, selections []CapabilitySelection) error {
	if len(provided) != len(selections) {
		return fmt.Errorf("Catalog reconcile capability selection must cover every provided App capability")
	}
	byID := make(map[string]CapabilitySelection, len(selections))
	wantRuntime := make([]string, 0, len(selections))
	for _, selection := range selections {
		byID[selection.AppCapabilityID] = selection
		wantRuntime = append(wantRuntime, selection.RuntimeCapabilityID)
	}
	for _, capability := range provided {
		selection, ok := byID[capability.CapabilityID]
		if !ok || capability.Kind != appauthority.CapabilityKindServiceHandler ||
			capability.HandlerRef != selection.HandlerRef || len(capability.RequiredRuntimeCapabilities) != 1 ||
			capability.RequiredRuntimeCapabilities[0] != selection.RuntimeCapabilityID {
			return fmt.Errorf("Catalog reconcile service selection does not match the verified AppRelease")
		}
	}
	slices.Sort(wantRuntime)
	wantRuntime = slices.Compact(wantRuntime)
	gotRuntime := append([]string(nil), required...)
	slices.Sort(gotRuntime)
	gotRuntime = slices.Compact(gotRuntime)
	if !slices.Equal(gotRuntime, wantRuntime) {
		return fmt.Errorf("Catalog reconcile Runtime capability selection does not match the verified AppRelease")
	}
	return nil
}

// validateReleaseCapabilityClosureV4 validates the complete AppRelease
// requirement graph without forcing every public App handler into a one-to-one
// Runtime binding. One App handler may compose multiple neutral Runtime
// capabilities, and multiple handlers may reuse the same Runtime capability.
//
// Runtime serving selections remain unique by Runtime capability. Catalog
// classifies disclosure dependencies through its owner predicate and derives
// their handler-workload grants during operation admission.
// A handler whose dependency graph includes a Runtime workload capability that
// Catalog neither selects nor discloses may remain outside the Catalog serving
// selection: Runtime workload admission owns that dependency and fails closed
// independently. A selected composite handler must explicitly declare such
// dependencies in handlerWorkloadCapabilities. When present, that list closes
// every handler workload dependency; an absent list retains the older v4
// representation for entirely workload-admitted handlers. A handler whose complete graph is Catalog-selected or
// Catalog-disclosed must still have an explicit Catalog handler selection.
// A required capability that is not a dependency of an App handler remains a
// consumer-only Runtime requirement owned by the verified AppRelease (for
// example, durable command acceptance by an independent Integration workload).
func validateReleaseCapabilityClosureV4(
	provided []appauthority.ProvidedCapability,
	required []string,
	selections []CapabilitySelection,
	workloadCapabilities ...string,
) error {
	requiredSet := make(map[string]struct{}, len(required))
	for _, capabilityID := range required {
		requiredSet[capabilityID] = struct{}{}
	}
	providedByID := make(
		map[string]appauthority.ProvidedCapability,
		len(provided),
	)
	for _, capability := range provided {
		providedByID[capability.CapabilityID] = capability
	}
	selectedRuntime := make(map[string]struct{}, len(selections))
	selectedApp := make(map[string]struct{}, len(selections))
	for _, selection := range selections {
		capability, ok := providedByID[selection.AppCapabilityID]
		if !ok ||
			capability.Kind != appauthority.CapabilityKindServiceHandler ||
			capability.HandlerRef != selection.HandlerRef ||
			!slices.Contains(
				capability.RequiredRuntimeCapabilities,
				selection.RuntimeCapabilityID,
			) {
			return fmt.Errorf(
				"Catalog reconcile service selection does not match the verified AppRelease",
			)
		}
		if _, required := requiredSet[selection.RuntimeCapabilityID]; !required {
			return fmt.Errorf(
				"Catalog reconcile Runtime capability selection does not match the verified AppRelease",
			)
		}
		selectedApp[selection.AppCapabilityID] = struct{}{}
		selectedRuntime[selection.RuntimeCapabilityID] = struct{}{}
	}
	workloadRuntime := make(map[string]struct{}, len(workloadCapabilities))
	for index, capabilityID := range workloadCapabilities {
		_, required := requiredSet[capabilityID]
		_, serving := selectedRuntime[capabilityID]
		if !required || serving || catalogapp.IsConsumerCapability(capabilityID) ||
			isHandlerRuntimeCompatibilityFence(capabilityID) ||
			(index > 0 && capabilityID <= workloadCapabilities[index-1]) {
			return fmt.Errorf("Catalog reconcile handler workload capabilities must be unique, sorted, required and disjoint from Catalog selections")
		}
		workloadRuntime[capabilityID] = struct{}{}
	}
	for _, capability := range provided {
		if capability.Kind != appauthority.CapabilityKindServiceHandler {
			return fmt.Errorf(
				"Catalog reconcile service selection does not match the verified AppRelease",
			)
		}
		_, appSelected := selectedApp[capability.CapabilityID]
		hasRuntimeWorkloadDependency := false
		for _, dependency := range capability.RequiredRuntimeCapabilities {
			if _, required := requiredSet[dependency]; !required {
				return fmt.Errorf(
					"Catalog reconcile App handler dependency is absent from the verified AppRelease requirements",
				)
			}
			if catalogapp.IsConsumerCapability(dependency) {
				continue
			}
			if isHandlerRuntimeCompatibilityFence(dependency) {
				continue
			}
			if _, selected := selectedRuntime[dependency]; selected {
				continue
			}
			_, workloadSelected := workloadRuntime[dependency]
			if (appSelected || len(workloadCapabilities) != 0) && !workloadSelected {
				return fmt.Errorf(
					"Catalog reconcile App handler dependency %q has no Runtime serving selection or explicit workload admission",
					dependency,
				)
			}
			hasRuntimeWorkloadDependency = true
		}
		if !appSelected && !hasRuntimeWorkloadDependency {
			return fmt.Errorf(
				"Catalog reconcile capability selection must cover every provided App capability",
			)
		}
	}
	return nil
}

// isHandlerRuntimeCompatibilityFence classifies closed host-advertised
// semantic fences that intentionally have no Runtime RPC or Catalog serving
// selection. The verified AppRelease and HostCompatibility still require the
// exact capability; this predicate only prevents manufacturing a fake serving
// binding for a deploy-time marker.
func isHandlerRuntimeCompatibilityFence(capabilityID string) bool {
	return capabilityID == permissionauthoringv1.RecordTypeObjectGroupsCapabilityName
}

func isCanonicalUUIDv7(value string) bool {
	if len(value) != 36 || value[8] != '-' || value[13] != '-' ||
		value[18] != '-' || value[23] != '-' || value[14] != '7' ||
		!strings.ContainsRune("89ab", rune(value[19])) {
		return false
	}
	for index, character := range value {
		if index == 8 || index == 13 || index == 18 || index == 23 {
			continue
		}
		if (character < '0' || character > '9') &&
			(character < 'a' || character > 'f') {
			return false
		}
	}
	return true
}

func (c ReleaseImportConfig) PublisherTrust() (catalogapp.PublisherTrust, error) {
	if strings.TrimSpace(c.Publisher.PublisherID) == "" || len(c.Publisher.Keys) == 0 {
		return catalogapp.PublisherTrust{}, fmt.Errorf("Catalog reconcile publisher and keys are required")
	}
	trust := catalogapp.PublisherTrust{PublisherID: c.Publisher.PublisherID, Active: true}
	for i, key := range c.Publisher.Keys {
		publicKey, err := base64.StdEncoding.Strict().DecodeString(key.PublicKeyBase64)
		if err != nil {
			return catalogapp.PublisherTrust{}, fmt.Errorf("decode publisher key[%d]: %w", i, err)
		}
		trust.Keys = append(trust.Keys, catalogapp.PublisherKey{
			KeyID: key.KeyID, Algorithm: key.Algorithm, PublicKey: publicKey,
			ValidFrom: key.ValidFrom, ValidUntil: key.ValidUntil,
		})
	}
	return trust, nil
}

func (c Config) Compatibility() releaseartifact.HostCompatibility {
	compatibility := releaseartifact.HostCompatibility{
		Protocols:          make([]releaseartifact.SupportedProtocol, 0, len(c.HostCompatibility.Protocols)),
		EngineCapabilities: append([]string(nil), c.HostCompatibility.EngineCapabilities...),
		ComponentKinds:     make([]releaseartifact.SupportedComponentKind, 0, len(c.HostCompatibility.ComponentKinds)),
	}
	for _, protocol := range c.HostCompatibility.Protocols {
		compatibility.Protocols = append(compatibility.Protocols, releaseartifact.SupportedProtocol{
			ProtocolID: protocol.ProtocolID, Major: protocol.Major, Minor: protocol.Minor,
		})
	}
	for _, kind := range c.HostCompatibility.ComponentKinds {
		compatibility.ComponentKinds = append(compatibility.ComponentKinds, releaseartifact.SupportedComponentKind{
			ComponentKind: kind.ComponentKind, ComponentFormatVersion: kind.ComponentFormatVersion,
		})
	}
	return compatibility
}
