//
//  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 service

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"ragflow/internal/common"
	"ragflow/internal/entity"
	"ragflow/internal/utility"
	"strings"
	"unicode/utf8"

	"ragflow/internal/dao"
)

var DefaultRerankModels = map[string]struct{}{
	"BAAI/bge-reranker-v2-m3":           {},
	"maidalun1020/bce-reranker-base_v1": {},
}

var ReadOnlyFields = map[string]struct{}{
	"id":          {},
	"tenant_id":   {},
	"created_by":  {},
	"create_time": {},
	"create_date": {},
	"update_time": {},
	"update_date": {},
}

// ChatService chat service
type ChatService struct {
	chatDAO       *dao.ChatDAO
	kbDAO         *dao.KnowledgebaseDAO
	userTenantDAO *dao.UserTenantDAO
	tenantDAO     *dao.TenantDAO
}

// NewChatService create chat service
func NewChatService() *ChatService {
	return &ChatService{
		chatDAO:       dao.NewChatDAO(),
		kbDAO:         dao.NewKnowledgebaseDAO(),
		userTenantDAO: dao.NewUserTenantDAO(),
		tenantDAO:     dao.NewTenantDAO(),
	}
}

// ChatWithKBNames chat with knowledge base names
type ChatWithKBNames struct {
	*entity.Chat
	KBNames      []string `json:"kb_names"`
	DatasetIDs   []string `json:"dataset_ids"`
	Nickname     string   `json:"nickname"`
	TenantAvatar *string  `json:"tenant_avatar,omitempty"`
}

// ListChatsResponse list chats response
type ListChatsResponse struct {
	Total int64              `json:"total"`
	Chats []*ChatWithKBNames `json:"chats"`
}

// ListChats list chats for a user
func (s *ChatService) ListChats(ctx context.Context, userID, status, keywords string, page, pageSize int, orderBy string, desc bool, ownerIDs []string) (*ListChatsResponse, error) {
	var chats []*entity.ChatListItem
	var total int64
	var err error

	if len(ownerIDs) == 0 {
		chats, total, err = s.chatDAO.ListByTenantIDs(
			ctx,
			dao.DB,
			nil,
			userID,
			page,
			pageSize,
			orderBy,
			desc,
			keywords,
		)
		if err != nil {
			return nil, err
		}
	} else {
		var filterOwnerIDs []string
		filterOwnerIDs, err = s.filterAccessibleChatOwnerIDs(ctx, userID, ownerIDs)
		if err != nil {
			return nil, err
		}
		if len(filterOwnerIDs) == 0 {
			return &ListChatsResponse{
				Total: 0,
				Chats: []*ChatWithKBNames{},
			}, nil
		}

		chats, total, err = s.chatDAO.ListByOwnerIDs(ctx, dao.DB, filterOwnerIDs, userID, orderBy, desc, keywords)
		if err != nil {
			return nil, err
		}

		if page > 0 && pageSize > 0 {
			start := (page - 1) * pageSize
			end := start + pageSize
			if start < int(total) {
				if end > int(total) {
					end = int(total)
				}
				chats = chats[start:end]
			} else {
				chats = []*entity.ChatListItem{}
			}
		}
	}

	// Enrich with knowledge base names
	chatsWithKBNames := make([]*ChatWithKBNames, 0, len(chats))
	for _, chat := range chats {
		kbNames, datasetIDs := s.getDatasetNamesAndIDs(ctx, chat.KBIDs)
		chatsWithKBNames = append(chatsWithKBNames, &ChatWithKBNames{
			Chat:         &chat.Chat,
			KBNames:      kbNames,
			DatasetIDs:   datasetIDs,
			Nickname:     ownerNickname(chat.Nickname, chat.TenantID),
			TenantAvatar: chat.TenantAvatar,
		})
	}

	return &ListChatsResponse{
		Total: total,
		Chats: chatsWithKBNames,
	}, nil
}

func (s *ChatService) filterAccessibleChatOwnerIDs(ctx context.Context, userID string, ownerIDs []string) ([]string, error) {
	tenantIDs, err := s.userTenantDAO.GetTenantIDsByUserID(ctx, dao.DB, userID)
	if err != nil {
		return nil, err
	}

	allowed := map[string]struct{}{userID: {}}
	for _, tenantID := range tenantIDs {
		tenantID = strings.TrimSpace(tenantID)
		if tenantID != "" {
			allowed[tenantID] = struct{}{}
		}
	}

	filtered := make([]string, 0, len(ownerIDs))
	seen := make(map[string]struct{}, len(ownerIDs))
	for _, ownerID := range ownerIDs {
		ownerID = strings.TrimSpace(ownerID)
		if ownerID == "" {
			continue
		}
		if _, ok := allowed[ownerID]; !ok {
			continue
		}
		if _, ok := seen[ownerID]; ok {
			continue
		}
		seen[ownerID] = struct{}{}
		filtered = append(filtered, ownerID)
	}
	return filtered, nil
}

func ownerNickname(nickname *string, tenantID string) string {
	if nickname != nil && strings.TrimSpace(*nickname) != "" {
		return *nickname
	}
	return tenantID
}

type CreateChatRequest struct {
	Name                   string
	DatasetIDs             []string               `json:"dataset_ids"`
	KBIDs                  []string               `json:"kb_ids"`
	LLMID                  *string                `json:"llm_id"`
	LLMSetting             map[string]interface{} `json:"llm_setting"`
	RerankID               *string                `json:"rerank_id"`
	PromptConfig           map[string]interface{} `json:"prompt_config"`
	Description            *string
	TopN                   *int
	TopK                   *int
	SimilarityThreshold    *float64
	VectorSimilarityWeight *float64
	Icon                   *string
	TenantID               *string `json:"tenant_id"`
}

