package metrics

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

// AgentDashboardMetrics is the bounded aggregate used by the Desktop Machine
// dashboard. It is intentionally computed for an explicit Agent set so
// metrics from another Workspace or Machine cannot leak into the view.
type AgentDashboardMetrics struct {
	MessageSummary AgentMessageSummary
	MessageTrend   ServerMessageDirectionalTimeSeriesResponse
	MessageBar     BarSeriesResponse
	TokenSummary   AgentTokenSummary
	TokenTrend     AgentTokenTimeSeriesResponse
	InputTokenBar  BarSeriesResponse
	CachedTokenBar BarSeriesResponse
	OutputTokenBar BarSeriesResponse
	Agents         []AgentDashboardRow
}

type AgentDashboardRow struct {
	AgentID  string `json:"agentId"`
	Received uint64 `json:"received"`
	Sent     uint64 `json:"sent"`
	Input    uint64 `json:"input"`
	Cached   uint64 `json:"cached"`
	Output   uint64 `json:"output"`
	Total    uint64 `json:"total"`
}

// QueryAgentsDashboard aggregates messages and token usage for an explicit,
// bounded Agent set while holding the collector lock once.
func (c *ServerMessageCollector) QueryAgentsDashboard(agentIDs []string, window TimeWindow) AgentDashboardMetrics {
	snapshotAt := time.Now()
	return c.queryAgentsDashboard(agentIDs, window, snapshotAt, false)
}

// QueryAgentsDashboardAt aggregates through a fixed snapshot boundary so
// diagnostics can align low-cardinality totals with detailed evidence.
func (c *ServerMessageCollector) QueryAgentsDashboardAt(agentIDs []string, window TimeWindow, snapshotAt time.Time) AgentDashboardMetrics {
	if snapshotAt.IsZero() {
		snapshotAt = time.Now()
	}
	return c.queryAgentsDashboard(agentIDs, window, snapshotAt, true)
}

