// This file implements structured telemetry collection for a loop run
// (observability -- structured telemetry collection). A lightweight accumulator observes the AgentEvents the loop
// already emits and folds them into a compact summary — per-tool wall-clock
// durations, turn count, truncation count, compaction count, and the latest
// context-utilization ratio — that is surfaced once at run end as a
// TelemetryEvent (just before agent_end).
//
// The design is deliberately additive: telemetry rides the existing AgentEvent
// family and the existing stream-json output path, so no new dependency
// (Prometheus/OTLP) is introduced and existing stream-json consumers that do
// not know the "telemetry" event type keep working unchanged. Collection is
// passive — observing an event never changes loop behavior.
package runtime

import (
	"sync"
	"time"

	"github.com/smallnest/pigo/internal/agentcore"
)

// telemetry is the per-run accumulator. Most fields are folded from events on
// the single runLoop goroutine, but tool_execution_* events fire from the
// parallel tool-batch goroutines (ExecuteToolCalls), so all mutation goes
// through mu.
type telemetry struct {
	mu              sync.Mutex
	turns           int
	truncationCount int
	compactionCount int

	// toolStarts maps an in-flight tool call id to the wall-clock time its
	// execution began, so the matching end event can compute a duration. Keying
	// by call id (not tool name) keeps parallel tool batches correct.
	toolStarts map[string]time.Time
	// toolTimings aggregates finished tool durations by tool name.
	toolTimings map[string]agentcore.ToolTiming

	// contextTokens / contextWindow capture the most recent context accounting so
	// the summary can report the latest utilization ratio. contextWindow == 0
	// means the window is unknown (utilization is then reported as 0).
	contextTokens int
	contextWindow int

	// now is the clock, injectable for deterministic tests. Defaults to
	// time.Now.
	now func() time.Time
}

// newTelemetry constructs an empty accumulator using the wall clock.
func newTelemetry() *telemetry {
	return &telemetry{
		toolStarts:  make(map[string]time.Time),
		toolTimings: make(map[string]agentcore.ToolTiming),
		now:         time.Now,
	}
}

// observe folds a single emitted event into the accumulator. It is a no-op for
// event types that carry no telemetry signal, and it never mutates the event.
func (t *telemetry) observe(ev agentcore.AgentEvent) {
	if t == nil {
		return
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	switch e := ev.(type) {
	case agentcore.TurnStartEvent:
		t.turns++
	case agentcore.ToolExecutionStartEvent:
		t.toolStarts[e.ToolCallID] = t.now()
	case agentcore.ToolExecutionEndEvent:
		start, ok := t.toolStarts[e.ToolCallID]
		if !ok {
			return
		}
		delete(t.toolStarts, e.ToolCallID)
		elapsed := t.now().Sub(start).Milliseconds()
		if elapsed < 0 {
			elapsed = 0
		}
		agg := t.toolTimings[e.ToolName]
		agg.Count++
		agg.TotalMs += elapsed
		t.toolTimings[e.ToolName] = agg
	case agentcore.TurnEndEvent:
		if e.Message.StopReason == agentcore.StopReasonLength {
			t.truncationCount++
		}
	case agentcore.CompactionEvent:
		// Count only successful compactions; a failed one (ErrorMessage set) left
		// the context unchanged.
		if e.ErrorMessage == "" {
			t.compactionCount++
		}
	}
}

// recordContext captures the latest context-token usage and window so the
// summary can report the current utilization ratio. A non-positive window is
// treated as unknown.
func (t *telemetry) recordContext(tokens, window int) {
	if t == nil {
		return
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	t.contextTokens = tokens
	if window > 0 {
		t.contextWindow = window
	}
}

// summary materializes the accumulated metrics into a TelemetryEvent. The
// per-tool map is copied so the emitted event does not alias the accumulator's
// live state.
func (t *telemetry) summary() agentcore.TelemetryEvent {
	t.mu.Lock()
	defer t.mu.Unlock()
	timings := make(map[string]agentcore.ToolTiming, len(t.toolTimings))
	for name, v := range t.toolTimings {
		timings[name] = v
	}
	var utilization float64
	if t.contextWindow > 0 {
		utilization = float64(t.contextTokens) / float64(t.contextWindow)
	}
	return agentcore.TelemetryEvent{
		Turns:              t.turns,
		ToolDurationsMs:    timings,
		TruncationCount:    t.truncationCount,
		CompactionCount:    t.compactionCount,
		ContextUtilization: utilization,
		ContextTokens:      t.contextTokens,
		ContextWindow:      t.contextWindow,
	}
}
