package controller

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/songquanpeng/one-api/common/ctxkey"
	"github.com/songquanpeng/one-api/model"
	billingratio "github.com/songquanpeng/one-api/relay/billing/ratio"
	relay "github.com/songquanpeng/one-api/relay"
	"github.com/songquanpeng/one-api/relay/adaptor/openai"
	"github.com/songquanpeng/one-api/relay/apitype"
	"github.com/songquanpeng/one-api/relay/channeltype"
	"github.com/songquanpeng/one-api/relay/meta"
	relaymodel "github.com/songquanpeng/one-api/relay/model"
)

// https://platform.openai.com/docs/api-reference/models/list

type OpenAIModelPermission struct {
	Id                 string  `json:"id"`
	Object             string  `json:"object"`
	Created            int     `json:"created"`
	AllowCreateEngine  bool    `json:"allow_create_engine"`
	AllowSampling      bool    `json:"allow_sampling"`
	AllowLogprobs      bool    `json:"allow_logprobs"`
	AllowSearchIndices bool    `json:"allow_search_indices"`
	AllowView          bool    `json:"allow_view"`
	AllowFineTuning    bool    `json:"allow_fine_tuning"`
	Organization       string  `json:"organization"`
	Group              *string `json:"group"`
	IsBlocking         bool    `json:"is_blocking"`
}

type OpenAIModels struct {
	Id         string                  `json:"id"`
	Object     string                  `json:"object"`
	Created    int                     `json:"created"`
	OwnedBy    string                  `json:"owned_by"`
	Permission []OpenAIModelPermission `json:"permission"`
	Root       string                  `json:"root"`
	Parent     *string                 `json:"parent"`
}

var models []OpenAIModels
var modelsMap map[string]OpenAIModels
var channelId2Models map[int][]string

func init() {
	var permission []OpenAIModelPermission
	permission = append(permission, OpenAIModelPermission{
		Id:                 "modelperm-LwHkVFn8AcMItP432fKKDIKJ",
		Object:             "model_permission",
		Created:            1626777600,
		AllowCreateEngine:  true,
		AllowSampling:      true,
		AllowLogprobs:      true,
		AllowSearchIndices: false,
		AllowView:          true,
		AllowFineTuning:    false,
		Organization:       "*",
		Group:              nil,
		IsBlocking:         false,
	})
	// https://platform.openai.com/docs/models/model-endpoint-compatibility
	for i := 0; i < apitype.Dummy; i++ {
		if i == apitype.AIProxyLibrary {
			continue
		}
		adaptor := relay.GetAdaptor(i)
		channelName := adaptor.GetChannelName()
		modelNames := adaptor.GetModelList()
		for _, modelName := range modelNames {
			models = append(models, OpenAIModels{
				Id:         modelName,
				Object:     "model",
				Created:    1626777600,
				OwnedBy:    channelName,
				Permission: permission,
				Root:       modelName,
				Parent:     nil,
			})
		}
	}
	for _, channelType := range openai.CompatibleChannels {
		if channelType == channeltype.Azure {
			continue
		}
		channelName, channelModelList := openai.GetCompatibleChannelMeta(channelType)
		for _, modelName := range channelModelList {
			models = append(models, OpenAIModels{
				Id:         modelName,
				Object:     "model",
				Created:    1626777600,
				OwnedBy:    channelName,
				Permission: permission,
				Root:       modelName,
				Parent:     nil,
			})
		}
	}
	modelsMap = make(map[string]OpenAIModels)
	for _, model := range models {
		modelsMap[model.Id] = model
	}
	channelId2Models = make(map[int][]string)
	for i := 1; i < channeltype.Dummy; i++ {
		adaptor := relay.GetAdaptor(channeltype.ToAPIType(i))
		meta := &meta.Meta{
			ChannelType: i,
		}
		adaptor.Init(meta)
		channelId2Models[i] = adaptor.GetModelList()
	}
}

func DashboardListModels(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"message": "",
		"data":    channelId2Models,
	})
}

func ListAllModels(c *gin.Context) {
	c.JSON(200, gin.H{
		"object": "list",
		"data":   models,
	})
}

