package runtimeevents

import (
	"encoding/json"
	"strings"
	"time"
)

const maxTextLen = 2000

// EventData is the interface for kind-specific event data. Each concrete type
// declares its expected Kind and knows how to scrub itself.
type EventData interface {
	EventKind() Kind
	Scrubbed() EventData
}

// DataAs is a generic helper for type-safe data access.
func DataAs[T EventData](ev Event) (T, bool) {
	d, ok := ev.Data.(T)
	return d, ok
}

// --- Concrete data types ----------------------------------------------------

// LifecycleData carries session lifecycle activity and detail.
type LifecycleData struct {
	kind       Kind
	Activity   string         `json:"activity,omitempty"`
	Detail     string         `json:"detail,omitempty"`
	Text       string         `json:"text,omitempty"`
	PromptKind string         `json:"prompt_kind,omitempty"`
	EventType  string         `json:"event_type,omitempty"`
	Metadata   map[string]any `json:"metadata,omitempty"`
}

// NewLifecycleData creates lifecycle-style event data for runtime/session,
// delivery, recovery, and channel activity kinds that share activity/detail.
func NewLifecycleData(kind Kind, activity, detail string) *LifecycleData {
	return &LifecycleData{
		kind:     kind,
		Activity: activity,
		Detail:   detail,
	}
}

// NewLifecycleInputData creates a lifecycle-style event data payload that also
// carries a prompt input projection for delivery-related activity rows.
func NewLifecycleInputData(kind Kind, activity, detail, text, promptKind, eventType string, metadata map[string]any) *LifecycleData {
	return &LifecycleData{
		kind:       kind,
		Activity:   activity,
		Detail:     detail,
		Text:       text,
		PromptKind: promptKind,
		EventType:  eventType,
		Metadata:   metadata,
	}
}

// EventKind returns the concrete event kind carried by this lifecycle payload.
func (d *LifecycleData) EventKind() Kind { return d.kind }

// Scrubbed returns a copy with user-visible text fields truncated.
func (d *LifecycleData) Scrubbed() EventData {
	return &LifecycleData{
		kind:       d.kind,
		Activity:   truncate(d.Activity),
		Detail:     truncate(d.Detail),
		Text:       truncate(d.Text),
		PromptKind: d.PromptKind,
		EventType:  d.EventType,
		Metadata:   scrubMetadata(d.Metadata),
	}
}

// WakeData carries wake-specific fields. WakeToken/MessageID/Target live in EventTrace.
type WakeData struct {
	kind   Kind
	Detail string `json:"detail,omitempty"`
}

// NewWakeData creates data for wake-to-reply lifecycle events.
func NewWakeData(kind Kind, detail string) *WakeData {
	return &WakeData{
		kind:   kind,
		Detail: detail,
	}
}

// EventKind returns the wake lifecycle kind carried by this payload.
func (d *WakeData) EventKind() Kind { return d.kind }

// Scrubbed returns a copy with long detail text truncated.
func (d *WakeData) Scrubbed() EventData {
	return &WakeData{
		kind:   d.kind,
		Detail: truncate(d.Detail),
	}
}

// RuntimeDiagnosticData carries legacy EventClass and detail for runtime events.
type RuntimeDiagnosticData struct {
	kind       Kind
	EventClass string `json:"event_class,omitempty"`
	Detail     string `json:"detail,omitempty"`
}

// NewRuntimeDiagnosticData creates diagnostic data that preserves a legacy
// EventClass while the event Kind carries the structured meaning.
func NewRuntimeDiagnosticData(kind Kind, eventClass, detail string) *RuntimeDiagnosticData {
	return &RuntimeDiagnosticData{
		kind:       kind,
		EventClass: eventClass,
		Detail:     detail,
	}
}

// EventKind returns the diagnostic event kind carried by this payload.
func (d *RuntimeDiagnosticData) EventKind() Kind { return d.kind }

// Scrubbed returns a copy with diagnostic detail truncated.
func (d *RuntimeDiagnosticData) Scrubbed() EventData {
	return &RuntimeDiagnosticData{
		kind:       d.kind,
		EventClass: d.EventClass,
		Detail:     truncate(d.Detail),
	}
}

