//go:build crm

package crm

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"reflect"
	"sort"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/auth"
	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/backend/distributions/appsource"
	"github.com/nexuscrm/backend/pkg/serverruntime"
	dialectsfcrm "github.com/nexuscrm/dialect-sf-crm"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/dialect"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/approvalir"
	contractcp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

const SalesforceCRMStandardPackSourceRevision = appsource.SalesforceCRMStandardPackSourceRevision

var crmDistributionPackName = dialectsfcrm.NewCompositeTreePartition().OwningPack()

const (
	publicLoginProviderSalesforce              = "salesforce"
	oauthUserInfoProfileSalesforce             = "salesforce-compatible"
	oauthUserInfoSessionClassSalesforce        = "salesforce_compat"
	oauthTokenResponseProfileSalesforce        = "salesforce-compatible"
	runtimeRouteSurfaceSalesforceRootIdentity  = "salesforce-root-identity"
	runtimeRouteSurfaceGmailConnectedMailbox   = "gmail-connected-mailbox"
	runtimeRouteSurfaceScimIdentity            = "scim-identity"
	dynamicCORSRouteFamilySalesforceDataAPI    = "salesforce-data-api"
	oauthScopeRouteFamilySalesforceChatterAPI  = "salesforce-chatter-api"
	oauthScopeRouteFamilySalesforceOpenID      = "salesforce-openid"
	oauthScopeRouteFamilyExternalRecordsMCP    = "external-records-mcp"
	authChallengeProfileSalesforceRootIdentity = "salesforce-root-identity"
	salesforceLoginEnvVar                      = "SALESFORCE_ENVIRONMENT"
	legacySalesforceLoginEnvVar                = "REACT_APP_SALESFORCE_ENVIRONMENT"
	defaultSalesforceLoginEnv                  = "production"
	salesforceSandboxLoginEnv                  = "sandbox"
)

// RouteFamilyPrefix is the canonical Salesforce data-API route-family prefix the
// CRM distribution supplies to the app-blind OAuth scope readers. It is the
// single source for that prefix; the kernel does not own it.
const RouteFamilyPrefix = "/services/data/"

func PackName() string {
	return crmDistributionPackName
}

func HostPrimitives() []serverruntime.HostPrimitive {
	return []serverruntime.HostPrimitive{
		{Kind: "renderer", Name: "app-pack:crm/action-renderer/lead-convert"},
		{Kind: "renderer", Name: "app-pack:crm/action-renderer/create-record-from-source"},
		{Kind: "renderer", Name: "app-pack:crm/action-renderer/task-activity-quick-create"},
		{Kind: "renderer", Name: "app-pack:crm/action-renderer/email-compose"},
		{Kind: "renderer", Name: "app-pack:crm/record-panel/activity-timeline"},
		{Kind: "renderer", Name: "app-pack:crm/record-panel/record-activity-feed"},
		{Kind: "renderer", Name: "app-pack:crm/record-panel/record-child-bar-chart"},
		{Kind: "renderer", Name: "app-pack:crm/banner/converted-lead"},
	}
}

func FlowActionCallHandlerImplementations() []automation.FlowActionCallHandlerImplementation {
	return flowActionCallHandlerImplementations(&sendRecordEmailRuntimeBinding{})
}

func flowActionCallHandlerImplementations(sendRecordEmailRuntime *sendRecordEmailRuntimeBinding) []automation.FlowActionCallHandlerImplementation {
	return []automation.FlowActionCallHandlerImplementation{
		automation.LegacySendEmailAliasFlowActionCallHandlerImplementation("crm:automation/email-simple"),
		automation.DisabledWorkflowAlertFlowActionCallHandlerImplementation("crm:automation/email-alert-disabled"),
		recoverConnectorRemoteIdentityFlowActionCallHandlerImplementation(),
		sendRecordEmailFlowActionCallHandlerImplementation(sendRecordEmailRuntime),
		changeOwnerFlowActionCallHandlerImplementation(),
		stampConvertedAnchorsFlowActionCallHandlerImplementation(),
	}
}

func publicLoginProviders() []serverruntime.PublicLoginProviderRegistration {
	return []serverruntime.PublicLoginProviderRegistration{{
		ProviderID: publicLoginProviderSalesforce,
		Aliases:    []string{"salesforce_oauth", "salesforce_direct"},
		Build: func(ctx auth.PublicLoginProviderContext) auth.PublicLoginProviderDescriptor {
			return auth.PublicLoginProviderDescriptor{
				Kind:               "oauth2_pkce",
				Label:              "Salesforce",
				Placement:          "advanced",
				DefaultEnvironment: normalizeSalesforceLoginEnvironment(ctx.Env(salesforceLoginEnvVar), ctx.Env(legacySalesforceLoginEnvVar)),
			}
		},
	}}
}

func buildSalesforceUserInfoClaims(ctx auth.UserInfoProfileContext) map[string]any {
	baseURL := strings.TrimRight(ctx.InstanceURL, "/")
	orgID := strings.TrimSpace(ctx.OrgID)
	if orgID == "" {
		orgID = constants.DefaultOrgID
	}
	apiVersion := strings.TrimSpace(ctx.APIVersion)
	if apiVersion == "" {
		apiVersion = "v63.0"
	}
	dataBaseURL := fmt.Sprintf("%s/services/data/%s", baseURL, apiVersion)
	return map[string]any{
		"sub":             fmt.Sprintf("%s/id/%s/%s", baseURL, orgID, ctx.UserID),
		"organization_id": orgID,
		"user_type":       "STANDARD",
		"language":        "en_US",
		"urls": map[string]any{
			"enterprise": fmt.Sprintf("%s/services/Soap/c/%s/%s", baseURL, apiVersionNumber(apiVersion), orgID),
			"rest":       fmt.Sprintf("%s/", dataBaseURL),
			"sobjects":   fmt.Sprintf("%s/sobjects/", dataBaseURL),
			"query":      fmt.Sprintf("%s/query/", dataBaseURL),
			"search":     fmt.Sprintf("%s/search/", dataBaseURL),
			"profile":    fmt.Sprintf("%s/005/%s", baseURL, ctx.UserID),
		},
	}
}

func salesforceUserInfoProfileApplies(ctx auth.UserInfoProfileContext) bool {
	return strings.TrimSpace(ctx.ConnectedAppID) != "" &&
		strings.EqualFold(strings.TrimSpace(ctx.SessionClass), oauthUserInfoSessionClassSalesforce)
}

func salesforceTokenResponseProfileApplies(ctx auth.TokenResponseProfileContext) bool {
	return strings.TrimSpace(ctx.ConnectedAppID) != "" &&
		strings.EqualFold(strings.TrimSpace(ctx.SessionClass), oauthUserInfoSessionClassSalesforce)
}

func apiVersionNumber(apiVersion string) string {
	return strings.TrimPrefix(strings.TrimSpace(apiVersion), "v")
}

func buildSalesforceTokenResponseClaims(ctx auth.TokenResponseProfileContext) map[string]any {
	baseURL := strings.TrimRight(ctx.InstanceURL, "/")
	orgID := strings.TrimSpace(ctx.OrgID)
	if orgID == "" {
		orgID = constants.DefaultOrgID
	}
	claims := map[string]any{}
	tokenID := ""
	if strings.TrimSpace(ctx.UserID) != "" && strings.TrimSpace(baseURL) != "" {
		tokenID = fmt.Sprintf("%s/id/%s/%s", baseURL, orgID, ctx.UserID)
		claims["id"] = tokenID
	}
	if secret := strings.TrimSpace(ctx.ClientSecret); secret != "" {
		if strings.TrimSpace(tokenID) != "" && strings.TrimSpace(ctx.IssuedAt) != "" {
			claims["signature"] = computeSalesforceTokenSignature(secret, tokenID, ctx.IssuedAt)
		}
	}
	return claims
}

