//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package models

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// ReasoningSimple represents simple reasoning capability
type ReasoningSimple struct {
	Type    string `json:"type"`
	Enabled bool   `json:"enabled"`
	Default bool   `json:"default"`
}

// ReasoningBudget represents budget-based reasoning capability
type ReasoningBudget struct {
	Type          string `json:"type"`
	Enabled       bool   `json:"enabled"`
	DefaultTokens int    `json:"default_tokens"`
	TokenRange    struct {
		Min int `json:"min"`
		Max int `json:"max"`
	} `json:"token_range"`
}

// ReasoningEffort represents effort-based reasoning capability
type ReasoningEffort struct {
	Type    string   `json:"type"`
	Enabled bool     `json:"enabled"`
	Default string   `json:"default"`
	Options []string `json:"options"`
}

// Reasoning represents the reasoning capability (can be one of three types)
type Reasoning struct {
	Simple  *ReasoningSimple `json:"-"`
	Budget  *ReasoningBudget `json:"-"`
	Effort  *ReasoningEffort `json:"-"`
	RawType string           `json:"type"`
}

// Reasoning represents the reasoning capability (can be one of three types)
type ClearReasoningContent struct {
	DefaultValue    bool     `json:"default_value"`
	SupportedModels []string `json:"supported_models"`
}

// Reasoning represents the reasoning capability (can be one of three types)
type Thinking struct {
	DefaultValue    bool     `json:"default_value"`
	SupportedModels []string `json:"supported_models"`
}

// UnmarshalJSON custom unmarshal for Reasoning
func (r *Reasoning) UnmarshalJSON(data []byte) error {
	var temp map[string]interface{}
	if err := json.Unmarshal(data, &temp); err != nil {
		return err
	}

	typeVal, ok := temp["type"].(string)
	if !ok {
		return fmt.Errorf("reasoning type is required")
	}

	r.RawType = typeVal

	switch typeVal {
	case "simple":
		var simple ReasoningSimple
		dataBytes, _ := json.Marshal(temp)
		if err := json.Unmarshal(dataBytes, &simple); err != nil {
			return err
		}
		r.Simple = &simple
	case "budget":
		var budget ReasoningBudget
		dataBytes, _ := json.Marshal(temp)
		if err := json.Unmarshal(dataBytes, &budget); err != nil {
			return err
		}
		r.Budget = &budget
	case "effort":
		var effort ReasoningEffort
		dataBytes, _ := json.Marshal(temp)
		if err := json.Unmarshal(dataBytes, &effort); err != nil {
			return err
		}
		r.Effort = &effort
	default:
		return fmt.Errorf("unknown reasoning type: %s", typeVal)
	}

	return nil
}

// MarshalJSON custom marshal for Reasoning
func (r *Reasoning) MarshalJSON() ([]byte, error) {
	switch r.RawType {
	case "simple":
		if r.Simple != nil {
			return json.Marshal(r.Simple)
		}
	case "budget":
		if r.Budget != nil {
			return json.Marshal(r.Budget)
		}
	case "effort":
		if r.Effort != nil {
			return json.Marshal(r.Effort)
		}
	}
	return nil, fmt.Errorf("invalid reasoning state")
}

// Multimodal represents multimodal capability
type Multimodal struct {
	Enabled          bool     `json:"enabled"`
	InputModalities  []string `json:"input_modalities,omitempty"`
	OutputModalities []string `json:"output_modalities,omitempty"`
}

// Features represents all features of a model
type Features struct {
	Multimodal    *Multimodal            `json:"multimodal,omitempty"`
	Reasoning     *Reasoning             `json:"reasoning,omitempty"`
	Thinking      *Thinking              `json:"thinking,omitempty"`
	ClearThinking *ClearReasoningContent `json:"clear_thinking,omitempty"`
}

type ModelThinking struct {
	DefaultValue  bool `json:"default_value"`
	ClearThinking bool `json:"clear_thinking"`
}

type ModelTools struct {
	Support bool `json:"support"`
}