func (c *ServerMessageCollector) queryAgentsDashboard(agentIDs []string, window TimeWindow, snapshotAt time.Time, exactTokenBoundary bool) AgentDashboardMetrics {
	c.mu.RLock()
	defer c.mu.RUnlock()

	selected := make(map[string]struct{}, len(agentIDs))
	orderedIDs := make([]string, 0, len(agentIDs))
	for _, agentID := range agentIDs {
		agentID = strings.TrimSpace(agentID)
		if agentID == "" {
			continue
		}
		if _, exists := selected[agentID]; exists {
			continue
		}
		selected[agentID] = struct{}{}
		orderedIDs = append(orderedIDs, agentID)
	}
	sort.Strings(orderedIDs)

	interval := MachineDashboardOverviewAggregationInterval(window)
	barInterval := interval
	result := AgentDashboardMetrics{
		MessageTrend: ServerMessageDirectionalTimeSeriesResponse{
			Interval: int(interval.Seconds()),
			Data:     []ServerMessageDirectionalTimeSeriesPoint{},
		},
		MessageBar: emptyDashboardBar(barInterval, []BarSeriesEntry{
			{Key: "inbound", Label: "Inbound"},
			{Key: "outbound", Label: "Outbound"},
		}),
		TokenTrend: AgentTokenTimeSeriesResponse{
			Interval: int(interval.Seconds()),
			Data:     []TimeSeriesDataPoint{},
		},
		InputTokenBar:  emptyDashboardBar(barInterval, nil),
		CachedTokenBar: emptyDashboardBar(barInterval, nil),
		OutputTokenBar: emptyDashboardBar(barInterval, nil),
		Agents:         make([]AgentDashboardRow, 0, len(orderedIDs)),
	}
	if len(selected) == 0 {
		return result
	}

	rows := make(map[string]*AgentDashboardRow, len(orderedIDs))
	for _, agentID := range orderedIDs {
		row := &AgentDashboardRow{AgentID: agentID}
		rows[agentID] = row
	}
	messageBuckets := make(map[int64]*ServerMessageDirectionalTimeSeriesPoint)
	messageBarBuckets := make(map[int64]map[string]uint64)
	tokenBuckets := make(map[int64]*TimeSeriesDataPoint)
	inputBarBuckets := make(map[int64]map[string]uint64)
	cachedBarBuckets := make(map[int64]map[string]uint64)
	outputBarBuckets := make(map[int64]map[string]uint64)
	modelSet := make(map[string]struct{})
	cutoff := snapshotAt.Add(-window.Duration())
	firstBucket := cutoff.Truncate(interval)
	lastBucket := snapshotAt.Truncate(interval)
	firstBarBucket := cutoff.Truncate(barInterval)
	lastBarBucket := snapshotAt.Truncate(barInterval)
	hasMessages := false
	hasTokens := false
	addToken := func(agentID, model string, stats TokenUsageStats, timestamp time.Time) {
		row := rows[agentID]
		row.Input += stats.Input
		row.Cached += stats.CachedInput
		row.Output += stats.Output
		result.TokenSummary.Count += stats.Count
		result.TokenSummary.Input += stats.Input
		result.TokenSummary.Cached += stats.CachedInput
		result.TokenSummary.Output += stats.Output
		if stats.Count > 0 || stats.Input > 0 || stats.CachedInput > 0 || stats.Output > 0 {
			hasTokens = true
			bucketTimestamp := timestamp.Truncate(interval).Unix()
			point := tokenBuckets[bucketTimestamp]
			if point == nil {
				point = &TimeSeriesDataPoint{Timestamp: bucketTimestamp}
				tokenBuckets[bucketTimestamp] = point
			}
			point.Count += stats.Count
			point.Input += stats.Input
			point.Cached += stats.CachedInput
			point.Output += stats.Output
		}
		modelSet[model] = struct{}{}
		barTimestamp := timestamp.Truncate(barInterval).Unix()
		dashboardBarValues(inputBarBuckets, barTimestamp)[model] += stats.Input
		dashboardBarValues(cachedBarBuckets, barTimestamp)[model] += stats.CachedInput
		dashboardBarValues(outputBarBuckets, barTimestamp)[model] += stats.Output
	}

	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) || bucket.timestamp.After(snapshotAt) {
			continue
		}
		bucketTimestamp := bucket.timestamp.Truncate(interval).Unix()
		for agentID := range selected {
			row := rows[agentID]
			received := bucket.agentReceived[agentID]
			sent := bucket.agentSent[agentID]
			row.Received += received
			row.Sent += sent
			if received > 0 || sent > 0 {
				hasMessages = true
				point := messageBuckets[bucketTimestamp]
				if point == nil {
					point = &ServerMessageDirectionalTimeSeriesPoint{Timestamp: bucketTimestamp}
					messageBuckets[bucketTimestamp] = point
				}
				point.Received += received
				point.Sent += sent
				values := dashboardBarValues(messageBarBuckets, bucket.timestamp.Truncate(barInterval).Unix())
				values["inbound"] += received
				values["outbound"] += sent
			}

			if !exactTokenBoundary {
				for model, stats := range bucket.agentTokens[agentID] {
					addToken(agentID, model, stats, bucket.timestamp)
				}
			}
		}
	}
	if exactTokenBoundary {
		for _, event := range c.tokenEvents {
			if event.timestamp.Before(cutoff) || event.timestamp.After(snapshotAt) {
				continue
			}
			if _, ok := selected[event.agentID]; ok {
				addToken(event.agentID, event.model, event.stats, event.timestamp)
			}
		}
	}

	for _, agentID := range orderedIDs {
		row := rows[agentID]
		// Cached is a subset of input, so exclude it from the total to avoid
		// double-counting (total = input + output matches the provider total).
		row.Total = row.Input + row.Output
		result.Agents = append(result.Agents, *row)
		result.MessageSummary.Received += row.Received
		result.MessageSummary.Sent += row.Sent
	}
	result.MessageSummary.Total = result.MessageSummary.Received + result.MessageSummary.Sent
	result.TokenSummary.Total = result.TokenSummary.Input + result.TokenSummary.Output

	if hasMessages {
		for timestamp := firstBucket; !timestamp.After(lastBucket); timestamp = timestamp.Add(interval) {
			point := messageBuckets[timestamp.Unix()]
			if point == nil {
				point = &ServerMessageDirectionalTimeSeriesPoint{Timestamp: timestamp.Unix()}
			}
			result.MessageTrend.Data = append(result.MessageTrend.Data, *point)
		}
	}
	if hasTokens {
		for timestamp := firstBucket; !timestamp.After(lastBucket); timestamp = timestamp.Add(interval) {
			point := tokenBuckets[timestamp.Unix()]
			if point == nil {
				point = &TimeSeriesDataPoint{Timestamp: timestamp.Unix()}
			}
			result.TokenTrend.Data = append(result.TokenTrend.Data, *point)
		}
	}
	if hasMessages {
		result.MessageBar.Data = dashboardBarData(messageBarBuckets, result.MessageBar.Series, firstBarBucket, lastBarBucket, barInterval)
	}
	models := make([]string, 0, len(modelSet))
	for model := range modelSet {
		models = append(models, model)
	}
	sort.Strings(models)
	series := make([]BarSeriesEntry, 0, len(models))
	for _, model := range models {
		series = append(series, BarSeriesEntry{Key: model, Label: shortModelLabel(model)})
	}
	result.InputTokenBar.Series = series
	result.CachedTokenBar.Series = series
	result.OutputTokenBar.Series = series
	if hasTokens {
		result.InputTokenBar.Data = dashboardBarData(inputBarBuckets, series, firstBarBucket, lastBarBucket, barInterval)
		result.CachedTokenBar.Data = dashboardBarData(cachedBarBuckets, series, firstBarBucket, lastBarBucket, barInterval)
		result.OutputTokenBar.Data = dashboardBarData(outputBarBuckets, series, firstBarBucket, lastBarBucket, barInterval)
	}

	return result
}

