package packs

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"sync"

	"github.com/nexuscrm/automation"
	kernelconnector "github.com/nexuscrm/kernel/pkg/connector"
	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	connectorcontract "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
	"github.com/pingcap-inc/nexuscrm-controlplane/platform"
	"golang.org/x/text/unicode/norm"
)

const (
	PackSourceKindOfficialRepo = "official_repo"
	PackSourceKindLocalPath    = "local_path"
	// PackSourceKindReleaseRepo identifies a pack from an explicitly supplied,
	// revision-addressed repository checkout. Unlike local_path, the resolver
	// reads every byte from the exact registered revision. Unlike
	// official_repo, the stable URI does not imply L1 or product ownership.
	PackSourceKindReleaseRepo = "release_repo"

	// packSourceKindInRepoLegacy is the pre-externalization spelling of the
	// official-repo source kind that older _System_InstalledPack rows carry.
	// ParsePackSourceRef normalizes it to PackSourceKindOfficialRepo so the
	// new binary can boot against a database written by the old binary
	// without a substrate-level data migration. New writes must use
	// PackSourceKindOfficialRepo.
	packSourceKindInRepoLegacy = "in_repo"

	OfficialAppPacksRootEnvVar = "NEXUSCRM_OFFICIAL_APP_PACKS_ROOT"

	// SupportedManifestFormatVersion is the baseline manifest version accepted
	// by this platform build.  New packs should use this version.
	SupportedManifestFormatVersion = "1"
	// ManifestFormatVersion2 is the strict-enforcement version: packs that
	// declare it must use the canonical type:formula + returnType shape for
	// every formula field.  The pre-957 legacy shape (type != formula,
	// formulaMode == "active") is rejected on this version.
	ManifestFormatVersion2 = "2"

	hostPrimitiveKindRenderer = "renderer"
)

// supportedManifestFormatVersions is the complete set of manifest format
// versions this platform build will accept.  Add new versions here when the
// platform is ready to enforce their contracts; remove old versions only after
// all deployed packs have migrated.
var supportedManifestFormatVersions = map[string]bool{
	SupportedManifestFormatVersion: true,
	ManifestFormatVersion2:         true,
}

// PackSourceRef is the parsed source reference supplied to validate/install/
// verify. The supported WT2 kinds are official_repo and local_path.
type PackSourceRef struct {
	Kind string
	URI  string
}

// ResolvedPackSource is the reusable manifest-backed read model produced by
// WT2. Files is a lookup cache keyed by slash-separated, NFC-normalized,
// pack-root-relative paths; consumers that need deterministic order must use
// FileRefs rather than ranging over Files.
type ResolvedPackSource struct {
	Kind          string
	URI           string
	Revision      string
	ManifestBytes []byte
	Manifest      *PackManifest
	Files         map[string][]byte
	FileRefs      []string
}

type PackManifest struct {
	ManifestFormatVersion string                     `json:"manifestFormatVersion"`
	Name                  string                     `json:"name"`
	DisplayName           string                     `json:"displayName"`
	Category              Category                   `json:"category"`
	Version               string                     `json:"version"`
	PlatformCompatibility PlatformCompatibility      `json:"platformCompatibility"`
	Provenance            *PackProvenance            `json:"provenance,omitempty"`
	Dependencies          []PackDependency           `json:"dependencies"`
	HostRequirements      []HostPrimitiveRequirement `json:"hostRequirements,omitempty"`
	Components            []ManifestComponent        `json:"components"`
	// Extensions is an informational namespace for pack-author annotations
	// (build provenance, source-system markers, custom labels). It does NOT
	// participate in the canonical payload hash and does NOT affect apply or
	// verify behavior — it exists so authors can attach metadata without
	// hitting strict-decode rejection on the rest of the manifest. The
	// existing typed `provenance` block is the recommended home for the
	// common provenance fields (sourceSystem/sourceExportId/generatedAt);
	// extensions catches everything else.
	Extensions json.RawMessage `json:"extensions,omitempty"`
}

type PlatformCompatibility struct {
	Min          string `json:"min"`
	MaxExclusive string `json:"maxExclusive"`
}

type PackProvenance struct {
	SourceSystem   string `json:"sourceSystem,omitempty"`
	SourceExportID string `json:"sourceExportId,omitempty"`
	GeneratedAt    string `json:"generatedAt,omitempty"`
}

type PackDependency struct {
	Name              string `json:"name"`
	VersionConstraint string `json:"versionConstraint,omitempty"`
}

type HostPrimitiveRequirement struct {
	Kind    string `json:"kind"`
	Name    string `json:"name"`
	Version string `json:"version,omitempty"`
}

type ManifestComponent struct {
	Kind          ComponentKind        `json:"kind"`
	BusinessKey   string               `json:"businessKey"`
	Manageability Manageability        `json:"manageability"`
	Path          string               `json:"path,omitempty"`
	Payload       json.RawMessage      `json:"payload,omitempty"`
	DependsOn     []ComponentReference `json:"dependsOn,omitempty"`
}

type PackDiagnostic struct {
	Code    string
	Path    string
	Message string
}

type PackValidationReport struct {
	Source      *ResolvedPackSource
	Diagnostics []PackDiagnostic
}

type InstallablePackSourceReport struct {
	Source      *ResolvedPackSource
	Pack        *SourceBackedPack
	Diagnostics []PackDiagnostic
}

func (r PackValidationReport) HasErrors() bool {
	return len(r.Diagnostics) > 0
}

func (r InstallablePackSourceReport) HasErrors() bool {
	return len(r.Diagnostics) > 0
}

func (r PackValidationReport) Error() string {
	if !r.HasErrors() {
		return ""
	}
	lines := make([]string, 0, len(r.Diagnostics))
	for _, d := range r.Diagnostics {
		lines = append(lines, d.String())
	}
	return strings.Join(lines, "\n")
}

func (r InstallablePackSourceReport) Error() string {
	if !r.HasErrors() {
		return ""
	}
	lines := make([]string, 0, len(r.Diagnostics))
	for _, d := range r.Diagnostics {
		lines = append(lines, d.String())
	}
	return strings.Join(lines, "\n")
}

func (d PackDiagnostic) String() string {
	if d.Path == "" {
		return fmt.Sprintf("%s: %s", d.Code, d.Message)
	}
	return fmt.Sprintf("%s %s: %s", d.Code, d.Path, d.Message)
}

type PackSourceOptions struct {
	OfficialRepoRoot            string
	OfficialRepoSourceRevisions map[string]string
	ReleaseRepoSources          map[string]ReleaseRepoSource
	// ReleaseRepoStoredRevisionSources carries exact, operation-local authority
	// for historical revisions observed in one frozen active-runtime snapshot.
	// Fresh candidate resolution never consults this map; it remains bound to
	// ReleaseRepoSources. The map key is minted internally from URI+revision.
	ReleaseRepoStoredRevisionSources     map[string]ReleaseRepoSource
	PlatformVersion                      string
	ComponentKindRegistry                ComponentKindRegistry
	HostPrimitiveRegistry                HostPrimitiveRegistry
	SourceBackedComponentCapabilities    SourceBackedComponentCapabilityRegistry
	ObjectSchemaResolver                 func(string) *models.ObjectMetadata
	FlowActionCallRegistrations          []automation.FlowActionCallRegistration
	FlowActionCallHandlerImplementations []automation.FlowActionCallHandlerImplementation
}

// ReleaseRepoSource is operation-local resolution authority for one stable
// release_repo URI. Root names the pack directory in a Git repository;
// Revision is the exact immutable commit whose bytes must be loaded.
type ReleaseRepoSource struct {
	Root     string
	Revision string
}

type ComponentKindRegistry map[ComponentKind]struct{}

func (r ComponentKindRegistry) Supports(kind ComponentKind) bool {
	_, ok := r[kind]
	return ok
}

type SourceBackedComponentCapability struct {
	Kind            ComponentKind
	Installable     bool
	Manageabilities map[Manageability]struct{}
}

func (c SourceBackedComponentCapability) SupportsManageability(manageability Manageability) bool {
	_, ok := c.Manageabilities[manageability]
	return ok
}

type SourceBackedComponentCapabilityRegistry map[ComponentKind]SourceBackedComponentCapability

func (r SourceBackedComponentCapabilityRegistry) CapabilityFor(kind ComponentKind) (SourceBackedComponentCapability, bool) {
	capability, ok := r[kind]
	return capability, ok
}

type HostPrimitiveRegistry map[string]HostPrimitive

type HostPrimitive struct {
	Kind    string
	Name    string
	Version string
}

var defaultHostPrimitives = struct {
	sync.RWMutex
	registry HostPrimitiveRegistry
}{
	registry: HostPrimitiveRegistry{},
}

var defaultOfficialRepoSourceRevisions = struct {
	sync.RWMutex
	revisions map[string]string
}{
	revisions: map[string]string{},
}

func (r HostPrimitiveRegistry) Supports(req HostPrimitiveRequirement) bool {
	key := hostPrimitiveKey(req.Kind, req.Name)
	primitive, ok := r[key]
	if !ok {
		return false
	}
	if strings.TrimSpace(req.Version) == "" {
		return true
	}
	return primitive.Version == strings.TrimSpace(req.Version)
}

// RegisterDefaultHostPrimitives adds host capabilities supplied by the active
// distribution build. The pack substrate owns the registry mechanism; concrete
// app renderers must be registered by a distribution composition root, not by
// the platform default.
func RegisterDefaultHostPrimitives(primitives ...HostPrimitive) {
	defaultHostPrimitives.Lock()
	defer defaultHostPrimitives.Unlock()
	for _, primitive := range primitives {
		kind := strings.ToLower(strings.TrimSpace(primitive.Kind))
		name := strings.TrimSpace(primitive.Name)
		if kind == "" || name == "" {
			panic("packs: host primitive registrations require non-empty kind and name")
		}
		primitive.Kind = kind
		primitive.Name = name
		primitive.Version = strings.TrimSpace(primitive.Version)
		defaultHostPrimitives.registry[hostPrimitiveKey(kind, name)] = primitive
	}
}

// RegisterDefaultOfficialRepoSourceRevision records the canonical source
// revision for one first-party official_repo pack supplied by the active
// distribution build. The pack substrate owns the mapping mechanism; concrete
// product revisions must be registered by L1 distribution composition.
func RegisterDefaultOfficialRepoSourceRevision(name, revision string) {
	name = strings.TrimSpace(name)
	revision = strings.TrimSpace(revision)
	if name == "" || revision == "" {
		panic("packs: official_repo source revision registrations require non-empty name and revision")
	}
	key := strings.ToLower(name)
	defaultOfficialRepoSourceRevisions.Lock()
	defer defaultOfficialRepoSourceRevisions.Unlock()
	if existing, ok := defaultOfficialRepoSourceRevisions.revisions[key]; ok && existing != revision {
		panic(fmt.Sprintf("packs: official_repo source revision for %q registered as both %q and %q", name, existing, revision))
	}
	defaultOfficialRepoSourceRevisions.revisions[key] = revision
}

