// Пример комплексного performance-теста LineDb.
// Запуск:
//
//	go run ./examples/perf/main.go
//
// Сценарий:
//  1. Вставка 5000 записей, каждая ~1800 символов.
//  2. N выборок ReadByFilter по индексируемому полю, замер среднего времени.
//  3. 100 случайных удалений, замер среднего времени.
//  4. Замеры памяти процесса через runtime.MemStats.
package main

import (
	"fmt"
	"log"
	"math/rand"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"direct-dev.ru/gitea/GiteaAdmin/elowdb-go/pkg/linedb"
)

const (
	recordsCount      = 3000
	payloadSize       = 1024
	readOps           = 2000
	updateOps         = 500
	deleteOps         = 500
	collectionName    = "perf_items"
	baseDBDir         = "./data/perf-benchmark"
	allocSizeEstimate = 2048
)

func main() {
	rand.Seed(time.Now().UnixNano())

	if err := os.MkdirAll(baseDBDir, 0755); err != nil {
		log.Fatalf("mkdir: %v", err)
	}

	fmt.Printf("LineDB perf сравнение: без индекса vs с индексом\n")
	fmt.Printf("records=%d payload~%d readOps=%d updateOps=%d deleteOps=%d allocSize=%d\n\n",
		recordsCount, payloadSize, readOps, updateOps, deleteOps, allocSizeEstimate)

	noIdx := runScenario(false)
	runtime.GC()
	withIdx := runScenario(true)

	fmt.Printf("\n=== Сводка (рядом) ===\n")
	fmt.Printf("%-26s | %-18s | %-18s\n", "Метрика", "Без индекса", "С индексом")
	fmt.Printf("%-26s | %-18v | %-18v\n", "Insert total", noIdx.insertTotal, withIdx.insertTotal)
	fmt.Printf("%-26s | %-18v | %-18v\n", "Index build", noIdx.indexBuildTotal, withIdx.indexBuildTotal)
	fmt.Printf("%-26s | %-18v | %-18v\n", "ReadByFilter avg", noIdx.readAvg, withIdx.readAvg)
	fmt.Printf("%-26s | %-18v | %-18v\n", "Update avg", noIdx.updateAvg, withIdx.updateAvg)
	fmt.Printf("%-26s | %-18v | %-18v\n", "Delete avg", noIdx.deleteAvg, withIdx.deleteAvg)
	fmt.Printf("%-26s | %-18d | %-18d\n", "Final records", noIdx.finalCount, withIdx.finalCount)
	fmt.Printf("%-26s | %-18.2f | %-18.2f\n", "Mem Alloc (MB)", noIdx.memAllocMB, withIdx.memAllocMB)
	fmt.Printf("%-26s | %-18.2f | %-18.2f\n", "Mem Sys (MB)", noIdx.memSysMB, withIdx.memSysMB)
	fmt.Printf("%-26s | %-18d | %-18d\n", "NumGC", noIdx.numGC, withIdx.numGC)
	fmt.Printf("\nData directories:\n  no-index:  %s\n  with-index:%s\n", noIdx.dbDir, withIdx.dbDir)
}

type scenarioResult struct {
	dbDir           string
	insertTotal     time.Duration
	indexBuildTotal time.Duration
	readAvg         time.Duration
	updateAvg       time.Duration
	deleteAvg       time.Duration
	finalCount      int
	memAllocMB      float64
	memSysMB        float64
	numGC           uint32
}

