package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"math"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/denisovdennis/autohero/internal/game"
	"github.com/denisovdennis/autohero/internal/model"
	"github.com/denisovdennis/autohero/internal/storage"
	"github.com/denisovdennis/autohero/internal/tuning"
)

func main() {
	var (
		dsnFlag = flag.String("dsn", "", "Postgres DSN (default: DATABASE_URL env)")

		iterPerLevel = flag.Int("iterations-per-level", 40, "Monte Carlo iterations per hero level (higher = slower, smoother)")
		seed         = flag.Int64("seed", 20260331, "RNG seed base")
		restSec      = flag.Float64("rest-sec", 400, "seconds of rest after each fight")
		accountLoss  = flag.Bool("account-losses", false, "if true, XP rate uses all fights; else wins-only expectation")

		gearStr = flag.String("gear", "median", "median|rolled reference gear")

		typesFilter = flag.String("types", "", "comma-separated enemy types to include in SQL output (empty = all)")

		maxHeroLevel = flag.Int("max-level", 49, "max hero level step simulated (L→L+1 up to this L); use lower if high levels never win")

		targetW1 = flag.Float64("target-weeks-1-10", 1, "target wall-clock weeks for level-ups 1→10")
		targetW2 = flag.Float64("target-weeks-10-20", 3, "target weeks for 10→20")
		targetW3 = flag.Float64("target-weeks-20-30", 6, "target weeks for 20→30")
		targetW4 = flag.Float64("target-weeks-30-40", 10, "target weeks for 30→40")
		targetW5 = flag.Float64("target-weeks-40-50", 20, "target weeks for 40→50")
		weekDur  = flag.Duration("week", 7*24*time.Hour, "duration of one target week (default 7d)")

		runReport = flag.Bool("report", true, "print band durations vs targets (baseline DB xp_reward)")
		optTypes  = flag.Bool("optimize-types", false, "optimize per-row xp_reward (each enemies.type); no global multiplier")
		optBands  = flag.Bool("optimize-bands", false, "optimize five content-tier multipliers (TemplateProgressionBand), not per-type")
		eliteMul  = flag.Float64("elite-scale", 1, "multiplier on is_elite rows (applied with per-type / per-band scales)")
		optIters  = flag.Int("optimize-iters", 100, "coordinate-descent passes per optimize round (-optimize-types / -optimize-bands)")
		optRounds = flag.Int("optimize-rounds", 8, "for -optimize-types: repeat from last scaled values until -target-max-rel-err or max rounds")
		enforceTierXP = flag.Bool("enforce-tier-xp", true, "for -optimize-types: xp_reward non-decreasing with min/max level tier (strict up when tier rises)")
		targetMaxRelErr = flag.Float64("target-max-rel-err", 0.18, "stop optimize rounds when max relative error on bands+total is below this (e.g. 0.18 = 18%)")

		printSQL    = flag.Bool("sql", true, "print suggested UPDATE enemies SET xp_reward=...")
		sqlAll      = flag.Bool("sql-all", true, "emit UPDATE for every enemy row (not only changed)")
	)
	flag.Parse()

	dsn := strings.TrimSpace(*dsnFlag)
	if dsn == "" {
		dsn = os.Getenv("DATABASE_URL")
	}
	if dsn == "" {
		log.Fatal("DATABASE_URL or -dsn is required")
	}

	if *optTypes && *optBands {
		log.Fatal("use only one of -optimize-types or -optimize-bands")
	}

	ctx := context.Background()
	pool, err := pgxpool.New(ctx, dsn)
	if err != nil {
		log.Fatalf("open db: %v", err)
	}
	defer pool.Close()

	rcStore := storage.NewRuntimeConfigStore(pool)
	if err := tuning.ReloadNow(ctx, nil, rcStore); err != nil {
		log.Fatalf("load runtime config: %v", err)
	}

	cs := storage.NewContentStore(pool)
	baseTemplates, err := cs.LoadEnemyTemplates(ctx)
	if err != nil {
		log.Fatalf("load enemies: %v", err)
	}
	if len(baseTemplates) == 0 {
		log.Fatal("no enemy templates in database")
	}

	gear := game.ReferenceGearMedian
	if strings.EqualFold(strings.TrimSpace(*gearStr), "rolled") {
		gear = game.ReferenceGearRolled
	}

	maxL := *maxHeroLevel
	if maxL < 1 {
		maxL = 1
	}
	if maxL > 49 {
		maxL = 49
	}

	params := game.ProgressionSimParams{
		IterationsPerLevel:      *iterPerLevel,
		Seed:                  *seed,
		RestAfterCombat:       time.Duration(*restSec * float64(time.Second)),
		Gear:                  gear,
		AccountLosses:         *accountLoss,
		MinHeroLevel:          1,
		MaxHeroLevelInclusive: maxL,
	}

	fullTargets := [5]time.Duration{
		time.Duration(*targetW1 * float64(*weekDur)),
		time.Duration(*targetW2 * float64(*weekDur)),
		time.Duration(*targetW3 * float64(*weekDur)),
		time.Duration(*targetW4 * float64(*weekDur)),
		time.Duration(*targetW5 * float64(*weekDur)),
	}
	targets := game.ProratedBandTargets(maxL, fullTargets)
	totalTarget := game.SumBandTargets(targets)
	fmt.Printf("# xpprogsim: maxHeroLevel=%d | prorated target sum=%s (full 1→50 would be %s)\n",
		maxL, totalTarget.Round(time.Second), (fullTargets[0]+fullTargets[1]+fullTargets[2]+fullTargets[3]+fullTargets[4]).Round(time.Second))

	typeFilter := parseTypesFilter(*typesFilter)

	if *runReport {
		res, err := game.SimulateProgressionBands(params, game.CloneEnemyTemplates(game.EnemyTemplatesFromSlice(baseTemplates)))
		if err != nil {
			log.Fatalf("simulate: %v", err)
		}
		printReport("baseline (DB xp_reward)", res, targets, totalTarget)
	}

	if *optTypes {
		baseRound := game.CloneEnemyTemplates(game.EnemyTemplatesFromSlice(baseTemplates))
		iters := *optIters
		var lastScaled map[string]model.Enemy
		var lastRes game.ProgressionBandResult
		var lastSq float64
		var lastPerType map[string]float64
		for round := 0; round < *optRounds; round++ {
			var scaled map[string]model.Enemy
			lastPerType, scaled, lastRes, lastSq = game.OptimizePerTypeScales(baseRound, params, targets, *eliteMul, iters, *enforceTierXP)
			lastScaled = scaled
			maxRel := game.MaxRelativeErrorVsTargets(lastRes.BandDurations, targets, lastRes.Total, totalTarget)
			fmt.Printf("\n# optimize-types round %d/%d: iters=%d sqErr=%.6f maxRelErr=%.2f%% enforceTier=%v\n",
				round+1, *optRounds, iters, lastSq, 100*maxRel, *enforceTierXP)
			if !math.IsInf(lastRes.TotalSec, 1) && maxRel <= *targetMaxRelErr {
				fmt.Printf("# stopped: max relative error <= %.0f%%\n", 100*(*targetMaxRelErr))
				break
			}
			baseRound = game.CloneEnemyTemplates(scaled)
			iters += *optIters / 3
			if iters > 400 {
				iters = 400
			}
		}
		if lastPerType != nil {
			printPerTypeMultipliers(lastPerType)
		}
		printReport("after per-type optimization (final)", lastRes, targets, totalTarget)
		if *printSQL && lastScaled != nil {
			if *sqlAll {
				printSQLAll(lastScaled, typeFilter)
			} else {
				printSQLDiff(game.EnemyTemplatesFromSlice(baseTemplates), lastScaled, typeFilter)
			}
		}
	}

	if *optBands {
		scales, res, sqErr := game.OptimizeBandScales(game.EnemyTemplatesFromSlice(baseTemplates), params, targets, 1, *eliteMul, *optIters)
		fmt.Printf("\n# optimize-bands: per-band scales [%v] sqErr=%.6f\n", formatFloats(scales[:]), sqErr)
		printReport("after band-tier optimization", res, targets, totalTarget)
		spec := game.XPRewardScaleSpec{Global: 1, Elite: *eliteMul, PerBand: scales}
		scaled := game.ApplyXPRewardScaleSpec(game.EnemyTemplatesFromSlice(baseTemplates), spec)
		if *printSQL {
			printSQLDiff(game.EnemyTemplatesFromSlice(baseTemplates), scaled, typeFilter)
		}
	}

	if !*runReport && !*optTypes && !*optBands {
		log.Fatal("nothing to do: enable -report and/or -optimize-types and/or -optimize-bands")
	}
}

