package cmd

import (
	"fmt"
	"sort"
	"strings"
	"time"
)

const diagnoseTimelineEntryCap = 500

type diagnoseSeedGroupScope struct {
	Kind      string
	Keyspace  string
	ClusterID string
	Namespace string
}

type diagnoseGroupBucket struct {
	scope           diagnoseSeedGroupScope
	seedMatchIDs    map[string]struct{}
	relatedLogIDs   map[string]struct{}
	eventIDs        map[string]struct{}
	componentValues map[string]struct{}
	firstSeen       time.Time
	lastSeen        time.Time
	hasSeen         bool
}

type diagnoseTimelineSortableEntry struct {
	time  time.Time
	entry DiagnoseTimelineEntry
}

func buildDiagnoseGroupsTimelinesAndSummaries(
	scope DiagnoseBundleScope,
	queryPlan []DiagnoseQueryRecord,
	seedMatches []DiagnoseLogEntry,
	relatedLogs []DiagnoseLogEntry,
	events []DiagnoseEvent,
	location *time.Location,
) ([]DiagnoseGroup, []DiagnoseTimeline, DiagnoseSummaries, []DiagnoseWarning) {
	if location == nil {
		location = time.Local
	}

	seedByID := make(map[string]DiagnoseLogEntry, len(seedMatches))
	relatedByID := make(map[string]DiagnoseLogEntry, len(relatedLogs))
	seedScopes := make(map[string]diagnoseSeedGroupScope, len(seedMatches))
	groupBuckets := make(map[string]*diagnoseGroupBucket)
	seedKeyspaces := make(map[string]struct{})
	warnings := make([]DiagnoseWarning, 0)

	for _, seed := range seedMatches {
		seedByID[seed.ID] = seed
		groupScope := classifyDiagnoseSeedGroupScope(scope, seed)
		seedScopes[seed.ID] = groupScope
		if groupScope.Keyspace != "" {
			seedKeyspaces[groupScope.Keyspace] = struct{}{}
		}
		bucket := ensureDiagnoseGroupBucket(groupBuckets, groupScope)
		bucket.seedMatchIDs[seed.ID] = struct{}{}
		if seed.Component != "" {
			bucket.componentValues[seed.Component] = struct{}{}
		}
		if parsed, ok := parseDiagnoseBundleTimestamp(seed.TimeUTC); ok {
			updateDiagnoseBucketSeenRange(bucket, parsed)
		}
	}
	if len(seedKeyspaces) > 1 {
		warnings = append(warnings, DiagnoseWarning{
			Code:    "multi_keyspace",
			Source:  "seed_matches",
			Message: "seed matches span multiple keyspaces",
		})
	}

	for _, related := range relatedLogs {
		relatedByID[related.ID] = related
		targetGroupKeys := make(map[string]struct{})
		for _, relation := range related.Relations {
			seedScope, ok := seedScopes[relation.SeedID]
			if !ok {
				continue
			}
			targetGroupKeys[diagnoseGroupBucketKey(seedScope)] = struct{}{}
		}
		if len(targetGroupKeys) == 0 {
			continue
		}
		parsedTime, parsedTimeOK := parseDiagnoseBundleTimestamp(related.TimeUTC)
		for groupKey := range targetGroupKeys {
			bucket := groupBuckets[groupKey]
			if bucket == nil {
				continue
			}
			bucket.relatedLogIDs[related.ID] = struct{}{}
			if related.Component != "" {
				bucket.componentValues[related.Component] = struct{}{}
			}
			if parsedTimeOK {
				updateDiagnoseBucketSeenRange(bucket, parsedTime)
			}
		}
	}

	eventByID := make(map[string]DiagnoseEvent, len(events))
	eventTimeByID := make(map[string]time.Time, len(events))
	for index, event := range events {
		eventID := strings.TrimSpace(event.ID)
		if eventID == "" {
			eventID = fmt.Sprintf("event-%03d", index+1)
		}
		event.ID = eventID
		eventByID[eventID] = event
		if parsed, ok := parseDiagnoseBundleTimestamp(event.Timestamp); ok {
			eventTimeByID[eventID] = parsed
		}
	}
	for _, bucket := range groupBuckets {
		for eventID := range eventByID {
			bucket.eventIDs[eventID] = struct{}{}
			if parsed, ok := eventTimeByID[eventID]; ok {
				updateDiagnoseBucketSeenRange(bucket, parsed)
			}
		}
	}

	sortedBucketKeys := make([]string, 0, len(groupBuckets))
	for key := range groupBuckets {
		sortedBucketKeys = append(sortedBucketKeys, key)
	}
	sort.Strings(sortedBucketKeys)

	groups := make([]DiagnoseGroup, 0, len(sortedBucketKeys))
	timelines := make([]DiagnoseTimeline, 0, len(sortedBucketKeys))
	for index, key := range sortedBucketKeys {
		bucket := groupBuckets[key]
		groupID := fmt.Sprintf("group-%03d", index+1)
		timelineID := fmt.Sprintf("timeline-%03d", index+1)
		group := DiagnoseGroup{
			ID:            groupID,
			Kind:          bucket.scope.Kind,
			Keyspace:      bucket.scope.Keyspace,
			ClusterID:     bucket.scope.ClusterID,
			Namespace:     bucket.scope.Namespace,
			Components:    diagnoseSortedSetValues(bucket.componentValues),
			SeedMatchIDs:  diagnoseSortedSetValues(bucket.seedMatchIDs),
			RelatedLogIDs: diagnoseSortedSetValues(bucket.relatedLogIDs),
			EventIDs:      diagnoseSortedSetValues(bucket.eventIDs),
		}
		if bucket.hasSeen {
			group.FirstSeenUTC = bucket.firstSeen.UTC().Format(time.RFC3339Nano)
			group.LastSeenUTC = bucket.lastSeen.UTC().Format(time.RFC3339Nano)
		}
		groups = append(groups, group)

		timeline := DiagnoseTimeline{
			ID:       timelineID,
			GroupID:  groupID,
			Timezone: diagnoseTimelineTimezone(scope, location),
			Entries:  buildDiagnoseTimelineEntries(group, seedByID, relatedByID, eventByID, eventTimeByID, location),
		}
		if len(timeline.Entries) > diagnoseTimelineEntryCap {
			timeline.Entries = timeline.Entries[:diagnoseTimelineEntryCap]
			warnings = append(warnings, DiagnoseWarning{
				Code:    "timeline_truncated",
				Source:  group.ID,
				Message: fmt.Sprintf("timeline entry count exceeded %d; output truncated", diagnoseTimelineEntryCap),
			})
		}
		timelines = append(timelines, timeline)
	}

	summaries := buildDiagnoseSummaries(scope, queryPlan, seedMatches, relatedLogs, seedScopes)
	return groups, timelines, summaries, warnings
}

