package pipeline

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sort"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/core/source"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)

type CheckpointFingerprint struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

// CheckpointFingerprintProvider supplies stable, non-secret semantic identity
// for a prepared module or validator. Values must not contain source content,
// credentials, local paths, timestamps, or other invocation-specific data.
type CheckpointFingerprintProvider interface {
	CheckpointFingerprints() []CheckpointFingerprint
}

type CheckpointRecorder interface {
	SourceRunning(moduleKey string) error
	SourceSucceeded(moduleKey string, doc *source.SourceDocument) error
	SourceFailed(moduleKey string, err error) error
	ExtractRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
	ExtractSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error
	ExtractFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
	MergeRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
	MergeSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact) error
	MergeRejected(laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
	MergeFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
	NormalizeRunning(laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
	NormalizeSucceeded(laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact) error
	NormalizeRejected(laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
	NormalizeFailed(laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
}

// StepCheckpointRecorder is implemented by checkpoint stores that isolate
// lane artifacts by their ordered pipeline step. The legacy recorder methods
// remain available for callers that do not have step context.
type StepCheckpointRecorder interface {
	ExtractRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
	ExtractSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error
	ExtractFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
	MergeRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
	MergeSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact) error
	MergeRejectedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
	MergeFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
	NormalizeRunningForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) error
	NormalizeSucceededForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, output CheckpointArtifact) error
	NormalizeRejectedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, rejected contracts.RejectedOutput) error
	NormalizeFailedForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint, err error) error
}

type CheckpointDecisionCategory string

const (
	CheckpointDecisionExecuted              CheckpointDecisionCategory = "executed"
	CheckpointDecisionReused                CheckpointDecisionCategory = "reused"
	CheckpointDecisionForcedRecompute       CheckpointDecisionCategory = "forced_recompute"
	CheckpointDecisionDependencyInvalidated CheckpointDecisionCategory = "dependency_invalidated"
)

type CheckpointReasonCode string

const (
	CheckpointReasonLoadingDisabled             CheckpointReasonCode = "loading_disabled"
	CheckpointReasonMissing                     CheckpointReasonCode = "checkpoint_missing"
	CheckpointReasonPathInvalid                 CheckpointReasonCode = "checkpoint_path_invalid"
	CheckpointReasonReadFailed                  CheckpointReasonCode = "checkpoint_read_failed"
	CheckpointReasonDecodeFailed                CheckpointReasonCode = "checkpoint_decode_failed"
	CheckpointReasonWorkspaceSchemaIncompatible CheckpointReasonCode = "workspace_schema_incompatible"
	CheckpointReasonIdentityMismatch            CheckpointReasonCode = "identity_mismatch"
	CheckpointReasonStageMismatch               CheckpointReasonCode = "stage_mismatch"
	CheckpointReasonStepMismatch                CheckpointReasonCode = "step_mismatch"
	CheckpointReasonLaneMismatch                CheckpointReasonCode = "lane_mismatch"
	CheckpointReasonModuleMismatch              CheckpointReasonCode = "module_mismatch"
	CheckpointReasonStatusNotReusable           CheckpointReasonCode = "status_not_reusable"
	CheckpointReasonDependencyMismatch          CheckpointReasonCode = "dependency_mismatch"
	CheckpointReasonArtifactPayloadInvalid      CheckpointReasonCode = "artifact_payload_invalid"
	CheckpointReasonArtifactDigestMismatch      CheckpointReasonCode = "artifact_digest_mismatch"
	CheckpointReasonArtifactCodecIncompatible   CheckpointReasonCode = "artifact_codec_incompatible"
	CheckpointReasonArtifactNotCanonical        CheckpointReasonCode = "artifact_not_canonical"
	CheckpointReasonReused                      CheckpointReasonCode = "checkpoint_reused"
	CheckpointReasonAcceptedArtifactReused      CheckpointReasonCode = "accepted_artifact_reused"
	CheckpointReasonRecomputeStep               CheckpointReasonCode = "recompute_step"
	CheckpointReasonValidationIncompleteLineage CheckpointReasonCode = "validation_incomplete_lineage"
)