func ListModels(c *gin.Context) {
	ctx := c.Request.Context()
	var availableModels []string
	if c.GetString(ctxkey.AvailableModels) != "" {
		availableModels = strings.Split(c.GetString(ctxkey.AvailableModels), ",")
	} else {
		userId := c.GetInt(ctxkey.Id)
		userGroup, _ := model.CacheGetUserGroup(userId)
		availableModels, _ = model.CacheGetGroupModels(ctx, userGroup)
	}
	modelSet := make(map[string]bool)
	for _, availableModel := range availableModels {
		modelSet[availableModel] = true
	}
	availableOpenAIModels := make([]OpenAIModels, 0)
	for _, model := range models {
		if _, ok := modelSet[model.Id]; ok {
			modelSet[model.Id] = false
			availableOpenAIModels = append(availableOpenAIModels, model)
		}
	}
	for modelName, ok := range modelSet {
		if ok {
			availableOpenAIModels = append(availableOpenAIModels, OpenAIModels{
				Id:      modelName,
				Object:  "model",
				Created: 1626777600,
				OwnedBy: "custom",
				Root:    modelName,
				Parent:  nil,
			})
		}
	}
	c.JSON(200, gin.H{
		"object": "list",
		"data":   availableOpenAIModels,
	})
}

func RetrieveModel(c *gin.Context) {
	modelId := c.Param("model")
	if model, ok := modelsMap[modelId]; ok {
		c.JSON(200, model)
	} else {
		Error := relaymodel.Error{
			Message: fmt.Sprintf("The model '%s' does not exist", modelId),
			Type:    "invalid_request_error",
			Param:   "model",
			Code:    "model_not_found",
		}
		c.JSON(200, gin.H{
			"error": Error,
		})
	}
}

func GetUserAvailableModels(c *gin.Context) {
	ctx := c.Request.Context()
	id := c.GetInt(ctxkey.Id)
	userGroup, err := model.CacheGetUserGroup(id)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"success": false,
			"message": err.Error(),
		})
		return
	}
	models, err := model.CacheGetGroupModels(ctx, userGroup)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"success": false,
			"message": err.Error(),
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"message": "",
		"data":    models,
	})
	return
}

// --- Model Catalog (powered by Zenmux) ---

// ModelCatalogItem is the enriched model record returned to aiscri-xiong clients.
type ModelCatalogItem struct {
	Id               string   `json:"id"`
	Name             string   `json:"name"`
	Provider         string   `json:"provider"`
	Description      string   `json:"description"`
	InputModalities  []string `json:"input_modalities"`
	OutputModalities []string `json:"output_modalities"`
	ContextLength    int      `json:"context_length"`
	MaxOutputTokens  int      `json:"max_output_tokens"`
	InputPrice       float64  `json:"input_price"`
	OutputPrice      float64  `json:"output_price"`
	// PricingDiscount: e.g. "0.8" means 20% off; applied to both input and output prices
	PricingDiscount  string   `json:"pricing_discount"`
	Tags             []string `json:"tags"`
	// SupportsReasoning: 0=none, 1=always-on, 2=toggleable
	SupportsReasoning int    `json:"supports_reasoning"`
	SuitableApi       string `json:"suitable_api"`
}

// zenmuxModel is the raw response shape from Zenmux listByFilter API.
type zenmuxModel struct {
	Slug                string `json:"slug"`
	Name                string `json:"name"`
	Author              string `json:"author"`
	Description         string `json:"description"`
	InputModalities     string `json:"input_modalities"`  // comma-separated
	OutputModalities    string `json:"output_modalities"` // comma-separated
	ContextLength       int    `json:"context_length"`
	MaxCompletionTokens int    `json:"max_completion_tokens"`
	PricingPrompt       string `json:"pricing_prompt"`
	PricingCompletion   string `json:"pricing_completion"`
	PricingDiscount     string `json:"pricing_discount"`
	SuitableApi         string `json:"suitable_api"` // comma-separated
	SupportsReasoning   int    `json:"supports_reasoning"`
	SupportedParameters string `json:"supported_parameters"` // comma-separated
}

type zenmuxListResponse struct {
	Success bool          `json:"success"`
	Data    []zenmuxModel `json:"data"`
}

var (
	catalogCache     []ModelCatalogItem
	catalogCacheTime time.Time
	catalogMu        sync.RWMutex
	catalogCacheTTL  = 5 * time.Minute

	// zenmuxCToken is the public frontend token embedded in the Zenmux website.
	// Override via env var ZENMUX_CTOKEN if it ever changes.
	zenmuxCToken = func() string {
		if v := os.Getenv("ZENMUX_CTOKEN"); v != "" {
			return v
		}
		return "2uF44yawHUs41edv2fRcV_eE"
	}()
)