func classifyDiagnoseSeedGroupScope(scope DiagnoseBundleScope, entry DiagnoseLogEntry) diagnoseSeedGroupScope {
	activeClusterID := strings.TrimSpace(scope.ClusterID)
	namespace := firstNonEmpty(
		streamLabelValue(entry.Labels, "namespace"),
		diagnoseAnchorString(entry, "namespace"),
		scope.Namespace,
	)

	keyspace := diagnoseSeedKeyspace(entry)
	if keyspace != "" {
		if activeClusterID != "" && keyspace == activeClusterID {
			return diagnoseSeedGroupScope{
				Kind:      "active_profile_keyspace",
				Keyspace:  keyspace,
				ClusterID: activeClusterID,
				Namespace: namespace,
			}
		}
		return diagnoseSeedGroupScope{
			Kind:      "other_keyspace",
			Keyspace:  keyspace,
			Namespace: namespace,
		}
	}

	clusterIDLabel := streamLabelValue(entry.Labels, "cluster_id")
	tidbClusterIDLabel := streamLabelValue(entry.Labels, "tidb_cluster_id")
	if activeClusterID != "" && (clusterIDLabel == activeClusterID || tidbClusterIDLabel == activeClusterID) {
		return diagnoseSeedGroupScope{
			Kind:      "active_profile_keyspace",
			ClusterID: activeClusterID,
			Namespace: namespace,
		}
	}

	if strings.EqualFold(clusterIDLabel, "unknown") &&
		diagnoseNamespaceOrSharedpoolMatches(scope, entry.Labels) {
		return diagnoseSeedGroupScope{
			Kind:      "shared_component",
			Namespace: namespace,
		}
	}

	return diagnoseSeedGroupScope{
		Kind:      "unknown_scope",
		Namespace: namespace,
	}
}

