// Package types defines the unified (OpenAI-compatible) wire format that the
// gateway exposes to its clients, plus the unified internal representation.
package types

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

// ErrBusy is the soft "source at capacity" sentinel shared by the provider
// layer (returns it) and the scheduler layer (reacts to it): busy is not a
// failure, so no cooldown/preference penalty is recorded, and gateways map
// it to HTTP 429. Defined here so the scheduler does not depend on the
// provider package (which pulls in the Lua runtime).
var ErrBusy = errors.New("provider busy")

// ---- in-bound request ----
//
// ChatRequest now lives in maxwire.go as an alias of UnifiedRequest: the
// kernel's internal format is the maximal set (OpenAI / Anthropic / Gemini /
// Ollama / DeepSeek fields unioned), not "whatever an OpenAI client sent". The
// type stays named ChatRequest at every call site for now — renaming 19
// non-test + 30 test references in one sweep would bury the semantic change in
// mechanical noise, and an alias makes the migration free when it happens.
//
// The v1 struct that lived here is gone. Its fields and their fate:
//
//   Model/Messages/Temperature/MaxTokens/Stream  → UnifiedRequest, same tags
//   Tools/ToolChoice                              → json.RawMessage (raw passthrough
//                                                   to the adapter; the kernel never
//                                                   parses them)
//   DisableThinking  (json:"disable_thinking")    → REPLACED by Thinking. This was
//                                                   the private field that leaked into
//                                                   the body handed to adapters, forcing
//                                                   10 adapters to nil it out.
//   ExtraBody       (hand-injected by MarshalJSON) → a real, declared field on
//                                                   UnifiedRequest — no more
//                                                   MarshalJSON back door
//   ClientSession   (json:"-")                     → kept, still never serialized
//
// The custom MarshalJSON is gone with it: its only job was splicing ExtraBody
// into an OpenAI-shaped body, which is no longer how the type serializes.

// ChatMessage supports both plain string content and multimodal arrays
// (RawMessage preserves whatever the client sent for the adapter to process).
type ChatMessage struct {
	Role             string          `json:"role"`
	Content          json.RawMessage `json:"content,omitempty"`
	ReasoningContent string          `json:"reasoning_content,omitempty"`
	ToolCallID       string          `json:"tool_call_id,omitempty"`
	ToolCalls        json.RawMessage `json:"tool_calls,omitempty"`
}

func StringContent(s string) json.RawMessage { b, _ := json.Marshal(s); return b }

// isEmptyJSONArray reports whether raw is the literal empty JSON array [].
func isEmptyJSONArray(raw json.RawMessage) bool {
	t := bytes.TrimSpace(raw)
	return len(t) == 2 && t[0] == '[' && t[1] == ']'
}

// UnmarshalJSON decodes a chat message, normalising an empty-array content
// ("content":[]) to an empty string and dropping an empty tool_calls array.
//
// Why this exists: Lua adapters json.decode the request and re-encode it, and
// an empty Lua table is indistinguishable from an empty JSON array — the
// encoder emits {} for both. Agent clients serialise an assistant turn that
// carries tool_calls and no text as content:[], so every pass-through adapter
// turned it into content:{} — a shape that is not valid OpenAI (content is
// string | array of parts | null) and that real upstreams reject with
// "400 invalid arguments". Normalising at the decode boundary fixes every
// adapter at once, including ones added later.
func (m *ChatMessage) UnmarshalJSON(b []byte) error {
	type alias ChatMessage
	var a alias
	if err := json.Unmarshal(b, &a); err != nil {
		return err
	}
	*m = ChatMessage(a)
	if isEmptyJSONArray(m.Content) {
		m.Content = StringContent("")
	}
	if isEmptyJSONArray(m.ToolCalls) {
		m.ToolCalls = nil
	}
	return nil
}

type ToolCall struct {
	ID        string                 `json:"id"`
	Type      string                 `json:"type"`
	Name      string                 `json:"name"`
	Arguments map[string]interface{} `json:"arguments"`
}

// ---- Unified internal representation (what adapters produce) ----

type UnifiedResponse struct {
	Model            string     `json:"model,omitempty"`
	Content          string     `json:"content"`
	ReasoningContent string     `json:"reasoning_content,omitempty"`
	FinishReason     string     `json:"finish_reason,omitempty"`
	TokenUsage       TokenUsage `json:"token_usage"`
	ToolCalls        []ToolCall `json:"tool_calls,omitempty"`
	// ImageData used by image-generation adapters.
	ImageData []ImageData `json:"image_data,omitempty"`
	// Cost is the upstream-reported charge for this request, passed through
	// verbatim (OpenCode Zen/Go put a decimal **string** at the response top
	// level). Deliberately not parsed or summed by the gateway: it is an
	// upstream fact, and only some upstreams report it at all.
	Cost string `json:"cost,omitempty"`
}

type TokenUsage struct {
	Prompt     int `json:"prompt"`
	Completion int `json:"completion"`
	Total      int `json:"total"`
	// PromptTokensDetails mirrors the OpenAI v2 usage.prompt_tokens_details
	// object so cache-hit counts reported by OpenAI-compatible upstreams
	// (and by adapters that normalize their own cache fields into it) pass
	// through to clients that read it — dsh reads cached_tokens from here.
	PromptTokensDetails *PromptTokensDetails `json:"prompt_tokens_details,omitempty"`
	// PromptCacheHit / PromptCacheMiss carry the DeepSeek-legacy standalone
	// fields; dsh falls back to prompt_cache_hit_tokens when
	// prompt_tokens_details.cached_tokens is absent.
	PromptCacheHit  int `json:"prompt_cache_hit_tokens,omitempty"`
	PromptCacheMiss int `json:"prompt_cache_miss_tokens,omitempty"`
	// CompletionTokensDetails mirrors the OpenAI v2 usage.completion_tokens_details
	// object. OpenCode Zen/Go report how much of the completion was thinking
	// tokens here; without it clients cannot tell how much of the billed output
	// was reasoning rather than visible answer.
	CompletionTokensDetails *CompletionTokensDetails `json:"completion_tokens_details,omitempty"`
}

