package ai

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strings"
	"time"
)

// OpenAICompatible talks to OpenAI-style /models and /chat/completions.
// xAI: https://api.x.ai/v1
// OpenCode Go: https://opencode.ai/zen/go/v1
type OpenAICompatible struct {
	ID      string
	BaseURL string
	Client  *http.Client
}

func NewOpenAICompatible(id, baseURL string) *OpenAICompatible {
	return &OpenAICompatible{
		ID:      id,
		BaseURL: strings.TrimRight(baseURL, "/"),
		// Job worker 可能跑到 4 分鐘；HTTP 同步路徑靠 ctx deadline 提早取消
		Client: &http.Client{Timeout: 4 * time.Minute},
	}
}

func (p *OpenAICompatible) Complete(ctx context.Context, apiKey, model, prompt string) (string, error) {
	if strings.TrimSpace(apiKey) == "" {
		return "", fmt.Errorf("missing API key for %s", p.ID)
	}
	if model == "" {
		model = "default"
	}
	// 強制注入系統語言 prompt（依 ctx 語言；預設繁中）
	sys := SystemPromptForLanguage(ResponseLanguageFrom(ctx))

	// OpenCode Go（kimi/deepseek/glm）reasoning 常先燒 200～1000+ token；
	// max_tokens 太小會 finish=length 且 content=null（xAI grok 較少踩雷）
	// 靈感等路徑可用 WithMaxTokens 壓小以加速。
	maxTokens := MaxTokensFrom(ctx)
	if maxTokens <= 0 {
		maxTokens = 4096
		if p.ID == ProviderOpenCodeGo {
			maxTokens = 8192
		}
	}
	temp := 0.9
	if t := TemperatureFrom(ctx); t > 0 {
		temp = t
	}

	text, meta, err := p.doChat(ctx, apiKey, model, sys, prompt, maxTokens, temp)
	if err != nil {
		return "", err
	}
	if text != "" && meta.FinishReason != "length" {
		return text, nil
	}
	// content 空 + length：reasoning 模型常把預算燒完；加大後再試一次
	if meta.FinishReason == "length" && p.ID == ProviderOpenCodeGo {
		retry := 12288
		// 靈感等路徑有短預算：不要直接跳 12k（更慢），但要夠過 reasoning 門檻
		if cap := MaxTokensFrom(ctx); cap > 0 {
			retry = cap * 3
			if retry < 4096 {
				retry = 4096
			}
			if retry > 8192 {
				retry = 8192
			}
		}
		text2, meta2, err2 := p.doChat(ctx, apiKey, model, sys, prompt, retry, 0.7)
		if err2 == nil && strings.TrimSpace(text2) != "" {
			text = strings.TrimSpace(text2)
			meta = meta2
			maxTokens = retry
			if meta.FinishReason != "length" {
				return text, nil
			}
		}
	}
	if text != "" && meta.FinishReason == "length" {
		return p.continueCompletion(ctx, apiKey, model, sys, prompt, text, maxTokens, temp)
	}
	return "", fmt.Errorf("%s returned empty content (finish=%s model=%s sample=%s)",
		p.ID, meta.FinishReason, meta.Model, truncateRunes(meta.RawSnippet, 160))
}

const maxContinuationRounds = 4

func (p *OpenAICompatible) continueCompletion(ctx context.Context, apiKey, model, sys, prompt, partial string, maxTokens int, temperature float64) (string, error) {
	full := strings.TrimSpace(partial)
	for range maxContinuationRounds {
		messages := completionContinuationMessages(sys, prompt, full)
		next, meta, err := p.doChatMessages(ctx, apiKey, model, messages, maxTokens, temperature)
		if err != nil {
			return "", err
		}
		next = strings.TrimSpace(next)
		if next == "" {
			return "", fmt.Errorf("%s returned empty continuation (finish=%s model=%s)", p.ID, meta.FinishReason, meta.Model)
		}
		full = appendWithoutOverlap(full, next)
		if meta.FinishReason != "length" {
			return full, nil
		}
	}
	return "", fmt.Errorf("%s could not finish content after continuation retries", p.ID)
}