// Model represents a single LLM model
type Model struct {
	Name         string         `json:"name"`
	MaxTokens    *int           `json:"max_tokens"`
	ModelTypes   []string       `json:"model_types"`
	Thinking     *ModelThinking `json:"thinking"`
	Tools        *ModelTools    `json:"tools"`
	Class        *string        `json:"class"`
	MaxDimension *int           `json:"max_dimension"` // used by embedding models
	Dimensions   []int          `json:"dimensions"`
	Alias        []string       `json:"alias"`
	ModelTypeMap map[string]bool
}

// Provider represents an LLM provider
type Provider struct {
	Name        string            `json:"name"`
	Rank        int               `json:"rank"`
	URL         map[string]string `json:"url"`
	URLSuffix   URLSuffix         `json:"url_suffix"`
	Models      []*Model          `json:"models"`
	Features    Features          `json:"features"`
	Class       string            `json:"class"`
	ModelDriver ModelDriver
}

// ProviderManager manages provider and model operations
type ProviderManager struct {
	Providers        []Provider     `json:"model_providers"`
	AllModels        []Model        `json:"all_models"`
	Alias2ModelIndex map[string]int `json:"alias2_model_index_map"`
}

// ModelResponse represents the standard response structure
type ModelResponse struct {
	Code    int                      `json:"code"`
	Data    []map[string]interface{} `json:"data"`
	Message string                   `json:"message"`
}

func decodeProviderConfig(data []byte) (Provider, error) {
	var provider Provider
	if err := json.Unmarshal(data, &provider); err != nil {
		return Provider{}, err
	}

	var rawProvider struct {
		URLSuffix json.RawMessage `json:"url_suffix"`
	}
	if err := json.Unmarshal(data, &rawProvider); err != nil {
		return Provider{}, err
	}
	if len(rawProvider.URLSuffix) == 0 {
		return provider, nil
	}

	decoder := json.NewDecoder(bytes.NewReader(rawProvider.URLSuffix))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&provider.URLSuffix); err != nil {
		return Provider{}, err
	}

	return provider, nil
}

var providerManager *ProviderManager

func GetProviderManager() *ProviderManager {
	return providerManager
}

// InitProviderManager creates a new ProviderManager by reading all JSON files from a directory
func InitProviderManager(dirPath string) error {
	providers := []Provider{}

	// Read all files in the directory
	files, err := os.ReadDir(dirPath)
	if err != nil {
		return fmt.Errorf("error reading directory %s: %w", dirPath, err)
	}

	modelFactory := NewModelFactory()

	// Iterate through all files
	for _, file := range files {
		// Skip directories
		if file.IsDir() {
			continue
		}

		// Only process JSON files
		if !strings.HasSuffix(file.Name(), ".json") {
			continue
		}

		// Build full file path
		filePath := filepath.Join(dirPath, file.Name())

		// Read the file
		var data []byte
		data, err = os.ReadFile(filePath)
		if err != nil {
			return fmt.Errorf("error reading file %s: %w", filePath, err)
		}

		// Parse JSON
		var provider Provider
		if provider, err = decodeProviderConfig(data); err != nil {
			return fmt.Errorf("error parsing JSON from file %s: %w", filePath, err)
		}

		for _, model := range provider.Models {
			// if the prefix of mode.Name is matched with keys of modelSupportThinking
			if provider.Class == "" {
				pos := strings.Index(model.Name, "-")
				if pos >= 0 {
					modelClass := model.Name[0:pos]
					model.Class = &modelClass
				}
			} else {
				model.Class = &provider.Name
			}

			model.ModelTypeMap = make(map[string]bool)
			for _, modelType := range model.ModelTypes {
				model.ModelTypeMap[modelType] = true
			}
		}

		provider.ModelDriver, err = modelFactory.CreateModelDriver(provider.Name, provider.URL, provider.URLSuffix)
		if err != nil {
			return fmt.Errorf("error creating model driver for provider %s: %w", provider.Name, err)
		}

		// Add to providers list
		providers = append(providers, provider)
	}

	if len(providers) == 0 {
		return fmt.Errorf("no JSON files found in directory %s", dirPath)
	}

	// Read the file.  Use a repo-root-relative path so that go test
	// (which sets CWD to the package directory) can still find it.
	var data []byte
	data, err = os.ReadFile(filepath.Join(findRepoRoot(), "conf", "all_models.json"))
	if err != nil {
		return fmt.Errorf("error reading file 'conf/all_models.json': %w", err)
	}

	// Parse JSON
	type AllModels struct {
		Models []Model `json:"models"`
	}
	var allModels AllModels
	if err = json.Unmarshal(data, &allModels); err != nil {
		return fmt.Errorf("error parsing JSON from file 'conf/all_models.json': %w", err)
	}

	alias2ModelIndex := make(map[string]int)
	for idx, model := range allModels.Models {
		addModelAlias := func(alias string) error {
			alias = strings.TrimSpace(alias)
			if alias == "" {
				return nil
			}
			lowerAlias := strings.ToLower(alias)
			if existingIdx, ok := alias2ModelIndex[lowerAlias]; ok && existingIdx != idx {
				return fmt.Errorf("duplicate alias %q for models %q and %q", alias, allModels.Models[existingIdx].Name, model.Name)
			}
			alias2ModelIndex[lowerAlias] = idx
			return nil
		}
		if err = addModelAlias(model.Name); err != nil {
			return err
		}
		for _, alias := range model.Alias {
			if err = addModelAlias(alias); err != nil {
				return err
			}
		}
	}

	providerManager = &ProviderManager{
		Providers:        providers,
		AllModels:        allModels.Models,
		Alias2ModelIndex: alias2ModelIndex,
	}
	return nil
}