func computeSalesforceTokenSignature(consumerSecret, tokenID, issuedAt string) string {
	mac := hmac.New(sha256.New, []byte(consumerSecret))
	mac.Write([]byte(tokenID + issuedAt))
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

func normalizeSalesforceLoginEnvironment(values ...string) string {
	for _, value := range values {
		switch strings.ToLower(strings.TrimSpace(value)) {
		case salesforceSandboxLoginEnv:
			return salesforceSandboxLoginEnv
		case defaultSalesforceLoginEnv:
			return defaultSalesforceLoginEnv
		}
	}
	return defaultSalesforceLoginEnv
}

func ProductHooks() serverruntime.ProductHooks {
	return productHooks(&sendRecordEmailRuntimeBinding{})
}

func productHooks(sendRecordEmailRuntime *sendRecordEmailRuntimeBinding) serverruntime.ProductHooks {
	return serverruntime.ProductHooks{
		OwningAppPacks: []string{crmDistributionPackName},
		KnownProductAppPacks: append(
			appsource.FirstPartyProductPackNames(),
			appsource.RetiredFirstPartyProductPackNames()...,
		),
		Capabilities: serverruntime.ProductCapabilities{
			HostPrimitives: HostPrimitives(),
			ApprovalDefinitionDecoder: dialectsfcrm.NewApprovalProcessDefinitionDecoderWithFormulaValidator(
				approvalir.FormulaValidatorFunc(func(source string) error {
					return formula.NewEngine().ValidateFilterFormulaExpression(source, nil)
				}),
			),
			ApprovalQueryOnlyOwnerPacks:                     []string{crmDistributionPackName},
			ConnectorInboundRemoteIdentityRecoveryValidator: crmConnectorInboundRemoteIdentityRecoveryValidator{},
			OfficialRepoSourceRevisions: []serverruntime.OfficialRepoSourceRevision{
				{Name: crmDistributionPackName, Revision: SalesforceCRMStandardPackSourceRevision},
			},
			ComponentKindManagedLifecycleRegistrationProviders: []serverruntime.ComponentKindManagedLifecycleRegistrationProvider{
				permissions.NewCustomPermissionManagedLifecycleRegistration,
				permissions.NewPermissionSetDefinitionManagedLifecycleRegistration,
				newCRMAssignmentPolicyManagedLifecycleRegistration,
				dialectsfcrm.NewApprovalProcessManagedLifecycleRegistration,
			},
			RuntimeRouteSurfaces: []serverruntime.RuntimeRouteSurfaceRegistration{
				{Name: runtimeRouteSurfaceGmailConnectedMailbox, Register: func(ctx serverruntime.RuntimeRouteSurfaceContext) error {
					return registerGmailConnectedMailboxSurface(ctx, sendRecordEmailRuntime)
				}},
				{Name: runtimeRouteSurfaceScimIdentity, Register: registerScimIdentitySurface},
			},
			DynamicCORSRouteFamilies: []serverruntime.DynamicCORSRouteFamilyRegistration{
				{Name: dynamicCORSRouteFamilySalesforceDataAPI, Allows: allowSalesforceDataAPIDynamicCORS},
				{Name: runtimeRouteSurfaceSalesforceRootIdentity, Allows: allowSalesforceRootIdentityDynamicCORS},
			},
			OAuthRouteScopes: []serverruntime.OAuthRouteScopeRegistration{
				{Scope: "chatter_api"},
			},
			OAuthScopeRouteFamilies: []serverruntime.OAuthScopeRouteFamilyRegistration{
				{Name: oauthScopeRouteFamilySalesforceChatterAPI, Scopes: []string{"chatter_api"}, Allows: allowSalesforceChatterAPIScopeRoute},
				{Name: oauthScopeRouteFamilySalesforceOpenID, Scopes: []string{"openid"}, Allows: allowSalesforceOpenIDScopeRoute},
				{Name: oauthScopeRouteFamilyExternalRecordsMCP, Scopes: []string{"external.records.read"}, Allows: allowExternalRecordsMCPScopeRoute, RequireActiveSourceScope: true},
			},
			// The Salesforce data-API versioned route prefix the host installs into
			// the app-blind OAuth scope readers to canonicalize/validate stored
			// route patterns. Single source = RouteFamilyPrefix.
			OAuthRouteFamilyPrefix: RouteFamilyPrefix,
			AuthChallengeProfiles: []serverruntime.AuthChallengeProfileRegistration{
				{ProfileID: authChallengeProfileSalesforceRootIdentity, Build: buildSalesforceRootIdentityAuthChallenge},
			},
			PublicLoginProviders: publicLoginProviders(),
			OAuthUserInfoProfiles: []serverruntime.OAuthUserInfoProfileRegistration{
				{ProfileID: oauthUserInfoProfileSalesforce, Applies: salesforceUserInfoProfileApplies, Build: buildSalesforceUserInfoClaims},
			},
			OAuthSessionIssuerClasses: []serverruntime.OAuthSessionIssuerClassRegistration{
				{Class: oauthUserInfoSessionClassSalesforce},
			},
			OAuthTokenResponseProfiles: []serverruntime.OAuthTokenResponseProfileRegistration{
				{ProfileID: oauthTokenResponseProfileSalesforce, Applies: salesforceTokenResponseProfileApplies, Build: buildSalesforceTokenResponseClaims},
			},
			FlowActionCallHandlerImplementations: flowActionCallHandlerImplementations(sendRecordEmailRuntime),
			StreamingEventChannelMappings:        StreamingEventChannelMappings(),
			DetachedReferenceResolvers: []serverruntime.DetachedReferenceResolverRegistration{
				{
					SourceType:     "content_compat",
					Resolver:       dialectsfcrm.DetachedFileReferenceResolver(),
					OwningAppPacks: []string{crmDistributionPackName},
				},
			},
		},
		Dialects:                              crmProductDialects,
		ValidateComposition:                   validateProductDialectComposition,
		AssignmentPolicyEligibilityProjection: newAssignmentPolicyEligibilityProjectionProvider(),
		InternalErrorProjector:                salesforceInternalErrorProjector,
		AuthFailureProjector:                  salesforceAuthFailureProjector,
	}
}

func newCRMAssignmentPolicyManagedLifecycleRegistration() (
	contractcp.ComponentKindRegistration,
	contractcp.ComponentManagedLifecycleRegistration,
	error,
) {
	return permissions.NewPermissionSetAssignmentPolicyManagedLifecycleRegistrationWithEligibility(
		newAssignmentPolicyEligibilityProjectionProvider(),
	)
}

// salesforceAuthFailureProjector supplies the Salesforce v63 authentication-failure
// wire body for the app-blind L0 auth middleware. It matches the pre-PR-6 behavior
// the kernel previously hardcoded: default status 401, default code
// SFCodeInvalidSessionID, body = the Salesforce error-array shape. The L0 default
// already supplies status 401 and the "Session expired or invalid" message; this
// projector owns the Salesforce vocabulary (code default + array shape).
func salesforceAuthFailureProjector(c *gin.Context, status int, code, message string) (int, any) {
	return status, SFErrorResponse(orDefault(code, SFCodeInvalidSessionID), message)
}

func orDefault(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

// salesforceInternalErrorProjector supplies the Salesforce v63 generic-500 wire body
// for the global error handler — but only on the Salesforce data-API route family.
// Other (legacy/product) routes return (0, nil) so app-blind L0 emits its neutral
// default; this keeps Salesforce wire skin off non-dialect routes.
func salesforceInternalErrorProjector(c *gin.Context) (int, any) {
	path := c.FullPath()
	if path == "" && c.Request != nil && c.Request.URL != nil {
		path = c.Request.URL.Path
	}
	// Derive route-family ownership from the single canonical predicate
	// (isSalesforceDataAPIPath) so this projector cannot drift from CORS / the rest
	// of the data-API family logic; it covers both the bare discovery root and the
	// versioned tree.
	if !isSalesforceDataAPIPath(path) {
		return 0, nil
	}
	// V63InternalErrorProjector ignores its request argument.
	return dialectsfcrm.V63InternalErrorProjector(nil)
}

func crmProductDialects(ctx serverruntime.DialectRegistrationContext) []dialect.Dialect {
	db := ctx.DB
	fieldsForObject := ctx.HrefFieldsForObject
	if fieldsForObject == nil {
		fieldsForObject = func(string) []string { return nil }
	}
	return []dialect.Dialect{
		dialectsfcrm.NewCompositeTreePartition(
			dialectsfcrm.WithContentDistributionRecordReadDecoration(
				fieldsForObject,
				dialectsfcrm.NewContentDistributionFileLookup(db),
			),
		),
	}
}

// validateProductDialectComposition is the CRM distribution's boot-time
// composition guard. It follows #2063's split:
//   - compiled dialect without an active pack is normal for greenfield boot and
//     route mounting will skip the inactive pack;
//   - active pack without the matching compiled dialect/payload must fail loud.
//
// The generic host owns the check, but the CRM module owns the required payload
// inventory. This keeps L0 from authoring CRM facts while still refusing to serve
// a CRM install whose pack-owned write-guard metadata has not been applied.
func validateProductDialectComposition(ctx context.Context, db *sql.DB, activeAppPacks []serverruntime.ActiveAppPack) error {
	activeAppPacks = normalizeActiveAppPacks(activeAppPacks)
	if len(activeAppPacks) == 0 {
		return nil
	}

	// An unknown companion app-pack (flows / custom objects, no compiled product
	// dialect of its own) installed ALONGSIDE the active CRM pack is a legitimate
	// co-install. First-party product packs are different: ComposeProductDistribution
	// applies KnownProductAppPacks coverage before invoking this validator. This
	// local check still fails loud when NO active app-pack
	// is the CRM pack: the bundled CRM dialect is gated on it, so without it the
	// server would boot with the product /services/data surface silently unmounted.
	var crmPack *serverruntime.ActiveAppPack
	for i := range activeAppPacks {
		if strings.EqualFold(activeAppPacks[i].Name, crmDistributionPackName) {
			crmPack = &activeAppPacks[i]
			break
		}
	}
	if crmPack == nil {
		return fmt.Errorf(
			"cmd/crm-server CRM distribution bundles product dialects for %q, but active app pack(s) are installed: %s; start a distribution build that includes those packs or deactivate them",
			crmDistributionPackName,
			strings.Join(activeAppPackNames(activeAppPacks), ", "),
		)
	}
	if problem := validateCRMDistributionPackLock(*crmPack); problem != "" {
		return fmt.Errorf(
			"cmd/crm-server CRM distribution active app pack %q does not match the compiled distribution lock; re-install/re-apply the matching CRM app-pack revision before serving: %s",
			crmDistributionPackName,
			problem,
		)
	}
	if err := validateCRMRecordMutationPolicyPayload(db, crmPack.SourceKind, crmPack.SourceRevision); err != nil {
		return err
	}
	if err := validateCRMObjectLifecyclePolicyPayload(db); err != nil {
		return err
	}
	if err := validateCRMFolderTypePermissionPayload(db, crmPack.SourceRevision); err != nil {
		return err
	}
	if err := validateCRMCustomPermissionExactSetPayload(db); err != nil {
		return err
	}
	return validateCRMApprovalProcessTopology(ctx, db)
}

func normalizeActiveAppPacks(rows []serverruntime.ActiveAppPack) []serverruntime.ActiveAppPack {
	seen := map[string]struct{}{}
	out := make([]serverruntime.ActiveAppPack, 0, len(rows))
	for _, row := range rows {
		row.Name = strings.TrimSpace(row.Name)
		row.SourceKind = strings.TrimSpace(row.SourceKind)
		row.SourceURI = strings.TrimSpace(row.SourceURI)
		row.SourceRevision = strings.TrimSpace(row.SourceRevision)
		row.AppliedPayloadHash = strings.TrimSpace(row.AppliedPayloadHash)
		row.DesiredPayloadHash = strings.TrimSpace(row.DesiredPayloadHash)
		name := row.Name
		if name == "" {
			continue
		}
		key := strings.ToLower(name)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, row)
	}
	sort.Slice(out, func(i, j int) bool {
		return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
	})
	return out
}

func activeAppPackNames(rows []serverruntime.ActiveAppPack) []string {
	names := make([]string, 0, len(rows))
	for _, row := range rows {
		if name := strings.TrimSpace(row.Name); name != "" {
			names = append(names, name)
		}
	}
	sort.Strings(names)
	return names
}

func validateCRMDistributionPackLock(row serverruntime.ActiveAppPack) string {
	if problem := validateCRMDistributionPackApplyState(row); problem != "" {
		return problem
	}
	return validateCRMDistributionPackSourceLock(row)
}

func validateCRMDistributionPackApplyState(row serverruntime.ActiveAppPack) string {
	if strings.TrimSpace(row.AppliedPayloadHash) == "" {
		return "AppliedPayloadHash is empty"
	}
	if strings.TrimSpace(row.DesiredPayloadHash) == "" {
		return "DesiredPayloadHash is empty"
	}
	if strings.TrimSpace(row.AppliedPayloadHash) != strings.TrimSpace(row.DesiredPayloadHash) {
		return fmt.Sprintf("AppliedPayloadHash = %q does not match DesiredPayloadHash = %q", row.AppliedPayloadHash, row.DesiredPayloadHash)
	}
	return ""
}

func validateCRMDistributionPackSourceLock(row serverruntime.ActiveAppPack) string {
	sourceKind := normalizedDistributionSourceKind(row.SourceKind)
	// The L1 binary owns compatibility for its directly resolved official pack.
	// Local and immutable release sources are composed by Controlplane, which
	// owns their URI, revision, payload hash and generation proof. Binding those
	// external identities back to this L1 repository would make L1 depend on L2.
	// Their CRM compatibility is established by the live capability checks below,
	// including CustomPermission exact-set payload proof.
	switch sourceKind {
	case serverruntime.PackSourceKindLocalPath, "release_repo":
		if strings.TrimSpace(row.SourceURI) == "" {
			return fmt.Sprintf("SourceURI is empty for %s", sourceKind)
		}
		return ""
	case serverruntime.PackSourceKindOfficialRepo:
	default:
		return fmt.Sprintf(
			"SourceKind = %q, want %q, %q, or %q",
			row.SourceKind,
			serverruntime.PackSourceKindOfficialRepo,
			serverruntime.PackSourceKindLocalPath,
			"release_repo",
		)
	}
	if !strings.EqualFold(strings.TrimSpace(row.SourceURI), crmDistributionPackName) {
		return fmt.Sprintf("SourceURI = %q, want %q", row.SourceURI, crmDistributionPackName)
	}
	sourceRevision := strings.TrimSpace(row.SourceRevision)
	if sourceRevision == "" {
		return "SourceRevision is empty"
	}
	if !isAcceptedCRMDistributionSourceRevision(sourceRevision) {
		return fmt.Sprintf("SourceRevision = %q, want %s", sourceRevision, acceptedCRMDistributionSourceRevisionSummary())
	}
	return ""
}

func isAcceptedCRMDistributionSourceRevision(revision string) bool {
	contract, ok := appsource.LookupSalesforceCRMStandardPackSourceContract(revision)
	return ok && contract.DeclaresCustomPermissionExactSet
}

func acceptedCRMDistributionSourceRevisionSummary() string {
	return fmt.Sprintf("%q with first-class CustomPermission exact-set declarations", SalesforceCRMStandardPackSourceRevision)
}

func normalizedDistributionSourceKind(sourceKind string) string {
	sourceKind = strings.TrimSpace(sourceKind)
	if strings.EqualFold(sourceKind, "in_repo") {
		return serverruntime.PackSourceKindOfficialRepo
	}
	return sourceKind
}

func validateCRMRecordMutationPolicyPayload(db *sql.DB, sourceKind, sourceRevision string) error {
	if db == nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q record-mutation policies: database is unavailable", crmDistributionPackName)
	}
	contractRevision := sourceRevision
	if usesExternalCRMCapabilityProof(sourceKind) {
		contractRevision = SalesforceCRMStandardPackSourceRevision
	}
	sourceContract, ok := appsource.LookupSalesforceCRMStandardPackSourceContract(contractRevision)
	if !ok {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q record-mutation policies: source revision %q has no accepted payload contract", crmDistributionPackName, strings.TrimSpace(sourceRevision))
	}
	requiredPolicies := dialectsfcrm.RequiredRecordMutationPolicies()
	required := make([]string, 0, len(requiredPolicies))
	acceptedByObject := make(map[string][]models.RecordMutationAccessPolicy, len(requiredPolicies))
	for _, policy := range requiredPolicies {
		objectAPIName := strings.TrimSpace(policy.ObjectAPIName)
		if strings.EqualFold(objectAPIName, "ProcessNode") && !sourceContract.RequiresProcessNodeRecordMutationPolicy {
			continue
		}
		if strings.EqualFold(objectAPIName, "IdentitySyncSetting") && !sourceContract.RequiresIdentitySyncSettingRecordMutationPolicy {
			continue
		}
		if strings.EqualFold(objectAPIName, "IdentitySyncDeactivationTicket") && !sourceContract.RequiresIdentitySyncDeactivationTicketRecordMutationPolicy {
			continue
		}
		required = append(required, objectAPIName)
		key := strings.ToLower(objectAPIName)
		acceptedByObject[key] = append(acceptedByObject[key], policy.Policy)
	}
	for _, policy := range dialectsfcrm.RolloutCompatibleRecordMutationPolicies() {
		objectAPIName := strings.TrimSpace(policy.ObjectAPIName)
		if objectAPIName == "" {
			continue
		}
		key := strings.ToLower(objectAPIName)
		if _, ok := acceptedByObject[key]; ok {
			acceptedByObject[key] = append(acceptedByObject[key], policy.Policy)
		}
	}
	if len(required) == 0 {
		return nil
	}
	placeholders := make([]string, 0, len(required))
	args := make([]any, 0, len(required))
	for _, objectAPIName := range required {
		placeholders = append(placeholders, "?")
		args = append(args, strings.ToLower(strings.TrimSpace(objectAPIName)))
	}
	query := fmt.Sprintf(`
SELECT ApiName, AccessPolicy, AccessPolicyRecordMutationPackName, AccessPolicyRecordMutationPackBusinessKey, AccessPolicyRecordMutationPackValue
FROM _System_Object
WHERE LOWER(ApiName) IN (%s) AND COALESCE(IsDeleted, 0) = 0`, strings.Join(placeholders, ","))
	rows, err := db.QueryContext(context.Background(), query, args...)
	if err != nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q record-mutation policies: %w", crmDistributionPackName, err)
	}
	defer rows.Close()

	live := map[string]crmRecordMutationPolicyRow{}
	for rows.Next() {
		var row crmRecordMutationPolicyRow
		if err := rows.Scan(&row.apiName, &row.accessPolicy, &row.packName, &row.packBusinessKey, &row.packValue); err != nil {
			return fmt.Errorf("cmd/crm-server CRM distribution cannot scan record-mutation policy ownership: %w", err)
		}
		live[strings.ToLower(strings.TrimSpace(row.apiName))] = row
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot iterate record-mutation policy ownership: %w", err)
	}

	var problems []string
	for _, objectAPIName := range required {
		key := strings.ToLower(strings.TrimSpace(objectAPIName))
		row, ok := live[key]
		if !ok {
			problems = append(problems, fmt.Sprintf("%s missing live _System_Object row", objectAPIName))
			continue
		}
		if problem := validateCRMRecordMutationPolicyRow(objectAPIName, acceptedByObject[key], row); problem != "" {
			problems = append(problems, problem)
		}
	}
	if len(problems) > 0 {
		sort.Strings(problems)
		return fmt.Errorf(
			"cmd/crm-server CRM distribution active app pack %q is missing required record-mutation policy payload; re-apply/install the matching CRM app-pack revision before serving: %s",
			crmDistributionPackName,
			strings.Join(problems, "; "),
		)
	}
	return nil
}

