// main.go
package main

import (
	"Learn/utils"
	"bufio"
	"encoding/gob"
	"fmt"
	"gorgonia.org/gorgonia"
	tensor "gorgonia.org/tensor"
	"log"
	"os"
	"strconv"
	"strings"
)

const (
	TrainMode         = "t"
	ValidationMode    = "v"
	DialogueMode      = "d"
	ModelFile         = "models/model.gob"
	VocabFile         = "models/vocab.gob"
	TrainingDataDir   = "train"
	ValidationDataDir = "valid" // Создайте эту папку и наполните данными для валидации
	MaxSeqLen         = 1       // Обработка по одному токену
	EmbeddingSize     = 128     // Размер вектора Embedding
)

type Model struct {
	E []float32
	W []float32
	B []float32
}

func main() {
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Введите режим работы (t для обучения, v для валидации, d для диалога): ")
	mode, _ := reader.ReadString('\n')
	mode = strings.TrimSpace(mode)

	switch mode {
	case TrainMode:
		train(reader)
	case ValidationMode:
		validate()
	case DialogueMode:
		dialogue(reader)
	default:
		fmt.Println("Некорректный режим работы.")
	}
}

// PreprocessData преобразует сырые данные в пары (input, target) как индексы
func PreprocessData(data []string, vocab utils.Vocabulary) ([]int, []int) {
	var input []int
	var target []int
	sampleCount := 0

	for _, line := range data {
		tokens := utils.Tokenize(line)
		for j := 0; j < len(tokens)-1; j++ {
			currentToken := tokens[j]
			nextToken := tokens[j+1]

			inputID, exists := vocab.TokenToID[currentToken]
			if !exists {
				inputID = vocab.TokenToID["<UNK>"]
			}

			targetID, exists := vocab.TokenToID[nextToken]
			if !exists {
				targetID = vocab.TokenToID["<UNK>"]
			}

			input = append(input, inputID)
			target = append(target, targetID)
			sampleCount++
		}
	}
	fmt.Printf("Предобработано %d пар (input, target)\n", sampleCount)
	return input, target
}

// readInt читает строку из reader и преобразует её в int
func readInt(reader *bufio.Reader, prompt string) int {
	for {
		fmt.Print(prompt)
		input, err := reader.ReadString('\n')
		if err != nil {
			log.Fatalf("Ошибка при чтении ввода: %v", err)
		}
		input = strings.TrimSpace(input)
		val, err := strconv.Atoi(input)
		if err != nil {
			fmt.Println("Неверный ввод. Пожалуйста, введите целое число.")
			continue
		}
		return val
	}
}

// readString читает строку из reader после вывода prompt
func readString(reader *bufio.Reader, prompt string) string {
	fmt.Print(prompt)
	input, err := reader.ReadString('\n')
	if err != nil {
		log.Fatalf("Ошибка при чтении ввода: %v", err)
	}
	return strings.TrimSpace(input)
}

// CrossEntropy реализует функцию потерь категориальной кросс-энтропии
func CrossEntropy(logits, y *gorgonia.Node, vocabSize int) (*gorgonia.Node, error) {
	// Применяем SoftMax к логитам
	softmax := gorgonia.Must(gorgonia.SoftMax(logits))

	// Преобразуем метки y в one-hot кодировку
	yOneHot, err := gorgonia.OneHot(y, vocabSize, 1.0, 0.0)
	if err != nil {
		return nil, err
	}

	// Вычисляем логарифм softmax
	logSoftmax := gorgonia.Must(gorgonia.Log(softmax))

	// Вычисляем Hadamard Product yOneHot * logSoftmax
	loss := gorgonia.Must(gorgonia.HadamardProd(yOneHot, logSoftmax))

	// Вычисляем среднее значение потерь и добавляем отрицательный знак
	meanLoss, err := gorgonia.Mean(loss)
	if err != nil {
		return nil, err
	}
	finalLoss := gorgonia.Must(gorgonia.Neg(meanLoss))

	return finalLoss, nil
}

