package packs

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"reflect"
	"sort"
	"strings"

	"github.com/pingcap-inc/nexuscrm-controlplane/componentbehavior"
)

type ComponentKind string

const (
	ComponentKindObjectSpec                    ComponentKind = "object_spec"
	ComponentKindObjectRecordMutationPolicy    ComponentKind = "object_record_mutation_policy"
	ComponentKindObjectLifecyclePolicy         ComponentKind = "object_lifecycle_policy"
	ComponentKindRecordType                    ComponentKind = "record_type"
	ComponentKindRecordTypePicklistValue       ComponentKind = "record_type_picklist_value"
	ComponentKindListFields                    ComponentKind = "list_fields"
	ComponentKindValidationRule                ComponentKind = "validation_rule"
	ComponentKindPermission                    ComponentKind = "permission"
	ComponentKindSystemPermissionGrants        ComponentKind = "system_permission_grants"
	ComponentKindUIAction                      ComponentKind = "ui_action"
	ComponentKindInvocableAction               ComponentKind = "invocable_action"
	ComponentKindFlowActionCallHandler         ComponentKind = "flow_action_call_handler"
	ComponentKindPageLayout                    ComponentKind = "page_layout"
	ComponentKindLayoutAssignment              ComponentKind = "layout_assignment"
	ComponentKindRelatedListRule               ComponentKind = "related_list_rule"
	ComponentKindReportType                    ComponentKind = "report_type"
	ComponentKindStandardFlow                  ComponentKind = "standard_flow"
	ComponentKindApprovalProcess               ComponentKind = "approval_process"
	ComponentKindEmailTemplate                 ComponentKind = "email_template"
	ComponentKindCustomField                   ComponentKind = "custom_field"
	ComponentKindFieldMapping                  ComponentKind = "field_mapping"
	ComponentKindFormulaFunctionBundle         ComponentKind = "formula_function_bundle"
	ComponentKindAutoNumberFormat              ComponentKind = "autonumber_format"
	ComponentKindRecordTypeCode                ComponentKind = "record_type_code"
	ComponentKindFrontendSourceManifest        ComponentKind = "frontend_source_manifest"
	ComponentKindAutoNumberCounter             ComponentKind = "autonumber_counter"
	ComponentKindConnectedApp                  ComponentKind = "connected_app"
	ComponentKindOAuthCustomScope              ComponentKind = "oauth_custom_scope"
	ComponentKindCustomPermission              ComponentKind = "custom_permission"
	ComponentKindTheme                         ComponentKind = "theme"
	ComponentKindUIComponent                   ComponentKind = "ui_component"
	ComponentKindSetupPage                     ComponentKind = "setup_page"
	ComponentKindAppMenuItem                   ComponentKind = "app_menu_item"
	ComponentKindFolderType                    ComponentKind = "folder_type"
	ComponentKindReportFolder                  ComponentKind = "report_folder"
	ComponentKindFolderShare                   ComponentKind = "folder_share"
	ComponentKindChildCloneRules               ComponentKind = "child_clone_rules"
	ComponentKindSearchScopeOrder              ComponentKind = "search_scope_order"
	ComponentKindSearchAllDefaultFields        ComponentKind = "search_all_default_fields"
	ComponentKindHrefRewriteFields             ComponentKind = "href_rewrite_fields"
	ComponentKindWebhookSecret                 ComponentKind = "webhook_secret"
	ComponentKindConnectorCapability           ComponentKind = "connector_capability"
	ComponentKindConnectorRateLimit            ComponentKind = "connector_rate_limit"
	ComponentKindConnectorInboundHandlerConfig ComponentKind = "connector_inbound_handler_config"
	ComponentKindConnectorInboundRoute         ComponentKind = "connector_inbound_route"
	ComponentKindConnectorCalloutProfile       ComponentKind = "connector_callout_profile"
	ComponentKindConnectorFieldMapping         ComponentKind = "connector_field_mapping"
	ComponentKindConnectorPushContract         ComponentKind = "connector_push_contract"
)