type CheckpointDecision struct {
	Reused     bool                       `json:"reused"`
	Category   CheckpointDecisionCategory `json:"category,omitempty"`
	ReasonCode CheckpointReasonCode       `json:"reason_code,omitempty"`
	Detail     string                     `json:"detail,omitempty"`
	// Reason is retained as a compatibility/debug field for existing callers.
	// Detail and Reason are derived from the stable reason code.
	Reason string `json:"reason,omitempty"`
}

const checkpointDecisionDetailLimit = 512

func NewCheckpointDecision(category CheckpointDecisionCategory, reasonCode CheckpointReasonCode) CheckpointDecision {
	return checkpointDecision(category, reasonCode)
}

func checkpointDecision(category CheckpointDecisionCategory, reasonCode CheckpointReasonCode) CheckpointDecision {
	detail := normalizeCheckpointDecisionDetail(checkpointDecisionDetail(reasonCode))
	return CheckpointDecision{
		Reused:     category == CheckpointDecisionReused,
		Category:   category,
		ReasonCode: reasonCode,
		Detail:     detail,
		Reason:     detail,
	}
}

func checkpointDecisionDetail(reasonCode CheckpointReasonCode) string {
	switch reasonCode {
	case CheckpointReasonLoadingDisabled:
		return "checkpoint loading is disabled"
	case CheckpointReasonMissing:
		return "checkpoint artifact is missing"
	case CheckpointReasonPathInvalid:
		return "checkpoint location is invalid"
	case CheckpointReasonReadFailed:
		return "checkpoint artifact could not be read"
	case CheckpointReasonDecodeFailed:
		return "checkpoint artifact could not be decoded"
	case CheckpointReasonWorkspaceSchemaIncompatible:
		return "checkpoint workspace schema is incompatible"
	case CheckpointReasonIdentityMismatch:
		return "checkpoint identity does not match the current invocation"
	case CheckpointReasonStageMismatch:
		return "checkpoint stage does not match"
	case CheckpointReasonStepMismatch:
		return "checkpoint step does not match"
	case CheckpointReasonLaneMismatch:
		return "checkpoint lane does not match"
	case CheckpointReasonModuleMismatch:
		return "checkpoint module does not match"
	case CheckpointReasonStatusNotReusable:
		return "checkpoint status is not reusable"
	case CheckpointReasonDependencyMismatch:
		return "checkpoint dependencies do not match"
	case CheckpointReasonArtifactPayloadInvalid:
		return "checkpoint artifact payload is invalid"
	case CheckpointReasonArtifactDigestMismatch:
		return "checkpoint artifact digest does not match"
	case CheckpointReasonArtifactCodecIncompatible:
		return "checkpoint artifact is incompatible with the registered codec"
	case CheckpointReasonArtifactNotCanonical:
		return "checkpoint artifact is not canonical"
	case CheckpointReasonReused:
		return "checkpoint is reusable"
	case CheckpointReasonAcceptedArtifactReused:
		return "accepted normalized artifact is reusable"
	case CheckpointReasonRecomputeStep:
		return "selected step requires execution"
	case CheckpointReasonValidationIncompleteLineage:
		return "checkpoint reuse is disabled by validation-incomplete input lineage"
	default:
		return "checkpoint decision"
	}
}

func normalizeCheckpointDecisionDetail(detail string) string {
	detail = strings.TrimSpace(strings.ToValidUTF8(detail, "?"))
	var b strings.Builder
	for _, r := range detail {
		if r < 0x20 || r == 0x7f {
			r = ' '
		}
		if b.Len()+len(string(r)) > checkpointDecisionDetailLimit {
			break
		}
		b.WriteRune(r)
	}
	return strings.Join(strings.Fields(b.String()), " ")
}

type CheckpointEvent struct {
	Stage      string                     `json:"stage"`
	StepID     string                     `json:"step_id,omitempty"`
	LaneID     string                     `json:"lane_id,omitempty"`
	ModuleKey  string                     `json:"module_key,omitempty"`
	Action     CheckpointDecisionCategory `json:"action"`
	Category   CheckpointDecisionCategory `json:"category,omitempty"`
	ReasonCode CheckpointReasonCode       `json:"reason_code,omitempty"`
	Detail     string                     `json:"detail,omitempty"`
	Reason     string                     `json:"reason,omitempty"`
}

type CheckpointExecutionPolicy struct {
	ForcedLanes          map[string]struct{}
	RequireReusableLanes map[string]struct{}
}