// train осуществляет обучение модели
func train(reader *bufio.Reader) {
	// Ввод параметров обучения
	epochs := readInt(reader, "Введите количество эпох: ")
	batchSize := readInt(reader, "Введите размер батча: ")
	maxTrainMessages := readInt(reader, "Введите максимальное количество сообщений для обучения (0 для всех): ")
	optimizerChoice := readString(reader, "Выберите оптимизатор (adam/sgd): ")

	// Загрузка данных
	fmt.Println("Загрузка данных для обучения...")
	data := utils.LoadData(TrainingDataDir, maxTrainMessages)
	totalLoaded := len(data)
	if maxTrainMessages > 0 && totalLoaded > maxTrainMessages {
		data = data[:maxTrainMessages]
		fmt.Printf("Используется %d из %d доступных сообщений.\n", maxTrainMessages, totalLoaded)
	} else {
		fmt.Printf("Используется все %d доступных сообщений.\n", totalLoaded)
	}

	// Построение словаря с фильтрацией редких токенов
	fmt.Println("Построение словаря...")
	vocab := utils.BuildVocabulary(data, 5) // Фильтрация токенов, встречающихся менее 5 раз
	utils.SaveVocabulary(vocab, VocabFile)
	vocabSize := len(vocab.TokenToID)
	fmt.Printf("Размер словаря после фильтрации: %d\n", vocabSize)

	// Предобработка данных
	fmt.Println("Предобработка данных...")
	input, target := PreprocessData(data, vocab)
	if len(input) == 0 || len(target) == 0 {
		log.Fatalf("Нет данных для обучения после предобработки.")
	}

	// Инициализация графа Gorgonia
	g := gorgonia.NewGraph()

	// Входные данные как индексы
	x := gorgonia.NewVector(g,
		tensor.Int,
		gorgonia.WithShape(batchSize),
		gorgonia.WithName("x"))

	// Целевые данные
	y := gorgonia.NewVector(g,
		tensor.Int,
		gorgonia.WithShape(batchSize),
		gorgonia.WithName("y"))

	// Embedding слой
	E := gorgonia.NewMatrix(g,
		tensor.Float32,
		gorgonia.WithShape(vocabSize, EmbeddingSize),
		gorgonia.WithName("E"),
		gorgonia.WithInit(gorgonia.GlorotN(1.0)))

	// Получение embedding для входов
	embedded := gorgonia.Must(gorgonia.Lookup(E, x))

	// Линейный слой
	W := gorgonia.NewMatrix(g,
		tensor.Float32,
		gorgonia.WithShape(EmbeddingSize, vocabSize),
		gorgonia.WithName("W"),
		gorgonia.WithInit(gorgonia.GlorotN(1.0)))

	b := gorgonia.NewVector(g,
		tensor.Float32,
		gorgonia.WithShape(vocabSize),
		gorgonia.WithName("b"),
		gorgonia.WithInit(gorgonia.Zeroes()))

	// Прогнозирование: logits = E[x] * W + b
	logits := gorgonia.Must(gorgonia.Mul(embedded, W))
	logits = gorgonia.Must(gorgonia.Add(logits, b))

	// Функция потерь: Categorical Cross-Entropy
	loss, err := CrossEntropy(logits, y, vocabSize)
	if err != nil {
		log.Fatalf("Ошибка вычисления CrossEntropy: %v", err)
	}

	// Создание машины ленточных вычислений (Tape Machine)
	machine := gorgonia.NewTapeMachine(g, gorgonia.BindDualValues(W, b, E))

	// Определение оптимизатора
	var solver gorgonia.Solver
	switch strings.ToLower(optimizerChoice) {
	case "adam":
		solver = gorgonia.NewAdamSolver(gorgonia.WithLearnRate(0.001))
	case "sgd":
		solver = gorgonia.NewVanillaSolver(gorgonia.WithLearnRate(0.01))
	default:
		log.Fatalf("Неизвестный оптимизатор: %s", optimizerChoice)
	}

	// Цикл обучения
	fmt.Println("Начало обучения...")
	for epoch := 1; epoch <= epochs; epoch++ {
		totalLoss := float32(0)
		numBatches := 0
		for i := 0; i < len(input); i += batchSize {
			end := i + batchSize
			if end > len(input) {
				end = len(input)
			}
			currentBatchSize := end - i

			// Если последний батч меньше batchSize, пропускаем его
			if currentBatchSize != batchSize {
				continue
			}

			batchInput := input[i:end]
			batchTarget := target[i:end]

			// Создание тензоров для текущего батча
			xVal := tensor.New(tensor.WithShape(batchSize), tensor.Of(tensor.Int), tensor.WithBacking(batchInput))
			yVal := tensor.New(tensor.WithShape(batchSize), tensor.Of(tensor.Int), tensor.WithBacking(batchTarget))

			// Ввод значений в граф
			if err := gorgonia.Let(x, xVal); err != nil {
				log.Fatalf("Ошибка установки x: %v", err)
			}
			if err := gorgonia.Let(y, yVal); err != nil {
				log.Fatalf("Ошибка установки y: %v", err)
			}

			// Выполнение форвардного и бэкового прохода
			if err := machine.RunAll(); err != nil {
				log.Fatalf("Ошибка выполнения графа: %v", err)
			}

			// Получение значения потерь
			lossVal, ok := loss.Value().Data().([]float32)
			if !ok {
				log.Fatalf("Не удалось преобразовать значение потерь")
			}
			currentLoss := lossVal[0]
			totalLoss += currentLoss
			numBatches++

			// Обновление параметров
			if err := solver.Step(gorgonia.NodesToValueGrads([]*gorgonia.Node{W, b, E})); err != nil {
				log.Fatalf("Ошибка обновления параметров: %v", err)
			}

			// Сброс машины для следующего шага
			machine.Reset()

			// Прогресс
			epochProgress := float64(end) / float64(len(input)) * 100
			if epochProgress > 100 {
				epochProgress = 100
			}
			overallProgress := float64(epoch) / float64(epochs) * 100
			if overallProgress > 100 {
				overallProgress = 100
			}
			fmt.Printf("Epoch %d: Processed batch from %d to %d (%.2f%% complete)(%.2f%% all complete)\n",
				epoch, i, end, epochProgress, overallProgress)
		}
		avgLoss := totalLoss / float32(numBatches)
		fmt.Printf("Epoch %d завершен. Средняя потеря: %.4f\n", epoch, avgLoss)
	}

	// Сохранение модели
	fmt.Println("Сохранение модели...")
	// Извлечение значений весов
	EData, err := extractMatrixValues(E)
	if err != nil {
		log.Fatalf("Ошибка извлечения E: %v", err)
	}
	WData, err := extractMatrixValues(W)
	if err != nil {
		log.Fatalf("Ошибка извлечения W: %v", err)
	}
	BData, err := extractVectorValues(b)
	if err != nil {
		log.Fatalf("Ошибка извлечения B: %v", err)
	}

	modelStruct := Model{
		E: EData,
		W: WData,
		B: BData,
	}
	saveModel(&modelStruct)
	fmt.Println("Обучение завершено и модель сохранена.")
}