type Manageability string

// Manageability tiers track the L1-defaults-vs-L2-overrides contract. The
// current architecture summary is in `docs/design/l0-platform-substrate.md`
// under "Verify"; this block records the per-kind enforcement reality so the
// contract does not drift from implementation. Only tiers actually wired to a
// component kind are defined here; future tiers (e.g. `additive`,
// `overlayable`) must be added in the same change that enables them on
// at least one component.
//
// Enforcement reality (per kind, recorded so the contract doesn't drift
// from the implementation):
//
//   - `locked` — fully enforced for object_spec via specdrift's
//     CustomizationPolicyDrift (see specdrift.CustomizationPolicy);
//     other locked kinds rely on per-kind verifiers.
//   - `defaults_only` — IsCustomized-as-ownership-marker is honored by
//     specdrift for object_spec via CustomizationPolicyAllow. Other
//     defaults_only kinds (permission, system_permission_grants,
//     validation_rule, field_mapping, auto_number_counter, standard_flow,
//     invocable_action, related_list_rule, page_layout, list_fields) carry
//     the marker for contract intent; per-kind override-preservation
//     enforcement varies.
//     New component kinds must not claim defaults_only without delivering the
//     override-preservation path in the same change; use locked until that
//     path exists.
const (
	ManageabilityLocked       Manageability = "locked"
	ManageabilityDefaultsOnly Manageability = "defaults_only"
)

// PackComponent is one pack-owned metadata surface. The pack substrate owns
// ordering, hashing, and default drift detection; individual components own
// only the component-specific projection between shipped payload and live DB.
//
// Manageability is part of the component identity and documents the intended
// reconciliation contract. Default verification compares the declared expected
// projection with the live projection exactly. Components with admin/customized
// overlays must encode that manageability in their projections or provide a
// custom verifier because ownership/customization markers are surface-specific
// (row-scoped OwnerPack, Source+IsCustomized, or column-scoped flags).
type PackComponent interface {
	Kind() ComponentKind
	PackName() string
	BusinessKey() string
	Manageability() Manageability
	ExpectedPayload() ([]byte, error)
	ExpectedProjection() (any, error)
	LiveProjection(ctx context.Context, deps VerifyDeps) (any, error)
	Apply(ctx context.Context, deps ApplyDeps) error
}

type ComposablePack interface {
	Pack
	Components() []PackComponent
}

type ComponentReference struct {
	Kind        ComponentKind `json:"kind"`
	BusinessKey string        `json:"businessKey"`
	Required    bool          `json:"required"`
}

type ComponentDependencyProvider interface {
	DependsOn() []ComponentReference
}

type PackDependencyProvider interface {
	PackDependencies() []PackDependency
}

type DeclaredComponent struct {
	PackName      string               `json:"packName"`
	Kind          ComponentKind        `json:"kind"`
	BusinessKey   string               `json:"businessKey"`
	Required      bool                 `json:"required"`
	Manageability Manageability        `json:"manageability"`
	PayloadHash   string               `json:"payloadHash,omitempty"`
	References    []ComponentReference `json:"references,omitempty"`
}

// Digest returns the canonical identity-and-payload digest used to compare one
// component across immutable pack revisions. Pack-level source identity and L0
// release compatibility are separate authorities; this digest answers only
// whether this component's declared desired state changed.
func (c DeclaredComponent) Digest() (string, error) {
	encoded, err := json.Marshal(c)
	if err != nil {
		return "", fmt.Errorf("marshal declared component digest: %w", err)
	}
	return sha256Hex(encoded), nil
}

// ComponentDeclarer exposes the row-level component manifest for a pack. The
// manifest is the ownership oracle used by clean-L0 verification and future
// verify-pack commands; live rows are mapped to the same (pack, kind,
// business-key) shape before ownership is accepted.
type ComponentDeclarer interface {
	DeclaredComponents() ([]DeclaredComponent, error)
}

