package pipeline

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"path"
	"time"

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

func loadMerge(loader CheckpointLoader, stepID, laneID, moduleKey string, deps []CheckpointFingerprint) (MergeCheckpoint, CheckpointDecision) {
	if stepAware, ok := loader.(StepCheckpointLoader); ok {
		return stepAware.MergeForStep(stepID, laneID, moduleKey, deps)
	}
	return loader.Merge(laneID, moduleKey, deps)
}
func loadNormalize(loader CheckpointLoader, stepID, laneID, moduleKey string, deps []CheckpointFingerprint) (NormalizeCheckpoint, CheckpointDecision) {
	if stepAware, ok := loader.(StepCheckpointLoader); ok {
		return stepAware.NormalizeForStep(stepID, laneID, moduleKey, deps)
	}
	return loader.Normalize(laneID, moduleKey, deps)
}
func recordMerge(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
	return checkpointMergeSucceeded(recorder, stepID, laneID, moduleKey, deps, output, warnings)
}
func recordNormalize(recorder CheckpointRecorder, stepID, laneID, moduleKey string, deps []CheckpointFingerprint, output CheckpointArtifact, warnings []contracts.Warning) error {
	return checkpointNormalizeSucceeded(recorder, stepID, laneID, moduleKey, deps, output, warnings)
}
func cloneCheckpointArtifact(output CheckpointArtifact) CheckpointArtifact {
	output.Artifact = contracts.CloneSerializedArtifact(output.Artifact)
	return output
}

func hydrateCheckpointArtifact(codec artifactCodecEntry, output CheckpointArtifact, value any) (CheckpointArtifact, error) {
	output.Artifact.Schema = contracts.CloneArtifactSchema(codec.spec.Schema)
	if codec.metadata != nil {
		metadata, err := codec.metadata(value)
		if err != nil {
			return CheckpointArtifact{}, fmt.Errorf("clone artifact metadata: %w", err)
		}
		output.Artifact.Metadata = metadata
	} else {
		output.Artifact.Metadata = nil
	}
	return output, nil
}
func artifactCheckpointDigests(outputs []CheckpointArtifact) []CheckpointFingerprint {
	values := make([]CheckpointFingerprint, 0, len(outputs))
	for i, output := range outputs {
		sum := sha256.Sum256(output.Artifact.Content)
		values = append(values, CheckpointFingerprint{Name: fmt.Sprintf("artifact[%d]", i), Value: "sha256:" + hex.EncodeToString(sum[:])})
	}
	return normalizeCheckpointFingerprints(values)
}
func debugCheckpointArtifact(output CheckpointArtifact) map[string]any {
	artifact := output.Artifact
	schema := contracts.CloneArtifactSchema(artifact.Schema)
	digest := output.SchemaDigest
	if digest == "" {
		digest = contracts.DigestArtifactSchema(schema)
	}
	schema.JSONSchema = nil
	content := debugContentEnvelope(artifact.Content, artifact.MediaType, artifact.Metadata, nil)
	content.ContentDigest = debugContentDigest(artifact.Content)
	return map[string]any{"lane_id": output.LaneID, "module_key": output.ModuleKey, "source_id": output.SourceID, "chunk_id": output.ChunkID, "chunk_index": output.ChunkIndex, "chunk_ref": output.ChunkRef, "artifact_kind": artifact.Kind, "schema": schema, "schema_digest": digest, "content": content}
}
func debugCheckpointArtifacts(outputs []CheckpointArtifact) []map[string]any {
	if len(outputs) == 0 {
		return nil
	}
	out := make([]map[string]any, 0, len(outputs))
	for _, output := range outputs {
		out = append(out, debugCheckpointArtifact(output))
	}
	return out
}

func serializeArtifact(codec artifactCodecEntry, value any, candidate bool) (contracts.SerializedArtifact, error) {
	encode := codec.encode
	if candidate {
		encode = codec.encodeCandidate
	}
	content, err := encode(value)
	if err != nil {
		return contracts.SerializedArtifact{}, err
	}
	schema := codec.spec.Schema
	metadata := map[string]any(nil)
	if codec.metadata != nil {
		metadata, err = codec.metadata(value)
		if err != nil {
			return contracts.SerializedArtifact{}, fmt.Errorf("clone artifact metadata: %w", err)
		}
	}
	return contracts.SerializedArtifact{Kind: codec.spec.Kind, Schema: contracts.CloneArtifactSchema(schema), MediaType: codec.spec.MediaType, Content: append([]byte(nil), content...), Metadata: metadata}, nil
}

