// Package sandbox handles the 7 Days to Die V3.0 "SandboxCode" — the single
// encoded serverconfig.xml property that carries all 150 in-game Sandbox Options
// introduced in V3.0 "Dead Hot Summer".
//
// V3.0 collapses ~29 legacy gameplay serverconfig properties (GameDifficulty,
// BloodMoonFrequency, LootAbundance, …) into one deterministic, OFFLINE-encoded
// string. The encode/decode algorithm ships in the game binary; this package is
// a faithful Go port of it, cracked from the decompiled 3.0 Assembly-CSharp
// (SandboxOptions.SandboxOptionManager + SandboxOptionPreset, buildid 23705258).
//
// ## The encoding (verified against the game's own default code)
//
//	code[0]            = version char ('A', SandboxOptionManager.currentVersion)
//	then 3-char triples, one per option that differs from its default:
//	  triple[0..1]     = the SandboxOptions enum int, base-26: c0*26 + c1 (A=0)
//	  triple[2]        = the selected value INDEX in that option's value-set (A=0)
//
// Decode resets every option to its default, then applies each triple. Encode is
// the inverse: emit a triple for every option whose chosen index ≠ default index
// (options omitted from the code decode to default — identical result, shorter
// code, matching how the game emits difficulty presets like AAAJABJACJADJARFBNC).
//
// Round-trip + the game's example codes are checked in sandbox_test.go.
package sandbox

import (
	"errors"
	"fmt"
	"math"
	"sort"
	"strconv"
	"strings"
)

// codeVersion is SandboxOptionManager.currentVersion. A code must start with it.
const codeVersion = 'A'

// ErrCodecUnavailable is retained for API compatibility with the day-one
// passthrough scaffold. The real codec no longer returns it.
var ErrCodecUnavailable = errors.New("sandbox: per-option SandboxCode codec unavailable")

// optByEnum / optByName index the options table for O(1) lookup. defaultIdx
// holds each option's default value's index in its value-set.
var (
	optByEnum  = map[int]*option{}
	optByName  = map[string]*option{}
	defaultIdx = map[string]int{}
)

func init() {
	for i := range options {
		o := &options[i]
		e, ok := enumByName[o.Name]
		if !ok {
			panic("sandbox: option " + o.Name + " has no SandboxOptions enum entry")
		}
		o.enum = e
		vs, ok := valueSets[o.ValueSet]
		if !ok {
			panic("sandbox: option " + o.Name + " references unknown value-set " + o.ValueSet)
		}
		di, ok := indexOfDefault(*o, vs)
		if !ok {
			panic(fmt.Sprintf("sandbox: option %s default not present in value-set %s", o.Name, o.ValueSet))
		}
		o.set = vs
		defaultIdx[o.Name] = di
		optByEnum[e] = o
		optByName[o.Name] = o
	}
}

// indexOfDefault finds the index of an option's default value in its value-set.
func indexOfDefault(o option, vs valueSet) (int, bool) {
	switch o.Type {
	case optFloat:
		for i, f := range vs.Floats {
			if floatEq(f, o.DefaultFloat) {
				return i, true
			}
		}
	case optInt:
		for i, n := range vs.Ints {
			if n == o.DefaultInt {
				return i, true
			}
		}
	case optBool:
		for i, b := range vs.Bools {
			if b == o.DefaultBool {
				return i, true
			}
		}
	}
	return 0, false
}

func floatEq(a, b float64) bool { return math.Abs(a-b) < 1e-6 }

// Codec converts between a SandboxCode string and a per-option value map.
// The map is keyed by the SandboxOptions member name (e.g. "BloodMoonFrequency")
// with string values (the human value: "7", "1.5", "true"). Encode emits a code
// for the options whose value differs from default; Decode returns the FULL set
// of 150 options at their effective values (defaults for any omitted from code).
type Codec interface {
	Version() string
	Encode(options map[string]string) (string, error)
	Decode(code string) (map[string]string, error)
}

// V3Codec is the real 3.0 SandboxCode codec.
type V3Codec struct{}

func (V3Codec) Version() string { return string(codeVersion) }

