package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/matrix-org/gomatrix"
)

// BotConfig represents the structure of the configuration file
type BotConfig struct {
	Homeserver  string            `json:"homeserver"`
	Username    string            `json:"username"`
	Password    string            `json:"password"`
	RoomID      string            `json:"room_id"`
	AccessToken string            `json:"access_token"`
	AIAPIKey    string            `json:"ai_api_key"`
	AIConfig    AIConfig          `json:"ai_config"`
	Prompt      string            `json:"prompt"`   // Global prompt for initial context
	Commands    map[string]string `json:"commands"` // Command-specific prompts
}

// AIConfig represents the static part of the AI API request
type AIConfig struct {
	Model            string   `json:"model"`
	Stream           bool     `json:"stream"`
	MaxTokens        int      `json:"max_tokens"`
	Stop             []string `json:"stop"`
	Temperature      float64  `json:"temperature"`
	TopP             float64  `json:"top_p"`
	TopK             int      `json:"top_k"`
	FrequencyPenalty float64  `json:"frequency_penalty"`
	N                int      `json:"n"`
}

// AIRequest represents the complete request structure for the AI API
type AIRequest struct {
	Model            string    `json:"model"`
	Messages         []Message `json:"messages"`
	Stream           bool      `json:"stream"`
	MaxTokens        int       `json:"max_tokens"`
	Stop             []string  `json:"stop"`
	Temperature      float64   `json:"temperature"`
	TopP             float64   `json:"top_p"`
	TopK             int       `json:"top_k"`
	FrequencyPenalty float64   `json:"frequency_penalty"`
	N                int       `json:"n"`
}

// Message represents a single message structure
type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

// AIResponse represents the response structure from the AI API
type AIResponse struct {
	ID      string   `json:"id"`
	Choices []Choice `json:"choices"`
	Usage   Usage    `json:"usage"`
	Created int64    `json:"created"`
	Model   string   `json:"model"`
	Object  string   `json:"object"`
}

// Choice represents a single choice in the AI response
type Choice struct {
	Message      Message `json:"message"`
	FinishReason string  `json:"finish_reason"`
}

// Usage represents the usage information in the AI response
type Usage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
}

// TemporaryPrompts maintains a map of user and command specific prompts
var TemporaryPrompts = make(map[string]map[string]string)

func main() {
	// Load configuration
	config, err := loadBotConfig("bot-config.json")
	if err != nil {
		log.Fatalf("Failed to load config: %v", err)
	}

	// Create Matrix client
	client, err := gomatrix.NewClient(config.Homeserver, "", "")
	if err != nil {
		log.Fatalf("Failed to create Matrix client: %v", err)
	}

	// Login to Matrix
	resp, err := client.Login(&gomatrix.ReqLogin{
		Type:     "m.login.password",
		User:     config.Username,
		Password: config.Password,
	})
	if err != nil {
		log.Fatalf("Failed to login: %v", err)
	}

	client.SetCredentials(resp.UserID, resp.AccessToken)

	// Join room
	_, err = client.JoinRoom(config.RoomID, "", nil)
	if err != nil {
		log.Fatalf("Failed to join room: %v", err)
	}

	log.Println("Successfully login")

	// Listen for messages
	syncer := client.Syncer.(*gomatrix.DefaultSyncer)
	syncer.OnEventType("m.room.message", func(event *gomatrix.Event) {
		if event.Sender != resp.UserID {
			handleMessage(client, config, event)
		}
	})

	// Start syncing
	if err := client.Sync(); err != nil {
		log.Fatalf("Sync failed: %v", err)
	}
}

func handleMessage(client *gomatrix.Client, config *BotConfig, event *gomatrix.Event) {
	if event.Content["msgtype"] == "m.text" {
		userMessage := event.Content["body"].(string)
		// Check if the message starts with $$
		if strings.HasPrefix(userMessage, "$$") {
			// Remove the $$ to get the actual message content
			userMessage = strings.TrimPrefix(userMessage, "$$")
			command := getCommand(userMessage)

			aiResponse, err := getAIResponse(client, config, event.Sender, userMessage, command)
			if err != nil {
				log.Printf("Failed to get AI response: %v", err)

				if strings.Contains(err.Error(), "429") {
					aiResponse = "Sorry Reached Limited"
				} else {
					return
				}
			}

			_, err = client.SendText(config.RoomID, aiResponse)
			if err != nil {
				log.Printf("Failed to send message: %v", err)
			}

			// Log the interaction
			logInteraction(event.Sender, userMessage, aiResponse)
		}
	}
}

func getCommand(message string) string {
	// Extract the command from the message
	parts := strings.Fields(message)
	if len(parts) > 0 {
		return parts[0]
	}
	return ""
}