func DefaultPackSourceOptions() PackSourceOptions {
	return PackSourceOptions{
		OfficialRepoRoot:                     defaultOfficialRepoRoot(),
		OfficialRepoSourceRevisions:          DefaultOfficialRepoSourceRevisions(),
		PlatformVersion:                      platform.Version,
		ComponentKindRegistry:                DefaultSourceBackedComponentKindRegistry(),
		HostPrimitiveRegistry:                DefaultHostPrimitiveRegistry(),
		SourceBackedComponentCapabilities:    DefaultSourceBackedComponentCapabilityRegistry(),
		FlowActionCallRegistrations:          automation.DefaultAppFlowActionCallRegistrations(),
		FlowActionCallHandlerImplementations: automation.DefaultAppFlowActionCallHandlerImplementations(),
	}
}

func DefaultOfficialRepoSourceRevisions() map[string]string {
	defaultOfficialRepoSourceRevisions.RLock()
	defer defaultOfficialRepoSourceRevisions.RUnlock()
	revisions := make(map[string]string, len(defaultOfficialRepoSourceRevisions.revisions))
	for name, revision := range defaultOfficialRepoSourceRevisions.revisions {
		revisions[name] = revision
	}
	return revisions
}

func DefaultHostPrimitiveRegistry() HostPrimitiveRegistry {
	defaultHostPrimitives.RLock()
	defer defaultHostPrimitives.RUnlock()
	registry := make(HostPrimitiveRegistry, len(defaultHostPrimitives.registry))
	for key, primitive := range defaultHostPrimitives.registry {
		registry[key] = primitive
	}
	return registry
}

// DefaultSourceBackedComponentKindRegistry lists component kinds that WT2 can
// structurally validate as source-backed generic components. It intentionally
// does not mean every ComponentKind constant is installable from a source pack.
func DefaultSourceBackedComponentKindRegistry() ComponentKindRegistry {
	return ComponentKindRegistry{
		ComponentKindObjectSpec:                    {},
		ComponentKindObjectRecordMutationPolicy:    {},
		ComponentKindObjectLifecyclePolicy:         {},
		ComponentKindRecordType:                    {},
		ComponentKindRecordTypePicklistValue:       {},
		ComponentKindListFields:                    {},
		ComponentKindValidationRule:                {},
		ComponentKindCustomField:                   {},
		ComponentKindPermission:                    {},
		ComponentKindSystemPermissionGrants:        {},
		ComponentKindOAuthCustomScope:              {},
		ComponentKindConnectedApp:                  {},
		ComponentKindUIAction:                      {},
		ComponentKindInvocableAction:               {},
		ComponentKindFlowActionCallHandler:         {},
		ComponentKindPageLayout:                    {},
		ComponentKindLayoutAssignment:              {},
		ComponentKindRelatedListRule:               {},
		ComponentKindReportType:                    {},
		ComponentKindStandardFlow:                  {},
		ComponentKindApprovalProcess:               {},
		ComponentKindEmailTemplate:                 {},
		ComponentKindFieldMapping:                  {},
		ComponentKindFormulaFunctionBundle:         {},
		ComponentKindAutoNumberFormat:              {},
		ComponentKindAutoNumberCounter:             {},
		ComponentKindChildCloneRules:               {},
		ComponentKindFolderType:                    {},
		ComponentKindSearchScopeOrder:              {},
		ComponentKindSearchAllDefaultFields:        {},
		ComponentKindHrefRewriteFields:             {},
		ComponentKindWebhookSecret:                 {},
		ComponentKindConnectorCapability:           {},
		ComponentKindConnectorRateLimit:            {},
		ComponentKindConnectorInboundHandlerConfig: {},
		ComponentKindConnectorInboundRoute:         {},
		ComponentKindConnectorCalloutProfile:       {},
		ComponentKindConnectorFieldMapping:         {},
		ComponentKindConnectorPushContract:         {},
	}
}

// allowedComponentKindsByCategory is the per-Category allowlist for ComponentKinds.
// ConnectorPack: connector-pack-specific identity/mechanism kinds.
// AppPack: all installable kinds EXCEPT connector-pack-specific kinds.
// connector_field_mapping and connector_push_contract intentionally remain
// AppPack-installable: they are companion app-pack data-flow metadata whose
// Source/ConnectorOwnerKey is bound to the executing Flow's OwnerPack, not
// ConnectorPack identity metadata.
// Widening ConnectorPack requires an explicit RFC per NCRM-INT-04A; adding a new
// connector-pack-specific kind requires extending both connectorPackOnlyKinds()
// (so it's allowed in connector-pack manifests) and appPackInstallableComponentKinds
// (so it stays excluded from app-pack manifests).
var allowedComponentKindsByCategory = map[Category]map[ComponentKind]struct{}{
	AppPack:       appPackInstallableComponentKinds(),
	ConnectorPack: connectorPackOnlyKinds(),
}

var sourceBackedComponentKindsNotYetInstallable = map[ComponentKind]struct{}{}

// connectorPackOnlyKinds returns the ComponentKinds that are valid in
// connector-pack manifests. Currently:
//   - connector_capability (NCRM-INT-04A): binding declarations consumed at
//     dispatch by IsCapabilityDeclaredByConnectorPack.
//   - webhook_secret (NCRM-INT-03 / PR #1499): pack-owned webhook signing
//     secrets consumed at runtime by features/webhooks/webhook_secret_verifier.
//   - connector_rate_limit (NCRM-INT-04B): pack-owned per-(OwnerPack,
//     EndpointClass) outbound rate-limit policy consumed at runtime by
//     features/connector/throttle's SQL LimitProvider.
//   - connector_inbound_handler_config (NCRM-INT-11c): pack-owned signature
//     verifier / envelope parser config consumed by the connector inbound
//     handler registrar.
//   - connector_inbound_route (NCRM-INT-11b / #1606): per-EventKind flow routing
//     declarations consumed at runtime by features/connector/inboundrouting's
//     DBRouteResolver.
//   - connector_callout_profile (NCRM-INT-10): pack-owned outbound-callout
//     profile consumed at runtime by features/connector/callout.LoadProfile.
//
// Each entry here is enforced symmetrically in source_backed_pack.go's
// buildComponents() Category gate — the two sites must stay in sync.
func connectorPackOnlyKinds() map[ComponentKind]struct{} {
	return map[ComponentKind]struct{}{
		ComponentKindConnectorCapability:           {},
		ComponentKindWebhookSecret:                 {},
		ComponentKindConnectorRateLimit:            {},
		ComponentKindConnectorInboundHandlerConfig: {},
		ComponentKindConnectorInboundRoute:         {},
		ComponentKindConnectorCalloutProfile:       {},
	}
}

// appPackInstallableComponentKinds returns installable kinds for AppPack,
// excluding any ComponentKind in connectorPackOnlyKinds().
func appPackInstallableComponentKinds() map[ComponentKind]struct{} {
	all := DefaultSourceBackedComponentKindRegistry()
	excluded := connectorPackOnlyKinds()
	out := make(map[ComponentKind]struct{}, len(all))
	for k := range all {
		if _, isConnectorOnly := excluded[k]; isConnectorOnly {
			continue
		}
		if _, notYetInstallable := sourceBackedComponentKindsNotYetInstallable[k]; notYetInstallable {
			continue
		}
		out[k] = struct{}{}
	}
	return out
}

// IsAppPackInstallableComponentKind reports the compiler's current generic
// support for an AppPackage component kind. Release packaging uses this owner
// fact to prevent an image from advertising component support that its
// compiler cannot install.
func IsAppPackInstallableComponentKind(kind ComponentKind) bool {
	_, ok := appPackInstallableComponentKinds()[kind]
	return ok
}

// AppPackInstallableComponentKinds returns the compiler-owned AppPackage
// component capability set in canonical order. Release publishers must derive
// their advertised host compatibility from this fact rather than maintaining
// a second allowlist.
func AppPackInstallableComponentKinds() []ComponentKind {
	set := appPackInstallableComponentKinds()
	out := make([]ComponentKind, 0, len(set))
	for kind := range set {
		out = append(out, kind)
	}
	sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
	return out
}

func DefaultSourceBackedComponentCapabilityRegistry() SourceBackedComponentCapabilityRegistry {
	contracts := DefaultComponentKindContracts()
	supported := DefaultSourceBackedComponentKindRegistry()
	registry := SourceBackedComponentCapabilityRegistry{}
	for _, kind := range sortedContractKinds(contracts) {
		if !supported.Supports(kind) {
			continue
		}
		contract := contracts[kind]
		_, notYetInstallable := sourceBackedComponentKindsNotYetInstallable[kind]
		registry[kind] = SourceBackedComponentCapability{
			Kind:            kind,
			Installable:     !notYetInstallable,
			Manageabilities: copyManageabilitySet(contract.Manageabilities),
		}
	}
	return registry
}

func copyManageabilitySet(values map[Manageability]struct{}) map[Manageability]struct{} {
	out := make(map[Manageability]struct{}, len(values))
	for value := range values {
		out[value] = struct{}{}
	}
	return out
}

func ValidatePackSource(raw string) PackValidationReport {
	return ValidatePackSourceWithOptions(raw, DefaultPackSourceOptions())
}

func ValidatePackSourceWithOptions(raw string, opts PackSourceOptions) PackValidationReport {
	resolved, diagnostics := ResolvePackSourceWithOptions(raw, opts)
	if resolved != nil {
		diagnostics = append(diagnostics, validateResolvedSource(resolved, opts)...)
	}
	sortDiagnostics(diagnostics)
	return PackValidationReport{Source: resolved, Diagnostics: diagnostics}
}

func ValidatePackSourceWithComponentKindRegistrationSet(raw string, opts PackSourceOptions, registrations ComponentKindRegistrationSet) PackValidationReport {
	return ValidatePackSourceWithOptions(raw, registrations.withManagedValidationInputs(opts))
}

func ValidateInstallablePackSource(raw string) InstallablePackSourceReport {
	return ValidateInstallablePackSourceWithOptions(raw, DefaultPackSourceOptions())
}

func ValidateInstallablePackSourceWithOptions(raw string, opts PackSourceOptions) InstallablePackSourceReport {
	return validateInstallablePackSourceWithCompat(raw, opts, false, false)
}

// ValidateInstallablePackSourceWithComponentKindRegistrationSet validates and
// constructs a fresh pack through one immutable distribution-owned set.
func ValidateInstallablePackSourceWithComponentKindRegistrationSet(raw string, opts PackSourceOptions, registrations ComponentKindRegistrationSet) InstallablePackSourceReport {
	validationOpts := registrations.withManagedValidationInputs(opts)
	sourceReport := ValidatePackSourceWithOptions(raw, validationOpts)
	return buildInstallablePackSourceReportFromResolvedSourceWithFormulaFunctionBundlesAndRegistrationSet(
		sourceReport.Source, sourceReport.Diagnostics, opts, false, false, nil, false, &registrations,
	)
}

func ValidateInstallablePackSourceWithRuntimeFormulaFunctionBundlesAndComponentKindRegistrationSet(raw string, opts PackSourceOptions, formulaFunctionBundles []formula.FunctionBundle, registrations ComponentKindRegistrationSet) InstallablePackSourceReport {
	validationOpts := registrations.withManagedValidationInputs(opts)
	sourceReport := ValidatePackSourceWithOptions(raw, validationOpts)
	sourceBundles := RuntimeFormulaFunctionBundlesExcludingResolvedSource(sourceReport.Source, formulaFunctionBundles)
	return buildInstallablePackSourceReportFromResolvedSourceWithFormulaFunctionBundlesAndRegistrationSet(
		sourceReport.Source, sourceReport.Diagnostics, opts, false, false,
		formula.CloneFunctionBundles(formulaFunctionBundles), true, &registrations,
		WithRuntimeFormulaFunctionBundlesForValidation(sourceBundles),
	)
}