// extractMatrixValues извлекает значения из матричного узла
func extractMatrixValues(node *gorgonia.Node) ([]float32, error) {
	value := node.Value()
	if value == nil {
		return nil, fmt.Errorf("Узел %s не имеет значения", node.Name())
	}
	data, ok := value.Data().([]float32)
	if !ok {
		return nil, fmt.Errorf("Невозможно преобразовать данные узла %s", node.Name())
	}
	return data, nil
}

// extractVectorValues извлекает значения из векторного узла
func extractVectorValues(node *gorgonia.Node) ([]float32, error) {
	value := node.Value()
	if value == nil {
		return nil, fmt.Errorf("Узел %s не имеет значения", node.Name())
	}
	data, ok := value.Data().([]float32)
	if !ok {
		return nil, fmt.Errorf("Невозможно преобразовать данные узла %s", node.Name())
	}
	return data, nil
}

// saveModel сохраняет модель в файл
func saveModel(m *Model) {
	// Проверка и создание директории models, если она не существует
	if _, err := os.Stat("models"); os.IsNotExist(err) {
		err := os.MkdirAll("models", os.ModePerm)
		if err != nil {
			log.Fatalf("Не удалось создать директорию models: %v", err)
		}
	}

	file, err := os.Create(ModelFile)
	if err != nil {
		log.Fatalf("Не удалось создать файл модели: %v", err)
	}
	defer file.Close()

	encoder := gob.NewEncoder(file)
	err = encoder.Encode(m)
	if err != nil {
		log.Fatalf("Не удалось закодировать модель: %v", err)
	}
}

// loadModel загружает модель из файла
func loadModel() *Model {
	file, err := os.Open(ModelFile)
	if err != nil {
		log.Fatalf("Не удалось открыть файл модели: %v", err)
	}
	defer file.Close()

	var m Model
	decoder := gob.NewDecoder(file)
	err = decoder.Decode(&m)
	if err != nil {
		log.Fatalf("Не удалось декодировать модель: %v", err)
	}
	return &m
}