type componentCustomVerifier interface {
	VerifyComponent(ctx context.Context, deps VerifyDeps) error
}

func DeclaredComponentsForPacks(packList []Pack) ([]DeclaredComponent, error) {
	var out []DeclaredComponent
	for _, pack := range packList {
		components, err := DeclaredComponentsForPack(pack)
		if err != nil {
			return nil, err
		}
		out = append(out, components...)
	}
	return out, nil
}

func DeclaredComponentsForPack(pack Pack) ([]DeclaredComponent, error) {
	if declarer, ok := pack.(ComponentDeclarer); ok {
		components, err := declarer.DeclaredComponents()
		if err != nil {
			return nil, fmt.Errorf("%s declared components: %w", pack.Name(), err)
		}
		return normalizeDeclaredComponents(pack.Name(), components)
	}
	if composable, ok := pack.(ComposablePack); ok {
		components := composable.Components()
		declared := make([]DeclaredComponent, 0, len(components))
		for _, component := range components {
			converted, err := declaredComponentFromPackComponent(component)
			if err != nil {
				return nil, fmt.Errorf("%s %s declared component: %w", component.Kind(), component.BusinessKey(), err)
			}
			declared = append(declared, converted)
		}
		return normalizeDeclaredComponents(pack.Name(), declared)
	}
	return nil, nil
}

func normalizeDeclaredComponents(packName string, components []DeclaredComponent) ([]DeclaredComponent, error) {
	out := make([]DeclaredComponent, 0, len(components))
	for _, component := range components {
		component.PackName = strings.TrimSpace(component.PackName)
		if component.PackName == "" {
			component.PackName = packName
		}
		component.BusinessKey = strings.TrimSpace(component.BusinessKey)
		if component.PackName == "" {
			return nil, fmt.Errorf("component %s/%s has empty pack name", component.Kind, component.BusinessKey)
		}
		if component.Kind == "" {
			return nil, fmt.Errorf("component %s/%s has empty kind", component.PackName, component.BusinessKey)
		}
		if component.BusinessKey == "" {
			return nil, fmt.Errorf("component %s/%s has empty business key", component.PackName, component.Kind)
		}
		out = append(out, component)
	}
	return out, nil
}

func declaredComponentFromPackComponent(component PackComponent) (DeclaredComponent, error) {
	payload, err := component.ExpectedPayload()
	if err != nil {
		return DeclaredComponent{}, err
	}
	if len(payload) == 0 {
		projection, err := component.ExpectedProjection()
		if err != nil {
			return DeclaredComponent{}, err
		}
		payload, err = marshalComponentPayload(projection)
		if err != nil {
			return DeclaredComponent{}, err
		}
	}
	payloadHash := ""
	if len(payload) > 0 {
		payloadHash = sha256Hex(payload)
	}
	return DeclaredComponent{
		PackName:      component.PackName(),
		Kind:          component.Kind(),
		BusinessKey:   component.BusinessKey(),
		Required:      true,
		Manageability: component.Manageability(),
		PayloadHash:   payloadHash,
	}, nil
}

func JoinComponentBusinessKey(parts ...string) string {
	trimmed := make([]string, 0, len(parts))
	for _, part := range parts {
		trimmed = append(trimmed, strings.TrimSpace(part))
	}
	return strings.Join(trimmed, "/")
}

func sha256Hex(payload []byte) string {
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:])
}

func isLowerHex64(s string) bool {
	if len(s) != 64 {
		return false
	}
	for _, r := range s {
		if (r < '0' || r > '9') && (r < 'a' || r > 'f') {
			return false
		}
	}
	return true
}

func ComponentPayloadHash(payload any) (string, error) {
	encoded, err := marshalComponentPayload(payload)
	if err != nil {
		return "", err
	}
	return sha256Hex(encoded), nil
}