// Encode builds a SandboxCode from a name→value map. Unknown keys are ignored;
// a value that doesn't match any entry in its option's value-set is an error
// (prevents silently writing an out-of-range setting). Options absent from the
// map, or equal to their default, are omitted from the code (decode to default).
// Triples are emitted in ascending enum order for a stable, diffable code.
func (V3Codec) Encode(vals map[string]string) (string, error) {
	type triple struct {
		enum, idx int
	}
	var triples []triple
	for name, raw := range vals {
		o, ok := optByName[name]
		if !ok {
			continue // unknown option key — ignore (forward-compat)
		}
		idx, err := o.indexOfValue(raw)
		if err != nil {
			return "", fmt.Errorf("sandbox: option %s: %w", name, err)
		}
		if idx == defaultIdx[name] {
			continue // equals default — omit (decodes to same value)
		}
		triples = append(triples, triple{o.enum, idx})
	}
	sort.Slice(triples, func(i, j int) bool { return triples[i].enum < triples[j].enum })

	var b strings.Builder
	b.WriteByte(codeVersion)
	for _, t := range triples {
		s, err := indexToAlpha2(t.enum)
		if err != nil {
			return "", err
		}
		c, err := indexToAlpha(t.idx)
		if err != nil {
			return "", err
		}
		b.WriteString(s)
		b.WriteByte(c)
	}
	return b.String(), nil
}

// Decode parses a SandboxCode into the FULL effective option set (all 150 at
// their resolved values: code-specified or default). An empty code returns all
// defaults. A code with a wrong version char, bad length, or unknown enum is an
// error (unknown enums could mean a version drift we shouldn't silently accept).
func (V3Codec) Decode(code string) (map[string]string, error) {
	out := DefaultsMap()
	if code == "" {
		return out, nil
	}
	if rune(code[0]) != codeVersion {
		return nil, fmt.Errorf("sandbox: code version %q unsupported (want %q)", string(code[0]), string(codeVersion))
	}
	body := code[1:]
	if len(body)%3 != 0 {
		return nil, fmt.Errorf("sandbox: code body length %d is not a multiple of 3", len(body))
	}
	for i := 0; i < len(body); i += 3 {
		enum, err := alpha2ToIndex(body[i : i+2])
		if err != nil {
			return nil, err
		}
		idx, err := alphaToIndex(body[i+2])
		if err != nil {
			return nil, err
		}
		o, ok := optByEnum[enum]
		if !ok {
			return nil, fmt.Errorf("sandbox: code references unknown option enum %d (game version drift?)", enum)
		}
		if !o.set.IsValidIndex(idx) {
			return nil, fmt.Errorf("sandbox: option %s index %d out of range (0..%d)", o.Name, idx, o.set.Len()-1)
		}
		out[o.Name] = o.valueAtIndex(idx)
	}
	return out, nil
}

// indexOfValue resolves a human value string to its index in the option's set.
func (o option) indexOfValue(raw string) (int, error) {
	raw = strings.TrimSpace(raw)
	switch o.Type {
	case optFloat:
		f, err := strconv.ParseFloat(raw, 64)
		if err != nil {
			return 0, fmt.Errorf("expected a number, got %q", raw)
		}
		for i, v := range o.set.Floats {
			if floatEq(v, f) {
				return i, nil
			}
		}
		return 0, fmt.Errorf("value %s not an allowed option (set %s)", raw, o.ValueSet)
	case optInt:
		n, err := strconv.Atoi(raw)
		if err != nil {
			return 0, fmt.Errorf("expected an integer, got %q", raw)
		}
		for i, v := range o.set.Ints {
			if v == n {
				return i, nil
			}
		}
		return 0, fmt.Errorf("value %d not an allowed option (set %s)", n, o.ValueSet)
	case optBool:
		b, err := parseBool(raw)
		if err != nil {
			return 0, err
		}
		for i, v := range o.set.Bools {
			if v == b {
				return i, nil
			}
		}
		return 0, fmt.Errorf("value %v not an allowed option (set %s)", b, o.ValueSet)
	}
	return 0, fmt.Errorf("option %s has unknown type", o.Name)
}

// valueAtIndex returns the human value string for an index in the option's set.
func (o option) valueAtIndex(i int) string {
	switch o.Type {
	case optFloat:
		return strconv.FormatFloat(o.set.Floats[i], 'f', -1, 64)
	case optInt:
		return strconv.Itoa(o.set.Ints[i])
	case optBool:
		return strconv.FormatBool(o.set.Bools[i])
	}
	return ""
}