func CheckpointLaneKey(stepID, laneID string) string {
	return strings.TrimSpace(stepID) + "\x00" + strings.TrimSpace(laneID)
}

func (policy CheckpointExecutionPolicy) forced(stepID, laneID string) bool {
	_, ok := policy.ForcedLanes[CheckpointLaneKey(stepID, laneID)]
	return ok
}

func (policy CheckpointExecutionPolicy) requiresReusable(stepID, laneID string) bool {
	_, ok := policy.RequireReusableLanes[CheckpointLaneKey(stepID, laneID)]
	return ok
}

func forceCheckpointDecision(policy CheckpointExecutionPolicy, stepID, laneID string, decision CheckpointDecision) CheckpointDecision {
	if policy.forced(stepID, laneID) {
		return checkpointDecision(CheckpointDecisionForcedRecompute, CheckpointReasonRecomputeStep)
	}
	return decision
}

func requireReusableCheckpoint(policy CheckpointExecutionPolicy, stepID, laneID string, decision CheckpointDecision) error {
	if policy.requiresReusable(stepID, laneID) && !policy.forced(stepID, laneID) && !decision.Reused {
		return fmt.Errorf("required reusable checkpoint unavailable for step %q lane %q (%s)", strings.TrimSpace(stepID), strings.TrimSpace(laneID), decision.ReasonCode)
	}
	return nil
}

// resolveCheckpointDecision applies runner policy and canonical payload
// validation at the single point where a stage's observable decision is made.
type checkpointResolution struct {
	decision  CheckpointDecision
	values    []any
	artifacts []CheckpointArtifact
}

func resolveCheckpointDecision(output *RunOutput, loader CheckpointLoader, policy CheckpointExecutionPolicy, stage ModuleStage, stepID, laneID, moduleKey string, decision CheckpointDecision, codec artifactCodecEntry, artifacts []CheckpointArtifact) (checkpointResolution, error) {
	decision = forceCheckpointDecision(policy, stepID, laneID, decision)
	resolution := checkpointResolution{decision: decision}
	if decision.Reused {
		resolution.values = make([]any, 0, len(artifacts))
		resolution.artifacts = make([]CheckpointArtifact, 0, len(artifacts))
		for _, artifact := range artifacts {
			value, hydrated, err := decodeCanonicalCheckpointArtifact(codec, artifact)
			if err != nil {
				decision = checkpointDecision(CheckpointDecisionExecuted, checkpointArtifactReasonCode(err))
				resolution.values = nil
				resolution.artifacts = nil
				break
			}
			resolution.values = append(resolution.values, value)
			resolution.artifacts = append(resolution.artifacts, hydrated)
		}
	}
	resolution.decision = decision
	if output != nil {
		recordCheckpointEvent(output, loader, string(stage), stepID, laneID, moduleKey, decision)
	}
	if err := requireReusableCheckpoint(policy, stepID, laneID, decision); err != nil {
		return resolution, err
	}
	return resolution, nil
}

type SourceCheckpoint struct {
	Document *source.SourceDocument
}

// CheckpointArtifact is the durable, domain-neutral value stored at a lane
// checkpoint boundary.
type CheckpointArtifact struct {
	LaneID       string
	ModuleKey    string
	SourceID     string
	ChunkID      string
	ChunkIndex   int
	ChunkRef     source.SourceRef
	Artifact     contracts.SerializedArtifact
	SchemaDigest string
	Diagnostics  []CheckpointDiagnostic
}

// CheckpointDiagnostic stores a producer-local diagnostic alongside a
// reusable artifact. The runner supplies the current run's origin when it
// promotes this value into a diagnostic group.
type CheckpointDiagnostic struct {
	Diagnostic   contracts.ProducerDiagnostic `json:"diagnostic"`
	ValidatorKey string                       `json:"validator_key,omitempty"`
}

type ExtractCheckpoint struct {
	Outputs  []CheckpointArtifact
	Rejected []contracts.RejectedOutput
}

type MergeCheckpoint struct {
	Output CheckpointArtifact
}
type NormalizeCheckpoint struct {
	Output CheckpointArtifact
}

