//
//  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 (
	"bytes"
	"context"
	"fmt"
	"regexp"
	"strings"
	"text/template"
	"time"

	"ragflow/internal/common"
	"ragflow/internal/entity"
	modelModule "ragflow/internal/entity/models"

	"go.uber.org/zap"
)

// KeywordExtraction extracts keywords from content using LLM.
//
// Uses ChatModel to call the LLM with a keyword extraction prompt.
// Returns comma-separated top N important keywords/phrases from the content.
func KeywordExtraction(ctx context.Context, chatModel *modelModule.ChatModel, content string, topN int) (string, error) {
	if chatModel == nil {
		return "", fmt.Errorf("chat model is nil")
	}

	if content == "" {
		return "", nil
	}

	if topN <= 0 {
		topN = 3
	}

	// Load keyword prompt template from file
	keywordPromptTemplate, err := LoadPrompt("keyword_prompt")
	if err != nil {
		return "", fmt.Errorf("failed to load keyword prompt: %w", err)
	}

	// Render template with content and topn
	renderedPrompt := RenderPrompt(keywordPromptTemplate, map[string]interface{}{
		"content": content,
		"topn":    topN,
	})

	// Build messages: system prompt + user "Output:"
	messages := []modelModule.Message{
		{Role: "system", Content: renderedPrompt},
		{Role: "user", Content: "Output: "},
	}

	// Use low temperature for deterministic keyword extraction (matching Python behavior)
	modelConfig := &modelModule.ChatConfig{
		Temperature: func() *float64 { t := 0.2; return &t }(),
	}

	// Call LLM using ChatModel
	response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, modelConfig)
	if err != nil {
		return "", fmt.Errorf("failed to extract keywords: %w", err)
	}

	if response == nil || response.Answer == nil {
		return "", fmt.Errorf("empty response from keyword extraction")
	}

	common.Info("KeywordExtraction result", zap.String("response", *response.Answer))

	// Clean up response - remove thinking tags if present
	result := strings.TrimSpace(*response.Answer)
	result = thinkBlockRE.ReplaceAllString(result, "")
	result = strings.TrimSpace(result)

	if strings.Contains(result, "**ERROR**") {
		return "", fmt.Errorf("error in keyword extraction response")
	}

	return result, nil
}

// CrossLanguages translates a question into multiple languages using LLM.
// The model is fetched internally based on llmID:
//   - If llmID is empty, fetches tenant's default chat model
//   - If llmID is not empty, fetches the specified model (or image2text if type matches)
func CrossLanguages(ctx context.Context, tenantID string, llmID string, query string, languages []string) (string, error) {
	common.Debug("CrossLanguages invoked",
		zap.String("tenantID", tenantID),
		zap.String("llmID", llmID),
		zap.Strings("languages", languages))

	modelProviderSvc := NewModelProviderService()
	var chatModel *modelModule.ChatModel
	var err error

	if llmID != "" {
		modelTypes, err := modelProviderSvc.ResolveModelType(tenantID, llmID)
		if err != nil {
			return query, fmt.Errorf("failed to get model type: %w", err)
		}
		resolvedType := entity.ModelTypeChat
		for _, mt := range modelTypes {
			if mt == entity.ModelTypeImage2Text {
				resolvedType = entity.ModelTypeImage2Text
				break
			}
		}
		driver, modelName, apiConfig, _, err := modelProviderSvc.ResolveModelConfig(tenantID, resolvedType, llmID)
		if err != nil {
			return query, fmt.Errorf("failed to get chat model: %w", err)
		}
		chatModel = modelModule.NewChatModel(driver, &modelName, apiConfig)
	} else {
		driver, modelName, apiConfig, _, err := modelProviderSvc.GetTenantDefaultModelByType(tenantID, entity.ModelTypeChat)
		if err != nil {
			return query, fmt.Errorf("failed to get default chat model: %w", err)
		}
		chatModel = modelModule.NewChatModel(driver, &modelName, apiConfig)
	}
	if chatModel == nil {
		return query, fmt.Errorf("failed to get chat model: nil chat model")
	}

	if query == "" {
		return query, nil
	}

	if len(languages) == 0 {
		return query, nil
	}

	// Load system prompt from embedded file
	systemPrompt, err := LoadPrompt("cross_languages_sys_prompt")
	if err != nil {
		return query, fmt.Errorf("failed to load system prompt: %w", err)
	}

	// Load user prompt template from file
	userPromptTemplate, err := LoadPrompt("cross_languages_user_prompt")
	if err != nil {
		return query, fmt.Errorf("failed to load user prompt: %w", err)
	}

	// Render user prompt with query and languages
	userPrompt := RenderPrompt(userPromptTemplate, map[string]interface{}{
		"query":     query,
		"languages": languages,
	})

	// Build messages: system prompt + user prompt
	messages := []modelModule.Message{
		{Role: "system", Content: systemPrompt},
		{Role: "user", Content: userPrompt},
	}

	// Use low temperature for deterministic translation (matching Python behavior)
	modelConfig := &modelModule.ChatConfig{
		Temperature: func() *float64 { t := 0.2; return &t }(),
	}

	// Call LLM using ChatModel
	response, err := chatModel.ModelDriver.ChatWithMessages(*chatModel.ModelName, messages, chatModel.APIConfig, modelConfig)
	if err != nil {
		return query, fmt.Errorf("failed to translate question: %w", err)
	}

	if response == nil || response.Answer == nil {
		return query, fmt.Errorf("empty response from cross languages translation")
	}

	result := *response.Answer

	// Clean up response - remove think tags and trim
	result = thinkBlockRE.ReplaceAllString(result, "")

	if strings.Contains(result, "**ERROR**") {
		return query, nil
	}

	// Parse response
	result = regexp.MustCompile(`(?i)^output:\s*`).ReplaceAllString(result, "")
	result = regexp.MustCompile(`\n+`).ReplaceAllString(result, "")

	parts := strings.Split(result, "===")
	var translations []string
	for _, part := range parts {
		trimmed := strings.TrimSpace(part)
		if trimmed != "" {
			translations = append(translations, trimmed)
		}
	}

	if len(translations) > 0 {
		return strings.Join(translations, "\n"), nil
	}

	return query, nil
}

