// Package empyrionitems parses Empyrion's ItemsConfig.ecf — Eleon's text
// config format — into a flat item index keyed by numeric ID. Used by the
// panel controller to populate the EAH-style give-item picker, ID-to-name
// lookups for inventory views, and category filtering.
//
// The format is brace-delimited with `Key: Value` properties, comment lines
// starting with #, and entries opened by `{ Item Id: N, Name: X` or
// `+ Item Id: N, Name: X` (the `+` form extends a prior block; semantically
// the same for our purposes). Values may be quoted strings, raw tokens, or
// scalar numbers — we keep them as strings and let callers coerce.
//
// We deliberately ignore nested `{ Child ... }` blocks except for tracking
// the brace depth so we don't accidentally close the outer item early.
package empyrionitems

import (
	"bufio"
	"errors"
	"io"
	"os"
	"strconv"
	"strings"
)

// Item is a single Empyrion item / block / weapon entry. Fields populated
// best-effort from properties seen in stock ItemsConfig.ecf; Extras catches
// anything we didn't promote to a typed field so callers can still surface
// scenario-specific keys (Reforged Eden adds plenty).
type Item struct {
	ID          int               `json:"id"`
	Name        string            `json:"name"`        // canonical name token (e.g. "EnergyCellHydrogen")
	Ref         string            `json:"ref,omitempty"`        // template/parent ref
	Category    string            `json:"category,omitempty"`   // ingredients, components, food, weapons, ammo, etc
	Group       string            `json:"group,omitempty"`      // displayed UI grouping
	Mass        string            `json:"mass,omitempty"`       // kg, raw token
	Volume      string            `json:"volume,omitempty"`     // L
	MarketPrice string            `json:"marketPrice,omitempty"`
	StackSize   string            `json:"stack,omitempty"`
	Class       string            `json:"class,omitempty"`      // Ranged / Tool / Item / Block
	HoldType    string            `json:"holdType,omitempty"`
	CustomIcon  string            `json:"customIcon,omitempty"`
	AllowAt     string            `json:"allowAt,omitempty"`
	Description string            `json:"description,omitempty"`
	Extras      map[string]string `json:"extras,omitempty"`
}

// Index is a queryable item store.
type Index struct {
	Items   []Item
	byID    map[int]*Item
	byName  map[string]*Item
}

// LoadFromFile reads ItemsConfig.ecf from disk.
func LoadFromFile(path string) (*Index, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	return LoadFromReader(f)
}

// LoadFromReaders parses multiple ECF streams into a single merged Index.
// Order matters for ID collisions: later readers win, matching Empyrion's
// own load order where BlocksConfig is read after ItemsConfig.
func LoadFromReaders(readers ...io.Reader) (*Index, error) {
	merged := &Index{byID: map[int]*Item{}, byName: map[string]*Item{}}
	for _, r := range readers {
		idx, err := LoadFromReader(r)
		if err != nil {
			return nil, err
		}
		for i := range idx.Items {
			it := idx.Items[i]
			merged.add(&it)
		}
	}
	return merged, nil
}

