package tracedetail

import (
	"slices"
	"sort"

	"github.com/SigNoz/signoz/pkg/query-service/model"
)

var (
	SPAN_LIMIT_PER_REQUEST_FOR_WATERFALL float64 = 500
)

type Interval struct {
	StartTime uint64
	Duration  uint64
	Service   string
}

func mergeIntervals(intervals []Interval) []Interval {
	if len(intervals) == 0 {
		return nil
	}

	var merged []Interval
	current := intervals[0]

	for i := 1; i < len(intervals); i++ {
		next := intervals[i]
		if current.StartTime+current.Duration >= next.StartTime {
			endTime := max(current.StartTime+current.Duration, next.StartTime+next.Duration)
			current.Duration = endTime - current.StartTime
		} else {
			merged = append(merged, current)
			current = next
		}
	}
	// Add the last interval
	merged = append(merged, current)

	return merged
}

func ContainsWaterfallSpan(slice []*model.Span, item *model.Span) bool {
	for _, v := range slice {
		if v.SpanID == item.SpanID {
			return true
		}
	}
	return false
}

func findIndexForSelectedSpanFromPreOrder(spans []*model.Span, selectedSpanId string) int {
	var selectedSpanIndex = -1

	for index, span := range spans {
		if span.SpanID == selectedSpanId {
			selectedSpanIndex = index
			break
		}
	}

	return selectedSpanIndex
}

func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, uncollapsedSpans []string, isSelectedSpanIDUnCollapsed bool) (bool, []string) {
	spansFromRootToNode := []string{}

	if node.SpanID == selectedSpanId {
		if isSelectedSpanIDUnCollapsed {
			spansFromRootToNode = append(spansFromRootToNode, node.SpanID)
		}
		return true, spansFromRootToNode
	}

	isPresentInSubtreeForTheNode := false
	for _, child := range node.Children {
		isPresentInThisSubtree, _spansFromRootToNode := getPathFromRootToSelectedSpanId(child, selectedSpanId, uncollapsedSpans, isSelectedSpanIDUnCollapsed)
		// if the interested node is present in the given subtree then add the span node to uncollapsed node list
		if isPresentInThisSubtree {
			if !slices.Contains(uncollapsedSpans, node.SpanID) {
				spansFromRootToNode = append(spansFromRootToNode, node.SpanID)
			}
			isPresentInSubtreeForTheNode = true
			spansFromRootToNode = append(spansFromRootToNode, _spansFromRootToNode...)
		}
	}
	return isPresentInSubtreeForTheNode, spansFromRootToNode
}

func traverseTrace(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreOrder bool, hasSibling bool, selectedSpanId string) []*model.Span {
	preOrderTraversal := []*model.Span{}

	// sort the children to maintain the order across requests
	sort.Slice(span.Children, func(i, j int) bool {
		if span.Children[i].TimeUnixNano == span.Children[j].TimeUnixNano {
			return span.Children[i].Name < span.Children[j].Name
		}
		return span.Children[i].TimeUnixNano < span.Children[j].TimeUnixNano
	})

	span.SubTreeNodeCount = 0
	nodeWithoutChildren := model.Span{
		SpanID:           span.SpanID,
		TraceID:          span.TraceID,
		ServiceName:      span.ServiceName,
		TimeUnixNano:     span.TimeUnixNano,
		Name:             span.Name,
		Kind:             int32(span.Kind),
		DurationNano:     span.DurationNano,
		HasError:         span.HasError,
		StatusMessage:    span.StatusMessage,
		StatusCodeString: span.StatusCodeString,
		SpanKind:         span.SpanKind,
		References:       span.References,
		Events:           span.Events,
		TagMap:           span.TagMap,
		Children:         make([]*model.Span, 0),
		HasChildren:      len(span.Children) > 0,
		Level:            level,
		HasSiblings:      hasSibling,
		SubTreeNodeCount: 0,
	}

	if isPartOfPreOrder {
		preOrderTraversal = append(preOrderTraversal, &nodeWithoutChildren)
	}

	for index, child := range span.Children {
		_childTraversal := traverseTrace(child, uncollapsedSpans, level+1, isPartOfPreOrder && slices.Contains(uncollapsedSpans, span.SpanID), index != (len(span.Children)-1), selectedSpanId)
		preOrderTraversal = append(preOrderTraversal, _childTraversal...)
		nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1
		span.SubTreeNodeCount += child.SubTreeNodeCount + 1
	}

	nodeWithoutChildren.SubTreeNodeCount += 1
	return preOrderTraversal

}