func (s *ChatService) Create(ctx context.Context, userID string, req map[string]interface{}) (map[string]interface{}, common.ErrorCode, error) {
	tenant, err := s.tenantDAO.GetByID(ctx, dao.DB, userID)
	if err != nil {
		return nil, common.CodeDataError, errors.New("tenant not found")
	}

	if tenantValue, ok := req["tenant_id"]; ok && isTruthy(tenantValue) {
		return nil, common.CodeDataError, errors.New("`tenant_id` must not be provided")
	}

	name, err := validateCreateChatName(req["name"])
	if err != nil {
		return nil, common.CodeDataError, err
	}
	req["name"] = name

	if datasetIDsValue, ok := req["dataset_ids"]; ok {
		kbIDs, err := s.validateCreateDatasetIDs(ctx, datasetIDsValue, userID)
		if err != nil {
			return nil, common.CodeDataError, err
		}
		req["kb_ids"] = kbIDs
		delete(req, "dataset_ids")
	}

	if llmIDValue, ok := req["llm_id"]; ok {
		llmID := stringFromValue(llmIDValue)
		llmSetting, _ := mapFromValue(req["llm_setting"])
		tenantLLMID, err := resolveCreateLLMID(ctx, llmID, userID, llmSetting)
		if err != nil {
			return nil, common.CodeDataError, err
		}
		if tenantLLMID != "" {
			req["tenant_llm_id"] = tenantLLMID
		}
	}

	if rerankIDValue, ok := req["rerank_id"]; ok {
		rerankID := stringFromValue(rerankIDValue)
		tenantRerankID, err := resolveCreateRerankID(ctx, rerankID, userID)
		if err != nil {
			return nil, common.CodeDataError, err
		}
		if tenantRerankID != "" {
			req["tenant_rerank_id"] = tenantRerankID
		}
	}

	if promptConfigValue, ok := req["prompt_config"]; ok {
		if _, ok := mapFromValue(promptConfigValue); !ok {
			return nil, common.CodeDataError, errors.New("`prompt_config` should be an object")
		}
	}

	if metaDataFilterValue, ok := req["meta_data_filter"]; ok && metaDataFilterValue != nil {
		if _, ok := mapFromValue(metaDataFilterValue); !ok {
			return nil, common.CodeDataError, errors.New("`meta_data_filter` should be an object")
		}
	}

	if _, ok := req["kb_ids"]; !ok {
		req["kb_ids"] = []string{}
	}
	if _, ok := req["llm_id"]; !ok || req["llm_id"] == nil {
		req["llm_id"] = tenant.LLMID
		if tenant.TenantLLMID != nil {
			req["tenant_llm_id"] = *tenant.TenantLLMID
		}
	}
	if stringFromValue(req["llm_id"]) != "" && !isTruthy(req["tenant_llm_id"]) {
		llmSetting, _ := mapFromValue(req["llm_setting"])
		tenantLLMID, err := resolveCreateLLMID(ctx, stringFromValue(req["llm_id"]), userID, llmSetting)
		if err != nil {
			return nil, common.CodeDataError, err
		}
		if tenantLLMID != "" {
			req["tenant_llm_id"] = tenantLLMID
		}
	}
	if _, ok := req["llm_setting"]; !ok || req["llm_setting"] == nil {
		req["llm_setting"] = map[string]interface{}{}
	}
	if _, ok := req["description"]; !ok {
		req["description"] = "A helpful Assistant"
	}
	if _, ok := req["top_n"]; !ok {
		req["top_n"] = 6
	}
	if _, ok := req["top_k"]; !ok {
		req["top_k"] = 1024
	}
	if _, ok := req["rerank_id"]; !ok {
		req["rerank_id"] = ""
		req["tenant_rerank_id"] = nil
	}
	if _, ok := req["similarity_threshold"]; !ok {
		req["similarity_threshold"] = 0.1
	}
	if _, ok := req["vector_similarity_weight"]; !ok {
		req["vector_similarity_weight"] = 0.3
	}
	if _, ok := req["do_refer"]; !ok {
		req["do_refer"] = "1"
	}
	if _, ok := req["icon"]; !ok {
		req["icon"] = ""
	}
	if _, ok := req["meta_data_filter"]; !ok || req["meta_data_filter"] == nil {
		req["meta_data_filter"] = map[string]interface{}{}
	}

	applyCreatePromptDefaults(req)
	filterCreateChatPersistedFields(req)

	exists, err := s.chatDAO.ExistsByNameTenantStatus(ctx, dao.DB, name, userID, string(entity.StatusValid))
	if err != nil {
		return nil, common.CodeServerError, err
	}
	if exists {
		return nil, common.CodeDataError, errors.New("duplicated chat name in creating chat")
	}

	chat := buildCreateChatEntity(req, userID)
	if err = s.chatDAO.Create(ctx, dao.DB, chat); err != nil {
		return nil, common.CodeDataError, errors.New("failed to create chat")
	}

	chat, err = s.chatDAO.GetByID(ctx, dao.DB, chat.ID)
	if err != nil {
		return nil, common.CodeDataError, errors.New("failed to retrieve created chat")
	}

	response, err := s.buildCreateChatResponse(ctx, chat)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	return response, common.CodeSuccess, nil
}