func emptyDashboardBar(interval time.Duration, series []BarSeriesEntry) BarSeriesResponse {
	return BarSeriesResponse{Interval: int(interval.Seconds()), Series: series, Data: []BarSeriesPoint{}}
}

func dashboardBarValues(buckets map[int64]map[string]uint64, timestamp int64) map[string]uint64 {
	values := buckets[timestamp]
	if values == nil {
		values = make(map[string]uint64)
		buckets[timestamp] = values
	}
	return values
}

func dashboardBarData(buckets map[int64]map[string]uint64, series []BarSeriesEntry, first, last time.Time, interval time.Duration) []BarSeriesPoint {
	data := make([]BarSeriesPoint, 0, int(last.Sub(first)/interval)+1)
	for timestamp := first; !timestamp.After(last); timestamp = timestamp.Add(interval) {
		values := make(map[string]uint64, len(series))
		for _, entry := range series {
			values[entry.Key] = buckets[timestamp.Unix()][entry.Key]
		}
		data = append(data, BarSeriesPoint{Timestamp: timestamp.Unix(), Values: values})
	}
	return data
}

// QueryTimeSeries returns per-server message counts aggregated into time buckets.
func (c *ServerMessageCollector) QueryTimeSeries(window TimeWindow) ServerMessageTimeSeriesResponse {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now()
	cutoff := now.Add(-window.Duration())
	interval := aggregationInterval(window)
	intervalSeconds := int(interval.Seconds())
	firstBucket := cutoff.Truncate(interval)
	lastBucket := now.Truncate(interval)

	knownServers := make(map[string]struct{}, len(c.counters))
	for id := range c.counters {
		knownServers[id] = struct{}{}
	}

	bucketCounts := make(map[int64]map[string]uint64)
	hasValues := false
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		bucketTS := bucket.timestamp.Truncate(interval).Unix()
		counts := bucketCounts[bucketTS]
		if counts == nil {
			counts = make(map[string]uint64)
			bucketCounts[bucketTS] = counts
		}
		for serverID, count := range bucket.counts {
			if _, ok := knownServers[serverID]; ok {
				counts[serverID] += count
				if count > 0 {
					hasValues = true
				}
			}
		}
	}

	if !hasValues {
		return ServerMessageTimeSeriesResponse{
			Interval: intervalSeconds,
			Data:     []ServerMessageTimeSeriesPoint{},
		}
	}

	result := make([]ServerMessageTimeSeriesPoint, 0, int(lastBucket.Sub(firstBucket)/interval)+1)
	for t := firstBucket; !t.After(lastBucket); t = t.Add(interval) {
		existing := bucketCounts[t.Unix()]
		counts := make(map[string]uint64, len(existing))
		for serverID, count := range existing {
			counts[serverID] = count
		}
		result = append(result, ServerMessageTimeSeriesPoint{
			Timestamp: t.Unix(),
			Counts:    counts,
		})
	}

	return ServerMessageTimeSeriesResponse{
		Interval: intervalSeconds,
		Data:     result,
	}
}