func applyPackComponents(ctx context.Context, deps ApplyDeps, components []PackComponent) error {
	for _, c := range components {
		if err := c.Apply(ctx, deps); err != nil {
			return fmt.Errorf("%s %s: %w", c.Kind(), c.BusinessKey(), err)
		}
	}
	return nil
}

func verifyPackComponents(ctx context.Context, deps VerifyDeps, components []PackComponent) error {
	var errs []*ComponentVerifyError
	for _, c := range components {
		log.Printf("pack verify: start kind=%s key=%s", c.Kind(), c.BusinessKey())
		var err error
		if custom, ok := c.(componentCustomVerifier); ok {
			err = custom.VerifyComponent(ctx, deps)
		} else {
			err = verifyPackComponentDefault(ctx, deps, c)
		}
		if err != nil {
			log.Printf("pack verify: fail kind=%s key=%s err=%v", c.Kind(), c.BusinessKey(), err)
		} else {
			log.Printf("pack verify: done kind=%s key=%s", c.Kind(), c.BusinessKey())
		}
		if err != nil {
			errs = append(errs, &ComponentVerifyError{
				Kind:        c.Kind(),
				BusinessKey: c.BusinessKey(),
				Err:         err,
			})
		}
	}
	if len(errs) > 0 {
		return &ComponentVerifyErrors{errs: errs}
	}
	return nil
}

type ComponentVerifyError struct {
	Kind        ComponentKind
	BusinessKey string
	Err         error
}

func (e *ComponentVerifyError) Error() string {
	return fmt.Sprintf("%s %s: %v", e.Kind, e.BusinessKey, e.Err)
}

func (e *ComponentVerifyError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Err
}

type ComponentVerifyErrors struct {
	errs []*ComponentVerifyError
}

// ProjectionDriftError means the immutable component declaration was read
// successfully and its canonical live projection differs from the expected
// projection. It is deliberately distinct from database, decoding, and
// verification errors: callers may repair this deterministic state mismatch,
// but must continue to fail closed on transient or unclassified failures.
type ProjectionDriftError struct {
	Expected []byte
	Live     []byte
}

func (e *ProjectionDriftError) Error() string {
	if e == nil {
		return "projection drift"
	}
	return fmt.Sprintf("projection drift: expected %s, live %s", e.Expected, e.Live)
}

func (e *ComponentVerifyErrors) Error() string {
	if e == nil || len(e.errs) == 0 {
		return "component verify failed"
	}
	lines := make([]string, 0, len(e.errs))
	for _, err := range e.errs {
		lines = append(lines, err.Error())
	}
	return fmt.Sprintf("component verify failed:\n  - %s", strings.Join(lines, "\n  - "))
}

func (e *ComponentVerifyErrors) Unwrap() []error {
	if e == nil || len(e.errs) == 0 {
		return nil
	}
	out := make([]error, 0, len(e.errs))
	for _, err := range e.errs {
		out = append(out, err)
	}
	return out
}

func ComponentVerifyErrorList(err error) []*ComponentVerifyError {
	if err == nil {
		return nil
	}
	var aggregate *ComponentVerifyErrors
	if errors.As(err, &aggregate) {
		return append([]*ComponentVerifyError(nil), aggregate.errs...)
	}
	var single *ComponentVerifyError
	if errors.As(err, &single) {
		return []*ComponentVerifyError{single}
	}
	return nil
}

// IsDeterministicProjectionDrift reports whether every component verification
// failure is an explicitly typed canonical projection mismatch. Error text is
// never sufficient: a mixed aggregate or any database/transport/decoder error
// remains fail closed and must not trigger mutation.
func IsDeterministicProjectionDrift(err error) bool {
	hasDrift, allDrift := classifyProjectionDriftTree(err)
	return hasDrift && allDrift
}