func ValidateInstallablePackSourceWithRuntimeFormulaFunctionBundles(raw string, opts PackSourceOptions, formulaFunctionBundles []formula.FunctionBundle) InstallablePackSourceReport {
	sourceReport := ValidatePackSourceWithOptions(raw, opts)
	sourcePackFormulaFunctionBundles := RuntimeFormulaFunctionBundlesExcludingResolvedSource(sourceReport.Source, formulaFunctionBundles)
	return buildInstallablePackSourceReportFromResolvedSourceWithFormulaFunctionBundles(
		sourceReport.Source,
		sourceReport.Diagnostics,
		opts,
		false,
		false,
		formula.CloneFunctionBundles(formulaFunctionBundles),
		true,
		WithRuntimeFormulaFunctionBundlesForValidation(sourcePackFormulaFunctionBundles),
	)
}

// RuntimeFormulaFunctionBundlesExcludingResolvedSource removes the candidate
// source's own bundle copies from a full install-context bundle set. The
// SourceBackedPack constructor appends those source-local bundles itself.
func RuntimeFormulaFunctionBundlesExcludingResolvedSource(source *ResolvedPackSource, bundles []formula.FunctionBundle) []formula.FunctionBundle {
	sourceBundles, err := RuntimeFormulaFunctionBundlesFromResolvedSource(source)
	if err != nil || len(sourceBundles) == 0 {
		return formula.CloneFunctionBundles(bundles)
	}
	return formulaFunctionBundlesExcludingSourceFunctions(bundles, sourceBundles)
}

type installedPackFormulaCompat struct {
	allowLegacyActiveScalarFormula       bool
	allowLegacyAggregateFormulaAuthoring bool
}

func installedPackFormulaCompatForSource(source *ResolvedPackSource, allowStoredManifestV1Compat bool) installedPackFormulaCompat {
	allowLegacyManifestV1FormulaCompat := false
	if allowStoredManifestV1Compat &&
		source != nil &&
		source.Manifest != nil &&
		strings.TrimSpace(source.Manifest.ManifestFormatVersion) == SupportedManifestFormatVersion {
		allowLegacyManifestV1FormulaCompat = true
	}
	return installedPackFormulaCompat{
		allowLegacyActiveScalarFormula:       allowLegacyManifestV1FormulaCompat,
		allowLegacyAggregateFormulaAuthoring: allowLegacyManifestV1FormulaCompat,
	}
}

// validateInstallablePackSourceWithCompat is the single shared
// implementation for source-backed installability validation.
//
// Contract:
//   - public validate/install/reapply fail closed on the pre-957 active-scalar
//     shape for all manifest versions.
//   - installed-row recovery/readiness may pass
//     allowManifestV1LegacyActiveScalarFormula=true to keep reading already-
//     installed manifest-v1 legacy sources during the transition.
//   - when false, all manifest versions reject the legacy shape.
//   - isInstalledRow must be true only when called from installed-row
//     re-resolution (verify/reconcile/startup). It gates forward-authoring
//     rules that must not strand already-installed packs; it is orthogonal to
//     the manifest-v1 formula-compat flag.
func validateInstallablePackSourceWithCompat(raw string, opts PackSourceOptions, allowManifestV1LegacyActiveScalarFormula bool, isInstalledRow bool) InstallablePackSourceReport {
	sourceReport := ValidatePackSourceWithOptions(raw, opts)
	return buildInstallablePackSourceReportFromResolvedSource(
		sourceReport.Source,
		sourceReport.Diagnostics,
		opts,
		allowManifestV1LegacyActiveScalarFormula,
		isInstalledRow,
	)
}

func buildInstallablePackSourceReportFromResolvedSource(source *ResolvedPackSource, diagnostics []PackDiagnostic, opts PackSourceOptions, allowManifestV1LegacyActiveScalarFormula bool, isInstalledRow bool, sourcePackOpts ...SourceBackedPackOption) InstallablePackSourceReport {
	return buildInstallablePackSourceReportFromResolvedSourceWithFormulaFunctionBundles(source, diagnostics, opts, allowManifestV1LegacyActiveScalarFormula, isInstalledRow, nil, false, sourcePackOpts...)
}

func buildInstallablePackSourceReportFromResolvedSourceWithFormulaFunctionBundles(source *ResolvedPackSource, diagnostics []PackDiagnostic, opts PackSourceOptions, allowManifestV1LegacyActiveScalarFormula bool, isInstalledRow bool, formulaFunctionBundlesForValidation []formula.FunctionBundle, hasFormulaFunctionBundlesForValidation bool, sourcePackOpts ...SourceBackedPackOption) InstallablePackSourceReport {
	return buildInstallablePackSourceReportFromResolvedSourceWithFormulaFunctionBundlesAndRegistrationSet(
		source, diagnostics, opts, allowManifestV1LegacyActiveScalarFormula, isInstalledRow,
		formulaFunctionBundlesForValidation, hasFormulaFunctionBundlesForValidation, nil, sourcePackOpts...,
	)
}

func buildInstallablePackSourceReportFromResolvedSourceWithFormulaFunctionBundlesAndRegistrationSet(source *ResolvedPackSource, diagnostics []PackDiagnostic, opts PackSourceOptions, allowManifestV1LegacyActiveScalarFormula bool, isInstalledRow bool, formulaFunctionBundlesForValidation []formula.FunctionBundle, hasFormulaFunctionBundlesForValidation bool, registrations *ComponentKindRegistrationSet, sourcePackOpts ...SourceBackedPackOption) InstallablePackSourceReport {
	report := InstallablePackSourceReport{
		Source:      source,
		Diagnostics: append([]PackDiagnostic(nil), diagnostics...),
	}

	if report.HasErrors() || source == nil || source.Manifest == nil {
		sortDiagnostics(report.Diagnostics)
		return report
	}

	formulaCompat := installedPackFormulaCompatForSource(source, allowManifestV1LegacyActiveScalarFormula)
	validationFormulaFunctionBundles := formula.CloneFunctionBundles(formulaFunctionBundlesForValidation)
	if !hasFormulaFunctionBundlesForValidation {
		if sourceBundles, err := RuntimeFormulaFunctionBundlesFromResolvedSource(source); err == nil {
			validationFormulaFunctionBundles = sourceBundles
		}
	}
	flowActionCallRegistrations, err := flowActionCallRegistrationsFromSource(source, opts.FlowActionCallHandlerImplementations)
	if err != nil {
		report.Diagnostics = append(report.Diagnostics, diag("component.payload.invalid_schema", "components", err.Error()))
		sortDiagnostics(report.Diagnostics)
		return report
	}
	validationOpts := opts
	if registrations != nil {
		validationOpts = registrations.withManagedValidationInputs(opts)
	}
	if len(flowActionCallRegistrations) > 0 {
		merged, err := mergeFlowActionCallRegistrationsByDescriptor(opts.FlowActionCallRegistrations, flowActionCallRegistrations)
		if err != nil {
			report.Diagnostics = append(report.Diagnostics, diag("component.payload.invalid_schema", "components", err.Error()))
			sortDiagnostics(report.Diagnostics)
			return report
		}
		validationOpts.FlowActionCallRegistrations = merged
	}
	report.Diagnostics = append(report.Diagnostics, validateInstallableSourceBackedPlan(
		source,
		validationOpts,
		formulaCompat.allowLegacyActiveScalarFormula,
		formulaCompat.allowLegacyAggregateFormulaAuthoring,
		isInstalledRow,
		validationFormulaFunctionBundles,
		registrations,
	)...)
	sortDiagnostics(report.Diagnostics)
	if report.HasErrors() {
		return report
	}

	packOpts := append([]SourceBackedPackOption(nil), sourcePackOpts...)
	if registrations != nil {
		packOpts = append(packOpts, withComponentKindRegistrationSet(*registrations, nil))
	}
	if len(opts.FlowActionCallRegistrations) > 0 {
		packOpts = append(packOpts, withFlowActionCallRegistrations(opts.FlowActionCallRegistrations))
	}
	if len(opts.FlowActionCallHandlerImplementations) > 0 {
		packOpts = append(packOpts, withFlowActionCallHandlerImplementations(opts.FlowActionCallHandlerImplementations))
	}
	packOpts = append(packOpts, withConnectorPushValidationSchemaResolver(connectorPushValidationSchemaResolverForSource(source, opts, isInstalledRow)))
	pack, err := NewSourceBackedPack(source, packOpts...)
	if err != nil {
		report.Diagnostics = append(report.Diagnostics, diag("component.plan.invalid", "components", err.Error()))
		sortDiagnostics(report.Diagnostics)
		return report
	}
	if err := VerifySourceBackedPackWriteMaterializedFormulaPayload(pack); err != nil {
		report.Diagnostics = append(report.Diagnostics, diag("component.verify.invalid", "components", err.Error()))
		sortDiagnostics(report.Diagnostics)
		return report
	}
	report.Pack = pack
	return report
}

func validateResolvedSource(source *ResolvedPackSource, opts PackSourceOptions) []PackDiagnostic {
	if source.Manifest == nil {
		return nil
	}
	var diagnostics []PackDiagnostic
	m := source.Manifest
	if strings.TrimSpace(m.ManifestFormatVersion) == "" {
		diagnostics = append(diagnostics, diag("manifest.format.missing", "manifestFormatVersion", "manifest format version is required"))
	} else if !supportedManifestFormatVersions[m.ManifestFormatVersion] {
		diagnostics = append(diagnostics, diag("manifest.format.unsupported", "manifestFormatVersion", fmt.Sprintf("unsupported manifest format version %q", m.ManifestFormatVersion)))
	}
	if strings.TrimSpace(m.Name) == "" {
		diagnostics = append(diagnostics, diag("manifest.name.missing", "name", "pack name is required"))
	} else if isNamedPackSourceKind(source.Kind) && strings.TrimSpace(m.Name) != source.URI {
		diagnostics = append(diagnostics, diag("source.name_mismatch", "name", fmt.Sprintf("%s source %q resolved manifest name %q", source.Kind, source.URI, m.Name)))
	}
	if strings.TrimSpace(m.DisplayName) == "" {
		diagnostics = append(diagnostics, diag("manifest.display_name.missing", "displayName", "display name is required"))
	}
	if m.Category == "" {
		diagnostics = append(diagnostics, diag("manifest.category.missing", "category", "category is required"))
	} else if m.Category != AppPack && m.Category != ConnectorPack {
		diagnostics = append(diagnostics, diag("manifest.category.unsupported", "category", fmt.Sprintf("source-backed WT2 validation supports categories %q and %q, got %q", AppPack, ConnectorPack, m.Category)))
	}
	if strings.TrimSpace(m.Version) == "" {
		diagnostics = append(diagnostics, diag("manifest.version.missing", "version", "pack version is required"))
	} else if !isValidSemVer(m.Version) {
		diagnostics = append(diagnostics, diag("manifest.version.invalid", "version", fmt.Sprintf("invalid SemVer %q", m.Version)))
	}

	diagnostics = append(diagnostics, validatePlatformCompatibility(m.PlatformCompatibility, opts.PlatformVersion)...)
	diagnostics = append(diagnostics, validateManifestDependencies(m.Dependencies)...)
	diagnostics = append(diagnostics, validateHostRequirements(m.HostRequirements, opts.HostPrimitiveRegistry)...)
	diagnostics = append(diagnostics, validateReferencedHostRequirements(source, m.HostRequirements)...)
	diagnostics = append(diagnostics, validateManifestComponents(m.Components, opts.ComponentKindRegistry)...)
	return diagnostics
}