// Externally composed CRM packs are admitted by capability proof, not by an L2
// repository identity. Only the official_repo source is resolved and versioned
// by this L1 distribution itself.
func usesExternalCRMCapabilityProof(sourceKind string) bool {
	return normalizedDistributionSourceKind(sourceKind) != serverruntime.PackSourceKindOfficialRepo
}

type crmRecordMutationPolicyRow struct {
	apiName         string
	accessPolicy    sql.NullString
	packName        sql.NullString
	packBusinessKey sql.NullString
	packValue       sql.NullString
}

func validateCRMRecordMutationPolicyRow(objectAPIName string, accepted []models.RecordMutationAccessPolicy, row crmRecordMutationPolicyRow) string {
	if !strings.EqualFold(strings.TrimSpace(row.packName.String), crmDistributionPackName) {
		return fmt.Sprintf("%s recordMutation owner pack = %q, want %q", objectAPIName, strings.TrimSpace(row.packName.String), crmDistributionPackName)
	}
	if !strings.EqualFold(strings.TrimSpace(row.packBusinessKey.String), objectAPIName) {
		return fmt.Sprintf("%s recordMutation business key = %q, want %q", objectAPIName, strings.TrimSpace(row.packBusinessKey.String), objectAPIName)
	}
	liveRecordMutation, problem := decodeLiveRecordMutationPolicy(objectAPIName, row.accessPolicy)
	if problem != "" {
		return problem
	}
	sidecarRecordMutation, problem := decodeSidecarRecordMutationPolicy(objectAPIName, row.packValue)
	if problem != "" {
		return problem
	}
	if !reflect.DeepEqual(*liveRecordMutation, *sidecarRecordMutation) {
		return fmt.Sprintf("%s AccessPolicy.recordMutation does not match AccessPolicyRecordMutationPackValue", objectAPIName)
	}
	for _, candidate := range accepted {
		if reflect.DeepEqual(*liveRecordMutation, candidate) {
			return ""
		}
	}
	return fmt.Sprintf("%s AccessPolicy.recordMutation does not match the CRM distribution's required policy contract", objectAPIName)
}