func validateCreateChatName(value interface{}) (string, error) {
	if value == nil {
		return "", errors.New("`name` is required")
	}
	name, ok := value.(string)
	if !ok {
		return "", errors.New("chat name must be a string")
	}
	name = strings.TrimSpace(name)
	if name == "" {
		return "", errors.New("`name` is required")
	}
	if len([]byte(name)) > 255 {
		return "", fmt.Errorf("chat name length is %d which is larger than 255", len([]byte(name)))
	}
	return name, nil
}

func (s *ChatService) validateCreateDatasetIDs(ctx context.Context, value interface{}, tenantID string) ([]string, error) {
	if value == nil {
		return []string{}, nil
	}
	values, ok := listFromValue(value)
	if !ok {
		return nil, errors.New("`dataset_ids` should be a list")
	}

	normalizedIDs := make([]string, 0, len(values))
	kbs := make([]*entity.Knowledgebase, 0, len(values))
	for _, item := range values {
		if !isTruthy(item) {
			continue
		}
		datasetID := stringFromValue(item)
		normalizedIDs = append(normalizedIDs, datasetID)
	}

	for _, datasetID := range normalizedIDs {
		if !s.kbDAO.Accessible(ctx, dao.DB, datasetID, tenantID) {
			return nil, fmt.Errorf("you don't own the dataset %s", datasetID)
		}
		kb, err := s.kbDAO.GetByID(ctx, dao.DB, datasetID)
		if err != nil {
			return nil, fmt.Errorf("you don't own the dataset %s", datasetID)
		}
		if kb.ChunkNum == 0 {
			return nil, fmt.Errorf("the dataset %s doesn't own parsed file", datasetID)
		}
		kbs = append(kbs, kb)
	}

	if err := ValidateDatasetEmbeddingModels(kbs); err != nil {
		return nil, err
	}
	return normalizedIDs, nil
}

func resolveCreateLLMID(ctx context.Context, llmID, tenantID string, llmSetting map[string]interface{}) (string, error) {
	if llmID == "" {
		return "", nil
	}
	modelType := entity.ModelTypeChat
	switch confModelType := llmSetting["model_type"].(type) {
	case string:
		if confModelType == entity.ModelTypeImage2Text.String() {
			modelType = entity.ModelTypeImage2Text
		}
	case []interface{}:
		for _, item := range confModelType {
			if item == entity.ModelTypeImage2Text.String() {
				modelType = entity.ModelTypeImage2Text
				break
			}
		}
	case []string:
		for _, item := range confModelType {
			if item == entity.ModelTypeImage2Text.String() {
				modelType = entity.ModelTypeImage2Text
				break
			}
		}
	}
	modelProvider := NewModelProviderService()
	if _, _, _, _, err := modelProvider.ResolveModelConfig(ctx, tenantID, modelType, llmID); err != nil {
		return "", fmt.Errorf("`llm_id` %s doesn't exist", llmID)
	}
	tenantLLMID, err := modelProvider.ResolveModelID(ctx, tenantID, modelType, llmID)
	if err != nil {
		return "", err
	}
	return tenantLLMID, nil
}

func resolveCreateRerankID(ctx context.Context, rerankID, tenantID string) (string, error) {
	if rerankID == "" {
		return "", nil
	}
	llmName := strings.Split(rerankID, "@")[0]
	if _, ok := DefaultRerankModels[llmName]; ok {
		return "", nil
	}
	modelProvider := NewModelProviderService()
	if _, _, _, _, err := modelProvider.ResolveModelConfig(ctx, tenantID, entity.ModelTypeRerank, rerankID); err != nil {
		return "", fmt.Errorf("`rerank_id` %s doesn't exist", rerankID)
	}
	tenantRerankID, err := modelProvider.ResolveModelID(ctx, tenantID, entity.ModelTypeRerank, rerankID)
	if err != nil {
		return "", err
	}
	return tenantRerankID, nil
}

func applyCreatePromptDefaults(req map[string]interface{}) {
	promptConfig, _ := mapFromValue(req["prompt_config"])
	if promptConfig == nil {
		promptConfig = map[string]interface{}{}
	}
	if system, ok := promptConfig["system"]; !ok || !isTruthy(system) {
		promptConfig["system"] = pyDefaultSystemPrompt
	}
	if _, ok := promptConfig["prologue"]; !ok {
		promptConfig["prologue"] = pyDefaultPrologue
	}
	if _, ok := promptConfig["parameters"]; !ok {
		promptConfig["parameters"] = []interface{}{map[string]interface{}{"key": "knowledge", "optional": false}}
	}
	if _, ok := promptConfig["empty_response"]; !ok {
		promptConfig["empty_response"] = pyDefaultEmptyResponse
	}
	if _, ok := promptConfig["quote"]; !ok {
		promptConfig["quote"] = true
	}
	if _, ok := promptConfig["tts"]; !ok {
		promptConfig["tts"] = false
	}
	if _, ok := promptConfig["refine_multiturn"]; !ok {
		promptConfig["refine_multiturn"] = true
	}

	kbIDs, _ := listFromValue(req["kb_ids"])
	system, _ := promptConfig["system"].(string)
	if len(kbIDs) > 0 && !isTruthy(promptConfig["parameters"]) && strings.Contains(system, "{knowledge}") {
		promptConfig["parameters"] = []interface{}{map[string]interface{}{"key": "knowledge", "optional": false}}
	}
	req["prompt_config"] = promptConfig
}