func validatePlatformCompatibility(compat PlatformCompatibility, currentVersion string) []PackDiagnostic {
	var diagnostics []PackDiagnostic
	if strings.TrimSpace(compat.Min) == "" {
		diagnostics = append(diagnostics, diag("compat.min.missing", "platformCompatibility.min", "minimum platform version is required"))
	}
	if strings.TrimSpace(compat.MaxExclusive) == "" {
		diagnostics = append(diagnostics, diag("compat.max_exclusive.missing", "platformCompatibility.maxExclusive", "exclusive maximum platform version is required"))
	}
	if !isValidSemVer(currentVersion) {
		diagnostics = append(diagnostics, diag("compat.platform.invalid", "platformCompatibility", fmt.Sprintf("current platform version %q is not valid SemVer", currentVersion)))
		return diagnostics
	}
	if strings.TrimSpace(compat.Min) != "" && !isValidSemVer(compat.Min) {
		diagnostics = append(diagnostics, diag("compat.min.invalid", "platformCompatibility.min", fmt.Sprintf("invalid SemVer %q", compat.Min)))
	}
	if strings.TrimSpace(compat.MaxExclusive) != "" && !isValidSemVer(compat.MaxExclusive) {
		diagnostics = append(diagnostics, diag("compat.max_exclusive.invalid", "platformCompatibility.maxExclusive", fmt.Sprintf("invalid SemVer %q", compat.MaxExclusive)))
	}
	if isValidSemVer(compat.Min) && compareSemVer(currentVersion, compat.Min) < 0 {
		diagnostics = append(diagnostics, diag("compat.platform.too_old", "platformCompatibility", fmt.Sprintf("platform %s is below min %s", currentVersion, compat.Min)))
	}
	if isValidSemVer(compat.MaxExclusive) && compareSemVer(currentVersion, compat.MaxExclusive) >= 0 {
		diagnostics = append(diagnostics, diag("compat.platform.too_new", "platformCompatibility", fmt.Sprintf("platform %s is greater than or equal to maxExclusive %s", currentVersion, compat.MaxExclusive)))
	}
	return diagnostics
}

func validateManifestDependencies(dependencies []PackDependency) []PackDiagnostic {
	var diagnostics []PackDiagnostic
	seen := map[string]int{}
	for i, dep := range dependencies {
		p := fmt.Sprintf("dependencies[%d]", i)
		name := strings.TrimSpace(dep.Name)
		if name == "" {
			diagnostics = append(diagnostics, diag("dependency.name.missing", p+".name", "dependency name is required"))
		} else if prev, ok := seen[name]; ok {
			diagnostics = append(diagnostics, diag("dependency.name.duplicate", p+".name", fmt.Sprintf("dependency %q duplicates dependencies[%d]", name, prev)))
		} else {
			seen[name] = i
		}
		constraint := strings.TrimSpace(dep.VersionConstraint)
		if constraint != "" && !isSupportedVersionConstraint(constraint) {
			diagnostics = append(diagnostics, diag("dependency.version_constraint.invalid", p+".versionConstraint", fmt.Sprintf("unsupported version constraint %q", dep.VersionConstraint)))
		}
	}
	return diagnostics
}

func validateHostRequirements(requirements []HostPrimitiveRequirement, registry HostPrimitiveRegistry) []PackDiagnostic {
	var diagnostics []PackDiagnostic
	for i, req := range requirements {
		p := fmt.Sprintf("hostRequirements[%d]", i)
		if strings.TrimSpace(req.Kind) == "" {
			diagnostics = append(diagnostics, diag("host.kind.missing", p+".kind", "host primitive kind is required"))
		}
		if strings.TrimSpace(req.Name) == "" {
			diagnostics = append(diagnostics, diag("host.name.missing", p+".name", "host primitive name is required"))
		}
		if strings.TrimSpace(req.Kind) != "" && strings.TrimSpace(req.Name) != "" && !registry.Supports(req) {
			diagnostics = append(diagnostics, diag("host.unsupported", p, fmt.Sprintf("host primitive %s/%s is not provided by this platform", req.Kind, req.Name)))
		}
	}
	return diagnostics
}

func validateReferencedHostRequirements(source *ResolvedPackSource, requirements []HostPrimitiveRequirement) []PackDiagnostic {
	if source == nil || source.Manifest == nil {
		return nil
	}
	declared := manifestHostRequirementSet(requirements)
	references := map[string]string{}
	for _, component := range source.Manifest.Components {
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			continue
		}
		var decoded any
		if err := json.Unmarshal(payload, &decoded); err != nil {
			continue
		}
		collectRendererHostReferences(decoded, func(rendererID string) {
			rendererID = strings.TrimSpace(rendererID)
			if rendererID == "" {
				return
			}
			if _, ok := references[rendererID]; !ok {
				references[rendererID] = componentPayloadLabel(component)
			}
		})
	}
	if len(references) == 0 {
		return nil
	}
	var rendererIDs []string
	for rendererID := range references {
		rendererIDs = append(rendererIDs, rendererID)
	}
	sort.Strings(rendererIDs)
	var diagnostics []PackDiagnostic
	for _, rendererID := range rendererIDs {
		if _, ok := declared[hostPrimitiveKey(hostPrimitiveKindRenderer, rendererID)]; ok {
			continue
		}
		diagnostics = append(diagnostics, diag(
			"host.renderer_id.undeclared",
			references[rendererID],
			fmt.Sprintf("rendererId %q is referenced by pack payload but not declared in hostRequirements", rendererID),
		))
	}
	return diagnostics
}

func manifestHostRequirementSet(requirements []HostPrimitiveRequirement) map[string]struct{} {
	out := make(map[string]struct{}, len(requirements))
	for _, req := range requirements {
		kind := strings.TrimSpace(req.Kind)
		name := strings.TrimSpace(req.Name)
		if kind == "" || name == "" {
			continue
		}
		out[hostPrimitiveKey(kind, name)] = struct{}{}
	}
	return out
}

func collectRendererHostReferences(value any, emit func(string)) {
	switch typed := value.(type) {
	case map[string]any:
		for key, child := range typed {
			if key == "rendererId" {
				if rendererID, ok := child.(string); ok {
					emit(rendererID)
				}
			}
			collectRendererHostReferences(child, emit)
		}
	case []any:
		for _, child := range typed {
			collectRendererHostReferences(child, emit)
		}
	}
}

func validateManifestComponents(components []ManifestComponent, registry ComponentKindRegistry) []PackDiagnostic {
	var diagnostics []PackDiagnostic
	if len(components) == 0 {
		diagnostics = append(diagnostics, diag("component.missing", "components", "at least one component is required"))
	}
	identityIndex := map[string]int{}
	for i, component := range components {
		p := fmt.Sprintf("components[%d]", i)
		if component.Kind == "" {
			diagnostics = append(diagnostics, diag("component.kind.missing", p+".kind", "component kind is required"))
		} else if !registry.Supports(component.Kind) {
			diagnostics = append(diagnostics, diag("component.kind.unsupported", p+".kind", fmt.Sprintf("component kind %q is not source-backed for this platform version", component.Kind)))
		}
		if strings.TrimSpace(component.BusinessKey) == "" {
			diagnostics = append(diagnostics, diag("component.business_key.missing", p+".businessKey", "component business key is required"))
		}
		if component.Manageability == "" {
			diagnostics = append(diagnostics, diag("component.manageability.missing", p+".manageability", "component manageability is required"))
		} else if !isValidManageability(component.Manageability) {
			diagnostics = append(diagnostics, diag("component.manageability.unsupported", p+".manageability", fmt.Sprintf("unsupported manageability %q", component.Manageability)))
		}
		if strings.TrimSpace(component.Path) == "" && len(component.Payload) == 0 {
			diagnostics = append(diagnostics, diag("component.payload.missing", p, "component must declare path or inline payload"))
		}
		if strings.TrimSpace(component.Path) != "" && len(component.Payload) > 0 {
			diagnostics = append(diagnostics, diag("component.payload.ambiguous", p, "component must not declare both path and inline payload"))
		}
		if len(component.Payload) > 0 {
			if err := rejectDuplicateJSONKeys(component.Payload, p+".payload"); err != nil {
				diagnostics = append(diagnostics, diag("component.payload.duplicate_key", p+".payload", err.Error()))
			}
		}
		if component.Kind != "" && strings.TrimSpace(component.BusinessKey) != "" {
			key := componentIdentityKey(component.Kind, component.BusinessKey)
			if prev, ok := identityIndex[key]; ok {
				diagnostics = append(diagnostics, diag("component.identity.duplicate", p, fmt.Sprintf("component identity (%s, %s) duplicates components[%d]", component.Kind, strings.TrimSpace(component.BusinessKey), prev)))
			} else {
				identityIndex[key] = i
			}
		}
		for j, dep := range component.DependsOn {
			depPath := fmt.Sprintf("%s.dependsOn[%d]", p, j)
			if dep.Kind == "" {
				diagnostics = append(diagnostics, diag("component.depends_on.kind.missing", depPath+".kind", "dependsOn kind is required"))
			}
			if strings.TrimSpace(dep.BusinessKey) == "" {
				diagnostics = append(diagnostics, diag("component.depends_on.business_key.missing", depPath+".businessKey", "dependsOn businessKey is required"))
			}
		}
	}

	for i, component := range components {
		if component.Kind == "" || strings.TrimSpace(component.BusinessKey) == "" {
			continue
		}
		for j, dep := range component.DependsOn {
			if dep.Kind == "" || strings.TrimSpace(dep.BusinessKey) == "" {
				continue
			}
			if _, ok := identityIndex[componentIdentityKey(dep.Kind, dep.BusinessKey)]; !ok {
				diagnostics = append(diagnostics, diag("component.depends_on.unresolved", fmt.Sprintf("components[%d].dependsOn[%d]", i, j), fmt.Sprintf("dependency (%s, %s) is not declared in this pack", dep.Kind, strings.TrimSpace(dep.BusinessKey))))
			}
		}
	}
	// child_clone_rules is currently a singleton-per-pack: the apply/verify
	// converge in platform_config.go scopes ownership at the pack level (the
	// full owned-object set is passed into every component), so two
	// child_clone_rules components on the same pack would each erase the
	// other's payload on apply and fail each other on verify. Catch this at
	// manifest-validation time with an operator-actionable diagnostic; the
	// equivalent in-binary fail-safe lives in buildComponents().
	// PR #929 round 3 Codex HIGH.
	firstChildCloneRulesIdx := -1
	for i, component := range components {
		if component.Kind != ComponentKindChildCloneRules {
			continue
		}
		if firstChildCloneRulesIdx < 0 {
			firstChildCloneRulesIdx = i
			continue
		}
		diagnostics = append(diagnostics, diag(
			"component.child_clone_rules.duplicate",
			fmt.Sprintf("components[%d].kind", i),
			fmt.Sprintf("pack declares more than one child_clone_rules component; exactly one is supported (duplicates components[%d])", firstChildCloneRulesIdx),
		))
	}
	firstFolderTypeIdx := -1
	for i, component := range components {
		if component.Kind != ComponentKindFolderType {
			continue
		}
		if firstFolderTypeIdx < 0 {
			firstFolderTypeIdx = i
			continue
		}
		diagnostics = append(diagnostics, diag(
			"component.folder_type.duplicate",
			fmt.Sprintf("components[%d].kind", i),
			fmt.Sprintf("pack declares more than one folder_type component; exactly one catalog component is supported (duplicates components[%d])", firstFolderTypeIdx),
		))
	}
	diagnostics = append(diagnostics, detectComponentDependencyCycles(components)...)
	return diagnostics
}