// ServerMessageTimeSeriesResponse is the API response for server message time series.
type ServerMessageTimeSeriesResponse struct {
	Interval int                            `json:"interval"`
	Data     []ServerMessageTimeSeriesPoint `json:"data"`
}

// ServerMessageDirectionalTimeSeriesPoint represents a single data point with received/sent breakdown.
type ServerMessageDirectionalTimeSeriesPoint struct {
	Timestamp int64  `json:"timestamp"`
	Received  uint64 `json:"received"`
	Sent      uint64 `json:"sent"`
}

// ServerMessageDirectionalTimeSeriesResponse is the API response for directional server message time series.
type ServerMessageDirectionalTimeSeriesResponse struct {
	Interval int                                       `json:"interval"`
	Data     []ServerMessageDirectionalTimeSeriesPoint `json:"data"`
}

// QueryDirectionalTimeSeries returns aggregated received/sent message counts over time.
func (c *ServerMessageCollector) QueryDirectionalTimeSeries(window TimeWindow) ServerMessageDirectionalTimeSeriesResponse {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now()
	cutoff := now.Add(-window.Duration())
	interval := aggregationInterval(window)
	intervalSeconds := int(interval.Seconds())
	firstBucket := cutoff.Truncate(interval)
	lastBucket := now.Truncate(interval)

	bucketValues := make(map[int64]*ServerMessageDirectionalTimeSeriesPoint)
	hasValues := false
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		bucketTS := bucket.timestamp.Truncate(interval).Unix()
		point := bucketValues[bucketTS]
		if point == nil {
			point = &ServerMessageDirectionalTimeSeriesPoint{Timestamp: bucketTS}
			bucketValues[bucketTS] = point
		}
		for _, count := range bucket.received {
			point.Received += count
			if count > 0 {
				hasValues = true
			}
		}
		for _, count := range bucket.sent {
			point.Sent += count
			if count > 0 {
				hasValues = true
			}
		}
	}

	if !hasValues {
		return ServerMessageDirectionalTimeSeriesResponse{
			Interval: intervalSeconds,
			Data:     []ServerMessageDirectionalTimeSeriesPoint{},
		}
	}

	result := make([]ServerMessageDirectionalTimeSeriesPoint, 0, int(lastBucket.Sub(firstBucket)/interval)+1)
	for t := firstBucket; !t.After(lastBucket); t = t.Add(interval) {
		point := bucketValues[t.Unix()]
		if point == nil {
			point = &ServerMessageDirectionalTimeSeriesPoint{Timestamp: t.Unix()}
		}
		result = append(result, *point)
	}

	return ServerMessageDirectionalTimeSeriesResponse{
		Interval: intervalSeconds,
		Data:     result,
	}
}

