package packs

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"io"
	"strings"

	domainschema "github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

type sourceBackedObjectAccessPolicySidecarSpec struct {
	componentName          string
	ownedPayloadField      string
	accessPolicyPath       string
	packNameField          string
	packBusinessKeyField   string
	packManageabilityField string
	packValueField         string
	getValue               func(*models.ObjectAccessPolicy) any
	setValue               func(*models.ObjectAccessPolicy, any) error
	canonicalValue         func(any) ([]byte, error)
}

type sourceBackedObjectAccessPolicySidecarRow struct {
	ObjectAPIName     string
	Value             any
	PackName          string
	PackBusinessKey   string
	PackManageability string
}

var objectRecordMutationPolicySidecarSpec = sourceBackedObjectAccessPolicySidecarSpec{
	componentName:          "object_record_mutation_policy",
	ownedPayloadField:      "recordMutation",
	accessPolicyPath:       "AccessPolicy.recordMutation",
	packNameField:          constants.FieldSysObject_AccessPolicyRecordMutationPackName,
	packBusinessKeyField:   constants.FieldSysObject_AccessPolicyRecordMutationPackBusinessKey,
	packManageabilityField: constants.FieldSysObject_AccessPolicyRecordMutationPackManageability,
	packValueField:         constants.FieldSysObject_AccessPolicyRecordMutationPackValue,
	getValue: func(policy *models.ObjectAccessPolicy) any {
		if policy == nil {
			var nilPolicy *models.RecordMutationAccessPolicy
			return nilPolicy
		}
		return policy.RecordMutation
	},
	setValue: func(policy *models.ObjectAccessPolicy, value any) error {
		typed, ok := value.(*models.RecordMutationAccessPolicy)
		if !ok && value != nil {
			return fmt.Errorf("recordMutation policy value has type %T", value)
		}
		policy.RecordMutation = typed
		return nil
	},
	canonicalValue: func(value any) ([]byte, error) {
		typed, ok := value.(*models.RecordMutationAccessPolicy)
		if !ok && value != nil {
			return nil, fmt.Errorf("recordMutation policy value has type %T", value)
		}
		return canonicalRecordMutationPolicyPackValue(typed)
	},
}

var objectLifecyclePolicySidecarSpec = sourceBackedObjectAccessPolicySidecarSpec{
	componentName:          "object_lifecycle_policy",
	ownedPayloadField:      "lifecycle",
	accessPolicyPath:       "AccessPolicy.lifecycle",
	packNameField:          constants.FieldSysObject_AccessPolicyLifecyclePackName,
	packBusinessKeyField:   constants.FieldSysObject_AccessPolicyLifecyclePackBusinessKey,
	packManageabilityField: constants.FieldSysObject_AccessPolicyLifecyclePackManageability,
	packValueField:         constants.FieldSysObject_AccessPolicyLifecyclePackValue,
	getValue: func(policy *models.ObjectAccessPolicy) any {
		if policy == nil {
			var nilPolicy *models.RecordLifecycleAccessPolicy
			return nilPolicy
		}
		return policy.Lifecycle
	},
	setValue: func(policy *models.ObjectAccessPolicy, value any) error {
		typed, ok := value.(*models.RecordLifecycleAccessPolicy)
		if !ok && value != nil {
			return fmt.Errorf("lifecycle policy value has type %T", value)
		}
		policy.Lifecycle = typed
		return nil
	},
	canonicalValue: func(value any) ([]byte, error) {
		typed, ok := value.(*models.RecordLifecycleAccessPolicy)
		if !ok && value != nil {
			return nil, fmt.Errorf("lifecycle policy value has type %T", value)
		}
		return canonicalLifecyclePolicyPackValue(typed)
	},
}