type CheckpointLoader interface {
	Enabled() bool
	Source(moduleKey string) (SourceCheckpoint, CheckpointDecision)
	Extract(laneID string, moduleKey string, dependencies []CheckpointFingerprint) (ExtractCheckpoint, CheckpointDecision)
	Merge(laneID string, moduleKey string, dependencies []CheckpointFingerprint) (MergeCheckpoint, CheckpointDecision)
	Normalize(laneID string, moduleKey string, dependencies []CheckpointFingerprint) (NormalizeCheckpoint, CheckpointDecision)
	AcceptedNormalize(stepID, laneID, moduleKey string) (NormalizeCheckpoint, CheckpointDecision)
}

// StepCheckpointLoader is the step-aware counterpart used by the persistent
// checkpoint implementation. Loaders without this optional interface remain
// usable by framework callers and test doubles through the legacy methods.
type StepCheckpointLoader interface {
	ExtractForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) (ExtractCheckpoint, CheckpointDecision)
	MergeForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) (MergeCheckpoint, CheckpointDecision)
	NormalizeForStep(stepID, laneID string, moduleKey string, dependencies []CheckpointFingerprint) (NormalizeCheckpoint, CheckpointDecision)
}

type noopCheckpointRecorder struct{}
type noopCheckpointLoader struct{}

func NoopCheckpointRecorder() CheckpointRecorder { return noopCheckpointRecorder{} }
func NoopCheckpointLoader() CheckpointLoader     { return noopCheckpointLoader{} }

func (noopCheckpointRecorder) SourceRunning(string) error                           { return nil }
func (noopCheckpointRecorder) SourceSucceeded(string, *source.SourceDocument) error { return nil }
func (noopCheckpointRecorder) SourceFailed(string, error) error                     { return nil }
func (noopCheckpointRecorder) ExtractRunning(string, string, []CheckpointFingerprint) error {
	return nil
}
func (noopCheckpointRecorder) ExtractSucceeded(string, string, []CheckpointFingerprint, []CheckpointArtifact, []contracts.RejectedOutput) error {
	return nil
}
func (noopCheckpointRecorder) ExtractFailed(string, string, []CheckpointFingerprint, error) error {
	return nil
}
func (noopCheckpointRecorder) MergeRunning(string, string, []CheckpointFingerprint) error { return nil }
func (noopCheckpointRecorder) MergeSucceeded(string, string, []CheckpointFingerprint, CheckpointArtifact) error {
	return nil
}
func (noopCheckpointRecorder) MergeRejected(string, string, []CheckpointFingerprint, contracts.RejectedOutput) error {
	return nil
}
func (noopCheckpointRecorder) MergeFailed(string, string, []CheckpointFingerprint, error) error {
	return nil
}
func (noopCheckpointRecorder) NormalizeRunning(string, string, []CheckpointFingerprint) error {
	return nil
}
func (noopCheckpointRecorder) NormalizeSucceeded(string, string, []CheckpointFingerprint, CheckpointArtifact) error {
	return nil
}
func (noopCheckpointRecorder) NormalizeRejected(string, string, []CheckpointFingerprint, contracts.RejectedOutput) error {
	return nil
}
func (noopCheckpointRecorder) NormalizeFailed(string, string, []CheckpointFingerprint, error) error {
	return nil
}

func (noopCheckpointLoader) Enabled() bool { return false }
func (noopCheckpointLoader) Source(string) (SourceCheckpoint, CheckpointDecision) {
	return SourceCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
}
func (noopCheckpointLoader) Extract(string, string, []CheckpointFingerprint) (ExtractCheckpoint, CheckpointDecision) {
	return ExtractCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
}
func (noopCheckpointLoader) Merge(string, string, []CheckpointFingerprint) (MergeCheckpoint, CheckpointDecision) {
	return MergeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
}
func (noopCheckpointLoader) Normalize(string, string, []CheckpointFingerprint) (NormalizeCheckpoint, CheckpointDecision) {
	return NormalizeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
}
func (noopCheckpointLoader) AcceptedNormalize(string, string, string) (NormalizeCheckpoint, CheckpointDecision) {
	return NormalizeCheckpoint{}, checkpointDecision(CheckpointDecisionExecuted, CheckpointReasonLoadingDisabled)
}

