package providers

import (
	"context"
	"log/slog"
	"maps"
)

const (
	dashscopeDefaultBase  = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
	dashscopeDefaultModel = "qwen3-max"
)

// dashscopeThinkingModels lists DashScope models that accept the
// enable_thinking / thinking_budget parameters (Qwen3 open-weight and Qwen3.5 series).
// Models NOT in this set (e.g. qwen3-plus, qwen3-turbo) will silently
// skip thinking injection to avoid API "model not supported" errors.
var dashscopeThinkingModels = map[string]bool{
	// Qwen3.5 series — thinking + vision
	"qwen3.5-plus":    true,
	"qwen3.5-turbo":   true,
	// Qwen3 hosted
	"qwen3-max":       true,
	// Qwen3 open-weight (available as hosted inference)
	"qwen3-235b-a22b": true,
	"qwen3-32b":       true,
	"qwen3-14b":       true,
	"qwen3-8b":        true,
}

// DashScopeProvider wraps OpenAIProvider to handle DashScope-specific behaviors.
// Critical: DashScope does NOT support tools + streaming simultaneously.
// When tools are present, ChatStream falls back to non-streaming Chat().
type DashScopeProvider struct {
	*OpenAIProvider
}

func NewDashScopeProvider(name, apiKey, apiBase, defaultModel string) *DashScopeProvider {
	if apiBase == "" {
		apiBase = dashscopeDefaultBase
	}
	if defaultModel == "" {
		defaultModel = dashscopeDefaultModel
	}
	return &DashScopeProvider{
		OpenAIProvider: NewOpenAIProvider(name, apiKey, apiBase, defaultModel),
	}
}

// Name is inherited from the embedded OpenAIProvider (returns the user-specified name).
func (p *DashScopeProvider) SupportsThinking() bool { return true }

// ModelSupportsThinking implements ModelThinkingCapable.
// Returns true only for models that accept enable_thinking / thinking_budget.
func (p *DashScopeProvider) ModelSupportsThinking(model string) bool {
	return dashscopeThinkingModels[p.resolveModel(model)]
}

// applyThinkingGuard maps thinking_level to DashScope-specific params
// (enable_thinking / thinking_budget) only when the model supports it.
// Returns the (possibly mutated) request. Shared by Chat and ChatStream.
func (p *DashScopeProvider) applyThinkingGuard(req ChatRequest) ChatRequest {
	level, ok := req.Options[OptThinkingLevel].(string)
	if !ok || level == "" || level == "off" {
		return req
	}

	if p.ModelSupportsThinking(req.Model) {
		// Clone Options to avoid mutating caller's map
		opts := make(map[string]any, len(req.Options)+2)
		maps.Copy(opts, req.Options)
		opts[OptEnableThinking] = true
		opts[OptThinkingBudget] = dashscopeThinkingBudget(level)
		delete(opts, OptThinkingLevel) // don't pass generic key to OpenAI buildRequestBody
		req.Options = opts
	} else {
		slog.Debug("dashscope: model does not support thinking, skipping enable_thinking",
			"model", p.resolveModel(req.Model), "requested_level", level)
	}

	return req
}

// Chat overrides OpenAIProvider.Chat to apply the per-model thinking guard.
func (p *DashScopeProvider) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
	return p.OpenAIProvider.Chat(ctx, p.applyThinkingGuard(req))
}

// ChatStream handles DashScope's limitation: tools + streaming cannot coexist.
// When tools are present, falls back to non-streaming Chat() and synthesizes
// chunk callbacks for the caller.
func (p *DashScopeProvider) ChatStream(ctx context.Context, req ChatRequest, onChunk func(StreamChunk)) (*ChatResponse, error) {
	req = p.applyThinkingGuard(req)

	if len(req.Tools) > 0 {
		slog.Debug("dashscope: tools present, falling back to non-streaming Chat")
		resp, err := p.OpenAIProvider.Chat(ctx, req)
		if err != nil {
			return nil, err
		}
		if onChunk != nil {
			if resp.Thinking != "" {
				onChunk(StreamChunk{Thinking: resp.Thinking})
			}
			if resp.Content != "" {
				onChunk(StreamChunk{Content: resp.Content})
			}
			onChunk(StreamChunk{Done: true})
		}
		return resp, nil
	}
	return p.OpenAIProvider.ChatStream(ctx, req, onChunk)
}

// dashscopeThinkingBudget maps a thinking level to a DashScope thinking_budget value.
func dashscopeThinkingBudget(level string) int {
	switch level {
	case "low":
		return 4096
	case "medium":
		return 16384
	case "high":
		return 32768
	default:
		return 16384
	}
}