func parseTypesFilter(s string) map[string]bool {
	s = strings.TrimSpace(s)
	if s == "" {
		return nil
	}
	out := make(map[string]bool)
	for _, p := range strings.Split(s, ",") {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		out[p] = true
	}
	return out
}

func printPerTypeMultipliers(m map[string]float64) {
	keys := make([]string, 0, len(m))
	for t := range m {
		keys = append(keys, t)
	}
	sort.Strings(keys)
	fmt.Print("# multipliers vs DB xp_reward: ")
	for i, ks := range keys {
		if i > 0 {
			fmt.Print(" ")
		}
		fmt.Printf("%s=%.4f", ks, m[ks])
	}
	fmt.Println()
}

func printReport(title string, res game.ProgressionBandResult, targets [5]time.Duration, totalTarget time.Duration) {
	fmt.Printf("\n## %s\n", title)
	labels := []string{"1→10", "10→20", "20→30", "30→40", "40→50"}
	errs := game.BandErrors(res.BandDurations, targets)
	for i := 0; i < 5; i++ {
		fmt.Printf("  band %s: sim=%s target=%s rel_err=%.2f%%\n",
			labels[i], res.BandDurations[i].Round(time.Second), targets[i].Round(time.Second), 100*errs[i])
	}
	fmt.Printf("  TOTAL: sim=%s target=%s rel_err=%.2f%%\n",
		res.Total.Round(time.Second), totalTarget.Round(time.Second),
		100*(float64(res.Total)/float64(totalTarget)-1))
	if math.IsInf(res.TotalSec, 1) {
		fmt.Println("  NOTE: total time hit +Inf (some hero levels had zero XP rate — raise balance, use -account-losses, or -max-level).")
	} else {
		minWR := 1.0
		for _, w := range res.WinRates {
			if w < minWR {
				minWR = w
			}
		}
		if minWR < 0.05 && len(res.WinRates) > 0 {
			fmt.Printf("  NOTE: min MC win rate across levels=%.0f%% — progression may be unrealistic at high levels.\n", 100*minWR)
		}
	}
}