type checkpointArtifactValidationError struct {
	code CheckpointReasonCode
	err  error
}

func (e *checkpointArtifactValidationError) Error() string { return e.err.Error() }
func (e *checkpointArtifactValidationError) Unwrap() error { return e.err }

func checkpointArtifactValidationFailure(code CheckpointReasonCode, format string, args ...any) error {
	return &checkpointArtifactValidationError{code: code, err: fmt.Errorf(format, args...)}
}

func checkpointArtifactReasonCode(err error) CheckpointReasonCode {
	var validationErr *checkpointArtifactValidationError
	if errors.As(err, &validationErr) {
		return validationErr.code
	}
	return CheckpointReasonArtifactPayloadInvalid
}

func decodeCheckpointArtifact(codec artifactCodecEntry, artifact CheckpointArtifact) (any, error) {
	expectedDigest := contracts.DigestArtifactSchema(codec.spec.Schema)
	if artifact.Artifact.Kind != codec.spec.Kind {
		return nil, checkpointArtifactValidationFailure(CheckpointReasonArtifactCodecIncompatible, "artifact kind %q does not match codec %q", artifact.Artifact.Kind, codec.spec.Kind)
	}
	if artifact.Artifact.Schema.ID != codec.spec.Schema.ID || artifact.Artifact.Schema.Name != codec.spec.Schema.Name || artifact.Artifact.Schema.Version != codec.spec.Schema.Version {
		return nil, checkpointArtifactValidationFailure(CheckpointReasonArtifactCodecIncompatible, "artifact schema %q version %q does not match codec schema %q version %q", artifact.Artifact.Schema.ID, artifact.Artifact.Schema.Version, codec.spec.Schema.ID, codec.spec.Schema.Version)
	}
	if artifact.SchemaDigest != expectedDigest {
		return nil, checkpointArtifactValidationFailure(CheckpointReasonArtifactCodecIncompatible, "artifact schema digest %q does not match codec schema digest %q", artifact.SchemaDigest, expectedDigest)
	}
	if artifact.Artifact.MediaType != codec.spec.MediaType {
		return nil, checkpointArtifactValidationFailure(CheckpointReasonArtifactCodecIncompatible, "artifact media type %q does not match codec media type %q", artifact.Artifact.MediaType, codec.spec.MediaType)
	}
	value, err := codec.decode(append([]byte(nil), artifact.Artifact.Content...))
	if err != nil {
		return nil, checkpointArtifactValidationFailure(CheckpointReasonArtifactPayloadInvalid, "decode artifact payload: %w", err)
	}
	return value, nil
}

func decodeCanonicalCheckpointArtifact(codec artifactCodecEntry, artifact CheckpointArtifact) (any, CheckpointArtifact, error) {
	value, err := decodeCheckpointArtifact(codec, artifact)
	if err != nil {
		return nil, CheckpointArtifact{}, err
	}
	canonical, err := serializeArtifact(codec, value, false)
	if err != nil {
		return nil, CheckpointArtifact{}, checkpointArtifactValidationFailure(CheckpointReasonArtifactPayloadInvalid, "encode canonical artifact: %w", err)
	}
	if canonical.Kind != artifact.Artifact.Kind || canonical.Schema.ID != artifact.Artifact.Schema.ID || canonical.Schema.Name != artifact.Artifact.Schema.Name || canonical.Schema.Version != artifact.Artifact.Schema.Version || canonical.MediaType != artifact.Artifact.MediaType || !bytes.Equal(canonical.Content, artifact.Artifact.Content) || checkpointContentDigest(canonical.Content) != checkpointContentDigest(artifact.Artifact.Content) {
		return nil, CheckpointArtifact{}, checkpointArtifactValidationFailure(CheckpointReasonArtifactNotCanonical, "stored artifact is not canonical")
	}
	hydrated, err := hydrateCheckpointArtifact(codec, cloneCheckpointArtifact(artifact), value)
	if err != nil {
		return nil, CheckpointArtifact{}, checkpointArtifactValidationFailure(CheckpointReasonArtifactPayloadInvalid, "hydrate checkpoint artifact: %w", err)
	}
	return value, hydrated, nil
}