func filterCreateChatPersistedFields(req map[string]interface{}) {
	persisted := map[string]struct{}{
		"name": {}, "description": {}, "icon": {}, "language": {}, "llm_id": {}, "tenant_llm_id": {},
		"llm_setting": {}, "prompt_type": {}, "prompt_config": {}, "meta_data_filter": {},
		"similarity_threshold": {}, "vector_similarity_weight": {}, "top_n": {}, "top_k": {},
		"do_refer": {}, "rerank_id": {}, "tenant_rerank_id": {}, "kb_ids": {}, "status": {},
	}
	for key := range req {
		if _, ok := persisted[key]; !ok {
			delete(req, key)
		}
	}
	for key := range ReadOnlyFields {
		delete(req, key)
	}
}

func buildCreateChatEntity(req map[string]interface{}, tenantID string) *entity.Chat {
	name := stringFromValue(req["name"])
	description := stringFromValue(req["description"])
	icon := stringFromValue(req["icon"])
	llmID := stringFromValue(req["llm_id"])
	rerankID := stringFromValue(req["rerank_id"])
	tenantLLMID := stringFromValue(req["tenant_llm_id"])
	tenantRerankID := stringFromValue(req["tenant_rerank_id"])
	llmSetting, _ := mapFromValue(req["llm_setting"])
	promptConfig, _ := mapFromValue(req["prompt_config"])
	kbIDs, _ := stringListFromValue(req["kb_ids"])
	kbIDsJSON := make(entity.JSONSlice, 0, len(kbIDs))
	for _, id := range kbIDs {
		kbIDsJSON = append(kbIDsJSON, id)
	}
	status, hasStatus := req["status"]
	statusValue := string(entity.StatusValid)
	if hasStatus {
		statusValue = stringFromValue(status)
	}

	chat := &entity.Chat{
		ID:                     utility.GenerateUUID(),
		TenantID:               tenantID,
		Name:                   &name,
		Description:            &description,
		Icon:                   &icon,
		LLMID:                  llmID,
		TenantLLMID:            stringPtrIfNotEmpty(tenantLLMID),
		LLMSetting:             entity.JSONMap(llmSetting),
		PromptType:             stringFromValue(req["prompt_type"]),
		PromptConfig:           entity.JSONMap(promptConfig),
		SimilarityThreshold:    floatFromValue(req["similarity_threshold"]),
		VectorSimilarityWeight: floatFromValue(req["vector_similarity_weight"]),
		TopN:                   int64FromValue(req["top_n"]),
		TopK:                   int64FromValue(req["top_k"]),
		DoRefer:                stringFromValue(req["do_refer"]),
		RerankID:               rerankID,
		TenantRerankID:         stringPtrIfNotEmpty(tenantRerankID),
		KBIDs:                  kbIDsJSON,
		Status:                 &statusValue,
	}
	if chat.PromptType == "" {
		chat.PromptType = "simple"
	}
	if chat.DoRefer == "" {
		chat.DoRefer = "1"
	}
	if language := stringFromValue(req["language"]); language != "" {
		chat.Language = &language
	}
	if metaDataFilter, ok := mapFromValue(req["meta_data_filter"]); ok {
		metaDataFilterJSON := entity.JSONMap(metaDataFilter)
		chat.MetaDataFilter = &metaDataFilterJSON
	} else {
		metaDataFilterJSON := entity.JSONMap{}
		chat.MetaDataFilter = &metaDataFilterJSON
	}
	return chat
}

func stringPtrIfNotEmpty(value string) *string {
	if value == "" {
		return nil
	}
	return &value
}

func (s *ChatService) buildCreateChatResponse(ctx context.Context, chat *entity.Chat) (map[string]interface{}, error) {
	data, err := structToMap(chat)
	if err != nil {
		return nil, err
	}
	kbNames, datasetIDs := s.getDatasetNamesAndIDs(ctx, chat.KBIDs)
	data["dataset_ids"] = datasetIDs
	delete(data, "kb_ids")
	data["kb_names"] = kbNames
	data["meta_data_filter"] = normalizeMetaDataFilter(chat.MetaDataFilter)
	return data, nil
}

func structToMap(value interface{}) (map[string]interface{}, error) {
	bytes, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}
	result := map[string]interface{}{}
	if err = json.Unmarshal(bytes, &result); err != nil {
		return nil, err
	}
	return result, nil
}

func stringFromValue(value interface{}) string {
	switch typed := value.(type) {
	case nil:
		return ""
	case string:
		return typed
	default:
		return fmt.Sprint(typed)
	}
}

func mapFromValue(value interface{}) (map[string]interface{}, bool) {
	switch typed := value.(type) {
	case nil:
		return nil, false
	case map[string]interface{}:
		return typed, true
	case entity.JSONMap:
		return typed, true
	default:
		return nil, false
	}
}

func normalizeMetaDataFilter(value *entity.JSONMap) entity.JSONMap {
	if value == nil || *value == nil {
		return entity.JSONMap{}
	}
	return *value
}

func listFromValue(value interface{}) ([]interface{}, bool) {
	switch typed := value.(type) {
	case nil:
		return nil, false
	case []interface{}:
		return typed, true
	case []string:
		result := make([]interface{}, 0, len(typed))
		for _, item := range typed {
			result = append(result, item)
		}
		return result, true
	case entity.JSONSlice:
		return typed, true
	default:
		return nil, false
	}
}