// fullQuestionTmpl mirrors the Python Jinja2 template
// rag/prompts/full_question_prompt.md. The rendered output is used as the
// system message; the user message is just "Output: ".
var fullQuestionTmpl = template.Must(template.New("full_question").Parse(`## Role
A helpful assistant.

## Task & Steps
1. Generate a full user question that would follow the conversation.
2. If the user's question involves relative dates, convert them into absolute dates based on today ({{.Today}}).
   - "yesterday" = {{.Yesterday}}, "tomorrow" = {{.Tomorrow}}

## Requirements & Restrictions
- If the user's latest question is already complete, don't do anything — just return the original question.
- DON'T generate anything except a refined question.
{{- if .Language }}
- Text generated MUST be in {{.Language}}.
{{- else }}
- Text generated MUST be in the same language as the original user's question.
{{- end }}

---

## Examples

### Example 1
**Conversation:**

USER: What is the name of Donald Trump's father?
ASSISTANT: Fred Trump.
USER: And his mother?

**Output:** What's the name of Donald Trump's mother?

---

### Example 2
**Conversation:**

USER: What is the name of Donald Trump's father?
ASSISTANT: Fred Trump.
USER: And his mother?
ASSISTANT: Mary Trump.
USER: What's her full name?

**Output:** What's the full name of Donald Trump's mother Mary Trump?

---

### Example 3
**Conversation:**

USER: What's the weather today in London?
ASSISTANT: Cloudy.
USER: What's about tomorrow in Rochester?

**Output:** What's the weather in Rochester on {{.Tomorrow}}?

---

## Real Data

**Conversation:**

{{.Conversation}}
`))

var errorMarkerRE = regexp.MustCompile(`\*\*ERROR\*\*`)

// FullQuestion rewrites the latest user question in light of prior
// conversation context (pronouns, dates, follow-ups). Falls back to the
// latest user message on LLM error.
// When language is empty, the original language is preserved (matching Python).
//
// The prompt structure mirrors Python's full_question():
//   - System: fullQuestionTmpl (instructions, examples, conversation)
//   - User: "Output: "
//
// This matches rag/prompts/full_question_prompt.md rendered via Jinja2.
func FullQuestion(
	ctx context.Context,
	chatModel *modelModule.ChatModel,
	messages []map[string]interface{},
	language string,
) (string, error) {
	if chatModel == nil || chatModel.ModelDriver == nil {
		return "", fmt.Errorf("FullQuestion: nil chat model")
	}
	if len(messages) == 0 {
		return "", fmt.Errorf("FullQuestion: empty messages")
	}

	var convLines []string
	for _, m := range messages {
		role, _ := m["role"].(string)
		if role != "user" && role != "assistant" {
			continue
		}
		content, _ := m["content"].(string)
		convLines = append(convLines, fmt.Sprintf("%s: %s", strings.ToUpper(role), content))
	}
	conv := strings.Join(convLines, "\n")

	today := time.Now().Format("2006-01-02")
	tomorrow := time.Now().Add(24 * time.Hour).Format("2006-01-02")
	yesterday := time.Now().Add(-24 * time.Hour).Format("2006-01-02")

	var buf bytes.Buffer
	if err := fullQuestionTmpl.Execute(&buf, map[string]string{
		"Today":        today,
		"Yesterday":    yesterday,
		"Tomorrow":     tomorrow,
		"Conversation": conv,
		"Language":     language,
	}); err != nil {
		return fallbackToLatestUser(messages), fmt.Errorf("FullQuestion: render template: %w", err)
	}
	system := buf.String()

	modelName := ""
	if chatModel.ModelName != nil {
		modelName = *chatModel.ModelName
	}
	msgs := []modelModule.Message{
		{Role: "system", Content: system},
		{Role: "user", Content: "Output: "},
	}
	resp, err := chatModel.ModelDriver.ChatWithMessages(
		modelName, msgs, chatModel.APIConfig, nil,
	)
	if err != nil {
		return fallbackToLatestUser(messages), err
	}
	if resp == nil || resp.Answer == nil {
		return fallbackToLatestUser(messages), fmt.Errorf("FullQuestion: empty response")
	}
	cleaned := strings.TrimSpace(*resp.Answer)
	cleaned = thinkBlockRE.ReplaceAllString(cleaned, "")
	cleaned = strings.TrimSpace(cleaned)
	if errorMarkerRE.MatchString(cleaned) {
		return fallbackToLatestUser(messages), nil
	}
	if cleaned == "" {
		return fallbackToLatestUser(messages), nil
	}
	return cleaned, nil
}

// fallbackToLatestUser returns the last user message, or "" if none.
func fallbackToLatestUser(messages []map[string]interface{}) string {
	for i := len(messages) - 1; i >= 0; i-- {
		role, _ := messages[i]["role"].(string)
		if role == "user" {
			if c, ok := messages[i]["content"].(string); ok {
				return c
			}
			return ""
		}
	}
	return ""
}