func validateInstallableSourceBackedPlan(source *ResolvedPackSource, opts PackSourceOptions, allowLegacyInstalledActiveScalarFormula bool, allowLegacyInstalledAggregateFormulaAuthoring bool, allowInstalledRowCompat bool, formulaFunctionBundlesForValidation []formula.FunctionBundle, registrations *ComponentKindRegistrationSet) []PackDiagnostic {
	if source == nil || source.Manifest == nil {
		return nil
	}
	capabilities := opts.SourceBackedComponentCapabilities
	if capabilities == nil {
		capabilities = DefaultSourceBackedComponentCapabilityRegistry()
	}
	dependencySchemas := sourceBackedDependencyObjectSchemasForValidation(source, opts)
	objectSpecValidationResolver := newObjectSpecFieldResolverWithFallback(
		sourceBackedObjectSpecsForValidation(source),
		dependencySchemas,
		opts.ObjectSchemaResolver,
	)
	var diagnostics []PackDiagnostic
	if !allowInstalledRowCompat {
		diagnostics = validateInstallableSourceBackedDependencySources(source, opts)
	}
	connectedAppPublicScopePolicy, connectedAppPublicScopePolicyErr := sourceBackedConnectedAppPublicScopePolicyFromSource(source.Manifest.Name, source, allowInstalledRowCompat)
	for i, component := range source.Manifest.Components {
		p := fmt.Sprintf("components[%d]", i)
		if component.Kind == "" {
			continue
		}
		capability, ok := capabilities.CapabilityFor(component.Kind)
		if !ok {
			if opts.ComponentKindRegistry.Supports(component.Kind) {
				diagnostics = append(diagnostics, diag("component.kind.not_installable", p+".kind", fmt.Sprintf("component kind %q is not installable by this platform version", component.Kind)))
			}
			continue
		}
		if !capability.Installable {
			diagnostics = append(diagnostics, diag("component.kind.not_installable", p+".kind", fmt.Sprintf("component kind %q is not installable by this platform version", component.Kind)))
			continue
		}
		if component.Manageability != "" && isValidManageability(component.Manageability) && !capability.SupportsManageability(component.Manageability) {
			diagnostics = append(diagnostics, diag("component.manageability.unsupported_for_kind", p+".manageability", fmt.Sprintf("component kind %q supports manageability %s only in this platform version, got %s", component.Kind, sortedManageabilityList(capability.Manageabilities), component.Manageability)))
		}
		// Per-Category ComponentKind allowlist: fail closed when a manifest contains
		// a ComponentKind not in its Category's allowlist. ConnectorPack accepts
		// connectorPackOnlyKinds; AppPack accepts everything except those
		// connector-only kinds. New categories or widening requires an RFC.
		if entry, managed := registrations.managedEntry(component.Kind); managed {
			if !managedRegistrationAllowsCategory(entry.registration, source.Manifest.Category) {
				diagnostics = append(diagnostics, diag(
					"manifest.component.unsupported_for_category",
					p+".kind",
					fmt.Sprintf("component kind %q is not allowed in category %q manifests", component.Kind, source.Manifest.Category),
				))
				continue
			}
		} else if allowed, ok := allowedComponentKindsByCategory[source.Manifest.Category]; ok {
			if _, kindAllowed := allowed[component.Kind]; !kindAllowed {
				diagnostics = append(diagnostics, diag(
					"manifest.component.unsupported_for_category",
					p+".kind",
					fmt.Sprintf("component kind %q is not allowed in category %q manifests", component.Kind, source.Manifest.Category),
				))
				continue
			}
		}

		switch component.Kind {
		case ComponentKindObjectSpec:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			specs, err := parseObjectSpecsJSON(payload, componentPayloadLabel(component))
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.invalid_schema", p, err.Error()))
				continue
			}
			if err := validateSourceBackedObjectSpecAccessPolicySidecarSurface(specs, componentPayloadLabel(component)); err != nil {
				diagnostics = append(diagnostics, diag("component.object_spec.access_policy_sidecar_surface", p, err.Error()))
			}
			diagnostics = append(diagnostics, validateObjectSpecAgainstL0Reserved(specs, p)...)
			diagnostics = append(diagnostics, validateObjectSpecFieldShapesWithResolver(specs, objectSpecValidationResolver, allowLegacyInstalledActiveScalarFormula, allowLegacyInstalledAggregateFormulaAuthoring)...)
			if err := validateSpecReadSources(specs); err != nil {
				diagnostics = append(diagnostics, diag("component.object_spec.invalid_read_source", p, err.Error()))
			}
			// Validate LockRule expressions using the same dedicated verifier as
			// apply and verify-pack, so validate-pack, install-pack, and verify-pack
			// all report invalid lock rules consistently (lifecycle contract requires
			// validate-pack diagnostics to match what install-pack and verify-pack would reject).
			resolver := newObjectSpecFieldResolver(specs)
			if err := validateSpecLockRulesWithFunctionBundles(specs, resolver, formulaFunctionBundlesForValidation); err != nil {
				diagnostics = append(diagnostics, diag("component.object_spec.invalid_lock_rule", p, err.Error()))
			}
		case ComponentKindObjectRecordMutationPolicy:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			if _, err := newSourceBackedObjectRecordMutationPolicyComponent(source.Manifest.Name, component, payload, nil); err != nil {
				diagnostics = append(diagnostics, diag("component.object_record_mutation_policy.invalid_payload", p, err.Error()))
			}
		case ComponentKindObjectLifecyclePolicy:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			if _, err := newSourceBackedObjectLifecyclePolicyComponent(source.Manifest.Name, component, payload, nil); err != nil {
				diagnostics = append(diagnostics, diag("component.object_lifecycle_policy.invalid_payload", p, err.Error()))
			}
		case ComponentKindConnectorCapability:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			diagnostics = append(diagnostics, validateConnectorCapabilityComponent(source.Manifest.Name, component, payload, p)...)
		case ComponentKindConnectorInboundRoute:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			diagnostics = append(diagnostics, validateConnectorInboundRouteComponent(source.Manifest.Name, component, payload, p)...)
		case ComponentKindConnectorInboundHandlerConfig:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			diagnostics = append(diagnostics, validateConnectorInboundHandlerConfigComponent(source.Manifest.Name, component, payload, p)...)
		case ComponentKindConnectorFieldMapping:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			if err := validateConnectorFieldMappingBusinessKeySource(source.Manifest.Name, component); err != nil {
				diagnostics = append(diagnostics, diag("connector_field_mapping.business_key.mismatch_pack", p+".businessKey", err.Error()))
				continue
			}
			if _, err := kernelconnector.ParseConnectorFieldMappingInputsFromPayload(payload, source.Manifest.Name); err != nil {
				diagnostics = append(diagnostics, diag("component.payload.invalid_schema", p, err.Error()))
			}
		case ComponentKindConnectorPushContract:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			flowPayloads, err := sourceBackedStandardFlowPayloadsForValidation(source)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			if _, _, err := newSourceBackedConnectorPushContractComponent(source.Manifest.Name, component, payload, flowPayloads); err != nil {
				if strings.Contains(err.Error(), "businessKey") {
					diagnostics = append(diagnostics, diag("connector_push_contract.business_key.mismatch_payload", p+".businessKey", err.Error()))
				} else {
					diagnostics = append(diagnostics, diag("component.payload.invalid_schema", p, err.Error()))
				}
			}
		case ComponentKindFormulaFunctionBundle:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			if _, err := formula.ParseFunctionBundlePayload(payload); err != nil {
				diagnostics = append(diagnostics, diag("component.payload.invalid_schema", p, err.Error()))
			}
		case ComponentKindFlowActionCallHandler:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			packComponent, err := newSourceBackedFlowActionCallHandlerComponent(source.Manifest.Name, component, payload)
			if err == nil {
				err = validateFlowActionCallHandlerRuntimeRegistration(packComponent.projection, opts.FlowActionCallRegistrations...)
			}
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.invalid_schema", p, err.Error()))
			}
		case ComponentKindOAuthCustomScope:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			diagnostics = append(diagnostics, validateOAuthCustomScopeComponentWithCompat(component, payload, p, allowInstalledRowCompat)...)
		case ComponentKindConnectedApp:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			if connectedAppPublicScopePolicyErr != nil {
				diagnostics = append(diagnostics, diag("component.payload.invalid_schema", p, connectedAppPublicScopePolicyErr.Error()))
				continue
			}
			diagnostics = append(diagnostics, validateConnectedAppComponentWithPolicy(source.Manifest.Name, component, payload, p, connectedAppPublicScopePolicy)...)
		case ComponentKindSystemPermissionGrants:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			if _, _, err := newSourceBackedSystemPermissionGrantsComponent(source.Manifest.Name, component, payload); err != nil {
				diagnostics = append(diagnostics, diag("component.payload.invalid_schema", p, err.Error()))
			}
		case ComponentKindFolderType:
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				diagnostics = append(diagnostics, diag("component.payload.unavailable", p, err.Error()))
				continue
			}
			folderTypeCompat := folderTypeAuthoringCompat{}
			if allowInstalledRowCompat {
				folderTypeCompat.allowLegacyMissingCreatePermission = true
			}
			diagnostics = append(diagnostics, validateSourceBackedFolderTypeComponentWithCompat(source.Manifest.Name, component, payload, p, folderTypeCompat)...)
		}
	}
	diagnostics = append(diagnostics, validateInstallableSourceBackedDefaultChildRows(source)...)
	diagnostics = append(diagnostics, validateInstallableSourceBackedChildCloneRules(source)...)
	diagnostics = append(diagnostics, validateInstallableSourceBackedExclusiveSiblingFields(source)...)
	diagnostics = append(diagnostics, validateInstallableSourceBackedListFields(source)...)
	diagnostics = append(diagnostics, validateInstallableSourceBackedChildRelationshipNames(source)...)
	diagnostics = append(diagnostics, validateInstallableComponentPhaseDependencies(source.Manifest.Components, registrations)...)
	diagnostics = append(diagnostics, validateConnectorWebhookCapabilityPair(source, allowInstalledRowCompat)...)
	diagnostics = append(diagnostics, validateConnectorCapabilityHandlerConfigPair(source, allowInstalledRowCompat)...)
	diagnostics = append(diagnostics, validateConnectorInboundHandlerEndpointConfigs(source)...)
	diagnostics = append(diagnostics, validateConnectorCalloutProfileDuplicates(source)...)
	diagnostics = append(diagnostics, validateConnectorCalloutProfileBatchCoverageOverlaps(source)...)
	return diagnostics
}

func sourceBackedStandardFlowPayloadsForValidation(source *ResolvedPackSource) (map[string][]byte, error) {
	out := map[string][]byte{}
	if source == nil || source.Manifest == nil {
		return out, nil
	}
	for _, component := range source.Manifest.Components {
		if component.Kind != ComponentKindStandardFlow {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			return nil, fmt.Errorf("%s %s payload: %w", component.Kind, component.BusinessKey, err)
		}
		out[sourceBackedFlowFileName(component)] = payload
	}
	return out, nil
}