// LoadFromReader parses an ItemsConfig.ecf stream.
func LoadFromReader(r io.Reader) (*Index, error) {
	idx := &Index{byID: map[int]*Item{}, byName: map[string]*Item{}}
	sc := bufio.NewScanner(r)
	sc.Buffer(make([]byte, 1024*1024), 1024*1024)

	var (
		current   *Item
		depth     int   // brace depth WITHIN an item (for child blocks like Child Recipe { ... })
	)

	for sc.Scan() {
		line := stripComment(sc.Text())
		trim := strings.TrimSpace(line)
		if trim == "" {
			continue
		}

		// Open a new top-level entry: `{ Item Id: 12, ...`, `{ Block Id: 220, ...`,
		// `+ Item Id: ...`, `+ Block Id: ...`. Empyrion's ECF format uses
		// `Item`, `Block`, `Device` interchangeably for the entries we care
		// about — they're all addressable via the same numeric ID space.
		if current == nil && isEntryHeader(trim) {
			it := parseItemHeader(trim)
			if it != nil {
				current = it
				current.Extras = map[string]string{}
				depth = 1
				if strings.HasPrefix(trim, "+") {
					depth = 0
				}
			}
			continue
		}

		// Inside an item.
		if current != nil {
			// Track child brace depth so we don't close the outer item early.
			opens := strings.Count(trim, "{")
			closes := strings.Count(trim, "}")
			depth += opens - closes
			if depth <= 0 {
				idx.add(current)
				current = nil
				depth = 0
				continue
			}
			// `+ Item/Block` form has no closing brace; the next entry
			// header is its terminator.
			if isEntryHeader(trim) {
				idx.add(current)
				current = nil
				depth = 0
				it := parseItemHeader(trim)
				if it != nil {
					current = it
					current.Extras = map[string]string{}
					depth = 1
					if strings.HasPrefix(trim, "+") {
						depth = 0
					}
				}
				continue
			}
			parseProperty(current, trim)
		}
	}
	if current != nil {
		idx.add(current)
	}
	if err := sc.Err(); err != nil && !errors.Is(err, io.EOF) {
		return nil, err
	}
	return idx, nil
}

// isEntryHeader returns true for lines that open a top-level Item / Block /
// Device entry. Empyrion's ECF format uses several keywords interchangeably:
//   `{ Item Id: 12, Name: Foo`
//   `{ Block Id: 220, Name: MetalPieces`
//   `{ Device Id: 1234, Name: ConstructorT2`
//   `+ Item Id: 12, Name: Foo`
//   `+Block Id: 12, ...`
//   `{ +Block Name: Foo, Ref: Bar`   (additive — name-only, no Id)
func isEntryHeader(s string) bool {
	for _, p := range []string{
		"{ Item ", "{ Block ", "{ Device ",
		"+ Item ", "+ Block ", "+ Device ",
		"{Item ", "{Block ", "{Device ",
		"+Item ", "+Block ", "+Device ",
		"{ +Item ", "{ +Block ", "{ +Device ",
		"{+Item ", "{+Block ", "{+Device ",
	} {
		if strings.HasPrefix(s, p) {
			return true
		}
	}
	return false
}

// parseItemHeader handles entry headers in any of the forms isEntryHeader
// recognizes and returns the partial Item populated from Id/Name/Ref keys.
func parseItemHeader(line string) *Item {
	body := line
	for _, p := range []string{
		"{ +Item ", "{ +Block ", "{ +Device ",
		"{+Item ", "{+Block ", "{+Device ",
		"{ Item ", "{ Block ", "{ Device ",
		"+ Item ", "+ Block ", "+ Device ",
		"{Item ", "{Block ", "{Device ",
		"+Item ", "+Block ", "+Device ",
	} {
		body = strings.TrimPrefix(body, p)
	}
	body = strings.TrimSpace(body)
	it := &Item{}
	for _, kv := range splitTopLevel(body, ',') {
		parts := strings.SplitN(strings.TrimSpace(kv), ":", 2)
		if len(parts) != 2 {
			continue
		}
		key := strings.TrimSpace(parts[0])
		val := stripQuotes(strings.TrimSpace(parts[1]))
		switch key {
		case "Id":
			if id, err := strconv.Atoi(val); err == nil {
				it.ID = id
			}
		case "Name":
			it.Name = val
		case "Ref":
			it.Ref = val
		}
	}
	// Name-only `+Block Name: Foo, Ref: Bar` entries (additive overrides) are
	// kept only if we can resolve their ID later via the Ref-target. For now
	// we drop them since they're a small minority and don't affect the picker.
	if it.Name == "" {
		return nil
	}
	return it
}

func parseProperty(it *Item, line string) {
	parts := strings.SplitN(line, ":", 2)
	if len(parts) != 2 {
		return
	}
	key := strings.TrimSpace(parts[0])
	val := stripQuotes(strings.TrimSpace(parts[1]))
	// Trim trailing comment; key/value can be followed by inline `# ...`.
	val = stripComment(val)
	val = strings.TrimSpace(val)
	switch strings.ToLower(key) {
	case "category":
		it.Category = val
	case "group":
		it.Group = val
	case "mass":
		it.Mass = val
	case "volume":
		it.Volume = val
	case "marketprice":
		it.MarketPrice = val
	case "stacksize":
		it.StackSize = val
	case "class":
		it.Class = val
	case "holdtype":
		it.HoldType = val
	case "customicon":
		it.CustomIcon = val
	case "allowat":
		it.AllowAt = val
	case "description":
		it.Description = val
	default:
		if it.Extras == nil {
			it.Extras = map[string]string{}
		}
		it.Extras[key] = val
	}
}