func completionContinuationMessages(sys, prompt, partial string) []map[string]string {
	return []map[string]string{
		{"role": "system", "content": sys},
		{"role": "user", "content": prompt},
		{"role": "assistant", "content": partial},
		{"role": "user", "content": "請從剛才中斷的位置直接繼續正文，完成尚未講完的內容。不要重寫、不要摘要、不要重複已輸出的句子，也不要加任何說明。"},
	}
}

func appendWithoutOverlap(existing, continuation string) string {
	existing = strings.TrimSpace(existing)
	continuation = strings.TrimSpace(continuation)
	max := len([]rune(existing))
	if n := len([]rune(continuation)); n < max {
		max = n
	}
	if max > 200 {
		max = 200
	}
	er, cr := []rune(existing), []rune(continuation)
	for n := max; n > 0; n-- {
		if string(er[len(er)-n:]) == string(cr[:n]) {
			return existing + string(cr[n:])
		}
	}
	if existing == "" {
		return continuation
	}
	return existing + continuation
}

func (p *OpenAICompatible) doChat(ctx context.Context, apiKey, model, sys, prompt string, maxTokens int, temperature float64) (string, chatExtractMeta, error) {
	return p.doChatMessages(ctx, apiKey, model, []map[string]string{
		{"role": "system", "content": sys},
		{"role": "user", "content": prompt},
	}, maxTokens, temperature)
}