// TurnTextData carries thinking/text/error content.
type TurnTextData struct {
	kind      Kind
	Text      string         `json:"text,omitempty"`
	Title     string         `json:"title,omitempty"`
	SectionID string         `json:"section_id,omitempty"`
	Metadata  map[string]any `json:"metadata,omitempty"`
}

// NewTurnTextData creates turn-level thinking/text/error data.
func NewTurnTextData(kind Kind, text string) *TurnTextData {
	return &TurnTextData{
		kind: kind,
		Text: text,
	}
}

// NewTurnTextDataWithMeta creates turn-level text data with optional display metadata.
func NewTurnTextDataWithMeta(kind Kind, text string, title string, sectionID string) *TurnTextData {
	return &TurnTextData{
		kind:      kind,
		Text:      text,
		Title:     title,
		SectionID: sectionID,
	}
}

// NewTurnTextDataWithMetadata creates turn-level text data with machine-readable metadata.
func NewTurnTextDataWithMetadata(kind Kind, text string, metadata map[string]any) *TurnTextData {
	return &TurnTextData{
		kind:     kind,
		Text:     text,
		Metadata: cloneMetadata(metadata),
	}
}

// EventKind returns the turn text kind carried by this payload.
func (d *TurnTextData) EventKind() Kind { return d.kind }

// Scrubbed returns a copy with long text truncated.
func (d *TurnTextData) Scrubbed() EventData {
	return &TurnTextData{
		kind:      d.kind,
		Text:      truncate(d.Text),
		Title:     truncate(d.Title),
		SectionID: d.SectionID,
		Metadata:  scrubMetadata(d.Metadata),
	}
}

// ToolCallData carries tool invocation or output details.
type ToolCallData struct {
	kind       Kind
	ToolName   string         `json:"tool_name,omitempty"`
	ToolInput  string         `json:"tool_input,omitempty"`
	ToolCallID string         `json:"tool_call_id,omitempty"`
	Metadata   map[string]any `json:"metadata,omitempty"`
}

// NewToolCallData creates tool-call data for KindTurnToolCall.
func NewToolCallData(toolName, toolInput string) *ToolCallData {
	return NewToolEventData(KindTurnToolCall, toolName, toolInput)
}

// NewToolEventData creates tool event data for either tool-call or tool-output
// event kinds.
func NewToolEventData(kind Kind, toolName, toolInput string) *ToolCallData {
	return NewToolEventDataWithCallID(kind, toolName, toolInput, "")
}

// NewToolEventDataWithCallID creates a tool event and preserves the runtime
// tool-call identity used to associate tool start and output activity rows.
func NewToolEventDataWithCallID(kind Kind, toolName, toolInput, toolCallID string) *ToolCallData {
	return NewToolEventDataWithCallIDAndMetadata(kind, toolName, toolInput, toolCallID, nil)
}

// NewToolEventDataWithCallIDAndMetadata creates a tool event and preserves
// machine-readable metadata alongside the human-readable tool text.
func NewToolEventDataWithCallIDAndMetadata(kind Kind, toolName, toolInput, toolCallID string, metadata map[string]any) *ToolCallData {
	return &ToolCallData{
		kind:       kind,
		ToolName:   toolName,
		ToolInput:  toolInput,
		ToolCallID: toolCallID,
		Metadata:   cloneMetadata(metadata),
	}
}

// EventKind returns the tool event kind, defaulting old zero-value payloads to
// KindTurnToolCall for compatibility.
func (d *ToolCallData) EventKind() Kind {
	if d.kind == "" {
		return KindTurnToolCall
	}
	return d.kind
}

// Scrubbed returns a copy with tool names truncated and sensitive JSON tool
// input fields redacted.
func (d *ToolCallData) Scrubbed() EventData {
	return &ToolCallData{
		kind:       d.EventKind(),
		ToolName:   truncate(d.ToolName),
		ToolInput:  scrubToolInput(d.ToolInput),
		ToolCallID: truncate(d.ToolCallID),
		Metadata:   cloneMetadata(d.Metadata),
	}
}

func cloneMetadata(metadata map[string]any) map[string]any {
	if len(metadata) == 0 {
		return nil
	}
	out := make(map[string]any, len(metadata))
	for key, value := range metadata {
		out[key] = value
	}
	return out
}