func (idx *Index) add(it *Item) {
	if it == nil || it.Name == "" {
		return
	}
	// duplicate item IDs use the LAST seen entry (matches Empyrion's own override semantics).
	if it.ID > 0 {
		idx.byID[it.ID] = it
	}
	idx.byName[strings.ToLower(it.Name)] = it
	idx.Items = append(idx.Items, *it)
}

// ByID returns the item with the given numeric ID, or nil.
func (idx *Index) ByID(id int) *Item {
	if it, ok := idx.byID[id]; ok {
		c := *it
		return &c
	}
	return nil
}

// ByName looks up an item by its canonical name token (case-insensitive).
func (idx *Index) ByName(name string) *Item {
	if it, ok := idx.byName[strings.ToLower(name)]; ok {
		c := *it
		return &c
	}
	return nil
}

// Search returns items whose name OR description contains the query (case-
// insensitive). Empty query returns all items. limit <= 0 returns all matches.
func (idx *Index) Search(q string, category string, limit int) []Item {
	q = strings.ToLower(strings.TrimSpace(q))
	cat := strings.ToLower(strings.TrimSpace(category))
	out := make([]Item, 0, 64)
	for _, it := range idx.Items {
		if cat != "" && strings.ToLower(it.Category) != cat {
			continue
		}
		if q != "" {
			lname := strings.ToLower(it.Name)
			ldesc := strings.ToLower(it.Description)
			if !strings.Contains(lname, q) && !strings.Contains(ldesc, q) {
				continue
			}
		}
		out = append(out, it)
		if limit > 0 && len(out) >= limit {
			break
		}
	}
	return out
}

// Categories returns the distinct, sorted-by-frequency categories present in the index.
func (idx *Index) Categories() []string {
	counts := map[string]int{}
	for _, it := range idx.Items {
		if it.Category != "" {
			counts[it.Category]++
		}
	}
	out := make([]string, 0, len(counts))
	for k := range counts {
		out = append(out, k)
	}
	// stable order: descending by count, then alpha
	for i := 1; i < len(out); i++ {
		for j := i; j > 0 && (counts[out[j]] > counts[out[j-1]] || (counts[out[j]] == counts[out[j-1]] && out[j] < out[j-1])); j-- {
			out[j], out[j-1] = out[j-1], out[j]
		}
	}
	return out
}

func stripComment(s string) string {
	// `#` starts a comment unless inside quotes — items config doesn't use `#`
	// inside string values in stock content, so a naive strip is safe.
	if i := strings.Index(s, "#"); i >= 0 {
		return s[:i]
	}
	return s
}

func stripQuotes(s string) string {
	if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
		return s[1 : len(s)-1]
	}
	return s
}

// splitTopLevel splits on the given separator while ignoring separators
// inside braces or quotes. Used to break "Id: 12, Name: Foo" into pieces
// without accidentally splitting things like `BuffMod: "a, b, c"`.
func splitTopLevel(s string, sep byte) []string {
	var out []string
	var cur strings.Builder
	depth := 0
	inQuote := false
	for i := 0; i < len(s); i++ {
		c := s[i]
		switch {
		case c == '"':
			inQuote = !inQuote
			cur.WriteByte(c)
		case !inQuote && (c == '{' || c == '['):
			depth++
			cur.WriteByte(c)
		case !inQuote && (c == '}' || c == ']'):
			depth--
			cur.WriteByte(c)
		case !inQuote && depth == 0 && c == sep:
			out = append(out, cur.String())
			cur.Reset()
		default:
			cur.WriteByte(c)
		}
	}
	if cur.Len() > 0 {
		out = append(out, cur.String())
	}
	return out
}