func recordMutationPolicySidecarRow(row sourceBackedObjectRecordMutationPolicyRow) sourceBackedObjectAccessPolicySidecarRow {
	return sourceBackedObjectAccessPolicySidecarRow{
		ObjectAPIName:     row.ObjectAPIName,
		Value:             row.RecordMutation,
		PackName:          row.PackName,
		PackBusinessKey:   row.PackBusinessKey,
		PackManageability: row.PackManageability,
	}
}

func recordMutationPolicyRowFromSidecar(row sourceBackedObjectAccessPolicySidecarRow) sourceBackedObjectRecordMutationPolicyRow {
	typed, _ := row.Value.(*models.RecordMutationAccessPolicy)
	return sourceBackedObjectRecordMutationPolicyRow{
		ObjectAPIName:     row.ObjectAPIName,
		RecordMutation:    typed,
		PackName:          row.PackName,
		PackBusinessKey:   row.PackBusinessKey,
		PackManageability: row.PackManageability,
	}
}

func lifecyclePolicySidecarRow(row sourceBackedObjectLifecyclePolicyRow) sourceBackedObjectAccessPolicySidecarRow {
	return sourceBackedObjectAccessPolicySidecarRow{
		ObjectAPIName:     row.ObjectAPIName,
		Value:             row.Lifecycle,
		PackName:          row.PackName,
		PackBusinessKey:   row.PackBusinessKey,
		PackManageability: row.PackManageability,
	}
}

func lifecyclePolicyRowFromSidecar(row sourceBackedObjectAccessPolicySidecarRow) sourceBackedObjectLifecyclePolicyRow {
	typed, _ := row.Value.(*models.RecordLifecycleAccessPolicy)
	return sourceBackedObjectLifecyclePolicyRow{
		ObjectAPIName:     row.ObjectAPIName,
		Lifecycle:         typed,
		PackName:          row.PackName,
		PackBusinessKey:   row.PackBusinessKey,
		PackManageability: row.PackManageability,
	}
}

func validateObjectAccessPolicySidecarPayloadFields(payload []byte, source string, spec sourceBackedObjectAccessPolicySidecarSpec) error {
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(payload, &raw); err != nil {
		return fmt.Errorf("parse %s: %w", source, err)
	}
	for key := range raw {
		switch key {
		case "objectApiName", spec.ownedPayloadField:
		default:
			return fmt.Errorf("%s %s has unsupported field %q; this component owns only %s, not the full AccessPolicy blob", source, spec.componentName, key, spec.ownedPayloadField)
		}
	}
	return nil
}

func liveSourceBackedObjectAccessPolicySidecarProjection(ctx context.Context, deps VerifyDeps, objectAPIName string, spec sourceBackedObjectAccessPolicySidecarSpec) (sourceBackedObjectAccessPolicySidecarRow, error) {
	var raw, packName, packBusinessKey, packManageability sql.NullString
	err := deps.DB.QueryRowContext(ctx, fmt.Sprintf(`
SELECT AccessPolicy, %s, %s, %s
FROM _System_Object
WHERE LOWER(ApiName) = ? AND COALESCE(IsDeleted, 0) = 0
LIMIT 1`, spec.packNameField, spec.packBusinessKeyField, spec.packManageabilityField), strings.ToLower(strings.TrimSpace(objectAPIName))).Scan(&raw, &packName, &packBusinessKey, &packManageability)
	if err == sql.ErrNoRows {
		return sourceBackedObjectAccessPolicySidecarRow{}, fmt.Errorf("object %s object %s is missing", spec.ownedPayloadField, objectAPIName)
	}
	if err != nil {
		return sourceBackedObjectAccessPolicySidecarRow{}, fmt.Errorf("query object %s policy for %s: %w", spec.ownedPayloadField, objectAPIName, err)
	}
	row := sourceBackedObjectAccessPolicySidecarRow{
		ObjectAPIName:     strings.TrimSpace(objectAPIName),
		PackName:          nullStringTrimmed(packName),
		PackBusinessKey:   nullStringTrimmed(packBusinessKey),
		PackManageability: nullStringTrimmed(packManageability),
	}
	policy, err := decodeObjectAccessPolicy(raw, objectAPIName)
	if err != nil {
		return sourceBackedObjectAccessPolicySidecarRow{}, err
	}
	row.Value = spec.getValue(policy)
	return row, nil
}

