package openai

import (
	"bytes"
	"encoding/json"
	iop "iop/proto/gen/iop"
	"strings"
)

// providerAssembledObservation is the Edge-internal human-readable view of a
// provider Chat Completions/Responses body assembled by providerChatAssembler.
// It feeds Edge-local logging and usage metrics only and is never written to
// the caller response; pure passthrough relays provider bytes unmodified.
type providerAssembledObservation struct {
	Content       string   `json:"content,omitempty"`
	Reasoning     string   `json:"reasoning,omitempty"`
	ToolCallNames []string `json:"tool_call_names,omitempty"`
	BodyBytes     int      `json:"body_bytes"`
}

// providerChatAssembler accumulates a human-readable view of the provider
// Chat Completions response for the assembled observation. It
// tolerates non-JSON and partial payloads: whatever cannot be parsed simply
// yields an empty summary.
type providerChatAssembler struct {
	streaming          bool
	providerWire       string
	nonStreamingParsed bool
	bodyBytes          int
	pending            []byte
	content            strings.Builder
	reasoning          strings.Builder
	toolCallNames      []string
	// usage holds provider-reported token counts observed in the passthrough
	// body. It feeds Edge-internal metrics only.
	usage usageObservation
}

// providerChatDeltaEnvelope matches the provider fields the assembler
// observes; streaming uses delta, non-streaming uses message.
type providerChatDeltaEnvelope struct {
	Content          string `json:"content"`
	ReasoningContent string `json:"reasoning_content"`
	Reasoning        string `json:"reasoning"`
	ToolCalls        []struct {
		Function struct {
			Name string `json:"name"`
		} `json:"function"`
	} `json:"tool_calls"`
}

// providerUsageEnvelope matches the OpenAI-compatible usage object from
// both Chat Completions and Responses. Chat Completions uses prompt_tokens/
// completion_tokens; Responses uses input_tokens/output_tokens. The optional
// detail objects carry reasoning and cached-input tokens for both formats.
type providerUsageEnvelope struct {
	PromptTokens        int `json:"prompt_tokens"`
	CompletionTokens    int `json:"completion_tokens"`
	InputTokens         int `json:"input_tokens"`
	OutputTokens        int `json:"output_tokens"`
	PromptTokensDetails *struct {
		CachedTokens int `json:"cached_tokens"`
	} `json:"prompt_tokens_details"`
	CompletionTokensDetails *struct {
		ReasoningTokens int `json:"reasoning_tokens"`
	} `json:"completion_tokens_details"`
	InputTokensDetails *struct {
		CachedTokens int `json:"cached_tokens"`
	} `json:"input_tokens_details"`
	OutputTokensDetails *struct {
		ReasoningTokens int `json:"reasoning_tokens"`
	} `json:"output_tokens_details"`
}

// recordUsage stores the latest provider-reported usage. The provider tunnel
// body is never mutated; this observation feeds Edge-internal metrics only
// (SDD S05/D04). Chat Completions keys (prompt_tokens/completion_tokens) take
// precedence; if absent, Responses keys (input_tokens/output_tokens) are used.
func (a *providerChatAssembler) recordUsage(u *providerUsageEnvelope) {
	if u == nil {
		return
	}
	a.usage.providerReported = true
	// Prefer Chat Completions keys; fall back to Responses keys.
	if u.PromptTokens != 0 {
		a.usage.inputTokens = u.PromptTokens
		a.usage.outputTokens = u.CompletionTokens
		if d := u.PromptTokensDetails; d != nil {
			a.usage.cachedInputTokens = d.CachedTokens
		}
		if d := u.CompletionTokensDetails; d != nil {
			a.usage.reasoningTokens = d.ReasoningTokens
		}
		return
	}
	if u.InputTokens != 0 {
		a.usage.inputTokens = u.InputTokens
		a.usage.outputTokens = u.OutputTokens
		if d := u.InputTokensDetails; d != nil {
			a.usage.cachedInputTokens = d.CachedTokens
		}
		if d := u.OutputTokensDetails; d != nil {
			a.usage.reasoningTokens = d.ReasoningTokens
		}
	}
}

func (a *providerChatAssembler) recordMessagesUsage(u *providerUsageEnvelope) {
	if u == nil {
		return
	}
	a.usage.providerReported = true
	if u.InputTokens != 0 {
		a.usage.inputTokens = u.InputTokens
	}
	if u.OutputTokens != 0 {
		a.usage.outputTokens = u.OutputTokens
	}
}