func decodeLiveRecordMutationPolicy(objectAPIName string, raw sql.NullString) (*models.RecordMutationAccessPolicy, string) {
	var policy models.ObjectAccessPolicy
	if !raw.Valid || strings.TrimSpace(raw.String) == "" {
		return nil, fmt.Sprintf("%s AccessPolicy is empty", objectAPIName)
	}
	if err := json.Unmarshal([]byte(raw.String), &policy); err != nil {
		return nil, fmt.Sprintf("%s AccessPolicy is not valid JSON: %v", objectAPIName, err)
	}
	if policy.RecordMutation == nil || len(policy.RecordMutation.Deny) == 0 {
		return nil, fmt.Sprintf("%s AccessPolicy.recordMutation is missing", objectAPIName)
	}
	return policy.RecordMutation, ""
}

func decodeSidecarRecordMutationPolicy(objectAPIName string, raw sql.NullString) (*models.RecordMutationAccessPolicy, string) {
	if !raw.Valid || strings.TrimSpace(raw.String) == "" {
		return nil, fmt.Sprintf("%s recordMutation pack value sidecar is empty", objectAPIName)
	}
	var packValue models.RecordMutationAccessPolicy
	if err := json.Unmarshal([]byte(raw.String), &packValue); err != nil {
		return nil, fmt.Sprintf("%s recordMutation pack value is not valid JSON: %v", objectAPIName, err)
	}
	if len(packValue.Deny) == 0 {
		return nil, fmt.Sprintf("%s recordMutation pack value has no denials", objectAPIName)
	}
	return &packValue, ""
}