func applySourceBackedObjectAccessPolicySidecar(ctx context.Context, db *sql.DB, row sourceBackedObjectAccessPolicySidecarRow, spec sourceBackedObjectAccessPolicySidecarSpec) error {
	if db == nil {
		return fmt.Errorf("db is unavailable for %s %s", spec.componentName, row.ObjectAPIName)
	}
	key := strings.ToLower(strings.TrimSpace(row.ObjectAPIName))
	var id string
	var raw, livePackName, livePackBusinessKey sql.NullString
	if err := db.QueryRowContext(ctx, fmt.Sprintf(`
SELECT Id, AccessPolicy, %s, %s
FROM _System_Object
WHERE LOWER(ApiName) = ? AND COALESCE(IsDeleted, 0) = 0
LIMIT 1`, spec.packNameField, spec.packBusinessKeyField), key).Scan(&id, &raw, &livePackName, &livePackBusinessKey); err == sql.ErrNoRows {
		return fmt.Errorf("%s target object %s is missing", spec.componentName, row.ObjectAPIName)
	} else if err != nil {
		return fmt.Errorf("query %s target %s: %w", spec.componentName, row.ObjectAPIName, err)
	}
	if err := validateObjectAccessPolicySidecarOwnership(row, livePackName, livePackBusinessKey, spec); err != nil {
		return err
	}
	policy, err := decodeObjectAccessPolicy(raw, row.ObjectAPIName)
	if err != nil {
		return err
	}
	if err := spec.setValue(policy, row.Value); err != nil {
		return err
	}
	accessPolicyJSON, err := encodeObjectAccessPolicy(policy)
	if err != nil {
		return fmt.Errorf("marshal object %s policy for %s: %w", spec.ownedPayloadField, row.ObjectAPIName, err)
	}
	packValueJSON, err := spec.canonicalValue(row.Value)
	if err != nil {
		return fmt.Errorf("marshal object %s pack value for %s: %w", spec.ownedPayloadField, row.ObjectAPIName, err)
	}
	result, err := db.ExecContext(ctx,
		fmt.Sprintf(
			"UPDATE `%s` SET `%s` = CAST(? AS JSON), `%s` = ?, `%s` = ?, `%s` = ?, `%s` = CAST(? AS JSON), `%s` = NOW() "+
				"WHERE LOWER(`%s`) = ? AND COALESCE(`%s`, 0) = 0 "+
				"AND ((COALESCE(`%s`, '') = '' AND COALESCE(`%s`, '') = '') OR (`%s` = ? AND `%s` = ?))",
			constants.TableObject,
			constants.FieldSysObject_AccessPolicy,
			spec.packNameField,
			spec.packBusinessKeyField,
			spec.packManageabilityField,
			spec.packValueField,
			constants.FieldLastModifiedDate,
			constants.FieldSysObject_APIName,
			constants.FieldIsDeleted,
			spec.packNameField,
			spec.packBusinessKeyField,
			spec.packNameField,
			spec.packBusinessKeyField,
		),
		string(accessPolicyJSON),
		row.PackName,
		row.PackBusinessKey,
		row.PackManageability,
		string(packValueJSON),
		key,
		row.PackName,
		row.PackBusinessKey,
	)
	if err != nil {
		return fmt.Errorf("update object %s policy for %s: %w", spec.ownedPayloadField, row.ObjectAPIName, err)
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("update object %s policy for %s: rows affected: %w", spec.ownedPayloadField, row.ObjectAPIName, err)
	}
	if affected != 1 {
		if affected == 0 {
			converged, convergenceErr := sourceBackedObjectAccessPolicySidecarAlreadyConverged(ctx, db, row, spec)
			if convergenceErr != nil {
				return fmt.Errorf("update object %s policy for %s affected 0 rows and convergence check failed: %w", spec.ownedPayloadField, row.ObjectAPIName, convergenceErr)
			}
			if converged {
				return nil
			}
		}
		return fmt.Errorf("update object %s policy for %s affected %d rows; expected exactly one owned object row", spec.ownedPayloadField, row.ObjectAPIName, affected)
	}
	return nil
}