func CalculateServiceTime(serviceIntervals map[string][]Interval) map[string]uint64 {
	totalTimes := make(map[string]uint64)

	for service, serviceIntervals := range serviceIntervals {
		sort.Slice(serviceIntervals, func(i, j int) bool {
			return serviceIntervals[i].StartTime < serviceIntervals[j].StartTime
		})
		mergedIntervals := mergeIntervals(serviceIntervals)
		totalTime := uint64(0)
		for _, interval := range mergedIntervals {
			totalTime += interval.Duration
		}
		totalTimes[service] = totalTime
	}

	return totalTimes
}

func GetSelectedSpans(uncollapsedSpans []string, selectedSpanID string, traceRoots []*model.Span, spanIdToSpanNodeMap map[string]*model.Span, isSelectedSpanIDUnCollapsed bool) ([]*model.Span, []string, string, string) {

	var preOrderTraversal = make([]*model.Span, 0)
	var rootServiceName, rootServiceEntryPoint string
	updatedUncollapsedSpans := uncollapsedSpans

	selectedSpanIndex := -1
	for _, rootSpanID := range traceRoots {
		if rootNode, exists := spanIdToSpanNodeMap[rootSpanID.SpanID]; exists {
			_, spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, selectedSpanID, updatedUncollapsedSpans, isSelectedSpanIDUnCollapsed)
			updatedUncollapsedSpans = append(updatedUncollapsedSpans, spansFromRootToNode...)

			_preOrderTraversal := traverseTrace(rootNode, updatedUncollapsedSpans, 0, true, false, selectedSpanID)
			_selectedSpanIndex := findIndexForSelectedSpanFromPreOrder(_preOrderTraversal, selectedSpanID)

			if _selectedSpanIndex != -1 {
				selectedSpanIndex = _selectedSpanIndex + len(preOrderTraversal)
			}

			preOrderTraversal = append(preOrderTraversal, _preOrderTraversal...)

			if rootServiceName == "" {
				rootServiceName = rootNode.ServiceName
			}

			if rootServiceEntryPoint == "" {
				rootServiceEntryPoint = rootNode.Name
			}
		}
	}

	// if we couldn't find the selectedSpan in the trace then defaulting the selected index to 0
	if selectedSpanIndex == -1 && selectedSpanID != "" {
		selectedSpanIndex = 0
	}

	// get the 0.4*[span limit] before the interested span index
	startIndex := selectedSpanIndex - int(SPAN_LIMIT_PER_REQUEST_FOR_WATERFALL*0.4)
	// get the 0.6*[span limit] after the intrested span index
	endIndex := selectedSpanIndex + int(SPAN_LIMIT_PER_REQUEST_FOR_WATERFALL*0.6)

	// adjust the sliding window according to the available left and right spaces.
	if startIndex < 0 {
		endIndex = endIndex - startIndex
		startIndex = 0
	}
	if endIndex > len(preOrderTraversal) {
		startIndex = startIndex - (endIndex - len(preOrderTraversal))
		endIndex = len(preOrderTraversal)
	}
	if startIndex < 0 {
		startIndex = 0
	}

	return preOrderTraversal[startIndex:endIndex], updatedUncollapsedSpans, rootServiceName, rootServiceEntryPoint
}