func validateCRMObjectLifecyclePolicyPayload(db *sql.DB) error {
	if db == nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q lifecycle policies: database is unavailable", crmDistributionPackName)
	}
	requiredPolicies := dialectsfcrm.RequiredObjectLifecyclePolicies()
	required := make([]string, 0, len(requiredPolicies))
	acceptedByObject := make(map[string][]models.RecordLifecycleAccessPolicy, len(requiredPolicies))
	for _, policy := range requiredPolicies {
		objectAPIName := strings.TrimSpace(policy.ObjectAPIName)
		required = append(required, objectAPIName)
		key := strings.ToLower(objectAPIName)
		acceptedByObject[key] = append(acceptedByObject[key], policy.Policy)
	}
	if len(required) == 0 {
		return nil
	}
	placeholders := make([]string, 0, len(required))
	args := make([]any, 0, len(required))
	for _, objectAPIName := range required {
		placeholders = append(placeholders, "?")
		args = append(args, strings.ToLower(strings.TrimSpace(objectAPIName)))
	}
	query := fmt.Sprintf(`
SELECT ApiName, AccessPolicy, AccessPolicyLifecyclePackName, AccessPolicyLifecyclePackBusinessKey, AccessPolicyLifecyclePackValue
FROM _System_Object
WHERE LOWER(ApiName) IN (%s) AND COALESCE(IsDeleted, 0) = 0`, strings.Join(placeholders, ","))
	rows, err := db.QueryContext(context.Background(), query, args...)
	if err != nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q lifecycle policies: %w", crmDistributionPackName, err)
	}
	defer rows.Close()

	live := map[string]crmObjectLifecyclePolicyRow{}
	for rows.Next() {
		var row crmObjectLifecyclePolicyRow
		if err := rows.Scan(&row.apiName, &row.accessPolicy, &row.packName, &row.packBusinessKey, &row.packValue); err != nil {
			return fmt.Errorf("cmd/crm-server CRM distribution cannot scan lifecycle policy ownership: %w", err)
		}
		live[strings.ToLower(strings.TrimSpace(row.apiName))] = row
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot iterate lifecycle policy ownership: %w", err)
	}

	var problems []string
	for _, objectAPIName := range required {
		key := strings.ToLower(strings.TrimSpace(objectAPIName))
		row, ok := live[key]
		if !ok {
			problems = append(problems, fmt.Sprintf("%s missing live _System_Object row", objectAPIName))
			continue
		}
		if problem := validateCRMObjectLifecyclePolicyRow(objectAPIName, acceptedByObject[key], row); problem != "" {
			problems = append(problems, problem)
		}
	}
	if len(problems) > 0 {
		sort.Strings(problems)
		return fmt.Errorf(
			"cmd/crm-server CRM distribution active app pack %q is missing required lifecycle policy payload; re-apply/install the matching CRM app-pack revision before serving: %s",
			crmDistributionPackName,
			strings.Join(problems, "; "),
		)
	}
	return nil
}

type crmObjectLifecyclePolicyRow struct {
	apiName         string
	accessPolicy    sql.NullString
	packName        sql.NullString
	packBusinessKey sql.NullString
	packValue       sql.NullString
}

func validateCRMObjectLifecyclePolicyRow(objectAPIName string, accepted []models.RecordLifecycleAccessPolicy, row crmObjectLifecyclePolicyRow) string {
	if !strings.EqualFold(strings.TrimSpace(row.packName.String), crmDistributionPackName) {
		return fmt.Sprintf("%s lifecycle owner pack = %q, want %q", objectAPIName, strings.TrimSpace(row.packName.String), crmDistributionPackName)
	}
	if !strings.EqualFold(strings.TrimSpace(row.packBusinessKey.String), objectAPIName) {
		return fmt.Sprintf("%s lifecycle business key = %q, want %q", objectAPIName, strings.TrimSpace(row.packBusinessKey.String), objectAPIName)
	}
	liveLifecycle, problem := decodeLiveObjectLifecyclePolicy(objectAPIName, row.accessPolicy)
	if problem != "" {
		return problem
	}
	sidecarLifecycle, problem := decodeSidecarObjectLifecyclePolicy(objectAPIName, row.packValue)
	if problem != "" {
		return problem
	}
	if !reflect.DeepEqual(*liveLifecycle, *sidecarLifecycle) {
		return fmt.Sprintf("%s AccessPolicy.lifecycle does not match AccessPolicyLifecyclePackValue", objectAPIName)
	}
	for _, candidate := range accepted {
		if reflect.DeepEqual(*liveLifecycle, candidate) {
			return ""
		}
	}
	return fmt.Sprintf("%s AccessPolicy.lifecycle does not match the CRM distribution's required lifecycle contract", objectAPIName)
}

func decodeLiveObjectLifecyclePolicy(objectAPIName string, raw sql.NullString) (*models.RecordLifecycleAccessPolicy, string) {
	var policy models.ObjectAccessPolicy
	if !raw.Valid || strings.TrimSpace(raw.String) == "" {
		return nil, fmt.Sprintf("%s AccessPolicy is empty", objectAPIName)
	}
	if err := json.Unmarshal([]byte(raw.String), &policy); err != nil {
		return nil, fmt.Sprintf("%s AccessPolicy is not valid JSON: %v", objectAPIName, err)
	}
	if policy.Lifecycle == nil || len(policy.Lifecycle.Hooks) == 0 {
		return nil, fmt.Sprintf("%s AccessPolicy.lifecycle is missing", objectAPIName)
	}
	return policy.Lifecycle, ""
}

func decodeSidecarObjectLifecyclePolicy(objectAPIName string, raw sql.NullString) (*models.RecordLifecycleAccessPolicy, string) {
	if !raw.Valid || strings.TrimSpace(raw.String) == "" {
		return nil, fmt.Sprintf("%s lifecycle pack value sidecar is empty", objectAPIName)
	}
	var packValue models.RecordLifecycleAccessPolicy
	if err := json.Unmarshal([]byte(raw.String), &packValue); err != nil {
		return nil, fmt.Sprintf("%s lifecycle pack value is not valid JSON: %v", objectAPIName, err)
	}
	if len(packValue.Hooks) == 0 {
		return nil, fmt.Sprintf("%s lifecycle pack value has no hooks", objectAPIName)
	}
	return &packValue, ""
}

type crmFolderTypePermissionRequirement struct {
	apiName                           string
	filterGroup                       string
	createPermission                  string
	managePermission                  string
	personalFolderName                string
	personalFolderDeveloperNamePrefix string
}

type crmFolderTypePermissionRow struct {
	apiName                           string
	filterGroup                       sql.NullString
	createPermission                  sql.NullString
	managePermission                  sql.NullString
	personalFolderName                sql.NullString
	personalFolderDeveloperNamePrefix sql.NullString
}