func checkpointArtifact(codec artifactCodecEntry, laneID, moduleKey, sourceID string, value any) (CheckpointArtifact, error) {
	serialized, err := serializeArtifact(codec, value, false)
	if err != nil {
		return CheckpointArtifact{}, err
	}
	return CheckpointArtifact{LaneID: laneID, ModuleKey: moduleKey, SourceID: sourceID, Artifact: serialized, SchemaDigest: contracts.DigestArtifactSchema(serialized.Schema)}, nil
}

func serializeCandidateArtifact(codec artifactCodecEntry, laneID, moduleKey, sourceID string, value any) (CheckpointArtifact, error) {
	serialized, err := serializeArtifact(codec, value, true)
	if err != nil {
		return CheckpointArtifact{}, err
	}
	return CheckpointArtifact{LaneID: laneID, ModuleKey: moduleKey, SourceID: sourceID, Artifact: serialized, SchemaDigest: contracts.DigestArtifactSchema(serialized.Schema)}, nil
}

type laneRunError struct {
	stage ModuleStage
	err   error
}

func (e *laneRunError) Error() string { return e.err.Error() }
func (e *laneRunError) Unwrap() error { return e.err }

type mergeStageResult struct {
	artifact   erasedMergeArtifact
	serialized CheckpointArtifact
	terminal   bool
}

func (r *Runner) continueTypedLane(ctx context.Context, input RunInput, checkpoints CheckpointRecorder, loader CheckpointLoader, doc *source.SourceDocument, sourceInput contracts.LLMInputMaterial, sessionID string, prepared preparedLaneExecutor, extracts finalizedExtractResults, output *RunOutput) error {
	lane, typed := prepared.resolved, prepared.typed
	if typed == nil {
		return fmt.Errorf("typed lane %q executor is not prepared", lane.ID)
	}
	if err := setTypedLaneManifestMetadata(output, lane.ID, typed.extractor, typed.merger, typed.normalizer); err != nil {
		return err
	}
	merged, err := r.runMergeStage(ctx, input, checkpoints, loader, doc, sourceInput, sessionID, prepared, extracts, output)
	if err != nil {
		return &laneRunError{stage: StageMerge, err: err}
	}
	if merged.terminal {
		return nil
	}
	normalized, err := r.runNormalizeStage(ctx, input, checkpoints, loader, doc, sourceInput, sessionID, prepared, merged.artifact, merged.serialized, output)
	if err != nil {
		return &laneRunError{stage: StageNormalize, err: err}
	}
	if normalized.accepted {
		output.NormalizeOutputs = append(output.NormalizeOutputs, contracts.SerializedOutput{StepID: input.stepID, LaneID: lane.ID, NormalizerKey: lane.Normalize.Module, SourceID: doc.ID, Artifact: contracts.CloneSerializedArtifact(normalized.serialized.Artifact)})
	}
	return nil
}