func classifyProjectionDriftTree(err error) (hasDrift bool, allDrift bool) {
	if err == nil {
		return false, false
	}
	if _, ok := err.(*ProjectionDriftError); ok {
		return true, true
	}
	if joined, ok := err.(interface{ Unwrap() []error }); ok {
		children := joined.Unwrap()
		if len(children) == 0 {
			return false, false
		}
		hasAny := false
		for _, child := range children {
			hasChild, childAll := classifyProjectionDriftTree(child)
			if !hasChild || !childAll {
				return false, false
			}
			hasAny = true
		}
		return hasAny, hasAny
	}
	if wrapped, ok := err.(interface{ Unwrap() error }); ok {
		return classifyProjectionDriftTree(wrapped.Unwrap())
	}
	return false, false
}

// canonicalProjectionJSON normalizes a projection value for drift comparison by
// round-tripping through any. This eliminates byte-level differences in
// json.RawMessage fields caused by manifest-indented vs DB-compact JSON
// formatting. reflect.DeepEqual on the raw projection structs would compare
// RawMessage []byte literally, producing false drift when the only difference
// is whitespace. The round-trip ensures every JSON value is in canonical
// compact form regardless of how the RawMessage bytes were originally stored.
//
// UseNumber() preserves exact numeric precision during decode: json.Number
// retains the original numeric string, so int64 values above 2^53 are not
// aliased to float64. This is critical for projections containing unbounded
// int64 fields (e.g., sourceBackedAutoNumberFormatRow.Start).
//
// rejectDuplicateJSONKeys is called on the marshaled output before decode so
// that a RawMessage payload with duplicate keys (e.g. {"a":1,"a":2}) fails
// closed instead of silently deduplicating to {"a":2} and hiding drift.
//
// Source incident: #1806 — connector callout profile ExtraBodyFields carried
// indented RawMessage bytes from the manifest but compact bytes from the DB
// round-trip, causing install-pack finalize-verify to fail with a "projection
// drift" error whose printed expected and live were byte-identical.
func canonicalProjectionJSON(v any) ([]byte, error) {
	raw, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}
	if err := rejectDuplicateJSONKeys(raw, "projection"); err != nil {
		return nil, fmt.Errorf("projection contains duplicate JSON keys: %w", err)
	}
	dec := json.NewDecoder(bytes.NewReader(raw))
	dec.UseNumber()
	var decoded any
	if err := dec.Decode(&decoded); err != nil {
		return nil, err
	}
	return json.Marshal(decoded)
}

// clonePackComponentProjection returns the same concrete projection type with
// every reachable pointer, slice, map, and interface value detached. Reflection
// clones without invoking user code; JSON validation runs on a second detached
// copy so even a mutating custom marshaler cannot change the source or result.
func clonePackComponentProjection(value any) (any, error) {
	if value == nil {
		return nil, nil
	}
	cloned, err := clonePackComponentProjectionValue(
		reflect.ValueOf(value),
		map[packComponentProjectionVisit]bool{},
	)
	if err != nil {
		return nil, err
	}
	validationCopy, err := clonePackComponentProjectionValue(
		cloned,
		map[packComponentProjectionVisit]bool{},
	)
	if err != nil {
		return nil, err
	}
	if _, err := canonicalProjectionJSON(validationCopy.Interface()); err != nil {
		return nil, fmt.Errorf("projection is not JSON-shaped: %w", err)
	}
	return cloned.Interface(), nil
}

type packComponentProjectionVisit struct {
	valueType reflect.Type
	pointer   uintptr
}