func ensureDiagnoseGroupBucket(buckets map[string]*diagnoseGroupBucket, scope diagnoseSeedGroupScope) *diagnoseGroupBucket {
	key := diagnoseGroupBucketKey(scope)
	if existing, ok := buckets[key]; ok {
		return existing
	}
	created := &diagnoseGroupBucket{
		scope:           scope,
		seedMatchIDs:    make(map[string]struct{}),
		relatedLogIDs:   make(map[string]struct{}),
		eventIDs:        make(map[string]struct{}),
		componentValues: make(map[string]struct{}),
	}
	buckets[key] = created
	return created
}

func diagnoseGroupBucketKey(scope diagnoseSeedGroupScope) string {
	return strings.Join([]string{scope.Kind, scope.Keyspace, scope.ClusterID, scope.Namespace}, "\x00")
}

func parseDiagnoseBundleTimestamp(raw string) (time.Time, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return time.Time{}, false
	}

	if parsed, err := time.Parse(time.RFC3339Nano, raw); err == nil {
		return parsed.UTC(), true
	}

	parsedNS, err := parseFlexibleTimeToUnixNanoWithNow(raw, time.UTC, time.Unix(0, 0).UTC())
	if err != nil {
		return time.Time{}, false
	}
	return time.Unix(0, parsedNS).UTC(), true
}

func updateDiagnoseBucketSeenRange(bucket *diagnoseGroupBucket, value time.Time) {
	if bucket == nil {
		return
	}
	if !bucket.hasSeen {
		bucket.firstSeen = value
		bucket.lastSeen = value
		bucket.hasSeen = true
		return
	}
	if value.Before(bucket.firstSeen) {
		bucket.firstSeen = value
	}
	if value.After(bucket.lastSeen) {
		bucket.lastSeen = value
	}
}

func diagnoseTimelineTimezone(scope DiagnoseBundleScope, location *time.Location) string {
	if strings.TrimSpace(scope.Timezone) != "" {
		return strings.TrimSpace(scope.Timezone)
	}
	return diagnoseLocationName(location)
}