func validateCRMFolderTypePermissionPayload(db *sql.DB, sourceRevision string) error {
	if db == nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q folder-type permissions: database is unavailable", crmDistributionPackName)
	}
	// Every accepted CRM source revision now ships the Report/Dashboard
	// _System_FolderType rows, so the readiness guard can fail loud whenever the
	// rows are missing or their permission bindings drift.
	required := []crmFolderTypePermissionRequirement{
		{
			apiName:                           "Dashboard",
			filterGroup:                       "Dashboards",
			createPermission:                  string(constants.PermCreateDashboards),
			managePermission:                  string(constants.PermManageDashbdsInPubFolders),
			personalFolderName:                "My Personal Dashboards",
			personalFolderDeveloperNamePrefix: "my_personal_dashboards",
		},
		{
			apiName:                           "Report",
			filterGroup:                       "Reports",
			createPermission:                  string(constants.PermCreateCustomizeReports),
			managePermission:                  string(constants.PermManageReportsInPubFolders),
			personalFolderName:                "My Personal Custom Reports",
			personalFolderDeveloperNamePrefix: "my_personal_reports",
		},
	}
	placeholders := make([]string, 0, len(required))
	args := make([]any, 0, len(required))
	for _, requirement := range required {
		placeholders = append(placeholders, "?")
		args = append(args, strings.ToLower(strings.TrimSpace(requirement.apiName)))
	}
	query := fmt.Sprintf(`
SELECT ApiName, FilterGroup, CreatePermission, ManagePermission, PersonalFolderName, PersonalFolderDeveloperNamePrefix
FROM %s
WHERE LOWER(ApiName) IN (%s) AND COALESCE(IsDeleted, 0) = 0 AND COALESCE(IsActive, 0) = 1`, constants.TableFolderType, strings.Join(placeholders, ","))
	rows, err := db.QueryContext(context.Background(), query, args...)
	if err != nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q folder-type permissions: %w", crmDistributionPackName, err)
	}
	defer rows.Close()

	live := map[string][]crmFolderTypePermissionRow{}
	for rows.Next() {
		var row crmFolderTypePermissionRow
		if err := rows.Scan(
			&row.apiName,
			&row.filterGroup,
			&row.createPermission,
			&row.managePermission,
			&row.personalFolderName,
			&row.personalFolderDeveloperNamePrefix,
		); err != nil {
			return fmt.Errorf("cmd/crm-server CRM distribution cannot scan folder-type permission payload: %w", err)
		}
		key := strings.ToLower(strings.TrimSpace(row.apiName))
		if key == "" {
			continue
		}
		live[key] = append(live[key], row)
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot iterate folder-type permission payload: %w", err)
	}

	var problems []string
	for _, requirement := range required {
		key := strings.ToLower(strings.TrimSpace(requirement.apiName))
		rows := live[key]
		if len(rows) == 0 {
			problems = append(problems, fmt.Sprintf("%s missing live _System_FolderType row", requirement.apiName))
			continue
		}
		if len(rows) > 1 {
			problems = append(problems, fmt.Sprintf("%s has %d live _System_FolderType rows", requirement.apiName, len(rows)))
			continue
		}
		if problem := validateCRMFolderTypePermissionRow(requirement, rows[0]); problem != "" {
			problems = append(problems, problem)
		}
	}
	if len(problems) > 0 {
		sort.Strings(problems)
		return fmt.Errorf(
			"cmd/crm-server CRM distribution active app pack %q is missing required folder-type permission payload; re-apply/install the matching CRM app-pack revision before serving: %s",
			crmDistributionPackName,
			strings.Join(problems, "; "),
		)
	}
	return nil
}

func validateCRMFolderTypePermissionRow(requirement crmFolderTypePermissionRequirement, row crmFolderTypePermissionRow) string {
	filterGroup := strings.TrimSpace(row.filterGroup.String)
	if !row.filterGroup.Valid || filterGroup == "" {
		return fmt.Sprintf("%s FilterGroup is empty, want %q", requirement.apiName, requirement.filterGroup)
	}
	if filterGroup != requirement.filterGroup {
		return fmt.Sprintf("%s FilterGroup = %q, want %q", requirement.apiName, filterGroup, requirement.filterGroup)
	}
	createPermission := strings.TrimSpace(row.createPermission.String)
	if !row.createPermission.Valid || createPermission == "" {
		return fmt.Sprintf("%s CreatePermission is empty, want %q", requirement.apiName, requirement.createPermission)
	}
	if createPermission != requirement.createPermission {
		return fmt.Sprintf("%s CreatePermission = %q, want %q", requirement.apiName, createPermission, requirement.createPermission)
	}
	managePermission := strings.TrimSpace(row.managePermission.String)
	if !row.managePermission.Valid || managePermission == "" {
		return fmt.Sprintf("%s ManagePermission is empty, want %q", requirement.apiName, requirement.managePermission)
	}
	if managePermission != requirement.managePermission {
		return fmt.Sprintf("%s ManagePermission = %q, want %q", requirement.apiName, managePermission, requirement.managePermission)
	}
	personalFolderName := strings.TrimSpace(row.personalFolderName.String)
	if !row.personalFolderName.Valid || personalFolderName == "" {
		return fmt.Sprintf("%s PersonalFolderName is empty, want %q", requirement.apiName, requirement.personalFolderName)
	}
	if personalFolderName != requirement.personalFolderName {
		return fmt.Sprintf("%s PersonalFolderName = %q, want %q", requirement.apiName, personalFolderName, requirement.personalFolderName)
	}
	personalFolderDeveloperNamePrefix := strings.TrimSpace(row.personalFolderDeveloperNamePrefix.String)
	if !row.personalFolderDeveloperNamePrefix.Valid || personalFolderDeveloperNamePrefix == "" {
		return fmt.Sprintf("%s PersonalFolderDeveloperNamePrefix is empty, want %q", requirement.apiName, requirement.personalFolderDeveloperNamePrefix)
	}
	if personalFolderDeveloperNamePrefix != requirement.personalFolderDeveloperNamePrefix {
		return fmt.Sprintf("%s PersonalFolderDeveloperNamePrefix = %q, want %q", requirement.apiName, personalFolderDeveloperNamePrefix, requirement.personalFolderDeveloperNamePrefix)
	}
	return ""
}

func requiredCRMCustomPermissionAPINames() []string {
	return []string{
		dialectsfcrm.SendRecordEmailCustomPermission,
		"SkipOpportunityOnConvert",
	}
}

type crmCustomPermissionExactSetRow struct {
	apiName      string
	ownerPack    sql.NullString
	isActive     int64
	isCustomized int64
}

// validateCRMCustomPermissionExactSetPayload is the live serving proof that a
// CRM source actually declared the first-class CustomPermission exact set.
// official_repo revisions are gated by DeclaresCustomPermissionExactSet in the
// compiled source-contract map. local_path and release_repo identities are not
// compiled into that map on purpose (L1 must not bind L2 repository identity),
// so those sources prove the same payload from live CRM-owned catalog rows.
// Controlplane v0.2.68 separately refuses to mint ExactSet authority for kinds
// the source revision did not declare.
func validateCRMCustomPermissionExactSetPayload(db *sql.DB) error {
	if db == nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q custom-permission exact-set payload: database is unavailable", crmDistributionPackName)
	}
	required := requiredCRMCustomPermissionAPINames()
	requiredSet := make(map[string]struct{}, len(required))
	for _, apiName := range required {
		requiredSet[apiName] = struct{}{}
	}
	ownerLimit := permissions.CustomPermissionExactSetMaximumRows + 1
	ownerQuery := fmt.Sprintf(`
SELECT ApiName, OwnerPack, COALESCE(IsActive, 0), COALESCE(IsCustomized, 0)
FROM %s
WHERE COALESCE(IsDeleted, 0) = 0 AND OwnerPack = ?
LIMIT ?`, constants.TableCustomPermission)
	ownerRows, err := db.QueryContext(context.Background(), ownerQuery, crmDistributionPackName, ownerLimit)
	if err != nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q custom-permission exact-set payload: %w", crmDistributionPackName, err)
	}
	ownerSet, err := scanCRMCustomPermissionExactSetRows(ownerRows)
	if err != nil {
		return err
	}
	if len(ownerSet) >= ownerLimit {
		return fmt.Errorf(
			"cmd/crm-server CRM distribution cannot verify %q custom-permission exact-set payload: live owner set exceeds %d rows",
			crmDistributionPackName,
			permissions.CustomPermissionExactSetMaximumRows,
		)
	}

	placeholders := make([]string, 0, len(required))
	args := make([]any, 0, len(required)+1)
	for _, apiName := range required {
		placeholders = append(placeholders, "?")
		args = append(args, apiName)
	}
	requiredLimit := 4 * len(required)
	args = append(args, requiredLimit)
	requiredQuery := fmt.Sprintf(`
SELECT ApiName, OwnerPack, COALESCE(IsActive, 0), COALESCE(IsCustomized, 0)
FROM %s
WHERE COALESCE(IsDeleted, 0) = 0 AND ApiName IN (%s)
LIMIT ?`, constants.TableCustomPermission, strings.Join(placeholders, ","))
	requiredRows, err := db.QueryContext(context.Background(), requiredQuery, args...)
	if err != nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q custom-permission exact-set payload: %w", crmDistributionPackName, err)
	}
	requiredSetRows, err := scanCRMCustomPermissionExactSetRows(requiredRows)
	if err != nil {
		return err
	}
	if len(requiredSetRows) >= requiredLimit {
		return fmt.Errorf(
			"cmd/crm-server CRM distribution cannot verify %q custom-permission exact-set payload: required ApiName probe exceeded %d rows",
			crmDistributionPackName,
			requiredLimit,
		)
	}

	live := map[string][]crmCustomPermissionExactSetRow{}
	var problems []string
	for _, row := range mergeCRMCustomPermissionExactSetRows(ownerSet, requiredSetRows) {
		if row.apiName == "" {
			problems = append(problems, "empty ApiName live _System_CustomPermission row")
			continue
		}
		owner := ""
		if row.ownerPack.Valid {
			owner = row.ownerPack.String
		}
		// Fail closed on any stored owner that is not the exact CRM pack
		// bytes. OwnerPack = ? already returns collation aliases (case,
		// space, diacritic, width); the required ApiName probe must also
		// be CRM-owned.
		if owner != crmDistributionPackName {
			problems = append(problems, fmt.Sprintf("%s owner pack = %q, want %q", row.apiName, owner, crmDistributionPackName))
			continue
		}
		live[row.apiName] = append(live[row.apiName], row)
		if _, declared := requiredSet[row.apiName]; declared {
			continue
		}
		if row.isCustomized != 0 {
			continue
		}
		problems = append(problems, fmt.Sprintf("%s extra live managed _System_CustomPermission row", row.apiName))
	}

	for _, apiName := range required {
		matched := live[apiName]
		if len(matched) == 0 {
			problems = append(problems, fmt.Sprintf("%s missing live _System_CustomPermission row", apiName))
			continue
		}
		if len(matched) > 1 {
			problems = append(problems, fmt.Sprintf("%s has %d live _System_CustomPermission rows", apiName, len(matched)))
			continue
		}
		if problem := validateCRMCustomPermissionExactSetRow(apiName, matched[0]); problem != "" {
			problems = append(problems, problem)
		}
	}
	if len(problems) > 0 {
		sort.Strings(problems)
		return fmt.Errorf(
			"cmd/crm-server CRM distribution active app pack %q is missing required custom-permission exact-set payload; re-apply/install a CRM source that declares first-class CustomPermission components before serving: %s",
			crmDistributionPackName,
			strings.Join(problems, "; "),
		)
	}
	return nil
}