func clonePackComponentProjectionValue(
	value reflect.Value,
	active map[packComponentProjectionVisit]bool,
) (reflect.Value, error) {
	var releaseVisit func()
	switch value.Kind() {
	case reflect.Map, reflect.Pointer, reflect.Slice:
		if value.IsNil() {
			return reflect.Zero(value.Type()), nil
		}
		visit := packComponentProjectionVisit{valueType: value.Type(), pointer: value.Pointer()}
		if active[visit] {
			return reflect.Value{}, fmt.Errorf("projection contains a cycle through %s", value.Type())
		}
		active[visit] = true
		releaseVisit = func() { delete(active, visit) }
	}
	if releaseVisit != nil {
		defer releaseVisit()
	}

	switch value.Kind() {
	case reflect.Interface:
		if value.IsNil() {
			return reflect.Zero(value.Type()), nil
		}
		cloned, err := clonePackComponentProjectionValue(value.Elem(), active)
		if err != nil {
			return reflect.Value{}, err
		}
		out := reflect.New(value.Type()).Elem()
		out.Set(cloned)
		return out, nil
	case reflect.Pointer:
		cloned, err := clonePackComponentProjectionValue(value.Elem(), active)
		if err != nil {
			return reflect.Value{}, err
		}
		out := reflect.New(value.Type().Elem())
		out.Elem().Set(cloned)
		return out, nil
	case reflect.Slice:
		out := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
		for index := 0; index < value.Len(); index++ {
			cloned, err := clonePackComponentProjectionValue(value.Index(index), active)
			if err != nil {
				return reflect.Value{}, err
			}
			out.Index(index).Set(cloned)
		}
		return out, nil
	case reflect.Map:
		if packComponentProjectionTypeCanAlias(value.Type().Key()) {
			return reflect.Value{}, fmt.Errorf("projection map type %s has mutable keys", value.Type())
		}
		out := reflect.MakeMapWithSize(value.Type(), value.Len())
		iterator := value.MapRange()
		for iterator.Next() {
			key, err := clonePackComponentProjectionValue(iterator.Key(), active)
			if err != nil {
				return reflect.Value{}, err
			}
			item, err := clonePackComponentProjectionValue(iterator.Value(), active)
			if err != nil {
				return reflect.Value{}, err
			}
			out.SetMapIndex(key, item)
		}
		return out, nil
	case reflect.Struct:
		out := reflect.New(value.Type()).Elem()
		out.Set(value)
		for index := 0; index < value.NumField(); index++ {
			field := value.Type().Field(index)
			if field.PkgPath != "" {
				return reflect.Value{}, fmt.Errorf("projection type %s has unexported field %s", value.Type(), field.Name)
			}
			cloned, err := clonePackComponentProjectionValue(value.Field(index), active)
			if err != nil {
				return reflect.Value{}, err
			}
			out.Field(index).Set(cloned)
		}
		return out, nil
	case reflect.Array:
		out := reflect.New(value.Type()).Elem()
		for index := 0; index < value.Len(); index++ {
			cloned, err := clonePackComponentProjectionValue(value.Index(index), active)
			if err != nil {
				return reflect.Value{}, err
			}
			out.Index(index).Set(cloned)
		}
		return out, nil
	case reflect.Chan, reflect.Func, reflect.Uintptr, reflect.UnsafePointer:
		return reflect.Value{}, fmt.Errorf("projection contains unsupported mutable value of type %s", value.Type())
	default:
		return value, nil
	}
}

func packComponentProjectionTypeCanAlias(valueType reflect.Type) bool {
	switch valueType.Kind() {
	case reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice,
		reflect.Chan, reflect.Func, reflect.UnsafePointer:
		return true
	case reflect.Array:
		return packComponentProjectionTypeCanAlias(valueType.Elem())
	case reflect.Struct:
		for index := 0; index < valueType.NumField(); index++ {
			if packComponentProjectionTypeCanAlias(valueType.Field(index).Type) {
				return true
			}
		}
	}
	return false
}

func verifyPackComponentDefault(ctx context.Context, deps VerifyDeps, c PackComponent) error {
	expected, err := c.ExpectedProjection()
	if err != nil {
		return fmt.Errorf("expected projection: %w", err)
	}
	live, err := c.LiveProjection(ctx, deps)
	if err != nil {
		return fmt.Errorf("live projection: %w", err)
	}
	expectedCanonical, err := canonicalProjectionJSON(expected)
	if err != nil {
		return fmt.Errorf("canonicalize expected projection: %w", err)
	}
	liveCanonical, err := canonicalProjectionJSON(live)
	if err != nil {
		return fmt.Errorf("canonicalize live projection: %w", err)
	}
	if bytes.Equal(expectedCanonical, liveCanonical) {
		return nil
	}
	return &ProjectionDriftError{
		Expected: append([]byte(nil), expectedCanonical...),
		Live:     append([]byte(nil), liveCanonical...),
	}
}