// 1. List all providers
func (pm *ProviderManager) ListProviders() ([]map[string]interface{}, error) {

	var providers []map[string]interface{}

	for _, provider := range pm.Providers {

		modelTypeSet := make(map[string]struct{})
		for _, model := range provider.Models {
			for _, modelType := range model.ModelTypes {
				modelTypeSet[modelType] = struct{}{}
			}
		}

		// Always emit a non-nil slice. Python's provider_api_service.list_providers
		// returns `[]` for empty model_types; clients (e.g. AvailableModels in
		// web/src/pages/user-setting/setting-model/components/un-add-model.tsx)
		// call .some() / .forEach() on this field, which crashes on null.
		modelTypes := make([]string, 0, len(modelTypeSet))
		for modelType := range modelTypeSet {
			modelTypes = append(modelTypes, modelType)
		}

		providerData := map[string]interface{}{
			"name":        provider.Name,
			"rank":        provider.Rank,
			"url":         provider.URL,
			"model_types": modelTypes,
			"url_suffix":  provider.URLSuffix,
		}
		providers = append(providers, providerData)
	}

	// Sort providers by rank
	sort.Slice(providers, func(i, j int) bool {
		return providers[i]["rank"].(int) > providers[j]["rank"].(int)
	})

	if len(providers) == 0 {
		return nil, fmt.Errorf("no providers found")
	}

	return providers, nil
}

func (pm *ProviderManager) ListAllModels() ([]map[string]interface{}, error) {

	var modelList []map[string]interface{}

	for _, model := range pm.AllModels {

		modelData := map[string]interface{}{
			"name":        model.Name,
			"model_types": model.ModelTypes,
		}
		if model.Alias != nil {
			modelData["alias"] = model.Alias
		}
		if model.Thinking != nil {
			modelData["thinking"] = model.Thinking
		}
		if model.MaxTokens != nil {
			modelData["max_tokens"] = *model.MaxTokens
		}
		if model.MaxDimension != nil {
			modelData["max_dimension"] = *model.MaxDimension
		}
		if len(model.Dimensions) > 0 {
			modelData["dimensions"] = model.Dimensions
		}
		if model.Thinking != nil {
			modelData["thinking"] = "supported"
		}
		if model.Tools != nil {
			modelData["tools"] = "supported"
		}
		modelList = append(modelList, modelData)
	}

	if len(modelList) == 0 {
		return nil, fmt.Errorf("no models found")
	}

	return modelList, nil
}