func runScenario(useIndex bool) scenarioResult {
	label := "no-index"
	if useIndex {
		label = "with-index"
	}
	dbDir := filepath.Join(baseDBDir, label)
	_ = os.RemoveAll(dbDir)

	var store linedb.IndexStore
	lineOpts := linedb.LineDbOptions{}
	collOpts := linedb.JSONLFileOptions{
		CollectionName: collectionName,
		AllocSize:      allocSizeEstimate,
	}
	if useIndex {
		// Индексируем поля id и index (по ним идут фильтры в тесте)
		collOpts.IndexedFields = []string{"id", "index"}
		store = linedb.NewInMemoryIndexStore()
		lineOpts.IndexStore = store
	}

	initOptions := &linedb.LineDbInitOptions{
		CacheSize: 1000,
		CacheTTL:  time.Minute,
		DBFolder:  dbDir,
		Collections: []linedb.JSONLFileOptions{
			collOpts,
		},
	}

	db := linedb.NewLineDb(&lineOpts)
	if err := db.Init(true, initOptions); err != nil {
		log.Fatalf("[%s] Init failed: %v", label, err)
	}
	defer db.Close()

	fmt.Printf("=== %s ===\n", label)
	printMem("Before insert")

	fmt.Printf("1) Insert %d records...\n", recordsCount)
	start := time.Now()
	if err := insertRecords(db); err != nil {
		log.Fatalf("[%s] insertRecords failed: %v", label, err)
	}
	insertDur := time.Since(start)
	fmt.Printf("   Total insert time: %v, per record: %v\n", insertDur, insertDur/time.Duration(recordsCount))
	// Индекс строится внутри Write при DoIndexing: true (точечная индексация)

	printMem("After insert")

	all, err := db.Read(collectionName, linedb.LineDbAdapterOptions{})
	if err != nil {
		log.Fatalf("[%s] Read after insert failed: %v", label, err)
	}
	fmt.Printf("   Records in collection: %d\n", len(all))
	ids := collectIDs(all)
	indexVals := collectFieldValues(all, "index")
	if len(ids) == 0 || len(indexVals) == 0 {
		log.Fatalf("[%s] No IDs or index values collected, cannot continue", label)
	}

	fmt.Printf("\n2) Random ReadByFilter of %d ops (field=index)...\n", readOps)
	readAvg := benchmarkReads(db, indexVals, readOps)
	fmt.Printf("   Average ReadByFilter time: %v\n", readAvg)

	fmt.Printf("\n3) Random update of %d records...\n", updateOps)
	avgUpdate := benchmarkUpdates(db, ids, updateOps)
	fmt.Printf("   Average update time: %v\n", avgUpdate)
	printMem("After updates")

	fmt.Printf("\n4) Random delete of %d records...\n", deleteOps)
	avgDelete := benchmarkDeletes(db, ids, deleteOps)
	fmt.Printf("   Average delete time: %v\n", avgDelete)
	printMem("After deletes")

	final, err := db.Read(collectionName, linedb.LineDbAdapterOptions{})
	if err != nil {
		log.Fatalf("[%s] Final read failed: %v", label, err)
	}
	fmt.Printf("\nFinal records in collection: %d\n", len(final))

	mem := memSnapshot()
	return scenarioResult{
		dbDir:           dbDir,
		insertTotal:     insertDur,
		indexBuildTotal: 0, // индекс строится точечно при Write с DoIndexing
		readAvg:         readAvg,
		updateAvg:       avgUpdate,
		deleteAvg:       avgDelete,
		finalCount:      len(final),
		memAllocMB:      mem.allocMB,
		memSysMB:        mem.sysMB,
		numGC:           mem.numGC,
	}
}

func insertRecords(db *linedb.LineDb) error {
	// Сгенерируем базовый payload
	base := strings.Repeat("X", payloadSize)

	batch := make([]any, 0, 100)
	for i := 0; i < recordsCount; i++ {
		rec := map[string]any{
			"id":      i + 1,
			"index":   i,
			"payload": base,
			"created": time.Now().Format(time.RFC3339Nano),
		}
		batch = append(batch, rec)
		if len(batch) >= cap(batch) {
			if err := db.Write(batch, collectionName, linedb.LineDbAdapterOptions{DoIndexing: true}); err != nil {
				return err
			}
			batch = batch[:0]
		}
	}
	if len(batch) > 0 {
		if err := db.Write(batch, collectionName, linedb.LineDbAdapterOptions{DoIndexing: true}); err != nil {
			return err
		}
	}
	return nil
}