// validate выполняет валидацию модели
func validate() {
	// Загрузка модели
	model := loadModel()

	// Загрузка словаря
	vocab := utils.LoadVocabulary(VocabFile)
	vocabSize := len(vocab.TokenToID)
	fmt.Printf("Размер словаря: %d\n", vocabSize)

	// Загрузка валидационных данных
	fmt.Println("Загрузка данных для валидации...")
	data := utils.LoadData(ValidationDataDir, 10000) // Измените лимит при необходимости
	fmt.Printf("Загружено %d валидационных записей\n", len(data))

	// Предобработка данных
	input, target := PreprocessData(data, vocab)
	if len(input) == 0 || len(target) == 0 {
		log.Fatalf("Нет данных для валидации после предобработки.")
	}

	// Инициализация графа
	g := gorgonia.NewGraph()

	// Входные данные как индексы
	x := gorgonia.NewVector(g,
		tensor.Int,
		gorgonia.WithShape(len(input)),
		gorgonia.WithName("x"))

	// Целевые данные
	y := gorgonia.NewVector(g,
		tensor.Int,
		gorgonia.WithShape(len(target)),
		gorgonia.WithName("y"))

	// Embedding слой
	E := gorgonia.NewMatrix(g,
		tensor.Float32,
		gorgonia.WithShape(vocabSize, EmbeddingSize),
		gorgonia.WithName("E"))

	// Линейный слой
	W := gorgonia.NewMatrix(g,
		tensor.Float32,
		gorgonia.WithShape(EmbeddingSize, vocabSize),
		gorgonia.WithName("W"))

	b := gorgonia.NewVector(g,
		tensor.Float32,
		gorgonia.WithShape(vocabSize),
		gorgonia.WithName("b"))

	// Присвоение загруженных значений
	EVal := tensor.New(tensor.WithShape(vocabSize, EmbeddingSize), tensor.Of(tensor.Float32), tensor.WithBacking(model.E))
	WVal := tensor.New(tensor.WithShape(EmbeddingSize, vocabSize), tensor.Of(tensor.Float32), tensor.WithBacking(model.W))
	bVal := tensor.New(tensor.WithShape(vocabSize), tensor.Of(tensor.Float32), tensor.WithBacking(model.B))

	if err := gorgonia.Let(E, EVal); err != nil {
		log.Fatalf("Ошибка при установке E: %v", err)
	}
	if err := gorgonia.Let(W, WVal); err != nil {
		log.Fatalf("Ошибка при установке W: %v", err)
	}
	if err := gorgonia.Let(b, bVal); err != nil {
		log.Fatalf("Ошибка при установке b: %v", err)
	}

	// Получение embedding для входов
	embedded := gorgonia.Must(gorgonia.Lookup(E, x))

	// Прогнозирование: logits = E[x] * W + b
	logits := gorgonia.Must(gorgonia.Mul(embedded, W))
	logits = gorgonia.Must(gorgonia.Add(logits, b))

	// Функция потерь: Categorical Cross-Entropy
	loss, err := CrossEntropy(logits, y, vocabSize)
	if err != nil {
		log.Fatalf("Ошибка вычисления CrossEntropy: %v", err)
	}

	// Создание машины ленточных вычислений (Tape Machine)
	machine := gorgonia.NewTapeMachine(g)

	// Создание тензоров для валидации
	xVal := tensor.New(tensor.WithShape(len(input)), tensor.Of(tensor.Int), tensor.WithBacking(input))
	yVal := tensor.New(tensor.WithShape(len(target)), tensor.Of(tensor.Int), tensor.WithBacking(target))

	// Ввод значений в граф
	if err := gorgonia.Let(x, xVal); err != nil {
		log.Fatalf("Ошибка установки x: %v", err)
	}
	if err := gorgonia.Let(y, yVal); err != nil {
		log.Fatalf("Ошибка установки y: %v", err)
	}

	// Выполнение графа
	fmt.Println("Выполнение валидации...")
	if err := machine.RunAll(); err != nil {
		log.Fatalf("Ошибка выполнения графа: %v", err)
	}

	// Получение значения потерь
	lossVal, ok := loss.Value().Data().([]float32)
	if !ok {
		log.Fatalf("Не удалось преобразовать значение потерь")
	}
	fmt.Printf("Validation Loss: %.4f\n", lossVal[0])

	// Сброс машины
	machine.Reset()
}