// QueryServerBarSeries returns message counts grouped by server ID in coarse
// buckets suitable for overview bar charts.
func (c *ServerMessageCollector) QueryServerBarSeries(window TimeWindow) BarSeriesResponse {
	c.mu.RLock()
	defer c.mu.RUnlock()

	now := time.Now()
	cutoff := now.Add(-window.Duration())
	interval := BarAggregationInterval(window)
	firstBucket := cutoff.Truncate(interval)
	lastBucket := now.Truncate(interval)

	knownServers := make(map[string]struct{}, len(c.counters))
	for id := range c.counters {
		knownServers[id] = struct{}{}
	}

	serverSet := make(map[string]struct{})
	bucketValues := make(map[int64]map[string]uint64)
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		bucketTS := bucket.timestamp.Truncate(interval).Unix()
		values := bucketValues[bucketTS]
		if values == nil {
			values = make(map[string]uint64)
			bucketValues[bucketTS] = values
		}
		for serverID, count := range bucket.counts {
			if _, ok := knownServers[serverID]; !ok {
				continue
			}
			serverSet[serverID] = struct{}{}
			values[serverID] += count
		}
	}

	serverIDs := make([]string, 0, len(serverSet))
	for serverID := range serverSet {
		serverIDs = append(serverIDs, serverID)
	}
	sort.Strings(serverIDs)

	series := make([]BarSeriesEntry, 0, len(serverIDs))
	for _, serverID := range serverIDs {
		series = append(series, BarSeriesEntry{Key: serverID, Label: serverID})
	}

	data := make([]BarSeriesPoint, 0, int(lastBucket.Sub(firstBucket)/interval)+1)
	for t := firstBucket; !t.After(lastBucket); t = t.Add(interval) {
		values := make(map[string]uint64, len(serverIDs))
		existing := bucketValues[t.Unix()]
		for _, serverID := range serverIDs {
			values[serverID] = existing[serverID]
		}
		data = append(data, BarSeriesPoint{
			Timestamp: t.Unix(),
			Values:    values,
		})
	}

	return BarSeriesResponse{
		Interval: int(interval.Seconds()),
		Series:   series,
		Data:     data,
	}
}

// QueryAgentMessageBarSeries returns inbound/outbound message counts for one
// agent in coarse buckets suitable for the agent detail stacked bar chart.
func (c *ServerMessageCollector) QueryAgentMessageBarSeries(agentID string, window TimeWindow) BarSeriesResponse {
	c.mu.RLock()
	defer c.mu.RUnlock()

	agentID = strings.TrimSpace(agentID)
	interval := BarAggregationInterval(window)
	series := []BarSeriesEntry{
		{Key: "inbound", Label: "Inbound"},
		{Key: "outbound", Label: "Outbound"},
	}
	if agentID == "" {
		return BarSeriesResponse{
			Interval: int(interval.Seconds()),
			Series:   series,
			Data:     []BarSeriesPoint{},
		}
	}

	now := time.Now()
	cutoff := now.Add(-window.Duration())
	firstBucket := cutoff.Truncate(interval)
	lastBucket := now.Truncate(interval)

	bucketValues := make(map[int64]map[string]uint64)
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		bucketTS := bucket.timestamp.Truncate(interval).Unix()
		values := bucketValues[bucketTS]
		if values == nil {
			values = make(map[string]uint64)
			bucketValues[bucketTS] = values
		}
		values["inbound"] += bucket.agentReceived[agentID]
		values["outbound"] += bucket.agentSent[agentID]
	}

	data := make([]BarSeriesPoint, 0, int(lastBucket.Sub(firstBucket)/interval)+1)
	for t := firstBucket; !t.After(lastBucket); t = t.Add(interval) {
		values := map[string]uint64{
			"inbound":  0,
			"outbound": 0,
		}
		if existing := bucketValues[t.Unix()]; existing != nil {
			values["inbound"] = existing["inbound"]
			values["outbound"] = existing["outbound"]
		}
		data = append(data, BarSeriesPoint{
			Timestamp: t.Unix(),
			Values:    values,
		})
	}

	return BarSeriesResponse{
		Interval: int(interval.Seconds()),
		Series:   series,
		Data:     data,
	}
}

// AgentMessageSummary holds per-agent inbound/outbound totals within a time window.
type AgentMessageSummary struct {
	AgentID  string `json:"agentId"`
	Received uint64 `json:"received"`
	Sent     uint64 `json:"sent"`
	Total    uint64 `json:"total"`
}

// QueryAgentMessageSummary returns inbound/outbound totals for one agent.
func (c *ServerMessageCollector) QueryAgentMessageSummary(agentID string, window TimeWindow) AgentMessageSummary {
	c.mu.RLock()
	defer c.mu.RUnlock()

	agentID = strings.TrimSpace(agentID)
	summary := AgentMessageSummary{AgentID: agentID}
	if agentID == "" {
		return summary
	}

	cutoff := time.Now().Add(-window.Duration())
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		summary.Received += bucket.agentReceived[agentID]
		summary.Sent += bucket.agentSent[agentID]
	}
	summary.Total = summary.Received + summary.Sent
	return summary
}