func collectIDs(all []any) []any {
	ids := make([]any, 0, len(all))
	for _, r := range all {
		if m, ok := r.(map[string]any); ok {
			if id, ok := m["id"]; ok {
				ids = append(ids, id)
			}
		}
	}
	return ids
}

func collectFieldValues(all []any, field string) []any {
	vals := make([]any, 0, len(all))
	for _, r := range all {
		if m, ok := r.(map[string]any); ok {
			if v, ok := m[field]; ok {
				vals = append(vals, v)
			}
		}
	}
	return vals
}

func benchmarkReads(db *linedb.LineDb, values []any, ops int) time.Duration {
	if len(values) == 0 || ops == 0 {
		return 0
	}
	var total time.Duration
	for i := 0; i < ops; i++ {
		v := values[rand.Intn(len(values))]
		start := time.Now()
		_, err := db.ReadByFilter(map[string]any{"index": v}, collectionName, linedb.LineDbAdapterOptions{})
		dur := time.Since(start)
		if err != nil {
			log.Printf("ReadByFilter error (index=%v): %v", v, err)
			continue
		}
		total += dur
	}
	return total / time.Duration(ops)
}

func benchmarkUpdates(db *linedb.LineDb, ids []any, ops int) time.Duration {
	if len(ids) == 0 {
		return 0
	}
	var total time.Duration
	for i := 0; i < ops; i++ {
		id := ids[rand.Intn(len(ids))]
		update := map[string]any{
			"updatedAt": time.Now().Format(time.RFC3339Nano),
		}
		start := time.Now()
		_, err := db.Update(update, collectionName, map[string]any{"id": id}, linedb.LineDbAdapterOptions{})
		dur := time.Since(start)
		if err != nil {
			log.Printf("Update error (id=%v): %v", id, err)
			continue
		}
		total += dur
	}
	if ops == 0 {
		return 0
	}
	return total / time.Duration(ops)
}

func benchmarkDeletes(db *linedb.LineDb, ids []any, ops int) time.Duration {
	if len(ids) == 0 {
		return 0
	}
	var total time.Duration
	used := make(map[int]struct{})
	for i := 0; i < ops; i++ {
		// чтобы не удалять по одному и тому же id постоянно, постараемся брать разные индексы
		var idx int
		for tries := 0; tries < 10; tries++ {
			idx = rand.Intn(len(ids))
			if _, ok := used[idx]; !ok {
				break
			}
		}
		used[idx] = struct{}{}
		id := ids[idx]

		start := time.Now()
		_, err := db.Delete(map[string]any{"id": id}, collectionName, linedb.LineDbAdapterOptions{})
		dur := time.Since(start)
		if err != nil {
			log.Printf("Delete error (id=%v): %v", id, err)
			continue
		}
		total += dur
	}
	if ops == 0 {
		return 0
	}
	return total / time.Duration(ops)
}

func printMem(label string) {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("\n=== %s ===\n", label)
	fmt.Printf("  Alloc:      %.2f MB\n", float64(m.Alloc)/1024.0/1024.0)
	fmt.Printf("  TotalAlloc: %.2f MB\n", float64(m.TotalAlloc)/1024.0/1024.0)
	fmt.Printf("  Sys:        %.2f MB\n", float64(m.Sys)/1024.0/1024.0)
	fmt.Printf("  NumGC:      %d\n", m.NumGC)
}

type memSnap struct {
	allocMB float64
	sysMB   float64
	numGC   uint32
}

func memSnapshot() memSnap {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	return memSnap{
		allocMB: float64(m.Alloc) / 1024.0 / 1024.0,
		sysMB:   float64(m.Sys) / 1024.0 / 1024.0,
		numGC:   m.NumGC,
	}
}