func (pm *ProviderManager) GetModelByNameOrAlias(modelName string) *Model {
	lowerModelName := strings.ToLower(modelName)
	// Check if it is alias
	modelIndex, ok := pm.Alias2ModelIndex[lowerModelName]
	if ok {
		return &pm.AllModels[modelIndex]
	}
	return nil
}

// 2. Show specific provider information (including base_url)
func (pm *ProviderManager) GetProviderByName(providerName string) (map[string]interface{}, error) {

	provider := pm.FindProvider(providerName)
	if provider == nil {
		return nil, fmt.Errorf("provider '%s' not found", providerName)
	}

	providerInfo := map[string]interface{}{
		"name":         provider.Name,
		"base_url":     provider.URL,
		"total_models": len(provider.Models),
	}

	return providerInfo, nil
}

// 3. List models under a specific provider
func (pm *ProviderManager) ListModels(providerName string) ([]map[string]interface{}, error) {
	provider := pm.FindProvider(providerName)
	if provider == nil {
		return nil, fmt.Errorf("provider '%s' not found", providerName)
	}

	modelList := []map[string]interface{}{}
	for _, model := range provider.Models {
		// Field name "model_type" (singular) matches the IInstanceModel
		// contract in web/src/interfaces/database/llm.ts:75 and Python's
		// LLM.to_dict() (api/db/db_models.py). The Go port previously
		// emitted the plural "model_types", which broke used-model.tsx
		// — the view rendered empty because the "model_type" key was
		// undefined. IAvailableProvider.model_types (plural) is the
		// other interface used by un-add-model.tsx and continues to
		// receive the plural form from the dedicated /api/v1/providers/
		// handler.
		//
		// `max_dimension` and `dimensions` are always emitted (nil → null,
		// empty slice → null in JSON) to match Python's LLM.to_dict() and
		// keep the response shape stable for clients that destructure
		// the object.
		modelData := map[string]interface{}{
			"name":          model.Name,
			"max_tokens":    model.MaxTokens,
			"model_type":    model.ModelTypes,
			"max_dimension": model.MaxDimension,
			"dimensions":    model.Dimensions,
		}
		if model.Thinking != nil {
			modelData["thinking"] = "supported"
		}
		if model.Tools != nil {
			modelData["tools"] = "supported"
		}
		modelList = append(modelList, modelData)
	}

	if len(modelList) == 0 {
		return nil, fmt.Errorf("no models found")
	}

	return modelList, nil
}

func (pm *ProviderManager) GetModelByName(providerName, modelName string) (*Model, error) {
	provider := pm.FindProvider(providerName)
	if provider == nil {
		return nil, fmt.Errorf("provider '%s' not found", providerName)
	}
	model := pm.findModel(provider, modelName)
	if model == nil {
		return nil, fmt.Errorf("model '%s' not found", modelName)
	}
	return model, nil
}

func (pm *ProviderManager) GetModelUrl(providerName, modelName, modelType string) (*string, *string, error) {
	provider := pm.FindProvider(providerName)
	if provider == nil {
		return nil, nil, fmt.Errorf("provider '%s' not found", providerName)
	}
	model := pm.findModel(provider, modelName)
	if model == nil {
		return nil, nil, fmt.Errorf("model '%s' not found", modelName)
	}

	if !model.ModelTypeMap[modelType] {
		return nil, nil, fmt.Errorf("model '%s' does not support model type '%s'", modelName, modelType)
	}

	switch modelType {
	case "chat":
		url := fmt.Sprintf("%s%s", provider.URL, provider.URLSuffix.Chat)
		return &url, nil, nil
	case "async_chat":
		chatUrl := fmt.Sprintf("%s%s", provider.URL, provider.URLSuffix.AsyncChat)
		resultUrl := fmt.Sprintf("%s%s", provider.URL, provider.URLSuffix.AsyncResult)
		return &chatUrl, &resultUrl, nil
	case "embedding":
		url := fmt.Sprintf("%s%s", provider.URL, provider.URLSuffix.Embedding)
		return &url, nil, nil
	case "rerank":
		url := fmt.Sprintf("%s%s", provider.URL, provider.URLSuffix.Rerank)
		return &url, nil, nil
	default:
		return nil, nil, fmt.Errorf("model '%s' does not support model type '%s'", modelName, modelType)
	}
}

