// Command stress plays many greedy AI-vs-AI games and reports the DAWG move generator's
// speed and memory. It is a benchmark / regression tool for the production generator.
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"runtime"
	"strconv"
	"strings"
	"time"

	"gitea.iliadenisov.ru/developer/scrabble-solver/internal/dict"
	"gitea.iliadenisov.ru/developer/scrabble-solver/rules"
	"gitea.iliadenisov.ru/developer/scrabble-solver/scrabble"
	"gitea.iliadenisov.ru/developer/scrabble-solver/selfplay"
)

func main() {
	games := flag.Int("games", 100, "games to play")
	flag.Parse()

	rs := rules.English()
	if !dict.EnglishAvailable() {
		log.Fatal("English dictionary not available: dawg/en_sowpods.dawg missing")
	}
	f, err := dict.EnglishDAWG()
	if err != nil {
		log.Fatalf("load dawg: %v", err)
	}
	gen := scrabble.NewDAWGGenerator(rs, f)
	structSize := fileSize(dict.DAWGCache())

	runtime.GC()
	var m0 runtime.MemStats
	runtime.ReadMemStats(&m0)
	start := time.Now()

	var turns, plays, movesGen int
	var genTime time.Duration
	var score float64
	for seed := 1; seed <= *games; seed++ {
		res := selfplay.PlayGame(rs, gen, scrabble.Both, int64(seed), nil)
		turns += res.Turns
		plays += res.Plays
		movesGen += res.MovesGenerated
		genTime += res.GenTime
		score += float64(res.Scores[0] + res.Scores[1])
	}
	wall := time.Since(start)
	var m1 runtime.MemStats
	runtime.ReadMemStats(&m1)

	fmt.Printf("DAWG · English SOWPODS · %d games · board %dx%d · greedy self-play\n\n", *games, rs.Rows, rs.Cols)
	fmt.Printf("  structure size        %s\n", humanBytes(structSize))
	fmt.Printf("  turns / plays         %d / %d\n", turns, plays)
	fmt.Printf("  moves generated       %d\n", movesGen)
	fmt.Printf("  generation time       %s  (%.1f µs/turn)\n",
		genTime.Round(time.Millisecond), float64(genTime.Microseconds())/float64(turns))
	fmt.Printf("  moves generated/sec   %.0f\n", float64(movesGen)/genTime.Seconds())
	fmt.Printf("  wall time             %s\n", wall.Round(time.Millisecond))
	fmt.Printf("  heap allocated        %s  (%d GC cycles)\n",
		humanBytes(int64(m1.TotalAlloc-m0.TotalAlloc)), m1.NumGC-m0.NumGC)
	fmt.Printf("  avg final game score  %.1f\n", score/float64(*games))
	fmt.Printf("  peak process RSS      %s\n", humanKB(peakRSS()))
}

func fileSize(p string) int64 {
	if fi, err := os.Stat(p); err == nil {
		return fi.Size()
	}
	return 0
}

func peakRSS() int64 {
	data, err := os.ReadFile("/proc/self/status")
	if err != nil {
		return 0
	}
	for line := range strings.SplitSeq(string(data), "\n") {
		if rest, ok := strings.CutPrefix(line, "VmHWM:"); ok {
			if f := strings.Fields(rest); len(f) > 0 {
				kb, _ := strconv.ParseInt(f[0], 10, 64)
				return kb
			}
		}
	}
	return 0
}

func humanBytes(n int64) string {
	switch {
	case n >= 1<<20:
		return fmt.Sprintf("%.2f MB", float64(n)/(1<<20))
	case n >= 1<<10:
		return fmt.Sprintf("%.1f KB", float64(n)/(1<<10))
	default:
		return fmt.Sprintf("%d B", n)
	}
}

func humanKB(kb int64) string { return humanBytes(kb * 1024) }
