package pipeline

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

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

type referenceTargetKey struct {
	StepID string
	LaneID string
	Stage  ModuleStage
}

type generatedOutputKey struct {
	stepID string
	laneID string
}

type indexedGeneratedOutput struct {
	count  int
	output contracts.SerializedOutput
}

func keyForReferenceTarget(target ResolvedReferenceTarget) referenceTargetKey {
	return referenceTargetKey{StepID: target.StepID, LaneID: target.LaneID, Stage: target.Stage}
}

func operationReferenceSet(input RunInput, target ResolvedReferenceTarget) contracts.ReferenceSet {
	if input.references != nil {
		if set, ok := input.references[keyForReferenceTarget(target)]; ok {
			return CloneReferenceSet(set)
		}
	}
	return CloneReferenceSet(target.ReferenceSet)
}

// referenceTargetReuseEligible reports whether every generated artifact in a
// stage's reference set descends exclusively from fully validated work. Static
// references and callers that do not supply lineage metadata are reusable.
func referenceTargetReuseEligible(input RunInput, target ResolvedReferenceTarget) bool {
	if input.referenceReuseEligibility == nil {
		return true
	}
	eligible, ok := input.referenceReuseEligibility[keyForReferenceTarget(target)]
	return !ok || eligible
}

func laneReferencesReuseEligible(input RunInput, lane ResolvedArtifactLane) bool {
	return referenceTargetReuseEligible(input, lane.ExtractReferences) &&
		referenceTargetReuseEligible(input, lane.MergeReferences) &&
		referenceTargetReuseEligible(input, lane.NormalizeReferences)
}

// buildStepReferenceReuseEligibility carries validation completeness alongside
// generated references without exposing the internal lineage flag in artifact
// payloads. A target becomes ineligible when any generated input is ineligible.
func buildStepReferenceReuseEligibility(step PreparedPipelineStep, outputs map[generatedOutputKey]bool) map[referenceTargetKey]bool {
	eligibility := make(map[referenceTargetKey]bool)
	for _, prepared := range step.lanes {
		lane := prepared.resolved
		for _, target := range []ResolvedReferenceTarget{lane.ExtractReferences, lane.MergeReferences, lane.NormalizeReferences} {
			generated := false
			eligible := true
			for _, binding := range target.Bindings {
				if binding.Artifact == nil {
					continue
				}
				generated = true
				producer := generatedOutputKeyFor(binding.Artifact.Step, binding.Artifact.Lane)
				if reusable, ok := outputs[producer]; ok && !reusable {
					eligible = false
				}
			}
			if generated {
				eligibility[keyForReferenceTarget(target)] = eligible
			}
		}
	}
	if len(eligibility) == 0 {
		return nil
	}
	return eligibility
}

// buildStepReferenceSets resolves every generated binding for a step before
// any lane in that step is allowed to start. Each returned set is a fresh
// operation-time view; prepared reference sets are never modified.
func buildStepReferenceSets(input RunInput, step PreparedPipelineStep, outputs []contracts.SerializedOutput) (map[referenceTargetKey]contracts.ReferenceSet, []artifacts.ReferenceProvenance, error) {
	if len(step.lanes) == 0 {
		return nil, nil, nil
	}
	sets := make(map[referenceTargetKey]contracts.ReferenceSet)
	var provenance []artifacts.ReferenceProvenance
	outputsByProducer := indexGeneratedOutputs(outputs)
	canonical := make(map[generatedOutputKey]contracts.ReferenceItem)
	for _, prepared := range step.lanes {
		lane := prepared.resolved
		for _, target := range []ResolvedReferenceTarget{lane.ExtractReferences, lane.MergeReferences, lane.NormalizeReferences} {
			generated := false
			resolved := CloneReferenceSet(target.ReferenceSet)
			for _, binding := range target.Bindings {
				if binding.Artifact == nil {
					continue
				}
				generated = true
				item, err := generatedReferenceItem(input, binding, outputsByProducer, canonical)
				if err != nil {
					return nil, nil, contextualHandoffError(input, target, binding, err)
				}
				slotName := strings.TrimSpace(binding.SlotName)
				slot, ok := resolved.Slots[slotName]
				if !ok {
					return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q has no resolved declaration", slotName))
				}
				if len(slot.Items) > 0 {
					return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q already contains materialized items", slotName))
				}
				if len(slot.Slot.AcceptedArtifactKinds) == 0 || !containsArtifactKind(slot.Slot.AcceptedArtifactKinds, item.ArtifactKind) {
					return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q does not accept artifact kind %q", slotName, item.ArtifactKind))
				}
				if !referenceMediaTypeAccepted(item.MediaType, slot.Slot.AcceptedMediaTypes) {
					return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q does not accept media type %q", slotName, item.MediaType))
				}
				if slot.Slot.MaxBytes > 0 && item.SizeBytes > slot.Slot.MaxBytes {
					return nil, nil, contextualHandoffError(input, target, binding, fmt.Errorf("reference slot %q is %d bytes, limit %d", slotName, item.SizeBytes, slot.Slot.MaxBytes))
				}
				slot.Items = []contracts.ReferenceItem{contracts.CloneReferenceItem(item)}
				resolved.Slots[slotName] = slot
			}
			if !generated {
				continue
			}
			key := keyForReferenceTarget(target)
			sets[key] = resolved
			for _, item := range generatedItems(resolved) {
				provenance = append(provenance, referenceProvenanceForItem(target, item))
			}
		}
	}
	return sets, provenance, nil
}