// 4. Search specific model information with filtering by max_tokens or type
func (pm *ProviderManager) SearchModelInfo(providerName, modelName string, filterBy string, filterValue interface{}) ModelResponse {
	resp := ModelResponse{
		Code:    0,
		Data:    []map[string]interface{}{},
		Message: "success",
	}

	provider := pm.FindProvider(providerName)
	if provider == nil {
		resp.Code = 404
		resp.Message = fmt.Sprintf("Provider '%s' not found", providerName)
		return resp
	}

	model := pm.findModel(provider, modelName)
	if model == nil {
		resp.Code = 404
		resp.Message = fmt.Sprintf("Model '%s' not found in provider '%s'", modelName, providerName)
		return resp
	}

	// Apply filters
	matchFilter := true
	if filterBy != "" && filterValue != nil {
		switch filterBy {
		case "max_tokens":
			if maxVal, ok := filterValue.(int); ok {
				if *model.MaxTokens < maxVal {
					matchFilter = false
					resp.Code = 400
					resp.Message = fmt.Sprintf("Model does not meet filter criteria: max_tokens (%d) < %d",
						model.MaxTokens, maxVal)
				}
			}
		case "type":
			if typeVal, ok := filterValue.(string); ok {
				if !containsModelType(model.ModelTypes, typeVal) {
					matchFilter = false
					resp.Code = 400
					resp.Message = fmt.Sprintf("Model does not meet filter criteria: type '%s' not found", typeVal)
				}
			}
		}
	}

	if matchFilter {
		modelData := map[string]interface{}{
			"name":        model.Name,
			"max_tokens":  model.MaxTokens,
			"model_types": model.ModelTypes,
			//"features":    getFeaturesMap(model.Features),
		}

		if filterBy != "" && filterValue != nil {
			modelData["filter_applied"] = map[string]interface{}{
				"field": filterBy,
				"value": filterValue,
			}
		}

		resp.Data = append(resp.Data, modelData)
	}

	return resp
}

// 5. Display models with specific features
func (pm *ProviderManager) SearchByFeature(featureType string) ModelResponse {
	resp := ModelResponse{
		Code:    0,
		Data:    []map[string]interface{}{},
		Message: "success",
	}

	//for _, provider := range pm.Providers {
	//	for _, model := range provider.Models {
	//		if modelHasFeature(model.Features, featureType) {
	//			modelData := map[string]interface{}{
	//				"provider":    provider.Name,
	//				"name":        model.Name,
	//				"max_tokens":  model.MaxTokens,
	//				"model_types": model.ModelTypes,
	//				"features":    getFeaturesMap(model.Features),
	//			}
	//			resp.Data = append(resp.Data, modelData)
	//		}
	//	}
	//}

	if len(resp.Data) == 0 {
		resp.Code = 404
		resp.Message = fmt.Sprintf("No models found with feature '%s'", featureType)
	}

	return resp
}

// 6. Display models with specific type
func (pm *ProviderManager) SearchByType(modelType string) ModelResponse {
	resp := ModelResponse{
		Code:    0,
		Data:    []map[string]interface{}{},
		Message: "success",
	}

	for _, provider := range pm.Providers {
		for _, model := range provider.Models {
			if containsModelType(model.ModelTypes, modelType) {
				modelData := map[string]interface{}{
					"provider":    provider.Name,
					"name":        model.Name,
					"max_tokens":  model.MaxTokens,
					"model_types": model.ModelTypes,
					//"features":    getFeaturesMap(model.Features),
				}
				resp.Data = append(resp.Data, modelData)
			}
		}
	}

	if len(resp.Data) == 0 {
		resp.Code = 404
		resp.Message = fmt.Sprintf("No models found with type '%s'", modelType)
	}

	return resp
}

func GetFeatures(model *Model) []string {
	var features []string
	if model.Thinking != nil {
		features = append(features, "thinking")
	}
	return features
}