func buildDiagnoseTimelineEntries(
	group DiagnoseGroup,
	seedByID map[string]DiagnoseLogEntry,
	relatedByID map[string]DiagnoseLogEntry,
	eventByID map[string]DiagnoseEvent,
	eventTimeByID map[string]time.Time,
	location *time.Location,
) []DiagnoseTimelineEntry {
	sortable := make([]diagnoseTimelineSortableEntry, 0, len(group.SeedMatchIDs)+len(group.RelatedLogIDs)+len(group.EventIDs))
	seedStart, seedEnd, hasSeedRange := diagnoseSeedWindow(group.SeedMatchIDs, seedByID)

	for _, seedID := range group.SeedMatchIDs {
		seed, ok := seedByID[seedID]
		if !ok {
			continue
		}
		parsed, ok := parseDiagnoseBundleTimestamp(seed.TimeUTC)
		if !ok {
			continue
		}
		sortable = append(sortable, diagnoseTimelineSortableEntry{
			time: parsed,
			entry: DiagnoseTimelineEntry{
				TimeUTC:    seed.TimeUTC,
				TimeLocal:  diagnoseEntryTimeLocal(seed.TimeLocal, parsed, location),
				SourceID:   seed.ID,
				SourceKind: firstNonEmpty(seed.Kind, "log"),
				Category:   "seed_match",
				Component:  seed.Component,
				Instance:   seed.Instance,
				Level:      seed.Level,
				Message:    diagnoseTimelineMessage(seed),
			},
		})
	}

	for _, relatedID := range group.RelatedLogIDs {
		related, ok := relatedByID[relatedID]
		if !ok {
			continue
		}
		parsed, ok := parseDiagnoseBundleTimestamp(related.TimeUTC)
		if !ok {
			continue
		}
		sortable = append(sortable, diagnoseTimelineSortableEntry{
			time: parsed,
			entry: DiagnoseTimelineEntry{
				TimeUTC:    related.TimeUTC,
				TimeLocal:  diagnoseEntryTimeLocal(related.TimeLocal, parsed, location),
				SourceID:   related.ID,
				SourceKind: firstNonEmpty(related.Kind, "log"),
				Category:   "related_log",
				Component:  related.Component,
				Instance:   related.Instance,
				Level:      related.Level,
				Message:    diagnoseTimelineMessage(related),
			},
		})
	}

	for _, eventID := range group.EventIDs {
		event, ok := eventByID[eventID]
		if !ok {
			continue
		}
		parsed, ok := eventTimeByID[eventID]
		if !ok {
			continue
		}
		sortable = append(sortable, diagnoseTimelineSortableEntry{
			time: parsed,
			entry: DiagnoseTimelineEntry{
				TimeUTC:    parsed.UTC().Format(time.RFC3339Nano),
				TimeLocal:  parsed.In(location).Format(time.RFC3339Nano),
				SourceID:   eventID,
				SourceKind: "event",
				Category:   "event",
				Message:    firstNonEmpty(event.Message, event.Name, event.Type),
			},
		})
	}

	sort.Slice(sortable, func(i, j int) bool {
		if !sortable[i].time.Equal(sortable[j].time) {
			return sortable[i].time.Before(sortable[j].time)
		}
		if sortable[i].entry.SourceID != sortable[j].entry.SourceID {
			return sortable[i].entry.SourceID < sortable[j].entry.SourceID
		}
		return sortable[i].entry.Category < sortable[j].entry.Category
	})

	entries := make([]DiagnoseTimelineEntry, 0, len(sortable))
	for _, item := range sortable {
		item.entry.RelativeToSeedWindow = diagnoseRelativeSeedWindow(item.time, seedStart, seedEnd, hasSeedRange)
		entries = append(entries, item.entry)
	}
	return entries
}

func diagnoseSeedWindow(seedIDs []string, seedByID map[string]DiagnoseLogEntry) (time.Time, time.Time, bool) {
	var start time.Time
	var end time.Time
	hasSeed := false
	for _, seedID := range seedIDs {
		seed, ok := seedByID[seedID]
		if !ok {
			continue
		}
		parsed, ok := parseDiagnoseBundleTimestamp(seed.TimeUTC)
		if !ok {
			continue
		}
		if !hasSeed {
			start = parsed
			end = parsed
			hasSeed = true
			continue
		}
		if parsed.Before(start) {
			start = parsed
		}
		if parsed.After(end) {
			end = parsed
		}
	}
	return start, end, hasSeed
}

func diagnoseRelativeSeedWindow(value, seedStart, seedEnd time.Time, hasSeedWindow bool) string {
	if !hasSeedWindow {
		return "no_seed_matches"
	}
	if value.Before(seedStart) {
		return "before"
	}
	if value.After(seedEnd) {
		return "after"
	}
	return "during"
}

func diagnoseEntryTimeLocal(raw string, parsedUTC time.Time, location *time.Location) string {
	if strings.TrimSpace(raw) != "" {
		return raw
	}
	return parsedUTC.In(location).Format(time.RFC3339Nano)
}