func (r *Runner) runMergeStage(ctx context.Context, input RunInput, checkpoints CheckpointRecorder, loader CheckpointLoader, doc *source.SourceDocument, sourceInput contracts.LLMInputMaterial, sessionID string, prepared preparedLaneExecutor, extracts finalizedExtractResults, output *RunOutput) (mergeStageResult, error) {
	var stageResult mergeStageResult
	lane, typed := prepared.resolved, prepared.typed
	mergeInputs := make([]contracts.ExtractArtifact[any], len(extracts.accepted))
	for i, value := range extracts.accepted {
		mergeInputs[i] = contracts.ExtractArtifact[any]{LaneID: value.LaneID, ExtractorKey: value.ExtractorKey, SourceID: value.SourceID, ChunkID: value.ChunkID, ChunkIndex: value.ChunkIndex, ChunkRef: value.ChunkRef, Value: value.Value}
	}
	mergeReferences := operationReferenceSet(input, lane.MergeReferences)
	mergeDeps := append(artifactCheckpointDigests(extracts.serialized), generatedReferenceDependencies(mergeReferences)...)
	mergeDeps = normalizeCheckpointFingerprints(mergeDeps)
	mergeCP, mergeDecision := loadMerge(loader, input.stepID, lane.ID, lane.Merge.Module, mergeDeps)
	mergeResolution, err := resolveCheckpointDecision(output, loader, input.CheckpointPolicy, StageMerge, input.stepID, lane.ID, lane.Merge.Module, mergeDecision, typed.codec, []CheckpointArtifact{mergeCP.Output})
	if err != nil {
		return stageResult, err
	}
	mergeDecision = mergeResolution.decision
	if err := writeDebugTimed(input.Debug, path.Join("merge", debugPathComponent(lane.ID), "input.json"), debugTimedEnvelope{Stage: string(StageMerge), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Merge.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": mergeDecision.Reused, "decision": mergeDecision, "source": debugSourceDocumentEnvelope(doc), "extract_outputs": debugCheckpointArtifacts(extracts.serialized), "options": redactSensitiveMap(lane.Merge.Options), "metadata": redactSensitiveMap(input.Metadata)}}); err != nil {
		return stageResult, err
	}
	var merged erasedMergeArtifact
	var serializedMerge CheckpointArtifact
	var mergeWarnings []contracts.Warning
	if mergeDecision.Reused {
		merged = erasedMergeArtifact{LaneID: lane.ID, MergerKey: lane.Merge.Module, SourceID: doc.ID, Value: mergeResolution.values[0]}
		serializedMerge = mergeResolution.artifacts[0]
		mergeWarnings = cloneWarnings(mergeCP.Warnings)
		output.Warnings = append(output.Warnings, mergeWarnings...)
	} else {
		if err := checkpointMergeRunning(checkpoints, input.stepID, lane.ID, lane.Merge.Module, mergeDeps); err != nil {
			return stageResult, err
		}
		ok, rejection, runErr := runWithRetry(ctx, lane.Merge.Retries, func(attempt int) (bool, *contracts.RejectedOutput, error) {
			started := time.Now().UTC()
			attemptPath := path.Join("merge", debugPathComponent(lane.ID), fmt.Sprintf("attempt-%02d", attempt))
			attemptCtx, llmScope := withDebugLLMScope(ctx, attemptPath)
			terminal := newAttemptTerminalRecorder(input.Debug, attemptPath, "merge", llmScope, debugTimedEnvelope{Stage: string(StageMerge), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Merge.Module, Attempt: attempt, StartedAt: started})
			requestMetadata, metadataErr := cloneMetadata(input.Metadata)
			if metadataErr != nil {
				return false, nil, terminal.record(nil, fmt.Errorf("clone merge request metadata: %w", metadataErr))
			}
			result, callErr := typed.merge(attemptCtx, typed.merger, contracts.TypedMergeRequest[any]{Source: doc, LaneID: lane.ID, ExtractOutputs: mergeInputs, SourceInput: sourceInput.Clone(), SessionID: sessionID, References: CloneReferenceSet(mergeReferences), LLMProfile: lane.Merge.LLMProfile, Metadata: requestMetadata})
			if callErr != nil {
				attemptErr := fmt.Errorf("merge lane %q with merger %q: %w", lane.ID, lane.Merge.Module, callErr)
				return false, nil, terminal.record(nil, attemptErr)
			}
			candidate := erasedMergeArtifact{LaneID: lane.ID, MergerKey: lane.Merge.Module, SourceID: doc.ID, Value: result.Value}
			attemptWarnings := cloneWarnings(result.Warnings)
			serializedCandidate, encodeErr := serializeCandidateArtifact(typed.codec, candidate.LaneID, candidate.MergerKey, candidate.SourceID, candidate.Value)
			if encodeErr != nil {
				attemptErr := fmt.Errorf("serialize merge candidate for lane %q: %w", lane.ID, encodeErr)
				return false, nil, terminal.record(map[string]any{"warnings": debugWarningEnvelopes(attemptWarnings)}, attemptErr)
			}
			warnings, rejected, validateErr := r.validateTypedArtifact(attemptCtx, typed.codec, typedValidationTarget{stage: StageMerge, stepID: input.stepID, laneID: lane.ID, moduleKey: lane.Merge.Module, source: doc, sourceID: doc.ID, sourceInput: sourceInput.Clone(), sessionID: sessionID, references: mergeReferences, metadata: input.Metadata, value: result.Value, candidate: &serializedCandidate}, prepared.mergeValidators, attempt, input.Debug)
			attemptWarnings = append(attemptWarnings, warnings...)
			payload := map[string]any{"output": debugCheckpointArtifact(serializedCandidate), "warnings": debugWarningEnvelopes(attemptWarnings), "rejection": debugRejectedOutputPtr(rejected)}
			if validateErr != nil || rejected != nil {
				return false, rejected, terminal.record(payload, validateErr)
			}
			stored, encodeErr := checkpointArtifact(typed.codec, candidate.LaneID, candidate.MergerKey, candidate.SourceID, candidate.Value)
			if encodeErr != nil {
				attemptErr := fmt.Errorf("serialize accepted merge output for lane %q: %w", lane.ID, encodeErr)
				return false, nil, terminal.record(payload, attemptErr)
			}
			if debugErr := terminal.record(payload, nil); debugErr != nil {
				return false, nil, debugErr
			}
			merged, serializedMerge = candidate, stored
			mergeWarnings = attemptWarnings
			return true, nil, nil
		})
		if runErr != nil {
			_ = checkpointMergeFailed(checkpoints, input.stepID, lane.ID, lane.Merge.Module, mergeDeps, runErr)
			return stageResult, runErr
		}
		if !ok {
			output.Rejected = append(output.Rejected, *rejection)
			if err := checkpointMergeRejected(checkpoints, input.stepID, lane.ID, lane.Merge.Module, mergeDeps, *rejection); err != nil {
				return stageResult, err
			}
			stageResult.terminal = true
			return stageResult, nil
		}
		output.Warnings = append(output.Warnings, mergeWarnings...)
		if err := recordMerge(checkpoints, input.stepID, lane.ID, lane.Merge.Module, mergeDeps, serializedMerge, mergeWarnings); err != nil {
			return stageResult, err
		}
	}
	if err := writeDebugTimed(input.Debug, path.Join("merge", debugPathComponent(lane.ID), "output.json"), debugTimedEnvelope{Stage: string(StageMerge), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Merge.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": mergeDecision.Reused, "accepted": true, "output": debugCheckpointArtifact(serializedMerge), "warnings": debugWarningEnvelopes(mergeWarnings)}}); err != nil {
		return stageResult, err
	}
	stageResult.artifact = merged
	stageResult.serialized = serializedMerge
	return stageResult, nil
}

type normalizeStageResult struct {
	serialized CheckpointArtifact
	warnings   []contracts.Warning
	accepted   bool
}

func (r *Runner) runNormalizeStage(ctx context.Context, input RunInput, checkpoints CheckpointRecorder, loader CheckpointLoader, doc *source.SourceDocument, sourceInput contracts.LLMInputMaterial, sessionID string, prepared preparedLaneExecutor, merged erasedMergeArtifact, serializedMerge CheckpointArtifact, output *RunOutput) (normalizeStageResult, error) {
	var stageResult normalizeStageResult
	lane, typed := prepared.resolved, prepared.typed
	normalizeReferences := operationReferenceSet(input, lane.NormalizeReferences)
	normalizeDeps := append(artifactCheckpointDigests([]CheckpointArtifact{serializedMerge}), generatedReferenceDependencies(normalizeReferences)...)
	normalizeDeps = normalizeCheckpointFingerprints(normalizeDeps)
	normalizeCP, normalizeDecision := loadNormalize(loader, input.stepID, lane.ID, lane.Normalize.Module, normalizeDeps)
	normalizeResolution, err := resolveCheckpointDecision(output, loader, input.CheckpointPolicy, StageNormalize, input.stepID, lane.ID, lane.Normalize.Module, normalizeDecision, typed.codec, []CheckpointArtifact{normalizeCP.Output})
	if err != nil {
		return stageResult, err
	}
	normalizeDecision = normalizeResolution.decision
	if err := writeDebugTimed(input.Debug, path.Join("normalize", debugPathComponent(lane.ID), "input.json"), debugTimedEnvelope{Stage: string(StageNormalize), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": normalizeDecision.Reused, "decision": normalizeDecision, "source": debugSourceDocumentEnvelope(doc), "merge_output": debugCheckpointArtifact(serializedMerge), "options": redactSensitiveMap(lane.Normalize.Options), "metadata": redactSensitiveMap(input.Metadata)}}); err != nil {
		return stageResult, err
	}
	var serializedNormalize CheckpointArtifact
	var normalizeWarnings []contracts.Warning
	if normalizeDecision.Reused {
		serializedNormalize = normalizeResolution.artifacts[0]
		normalizeWarnings = cloneWarnings(normalizeCP.Warnings)
		output.Warnings = append(output.Warnings, normalizeWarnings...)
	} else {
		if err := checkpointNormalizeRunning(checkpoints, input.stepID, lane.ID, lane.Normalize.Module, normalizeDeps); err != nil {
			return stageResult, err
		}
		ok, rejection, runErr := runWithRetry(ctx, lane.Normalize.Retries, func(attempt int) (bool, *contracts.RejectedOutput, error) {
			started := time.Now().UTC()
			attemptPath := path.Join("normalize", debugPathComponent(lane.ID), fmt.Sprintf("attempt-%02d", attempt))
			attemptCtx, llmScope := withDebugLLMScope(ctx, attemptPath)
			terminal := newAttemptTerminalRecorder(input.Debug, attemptPath, "normalize", llmScope, debugTimedEnvelope{Stage: string(StageNormalize), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module, Attempt: attempt, StartedAt: started})
			requestMetadata, metadataErr := cloneMetadata(input.Metadata)
			if metadataErr != nil {
				return false, nil, terminal.record(nil, fmt.Errorf("clone normalize request metadata: %w", metadataErr))
			}
			result, callErr := typed.normalize(attemptCtx, typed.normalizer, contracts.TypedNormalizeRequest[any]{Source: doc, LaneID: lane.ID, MergeOutput: contracts.MergeArtifact[any]{LaneID: lane.ID, MergerKey: lane.Merge.Module, SourceID: doc.ID, Value: merged.Value}, SourceInput: sourceInput.Clone(), SessionID: sessionID, References: CloneReferenceSet(normalizeReferences), LLMProfile: lane.Normalize.LLMProfile, Metadata: requestMetadata})
			if callErr != nil {
				attemptErr := fmt.Errorf("normalize lane %q with normalizer %q: %w", lane.ID, lane.Normalize.Module, callErr)
				return false, nil, terminal.record(nil, attemptErr)
			}
			attemptWarnings := cloneWarnings(result.Warnings)
			serializedCandidate, encodeErr := serializeCandidateArtifact(typed.codec, lane.ID, lane.Normalize.Module, doc.ID, result.Value)
			if encodeErr != nil {
				attemptErr := fmt.Errorf("serialize normalize candidate for lane %q: %w", lane.ID, encodeErr)
				return false, nil, terminal.record(map[string]any{"warnings": debugWarningEnvelopes(attemptWarnings)}, attemptErr)
			}
			var retryPayload map[string]any
			if result.Retry != nil {
				if err := validateNormalizeRetry(result.Retry); err != nil {
					return false, nil, terminal.record(map[string]any{"output": debugCheckpointArtifact(serializedCandidate), "warnings": debugWarningEnvelopes(attemptWarnings)}, fmt.Errorf("normalize lane %q returned invalid retry directive: %w", lane.ID, err))
				}
				retryRemaining := attempt <= lane.Normalize.Retries
				retryPayload = map[string]any{
					"reason_code":       result.Retry.ReasonCode,
					"message":           result.Retry.Message,
					"another_attempt":   retryRemaining,
					"fallback_accepted": !retryRemaining,
				}
				if retryRemaining {
					payload := map[string]any{"output": debugCheckpointArtifact(serializedCandidate), "warnings": debugWarningEnvelopes(attemptWarnings), "retry": retryPayload}
					return false, nil, terminal.record(payload, nil)
				}
				attemptWarnings = append(attemptWarnings, cloneWarnings(result.Retry.FallbackWarnings)...)
			}
			warnings, rejected, validateErr := r.validateTypedArtifact(attemptCtx, typed.codec, typedValidationTarget{stage: StageNormalize, stepID: input.stepID, laneID: lane.ID, moduleKey: lane.Normalize.Module, source: doc, sourceID: doc.ID, sourceInput: sourceInput.Clone(), sessionID: sessionID, references: normalizeReferences, metadata: input.Metadata, value: result.Value, candidate: &serializedCandidate}, prepared.normalizeValidators, attempt, input.Debug)
			attemptWarnings = append(attemptWarnings, warnings...)
			payload := map[string]any{"output": debugCheckpointArtifact(serializedCandidate), "warnings": debugWarningEnvelopes(attemptWarnings), "rejection": debugRejectedOutputPtr(rejected)}
			if retryPayload != nil {
				payload["retry"] = retryPayload
			}
			if validateErr != nil || rejected != nil {
				return false, rejected, terminal.record(payload, validateErr)
			}
			stored, encodeErr := checkpointArtifact(typed.codec, lane.ID, lane.Normalize.Module, doc.ID, result.Value)
			if encodeErr != nil {
				attemptErr := fmt.Errorf("serialize accepted normalize output for lane %q: %w", lane.ID, encodeErr)
				return false, nil, terminal.record(payload, attemptErr)
			}
			if debugErr := terminal.record(payload, nil); debugErr != nil {
				return false, nil, debugErr
			}
			serializedNormalize = stored
			normalizeWarnings = attemptWarnings
			return true, nil, nil
		})
		if runErr != nil {
			_ = checkpointNormalizeFailed(checkpoints, input.stepID, lane.ID, lane.Normalize.Module, normalizeDeps, runErr)
			return stageResult, runErr
		}
		if !ok {
			output.Rejected = append(output.Rejected, *rejection)
			if err := checkpointNormalizeRejected(checkpoints, input.stepID, lane.ID, lane.Normalize.Module, normalizeDeps, *rejection); err != nil {
				return stageResult, err
			}
			return stageResult, nil
		}
		output.Warnings = append(output.Warnings, normalizeWarnings...)
		if err := recordNormalize(checkpoints, input.stepID, lane.ID, lane.Normalize.Module, normalizeDeps, serializedNormalize, normalizeWarnings); err != nil {
			return stageResult, err
		}
	}
	if err := writeDebugTimed(input.Debug, path.Join("normalize", debugPathComponent(lane.ID), "output.json"), debugTimedEnvelope{Stage: string(StageNormalize), StepID: input.stepID, LaneID: lane.ID, ModuleKey: lane.Normalize.Module, StartedAt: time.Now().UTC(), Payload: map[string]any{"reused": normalizeDecision.Reused, "accepted": true, "output": debugCheckpointArtifact(serializedNormalize), "warnings": debugWarningEnvelopes(normalizeWarnings)}}); err != nil {
		return stageResult, err
	}
	stageResult.serialized = serializedNormalize
	stageResult.warnings = normalizeWarnings
	stageResult.accepted = true
	return stageResult, nil
}

func setTypedLaneManifestMetadata(output *RunOutput, laneID string, extractor, merger, normalizer any) error {
	if output == nil {
		return nil
	}
	for i := range output.Manifest.ArtifactLanes {
		if output.Manifest.ArtifactLanes[i].ID != laneID {
			continue
		}
		metadata := make(map[string]any)
		for _, item := range []struct {
			name   string
			module any
		}{{"extractor", extractor}, {"merger", merger}, {"normalizer", normalizer}} {
			value, ok, err := moduleManifestMetadata(item.module)
			if err != nil {
				return fmt.Errorf("clone %s manifest metadata for lane %q: %w", item.name, laneID, err)
			}
			if ok {
				metadata[item.name] = value
			}
		}
		if len(metadata) > 0 {
			output.Manifest.ArtifactLanes[i].Metadata = metadata
		}
		return nil
	}
	return nil
}

func (r *Runner) validateTypedArtifact(ctx context.Context, codec artifactCodecEntry, target typedValidationTarget, chain preparedValidatorChain, attempt int, debug DebugRecorder) ([]contracts.Warning, *contracts.RejectedOutput, error) {
	var warnings []contracts.Warning
	if len(chain.validators) > 0 && target.candidate == nil {
		candidate, err := validationCandidateArtifact(codec, target)
		if err != nil {
			return nil, nil, fmt.Errorf("serialize %s candidate for validation: %w", target.stage, err)
		}
		target.candidate = &candidate
	}
	for index, item := range chain.validators {
		binding := item.resolved.Binding
		var result contracts.ValidationResult
		var err error
		started := time.Now().UTC()
		attemptPath := path.Join("validate", debugPathComponent(string(target.stage)), debugPathComponent(target.laneID), debugPathComponent(target.moduleKey), fmt.Sprintf("%02d-%s-attempt-%02d", index+1, debugPathComponent(binding.Module), attempt))
		validatorCtx, llmScope := withIsolatedDebugLLMScope(ctx, attemptPath)
		requestTarget := target
		requestTarget.sourceInput = target.sourceInput.Clone()
		requestTarget.references = CloneReferenceSet(target.references)
		requestTarget.metadata, err = cloneMetadata(target.metadata)
		if err != nil {
			return nil, nil, fmt.Errorf("clone typed validation metadata: %w", err)
		}
		requestTarget.chunk, err = cloneSourceChunkPtr(target.chunk)
		if err != nil {
			return nil, nil, fmt.Errorf("clone typed validation chunk: %w", err)
		}
		requestTarget.chunks, err = cloneSourceChunks(target.chunks)
		if err != nil {
			return nil, nil, fmt.Errorf("clone typed validation chunks: %w", err)
		}
		switch item.resolved.Target {
		case ValidatorTargetTyped:
			requestTarget.llmProfile = binding.LLMProfile
			result, err = item.typedValidate(validatorCtx, item.typed, requestTarget)
		case ValidatorTargetSerialized:
			artifact, encodeErr := validationCandidateArtifact(codec, target)
			if encodeErr != nil {
				err = encodeErr
				break
			}
			result, err = item.serialized.Validate(validatorCtx, contracts.SerializedValidationRequest{Stage: string(target.stage), LaneID: target.laneID, ModuleKey: target.moduleKey, Source: target.source, SourceID: target.sourceID, SourceInput: requestTarget.sourceInput, SessionID: target.sessionID, References: requestTarget.references, LLMProfile: binding.LLMProfile, Metadata: requestTarget.metadata, Chunk: requestTarget.chunk, Chunks: requestTarget.chunks, Schema: contracts.CloneArtifactSchema(artifact.Artifact.Schema), MediaType: artifact.Artifact.MediaType, Content: append([]byte(nil), artifact.Artifact.Content...)})
		default:
			return nil, nil, fmt.Errorf("validator %q is incompatible with typed artifact validation", binding.Module)
		}
		artifact, _ := validationCandidateArtifact(codec, target)
		debugCall := debugValidationCall{ValidatorName: binding.Module, Request: map[string]any{"stage": string(target.stage), "lane_id": target.laneID, "module_key": target.moduleKey, "source_id": target.sourceID, "artifact": debugCheckpointArtifact(artifact), "metadata": redactSensitiveMap(target.metadata)}, Result: debugValidationResultEnvelope(result)}
		if err != nil {
			debugCall.Error = err.Error()
		}
		if err != nil {
			validationErr := fmt.Errorf("validate typed %s output with validator %q: %w", target.stage, binding.Module, err)
			if debugErr := writeDebugAttempt(debug, attemptPath, debugTimedEnvelope{Stage: string(target.stage), StepID: target.stepID, LaneID: target.laneID, ModuleKey: target.moduleKey, Attempt: attempt, StartedAt: started, Payload: debugCall, Error: debugCall.Error}, llmScope); debugErr != nil {
				return warnings, nil, errors.Join(validationErr, fmt.Errorf("write typed validator attempt debug artifact: %w", debugErr))
			}
			return warnings, nil, validationErr
		}
		if debugErr := writeDebugAttempt(debug, attemptPath, debugTimedEnvelope{Stage: string(target.stage), StepID: target.stepID, LaneID: target.laneID, ModuleKey: target.moduleKey, Attempt: attempt, StartedAt: started, Payload: debugCall}, llmScope); debugErr != nil {
			return warnings, nil, fmt.Errorf("write typed validator attempt debug artifact: %w", debugErr)
		}
		if !result.Approved {
			reason := result.ReasonCode
			if reason == "" {
				reason = "artifact_rejected"
			}
			message := result.Message
			if message == "" {
				message = "artifact rejected"
			}
			return warnings, &contracts.RejectedOutput{Stage: string(target.stage), StepID: target.stepID, LaneID: target.laneID, ModuleKey: target.moduleKey, ChunkID: func() string {
				if target.chunk != nil {
					return target.chunk.ID
				}
				return ""
			}(), ChunkIndex: func() int {
				if target.chunk != nil {
					return target.chunk.Index
				}
				return 0
			}(), ValidatorName: binding.Module, ReasonCode: reason, Message: message, AttemptCount: attempt, DiagnosticArtifactPath: result.DiagnosticArtifactPath}, nil
		}
		warnings = append(warnings, result.Warnings...)
	}
	return warnings, nil, nil
}

func validationCandidateArtifact(codec artifactCodecEntry, target typedValidationTarget) (CheckpointArtifact, error) {
	if target.candidate != nil {
		return cloneCheckpointArtifact(*target.candidate), nil
	}
	return serializeCandidateArtifact(codec, target.laneID, target.moduleKey, target.sourceID, target.value)
}