func checkpointExtractRunning(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.ExtractRunningForStep(stepID, laneID, moduleKey, deps)
	}
	return recorder.ExtractRunning(laneID, moduleKey, deps)
}
func checkpointExtractSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, outputs []CheckpointArtifact, rejected []contracts.RejectedOutput) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.ExtractSucceededForStep(stepID, laneID, moduleKey, deps, outputs, rejected)
	}
	return recorder.ExtractSucceeded(laneID, moduleKey, deps, outputs, rejected)
}
func checkpointExtractFailed(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, err error) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.ExtractFailedForStep(stepID, laneID, moduleKey, deps, err)
	}
	return recorder.ExtractFailed(laneID, moduleKey, deps, err)
}
func checkpointMergeRunning(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.MergeRunningForStep(stepID, laneID, moduleKey, deps)
	}
	return recorder.MergeRunning(laneID, moduleKey, deps)
}
func checkpointMergeSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.MergeSucceededForStep(stepID, laneID, moduleKey, deps, output)
	}
	return recorder.MergeSucceeded(laneID, moduleKey, deps, output)
}
func checkpointMergeRejected(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.MergeRejectedForStep(stepID, laneID, moduleKey, deps, rejected)
	}
	return recorder.MergeRejected(laneID, moduleKey, deps, rejected)
}
func checkpointMergeFailed(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, err error) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.MergeFailedForStep(stepID, laneID, moduleKey, deps, err)
	}
	return recorder.MergeFailed(laneID, moduleKey, deps, err)
}
func checkpointNormalizeRunning(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.NormalizeRunningForStep(stepID, laneID, moduleKey, deps)
	}
	return recorder.NormalizeRunning(laneID, moduleKey, deps)
}
func checkpointNormalizeSucceeded(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.NormalizeSucceededForStep(stepID, laneID, moduleKey, deps, output)
	}
	return recorder.NormalizeSucceeded(laneID, moduleKey, deps, output)
}
func checkpointNormalizeRejected(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, rejected contracts.RejectedOutput) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.NormalizeRejectedForStep(stepID, laneID, moduleKey, deps, rejected)
	}
	return recorder.NormalizeRejected(laneID, moduleKey, deps, rejected)
}
func checkpointNormalizeFailed(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, err error) error {
	if stepAware, ok := recorder.(StepCheckpointRecorder); ok {
		return stepAware.NormalizeFailedForStep(stepID, laneID, moduleKey, deps, err)
	}
	return recorder.NormalizeFailed(laneID, moduleKey, deps, err)
}

func digestFingerprints(name string, digest string) []CheckpointFingerprint {
	digest = strings.TrimSpace(digest)
	if digest == "" {
		return nil
	}
	return []CheckpointFingerprint{{Name: name, Value: digest}}
}

func joinedChunkDigest(chunks []source.Chunk) (string, error) {
	if len(chunks) == 0 {
		return "", nil
	}
	values := make([]string, 0, len(chunks))
	for _, chunk := range chunks {
		digest, err := source.DigestChunk(chunk)
		if err != nil {
			return "", fmt.Errorf("digest chunk %q: %w", chunk.ID, err)
		}
		values = append(values, chunk.ID+"="+digest)
	}
	sort.Strings(values)
	sum := sha256.Sum256([]byte(strings.Join(values, "\n")))
	return "sha256:" + hex.EncodeToString(sum[:]), nil
}

func normalizeCheckpointFingerprints(values []CheckpointFingerprint) []CheckpointFingerprint {
	if len(values) == 0 {
		return nil
	}
	byName := make(map[string]string, len(values))
	for _, value := range values {
		name := strings.TrimSpace(value.Name)
		fingerprint := strings.TrimSpace(value.Value)
		if name == "" || fingerprint == "" {
			continue
		}
		byName[name] = fingerprint
	}
	if len(byName) == 0 {
		return nil
	}
	names := make([]string, 0, len(byName))
	for name := range byName {
		names = append(names, name)
	}
	sort.Strings(names)
	out := make([]CheckpointFingerprint, 0, len(names))
	for _, name := range names {
		out = append(out, CheckpointFingerprint{Name: name, Value: byName[name]})
	}
	return out
}

func checkpointContentDigest(content []byte) string {
	sum := sha256.Sum256(content)
	return "sha256:" + hex.EncodeToString(sum[:])
}