// QueryAgentMessageDirectionalTimeSeries returns inbound/outbound message
// counts for one agent in trend-chart buckets.
func (c *ServerMessageCollector) QueryAgentMessageDirectionalTimeSeries(agentID string, window TimeWindow) ServerMessageDirectionalTimeSeriesResponse {
	c.mu.RLock()
	defer c.mu.RUnlock()

	agentID = strings.TrimSpace(agentID)
	interval := aggregationInterval(window)
	intervalSeconds := int(interval.Seconds())
	if agentID == "" {
		return ServerMessageDirectionalTimeSeriesResponse{
			Interval: intervalSeconds,
			Data:     []ServerMessageDirectionalTimeSeriesPoint{},
		}
	}

	now := time.Now()
	cutoff := now.Add(-window.Duration())
	firstBucket := cutoff.Truncate(interval)
	lastBucket := now.Truncate(interval)

	bucketValues := make(map[int64]*ServerMessageDirectionalTimeSeriesPoint)
	hasValues := false
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		bucketTS := bucket.timestamp.Truncate(interval).Unix()
		point := bucketValues[bucketTS]
		if point == nil {
			point = &ServerMessageDirectionalTimeSeriesPoint{Timestamp: bucketTS}
			bucketValues[bucketTS] = point
		}
		received := bucket.agentReceived[agentID]
		sent := bucket.agentSent[agentID]
		point.Received += received
		point.Sent += sent
		if received > 0 || sent > 0 {
			hasValues = true
		}
	}

	if !hasValues {
		return ServerMessageDirectionalTimeSeriesResponse{
			Interval: intervalSeconds,
			Data:     []ServerMessageDirectionalTimeSeriesPoint{},
		}
	}

	result := make([]ServerMessageDirectionalTimeSeriesPoint, 0, int(lastBucket.Sub(firstBucket)/interval)+1)
	for t := firstBucket; !t.After(lastBucket); t = t.Add(interval) {
		point := bucketValues[t.Unix()]
		if point == nil {
			point = &ServerMessageDirectionalTimeSeriesPoint{Timestamp: t.Unix()}
		}
		result = append(result, *point)
	}

	return ServerMessageDirectionalTimeSeriesResponse{
		Interval: intervalSeconds,
		Data:     result,
	}
}

// AgentTokenSummary holds per-agent request and token totals within a time window.
type AgentTokenSummary struct {
	AgentID string `json:"agentId"`
	Count   uint64 `json:"count"`
	Input   uint64 `json:"input"`
	Cached  uint64 `json:"cached"`
	Output  uint64 `json:"output"`
	Total   uint64 `json:"total"`
}

// AgentTokenTimeSeriesResponse is the API response for agent token trends.
type AgentTokenTimeSeriesResponse struct {
	Interval int                   `json:"interval"`
	Data     []TimeSeriesDataPoint `json:"data"`
}

// QueryAgentTokenSummary returns request and token totals for one agent.
func (c *ServerMessageCollector) QueryAgentTokenSummary(agentID string, window TimeWindow) AgentTokenSummary {
	c.mu.RLock()
	defer c.mu.RUnlock()

	agentID = strings.TrimSpace(agentID)
	summary := AgentTokenSummary{AgentID: agentID}
	if agentID == "" {
		return summary
	}

	cutoff := time.Now().Add(-window.Duration())
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		for _, stats := range bucket.agentTokens[agentID] {
			summary.Count += stats.Count
			summary.Input += stats.Input
			summary.Cached += stats.CachedInput
			summary.Output += stats.Output
		}
	}
	// Cached is a subset of input; exclude it from the total to avoid
	// double-counting.
	summary.Total = summary.Input + summary.Output
	return summary
}