// CompletionTokensDetails is the OpenAI v2 completion_tokens_details object.
type CompletionTokensDetails struct {
	ReasoningTokens int `json:"reasoning_tokens"`
}

// PromptTokensDetails is the OpenAI v2 prompt_tokens_details object. Only
// CachedTokens is emitted (omitempty drops the whole object when zero).
type PromptTokensDetails struct {
	// CachedTokens is always emitted (even 0) so clients can distinguish
	// "upstream reports cache, this request missed" from "no cache data".
	CachedTokens int `json:"cached_tokens"`
}

// MarshalJSON emits both the legacy short keys (prompt/completion/total, used
// by the internal unified representation and older clients) and the OpenAI
// standard keys (prompt_tokens/completion_tokens/total_tokens). Standard
// clients such as DSH and DevEco Code read the *_tokens fields.
func (t TokenUsage) MarshalJSON() ([]byte, error) {
	// Auto-generate prompt_tokens_details from legacy DeepSeek fields when
	// the upstream adapter only set the standalone hit count (openai.lua
	// does this in Lua, but other adapters or the standardSSEChunk fallback
	// may not). dsh reads prompt_tokens_details.cached_tokens first.
	pdetails := t.PromptTokensDetails
	if pdetails == nil && t.PromptCacheHit > 0 {
		pdetails = &PromptTokensDetails{CachedTokens: t.PromptCacheHit}
	}
	return json.Marshal(struct {
		PromptTokens          int                      `json:"prompt_tokens"`
		CompletionTokens      int                      `json:"completion_tokens"`
		TotalTokens           int                      `json:"total_tokens"`
		Prompt                int                      `json:"prompt"`
		Completion            int                      `json:"completion"`
		Total                 int                      `json:"total"`
		PromptTokensDetails   *PromptTokensDetails     `json:"prompt_tokens_details,omitempty"`
		PromptCacheHitTokens  int                      `json:"prompt_cache_hit_tokens,omitempty"`
		PromptCacheMissTokens int                      `json:"prompt_cache_miss_tokens,omitempty"`
		CompletionDetails     *CompletionTokensDetails `json:"completion_tokens_details,omitempty"`
	}{
		PromptTokens:          t.Prompt,
		CompletionTokens:      t.Completion,
		TotalTokens:           t.Total,
		Prompt:                t.Prompt,
		Completion:            t.Completion,
		Total:                 t.Total,
		PromptTokensDetails:   pdetails,
		PromptCacheHitTokens:  t.PromptCacheHit,
		PromptCacheMissTokens: t.PromptCacheMiss,
		CompletionDetails:     t.CompletionTokensDetails,
	})
}

type ImageData struct {
	B64JSON string `json:"b64_json,omitempty"`
	URL     string `json:"url,omitempty"`
	Revised string `json:"revised_prompt,omitempty"`
}

// ---- Image generation (OpenAI /v1/images/generations wire) ----

type ImageGenRequest struct {
	Model          string `json:"model"`
	Prompt         string `json:"prompt"`
	N              int    `json:"n,omitempty"`
	Size           string `json:"size,omitempty"`
	ResponseFormat string `json:"response_format,omitempty"`
}

type ImageGenResponse struct {
	Created int64       `json:"created"`
	Data    []ImageData `json:"data"`
}

// ---- Unified streaming chunk produced by adapters ----

// UnifiedChunk is one streamed delta. ToolCalls carries the raw upstream
// streaming tool_calls array (incremental fragments with an index field), which
// OpenAI-compatible clients accumulate themselves.
type UnifiedChunk struct {
	Content string `json:"content"`
	Done    bool   `json:"done"`
	// FinishReason carries the upstream finish/stop reason ("tool_calls",
	// "length", ...) when the adapter provides it; the gateway emits it on
	// the terminating chunk instead of the default "stop".
	FinishReason     string          `json:"finish_reason,omitempty"`
	ToolCalls        json.RawMessage `json:"tool_calls,omitempty"`
	ReasoningContent string          `json:"reasoning_content,omitempty"`
	// Usage carries the upstream token usage when the stream chunk provides
	// it (OpenAI-style streams attach usage to some chunks; the final one
	// often has empty choices). Gateway uses it to emit exact usage in the
	// final stream chunk instead of estimates.
	Usage *TokenUsage `json:"usage,omitempty"`
	// Cost is upstream-reported charge for this request (decimal string),
	// passed through verbatim; OpenCode sends it on its own stream chunk.
	Cost string `json:"cost,omitempty"`
}

// Meta passed to Lua build_headers hook
type BuildMeta struct {
	URL       string                 `json:"url"`
	Method    string                 `json:"method"`
	Body      string                 `json:"body"`
	APIKey    string                 `json:"api_key"`
	Timestamp int64                  `json:"timestamp"`
	Source    map[string]interface{} `json:"source"`
}

func Now() int64 { return time.Now().Unix() }

// OneLine flattens an error string to a single line capped at n chars.
// Shared by the provider layer (upstream error reasons) and the gateway
// layer (client-facing AUTO-chain summaries).
func OneLine(s string, n int) string {
	s = strings.Join(strings.Fields(s), " ")
	if len(s) > n {
		s = s[:n] + "..."
	}
	return s
}