package evidencecontext

import (
	"fmt"
	"sort"
	"strings"

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

type contribution struct {
	laneID   string
	ref      source.SourceRef
	startPos int
	endPos   int
}

type expandedRange struct {
	startPos      int
	endPos        int
	contributions []contribution
}

// Build validates accepted direct references, expands them by source-document
// position, and returns their deterministic context union.
func Build(request BuildRequest) (Document, error) {
	if request.WindowUnits < 0 {
		return Document{}, fmt.Errorf("window_units must not be negative")
	}
	lanes, err := normalizeSelectedLanes(request.SelectedLanes)
	if err != nil {
		return Document{}, err
	}
	if err := source.ValidateDocument(request.Source); err != nil {
		return Document{}, fmt.Errorf("validate source document: %w", err)
	}
	digest, err := source.DigestDocument(request.Source)
	if err != nil {
		return Document{}, fmt.Errorf("digest source document: %w", err)
	}
	if digest != request.Source.Digest {
		return Document{}, fmt.Errorf("source digest does not match source document digest")
	}

	selected := make(map[string]struct{}, len(lanes))
	for _, laneID := range lanes {
		selected[laneID] = struct{}{}
	}
	index := source.NewDocumentIndex(request.Source)
	seen := make(map[evidenceKey]struct{})
	contributions := make([]contribution, 0)
	for laneIndex, laneEvidence := range request.LaneEvidence {
		laneID := strings.TrimSpace(laneEvidence.LaneID)
		if _, ok := selected[laneID]; !ok {
			return Document{}, fmt.Errorf("lane evidence[%d] lane %q is not selected", laneIndex, laneID)
		}
		for refIndex, ref := range laneEvidence.SourceRefs {
			if err := index.ValidateRef(ref); err != nil {
				return Document{}, fmt.Errorf("lane %q source reference[%d]: %w", laneID, refIndex, err)
			}
			key := evidenceKey{laneID: laneID, ref: ref}
			if _, exists := seen[key]; exists {
				continue
			}
			seen[key] = struct{}{}
			startPos, _ := index.Position(ref.StartUnitID)
			endPos, _ := index.Position(ref.EndUnitID)
			contributions = append(contributions, contribution{laneID: laneID, ref: ref, startPos: expandStart(startPos, request.WindowUnits), endPos: expandEnd(endPos, len(request.Source.Units), request.WindowUnits)})
		}
	}

	sort.Slice(contributions, func(i, j int) bool { return lessContribution(contributions[i], contributions[j]) })
	document := Document{
		SourceID:      request.Source.ID,
		SourceDigest:  digest,
		WindowUnits:   request.WindowUnits,
		SelectedLanes: lanes,
		Contexts:      make([]Context, 0),
	}
	for _, rangeValue := range mergeRanges(contributions) {
		context, err := buildContext(request.Source, rangeValue)
		if err != nil {
			return Document{}, err
		}
		document.Contexts = append(document.Contexts, context)
	}
	canonical, err := canonicalize(document)
	if err != nil {
		return Document{}, fmt.Errorf("validate evidence context: %w", err)
	}
	return clone(canonical)
}

type evidenceKey struct {
	laneID string
	ref    source.SourceRef
}

func normalizeSelectedLanes(values []string) ([]string, error) {
	if len(values) == 0 {
		return nil, fmt.Errorf("selected_lanes must not be empty")
	}
	seen := make(map[string]struct{}, len(values))
	lanes := make([]string, 0, len(values))
	for index, raw := range values {
		laneID := strings.TrimSpace(raw)
		if laneID == "" {
			return nil, fmt.Errorf("selected_lanes[%d] must not be empty", index)
		}
		if _, exists := seen[laneID]; exists {
			return nil, fmt.Errorf("selected_lanes lane %q is duplicated", laneID)
		}
		seen[laneID] = struct{}{}
		lanes = append(lanes, laneID)
	}
	sort.Strings(lanes)
	return lanes, nil
}

func expandStart(position, window int) int {
	if window > position {
		return 0
	}
	return position - window
}

func expandEnd(position, length, window int) int {
	last := length - 1
	if window > last-position {
		return last
	}
	return position + window
}

func lessContribution(left, right contribution) bool {
	if left.startPos != right.startPos {
		return left.startPos < right.startPos
	}
	if left.endPos != right.endPos {
		return left.endPos < right.endPos
	}
	return lessEvidenceRef(EvidenceRef{LaneID: left.laneID, SourceRef: left.ref}, EvidenceRef{LaneID: right.laneID, SourceRef: right.ref})
}

func mergeRanges(values []contribution) []expandedRange {
	if len(values) == 0 {
		return nil
	}
	ranges := make([]expandedRange, 0, len(values))
	for _, value := range values {
		if len(ranges) == 0 || value.startPos > ranges[len(ranges)-1].endPos+1 {
			ranges = append(ranges, expandedRange{startPos: value.startPos, endPos: value.endPos, contributions: []contribution{value}})
			continue
		}
		current := &ranges[len(ranges)-1]
		if value.endPos > current.endPos {
			current.endPos = value.endPos
		}
		current.contributions = append(current.contributions, value)
	}
	return ranges
}

func buildContext(document *source.SourceDocument, value expandedRange) (Context, error) {
	evidenceRefs := make([]EvidenceRef, 0, len(value.contributions))
	for _, contribution := range value.contributions {
		evidenceRefs = append(evidenceRefs, EvidenceRef{LaneID: contribution.laneID, SourceRef: contribution.ref})
	}
	sort.Slice(evidenceRefs, func(i, j int) bool { return lessEvidenceRef(evidenceRefs[i], evidenceRefs[j]) })
	units := make([]source.SourceUnit, 0, value.endPos-value.startPos+1)
	for position := value.startPos; position <= value.endPos; position++ {
		unit, err := cloneSourceUnit(document.Units[position])
		if err != nil {
			return Context{}, fmt.Errorf("clone source unit at position %d: %w", position, err)
		}
		units = append(units, unit)
	}
	return Context{
		ContextRef:   source.SourceRef{SourceID: document.ID, StartUnitID: units[0].ID, EndUnitID: units[len(units)-1].ID},
		EvidenceRefs: evidenceRefs,
		Units:        units,
	}, nil
}

func lessEvidenceRef(left, right EvidenceRef) bool {
	if left.LaneID != right.LaneID {
		return left.LaneID < right.LaneID
	}
	if left.SourceRef.SourceID != right.SourceRef.SourceID {
		return left.SourceRef.SourceID < right.SourceRef.SourceID
	}
	if left.SourceRef.StartUnitID != right.SourceRef.StartUnitID {
		return left.SourceRef.StartUnitID < right.SourceRef.StartUnitID
	}
	return left.SourceRef.EndUnitID < right.SourceRef.EndUnitID
}