// QueryAgentTokenTimeSeries returns aggregate token usage for one agent over time.
func (c *ServerMessageCollector) QueryAgentTokenTimeSeries(agentID string, window TimeWindow) AgentTokenTimeSeriesResponse {
	c.mu.RLock()
	defer c.mu.RUnlock()

	agentID = strings.TrimSpace(agentID)
	interval := TimeSeriesAggregationInterval(window)
	intervalSeconds := int(interval.Seconds())
	if agentID == "" {
		return AgentTokenTimeSeriesResponse{
			Interval: intervalSeconds,
			Data:     []TimeSeriesDataPoint{},
		}
	}

	now := time.Now()
	cutoff := now.Add(-window.Duration())
	firstBucket := cutoff.Truncate(interval)
	lastBucket := now.Truncate(interval)

	bucketValues := make(map[int64]*TimeSeriesDataPoint)
	hasValues := false
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		bucketTS := bucket.timestamp.Truncate(interval).Unix()
		point := bucketValues[bucketTS]
		if point == nil {
			point = &TimeSeriesDataPoint{Timestamp: bucketTS}
			bucketValues[bucketTS] = point
		}
		for _, stats := range bucket.agentTokens[agentID] {
			point.Count += stats.Count
			point.Input += stats.Input
			point.Cached += stats.CachedInput
			point.Output += stats.Output
			if stats.Count > 0 || stats.Input > 0 || stats.CachedInput > 0 || stats.Output > 0 {
				hasValues = true
			}
		}
	}

	if !hasValues {
		return AgentTokenTimeSeriesResponse{
			Interval: intervalSeconds,
			Data:     []TimeSeriesDataPoint{},
		}
	}

	result := make([]TimeSeriesDataPoint, 0, int(lastBucket.Sub(firstBucket)/interval)+1)
	for t := firstBucket; !t.After(lastBucket); t = t.Add(interval) {
		point := bucketValues[t.Unix()]
		if point == nil {
			point = &TimeSeriesDataPoint{Timestamp: t.Unix()}
		}
		result = append(result, *point)
	}

	return AgentTokenTimeSeriesResponse{
		Interval: intervalSeconds,
		Data:     result,
	}
}

// QueryAgentModelTokenBarSeries returns one token metric grouped by model for one agent.
func (c *ServerMessageCollector) QueryAgentModelTokenBarSeries(agentID string, window TimeWindow, tokenMetric string) BarSeriesResponse {
	c.mu.RLock()
	defer c.mu.RUnlock()

	agentID = strings.TrimSpace(agentID)
	interval := BarAggregationInterval(window)
	empty := BarSeriesResponse{
		Interval: int(interval.Seconds()),
		Series:   []BarSeriesEntry{},
		Data:     []BarSeriesPoint{},
	}
	if agentID == "" {
		return empty
	}

	now := time.Now()
	cutoff := now.Add(-window.Duration())
	firstBucket := cutoff.Truncate(interval)
	lastBucket := now.Truncate(interval)

	modelSet := make(map[string]struct{})
	bucketValues := make(map[int64]map[string]uint64)
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		modelStats := bucket.agentTokens[agentID]
		if len(modelStats) == 0 {
			continue
		}
		bucketTS := bucket.timestamp.Truncate(interval).Unix()
		values := bucketValues[bucketTS]
		if values == nil {
			values = make(map[string]uint64)
			bucketValues[bucketTS] = values
		}
		for model, stats := range modelStats {
			modelSet[model] = struct{}{}
			values[model] += agentTokenMetricValue(stats, tokenMetric)
		}
	}

	models := make([]string, 0, len(modelSet))
	for model := range modelSet {
		models = append(models, model)
	}
	sort.Strings(models)
	if len(models) == 0 {
		return empty
	}

	series := make([]BarSeriesEntry, 0, len(models))
	for _, model := range models {
		series = append(series, BarSeriesEntry{Key: model, Label: shortModelLabel(model)})
	}

	data := make([]BarSeriesPoint, 0, int(lastBucket.Sub(firstBucket)/interval)+1)
	for t := firstBucket; !t.After(lastBucket); t = t.Add(interval) {
		values := make(map[string]uint64, len(models))
		existing := bucketValues[t.Unix()]
		for _, model := range models {
			values[model] = existing[model]
		}
		data = append(data, BarSeriesPoint{
			Timestamp: t.Unix(),
			Values:    values,
		})
	}

	return BarSeriesResponse{
		Interval: int(interval.Seconds()),
		Series:   series,
		Data:     data,
	}
}