func sourceBackedObjectSpecsForValidation(source *ResolvedPackSource) []ObjectSpec {
	if source == nil || source.Manifest == nil {
		return nil
	}
	var specs []ObjectSpec
	for _, component := range source.Manifest.Components {
		if component.Kind != ComponentKindObjectSpec {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			continue
		}
		parsed, err := parseObjectSpecsJSON(payload, componentPayloadLabel(component))
		if err != nil {
			continue
		}
		specs = append(specs, parsed...)
	}
	return specs
}

func RuntimeFormulaFunctionBundlesFromResolvedSource(source *ResolvedPackSource) ([]formula.FunctionBundle, error) {
	if source == nil || source.Manifest == nil {
		return nil, nil
	}
	var bundles []formula.FunctionBundle
	for _, component := range source.Manifest.Components {
		if component.Kind != ComponentKindFormulaFunctionBundle {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			return nil, fmt.Errorf("%s %s payload: %w", component.Kind, component.BusinessKey, err)
		}
		bundle, err := formula.ParseFunctionBundlePayload(payload)
		if err != nil {
			return nil, fmt.Errorf("%s %s: %w", component.Kind, component.BusinessKey, err)
		}
		bundles = append(bundles, bundle)
	}
	return formula.CloneFunctionBundles(bundles), nil
}

func sourceBackedDependencyObjectSchemasForValidation(source *ResolvedPackSource, opts PackSourceOptions) map[string]*models.ObjectMetadata {
	if source == nil || source.Manifest == nil || len(source.Manifest.Dependencies) == 0 || strings.TrimSpace(opts.OfficialRepoRoot) == "" {
		return nil
	}
	schemas := map[string]*models.ObjectMetadata{}
	visited := map[string]bool{}
	var visit func([]PackDependency)
	visit = func(deps []PackDependency) {
		for _, dep := range deps {
			name := strings.TrimSpace(dep.Name)
			key := strings.ToLower(name)
			if name == "" || visited[key] {
				continue
			}
			visited[key] = true
			depReport := ValidatePackSourceWithOptions(PackSourceKindOfficialRepo+":"+name, opts)
			if depReport.Source == nil || depReport.Source.Manifest == nil {
				continue
			}
			depSpecs := sourceBackedObjectSpecsForValidation(depReport.Source)
			for _, schema := range buildObjectMetadata(depSpecs) {
				if schema == nil || strings.TrimSpace(schema.APIName) == "" {
					continue
				}
				schemas[strings.ToLower(strings.TrimSpace(schema.APIName))] = schema
			}
			for _, customField := range sourceBackedCustomFieldDeclarationsForValidation(depReport.Source, depSpecs) {
				objectAPIName := strings.TrimSpace(customField.ObjectAPIName)
				fieldAPIName := strings.TrimSpace(customField.Field.APIName)
				if objectAPIName == "" || fieldAPIName == "" {
					continue
				}
				key := strings.ToLower(objectAPIName)
				schema := schemas[key]
				if schema == nil {
					schema = &models.ObjectMetadata{APIName: objectAPIName}
					schemas[key] = schema
				}
				if connectorPushValidationSchemaHasField(schema, fieldAPIName) {
					continue
				}
				schema.Fields = append(schema.Fields, models.FieldMetadata{
					APIName:     fieldAPIName,
					Type:        constants.NormalizeFieldType(customField.Field.Type),
					ReferenceTo: append([]string(nil), customField.Field.ReferenceTo...),
				})
			}
			visit(depReport.Source.Manifest.Dependencies)
		}
	}
	visit(source.Manifest.Dependencies)
	if len(schemas) == 0 {
		return nil
	}
	return schemas
}

func validateInstallableSourceBackedDependencySources(source *ResolvedPackSource, opts PackSourceOptions) []PackDiagnostic {
	if source == nil || source.Manifest == nil || len(source.Manifest.Dependencies) == 0 || strings.TrimSpace(opts.OfficialRepoRoot) == "" {
		return nil
	}
	visited := map[string]bool{}
	var diagnostics []PackDiagnostic
	var visit func([]PackDependency, string)
	visit = func(deps []PackDependency, prefix string) {
		for i, dep := range deps {
			name := strings.TrimSpace(dep.Name)
			depPath := fmt.Sprintf("%sdependencies[%d]", prefix, i)
			key := strings.ToLower(name)
			if name == "" || visited[key] {
				continue
			}
			visited[key] = true
			depReport := ValidatePackSourceWithOptions(PackSourceKindOfficialRepo+":"+name, opts)
			for _, diagnostic := range depReport.Diagnostics {
				diagnostics = append(diagnostics, packDependencyDiagnostic(depPath, name, diagnostic))
			}
			if depReport.Source == nil || depReport.Source.Manifest == nil {
				continue
			}
			visit(depReport.Source.Manifest.Dependencies, depPath+".")
		}
	}
	visit(source.Manifest.Dependencies, "")
	return diagnostics
}

func packDependencyDiagnostic(depPath, depName string, diagnostic PackDiagnostic) PackDiagnostic {
	out := diagnostic
	if strings.TrimSpace(out.Path) == "" {
		out.Path = depPath
	} else {
		out.Path = depPath + "." + out.Path
	}
	if depName != "" {
		out.Message = fmt.Sprintf("dependency %q: %s", depName, out.Message)
	}
	return out
}

func sourceBackedCustomFieldDeclarationsForValidation(source *ResolvedPackSource, specs []ObjectSpec) []declaredCustomFieldExtension {
	if source == nil || source.Manifest == nil {
		return nil
	}
	var fields []declaredCustomFieldExtension
	for _, component := range source.Manifest.Components {
		if component.Kind != ComponentKindCustomField {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			continue
		}
		var ref sourceBackedCustomFieldPayload
		if err := json.Unmarshal(payload, &ref); err != nil {
			continue
		}
		declared, err := resolveDeclaredCustomFieldExtension(specs, ref.ObjectAPIName, ref.FieldAPIName)
		if err != nil {
			continue
		}
		fields = append(fields, declared)
	}
	return fields
}

func connectorPushValidationSchemaHasField(schema *models.ObjectMetadata, fieldAPIName string) bool {
	if schema == nil {
		return false
	}
	fieldAPIName = strings.TrimSpace(fieldAPIName)
	if fieldAPIName == "" {
		return false
	}
	for _, field := range schema.Fields {
		if strings.EqualFold(strings.TrimSpace(field.APIName), fieldAPIName) {
			return true
		}
	}
	return false
}

func connectorPushValidationSchemaResolverForSource(source *ResolvedPackSource, opts PackSourceOptions, allowLiveSchema bool) func(string) *models.ObjectMetadata {
	dependencySchemas := sourceBackedDependencyObjectSchemasForValidation(source, opts)
	if len(dependencySchemas) == 0 && (!allowLiveSchema || opts.ObjectSchemaResolver == nil) {
		return nil
	}
	var liveSchemaObjectKeys map[string]bool
	var liveSchemaPackKeys map[string]bool
	if allowLiveSchema && opts.ObjectSchemaResolver != nil {
		liveSchemaObjectKeys = sourceBackedConnectorPushLiveSchemaObjectKeysForValidation(source, dependencySchemas)
		liveSchemaPackKeys = sourceBackedConnectorPushLiveSchemaPackKeysForValidation(source, opts)
	}
	return func(apiName string) *models.ObjectMetadata {
		apiName = strings.TrimSpace(apiName)
		if apiName == "" {
			return nil
		}
		key := strings.ToLower(apiName)
		dependencySchema := dependencySchemas[key]
		var resolvedSchema *models.ObjectMetadata
		if allowLiveSchema && opts.ObjectSchemaResolver != nil && liveSchemaObjectKeys[key] {
			resolvedSchema = filterConnectorPushValidationLiveSchemaByPackKeys(opts.ObjectSchemaResolver(apiName), liveSchemaPackKeys)
		}
		return mergeConnectorPushValidationObjectSchemas(apiName, dependencySchema, resolvedSchema)
	}
}

func sourceBackedConnectorPushLiveSchemaObjectKeysForValidation(source *ResolvedPackSource, dependencySchemas map[string]*models.ObjectMetadata) map[string]bool {
	keys := map[string]bool{}
	addObjectKey := func(apiName string) {
		apiName = strings.TrimSpace(apiName)
		if apiName != "" {
			keys[strings.ToLower(apiName)] = true
		}
	}
	sourceSpecs := sourceBackedObjectSpecsForValidation(source)
	for _, spec := range sourceSpecs {
		addObjectKey(spec.APIName)
	}
	for _, customField := range sourceBackedCustomFieldDeclarationsForValidation(source, sourceSpecs) {
		addObjectKey(customField.ObjectAPIName)
	}
	// Installed-row recovery can validate connector contracts that depend on a
	// separately installed CRM pack. The contract explicitly names the only
	// additional schemas it needs, so permit live fallback for those objects.
	if source != nil && source.Manifest != nil {
		for _, component := range source.Manifest.Components {
			if component.Kind != ComponentKindConnectorPushContract {
				continue
			}
			payload, err := sourceBackedComponentPayload(source, component)
			if err != nil {
				continue
			}
			var contract connectorcontract.ConnectorPushDurableMetadataContract
			if err := json.Unmarshal(payload, &contract); err != nil {
				continue
			}
			addObjectKey(contract.SourceObject)
			for _, association := range contract.Associations {
				if association.Junction != nil {
					addObjectKey(association.Junction.Object)
				}
			}
		}
	}
	for key := range dependencySchemas {
		addObjectKey(key)
	}
	return keys
}

func sourceBackedConnectorPushLiveSchemaPackKeysForValidation(source *ResolvedPackSource, opts PackSourceOptions) map[string]bool {
	keys := map[string]bool{}
	if source == nil || source.Manifest == nil {
		return keys
	}
	if name := strings.TrimSpace(source.Manifest.Name); name != "" {
		keys[strings.ToLower(name)] = true
	}
	visited := map[string]bool{}
	var visit func([]PackDependency)
	visit = func(deps []PackDependency) {
		for _, dep := range deps {
			name := strings.TrimSpace(dep.Name)
			key := strings.ToLower(name)
			if name == "" || visited[key] {
				continue
			}
			visited[key] = true
			keys[key] = true
			if strings.TrimSpace(opts.OfficialRepoRoot) == "" {
				continue
			}
			depReport := ValidatePackSourceWithOptions(PackSourceKindOfficialRepo+":"+name, opts)
			if depReport.Source == nil || depReport.HasErrors() || depReport.Source.Manifest == nil {
				continue
			}
			visit(depReport.Source.Manifest.Dependencies)
		}
	}
	visit(source.Manifest.Dependencies)
	return keys
}

func filterConnectorPushValidationLiveSchemaByPackKeys(schema *models.ObjectMetadata, packKeys map[string]bool) *models.ObjectMetadata {
	if schema == nil {
		return nil
	}
	copy := *schema
	copy.Fields = nil
	for _, field := range schema.Fields {
		if connectorPushValidationLiveFieldAllowedByPackKeys(field, packKeys) {
			copy.Fields = append(copy.Fields, field)
		}
	}
	return &copy
}