func ConvertToFeaturesMap(model *Model) map[string]interface{} {
	featuresMap := make(map[string]interface{})
	if model.Thinking != nil {
		thinkingMap := map[string]interface{}{
			"default_value":   model.Thinking.DefaultValue,
			"clear_reasoning": model.Thinking.ClearThinking,
		}
		featuresMap["thinking"] = thinkingMap
	}
	return featuresMap
}

// Helper: Get features map for response
func getFeaturesMap(features Features) map[string]interface{} {
	featuresMap := make(map[string]interface{})

	if features.Multimodal != nil && features.Multimodal.Enabled {
		multimodalMap := map[string]interface{}{
			"enabled":           features.Multimodal.Enabled,
			"input_modalities":  features.Multimodal.InputModalities,
			"output_modalities": features.Multimodal.OutputModalities,
		}
		featuresMap["multimodal"] = multimodalMap
	}

	if features.Reasoning != nil {
		reasoningMap := make(map[string]interface{})
		switch features.Reasoning.RawType {
		case "simple":
			if features.Reasoning.Simple != nil {
				reasoningMap["type"] = "simple"
				reasoningMap["enabled"] = features.Reasoning.Simple.Enabled
				reasoningMap["default"] = features.Reasoning.Simple.Default
			}
		case "budget":
			if features.Reasoning.Budget != nil {
				reasoningMap["type"] = "budget"
				reasoningMap["enabled"] = features.Reasoning.Budget.Enabled
				reasoningMap["default_tokens"] = features.Reasoning.Budget.DefaultTokens
				reasoningMap["token_range"] = map[string]int{
					"min": features.Reasoning.Budget.TokenRange.Min,
					"max": features.Reasoning.Budget.TokenRange.Max,
				}
			}
		case "effort":
			if features.Reasoning.Effort != nil {
				reasoningMap["type"] = "effort"
				reasoningMap["enabled"] = features.Reasoning.Effort.Enabled
				reasoningMap["default"] = features.Reasoning.Effort.Default
				reasoningMap["options"] = features.Reasoning.Effort.Options
			}
		}
		featuresMap["reasoning"] = reasoningMap
	}

	return featuresMap
}

// Helper: Check if model has a specific feature
func modelHasFeature(features Features, featureType string) bool {
	switch strings.ToLower(featureType) {
	case "multimodal":
		return features.Multimodal != nil && features.Multimodal.Enabled
	case "reasoning":
		return features.Reasoning != nil
	case "reasoning_simple":
		return features.Reasoning != nil && features.Reasoning.RawType == "simple"
	case "reasoning_budget":
		return features.Reasoning != nil && features.Reasoning.RawType == "budget"
	case "reasoning_effort":
		return features.Reasoning != nil && features.Reasoning.RawType == "effort"
	default:
		return false
	}
}

// findRepoRoot walks up from CWD until it finds the repo root (marked by
// conf/all_models.json).  This makes tests work regardless of the Go test
// binary's CWD (which is set to the package directory by go test).
func findRepoRoot() string {
	dir, err := os.Getwd()
	if err != nil {
		return "."
	}
	for dir != "/" && dir != "" {
		if _, err := os.Stat(filepath.Join(dir, "conf", "all_models.json")); err == nil {
			return dir
		}
		dir = filepath.Dir(dir)
	}
	return "."
}

// Helper: Find provider by name
func (pm *ProviderManager) FindProvider(name string) *Provider {
	for i := range pm.Providers {
		if strings.EqualFold(pm.Providers[i].Name, name) {
			return &pm.Providers[i]
		}
	}
	return nil
}

// Helper: Find model by name
func (pm *ProviderManager) findModel(provider *Provider, modelName string) *Model {
	for i := range provider.Models {
		if strings.EqualFold(provider.Models[i].Name, modelName) {
			return provider.Models[i]
		}
	}
	return nil
}

// Helper: Check if model types contains target
func containsModelType(types []string, target string) bool {
	for _, t := range types {
		if strings.EqualFold(t, target) {
			return true
		}
	}
	return false
}