func parseBool(s string) (bool, error) {
	switch strings.ToLower(strings.TrimSpace(s)) {
	case "true", "1", "yes", "on":
		return true, nil
	case "false", "0", "no", "off":
		return false, nil
	}
	return false, fmt.Errorf("expected a boolean, got %q", s)
}

// ---- alpha <-> index helpers (ports of SandboxOptionManager.*) ----

func indexToAlpha(index int) (byte, error) {
	if index < 0 || index > 26 {
		return 0, fmt.Errorf("sandbox: value index %d out of code range", index)
	}
	return byte(65 + index%26), nil
}

func indexToAlpha2(index int) (string, error) {
	if index < 0 || index >= 676 {
		return "", fmt.Errorf("sandbox: enum index %d out of code range", index)
	}
	return string([]byte{byte(65 + index/26), byte(65 + index%26)}), nil
}

func alphaToIndex(value byte) (int, error) {
	v := upper(value)
	if v < 'A' || v > 'Z' {
		return 0, errors.New("sandbox: code value char must be A–Z")
	}
	return int(v - 65), nil
}

func alpha2ToIndex(value string) (int, error) {
	if len(value) != 2 {
		return 0, errors.New("sandbox: code option key must be exactly 2 letters")
	}
	a, b := upper(value[0]), upper(value[1])
	if a < 'A' || a > 'Z' || b < 'A' || b > 'Z' {
		return 0, errors.New("sandbox: code option key must contain only A–Z")
	}
	return int(a-65)*26 + int(b-65), nil
}

func upper(b byte) byte {
	if b >= 'a' && b <= 'z' {
		return b - 32
	}
	return b
}

// DefaultsMap returns every option at its default value (name → value string).
func DefaultsMap() map[string]string {
	out := make(map[string]string, len(options))
	for i := range options {
		o := &options[i]
		out[o.Name] = o.valueAtIndex(defaultIdx[o.Name])
	}
	return out
}

// Active is the codec the panel uses. Now the real V3 codec.
var Active Codec = V3Codec{}

// Preset is an official (or operator-saved) named SandboxCode.
type Preset struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Code        string `json:"code"`
}

// Presets are the V3.0 official presets surfaced in the UI. The default
// difficulty (Adventurer) code is shipped from the game's serverconfig comment;
// the named-preset codes are captured from the in-game Sandbox Options window
// (see scripts/capture-preset-codes). An empty Code == the game's built-in
// default sandbox.
var Presets = []Preset{
	{ID: "default", Name: "Default (Adventurer)", Description: "The game's built-in default sandbox.", Code: "AAAJABJACJADJARFBNC"},
	{ID: "undead_matinee", Name: "Undead Matinee", Description: "Official V3.0 preset.", Code: ""},
	{ID: "madmoles_mayhem", Name: "Madmole's Mayhem", Description: "Official V3.0 preset.", Code: ""},
	{ID: "almost_creative", Name: "Almost Creative Mode", Description: "Official V3.0 preset.", Code: ""},
	{ID: "bite_club", Name: "Bite Club", Description: "Official V3.0 preset.", Code: ""},
	{ID: "legacy_survival", Name: "Legacy Survival", Description: "Official V3.0 preset.", Code: ""},
}

// PresetByID returns the named preset, or ok=false if unknown.
func PresetByID(id string) (Preset, bool) {
	id = strings.TrimSpace(strings.ToLower(id))
	for _, p := range Presets {
		if strings.ToLower(p.ID) == id {
			return p, true
		}
	}
	return Preset{}, false
}

// Validate does a cheap structural sanity check on a SandboxCode before it's
// written into serverconfig.xml. An empty code is valid (== default sandbox).
// A non-empty code must decode cleanly (right version, length, known enums,
// in-range indices) and contain no XML-attribute-breaking characters.
func Validate(code string) error {
	if code == "" {
		return nil
	}
	if strings.ContainsAny(code, `"<>&`) {
		return errors.New("sandbox: code contains characters not valid in an XML attribute value")
	}
	if _, err := (V3Codec{}).Decode(code); err != nil {
		return err
	}
	return nil
}