// PromptInputData carries prompt input recording.
type PromptInputData struct {
	Text       string `json:"text,omitempty"`
	PromptKind string `json:"prompt_kind,omitempty"`
	EventType  string `json:"event_type,omitempty"`
}

// NewPromptInputData creates data for a prompt input record emitted before a
// runtime turn starts.
func NewPromptInputData(text, promptKind, eventType string) *PromptInputData {
	return &PromptInputData{
		Text:       text,
		PromptKind: promptKind,
		EventType:  eventType,
	}
}

// EventKind returns KindPromptInput.
func (d *PromptInputData) EventKind() Kind { return KindPromptInput }

// Scrubbed returns a copy with prompt text truncated.
func (d *PromptInputData) Scrubbed() EventData {
	return &PromptInputData{
		Text:       truncate(d.Text),
		PromptKind: d.PromptKind,
		EventType:  d.EventType,
	}
}

// RecoveryPointData carries recovery point details.
type RecoveryPointData struct {
	ThreadID    string `json:"thread_id,omitempty"`
	Strategy    string `json:"strategy,omitempty"`
	RecoveryRef string `json:"recovery_ref,omitempty"`
}

// NewRecoveryPointData creates data for a prepared runtime recovery point.
func NewRecoveryPointData(threadID, strategy, ref string) *RecoveryPointData {
	return &RecoveryPointData{
		ThreadID:    threadID,
		Strategy:    strategy,
		RecoveryRef: ref,
	}
}

// EventKind returns KindRecoveryPointPrepared.
func (d *RecoveryPointData) EventKind() Kind { return KindRecoveryPointPrepared }

// Scrubbed returns the recovery point data unchanged; recovery refs are
// runtime-owned opaque state and are already scoped to local daemon handling.
func (d *RecoveryPointData) Scrubbed() EventData { return d }

// UsageTokensData carries LLM token usage.
type UsageTokensData struct {
	APIKey              string        `json:"api_key,omitempty"`
	InputTokens         uint64        `json:"input_tokens,omitempty"`
	CachedInputTokens   uint64        `json:"cached_input_tokens,omitempty"`
	OutputTokens        uint64        `json:"output_tokens,omitempty"`
	ActiveContextTokens uint64        `json:"active_context_tokens,omitempty"`
	ContextWindowTokens uint64        `json:"context_window_tokens,omitempty"`
	Duration            time.Duration `json:"duration,omitempty"`
}

// NewUsageTokensData creates token-usage data for metrics and activity sinks.
func NewUsageTokensData(apiKey string, input, cachedInput, output uint64, duration time.Duration) *UsageTokensData {
	return &UsageTokensData{
		APIKey:            apiKey,
		InputTokens:       input,
		CachedInputTokens: cachedInput,
		OutputTokens:      output,
		Duration:          duration,
	}
}

// EventKind returns KindUsageTokens.
func (d *UsageTokensData) EventKind() Kind { return KindUsageTokens }

// Scrubbed returns a copy with the raw API key redacted.
func (d *UsageTokensData) Scrubbed() EventData {
	return &UsageTokensData{
		APIKey:              "***",
		InputTokens:         d.InputTokens,
		CachedInputTokens:   d.CachedInputTokens,
		OutputTokens:        d.OutputTokens,
		ActiveContextTokens: d.ActiveContextTokens,
		ContextWindowTokens: d.ContextWindowTokens,
		Duration:            d.Duration,
	}
}

// HeartbeatData carries runtime liveness snapshot.
type HeartbeatData struct {
	State            string    `json:"state,omitempty"`
	IsIdle           bool      `json:"is_idle,omitempty"`
	InboxDepth       int       `json:"inbox_depth,omitempty"`
	AckedCount       int       `json:"surfaced_count,omitempty"` // legacy persisted field name
	LastEventAt      time.Time `json:"last_event_at,omitempty"`
	PendingWake      bool      `json:"pending_wake,omitempty"`
	PendingMessageID string    `json:"pending_message_id,omitempty"`
}

// NewHeartbeatData creates an empty heartbeat snapshot for callers to populate.
func NewHeartbeatData() *HeartbeatData { return &HeartbeatData{} }