// recordProtoUsage stores usage carried on a provider tunnel USAGE frame. Like
// recordUsage it observes for metrics only and never alters the response body.
func (a *providerChatAssembler) recordProtoUsage(u *iop.Usage) {
	if u == nil {
		return
	}
	a.usage.providerReported = true
	a.usage.inputTokens = int(u.GetInputTokens())
	a.usage.outputTokens = int(u.GetOutputTokens())
	a.usage.reasoningTokens = int(u.GetReasoningTokens())
	a.usage.cachedInputTokens = int(u.GetCachedInputTokens())
}

// usageObservation returns the observed token usage plus the observed reasoning
// character count used by auxiliary and estimated-token reasoning metrics.
func (a *providerChatAssembler) usageObservation() usageObservation {
	obs := a.usage
	obs.reasoningChars = a.reasoning.Len()
	return obs
}

// finalizeUsageObservation idempotently parses a complete non-streaming body
// before returning the final observed usage snapshot.
func (a *providerChatAssembler) finalizeUsageObservation() usageObservation {
	a.observation()
	return a.usageObservation()
}

func (a *providerChatAssembler) Write(chunk []byte) {
	a.bodyBytes += len(chunk)
	a.pending = append(a.pending, chunk...)
	if !a.streaming {
		return
	}
	for {
		idx := bytes.IndexByte(a.pending, '\n')
		if idx < 0 {
			return
		}
		line := strings.TrimRight(string(a.pending[:idx]), "\r")
		a.pending = a.pending[idx+1:]
		a.consumeSSELine(line)
	}
}

func (a *providerChatAssembler) consumeSSELine(line string) {
	payload, ok := strings.CutPrefix(line, "data:")
	if !ok {
		return
	}
	payload = strings.TrimSpace(payload)
	if payload == "" || payload == "[DONE]" {
		return
	}
	var chunk struct {
		Choices []struct {
			Delta providerChatDeltaEnvelope `json:"delta"`
		} `json:"choices"`
		Usage *providerUsageEnvelope `json:"usage"`
	}
	if err := json.Unmarshal([]byte(payload), &chunk); err != nil {
		return
	}
	for _, choice := range chunk.Choices {
		a.consumeDelta(choice.Delta)
	}
	a.recordUsage(chunk.Usage)
	if a.providerWire == openAIProviderWireMessages {
		var messageEvent struct {
			Message struct {
				Usage *providerUsageEnvelope `json:"usage"`
			} `json:"message"`
			Usage *providerUsageEnvelope `json:"usage"`
		}
		if err := json.Unmarshal([]byte(payload), &messageEvent); err == nil {
			a.recordMessagesUsage(messageEvent.Message.Usage)
			a.recordMessagesUsage(messageEvent.Usage)
		}
	}

	// Responses streaming: nested response.usage from events like
	// response.completed. This captures provider-reported token usage from
	// the Responses API SSE payload (SDD S05/D04).
	if a.usage.inputTokens == 0 && a.usage.outputTokens == 0 {
		var respEvent struct {
			Response struct {
				Usage *providerUsageEnvelope `json:"usage"`
			} `json:"response"`
		}
		if err := json.Unmarshal([]byte(payload), &respEvent); err == nil {
			if respEvent.Response.Usage != nil {
				a.recordUsage(respEvent.Response.Usage)
			}
		}
	}
}

func (a *providerChatAssembler) consumeDelta(delta providerChatDeltaEnvelope) {
	a.content.WriteString(delta.Content)
	a.reasoning.WriteString(delta.ReasoningContent)
	a.reasoning.WriteString(delta.Reasoning)
	for _, call := range delta.ToolCalls {
		if name := strings.TrimSpace(call.Function.Name); name != "" {
			a.toolCallNames = append(a.toolCallNames, name)
		}
	}
}

func (a *providerChatAssembler) observation() *providerAssembledObservation {
	if !a.streaming && !a.nonStreamingParsed {
		a.nonStreamingParsed = true
		var resp struct {
			Choices []struct {
				Message providerChatDeltaEnvelope `json:"message"`
			} `json:"choices"`
			Usage *providerUsageEnvelope `json:"usage"`
		}
		if err := json.Unmarshal(a.pending, &resp); err == nil {
			for _, choice := range resp.Choices {
				a.consumeDelta(choice.Message)
			}
			a.recordUsage(resp.Usage)
			if a.providerWire == openAIProviderWireMessages {
				a.recordMessagesUsage(resp.Usage)
			}
		}
	}
	return &providerAssembledObservation{
		Content:       a.content.String(),
		Reasoning:     a.reasoning.String(),
		ToolCallNames: a.toolCallNames,
		BodyBytes:     a.bodyBytes,
	}
}