func splitCSV(s string) []string {
	if s == "" {
		return nil
	}
	parts := strings.Split(s, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		if t := strings.TrimSpace(p); t != "" {
			out = append(out, t)
		}
	}
	return out
}

func parsePrice(s string) float64 {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	var f float64
	fmt.Sscanf(s, "%f", &f)
	return f
}

func zenmuxToItem(z zenmuxModel) ModelCatalogItem {
	inMod := splitCSV(z.InputModalities)
	outMod := splitCSV(z.OutputModalities)
	params := splitCSV(z.SupportedParameters)

	// Derive tags
	tags := []string{}
	for _, p := range params {
		if p == "tools" {
			tags = append(tags, "function-call")
			break
		}
	}
	for _, m := range inMod {
		if m == "image" {
			tags = append(tags, "vision")
			break
		}
	}
	if z.SupportsReasoning > 0 {
		tags = append(tags, "reasoning")
	}

	return ModelCatalogItem{
		Id:                z.Slug,
		Name:              z.Name,
		Provider:          z.Author,
		Description:       z.Description,
		InputModalities:   inMod,
		OutputModalities:  outMod,
		ContextLength:     z.ContextLength,
		MaxOutputTokens:   z.MaxCompletionTokens,
		InputPrice:        parsePrice(z.PricingPrompt),
		OutputPrice:       parsePrice(z.PricingCompletion),
		PricingDiscount:   z.PricingDiscount,
		Tags:              tags,
		SupportsReasoning: z.SupportsReasoning,
		SuitableApi:       z.SuitableApi,
	}
}

func fetchZenmuxCatalog() ([]ModelCatalogItem, error) {
	catalogMu.RLock()
	if catalogCache != nil && time.Since(catalogCacheTime) < catalogCacheTTL {
		defer catalogMu.RUnlock()
		return catalogCache, nil
	}
	catalogMu.RUnlock()

	url := "https://zenmux.ai/api/frontend/model/listByFilter?ctoken=" + zenmuxCToken + "&sort=newest&context_length=&keyword="
	client := &http.Client{Timeout: 15 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		return nil, fmt.Errorf("zenmux catalog fetch failed: %w", err)
	}
	defer resp.Body.Close()

	var parsed zenmuxListResponse
	if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
		return nil, fmt.Errorf("zenmux catalog parse failed: %w", err)
	}
	if !parsed.Success {
		return nil, fmt.Errorf("zenmux catalog returned success=false")
	}

	items := make([]ModelCatalogItem, 0, len(parsed.Data))
	for _, z := range parsed.Data {
		if z.Slug != "" {
			items = append(items, zenmuxToItem(z))
		}
	}

	catalogMu.Lock()
	catalogCache = items
	catalogCacheTime = time.Now()
	catalogMu.Unlock()

	return items, nil
}

// GetTokenQuota returns quota info for the authenticated relay token.
// GET /api/token-quota — requires TokenAuth middleware.
func GetTokenQuota(c *gin.Context) {
	tokenId := c.GetInt(ctxkey.TokenId)
	token, err := model.GetTokenById(tokenId)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"data": gin.H{
			"remain_quota":    token.RemainQuota,
			"used_quota":      token.UsedQuota,
			"unlimited_quota": token.UnlimitedQuota,
			"expired_time":    token.ExpiredTime,
			"name":            token.Name,
		},
	})
}

// GetModelCatalog returns models with capability metadata, filtered to only those
// available to the requesting user's group.
func GetModelCatalog(c *gin.Context) {
	ctx := c.Request.Context()

	var availableModels []string
	if c.GetString(ctxkey.AvailableModels) != "" {
		availableModels = strings.Split(c.GetString(ctxkey.AvailableModels), ",")
	} else {
		id := c.GetInt(ctxkey.Id)
		userGroup, err := model.CacheGetUserGroup(id)
		if err != nil {
			c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
			return
		}
		var groupErr error
		availableModels, groupErr = model.CacheGetGroupModels(ctx, userGroup)
		if groupErr != nil {
			c.JSON(http.StatusOK, gin.H{"success": false, "message": groupErr.Error()})
			return
		}
	}

	available := make(map[string]bool, len(availableModels))
	for _, m := range availableModels {
		available[m] = true
	}

	catalog, err := fetchZenmuxCatalog()
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"success": true,
			"message": "model catalog unavailable: " + err.Error(),
			"data":    []ModelCatalogItem{},
		})
		return
	}

	result := make([]ModelCatalogItem, 0, len(catalog))
	for _, item := range catalog {
		if available[item.Id] {
			result = append(result, item)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"message": "",
		"data":    result,
	})
}