func stringListFromValue(value interface{}) ([]string, bool) {
	values, ok := listFromValue(value)
	if !ok {
		return nil, false
	}
	result := make([]string, 0, len(values))
	for _, item := range values {
		if !isTruthy(item) {
			continue
		}
		result = append(result, stringFromValue(item))
	}
	return result, true
}

func int64FromValue(value interface{}) int64 {
	switch typed := value.(type) {
	case int:
		return int64(typed)
	case int64:
		return typed
	case float64:
		return int64(typed)
	case json.Number:
		n, err := typed.Int64()
		if err == nil {
			return n
		}
		f, _ := typed.Float64()
		return int64(f)
	default:
		return 0
	}
}

func floatFromValue(value interface{}) float64 {
	switch typed := value.(type) {
	case float64:
		return typed
	case float32:
		return float64(typed)
	case int:
		return float64(typed)
	case int64:
		return float64(typed)
	case json.Number:
		n, _ := typed.Float64()
		return n
	default:
		return 0
	}
}

func isTruthy(value interface{}) bool {
	switch typed := value.(type) {
	case nil:
		return false
	case bool:
		return typed
	case string:
		return typed != ""
	case int:
		return typed != 0
	case int64:
		return typed != 0
	case float64:
		return typed != 0
	case json.Number:
		n, err := typed.Float64()
		return err != nil || n != 0
	case []interface{}:
		return len(typed) > 0
	case []string:
		return len(typed) > 0
	case map[string]interface{}:
		return len(typed) > 0
	default:
		return true
	}
}

// getDatasetNamesAndIDs gets knowledge base names by IDs
func (s *ChatService) getDatasetNamesAndIDs(ctx context.Context, kbIDs entity.JSONSlice) ([]string, []string) {
	var names = make([]string, 0, len(kbIDs))
	var ids = make([]string, 0, len(kbIDs))
	for _, kbID := range kbIDs {
		kbIDStr, ok := kbID.(string)
		if !ok {
			continue
		}
		kb, err := s.kbDAO.GetByID(ctx, dao.DB, kbIDStr)
		if err != nil || kb == nil {
			continue
		}
		// Only include valid KBs
		if kb.Status != nil && *kb.Status == "1" {
			names = append(names, kb.Name)
			ids = append(ids, kbIDStr)
		}
	}
	return names, ids
}

const (
	pyDefaultSystemPrompt = "You are an intelligent assistant. Please summarize the content of the dataset to answer the question. " +
		"Please list the data in the dataset and answer in detail. " +
		"When all dataset content is irrelevant to the question, your answer must include the sentence " +
		`"The answer you are looking for is not found in the dataset!" ` +
		"Answers need to consider chat history.\n" +
		"      Here is the knowledge base:\n" +
		"      {knowledge}\n" +
		"      The above is the knowledge base."

	pyDefaultPrologue      = "Hi! I'm your assistant. What can I do for you?"
	pyDefaultEmptyResponse = "Sorry! No relevant content was found in the knowledge base!"
)

// splitModelNameAndFactory extracts the base model name by stripping
// provider and instance suffixes, matching Python's rsplit("@", 2)[0].
func (s *ChatService) splitModelNameAndFactory(embeddingModelID string) string {
	if idx := strings.LastIndex(embeddingModelID, "@"); idx > 0 {
		// Strip the provider segment.
		base := embeddingModelID[:idx]
		// Strip the instance segment (second-to-last @).
		if idx2 := strings.LastIndex(base, "@"); idx2 > 0 {
			return base[:idx2]
		}
		return base
	}
	return embeddingModelID
}

func (s *ChatService) getOwnedValidChat(ctx context.Context, userID, chatID string) (*entity.Chat, error) {
	chat, err := s.chatDAO.GetByIDAndStatus(ctx, dao.DB, chatID, string(entity.StatusValid))
	if err != nil {
		return nil, errors.New("no authorization")
	}
	if chat.TenantID != userID {
		return nil, errors.New("no authorization")
	}
	return chat, nil
}

var chatPersistedFields = map[string]struct{}{
	"name":                     {},
	"description":              {},
	"icon":                     {},
	"language":                 {},
	"llm_id":                   {},
	"tenant_llm_id":            {},
	"llm_setting":              {},
	"prompt_type":              {},
	"prompt_config":            {},
	"meta_data_filter":         {},
	"similarity_threshold":     {},
	"vector_similarity_weight": {},
	"top_n":                    {},
	"top_k":                    {},
	"do_refer":                 {},
	"rerank_id":                {},
	"tenant_rerank_id":         {},
	"kb_ids":                   {},
	"status":                   {},
}

var chatReadonlyFields = map[string]struct{}{
	"id":          {},
	"tenant_id":   {},
	"created_by":  {},
	"create_time": {},
	"create_date": {},
	"update_time": {},
	"update_date": {},
}

var defaultRerankModels = map[string]struct{}{
	"BAAI/bge-reranker-v2-m3":           {},
	"maidalun1020/bce-reranker-base_v1": {},
}

// UpdateChat mirrors PUT /api/v1/chats/<chat_id> in the Python REST API.
func (s *ChatService) UpdateChat(ctx context.Context, userID, chatID string, req map[string]interface{}) (map[string]interface{}, error) {
	return s.updateChatREST(ctx, userID, chatID, req, false)
}