func diagnoseTimelineMessage(entry DiagnoseLogEntry) string {
	if strings.TrimSpace(entry.Message) != "" {
		return strings.TrimSpace(entry.Message)
	}
	return strings.TrimSpace(entry.Raw)
}

func buildDiagnoseSummaries(
	scope DiagnoseBundleScope,
	queryPlan []DiagnoseQueryRecord,
	seedMatches []DiagnoseLogEntry,
	relatedLogs []DiagnoseLogEntry,
	seedScopes map[string]diagnoseSeedGroupScope,
) DiagnoseSummaries {
	summaries := DiagnoseSummaries{
		MatchCounts: DiagnoseMatchCounts{
			ByComponent: make(map[string]int),
			ByKeyspace:  make(map[string]int),
		},
		AffectedComponents: make([]string, 0),
		AffectedInstances:  make([]string, 0),
		ExtractedAnchors:   make(map[string][]string),
		QueryLimits: DiagnoseQueryLimitSummary{
			HitLimitQueryIDs: make([]string, 0),
		},
	}

	affectedComponents := make(map[string]struct{})
	affectedInstances := make(map[string]struct{})
	extractedAnchors := make(map[string]map[string]struct{})

	firstSeed := time.Time{}
	lastSeed := time.Time{}
	hasSeedTime := false

	for _, seed := range seedMatches {
		summaries.MatchCounts.TotalSeedMatches++
		if seed.Component != "" {
			summaries.MatchCounts.ByComponent[seed.Component]++
			affectedComponents[seed.Component] = struct{}{}
		}
		if seed.Instance != "" {
			affectedInstances[seed.Instance] = struct{}{}
		}
		keyspace := diagnoseSeedGroupKeyspace(seedScopes[seed.ID], seed)
		if keyspace != "" {
			summaries.MatchCounts.ByKeyspace[keyspace]++
		}
		if parsed, ok := parseDiagnoseBundleTimestamp(seed.TimeUTC); ok {
			if !hasSeedTime {
				firstSeed = parsed
				lastSeed = parsed
				hasSeedTime = true
			} else {
				if parsed.Before(firstSeed) {
					firstSeed = parsed
				}
				if parsed.After(lastSeed) {
					lastSeed = parsed
				}
			}
		}
		scopeKind := seedScopes[seed.ID].Kind
		switch scopeKind {
		case "active_profile_keyspace":
			summaries.ActiveProfileSeedMatches++
		case "other_keyspace":
			summaries.OtherKeyspaceSeedMatches++
		case "shared_component":
			summaries.SharedComponentSeedMatches++
		}
		collectDiagnoseAnchors(extractedAnchors, seed.Anchors)
	}

	for _, related := range relatedLogs {
		if related.Component != "" {
			affectedComponents[related.Component] = struct{}{}
		}
		if related.Instance != "" {
			affectedInstances[related.Instance] = struct{}{}
		}
		collectDiagnoseAnchors(extractedAnchors, related.Anchors)
	}

	if hasSeedTime {
		summaries.FirstSeedMatchUTC = firstSeed.UTC().Format(time.RFC3339Nano)
		summaries.LastSeedMatchUTC = lastSeed.UTC().Format(time.RFC3339Nano)
	}
	summaries.AffectedComponents = diagnoseSortedSetValues(affectedComponents)
	summaries.AffectedInstances = diagnoseSortedSetValues(affectedInstances)
	summaries.ExtractedAnchors = diagnoseFinalizeExtractedAnchors(extractedAnchors)
	summaries.QueryLimits = diagnoseQueryLimitSummary(queryPlan)

	// Ensure maps are always objects instead of null in JSON.
	if summaries.MatchCounts.ByComponent == nil {
		summaries.MatchCounts.ByComponent = make(map[string]int)
	}
	if summaries.MatchCounts.ByKeyspace == nil {
		summaries.MatchCounts.ByKeyspace = make(map[string]int)
	}
	if summaries.ExtractedAnchors == nil {
		summaries.ExtractedAnchors = make(map[string][]string)
	}
	return summaries
}