// EventKind returns KindRuntimeHeartbeat.
func (d *HeartbeatData) EventKind() Kind { return KindRuntimeHeartbeat }

// Scrubbed returns heartbeat data unchanged; it contains only runtime state
// counters and IDs already present in the event context/trace.
func (d *HeartbeatData) Scrubbed() EventData { return d }

func scrubMetadata(metadata map[string]any) map[string]any {
	if len(metadata) == 0 {
		return nil
	}
	out := make(map[string]any, len(metadata))
	for k, v := range metadata {
		lk := strings.ToLower(k)
		if strings.Contains(lk, "key") || strings.Contains(lk, "token") ||
			strings.Contains(lk, "secret") || strings.Contains(lk, "password") ||
			strings.Contains(lk, "authorization") {
			out[k] = "***"
			continue
		}
		out[k] = scrubValue(v)
	}
	return out
}

// MapData is a compatibility wrapper for map[string]any payloads during migration.
type MapData struct {
	kind Kind
	Map  map[string]any `json:"map,omitempty"`
}

// NewMapData wraps an arbitrary map payload for a runtime event kind whose data
// shape has not yet been promoted to a concrete EventData type.
func NewMapData(kind Kind, m map[string]any) *MapData {
	return &MapData{
		kind: kind,
		Map:  m,
	}
}

// EventKind returns the event kind associated with this map payload.
func (d *MapData) EventKind() Kind { return d.kind }

// Scrubbed returns a copy with obvious secret-bearing fields redacted and long
// string values truncated.
func (d *MapData) Scrubbed() EventData {
	out := make(map[string]any, len(d.Map))
	for k, v := range d.Map {
		lk := strings.ToLower(k)
		if strings.Contains(lk, "key") || strings.Contains(lk, "token") ||
			strings.Contains(lk, "secret") || strings.Contains(lk, "password") ||
			strings.Contains(lk, "authorization") {
			out[k] = "***"
			continue
		}
		out[k] = scrubValue(v)
	}
	return &MapData{
		kind: d.kind,
		Map:  out,
	}
}

// RawData is a fallback for unknown kinds during JSON deserialization.
type RawData struct {
	Kind Kind            `json:"-"`
	Raw  json.RawMessage `json:"-"`
}

// EventKind returns the unknown event kind preserved with this raw payload.
func (d *RawData) EventKind() Kind { return d.Kind }

// Scrubbed returns RawData unchanged because the raw bytes are not retained in
// marshaled diagnostic history.
func (d *RawData) Scrubbed() EventData { return d }

// --- Helpers ----------------------------------------------------------------

func truncate(s string) string {
	if len(s) <= maxTextLen {
		return s
	}
	return s[:maxTextLen-len("...(truncated)")] + "...(truncated)"
}

func scrubValue(v any) any {
	switch val := v.(type) {
	case string:
		return truncate(val)
	case map[string]any:
		out := make(map[string]any, len(val))
		for k, v2 := range val {
			out[k] = scrubValue(v2)
		}
		return out
	default:
		return v
	}
}

func scrubToolInput(input string) string {
	trimmed := strings.TrimSpace(input)
	if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
		return input
	}

	var decoded any
	if err := json.Unmarshal([]byte(trimmed), &decoded); err != nil {
		return input
	}

	encoded, err := json.Marshal(scrubToolInputValue(decoded))
	if err != nil {
		return input
	}
	return string(encoded)
}

func scrubToolInputValue(v any) any {
	switch val := v.(type) {
	case string:
		return val
	case []any:
		out := make([]any, len(val))
		for i, item := range val {
			out[i] = scrubToolInputValue(item)
		}
		return out
	case map[string]any:
		out := make(map[string]any, len(val))
		for k, item := range val {
			lk := strings.ToLower(k)
			if strings.Contains(lk, "key") || strings.Contains(lk, "token") ||
				strings.Contains(lk, "secret") || strings.Contains(lk, "password") ||
				strings.Contains(lk, "authorization") {
				out[k] = "***"
				continue
			}
			out[k] = scrubToolInputValue(item)
		}
		return out
	default:
		return v
	}
}