func agentTokenMetricValue(stats TokenUsageStats, tokenMetric string) uint64 {
	switch strings.TrimSpace(tokenMetric) {
	case "cached":
		return stats.CachedInput
	case "output":
		return stats.Output
	default:
		return stats.Input
	}
}

func shortModelLabel(model string) string {
	parts := strings.Split(strings.TrimSpace(model), "/")
	for i := len(parts) - 1; i >= 0; i-- {
		if parts[i] != "" {
			return parts[i]
		}
	}
	return model
}

// ServerMessageWindowSummaryItem holds per-server received/sent totals within a time window.
type ServerMessageWindowSummaryItem struct {
	ServerID   string `json:"serverId"`
	ServerName string `json:"serverName"`
	Received   uint64 `json:"received"`
	Sent       uint64 `json:"sent"`
}

// QueryWindowSummary returns per-server received/sent totals within the given time window.
func (c *ServerMessageCollector) QueryWindowSummary(window TimeWindow) []ServerMessageWindowSummaryItem {
	c.mu.RLock()
	defer c.mu.RUnlock()

	cutoff := time.Now().Add(-window.Duration())

	// Accumulate per-server received/sent from buckets.
	receivedByServer := make(map[string]uint64)
	sentByServer := make(map[string]uint64)
	for _, bucket := range c.buckets {
		if bucket.timestamp.Before(cutoff) {
			continue
		}
		for serverID, count := range bucket.received {
			receivedByServer[serverID] += count
		}
		for serverID, count := range bucket.sent {
			sentByServer[serverID] += count
		}
	}

	// Build result for all known servers.
	result := make([]ServerMessageWindowSummaryItem, 0, len(c.counters))
	for id := range c.counters {
		r := receivedByServer[id]
		s := sentByServer[id]
		if r == 0 && s == 0 {
			continue
		}
		result = append(result, ServerMessageWindowSummaryItem{
			ServerID: id,
			Received: r,
			Sent:     s,
		})
	}
	return result
}

// Summary returns per-server message statistics.
func (c *ServerMessageCollector) Summary() []ServerMessageSummary {
	c.mu.RLock()
	defer c.mu.RUnlock()

	now := time.Now()
	result := make([]ServerMessageSummary, 0, len(c.counters))
	for id, cc := range c.counters {
		today := cc.today
		thisHour := cc.thisHour
		receivedToday := cc.receivedToday
		receivedThisHour := cc.receivedThisHour
		sentToday := cc.sentToday
		sentThisHour := cc.sentThisHour
		if cc.lastDay != now.YearDay() {
			today = 0
			receivedToday = 0
			sentToday = 0
		}
		if cc.lastHour != now.Hour() {
			thisHour = 0
			receivedThisHour = 0
			sentThisHour = 0
		}

		result = append(result, ServerMessageSummary{
			ServerID:         id,
			Total:            cc.total,
			Today:            today,
			ThisHour:         thisHour,
			ReceivedTotal:    cc.receivedTotal,
			ReceivedToday:    receivedToday,
			ReceivedThisHour: receivedThisHour,
			SentTotal:        cc.sentTotal,
			SentToday:        sentToday,
			SentThisHour:     sentThisHour,
		})
	}
	return result
}

// ServerMessageSummary holds message statistics for a single server.
type ServerMessageSummary struct {
	ServerID   string `json:"serverId"`
	ServerName string `json:"serverName"`

	// Aggregate counters (received + sent).
	Total    uint64 `json:"total"`
	Today    uint64 `json:"today"`
	ThisHour uint64 `json:"thisHour"`

	// Per-direction counters.
	ReceivedTotal    uint64 `json:"receivedTotal"`
	ReceivedToday    uint64 `json:"receivedToday"`
	ReceivedThisHour uint64 `json:"receivedThisHour"`
	SentTotal        uint64 `json:"sentTotal"`
	SentToday        uint64 `json:"sentToday"`
	SentThisHour     uint64 `json:"sentThisHour"`
}