// PatchChat mirrors PATCH /api/v1/chats/<chat_id> in the Python REST API.
func (s *ChatService) PatchChat(ctx context.Context, userID, chatID string, req map[string]interface{}) (map[string]interface{}, error) {
	return s.updateChatREST(ctx, userID, chatID, req, true)
}

func (s *ChatService) updateChatREST(ctx context.Context, userID, chatID string, req map[string]interface{}, patch bool) (map[string]interface{}, error) {
	currentChat, err := s.getOwnedValidChat(ctx, userID, chatID)
	if err != nil {
		return nil, err
	}
	if _, err = s.tenantDAO.GetByID(ctx, dao.DB, userID); err != nil {
		return nil, errors.New("tenant not found")
	}

	if !patch && isTruthy(req["tenant_id"]) {
		return nil, errors.New("`tenant_id` must not be provided")
	}

	if value, ok := req["name"]; ok {
		name, shouldSet, err := validateRESTChatName(value, !patch)
		if err != nil {
			return nil, err
		}
		if shouldSet {
			req["name"] = name
		} else {
			delete(req, "name")
		}
	}

	if value, ok := req["dataset_ids"]; ok {
		kbIDs, err := s.validateRESTDatasetIDs(ctx, value, userID)
		if err != nil {
			return nil, err
		}
		req["kb_ids"] = kbIDs
		delete(req, "dataset_ids")
	}

	var llmSetting map[string]interface{}
	llmSettingProvided := false
	if value, ok := req["llm_setting"]; ok {
		llmSettingProvided = true
		setting, ok := mapFromValue(value)
		if !ok {
			return nil, errors.New("`llm_setting` should be an object")
		}
		llmSetting = setting
	}

	if value, ok := req["llm_id"]; ok {
		llmID := fmt.Sprint(value)
		tenantLLMID, err := s.resolveRESTLLMID(ctx, llmID, userID, llmSetting)
		if err != nil {
			return nil, err
		}
		if tenantLLMID != "" {
			req["tenant_llm_id"] = tenantLLMID
		}
	}

	if value, ok := req["rerank_id"]; ok {
		rerankID := fmt.Sprint(value)
		tenantRerankID, err := s.resolveRESTRerankID(ctx, rerankID, userID)
		if err != nil {
			return nil, err
		}
		if tenantRerankID != "" {
			req["tenant_rerank_id"] = tenantRerankID
		}
	}

	if value, ok := req["prompt_config"]; ok {
		promptConfig, ok := mapFromValue(value)
		if !ok {
			return nil, errors.New("`prompt_config` should be an object")
		}
		if patch {
			req["prompt_config"] = mergeJSONMap(currentChat.PromptConfig, promptConfig)
		} else {
			req["prompt_config"] = entity.JSONMap(promptConfig)
		}
	}

	if llmSettingProvided {
		if patch {
			req["llm_setting"] = mergeJSONMap(currentChat.LLMSetting, llmSetting)
		} else {
			req["llm_setting"] = entity.JSONMap(llmSetting)
		}
	}

	if value, ok := req["meta_data_filter"]; ok {
		if value == nil {
			req["meta_data_filter"] = entity.JSONMap{}
		} else {
			metaDataFilter, ok := mapFromValue(value)
			if !ok {
				return nil, errors.New("`meta_data_filter` should be an object")
			}
			req["meta_data_filter"] = entity.JSONMap(metaDataFilter)
		}
	} else if currentChat.MetaDataFilter == nil || *currentChat.MetaDataFilter == nil {
		req["meta_data_filter"] = entity.JSONMap{}
	}

	updates := filterRESTChatUpdates(req)
	if value, ok := updates["name"]; ok {
		name := value.(string)
		currentName := ""
		if currentChat.Name != nil {
			currentName = *currentChat.Name
		}
		if strings.ToLower(name) != strings.ToLower(currentName) {
			existingNames, err := s.chatDAO.GetExistingNames(ctx, dao.DB, userID, string(entity.StatusValid))
			if err != nil {
				return nil, err
			}
			for _, existingName := range existingNames {
				if strings.EqualFold(existingName, name) {
					return nil, errors.New("duplicated chat name")
				}
			}
		}
	}

	if len(updates) > 0 {
		if err = s.chatDAO.UpdateByID(ctx, dao.DB, chatID, updates); err != nil {
			if patch {
				return nil, errors.New("failed to update chat")
			}
			return nil, errors.New("chat not found")
		}
	}

	updatedChat, err := s.chatDAO.GetByID(ctx, dao.DB, chatID)
	if err != nil {
		return nil, errors.New("failed to retrieve updated chat")
	}
	return s.buildRESTChatResponse(ctx, updatedChat), nil
}

func validateRESTChatName(value interface{}, required bool) (string, bool, error) {
	if value == nil {
		if required {
			return "", false, errors.New("`name` is required")
		}
		return "", false, nil
	}
	name, ok := value.(string)
	if !ok {
		return "", false, errors.New("chat name must be a string")
	}
	name = strings.TrimSpace(name)
	if name == "" {
		if required {
			return "", false, errors.New("`name` is required")
		}
		return "", false, errors.New("`name` cannot be empty")
	}
	if len([]byte(name)) > 255 {
		return "", false, fmt.Errorf("chat name length is %d which is larger than 255", len([]byte(name)))
	}
	return name, true, nil
}