// zenmuxChannelDef describes one Zenmux protocol → one-api channel mapping.
type zenmuxChannelDef struct {
	Name      string
	Type      int
	Protocols []string // suitable_api values to include
}

// allZenmuxChannelDefs is the canonical channel mapping for Zenmux.
//
// Zenmux exposes a single OpenAI-compatible ingress at /api/v1 for ALL text models
// (chat.completions, responses, messages, gemini, generate).
// suitable_api is upstream metadata only — Zenmux always accepts OpenAI format on input.
//
// Imagen and Veo use https://zenmux.ai/api/vertex-ai (Vertex AI SDK) and are
// not handled here (one-api's relay framework doesn't support that format yet).
var allZenmuxChannelDefs = []zenmuxChannelDef{
	{
		Name:      "Zenmux",
		Type:      channeltype.OpenAICompatible,
		Protocols: []string{"chat.completions", "responses", "messages", "gemini", "generate"},
	},
}

// GetZenmuxProtocols returns all distinct suitable_api values found in the Zenmux catalog.
// GET /api/zenmux/protocols — admin only, for debugging protocol → channel mapping.
func GetZenmuxProtocols(c *gin.Context) {
	catalog, err := fetchZenmuxCatalog()
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
		return
	}
	seen := make(map[string]int)
	for _, item := range catalog {
		for _, api := range splitCSV(item.SuitableApi) {
			api = strings.TrimSpace(api)
			if api != "" {
				seen[api]++
			}
		}
	}
	c.JSON(http.StatusOK, gin.H{"success": true, "data": seen})
}

// SetupZenmuxChannels creates all Zenmux protocol channels and syncs their model lists.
// POST /api/zenmux/setup  — body: {"key":"<zenmux-api-key>","base_url":"https://zenmux.ai"}
// Skips channels that already exist (matched by name). Idempotent.
func SetupZenmuxChannels(c *gin.Context) {
	var req struct {
		Key     string `json:"key"`
		BaseURL string `json:"base_url"`
	}
	if err := c.ShouldBindJSON(&req); err != nil || req.Key == "" {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "key is required"})
		return
	}
	baseURL := req.BaseURL
	if baseURL == "" {
		baseURL = "https://zenmux.ai/api/v1"
	}

	catalog, err := fetchZenmuxCatalog()
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "fetch zenmux catalog failed: " + err.Error()})
		return
	}

	type result struct {
		Name   string `json:"name"`
		Models int    `json:"models"`
		Action string `json:"action"` // "created" | "skipped"
	}
	results := make([]result, 0, len(allZenmuxChannelDefs))

	for _, def := range allZenmuxChannelDefs {
		// Build protocol set for fast lookup.
		wantProtos := make(map[string]bool, len(def.Protocols))
		for _, p := range def.Protocols {
			wantProtos[strings.ToLower(p)] = true
		}

		// Filter catalog to matching protocols.
		slugs := make([]string, 0)
		for _, item := range catalog {
			for _, api := range splitCSV(item.SuitableApi) {
				if wantProtos[strings.ToLower(strings.TrimSpace(api))] {
					slugs = append(slugs, item.Id)
					break
				}
			}
		}
		modelList := strings.Join(slugs, ",")

		// Check if a channel with this name already exists.
		var existing []model.Channel
		model.DB.Where("name = ?", def.Name).Find(&existing)
		if len(existing) > 0 {
			// Update model list on the first match.
			ch := existing[0]
			ch.Models = modelList
			_ = ch.Update()
			results = append(results, result{Name: def.Name, Models: len(slugs), Action: "updated"})
			continue
		}

		// Create new channel.
		priority := int64(0)
		weight := uint(1)
		ch := model.Channel{
			Type:    def.Type,
			Name:    def.Name,
			Key:     req.Key,
			BaseURL: &baseURL,
			Models:  modelList,
			Status:  1,
			Group:   "default",
			Priority: &priority,
			Weight:   &weight,
		}
		if err := ch.Insert(); err != nil {
			c.JSON(http.StatusOK, gin.H{"success": false, "message": fmt.Sprintf("create channel %q failed: %s", def.Name, err.Error())})
			return
		}
		results = append(results, result{Name: def.Name, Models: len(slugs), Action: "created"})
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"message": fmt.Sprintf("setup complete: %d channels processed", len(results)),
		"data":    results,
	})
}