func validateCRMCustomPermissionExactSetRow(apiName string, row crmCustomPermissionExactSetRow) string {
	if row.apiName != apiName {
		return fmt.Sprintf("%s ApiName = %q, want exact %q", apiName, row.apiName, apiName)
	}
	if !row.ownerPack.Valid {
		return fmt.Sprintf("%s owner pack is empty, want %q", apiName, crmDistributionPackName)
	}
	// Exact stored bytes, not EqualFold/TrimSpace: collation-insensitive owner
	// aliases must fail closed the same way the managed lifecycle owner-set read does.
	if row.ownerPack.String != crmDistributionPackName {
		return fmt.Sprintf("%s owner pack = %q, want %q", apiName, row.ownerPack.String, crmDistributionPackName)
	}
	if row.isActive != 1 {
		return fmt.Sprintf("%s IsActive = %d, want 1", apiName, row.isActive)
	}
	return ""
}

func scanCRMCustomPermissionExactSetRows(rows *sql.Rows) ([]crmCustomPermissionExactSetRow, error) {
	defer rows.Close()
	var out []crmCustomPermissionExactSetRow
	for rows.Next() {
		var row crmCustomPermissionExactSetRow
		if err := rows.Scan(&row.apiName, &row.ownerPack, &row.isActive, &row.isCustomized); err != nil {
			return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot scan custom-permission exact-set payload: %w", err)
		}
		out = append(out, row)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot iterate custom-permission exact-set payload: %w", err)
	}
	return out, nil
}

func mergeCRMCustomPermissionExactSetRows(groups ...[]crmCustomPermissionExactSetRow) []crmCustomPermissionExactSetRow {
	type rowKey struct {
		apiName string
		owner   string
	}
	seen := make(map[rowKey]struct{})
	out := make([]crmCustomPermissionExactSetRow, 0)
	for _, group := range groups {
		for _, row := range group {
			owner := ""
			if row.ownerPack.Valid {
				owner = row.ownerPack.String
			}
			key := rowKey{apiName: row.apiName, owner: owner}
			if _, exists := seen[key]; exists {
				continue
			}
			seen[key] = struct{}{}
			out = append(out, row)
		}
	}
	return out
}

type crmApprovalProcessTopology struct {
	id            string
	developerName string
	rawMetadata   string
	nodes         []crmApprovalProcessNodeTopology
}

type crmApprovalProcessNodeTopology struct {
	id            string
	name          string
	developerName string
	stepOrder     int64
}

const crmApprovalProcessTopologyDefinitionPageSize = 100

const crmApprovalProcessTopologyDefinitionPageQuery = `
SELECT pd.Id, pd.DeveloperName, pd.Metadata
FROM ProcessDefinition pd
WHERE pd.State = 'Active' AND pd.OwnerPack = ? AND (? = '' OR pd.Id > ?)
ORDER BY pd.Id
LIMIT ?`

const crmApprovalProcessTopologyNodePageQuery = `
SELECT pn.Id, pn.Name, pn.DeveloperName, pn.StepOrder
FROM ProcessNode pn
WHERE pn.ProcessDefinitionId = ? AND COALESCE(pn.IsDeleted, 0) = 0
ORDER BY pn.StepOrder, pn.Id
LIMIT ?`

// validateCRMApprovalProcessTopology is the read-only boot proof required by
// the query-only approval runtime. The app-pack lifecycle remains the only
// writer; when an active CRM-owned definition carries authored Metadata, this
// guard refuses to serve it unless the live ProcessNode projection exactly
// matches. Tenant and companion-pack definitions stay outside the CRM dialect
// proof. Blank or NULL Metadata retains the runtime's zero-step and node-driven
// compatibility representations.
func validateCRMApprovalProcessTopology(ctx context.Context, db *sql.DB) error {
	if db == nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q approval-process topology: database is unavailable", crmDistributionPackName)
	}
	if ctx == nil {
		return fmt.Errorf("cmd/crm-server CRM distribution cannot verify %q approval-process topology: context is unavailable", crmDistributionPackName)
	}
	cursor := ""
	for {
		definitions, err := queryCRMApprovalProcessTopologyDefinitionPage(ctx, db, cursor)
		if err != nil {
			return err
		}
		for index := range definitions {
			definitions[index].nodes, err = queryCRMApprovalProcessTopologyNodes(ctx, db, definitions[index].id)
			if err != nil {
				return err
			}
			if problem := validateCRMApprovalProcessDefinitionTopology(definitions[index]); problem != "" {
				return fmt.Errorf(
					"cmd/crm-server CRM distribution active app pack %q has incomplete approval-process topology; re-apply or resave the named ApprovalProcess through the canonical CRM metadata owner before serving: %s",
					crmDistributionPackName,
					problem,
				)
			}
		}
		if len(definitions) < crmApprovalProcessTopologyDefinitionPageSize {
			return nil
		}
		nextCursor := definitions[len(definitions)-1].id
		if nextCursor <= cursor {
			return fmt.Errorf("cmd/crm-server CRM distribution approval-process topology definition cursor did not advance beyond %q", cursor)
		}
		cursor = nextCursor
	}
}