func containsArtifactKind(kinds []contracts.ArtifactKind, want contracts.ArtifactKind) bool {
	for _, kind := range kinds {
		if kind == want {
			return true
		}
	}
	return false
}

func contextualHandoffError(input RunInput, target ResolvedReferenceTarget, binding ReferenceBinding, err error) error {
	return fmt.Errorf("pipeline %q step %q lane %q %s generated dependency %q from %q/%q: %w", input.pipeline.ID, target.StepID, target.LaneID, target.Stage, binding.SlotName, binding.Artifact.Step, binding.Artifact.Lane, err)
}

func generatedOutputKeyFor(stepID, laneID string) generatedOutputKey {
	return generatedOutputKey{stepID: strings.TrimSpace(stepID), laneID: strings.TrimSpace(laneID)}
}

func indexGeneratedOutputs(outputs []contracts.SerializedOutput) map[generatedOutputKey]indexedGeneratedOutput {
	indexed := make(map[generatedOutputKey]indexedGeneratedOutput, len(outputs))
	for _, output := range outputs {
		key := generatedOutputKeyFor(output.StepID, output.LaneID)
		entry := indexed[key]
		entry.count++
		if entry.count == 1 {
			entry.output = output
		}
		indexed[key] = entry
	}
	return indexed
}

func generatedReferenceItem(input RunInput, binding ReferenceBinding, outputsByProducer map[generatedOutputKey]indexedGeneratedOutput, cache map[generatedOutputKey]contracts.ReferenceItem) (contracts.ReferenceItem, error) {
	selector := binding.Artifact
	if selector == nil {
		return contracts.ReferenceItem{}, fmt.Errorf("generated reference selector must not be nil")
	}
	producerKey := generatedOutputKeyFor(selector.Step, selector.Lane)
	if item, ok := cache[producerKey]; ok {
		item.SlotName = strings.TrimSpace(binding.SlotName)
		item.BindingSource = strings.TrimSpace(binding.BindingSource)
		return contracts.CloneReferenceItem(item), nil
	}
	matched, ok := outputsByProducer[producerKey]
	if !ok {
		return contracts.ReferenceItem{}, fmt.Errorf("producer has no accepted normalized output")
	}
	if matched.count > 1 {
		return contracts.ReferenceItem{}, fmt.Errorf("producer has %d accepted normalized outputs; exactly one is required", matched.count)
	}
	stepID, laneID := producerKey.stepID, producerKey.laneID
	producer, ok := findResolvedLane(input.pipeline, stepID, laneID)
	if !ok {
		return contracts.ReferenceItem{}, fmt.Errorf("producer lane is not present in the resolved pipeline")
	}
	if input.Prepared == nil || input.Prepared.artifactCodecs == nil {
		return contracts.ReferenceItem{}, fmt.Errorf("artifact codec registry is unavailable")
	}
	serialized := contracts.CloneSerializedArtifact(matched.output.Artifact)
	if serialized.Kind != producer.ArtifactKind {
		return contracts.ReferenceItem{}, fmt.Errorf("producer artifact kind %q does not match resolved kind %q", serialized.Kind, producer.ArtifactKind)
	}
	value, err := input.Prepared.artifactCodecs.Decode(serialized)
	if err != nil {
		return contracts.ReferenceItem{}, fmt.Errorf("decode producer artifact: %w", err)
	}
	canonical, err := input.Prepared.artifactCodecs.Encode(producer.ArtifactKind, value)
	if err != nil {
		return contracts.ReferenceItem{}, fmt.Errorf("canonicalize producer artifact: %w", err)
	}
	expected, ok := input.Prepared.artifactCodecs.Spec(producer.ArtifactKind)
	if !ok {
		return contracts.ReferenceItem{}, fmt.Errorf("producer artifact codec %q is not registered", producer.ArtifactKind)
	}
	if canonical.Kind != expected.Kind || canonical.Schema.ID != expected.Schema.ID || canonical.Schema.Name != expected.Schema.Name || canonical.Schema.Version != expected.Schema.Version || contracts.DigestArtifactSchema(canonical.Schema) != expected.SchemaDigest || canonical.MediaType != expected.MediaType {
		return contracts.ReferenceItem{}, fmt.Errorf("canonical producer artifact does not match registered codec identity")
	}
	item := contracts.ReferenceItem{
		SlotName:       strings.TrimSpace(binding.SlotName),
		MediaType:      canonical.MediaType,
		Content:        append([]byte(nil), canonical.Content...),
		Digest:         referenceDigest(canonical.Content),
		Origin:         contracts.ReferenceOrigin{Type: "generated"},
		SizeBytes:      int64(len(canonical.Content)),
		BindingSource:  strings.TrimSpace(binding.BindingSource),
		ArtifactKind:   canonical.Kind,
		ArtifactSchema: contracts.CloneArtifactSchema(canonical.Schema),
		Producer: contracts.ReferenceProducer{
			PipelineID: input.pipeline.ID,
			StepID:     stepID,
			LaneID:     laneID,
			ModuleKey:  producer.Normalize.Module,
		},
	}
	cacheItem := contracts.CloneReferenceItem(item)
	cacheItem.SlotName = ""
	cacheItem.BindingSource = ""
	cache[producerKey] = cacheItem
	return item, nil
}