// SyncZenmuxModels fetches all models from Zenmux and updates the channel's model list.
// POST /api/channel/:id/sync-zenmux
// Optional query param: ?protocol=chat.completions
//   Accepts comma-separated values to match multiple protocols, e.g.
//   ?protocol=google.gemini,google.imagen,google.video
func SyncZenmuxModels(c *gin.Context) {
	channelId, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid channel id"})
		return
	}

	// Build a set of requested protocols (lowercased) for O(1) lookup.
	protocolParam := strings.TrimSpace(c.Query("protocol"))
	wantProtocols := make(map[string]bool)
	for _, p := range splitCSV(protocolParam) {
		if p != "" {
			wantProtocols[strings.ToLower(p)] = true
		}
	}

	catalog, err := fetchZenmuxCatalog()
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
		return
	}

	slugs := make([]string, 0, len(catalog))
	for _, item := range catalog {
		if len(wantProtocols) > 0 {
			matched := false
			for _, api := range splitCSV(item.SuitableApi) {
				if wantProtocols[strings.ToLower(strings.TrimSpace(api))] {
					matched = true
					break
				}
			}
			if !matched {
				continue
			}
		}
		slugs = append(slugs, item.Id)
	}
	modelList := strings.Join(slugs, ",")

	ch, err := model.GetChannelById(channelId, false)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "channel not found"})
		return
	}
	ch.Models = modelList
	if err := ch.Update(); err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
		return
	}

	msg := fmt.Sprintf("synced %d models to channel", len(slugs))
	if protocolParam != "" {
		msg = fmt.Sprintf("synced %d models (protocol=%s) to channel", len(slugs), protocolParam)
	}
	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"message": msg,
		"data":    len(slugs),
	})
}

// SyncZenmuxRatios fetches the Zenmux model catalog and updates one-api's
// ModelRatio and CompletionRatio maps so every Zenmux model has correct billing.
//
// Ratio formula:
//
//	modelRatio      = input_price  ($/1M tokens, raw)
//	completionRatio = output_price / input_price  (relative to input)
//
// POST /api/zenmux/sync-ratios — admin only.
func SyncZenmuxRatios(c *gin.Context) {
	catalog, err := fetchZenmuxCatalog()
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "fetch catalog failed: " + err.Error()})
		return
	}

	// Load current ratios so we only ADD/UPDATE Zenmux entries, not wipe custom ones.
	var modelRatioMap map[string]float64
	var completionRatioMap map[string]float64
	_ = json.Unmarshal([]byte(billingratio.ModelRatio2JSONString()), &modelRatioMap)
	_ = json.Unmarshal([]byte(billingratio.CompletionRatio2JSONString()), &completionRatioMap)
	if modelRatioMap == nil {
		modelRatioMap = make(map[string]float64)
	}
	if completionRatioMap == nil {
		completionRatioMap = make(map[string]float64)
	}

	updated, skipped := 0, 0
	for _, item := range catalog {
		if item.InputPrice <= 0 {
			skipped++
			continue
		}
		modelRatioMap[item.Id] = item.InputPrice
		if item.OutputPrice > 0 {
			completionRatioMap[item.Id] = item.OutputPrice / item.InputPrice
		}
		updated++
	}

	newModelJSON, err := json.Marshal(modelRatioMap)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "marshal model ratio failed: " + err.Error()})
		return
	}
	if err = billingratio.UpdateModelRatioByJSONString(string(newModelJSON)); err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "update model ratio failed: " + err.Error()})
		return
	}
	if err = model.UpdateOption("ModelRatio", string(newModelJSON)); err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "save model ratio failed: " + err.Error()})
		return
	}

	newCompletionJSON, err := json.Marshal(completionRatioMap)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "marshal completion ratio failed: " + err.Error()})
		return
	}
	if err = billingratio.UpdateCompletionRatioByJSONString(string(newCompletionJSON)); err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "update completion ratio failed: " + err.Error()})
		return
	}
	if err = model.UpdateOption("CompletionRatio", string(newCompletionJSON)); err != nil {
		c.JSON(http.StatusOK, gin.H{"success": false, "message": "save completion ratio failed: " + err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"message": fmt.Sprintf("synced ratios for %d models (%d skipped — no pricing)", updated, skipped),
	})
}