func diagnoseSeedGroupKeyspace(groupScope diagnoseSeedGroupScope, seed DiagnoseLogEntry) string {
	if groupScope.Keyspace != "" {
		return groupScope.Keyspace
	}
	clusterID := firstNonEmpty(
		streamLabelValue(seed.Labels, "cluster_id"),
		streamLabelValue(seed.Labels, "tidb_cluster_id"),
	)
	if strings.EqualFold(clusterID, "unknown") {
		return ""
	}
	return clusterID
}

func diagnoseSeedKeyspace(entry DiagnoseLogEntry) string {
	return firstNonEmpty(
		diagnoseAnchorString(entry, "keyspace"),
		diagnoseFieldValue(entry.Fields, "keyspaceName", "keyspace"),
		streamLabelValue(entry.Labels, "keyspace"),
		streamLabelValue(entry.Labels, "keyspaceName"),
	)
}

func diagnoseNamespaceOrSharedpoolMatches(scope DiagnoseBundleScope, labels map[string]string) bool {
	namespace := streamLabelValue(labels, "namespace")
	if scope.Namespace != "" && namespace == scope.Namespace {
		return true
	}
	sharedpool := firstNonEmpty(
		streamLabelValue(labels, "sharedpool"),
		streamLabelValue(labels, "sharedpool_id"),
		streamLabelValue(labels, "shared_pool_id"),
	)
	return scope.SharedpoolID != "" && sharedpool == scope.SharedpoolID
}

func collectDiagnoseAnchors(target map[string]map[string]struct{}, anchors map[string]any) {
	for key, raw := range anchors {
		key = strings.TrimSpace(key)
		if key == "" {
			continue
		}
		values := diagnoseAnchorValues(raw)
		if len(values) == 0 {
			continue
		}
		set, ok := target[key]
		if !ok {
			set = make(map[string]struct{})
			target[key] = set
		}
		for _, value := range values {
			set[value] = struct{}{}
		}
	}
}

func diagnoseAnchorValues(raw any) []string {
	switch typed := raw.(type) {
	case string:
		value := strings.TrimSpace(typed)
		if value == "" {
			return nil
		}
		return []string{value}
	case []string:
		values := make([]string, 0, len(typed))
		for _, item := range typed {
			item = strings.TrimSpace(item)
			if item != "" {
				values = append(values, item)
			}
		}
		return values
	case []any:
		values := make([]string, 0, len(typed))
		for _, item := range typed {
			text, ok := item.(string)
			if !ok {
				continue
			}
			text = strings.TrimSpace(text)
			if text != "" {
				values = append(values, text)
			}
		}
		return values
	default:
		return nil
	}
}

func diagnoseFinalizeExtractedAnchors(source map[string]map[string]struct{}) map[string][]string {
	result := make(map[string][]string, len(source))
	for key, set := range source {
		result[key] = diagnoseSortedSetValues(set)
	}
	return result
}

func diagnoseQueryLimitSummary(queryPlan []DiagnoseQueryRecord) DiagnoseQueryLimitSummary {
	summary := DiagnoseQueryLimitSummary{
		AnyQueryHitLimit: false,
		HitLimitQueryIDs: make([]string, 0),
	}
	for _, record := range queryPlan {
		if !record.HitLimit {
			continue
		}
		summary.AnyQueryHitLimit = true
		summary.HitLimitQueryIDs = append(summary.HitLimitQueryIDs, record.ID)
	}
	sort.Strings(summary.HitLimitQueryIDs)
	return summary
}

func diagnoseSortedSetValues(values map[string]struct{}) []string {
	sorted := make([]string, 0, len(values))
	for value := range values {
		if strings.TrimSpace(value) != "" {
			sorted = append(sorted, value)
		}
	}
	sort.Strings(sorted)
	return sorted
}