func sourceBackedObjectAccessPolicySidecarAlreadyConverged(ctx context.Context, db *sql.DB, expected sourceBackedObjectAccessPolicySidecarRow, spec sourceBackedObjectAccessPolicySidecarSpec) (bool, error) {
	var raw, packName, packBusinessKey, packManageability, packValue sql.NullString
	err := db.QueryRowContext(ctx, fmt.Sprintf(
		"SELECT `%s`, `%s`, `%s`, `%s`, `%s` "+
			"FROM `%s` "+
			"WHERE LOWER(`%s`) = ? AND COALESCE(`%s`, 0) = 0 "+
			"LIMIT 1",
		constants.FieldSysObject_AccessPolicy,
		spec.packNameField,
		spec.packBusinessKeyField,
		spec.packManageabilityField,
		spec.packValueField,
		constants.TableObject,
		constants.FieldSysObject_APIName,
		constants.FieldIsDeleted,
	), strings.ToLower(strings.TrimSpace(expected.ObjectAPIName))).Scan(&raw, &packName, &packBusinessKey, &packManageability, &packValue)
	if err == sql.ErrNoRows {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("query object %s convergence for %s: %w", spec.ownedPayloadField, expected.ObjectAPIName, err)
	}
	live := sourceBackedObjectAccessPolicySidecarRow{
		ObjectAPIName:     strings.TrimSpace(expected.ObjectAPIName),
		PackName:          nullStringTrimmed(packName),
		PackBusinessKey:   nullStringTrimmed(packBusinessKey),
		PackManageability: nullStringTrimmed(packManageability),
	}
	policy, err := decodeObjectAccessPolicy(raw, expected.ObjectAPIName)
	if err != nil {
		return false, err
	}
	live.Value = spec.getValue(policy)
	if !sourceBackedPolicyProjectionEqual(expected, live) {
		return false, nil
	}
	expectedPackValue, err := spec.canonicalValue(expected.Value)
	if err != nil {
		return false, fmt.Errorf("canonicalize object %s pack value for %s: %w", spec.ownedPayloadField, expected.ObjectAPIName, err)
	}
	livePackValue, err := canonicalPackSidecarJSON(packValue, fmt.Sprintf("%s %s", expected.ObjectAPIName, spec.packValueField))
	if err != nil {
		return false, err
	}
	return bytes.Equal(expectedPackValue, livePackValue), nil
}

func validateObjectAccessPolicySidecarOwnership(row sourceBackedObjectAccessPolicySidecarRow, livePackName, livePackBusinessKey sql.NullString, spec sourceBackedObjectAccessPolicySidecarSpec) error {
	liveName := nullStringTrimmed(livePackName)
	liveKey := nullStringTrimmed(livePackBusinessKey)
	if liveName == "" && liveKey == "" {
		return nil
	}
	if liveName == strings.TrimSpace(row.PackName) && liveKey == strings.TrimSpace(row.PackBusinessKey) {
		return nil
	}
	return fmt.Errorf("object %s %s already owned by pack %s component %s; refusing to apply pack %s component %s",
		row.ObjectAPIName, spec.accessPolicyPath, liveName, liveKey, row.PackName, row.PackBusinessKey)
}