func printSQLDiff(base, scaled map[string]model.Enemy, filter map[string]bool) {
	fmt.Println()
	for typ, b := range base {
		if filter != nil && !filter[typ] {
			continue
		}
		s, ok := scaled[typ]
		if !ok {
			continue
		}
		if s.XPReward == b.XPReward {
			continue
		}
		t := strings.ReplaceAll(typ, "'", "''")
		fmt.Printf("UPDATE public.enemies SET xp_reward = %d WHERE type = '%s';\n", s.XPReward, t)
	}
}

// printSQLAll emits UPDATE for every row in scaled (optionally filtered by type), tier order.
func printSQLAll(scaled map[string]model.Enemy, filter map[string]bool) {
	fmt.Println()
	order := game.SortEnemyTypesByLevelTier(scaled)
	for _, typ := range order {
		if filter != nil && !filter[typ] {
			continue
		}
		s, ok := scaled[typ]
		if !ok {
			continue
		}
		t := strings.ReplaceAll(typ, "'", "''")
		fmt.Printf("UPDATE public.enemies SET xp_reward = %d WHERE type = '%s';\n", s.XPReward, t)
	}
}

func formatFloats(v []float64) string {
	var b strings.Builder
	b.WriteByte('[')
	for i, x := range v {
		if i > 0 {
			b.WriteString(", ")
		}
		fmt.Fprintf(&b, "%.4f", x)
	}
	b.WriteByte(']')
	return b.String()
}