func (p *OpenAICompatible) doChatMessages(ctx context.Context, apiKey, model string, messages []map[string]string, maxTokens int, temperature float64) (string, chatExtractMeta, error) {
	meta := chatExtractMeta{}
	body := map[string]any{
		"model":       model,
		"max_tokens":  maxTokens,
		"temperature": temperature,
		"messages":    messages,
	}
	raw, err := json.Marshal(body)
	if err != nil {
		return "", meta, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.BaseURL+"/chat/completions", strings.NewReader(string(raw)))
	if err != nil {
		return "", meta, err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	// OpenCode（Cloudflare）會擋 Go 預設 UA
	req.Header.Set("User-Agent", "HarborDesk/1.0 (+threads-tool; OpenAI-compatible)")
	res, err := p.Client.Do(req)
	if err != nil {
		return "", meta, err
	}
	defer res.Body.Close()
	data, _ := io.ReadAll(io.LimitReader(res.Body, 2<<20))
	if res.StatusCode < 200 || res.StatusCode >= 300 {
		return "", meta, fmt.Errorf("%s chat failed: HTTP %d %s", p.ID, res.StatusCode, truncateRunes(string(data), 240))
	}
	text, meta, perr := extractChatContent(data)
	if perr != nil {
		return "", meta, fmt.Errorf("%s chat parse: %w", p.ID, perr)
	}
	return strings.TrimSpace(text), meta, nil
}

type chatExtractMeta struct {
	FinishReason string
	Model        string
	RawSnippet   string
}

// extractChatContent 支援：
// - message.content 字串
// - message.content 多段陣列 [{type,text}]
// - 部分供應商把正文放在 reasoning_content / output_text
func extractChatContent(data []byte) (string, chatExtractMeta, error) {
	meta := chatExtractMeta{RawSnippet: string(data)}
	var payload struct {
		Model   string `json:"model"`
		Choices []struct {
			FinishReason string `json:"finish_reason"`
			Message      struct {
				// content 可能是 string 或 array
				Content          json.RawMessage `json:"content"`
				ReasoningContent string          `json:"reasoning_content"`
				// OpenCode / 部分模型
				Reasoning  string `json:"reasoning"`
				OutputText string `json:"output_text"`
			} `json:"message"`
			// 舊式 text 欄位
			Text string `json:"text"`
		} `json:"choices"`
		// 部分 gateway 把錯誤包在 error 物件但仍 200
		Error *struct {
			Message string `json:"message"`
			Type    string `json:"type"`
		} `json:"error"`
	}
	if err := json.Unmarshal(data, &payload); err != nil {
		return "", meta, err
	}
	meta.Model = strings.TrimSpace(payload.Model)
	if payload.Error != nil && strings.TrimSpace(payload.Error.Message) != "" {
		return "", meta, fmt.Errorf("%s", payload.Error.Message)
	}
	if len(payload.Choices) == 0 {
		return "", meta, fmt.Errorf("no choices")
	}
	ch := payload.Choices[0]
	meta.FinishReason = strings.TrimSpace(ch.FinishReason)

	if t := decodeMessageContent(ch.Message.Content); t != "" {
		return t, meta, nil
	}
	if t := strings.TrimSpace(ch.Message.OutputText); t != "" {
		return t, meta, nil
	}
	if t := strings.TrimSpace(ch.Text); t != "" {
		return t, meta, nil
	}
	// 不把 reasoning / thinking 當使用者可見正文。
	// 舊邏輯會把「我們需要根據最近的對話…」這類思考過程寫進靈感聊天，看起來像壞掉。
	// content 空時回空字串，由呼叫端 retry 加大 max_tokens 或回錯誤。
	return "", meta, nil
}

func decodeMessageContent(raw json.RawMessage) string {
	if len(raw) == 0 || string(raw) == "null" {
		return ""
	}
	// 字串
	var s string
	if err := json.Unmarshal(raw, &s); err == nil {
		return strings.TrimSpace(s)
	}
	// 陣列：OpenAI/xAI 多段 content
	var parts []struct {
		Type string `json:"type"`
		Text string `json:"text"`
		// 有些用 output_text
		OutputText string `json:"output_text"`
	}
	if err := json.Unmarshal(raw, &parts); err == nil {
		var b strings.Builder
		for _, p := range parts {
			t := strings.TrimSpace(p.Text)
			if t == "" {
				t = strings.TrimSpace(p.OutputText)
			}
			if t == "" {
				continue
			}
			if b.Len() > 0 {
				b.WriteString("\n")
			}
			b.WriteString(t)
		}
		return strings.TrimSpace(b.String())
	}
	// 單一物件 {type,text}
	var one struct {
		Type string `json:"type"`
		Text string `json:"text"`
	}
	if err := json.Unmarshal(raw, &one); err == nil && strings.TrimSpace(one.Text) != "" {
		return strings.TrimSpace(one.Text)
	}
	return ""
}

func (p *OpenAICompatible) ListModels(ctx context.Context, apiKey string) ([]string, error) {
	if strings.TrimSpace(apiKey) == "" {
		return nil, fmt.Errorf("missing API key for %s", p.ID)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.BaseURL+"/models", nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Accept", "application/json")
	req.Header.Set("User-Agent", "HarborDesk/1.0 (+threads-tool; OpenAI-compatible)")
	res, err := p.Client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	data, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	if res.StatusCode < 200 || res.StatusCode >= 300 {
		return nil, fmt.Errorf("%s models failed: HTTP %d %s", p.ID, res.StatusCode, truncateRunes(string(data), 200))
	}
	var payload struct {
		Data []struct {
			ID string `json:"id"`
		} `json:"data"`
	}
	if err := json.Unmarshal(data, &payload); err != nil {
		return nil, fmt.Errorf("%s models parse: %w", p.ID, err)
	}
	models := make([]string, 0, len(payload.Data))
	for _, item := range payload.Data {
		id := strings.TrimSpace(item.ID)
		if id == "" {
			continue
		}
		models = append(models, id)
	}
	sort.Strings(models)
	if len(models) == 0 {
		return nil, fmt.Errorf("%s returned empty model list", p.ID)
	}
	return models, nil
}

func truncateRunes(s string, n int) string {
	r := []rune(s)
	if len(r) <= n {
		return s
	}
	return string(r[:n]) + "…"
}