func newSourceBackedObjectRecordMutationPolicyComponent(packName string, component ManifestComponent, payload []byte, objectSpecIndex map[string]sourceBackedObjectSpecTarget) (PackComponent, error) {
	expected, err := sourceBackedObjectRecordMutationPolicyProjection(payload, componentPayloadLabel(component))
	if err != nil {
		return nil, err
	}
	businessKey := strings.TrimSpace(component.BusinessKey)
	if businessKey == "" {
		return nil, fmt.Errorf("%s object_record_mutation_policy businessKey is required", componentPayloadLabel(component))
	}
	if !strings.EqualFold(businessKey, expected.ObjectAPIName) {
		return nil, fmt.Errorf("%s object_record_mutation_policy businessKey %q targets %s; businessKey must match objectApiName", componentPayloadLabel(component), businessKey, expected.ObjectAPIName)
	}
	if target, ok := objectSpecIndex[strings.ToLower(expected.ObjectAPIName)]; ok {
		if !hasComponentDependency(component.DependsOn, ComponentKindObjectSpec, target.businessKey) {
			return nil, fmt.Errorf("object_record_mutation_policy for %s requires dependsOn on object_spec %q", expected.ObjectAPIName, target.businessKey)
		}
		if target.hasRawRecordMutation {
			return nil, fmt.Errorf("object_record_mutation_policy for %s conflicts with object_spec.accessPolicy.recordMutation in object_spec %q; author recordMutation through exactly one pack surface", expected.ObjectAPIName, target.businessKey)
		}
	}
	expected.PackName = strings.TrimSpace(packName)
	expected.PackBusinessKey = businessKey
	expected.PackManageability = string(component.Manageability)
	return &sourceBackedManagedComponent{
		packName:          packName,
		manifestComponent: component,
		payload:           payload,
		expectedProjection: func() (any, error) {
			return expected, nil
		},
		liveProjection: func(ctx context.Context, deps VerifyDeps) (any, error) {
			return liveSourceBackedObjectRecordMutationPolicyProjection(ctx, deps, expected.ObjectAPIName)
		},
		apply: func(ctx context.Context, deps ApplyDeps) error {
			return applySourceBackedObjectRecordMutationPolicy(ctx, deps.DB, expected)
		},
	}, nil
}

type sourceBackedObjectRecordMutationPolicyRow struct {
	ObjectAPIName     string                             `json:"objectApiName"`
	RecordMutation    *models.RecordMutationAccessPolicy `json:"recordMutation"`
	PackName          string                             `json:"packName,omitempty"`
	PackBusinessKey   string                             `json:"packBusinessKey,omitempty"`
	PackManageability string                             `json:"packManageability,omitempty"`
}

func sourceBackedObjectRecordMutationPolicyProjection(payload []byte, source string) (sourceBackedObjectRecordMutationPolicyRow, error) {
	if err := validateObjectAccessPolicySidecarPayloadFields(payload, source, objectRecordMutationPolicySidecarSpec); err != nil {
		return sourceBackedObjectRecordMutationPolicyRow{}, err
	}
	var row sourceBackedObjectRecordMutationPolicyRow
	if err := json.Unmarshal(payload, &row); err != nil {
		return sourceBackedObjectRecordMutationPolicyRow{}, fmt.Errorf("parse %s: %w", source, err)
	}
	row.ObjectAPIName = strings.TrimSpace(row.ObjectAPIName)
	if row.ObjectAPIName == "" {
		return sourceBackedObjectRecordMutationPolicyRow{}, fmt.Errorf("%s object_record_mutation_policy requires objectApiName", source)
	}
	if row.RecordMutation == nil {
		return sourceBackedObjectRecordMutationPolicyRow{}, fmt.Errorf("%s object_record_mutation_policy for %s requires recordMutation", source, row.ObjectAPIName)
	}
	return row, nil
}

func liveSourceBackedObjectRecordMutationPolicyProjection(ctx context.Context, deps VerifyDeps, objectAPIName string) (any, error) {
	sidecar, err := liveSourceBackedObjectAccessPolicySidecarProjection(ctx, deps, objectAPIName, objectRecordMutationPolicySidecarSpec)
	if err != nil {
		return nil, err
	}
	return recordMutationPolicyRowFromSidecar(sidecar), nil
}