// dialogue осуществляет диалог с ИИ
func dialogue(reader *bufio.Reader) {
	// Загрузка модели
	model := loadModel()

	// Загрузка словаря
	vocab := utils.LoadVocabulary(VocabFile)
	vocabSize := len(vocab.TokenToID)
	idToToken := make(map[int]string)
	for id, token := range vocab.IDToToken {
		idToToken[id] = token
	}

	// Инициализация графа
	g := gorgonia.NewGraph()

	// Входные данные как индекс
	x := gorgonia.NewVector(g,
		tensor.Int,
		gorgonia.WithShape(1),
		gorgonia.WithName("x"))

	// Embedding слой
	E := gorgonia.NewMatrix(g,
		tensor.Float32,
		gorgonia.WithShape(vocabSize, EmbeddingSize),
		gorgonia.WithName("E"))

	// Линейный слой
	W := gorgonia.NewMatrix(g,
		tensor.Float32,
		gorgonia.WithShape(EmbeddingSize, vocabSize),
		gorgonia.WithName("W"))

	b := gorgonia.NewVector(g,
		tensor.Float32,
		gorgonia.WithShape(vocabSize),
		gorgonia.WithName("b"))

	// Присвоение загруженных значений
	EVal := tensor.New(tensor.WithShape(vocabSize, EmbeddingSize), tensor.Of(tensor.Float32), tensor.WithBacking(model.E))
	WVal := tensor.New(tensor.WithShape(EmbeddingSize, vocabSize), tensor.Of(tensor.Float32), tensor.WithBacking(model.W))
	bVal := tensor.New(tensor.WithShape(vocabSize), tensor.Of(tensor.Float32), tensor.WithBacking(model.B))

	if err := gorgonia.Let(E, EVal); err != nil {
		log.Fatalf("Ошибка при установке E: %v", err)
	}
	if err := gorgonia.Let(W, WVal); err != nil {
		log.Fatalf("Ошибка при установке W: %v", err)
	}
	if err := gorgonia.Let(b, bVal); err != nil {
		log.Fatalf("Ошибка при установке b: %v", err)
	}

	// Получение embedding для входа
	embedded := gorgonia.Must(gorgonia.Lookup(E, x))

	// Прогнозирование: logits = E[x] * W + b
	logits := gorgonia.Must(gorgonia.Mul(embedded, W))
	logits = gorgonia.Must(gorgonia.Add(logits, b))

	// Softmax для получения вероятностей
	probs := gorgonia.Must(gorgonia.SoftMax(logits))

	// Создание машины ленточных вычислений (Tape Machine)
	machine := gorgonia.NewTapeMachine(g)

	fmt.Println("Начните диалог с ИИ. Введите 'exit' для выхода.")

	for {
		fmt.Print("Вы: ")
		inputStr, err := reader.ReadString('\n')
		if err != nil {
			log.Fatalf("Ошибка при чтении ввода: %v", err)
		}
		inputStr = strings.TrimSpace(inputStr)
		if strings.ToLower(inputStr) == "exit" {
			break
		}

		// Токенизация и преобразование в ID
		tokens := utils.Tokenize(inputStr)
		if len(tokens) == 0 {
			fmt.Println("ИИ: <Нет ответа>")
			continue
		}

		// Берём последний токен для предсказания следующего
		lastToken := tokens[len(tokens)-1]
		inputID, exists := vocab.TokenToID[lastToken]
		if !exists {
			inputID = vocab.TokenToID["<UNK>"]
		}

		// Создание тензора
		xVal := tensor.New(tensor.WithShape(1), tensor.Of(tensor.Int), tensor.WithBacking([]int{inputID}))

		// Ввод значения в граф
		if err := gorgonia.Let(x, xVal); err != nil {
			log.Fatalf("Ошибка установки x: %v", err)
		}

		// Выполнение графа
		if err := machine.RunAll(); err != nil {
			log.Fatalf("Ошибка выполнения графа: %v", err)
		}

		// Получение предсказания
		probsVal, ok := probs.Value().Data().([]float32)
		if !ok {
			log.Fatalf("Не удалось преобразовать предсказание")
		}

		// Поиск индекса с наибольшим значением
		maxIdx := 0
		maxProb := probsVal[0]
		for i, prob := range probsVal {
			if prob > maxProb {
				maxProb = prob
				maxIdx = i
			}
		}

		// Получение слова по ID
		response := "<UNK>"
		if token, exists := idToToken[maxIdx]; exists {
			response = token
		}

		fmt.Printf("ИИ: %s\n", response)

		// Сброс машины для следующего шага
		machine.Reset()
	}
}


Unresolved reference 'Lookup' не существует в gorgonia испрправь  Too many arguments in call to 'utils.BuildVocabulary'