func (s *ChatService) validateRESTDatasetIDs(ctx context.Context, value interface{}, userID string) (entity.JSONSlice, error) {
	if value == nil {
		return entity.JSONSlice{}, nil
	}
	items, ok := value.([]interface{})
	if !ok {
		return nil, errors.New("`dataset_ids` should be a list")
	}

	var kbs []*entity.Knowledgebase
	kbIDs := make(entity.JSONSlice, 0, len(items))
	for _, item := range items {
		if !isTruthy(item) {
			continue
		}
		datasetID := fmt.Sprint(item)
		if !s.kbDAO.Accessible(ctx, dao.DB, datasetID, userID) {
			return nil, fmt.Errorf("you don't own the dataset %s", datasetID)
		}
		kb, err := s.kbDAO.GetByID(ctx, dao.DB, datasetID)
		if err != nil || kb == nil {
			return nil, fmt.Errorf("you don't own the dataset %s", datasetID)
		}
		if kb.ChunkNum == 0 {
			return nil, fmt.Errorf("the dataset %s doesn't own parsed file", datasetID)
		}
		kbs = append(kbs, kb)
		kbIDs = append(kbIDs, datasetID)
	}

	embeddingModelIDs := make([]string, 0, len(kbs))
	seenEmbedIDs := make(map[string]struct{})
	for _, kb := range kbs {
		embeddingModelIDs = append(embeddingModelIDs, kb.EmbdID)
		seenEmbedIDs[s.splitModelNameAndFactory(kb.EmbdID)] = struct{}{}
	}
	if len(seenEmbedIDs) > 1 {
		return nil, fmt.Errorf("datasets use different embedding models: %v", embeddingModelIDs)
	}
	return kbIDs, nil
}

func (s *ChatService) resolveRESTLLMID(ctx context.Context, llmID, tenantID string, llmSetting map[string]interface{}) (string, error) {
	if llmID == "" {
		return "", nil
	}
	modelType := entity.ModelTypeChat
	if rawModelType, ok := llmSetting["model_type"]; ok {
		switch typedModelType := rawModelType.(type) {
		case string:
			if typedModelType == entity.ModelTypeImage2Text.String() {
				modelType = entity.ModelTypeImage2Text
			}
		case []interface{}:
			for _, item := range typedModelType {
				if fmt.Sprint(item) == entity.ModelTypeImage2Text.String() {
					modelType = entity.ModelTypeImage2Text
					break
				}
			}
		}
	}
	modelProvider := NewModelProviderService()
	if _, _, _, _, err := modelProvider.ResolveModelConfig(ctx, tenantID, modelType, llmID); err != nil {
		return "", fmt.Errorf("`llm_id` %s doesn't exist", llmID)
	}
	tenantLLMID, err := modelProvider.ResolveModelID(ctx, tenantID, modelType, llmID)
	if err != nil {
		return "", err
	}
	return tenantLLMID, nil
}

func (s *ChatService) resolveRESTRerankID(ctx context.Context, rerankID, tenantID string) (string, error) {
	if rerankID == "" {
		return "", nil
	}
	baseName := s.splitModelNameAndFactory(rerankID)
	if _, ok := defaultRerankModels[baseName]; ok {
		return "", nil
	}
	modelProvider := NewModelProviderService()
	if _, _, _, _, err := modelProvider.ResolveModelConfig(ctx, tenantID, entity.ModelTypeRerank, rerankID); err != nil {
		return "", fmt.Errorf("`rerank_id` %s doesn't exist", rerankID)
	}
	tenantRerankID, err := modelProvider.ResolveModelID(ctx, tenantID, entity.ModelTypeRerank, rerankID)
	if err != nil {
		return "", err
	}
	return tenantRerankID, nil
}

func filterRESTChatUpdates(req map[string]interface{}) map[string]interface{} {
	updates := make(map[string]interface{})
	for field, value := range req {
		if _, ok := chatPersistedFields[field]; !ok {
			continue
		}
		if _, ok := chatReadonlyFields[field]; ok {
			continue
		}
		updates[field] = value
	}
	return updates
}

func mergeJSONMap(base entity.JSONMap, patch map[string]interface{}) entity.JSONMap {
	merged := entity.JSONMap{}
	for key, value := range base {
		merged[key] = value
	}
	for key, value := range patch {
		merged[key] = value
	}
	return merged
}

func (s *ChatService) buildRESTChatResponse(ctx context.Context, chat *entity.Chat) map[string]interface{} {
	kbNames, datasetIDs := s.getDatasetNamesAndIDs(ctx, chat.KBIDs)
	return map[string]interface{}{
		"id":                       chat.ID,
		"tenant_id":                chat.TenantID,
		"name":                     chat.Name,
		"description":              chat.Description,
		"icon":                     chat.Icon,
		"language":                 chat.Language,
		"llm_id":                   chat.LLMID,
		"tenant_llm_id":            chat.TenantLLMID,
		"llm_setting":              chat.LLMSetting,
		"prompt_type":              chat.PromptType,
		"prompt_config":            chat.PromptConfig,
		"meta_data_filter":         normalizeMetaDataFilter(chat.MetaDataFilter),
		"similarity_threshold":     chat.SimilarityThreshold,
		"vector_similarity_weight": chat.VectorSimilarityWeight,
		"top_n":                    chat.TopN,
		"top_k":                    chat.TopK,
		"do_refer":                 chat.DoRefer,
		"rerank_id":                chat.RerankID,
		"tenant_rerank_id":         chat.TenantRerankID,
		"dataset_ids":              datasetIDs,
		"kb_names":                 kbNames,
		"status":                   chat.Status,
		"create_time":              chat.CreateTime,
		"create_date":              chat.CreateDate,
		"update_time":              chat.UpdateTime,
		"update_date":              chat.UpdateDate,
	}
}