func applySourceBackedObjectRecordMutationPolicy(ctx context.Context, db *sql.DB, row sourceBackedObjectRecordMutationPolicyRow) error {
	return applySourceBackedObjectAccessPolicySidecar(ctx, db, recordMutationPolicySidecarRow(row), objectRecordMutationPolicySidecarSpec)
}

func newSourceBackedObjectLifecyclePolicyComponent(packName string, component ManifestComponent, payload []byte, objectSpecIndex map[string]sourceBackedObjectSpecTarget) (PackComponent, error) {
	expected, err := sourceBackedObjectLifecyclePolicyProjection(payload, componentPayloadLabel(component))
	if err != nil {
		return nil, err
	}
	businessKey := strings.TrimSpace(component.BusinessKey)
	if businessKey == "" {
		return nil, fmt.Errorf("%s object_lifecycle_policy businessKey is required", componentPayloadLabel(component))
	}
	if !strings.EqualFold(businessKey, expected.ObjectAPIName) {
		return nil, fmt.Errorf("%s object_lifecycle_policy businessKey %q targets %s; businessKey must match objectApiName", componentPayloadLabel(component), businessKey, expected.ObjectAPIName)
	}
	if target, ok := objectSpecIndex[strings.ToLower(expected.ObjectAPIName)]; ok {
		if !hasComponentDependency(component.DependsOn, ComponentKindObjectSpec, target.businessKey) {
			return nil, fmt.Errorf("object_lifecycle_policy for %s requires dependsOn on object_spec %q", expected.ObjectAPIName, target.businessKey)
		}
		if target.hasRawLifecycle {
			return nil, fmt.Errorf("object_lifecycle_policy for %s conflicts with object_spec.accessPolicy.lifecycle in object_spec %q; author lifecycle through exactly one pack surface", expected.ObjectAPIName, target.businessKey)
		}
	}
	expected.PackName = strings.TrimSpace(packName)
	expected.PackBusinessKey = businessKey
	expected.PackManageability = string(component.Manageability)
	return &sourceBackedManagedComponent{
		packName:          packName,
		manifestComponent: component,
		payload:           payload,
		expectedProjection: func() (any, error) {
			return expected, nil
		},
		liveProjection: func(ctx context.Context, deps VerifyDeps) (any, error) {
			return liveSourceBackedObjectLifecyclePolicyProjection(ctx, deps, expected.ObjectAPIName)
		},
		apply: func(ctx context.Context, deps ApplyDeps) error {
			return applySourceBackedObjectLifecyclePolicy(ctx, deps.DB, expected)
		},
	}, nil
}

type sourceBackedObjectLifecyclePolicyRow struct {
	ObjectAPIName     string                              `json:"objectApiName"`
	Lifecycle         *models.RecordLifecycleAccessPolicy `json:"lifecycle"`
	PackName          string                              `json:"packName,omitempty"`
	PackBusinessKey   string                              `json:"packBusinessKey,omitempty"`
	PackManageability string                              `json:"packManageability,omitempty"`
}

func sourceBackedObjectLifecyclePolicyProjection(payload []byte, source string) (sourceBackedObjectLifecyclePolicyRow, error) {
	if err := validateObjectAccessPolicySidecarPayloadFields(payload, source, objectLifecyclePolicySidecarSpec); err != nil {
		return sourceBackedObjectLifecyclePolicyRow{}, err
	}
	var row sourceBackedObjectLifecyclePolicyRow
	if err := json.Unmarshal(payload, &row); err != nil {
		return sourceBackedObjectLifecyclePolicyRow{}, fmt.Errorf("parse %s: %w", source, err)
	}
	row.ObjectAPIName = strings.TrimSpace(row.ObjectAPIName)
	if row.ObjectAPIName == "" {
		return sourceBackedObjectLifecyclePolicyRow{}, fmt.Errorf("%s object_lifecycle_policy requires objectApiName", source)
	}
	if row.Lifecycle == nil {
		return sourceBackedObjectLifecyclePolicyRow{}, fmt.Errorf("%s object_lifecycle_policy for %s requires lifecycle", source, row.ObjectAPIName)
	}
	return row, nil
}