type packPayloadHashV1Document struct {
	ManifestFormatVersion string                       `json:"manifestFormatVersion"`
	PackName              string                       `json:"packName"`
	PackVersion           string                       `json:"packVersion"`
	Category              Category                     `json:"category"`
	PlatformCompatibility json.RawMessage              `json:"platformCompatibility"`
	Dependencies          []packPayloadHashDependency  `json:"dependencies"`
	Components            []packPayloadHashV1Component `json:"components"`
}

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

type packPayloadHashV1Component struct {
	PackName      string                         `json:"packName"`
	Kind          ComponentKind                  `json:"kind"`
	BusinessKey   string                         `json:"businessKey"`
	Manageability Manageability                  `json:"manageability"`
	Payload       string                         `json:"payload"`
	DependsOn     []packPayloadHashComponentEdge `json:"dependsOn"`
}

type packPayloadHashComponentEdge struct {
	Kind        ComponentKind `json:"kind"`
	BusinessKey string        `json:"businessKey"`
}

func packComponentsPayloadHashV1(pack Pack, components []PackComponent) (string, error) {
	meta := PackHashMetadataForPack(pack)
	platformCompatibility := json.RawMessage(meta.PlatformCompatibility)
	if len(platformCompatibility) == 0 {
		platformCompatibility = json.RawMessage(BundledPlatformCompatibilityPayload)
	}
	if !json.Valid(platformCompatibility) {
		return "", fmt.Errorf("pack %s platform compatibility is not canonical JSON: %q", pack.Name(), meta.PlatformCompatibility)
	}

	ordered := append([]PackComponent(nil), components...)
	sort.SliceStable(ordered, func(i, j int) bool {
		if ordered[i].Kind() != ordered[j].Kind() {
			return ordered[i].Kind() < ordered[j].Kind()
		}
		if ordered[i].BusinessKey() != ordered[j].BusinessKey() {
			return ordered[i].BusinessKey() < ordered[j].BusinessKey()
		}
		return ordered[i].PackName() < ordered[j].PackName()
	})

	doc := packPayloadHashV1Document{
		ManifestFormatVersion: meta.ManifestFormatVersion,
		PackName:              pack.Name(),
		PackVersion:           meta.PackVersion,
		Category:              pack.Category(),
		PlatformCompatibility: platformCompatibility,
		Dependencies:          packDependenciesForHash(pack),
		Components:            make([]packPayloadHashV1Component, 0, len(ordered)),
	}

	for _, c := range ordered {
		payload, err := packComponentPayloadBytes(c)
		if err != nil {
			return "", err
		}
		canonicalPayload, err := canonicalComponentPayloadForHash(payload)
		if err != nil {
			return "", fmt.Errorf("%s %s canonical payload: %w", c.Kind(), c.BusinessKey(), err)
		}
		doc.Components = append(doc.Components, packPayloadHashV1Component{
			PackName:      c.PackName(),
			Kind:          c.Kind(),
			BusinessKey:   c.BusinessKey(),
			Manageability: c.Manageability(),
			Payload:       canonicalPayload,
			DependsOn:     componentDependsOnForHash(c),
		})
	}

	encoded, err := json.Marshal(doc)
	if err != nil {
		return "", fmt.Errorf("marshal v1 pack hash document: %w", err)
	}
	return PackPayloadHashV1Prefix + sha256Hex(encoded), nil
}