func findResolvedLane(pipeline ResolvedPipeline, stepID, laneID string) (ResolvedArtifactLane, bool) {
	for _, step := range pipeline.Steps {
		if step.ID != stepID {
			continue
		}
		for _, lane := range step.ArtifactLanes {
			if lane.ID == laneID {
				return lane, true
			}
		}
	}
	return ResolvedArtifactLane{}, false
}

func generatedItems(set contracts.ReferenceSet) []contracts.ReferenceItem {
	var items []contracts.ReferenceItem
	keys := make([]string, 0, len(set.Slots))
	for key := range set.Slots {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		for _, item := range set.Slots[key].Items {
			if item.Origin.Type == "generated" {
				items = append(items, item)
			}
		}
	}
	return items
}

func referenceProvenanceForItem(target ResolvedReferenceTarget, item contracts.ReferenceItem) artifacts.ReferenceProvenance {
	return artifacts.ReferenceProvenance{
		Stage:            string(target.Stage),
		StepID:           target.StepID,
		LaneID:           target.LaneID,
		SlotName:         item.SlotName,
		OriginType:       item.Origin.Type,
		OriginURI:        item.Origin.URI,
		Digest:           item.Digest,
		MediaType:        item.MediaType,
		SizeBytes:        item.SizeBytes,
		BindingSource:    item.BindingSource,
		ArtifactKind:     string(item.ArtifactKind),
		SchemaID:         item.ArtifactSchema.ID,
		SchemaName:       item.ArtifactSchema.Name,
		SchemaVersion:    item.ArtifactSchema.Version,
		SchemaDigest:     contracts.DigestArtifactSchema(item.ArtifactSchema),
		ProducerPipeline: item.Producer.PipelineID,
		ProducerStep:     item.Producer.StepID,
		ProducerLane:     item.Producer.LaneID,
		ProducerModule:   item.Producer.ModuleKey,
	}
}

type generatedReferenceFingerprintIdentity struct {
	ProducerPipeline string `json:"producer_pipeline_id"`
	ProducerStep     string `json:"producer_step_id"`
	ProducerLane     string `json:"producer_lane_id"`
	ProducerModule   string `json:"producer_module_key"`
	ArtifactKind     string `json:"artifact_kind"`
	SchemaID         string `json:"schema_id"`
	SchemaName       string `json:"schema_name"`
	SchemaVersion    string `json:"schema_version"`
	SchemaDigest     string `json:"schema_digest"`
	MediaType        string `json:"media_type"`
	ContentDigest    string `json:"content_digest"`
	SizeBytes        int64  `json:"size_bytes"`
}

// generatedReferenceDependencies returns the canonical semantic dependency
// identity that receiving operation checkpoints must include.
func generatedReferenceDependencies(set contracts.ReferenceSet) []CheckpointFingerprint {
	var fingerprints []CheckpointFingerprint
	keys := make([]string, 0, len(set.Slots))
	for key := range set.Slots {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, slotName := range keys {
		for index, item := range set.Slots[slotName].Items {
			if item.Origin.Type != "generated" {
				continue
			}
			identity := generatedReferenceFingerprintIdentity{
				ProducerPipeline: item.Producer.PipelineID,
				ProducerStep:     item.Producer.StepID,
				ProducerLane:     item.Producer.LaneID,
				ProducerModule:   item.Producer.ModuleKey,
				ArtifactKind:     string(item.ArtifactKind),
				SchemaID:         item.ArtifactSchema.ID,
				SchemaName:       item.ArtifactSchema.Name,
				SchemaVersion:    item.ArtifactSchema.Version,
				SchemaDigest:     contracts.DigestArtifactSchema(item.ArtifactSchema),
				MediaType:        item.MediaType,
				ContentDigest:    item.Digest,
				SizeBytes:        item.SizeBytes,
			}
			encoded, err := json.Marshal(identity)
			if err != nil {
				continue
			}
			sum := sha256.Sum256(encoded)
			fingerprints = append(fingerprints, CheckpointFingerprint{
				Name:  fmt.Sprintf("generated-reference:%s:%d", slotName, index),
				Value: "sha256:" + hex.EncodeToString(sum[:]),
			})
		}
	}
	return normalizeCheckpointFingerprints(fingerprints)
}