func liveSourceBackedObjectLifecyclePolicyProjection(ctx context.Context, deps VerifyDeps, objectAPIName string) (any, error) {
	sidecar, err := liveSourceBackedObjectAccessPolicySidecarProjection(ctx, deps, objectAPIName, objectLifecyclePolicySidecarSpec)
	if err != nil {
		return nil, err
	}
	return lifecyclePolicyRowFromSidecar(sidecar), nil
}

func applySourceBackedObjectLifecyclePolicy(ctx context.Context, db *sql.DB, row sourceBackedObjectLifecyclePolicyRow) error {
	return applySourceBackedObjectAccessPolicySidecar(ctx, db, lifecyclePolicySidecarRow(row), objectLifecyclePolicySidecarSpec)
}

func decodeObjectAccessPolicy(raw sql.NullString, objectAPIName string) (*models.ObjectAccessPolicy, error) {
	if !raw.Valid || strings.TrimSpace(raw.String) == "" {
		return &models.ObjectAccessPolicy{}, nil
	}
	var policy models.ObjectAccessPolicy
	if err := json.Unmarshal([]byte(raw.String), &policy); err != nil {
		return nil, fmt.Errorf("%s AccessPolicy is not valid JSON: %w", objectAPIName, err)
	}
	return &policy, nil
}

func encodeObjectAccessPolicy(policy *models.ObjectAccessPolicy) ([]byte, error) {
	if domainschema.ObjectAccessPolicyIsEmpty(policy) {
		return []byte("null"), nil
	}
	return json.Marshal(policy)
}

func canonicalRecordMutationPolicyPackValue(policy *models.RecordMutationAccessPolicy) ([]byte, error) {
	if policy == nil {
		return []byte("null"), nil
	}
	return canonicalProjectionJSON(policy)
}

func canonicalLifecyclePolicyPackValue(policy *models.RecordLifecycleAccessPolicy) ([]byte, error) {
	if policy == nil {
		return []byte("null"), nil
	}
	return canonicalProjectionJSON(policy)
}

func sourceBackedPolicyProjectionEqual(expected, live any) bool {
	expectedCanonical, err := canonicalProjectionJSON(expected)
	if err != nil {
		return false
	}
	liveCanonical, err := canonicalProjectionJSON(live)
	if err != nil {
		return false
	}
	return bytes.Equal(expectedCanonical, liveCanonical)
}

func canonicalPackSidecarJSON(raw sql.NullString, label string) ([]byte, error) {
	if !raw.Valid || strings.TrimSpace(raw.String) == "" {
		return []byte("null"), nil
	}
	if err := rejectDuplicateJSONKeys([]byte(raw.String), label); err != nil {
		return nil, fmt.Errorf("%s contains duplicate JSON keys: %w", label, err)
	}
	dec := json.NewDecoder(strings.NewReader(raw.String))
	dec.UseNumber()
	var decoded any
	if err := dec.Decode(&decoded); err != nil {
		return nil, fmt.Errorf("%s is not valid JSON: %w", label, err)
	}
	var extra any
	if err := dec.Decode(&extra); err != io.EOF {
		if err == nil {
			return nil, fmt.Errorf("%s has trailing JSON content", label)
		}
		return nil, fmt.Errorf("%s has trailing JSON content", label)
	}
	return json.Marshal(decoded)
}

func nullStringTrimmed(v sql.NullString) string {
	if !v.Valid {
		return ""
	}
	return strings.TrimSpace(v.String)
}