func queryCRMApprovalProcessTopologyDefinitionPage(ctx context.Context, db *sql.DB, cursor string) ([]crmApprovalProcessTopology, error) {
	rows, err := db.QueryContext(
		ctx,
		crmApprovalProcessTopologyDefinitionPageQuery,
		crmDistributionPackName,
		cursor,
		cursor,
		crmApprovalProcessTopologyDefinitionPageSize,
	)
	if err != nil {
		return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot query approval-process topology definitions: %w", err)
	}
	defer rows.Close()

	definitions := make([]crmApprovalProcessTopology, 0, crmApprovalProcessTopologyDefinitionPageSize)
	for rows.Next() {
		var definitionID string
		var definitionDeveloperName, rawMetadata sql.NullString
		if err := rows.Scan(&definitionID, &definitionDeveloperName, &rawMetadata); err != nil {
			return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot scan approval-process topology definition: %w", err)
		}
		definitionID = strings.TrimSpace(definitionID)
		if definitionID == "" {
			return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot scan approval-process topology definition: active ProcessDefinition has an empty Id")
		}
		if len(definitions) > 0 && definitionID <= definitions[len(definitions)-1].id {
			return nil, fmt.Errorf("cmd/crm-server CRM distribution approval-process topology definitions are not in strictly increasing Id order at %q", definitionID)
		}
		definitions = append(definitions, crmApprovalProcessTopology{
			id:            definitionID,
			developerName: strings.TrimSpace(definitionDeveloperName.String),
			rawMetadata:   rawMetadata.String,
		})
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot iterate approval-process topology definitions: %w", err)
	}
	return definitions, nil
}

func queryCRMApprovalProcessTopologyNodes(ctx context.Context, db *sql.DB, definitionID string) ([]crmApprovalProcessNodeTopology, error) {
	rows, err := db.QueryContext(
		ctx,
		crmApprovalProcessTopologyNodePageQuery,
		definitionID,
		automation.ApprovalProcessNodeReadLimit+1,
	)
	if err != nil {
		return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot query ProcessNode topology for ProcessDefinition %q: %w", definitionID, err)
	}
	defer rows.Close()

	nodes := make([]crmApprovalProcessNodeTopology, 0, automation.ApprovalProcessNodeReadLimit+1)
	for rows.Next() {
		var nodeID, nodeName, nodeDeveloperName sql.NullString
		var nodeStepOrder sql.NullInt64
		if err := rows.Scan(&nodeID, &nodeName, &nodeDeveloperName, &nodeStepOrder); err != nil {
			return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot scan ProcessNode topology for ProcessDefinition %q: %w", definitionID, err)
		}
		node := crmApprovalProcessNodeTopology{
			id:            strings.TrimSpace(nodeID.String),
			name:          strings.TrimSpace(nodeName.String),
			developerName: strings.TrimSpace(nodeDeveloperName.String),
			stepOrder:     nodeStepOrder.Int64,
		}
		if !nodeID.Valid || node.id == "" || !nodeStepOrder.Valid {
			return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot scan approval-process topology: ProcessDefinition %q has an incomplete ProcessNode projection row", definitionID)
		}
		nodes = append(nodes, node)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("cmd/crm-server CRM distribution cannot iterate ProcessNode topology for ProcessDefinition %q: %w", definitionID, err)
	}
	return nodes, nil
}

func validateCRMApprovalProcessDefinitionTopology(definition crmApprovalProcessTopology) string {
	displayName := definition.developerName
	if displayName == "" {
		displayName = definition.id
	}
	if len(definition.nodes) > automation.ApprovalProcessNodeReadLimit {
		return fmt.Sprintf(
			"ApprovalProcess %s (%s) has %d live ProcessNode rows, exceeding the runtime read bound of %d",
			displayName,
			definition.id,
			len(definition.nodes),
			automation.ApprovalProcessNodeReadLimit,
		)
	}
	if strings.TrimSpace(definition.rawMetadata) == "" {
		return ""
	}
	metadata, err := dialectsfcrm.DecodeApprovalProcessMetadata(definition.rawMetadata)
	if err != nil {
		return fmt.Sprintf("ApprovalProcess %s (%s) Metadata cannot be decoded: %v", displayName, definition.id, err)
	}
	if metadata == nil {
		return fmt.Sprintf("ApprovalProcess %s (%s) Metadata decoded to no definition", displayName, definition.id)
	}
	if len(metadata.Steps) > automation.ApprovalProcessNodeReadLimit {
		return fmt.Sprintf("ApprovalProcess %s (%s) has %d authored steps, exceeding the runtime read bound of %d", displayName, definition.id, len(metadata.Steps), automation.ApprovalProcessNodeReadLimit)
	}

	expectedByOrder := make(map[int64]string, len(metadata.Steps))
	expectedNames := make(map[string]int, len(metadata.Steps))
	for index, step := range metadata.Steps {
		name := strings.TrimSpace(step.Name)
		if name == "" {
			return fmt.Sprintf("ApprovalProcess %s (%s) authored step %d has an empty DeveloperName", displayName, definition.id, index+1)
		}
		folded := strings.ToLower(name)
		if previous, duplicate := expectedNames[folded]; duplicate {
			return fmt.Sprintf("ApprovalProcess %s (%s) authored step %d duplicates DeveloperName %q from step %d", displayName, definition.id, index+1, name, previous)
		}
		expectedNames[folded] = index + 1
		expectedByOrder[int64(index+1)] = folded
	}

	if len(definition.nodes) != len(metadata.Steps) {
		return fmt.Sprintf("ApprovalProcess %s (%s) has %d live ProcessNode rows, want %d authored steps", displayName, definition.id, len(definition.nodes), len(metadata.Steps))
	}
	liveByOrder := make(map[int64]string, len(definition.nodes))
	liveNames := make(map[string]int64, len(definition.nodes))
	for _, node := range definition.nodes {
		if node.developerName != "" {
			folded := strings.ToLower(node.developerName)
			if previous, duplicate := liveNames[folded]; duplicate {
				return fmt.Sprintf("ApprovalProcess %s (%s) ProcessNode %s duplicates DeveloperName %q at StepOrder %d", displayName, definition.id, node.id, node.developerName, previous)
			}
			liveNames[folded] = node.stepOrder
		}
		if previous, duplicate := liveByOrder[node.stepOrder]; duplicate {
			return fmt.Sprintf("ApprovalProcess %s (%s) ProcessNode %s duplicates StepOrder %d already owned by %q", displayName, definition.id, node.id, node.stepOrder, previous)
		}

		// Keep the boot proof on the runtime's legacy identity path:
		// DeveloperName, then Name/label, then ordered-position fallback.
		resolvedStep := metadata.StepForNames(node.developerName, node.name)
		if resolvedStep == nil && node.stepOrder > 0 && node.stepOrder <= int64(len(metadata.Steps)) {
			resolvedStep = &metadata.Steps[node.stepOrder-1]
		}
		if node.stepOrder <= 0 || node.stepOrder > int64(len(metadata.Steps)) {
			return fmt.Sprintf("ApprovalProcess %s (%s) ProcessNode %s has StepOrder %d outside the authored step range 1..%d", displayName, definition.id, node.id, node.stepOrder, len(metadata.Steps))
		}
		expectedStep := &metadata.Steps[node.stepOrder-1]
		if resolvedStep != expectedStep {
			resolvedName := ""
			if resolvedStep != nil {
				resolvedName = resolvedStep.Name
			}
			return fmt.Sprintf(
				"ApprovalProcess %s (%s) ProcessNode StepOrder %d has DeveloperName %q and Name %q resolving to authored step %q, want %q",
				displayName,
				definition.id,
				node.stepOrder,
				node.developerName,
				node.name,
				resolvedName,
				expectedStep.Name,
			)
		}
		liveByOrder[node.stepOrder] = expectedByOrder[node.stepOrder]
	}
	for index := range metadata.Steps {
		order := int64(index + 1)
		expectedName := expectedByOrder[order]
		liveName, exists := liveByOrder[order]
		if !exists {
			return fmt.Sprintf("ApprovalProcess %s (%s) is missing ProcessNode StepOrder %d for DeveloperName %q", displayName, definition.id, order, metadata.Steps[index].Name)
		}
		if liveName != expectedName {
			return fmt.Sprintf("ApprovalProcess %s (%s) ProcessNode StepOrder %d has DeveloperName %q, want %q", displayName, definition.id, order, liveName, expectedName)
		}
	}
	return ""
}