// DeleteChat soft deletes a single chat owned by the current user.
func (s *ChatService) DeleteChat(ctx context.Context, userID, chatID string) error {
	if _, err := s.getOwnedValidChat(ctx, userID, chatID); err != nil {
		return err
	}
	if err := s.chatDAO.UpdateByID(ctx, dao.DB, chatID, map[string]interface{}{
		"status": string(entity.StatusInvalid),
	}); err != nil {
		return fmt.Errorf("failed to delete chat %s", chatID)
	}

	return nil
}

// BulkDeleteChatsRequest matches DELETE /api/v1/chats request semantics.
type BulkDeleteChatsRequest struct {
	IDs       []string `json:"ids,omitempty"`
	DeleteAll bool     `json:"delete_all,omitempty"`
	ChatID    string   `json:"chat_id,omitempty"`
}

// checkDuplicateChatIDs
func checkDuplicateChatIDs(ids []string) ([]string, []string) {
	idCount := make(map[string]int, len(ids))
	uniqueIDs := make([]string, 0, len(ids))
	for _, id := range ids {
		id = strings.TrimSpace(id)
		if id == "" {
			continue
		}
		idCount[id]++
		if idCount[id] == 1 {
			uniqueIDs = append(uniqueIDs, id)
		}
	}

	duplicateMessages := make([]string, 0)
	for id, count := range idCount {
		if count > 1 {
			duplicateMessages = append(duplicateMessages, fmt.Sprintf("Duplicate chat ids: %s", id))
		}
	}
	return uniqueIDs, duplicateMessages
}

// BulkDeleteChats soft deletes chats owned by the current user with partial success semantics.
func (s *ChatService) BulkDeleteChats(ctx context.Context, userID string, req *BulkDeleteChatsRequest) (map[string]interface{}, error) {
	ids := req.IDs
	if len(ids) == 0 && req.DeleteAll {
		chats, err := s.chatDAO.ListByTenantID(ctx, dao.DB, userID, string(entity.StatusValid))
		if err != nil {
			return nil, err
		}
		for _, chat := range chats {
			ids = append(ids, chat.ID)
		}
		if len(ids) == 0 {
			return map[string]interface{}{}, nil
		}
	}

	uniqueIDs, duplicateMessages := checkDuplicateChatIDs(ids)
	errorsList := make([]string, 0, len(duplicateMessages))
	errorsList = append(errorsList, duplicateMessages...)
	successCount := 0

	for _, chatID := range uniqueIDs {
		if _, err := s.getOwnedValidChat(ctx, userID, chatID); err != nil {
			errorsList = append(errorsList, fmt.Sprintf("Chat(%s) not found.", chatID))
			continue
		}
		if err := s.chatDAO.UpdateByID(ctx, dao.DB, chatID, map[string]interface{}{
			"status": string(entity.StatusInvalid),
		}); err != nil {
			errorsList = append(errorsList, fmt.Sprintf("Failed to delete chat %s", chatID))
			continue
		}
		successCount++
	}

	if len(errorsList) == 0 {
		return map[string]interface{}{"success_count": successCount}, nil
	}
	if successCount > 0 {
		return map[string]interface{}{
			"success_count": successCount,
			"errors":        errorsList,
		}, nil
	}

	return nil, errors.New(strings.Join(errorsList, "; "))
}

// strPtr returns a pointer to a string
func strPtr(s string) *string {
	return &s
}

// Helper to count UTF-8 characters (not bytes)
func (s *ChatService) countRunes(str string) int {
	return utf8.RuneCountInString(str)
}

// GetChatResponse get chat response with kb_names
// Reference: Python _build_chat_response
type GetChatResponse struct {
	*entity.Chat
	DatasetIDs []string `json:"dataset_ids"`
	KBNames    []string `json:"kb_names"`
}

// GetChat gets chat detail by ID with permission check
func (s *ChatService) GetChat(ctx context.Context, userID string, chatID string) (*GetChatResponse, error) {
	// Step 1: Get user tenants (same as Python UserTenantService.query(user_id=current_user.id))
	tenants, err := s.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get user tenants: %w", err)
	}

	// Step 2: Check if user has permission to access this chat
	// Python: for tenant in tenants: if DialogService.query(tenant_id=tenant.tenant_id, id=chat_id, status=StatusEnum.VALID.value): break
	hasPermission := false
	for _, tenant := range tenants {
		chats, err := s.chatDAO.QueryByTenantIDAndID(ctx, dao.DB, tenant.TenantID, chatID, "1")
		if err != nil {
			continue // Try next tenant
		}
		if len(chats) > 0 {
			hasPermission = true
			break
		}
	}

	if !hasPermission {
		return nil, fmt.Errorf("no authorization")
	}

	// Step 3: Get chat detail (same as Python DialogService.get_by_id(chat_id))
	chat, err := s.chatDAO.GetByID(ctx, dao.DB, chatID)
	if err != nil {
		return nil, fmt.Errorf("chat not found")
	}

	// Step 4: Build response with kb_names (same as Python _build_chat_response)
	// Resolve kb_ids to kb_names
	kbNames, datasetIDs := s.getDatasetNamesAndIDs(ctx, chat.KBIDs)

	return &GetChatResponse{
		Chat:       chat,
		DatasetIDs: datasetIDs,
		KBNames:    kbNames,
	}, nil
}