func legacyPackComponentsPayloadHashV0(components []PackComponent) (string, error) {
	ordered := append([]PackComponent(nil), components...)
	sort.SliceStable(ordered, func(i, j int) bool {
		if ordered[i].Kind() != ordered[j].Kind() {
			return ordered[i].Kind() < ordered[j].Kind()
		}
		if ordered[i].BusinessKey() != ordered[j].BusinessKey() {
			return ordered[i].BusinessKey() < ordered[j].BusinessKey()
		}
		return ordered[i].PackName() < ordered[j].PackName()
	})

	sum := sha256.New()
	for _, c := range ordered {
		payload, err := packComponentPayloadBytes(c)
		if err != nil {
			return "", fmt.Errorf("%s %s payload: %w", c.Kind(), c.BusinessKey(), err)
		}

		sum.Write([]byte(c.PackName()))
		sum.Write([]byte{0})
		sum.Write([]byte(c.Kind()))
		sum.Write([]byte{0})
		sum.Write([]byte(c.BusinessKey()))
		sum.Write([]byte{0})
		sum.Write([]byte(c.Manageability()))
		sum.Write([]byte{0})
		sum.Write(payload)
		sum.Write([]byte{'\n'})
	}
	return hex.EncodeToString(sum.Sum(nil)), nil
}

func packComponentPayloadBytes(c PackComponent) ([]byte, error) {
	payload, err := c.ExpectedPayload()
	if err != nil {
		return nil, fmt.Errorf("%s %s payload: %w", c.Kind(), c.BusinessKey(), err)
	}
	if len(payload) > 0 {
		return payload, nil
	}
	projection, err := c.ExpectedProjection()
	if err != nil {
		return nil, fmt.Errorf("%s %s projection payload: %w", c.Kind(), c.BusinessKey(), err)
	}
	payload, err = marshalComponentPayload(projection)
	if err != nil {
		return nil, fmt.Errorf("%s %s marshal payload: %w", c.Kind(), c.BusinessKey(), err)
	}
	return payload, nil
}

func canonicalComponentPayloadForHash(payload []byte) (string, error) {
	canonical, err := componentbehavior.CanonicalJSONV1(payload)
	if err == nil {
		return string(canonical), nil
	}
	if !errors.Is(err, componentbehavior.ErrNotJSON) {
		return "", fmt.Errorf("canonical component payload: %w", err)
	}
	// Not valid JSON: fall back to line-ending normalization
	normalized := strings.ReplaceAll(string(payload), "\r\n", "\n")
	normalized = strings.ReplaceAll(normalized, "\r", "\n")
	return normalized, nil
}

func componentDependsOnForHash(c PackComponent) []packPayloadHashComponentEdge {
	provider, ok := c.(ComponentDependencyProvider)
	if !ok {
		return []packPayloadHashComponentEdge{}
	}
	refs := provider.DependsOn()
	out := make([]packPayloadHashComponentEdge, 0, len(refs))
	for _, ref := range refs {
		out = append(out, packPayloadHashComponentEdge{
			Kind:        ref.Kind,
			BusinessKey: strings.TrimSpace(ref.BusinessKey),
		})
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].Kind != out[j].Kind {
			return out[i].Kind < out[j].Kind
		}
		return out[i].BusinessKey < out[j].BusinessKey
	})
	return out
}

func packDependenciesForHash(pack Pack) []packPayloadHashDependency {
	provider, ok := pack.(PackDependencyProvider)
	if !ok {
		return []packPayloadHashDependency{}
	}
	deps := provider.PackDependencies()
	out := make([]packPayloadHashDependency, 0, len(deps))
	for _, dep := range deps {
		out = append(out, packPayloadHashDependency{
			Name:              strings.TrimSpace(dep.Name),
			VersionConstraint: strings.TrimSpace(dep.VersionConstraint),
		})
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].Name != out[j].Name {
			return out[i].Name < out[j].Name
		}
		return out[i].VersionConstraint < out[j].VersionConstraint
	})
	return out
}

func marshalComponentPayload(v any) ([]byte, error) {
	out, err := json.Marshal(v)
	if err != nil {
		return nil, err
	}
	return out, nil
}