func getAIResponse(client *gomatrix.Client, config *BotConfig, sender, userMessage, command string) (string, error) {
	url := "https://api.siliconflow.cn/v1/chat/completions"

	// Ensure TemporaryPrompts has an entry for the sender
	if _, exists := TemporaryPrompts[sender]; !exists {
		TemporaryPrompts[sender] = make(map[string]string)
	}

	// Check if command exists in configuration
	prompt, exists := config.Commands[command]
	if !exists {
		// If command doesn't exist, use default prompt
		command = "default"
		prompt = config.Prompt
	}

	// Initialize the prompt for the specific command if it doesn't exist
	if _, exists := TemporaryPrompts[sender][command]; !exists {
		TemporaryPrompts[sender][command] = prompt
	}

	// Prepare AI request with dynamic message content
	messages := []Message{
		{Role: "system", Content: TemporaryPrompts[sender][command]}, // Use user and command-specific prompt
		{Role: "user", Content: userMessage},
	}
	payload := AIRequest{
		Model:            config.AIConfig.Model,
		Messages:         messages,
		Stream:           config.AIConfig.Stream,
		MaxTokens:        config.AIConfig.MaxTokens,
		Stop:             config.AIConfig.Stop,
		Temperature:      config.AIConfig.Temperature,
		TopP:             config.AIConfig.TopP,
		TopK:             config.AIConfig.TopK,
		FrequencyPenalty: config.AIConfig.FrequencyPenalty,
		N:                config.AIConfig.N,
	}

	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("failed to marshal payload: %w", err)
	}

	req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes))
	if err != nil {
		return "", fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("accept", "application/json")
	req.Header.Set("content-type", "application/json")
	req.Header.Set("authorization", fmt.Sprintf("Bearer %s", config.AIAPIKey))

	clientHTTP := &http.Client{}
	resp, err := clientHTTP.Do(req)
	if err != nil {
		return "", fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		if resp.StatusCode == http.StatusTooManyRequests {
			// Notify user of rate limit and retry after 10 seconds
			_, err := client.SendText(config.RoomID, "Request limit reached. Retrying in 10 seconds...")
			if err != nil {
				log.Printf("Failed to send retry notification: %v", err)
			}
			time.Sleep(10 * time.Second)
			return getAIResponse(client, config, sender, userMessage, command)
		}
		return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("failed to read response body: %w", err)
	}

	var aiResp AIResponse
	if err := json.Unmarshal(body, &aiResp); err != nil {
		return "", fmt.Errorf("failed to unmarshal response: %w", err)
	}

	if len(aiResp.Choices) > 0 {
		// Update the user's prompt with the latest conversation for memory
		TemporaryPrompts[sender][command] += "\nUser: " + userMessage + "\nAI: " + aiResp.Choices[0].Message.Content
		fmt.Printf("Size %v\n", len(TemporaryPrompts[sender][command]))
		return aiResp.Choices[0].Message.Content, nil
	}

	return "No response from AI", nil
}

func loadBotConfig(filename string) (*BotConfig, error) {
	if _, err := os.Stat(filename); os.IsNotExist(err) {
		// Create default config
		defaultConfig := BotConfig{
			Homeserver:  "https://matrix.org",
			Username:    "your_username",
			Password:    "your_password",
			RoomID:      "!your_room_id:matrix.org",
			AccessToken: "",
			AIAPIKey:    "your_api_key",
			Prompt:      "Your default prompt here", // Default prompt
			AIConfig: AIConfig{
				Model:            "deepseek-ai/deepseek-v2-chat",
				Stream:           false,
				MaxTokens:        512,
				Stop:             []string{"string"},
				Temperature:      0.7,
				TopP:             0.7,
				TopK:             50,
				FrequencyPenalty: 0.5,
				N:                1,
			},
			Commands: map[string]string{
				"translate": "Translate prompt here",
				"summarize": "Summarize prompt here",
			},
		}

		file, err := os.Create(filename)
		if err != nil {
			return nil, fmt.Errorf("failed to create config file: %w", err)
		}
		defer file.Close()

		encoder := json.NewEncoder(file)
		if err := encoder.Encode(defaultConfig); err != nil {
			return nil, fmt.Errorf("failed to write default config to file: %w", err)
		}

		return &defaultConfig, nil
	}

	file, err := os.Open(filename)
	if err != nil {
		return nil, fmt.Errorf("failed to open config file: %w", err)
	}
	defer file.Close()

	var config BotConfig
	decoder := json.NewDecoder(file)
	if err := decoder.Decode(&config); err != nil {
		return nil, fmt.Errorf("failed to decode config file: %w", err)
	}

	return &config, nil
}

func logInteraction(sender, userMessage, aiResponse string) {
	logDir := "logs"
	if _, err := os.Stat(logDir); os.IsNotExist(err) {
		os.Mkdir(logDir, os.ModePerm)
	}

	timestamp := time.Now().Format("2006-01-02")
	logFilename := fmt.Sprintf("%s/%s.log", logDir, timestamp)

	logFile, err := os.OpenFile(logFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		log.Printf("Failed to open log file: %v", err)
		return
	}
	defer logFile.Close()

	logEntry := fmt.Sprintf("%s - %s: %s\nAI: %s\n\n", time.Now().Format(time.RFC3339), sender, userMessage, aiResponse)
	if _, err := logFile.WriteString(logEntry); err != nil {
		log.Printf("Failed to write log entry: %v", err)
	}
}
