package main

import (
       "bufio"
       "bytes"
       "encoding/json"
       "flag"
       "fmt"
       "net/http"
       "os"
       "strings"
       "time"
)

// Request struct defines the structure of a request sent to the Ollama API.
type Request struct {
       Model             string    `json:"model"`               // The name of the model to use for generating responses.
       Messages          []Message `json:"messages"`            // A slice of Message structs representing the conversation history.
       Stream            bool      `json:"stream"`              // A boolean flag indicating whether to stream the response.
       ContextWindowSize int       `json:"context_window_size"` // The size of the context window for the conversation.
}

// Message struct defines the structure of a single message in the chat.
type Message struct {
       Role    string `json:"role"`    // The role of the message sender, e.g., "user" or "system".
       Content string `json:"content"` // The content of the message.
}

// Response struct defines the structure of a response received from the Ollama API.
type Response struct {
       Model              string    `json:"model"`                // The name of the model used.
       CreatedAt          time.Time `json:"created_at"`           // The timestamp when the response was created.
       Message            Message   `json:"message"`              // The generated message.
       Done               bool      `json:"done"`                 // A boolean flag indicating whether the response is complete.
       TotalDuration      int64     `json:"total_duration"`       // The total duration of the response generation.
       LoadDuration       int       `json:"load_duration"`        // The duration of loading the model.
       PromptEvalCount    int       `json:"prompt_eval_count"`    // The number of prompt evaluations.
       PromptEvalDuration int       `json:"prompt_eval_duration"` // The duration of prompt evaluations.
       EvalCount          int       `json:"eval_count"`           // The number of evaluations.
       EvalDuration       int64     `json:"eval_duration"`        // The duration of evaluations.
}

// Config struct defines the structure of the configuration file.
type Config struct {
       OllamaURL         string `json:"ollamaURL"`
       OllamaPort        int    `json:"ollamaPort"`
       ModelName         string `json:"modelName"`
       ContextWindowSize int    `json:"contextWindowSize"`
       HumanName         string `json:"humanName"`
       AIName            string `json:"AIName"`
       SystemPrompt      string `json:"systemPrompt"`
       DisplayRows       int    `json:"displayRows"`    // Number of rows to display
       DisplayColumns    int    `json:"displayColumns"` // Number of columns to display
}

// LoadConfig function reads the configuration from a JSON file.
func LoadConfig(filename string) (Config, error) {
       var config Config
       configFile, err := os.Open(filename)
       if err != nil {
               return config, err
       }
       defer configFile.Close()

       jsonParser := json.NewDecoder(configFile)
       err = jsonParser.Decode(&config)
       return config, err
}

// talkToOllama function sends a request to the Ollama API and returns the response.
func talkToOllama(url string, ollamaReq Request) (*Response, error) {
       // Marshal the request struct into JSON format.
       js, err := json.Marshal(&ollamaReq)
       if err != nil {
               return nil, err // Return an error if marshaling fails.
       }

       // Create a new HTTP client.
       client := http.Client{}

       // Create a new HTTP POST request with the JSON data.
       httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(js))
       if err != nil {
               return nil, err // Return an error if creating the request fails.
       }

       // Send the HTTP request and get the response.
       httpResp, err := client.Do(httpReq)
       if err != nil {
               return nil, err // Return an error if sending the request fails.
       }
       defer httpResp.Body.Close() // Ensure the response body is closed after reading.

       // Decode the JSON response into the Response struct.
       ollamaResp := Response{}
       err = json.NewDecoder(httpResp.Body).Decode(&ollamaResp)
       return &ollamaResp, err // Return the response and any error that occurred.
}

func main() {
       // Define a command-line flag for the configuration file path.
       configPath := flag.String("config", "llama.json", "Path to the configuration file")
       flag.Parse()

       // Load the configuration from the config file.
       config, err := LoadConfig(*configPath)
       if err != nil {
               fmt.Printf("Error loading config: %v\n", err)
               return
       }

       scanner := bufio.NewScanner(os.Stdin)
       fmt.Println("Press 'Enter' to use default values from config or 'y' to enter custom values.")
       if scanner.Scan() {
               input := scanner.Text()
               if strings.ToLower(input) == "y" {
                       // Prompt for custom human name.
                       fmt.Print("Enter custom human name: ")
                       if scanner.Scan() {
                               config.HumanName = scanner.Text()
                       }

                       // Prompt for custom AI name.
                       fmt.Print("Enter custom AI name: ")
                       if scanner.Scan() {
                               config.AIName = scanner.Text()
                       }

                       // Prompt for custom system prompt.
                       fmt.Print("Enter custom system prompt: ")
                       if scanner.Scan() {
                               config.SystemPrompt = scanner.Text()
                       }
               }
       }

       // Define the system prompt message with the human name and AI name.
       systemPromptWithNames := fmt.Sprintf("%s Your name is %s. My name is %s.", config.SystemPrompt, config.HumanName, config.AIName)
       systemMsg := Message{
               Role:    "system",
               Content: systemPromptWithNames,
       }

       // Initialize a slice to store the conversation history.
       var conversationHistory []Message
       conversationHistory = append(conversationHistory, systemMsg)

       // Enter an infinite loop to continuously read user input.
       for {
               fmt.Printf("%s: ", config.HumanName)
               if !scanner.Scan() {
                       break // Exit the loop if reading input fails.
               }
               input := scanner.Text() // Get the user input from the scanner.
               if input == "exit" {
                       break // Exit the loop if the user types "exit".
               }

               // Define the user message and add it to the conversation history.
               userMsg := Message{
                       Role:    "user",
                       Content: input,
               }
               conversationHistory = append(conversationHistory, userMsg)

               // Create a new request with the model name, messages, streaming flag, and context window size.
               req := Request{
                       Model:             config.ModelName,
                       Stream:            false,
                       Messages:          conversationHistory, // Use the conversation history as context.
                       ContextWindowSize: config.ContextWindowSize,
               }

               // Send the request to the Ollama API and get the response.
               resp, err := talkToOllama(config.OllamaURL, req)
               if err != nil {
                       fmt.Printf("Error sending message: %v\n", err) // Print an error message if sending the request fails.
                       continue                                       // Continue to the next iteration of the loop.
               }

               // Split the response content into lines and wrap to fit within the specified column width.
               lines := wrapText(resp.Message.Content, config.DisplayColumns)
               buffer := lines
               startIndex := 0

               // Display the response in chunks based on DisplayRows.
               for startIndex < len(buffer) {
                       endIndex := startIndex + config.DisplayRows
                       if endIndex > len(buffer) {
                               endIndex = len(buffer)
                       }
                       for _, line := range buffer[startIndex:endIndex] {
                               fmt.Println(line)
                       }
                       if endIndex < len(buffer) {
                               fmt.Println("Press Enter to continue...")
                               scanner.Scan() // Wait for Enter key press
                       }
                       startIndex = endIndex
               }

               // Add the AI's response to the conversation history.
               conversationHistory = append(conversationHistory, resp.Message)
       }
}

// wrapText function wraps the text to fit within the specified column width.
func wrapText(text string, width int) []string {
       var lines []string
       var currentLine []rune

       for _, char := range text {
               currentLine = append(currentLine, char)
               if char == '\n' || len(currentLine) == width {
                       lines = append(lines, string(currentLine))
                       currentLine = []rune{}
               }
       }
       if len(currentLine) > 0 {
               lines = append(lines, string(currentLine))
       }

       return lines
}