func connectorPushValidationLiveFieldAllowedByPackKeys(field models.FieldMetadata, packKeys map[string]bool) bool {
	ownerPack := ""
	if field.OwnerPack != nil {
		ownerPack = strings.TrimSpace(*field.OwnerPack)
	}
	switch strings.TrimSpace(field.OwnerKind) {
	case constants.FieldOwnerKindPack, constants.FieldOwnerKindPackRetained, constants.FieldOwnerKindPackRemoving:
		return ownerPack != "" && packKeys[strings.ToLower(ownerPack)]
	case "", constants.FieldOwnerKindRuntime:
		return ownerPack == ""
	default:
		return false
	}
}

func mergeConnectorPushValidationObjectSchemas(apiName string, schemas ...*models.ObjectMetadata) *models.ObjectMetadata {
	apiName = strings.TrimSpace(apiName)
	var merged *models.ObjectMetadata
	seenFields := map[string]struct{}{}
	for _, schema := range schemas {
		if schema == nil {
			continue
		}
		if merged == nil {
			copy := *schema
			copy.Fields = nil
			if strings.TrimSpace(copy.APIName) == "" {
				copy.APIName = apiName
			}
			merged = &copy
		}
		for _, field := range schema.Fields {
			fieldAPIName := strings.TrimSpace(field.APIName)
			if fieldAPIName == "" {
				continue
			}
			key := strings.ToLower(fieldAPIName)
			if _, ok := seenFields[key]; ok {
				continue
			}
			merged.Fields = append(merged.Fields, field)
			seenFields[key] = struct{}{}
		}
	}
	return merged
}

func validateInstallableComponentPhaseDependencies(components []ManifestComponent, registrations *ComponentKindRegistrationSet) []PackDiagnostic {
	indexByKey := map[string]int{}
	for i, component := range components {
		if component.Kind == "" || strings.TrimSpace(component.BusinessKey) == "" {
			continue
		}
		indexByKey[componentIdentityKey(component.Kind, component.BusinessKey)] = i
	}

	var diagnostics []PackDiagnostic
	for i, component := range components {
		componentPhase, ok := sourceValidationInstallPhase(component.Kind, registrations)
		if !ok {
			continue
		}
		componentRank, ok := InstallPlanPhaseRank(componentPhase)
		if !ok {
			continue
		}
		for j, dep := range component.DependsOn {
			if _, ok := indexByKey[componentIdentityKey(dep.Kind, dep.BusinessKey)]; !ok {
				continue
			}
			depPhase, ok := sourceValidationInstallPhase(dep.Kind, registrations)
			if !ok {
				continue
			}
			depRank, ok := InstallPlanPhaseRank(depPhase)
			if !ok {
				continue
			}
			if depRank <= componentRank {
				continue
			}
			diagnostics = append(diagnostics, diag(
				"component.depends_on.phase_order",
				fmt.Sprintf("components[%d].dependsOn[%d]", i, j),
				fmt.Sprintf("component (%s, %s) is in install phase %q but depends on (%s, %s) in later phase %q; fixed-phase install plans cannot satisfy this dependency",
					component.Kind,
					strings.TrimSpace(component.BusinessKey),
					componentPhase,
					dep.Kind,
					strings.TrimSpace(dep.BusinessKey),
					depPhase,
				),
			))
		}
	}
	return diagnostics
}

func sortedManageabilityList(values map[Manageability]struct{}) string {
	if len(values) == 0 {
		return "[]"
	}
	out := make([]string, 0, len(values))
	for value := range values {
		out = append(out, string(value))
	}
	sort.Strings(out)
	return strings.Join(out, ", ")
}

func detectComponentDependencyCycles(components []ManifestComponent) []PackDiagnostic {
	indexByKey := map[string]int{}
	for i, component := range components {
		if component.Kind != "" && strings.TrimSpace(component.BusinessKey) != "" {
			indexByKey[componentIdentityKey(component.Kind, component.BusinessKey)] = i
		}
	}
	visiting := map[string]bool{}
	visited := map[string]bool{}
	var diagnostics []PackDiagnostic
	var visit func(key string, stack []string)
	visit = func(key string, stack []string) {
		if visited[key] {
			return
		}
		if visiting[key] {
			cycle := append(stack, key)
			diagnostics = append(diagnostics, diag("component.depends_on.cycle", "components", fmt.Sprintf("dependency cycle detected: %s", strings.Join(cycle, " -> "))))
			return
		}
		idx, ok := indexByKey[key]
		if !ok {
			return
		}
		visiting[key] = true
		component := components[idx]
		for _, dep := range component.DependsOn {
			depKey := componentIdentityKey(dep.Kind, dep.BusinessKey)
			if _, ok := indexByKey[depKey]; ok {
				visit(depKey, append(stack, key))
			}
		}
		visiting[key] = false
		visited[key] = true
	}
	keys := make([]string, 0, len(indexByKey))
	for key := range indexByKey {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		visit(key, nil)
	}
	return diagnostics
}

func normalizeManifestPath(raw string) (string, error) {
	if strings.TrimSpace(raw) == "" {
		return "", fmt.Errorf("path is required")
	}
	slashed := strings.ReplaceAll(raw, "\\", "/")
	if filepath.IsAbs(raw) || strings.HasPrefix(slashed, "/") || windowsDriveAbsRe.MatchString(slashed) {
		return "", fmt.Errorf("absolute paths are not allowed: %q", raw)
	}
	normalized := norm.NFC.String(slashed)
	parts := strings.Split(normalized, "/")
	for _, part := range parts {
		switch part {
		case "", ".", "..":
			return "", fmt.Errorf("path %q contains invalid segment %q", raw, part)
		}
	}
	return strings.Join(parts, "/"), nil
}

func validateOfficialRepoPackName(name string) error {
	if strings.TrimSpace(name) == "" {
		return fmt.Errorf("%s source name is required", PackSourceKindOfficialRepo)
	}
	if strings.ContainsAny(name, `/\`) || name == "." || name == ".." {
		return fmt.Errorf("%s source name %q must be a pack name, not a path", PackSourceKindOfficialRepo, name)
	}
	return nil
}

func isNamedPackSourceKind(kind string) bool {
	switch kind {
	case PackSourceKindOfficialRepo:
		return true
	default:
		return false
	}
}

// IsRevisionAddressedPackSourceKind reports whether the source identity is
// immutable only when paired with one exact revision. local_path is excluded
// even when its current checkout happens to be Git-backed.
func IsRevisionAddressedPackSourceKind(kind string) bool {
	switch strings.TrimSpace(kind) {
	case PackSourceKindOfficialRepo, PackSourceKindReleaseRepo:
		return true
	default:
		return false
	}
}

func shouldValidatePayloadAsJSON(kind ComponentKind, normalizedPath string) bool {
	if strings.EqualFold(path.Ext(normalizedPath), ".json") {
		return true
	}
	switch kind {
	case ComponentKindObjectSpec, ComponentKindObjectRecordMutationPolicy, ComponentKindObjectLifecyclePolicy, ComponentKindValidationRule, ComponentKindOAuthCustomScope, ComponentKindConnectedApp, ComponentKindEmailTemplate, ComponentKindFormulaFunctionBundle, ComponentKindHrefRewriteFields:
		return true
	default:
		return false
	}
}

func defaultOfficialRepoRoot() string {
	return strings.TrimSpace(os.Getenv(OfficialAppPacksRootEnvVar))
}

func officialRepoRootHelp() string {
	return fmt.Sprintf("%s is required for official_repo sources; set it to a nexuscrm-app-packs checkout root", OfficialAppPacksRootEnvVar)
}

func isWithinDir(root, target string) bool {
	rel, err := filepath.Rel(root, target)
	if err != nil {
		return false
	}
	return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
}

func isValidManageability(m Manageability) bool {
	switch m {
	case ManageabilityLocked, ManageabilityDefaultsOnly:
		return true
	default:
		return false
	}
}

func componentIdentityKey(kind ComponentKind, businessKey string) string {
	return string(kind) + "\x00" + strings.TrimSpace(businessKey)
}

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

// validateConnectorInboundHandlerConfigComponent validates one
// connector_inbound_handler_config component payload. It must be a locked,
// ConnectorPack-owned sibling of a connector_capability component and must build
// one of the closed v1 handler kinds through the generic interpreters.
func validateConnectorInboundHandlerConfigComponent(manifestName string, component ManifestComponent, payload []byte, pathPrefix string) []PackDiagnostic {
	var diagnostics []PackDiagnostic
	var p ConnectorInboundHandlerConfigComponentPayload
	dec := json.NewDecoder(bytes.NewReader(payload))
	dec.DisallowUnknownFields()
	if err := dec.Decode(&p); err != nil {
		diagnostics = append(diagnostics, diag("connector_inbound_handler_config.payload.invalid", pathPrefix, fmt.Sprintf("connector_inbound_handler_config payload parse failed: %v", err)))
		return diagnostics
	}
	if err := ensureJSONEOF(dec); err != nil {
		diagnostics = append(diagnostics, diag("connector_inbound_handler_config.payload.trailing_data", pathPrefix, fmt.Sprintf("connector_inbound_handler_config payload has trailing data: %v", err)))
	}
	p.Namespace = strings.TrimSpace(p.Namespace)
	p.CapabilityKind = strings.TrimSpace(p.CapabilityKind)
	manifestName = strings.TrimSpace(manifestName)
	if p.Namespace == "" {
		diagnostics = append(diagnostics, diag("connector_inbound_handler_config.namespace.missing", pathPrefix+".payload.namespace", "connector_inbound_handler_config namespace is required"))
	} else if manifestName != "" && p.Namespace != manifestName {
		diagnostics = append(diagnostics, diag("connector_inbound_handler_config.namespace.mismatch_pack_name", pathPrefix+".payload.namespace", fmt.Sprintf("connector_inbound_handler_config namespace %q must equal the manifest name %q", p.Namespace, manifestName)))
	}
	if p.CapabilityKind == "" {
		diagnostics = append(diagnostics, diag("connector_inbound_handler_config.capability_kind.missing", pathPrefix+".payload.capabilityKind", "connector_inbound_handler_config capabilityKind is required and must be non-empty"))
	} else if _, ok := connectorCapabilityKinds[p.CapabilityKind]; !ok {
		diagnostics = append(diagnostics, diag("connector_inbound_handler_config.capability_kind.unknown", pathPrefix+".payload.capabilityKind", fmt.Sprintf("connector_inbound_handler_config capabilityKind %q is not supported", p.CapabilityKind)))
	}
	if len(bytes.TrimSpace(p.Config)) == 0 {
		diagnostics = append(diagnostics, diag("connector_inbound_handler_config.config.missing", pathPrefix+".payload.config", "connector_inbound_handler_config config is required"))
	}
	if component.Manageability != "" && component.Manageability != ManageabilityLocked {
		diagnostics = append(diagnostics, diag("connector_inbound_handler_config.manageability.invalid", pathPrefix+".manageability", fmt.Sprintf("connector_inbound_handler_config requires manageability %q, got %q", ManageabilityLocked, component.Manageability)))
	}
	if p.Namespace != "" && p.CapabilityKind != "" {
		expectedBK := p.Namespace + ":" + p.CapabilityKind
		gotBK := strings.TrimSpace(component.BusinessKey)
		if gotBK != expectedBK {
			diagnostics = append(diagnostics, diag("connector_inbound_handler_config.business_key.mismatch_payload", pathPrefix+".businessKey", fmt.Sprintf("connector_inbound_handler_config businessKey %q must equal canonical %q (namespace:capabilityKind)", gotBK, expectedBK)))
		}
	}
	if len(diagnostics) == 0 {
		if _, err := newConnectorInboundHandlerConfigComponent(manifestName, component, payload); err != nil {
			diagnostics = append(diagnostics, diag("connector_inbound_handler_config.config.invalid", pathPrefix+".payload.config", err.Error()))
		}
	}
	return diagnostics
}

// validateConnectorInboundRouteComponent validates a single connector_inbound_route
// component payload. Each component declares exactly ONE route (V2 single-route shape).
//
// Validation rules (each independent; all reported before returning):
//   - Payload parses via json.NewDecoder + DisallowUnknownFields; trailing token → diagnostic.
//   - payload.Namespace REQUIRED and must equal manifestName (Challenger MED #4 — no optional-
//     with-default; mirrors connector_capability_component.go:18-21).
//   - payload.EventKind REQUIRED non-empty.
//   - payload.FlowDeveloperName REQUIRED non-empty.
//   - component.Manageability must be ManageabilityLocked (or empty, defaulting to locked).
//   - BusinessKey must equal JoinComponentBusinessKey(manifestName, canonicalEventKind).
//     The generic componentIdentityKey at pack_source.go:880 catches (kind, businessKey)
//     duplicates — since BusinessKey includes the canonical eventKind, duplicate eventKinds
//     across components of the same pack are caught by the standard identity-key check.
func validateConnectorInboundRouteComponent(manifestName string, component ManifestComponent, payload []byte, pathPrefix string) []PackDiagnostic {
	var diagnostics []PackDiagnostic
	var p connectorInboundRoutePayload
	dec := json.NewDecoder(bytes.NewReader(payload))
	dec.DisallowUnknownFields()
	if err := dec.Decode(&p); err != nil {
		diagnostics = append(diagnostics, diag("connector_inbound_route.payload.invalid", pathPrefix, fmt.Sprintf("connector_inbound_route payload parse failed: %v", err)))
		return diagnostics // cannot proceed without valid payload
	}
	if err := ensureJSONEOF(dec); err != nil {
		diagnostics = append(diagnostics, diag("connector_inbound_route.payload.trailing_data", pathPrefix, fmt.Sprintf("connector_inbound_route payload has trailing data: %v", err)))
	}
	p.Namespace = strings.TrimSpace(p.Namespace)
	p.EventKind = strings.ToLower(strings.TrimSpace(p.EventKind))
	p.FlowDeveloperName = strings.TrimSpace(p.FlowDeveloperName)

	manifestName = strings.TrimSpace(manifestName)
	if p.Namespace == "" {
		diagnostics = append(diagnostics, diag("connector_inbound_route.namespace.missing", pathPrefix+".payload.namespace", "connector_inbound_route namespace is required"))
	} else if manifestName != "" && p.Namespace != manifestName {
		diagnostics = append(diagnostics, diag("connector_inbound_route.namespace.mismatch_pack_name", pathPrefix+".payload.namespace", fmt.Sprintf("connector_inbound_route namespace %q must equal the manifest name %q", p.Namespace, manifestName)))
	}
	if p.EventKind == "" {
		diagnostics = append(diagnostics, diag("connector_inbound_route.event_kind.missing", pathPrefix+".payload.eventKind", "connector_inbound_route eventKind is required and must be non-empty"))
	}
	if p.FlowDeveloperName == "" {
		diagnostics = append(diagnostics, diag("connector_inbound_route.flow_developer_name.missing", pathPrefix+".payload.flowDeveloperName", "connector_inbound_route flowDeveloperName is required and must be non-empty"))
	}
	if component.Manageability != "" && component.Manageability != ManageabilityLocked {
		diagnostics = append(diagnostics, diag("connector_inbound_route.manageability.invalid", pathPrefix+".manageability", fmt.Sprintf("connector_inbound_route requires manageability %q, got %q", ManageabilityLocked, component.Manageability)))
	}
	// Enforce that the manifest businessKey matches JoinComponentBusinessKey(manifestName, canonicalEventKind).
	// This guarantees the (kind, businessKey) identity-key check at pack_source.go:880 deduplicates
	// same-eventKind components (since businessKey encodes the canonical eventKind).
	if manifestName != "" && p.EventKind != "" {
		expectedBK := JoinComponentBusinessKey(manifestName, p.EventKind)
		gotBK := strings.TrimSpace(component.BusinessKey)
		if gotBK != expectedBK {
			diagnostics = append(diagnostics, diag("connector_inbound_route.business_key.mismatch_payload", pathPrefix+".businessKey", fmt.Sprintf("connector_inbound_route businessKey %q must equal canonical %q (packName/eventKind)", gotBK, expectedBK)))
		}
	}
	return diagnostics
}

// validateConnectorCalloutProfileDuplicates rejects manifests that declare two
// or more connector_callout_profile components whose (packName, profileName)
// tuple is identical after case-normalization (strings.ToLower + TrimSpace on
// both), which is the same canonicalization that
// newSourceBackedConnectorCalloutProfileComponent uses when computing the live
// identity and writing _System_ConnectorCalloutProfile.
//
// Without this validator, a manifest could declare both "Acme-Connector/Default"
// and "acme-connector/default" with different payloads. ValidateInstallablePackSource
// would pass, both components would attempt to upsert the same (OwnerPack,
// ProfileName) DB row, the install would fail at final verify after the first
// write had already landed, and the orphaned live row would strand subsequent
// install/reapply attempts (owner-surface preflight rejects non-active rows;
// reapply requires pack=active).
//
// This is a forward-authoring gate: payload resolution failures are already
// reported by the main component loop and are not re-emitted here.
func validateConnectorCalloutProfileDuplicates(source *ResolvedPackSource) []PackDiagnostic {
	if source == nil || source.Manifest == nil {
		return nil
	}
	// canonical key → first seen raw (packName/profileName) for the error message.
	type seenEntry struct {
		rawPack    string
		rawProfile string
	}
	seen := map[string]seenEntry{}
	var diagnostics []PackDiagnostic
	for _, component := range source.Manifest.Components {
		if component.Kind != ComponentKindConnectorCalloutProfile {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			// Payload-resolution failures are already reported by the main loop.
			continue
		}
		var def connectorCalloutProfileDefinition
		if err := json.Unmarshal(payload, &def); err != nil {
			// Parse failures are reported by newSourceBackedConnectorCalloutProfileComponent.
			continue
		}
		rawPack := strings.TrimSpace(source.Manifest.Name)
		rawProfile := strings.TrimSpace(def.ProfileName)
		canonicalKey := strings.ToLower(rawPack) + "\x00" + strings.ToLower(rawProfile)
		if first, already := seen[canonicalKey]; already {
			diagnostics = append(diagnostics, diag(
				"connector_callout_profile.duplicate_after_canonicalization",
				"components",
				fmt.Sprintf(
					"duplicate connector_callout_profile after case-normalization: pack=%s profile=%s conflicts with pack=%s profile=%s",
					strings.ToLower(rawPack), strings.ToLower(rawProfile),
					strings.ToLower(first.rawPack), strings.ToLower(first.rawProfile),
				),
			))
		} else {
			seen[canonicalKey] = seenEntry{rawPack: rawPack, rawProfile: rawProfile}
		}
	}
	return diagnostics
}

// validateConnectorCalloutProfileBatchCoverageOverlaps rejects connector-pack
// manifests that declare multiple executable batch profiles covering the same
// namespace/objectKind pair. Runtime startup rejects the same ambiguity in
// connector_inbound.NewCalloutBatchReadAdapter, so install-time validation must
// fail closed before setup apply writes a pack that later crashes worker
// startup.
//
// This validator intentionally mirrors runtime eligibility:
//   - a profile must have a fully executable batchSpec under the current active
//     contract (payload-only or malformed batchSpec does not count)
//   - ObjectPathMap entries with blank object kind or blank path do not count
//     as executable coverage
//
// Payload-resolution and payload-parse failures are already reported elsewhere
// and are not duplicated here.
func validateConnectorCalloutProfileBatchCoverageOverlaps(source *ResolvedPackSource) []PackDiagnostic {
	if source == nil || source.Manifest == nil {
		return nil
	}

	type seenEntry struct {
		profileName string
	}

	namespace := strings.TrimSpace(source.Manifest.Name)
	seen := map[string]seenEntry{}
	var diagnostics []PackDiagnostic
	for _, component := range source.Manifest.Components {
		if component.Kind != ComponentKindConnectorCalloutProfile {
			continue
		}
		payload, err := sourceBackedComponentPayload(source, component)
		if err != nil {
			continue
		}
		var def connectorCalloutProfileDefinition
		if err := json.Unmarshal(payload, &def); err != nil {
			continue
		}
		if !connectorCalloutProfileHasExecutableBatchCoverage(def) {
			continue
		}
		profileName := strings.ToLower(strings.TrimSpace(def.ProfileName))
		for objectKind, objectPath := range def.ObjectPathMap {
			objectKind = strings.TrimSpace(objectKind)
			objectPath = strings.TrimSpace(objectPath)
			if objectKind == "" || objectPath == "" {
				continue
			}
			coverageKey := namespace + "\x00" + objectKind
			if first, exists := seen[coverageKey]; exists {
				diagnostics = append(diagnostics, diag(
					"connector_callout_profile.batch_object_overlap",
					"components",
					fmt.Sprintf(
						"multiple executable connector_callout_profile components cover namespace=%s objectKind=%s: profiles %s and %s",
						namespace,
						objectKind,
						first.profileName,
						profileName,
					),
				))
				continue
			}
			seen[coverageKey] = seenEntry{profileName: profileName}
		}
	}
	return diagnostics
}

func connectorCalloutProfileHasExecutableBatchCoverage(def connectorCalloutProfileDefinition) bool {
	method := strings.ToUpper(strings.TrimSpace(def.BatchSpec.Method))
	if method == "" ||
		strings.TrimSpace(def.BatchSpec.CredentialRef) == "" ||
		strings.TrimSpace(def.BatchSpec.EndpointClass) == "" ||
		strings.TrimSpace(def.BatchSpec.IDsRequestField) == "" ||
		def.BatchSpec.MaxBatchSize < 1 {
		return false
	}
	if method == "GET" || method == "HEAD" {
		return false
	}
	if trimmedIDPath := strings.TrimSpace(def.BatchSpec.IDResponseField); trimmedIDPath != "" {
		if err := validateConnectorCalloutProfileDottedPath(trimmedIDPath); err != nil {
			return false
		}
	}
	if trimmedRecordsPath := strings.TrimSpace(def.BatchSpec.RecordsResponsePath); trimmedRecordsPath != "" {
		if err := validateConnectorCalloutProfileDottedPath(trimmedRecordsPath); err != nil {
			return false
		}
	}
	for objectKind, objectPath := range def.ObjectPathMap {
		if strings.TrimSpace(objectKind) == "" || strings.TrimSpace(objectPath) == "" {
			continue
		}
		return true
	}
	return false
}

func diag(code, path, message string) PackDiagnostic {
	return PackDiagnostic{Code: code, Path: path, Message: message}
}

func sortDiagnostics(diagnostics []PackDiagnostic) {
	sort.SliceStable(diagnostics, func(i, j int) bool {
		if diagnostics[i].Code != diagnostics[j].Code {
			return diagnostics[i].Code < diagnostics[j].Code
		}
		if diagnostics[i].Path != diagnostics[j].Path {
			return diagnostics[i].Path < diagnostics[j].Path
		}
		return diagnostics[i].Message < diagnostics[j].Message
	})
}
