package topdata

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"

	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
)

const nullValue = "****"

type nativeDataset struct {
	Kind             nativeDatasetKind
	Name             string
	RootPath         string
	BasePath         string
	LockPath         string
	ModulesDir       string
	GeneratedDir     string
	OutputName       string
	Spec             datasetTextSpec
	Columns          []string
	CompareReference bool
}

type nativeDatasetKind string

const (
	nativeDatasetBase  nativeDatasetKind = "base"
	nativeDatasetPlain nativeDatasetKind = "plain"
)

type nativeCollectedDataset struct {
	Dataset      nativeDataset
	Columns      []string
	Rows         []map[string]any
	LockData     map[string]int
	RetiredKeys  map[string]struct{}
	TableKey     string
	LockAdded    int
	LockPruned   int
	LockModified bool
}

type resolvedTable struct {
	Key         string
	DatasetName string
	OutputName  string
	OutputStem  string
	Columns     []string
	Rows        []map[string]any
	LockData    map[string]int
	Kind        nativeDatasetKind
}

type resolvedTableRegistry struct {
	stemByKey  map[string]string
	tableByKey map[string]resolvedTable
}

func newResolvedTableRegistry(collected []nativeCollectedDataset) (resolvedTableRegistry, error) {
	registry := resolvedTableRegistry{
		stemByKey:  map[string]string{},
		tableByKey: map[string]resolvedTable{},
	}
	for _, collectedDataset := range collected {
		if collectedDataset.TableKey == "" {
			continue
		}
		key := collectedDataset.TableKey
		if existing, ok := registry.tableByKey[key]; ok {
			return resolvedTableRegistry{}, fmt.Errorf(
				"duplicate table key %q for %s (%s) conflicts with %s (%s)",
				key,
				collectedDataset.Dataset.Name,
				collectedDataset.Dataset.OutputName,
				existing.DatasetName,
				existing.OutputName,
			)
		}
		rows := make([]map[string]any, 0, len(collectedDataset.Rows))
		for _, row := range collectedDataset.Rows {
			rows = append(rows, cloneRowMap(row))
		}
		lockData := map[string]int{}
		for lockKey, rowID := range collectedDataset.LockData {
			lockData[lockKey] = rowID
		}
		registryTable := resolvedTable{
			Key:         key,
			DatasetName: collectedDataset.Dataset.Name,
			OutputName:  collectedDataset.Dataset.OutputName,
			OutputStem:  outputStem(collectedDataset.Dataset.OutputName),
			Columns:     append([]string(nil), collectedDataset.Columns...),
			Rows:        rows,
			LockData:    lockData,
			Kind:        collectedDataset.Dataset.Kind,
		}
		registry.stemByKey[key] = registryTable.OutputStem
		registry.tableByKey[key] = registryTable
	}
	return registry, nil
}

func (r resolvedTableRegistry) resolveTableByKey(key string) (resolvedTable, bool) {
	table, ok := r.tableByKey[key]
	return table, ok
}

func cloneRowMap(row map[string]any) map[string]any {
	if row == nil {
		return nil
	}
	cloned := make(map[string]any, len(row))
	for key, value := range row {
		cloned[key] = cloneAuthoringValue(value)
	}
	return cloned
}

type legacyTLKData struct {
	Language string
	Entries  map[string]tlkEntryData
	Lock     map[string]int
}

func Build(p *project.Project, progress func(string)) (BuildResult, error) {
	return BuildNative(p, progress)
}

type NativeBuildOptions struct {
	BuildWiki bool
	Force     bool
}

type BuildWikiOptions struct {
	Force bool
}

func BuildWikiWithOptions(p *project.Project, opts BuildWikiOptions, progress func(string)) (wikiResult, error) {
	if progress == nil {
		progress = func(string) {}
	}
	if !p.HasTopData() {
		return wikiResult{}, errors.New("topdata is not configured for this project")
	}

	output2DA := compiled2DAOutputDir(p)
	outputTLK := compiledTLKOutputDir(p)
	if _, err := os.Stat(output2DA); os.IsNotExist(err) {
		return wikiResult{}, fmt.Errorf("topdata build output missing: run build-topdata first")
	}
	if _, err := os.Stat(outputTLK); os.IsNotExist(err) {
		return wikiResult{}, fmt.Errorf("topdata tlk output missing: run build-topdata first")
	}

	nativeResult := BuildResult{
		Mode:         "native",
		Output2DADir: output2DA,
		OutputTLKDir: outputTLK,
	}

	wikiBuild, err := buildWiki(p, nativeResult, opts.Force, progress)
	if err != nil {
		return wikiResult{}, err
	}

	return wikiBuild, nil
}

func BuildNative(p *project.Project, progress func(string)) (BuildResult, error) {
	return BuildNativeWithOptions(p, NativeBuildOptions{BuildWiki: true}, progress)
}

func BuildNativeWithOptions(p *project.Project, opts NativeBuildOptions, progress func(string)) (BuildResult, error) {
	if progress == nil {
		progress = func(string) {}
	}
	if !p.HasTopData() {
		return BuildResult{}, errors.New("topdata is not configured for this project")
	}

	report := ValidateProject(p)
	if report.HasErrors() {
		summary := report.SummaryLines(3)
		if len(summary) > 0 {
			return BuildResult{}, fmt.Errorf("topdata validation failed with %d error(s): %s", report.ErrorCount(), summary[0])
		}
		return BuildResult{}, fmt.Errorf("topdata validation failed with %d error(s)", report.ErrorCount())
	}
	if warnings := report.WarningCount(); warnings > 0 {
		progress(fmt.Sprintf("topdata validation warnings: %d", warnings))
		for _, line := range report.SummaryLines(5) {
			if strings.HasPrefix(line, "warning:") {
				progress(line)
			}
		}
	}
	var prebuiltWiki *wikiResult
	if opts.BuildWiki {
		if newestWikiSource, _, err := newestRelevantWikiSource(p.TopDataSourceDir()); err == nil && !newestWikiSource.IsZero() {
			statePath := wikiOutputStatePath(p)
			outputDir := wikiOutputPagesDir(p)
			if stateInfo, err := os.Stat(statePath); err == nil && !newestWikiSource.After(stateInfo.ModTime()) {
				if outputInfo, err := os.Stat(outputDir); err == nil && outputInfo.IsDir() {
					if state, err := loadWikiState(statePath); err == nil {
						prebuiltWiki = &wikiResult{
							OutputDir: outputDir,
							PageCount: state.Pages,
							Status:    wikiSkippedStatus,
							Digest:    state.Digest,
						}
					}
				}
			}
		}
	}
	return buildNativeUnchecked(p, opts, progress, prebuiltWiki)
}

func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress func(string), prebuiltWiki *wikiResult) (BuildResult, error) {
	if progress == nil {
		progress = func(string) {}
	}
	sourceDir := p.TopDataSourceDir()
	dataDir := filepath.Join(sourceDir, "data")
	datasets, err := discoverNativeDatasets(dataDir)
	if err != nil {
		return BuildResult{}, err
	}
	registryDatasets, err := collectGeneratedRegistryDatasets(dataDir)
	if err != nil {
		return BuildResult{}, err
	}
	if len(datasets) == 0 && len(registryDatasets) == 0 {
		return BuildResult{}, fmt.Errorf("no canonical topdata datasets were found in topdata/data")
	}

	compiler, err := newTLKCompiler(sourceDir, nil)
	if err != nil {
		return BuildResult{}, err
	}

	output2DA := compiled2DAOutputDir(p)
	outputTLK := compiledTLKOutputDir(p)
	progress("Preparing native topdata build outputs...")
	if err := os.RemoveAll(output2DA); err != nil {
		return BuildResult{}, fmt.Errorf("clean topdata 2da output dir: %w", err)
	}
	if err := os.MkdirAll(output2DA, 0o755); err != nil {
		return BuildResult{}, fmt.Errorf("create 2da output dir: %w", err)
	}
	if err := os.MkdirAll(outputTLK, 0o755); err != nil {
		return BuildResult{}, fmt.Errorf("create tlk output dir: %w", err)
	}

	progress("Building native topdata datasets...")
	collected := make([]nativeCollectedDataset, 0, len(datasets)+len(registryDatasets))
	globalKeyToID := map[string]int{}
	globalRowByKey := map[string]map[string]any{}
	for _, dataset := range datasets {
		collectedDataset, err := collectNativeDataset(dataset)
		if err != nil {
			return BuildResult{}, err
		}
		collected = append(collected, collectedDataset)
		for key, rowID := range collectedDataset.LockData {
			globalKeyToID[key] = rowID
		}
		for _, row := range collectedDataset.Rows {
			if key, ok := row["key"].(string); ok && key != "" {
				globalKeyToID[key] = row["id"].(int)
				globalRowByKey[key] = row
			}
		}
	}
	for _, collectedDataset := range registryDatasets {
		collected = append(collected, collectedDataset)
		for key, rowID := range collectedDataset.LockData {
			globalKeyToID[key] = rowID
		}
		for _, row := range collectedDataset.Rows {
			if key, ok := row["key"].(string); ok && key != "" {
				globalKeyToID[key] = row["id"].(int)
				globalRowByKey[key] = row
			}
		}
	}
	collected, err = applyAutogenConsumers(p, collected, progress)
	if err != nil {
		return BuildResult{}, err
	}
	collected, err = applyPartOverrides(sourceDir, collected)
	if err != nil {
		return BuildResult{}, err
	}

	tableRegistry, err := newResolvedTableRegistry(collected)
	if err != nil {
		return BuildResult{}, err
	}
	collected, err = mergeExpansionData(collected)
	if err != nil {
		return BuildResult{}, err
	}
	lockAdded, lockPruned, err := saveNativeLockfiles(collected)
	if err != nil {
		return BuildResult{}, err
	}
	if lockAdded > 0 || lockPruned > 0 {
		progress(fmt.Sprintf("Updated native lockfiles (%d new IDs, %d stale IDs pruned)", lockAdded, lockPruned))
	}
	globalKeyToID = map[string]int{}
	globalRowByKey = map[string]map[string]any{}
	for _, collectedDataset := range collected {
		for key, rowID := range collectedDataset.LockData {
			globalKeyToID[key] = rowID
		}
		for _, row := range collectedDataset.Rows {
			if key, ok := row["key"].(string); ok && key != "" {
				globalKeyToID[key] = row["id"].(int)
				globalRowByKey[key] = row
			}
		}
	}
	tableRegistry, err = newResolvedTableRegistry(collected)
	if err != nil {
		return BuildResult{}, err
	}
	supplementalKeyToID, err := collectSupplementalLockIDs(sourceDir, "")
	if err != nil {
		return BuildResult{}, err
	}
	for key, rowID := range supplementalKeyToID {
		if _, ok := globalKeyToID[key]; ok {
			continue
		}
		globalKeyToID[key] = rowID
	}

	groupStats := nativeCompileGroupStats(collected)
	currentGroup := ""
	for _, dataset := range collected {
		group := nativeCompileGroup(dataset.Dataset.Name)
		if group != currentGroup {
			currentGroup = group
			stats := groupStats[group]
			progress(fmt.Sprintf(
				"Compiling %s datasets (%d output tables from %d source fragments)...",
				group,
				stats.OutputTables,
				stats.SourceFragments,
			))
		}
		compiled, err := resolveNativeDataset(dataset, globalKeyToID, globalRowByKey, tableRegistry, compiler)
		if err != nil {
			return BuildResult{}, err
		}
		if err := write2DA(compiled, filepath.Join(output2DA, dataset.Dataset.OutputName), dataset.Dataset.Kind == nativeDatasetBase); err != nil {
			return BuildResult{}, err
		}
	}

	filesTLK, err := compiler.finish(outputTLK, filepath.Base(p.TopDataCompiledTLKPath()))
	if err != nil {
		return BuildResult{}, err
	}
	files2DA, err := countFiles(output2DA)
	if err != nil {
		return BuildResult{}, err
	}

	wikiBuild := wikiResult{Status: wikiSkippedStatus}
	if prebuiltWiki != nil {
		wikiBuild = *prebuiltWiki
	} else if opts.BuildWiki {
		wikiBuild, err = buildWiki(p, BuildResult{
			Mode:         "native",
			Output2DADir: output2DA,
			OutputTLKDir: outputTLK,
			Files2DA:     files2DA,
			FilesTLK:     filesTLK,
		}, opts.Force, progress)
		if err != nil {
			return BuildResult{}, err
		}
	}

	return BuildResult{
		Mode:          "native",
		Output2DADir:  output2DA,
		OutputTLKDir:  outputTLK,
		Files2DA:      files2DA,
		FilesTLK:      filesTLK,
		WikiOutputDir: wikiBuild.OutputDir,
		WikiPages:     wikiBuild.PageCount,
		WikiStatus:    wikiBuild.Status,
	}, nil
}

func nativeCompileGroup(datasetName string) string {
	head, _, ok := strings.Cut(datasetName, "/")
	if !ok {
		return datasetName
	}
	switch head {
	case "itemprops", "racialtypes", "damagetypes", "parts", "classes":
		return head
	default:
		return head
	}
}

func saveNativeLockfiles(collected []nativeCollectedDataset) (int, int, error) {
	added := 0
	pruned := 0
	for _, dataset := range collected {
		if dataset.Dataset.LockPath == "" {
			continue
		}
		current, err := loadLockfile(dataset.Dataset.LockPath)
		if err != nil {
			return 0, 0, fmt.Errorf("dataset %s: %w", dataset.Dataset.Name, err)
		}
		added += countAddedLockEntries(current, dataset.LockData)
		pruned += countAddedLockEntries(dataset.LockData, current)
		if lockDataEqual(current, dataset.LockData) {
			continue
		}
		if err := saveLockfile(dataset.Dataset.LockPath, dataset.LockData); err != nil {
			return 0, 0, fmt.Errorf("dataset %s: %w", dataset.Dataset.Name, err)
		}
	}
	return added, pruned, nil
}

func countAddedLockEntries(before, after map[string]int) int {
	count := 0
	for key, afterID := range after {
		if beforeID, ok := before[key]; !ok || beforeID != afterID {
			count++
		}
	}
	return count
}

func lockDataEqual(a, b map[string]int) bool {
	if len(a) != len(b) {
		return false
	}
	for key, aID := range a {
		if bID, ok := b[key]; !ok || bID != aID {
			return false
		}
	}
	return true
}

type nativeCompileGroupStat struct {
	OutputTables    int
	SourceFragments int
}

func nativeCompileGroupStats(collected []nativeCollectedDataset) map[string]nativeCompileGroupStat {
	stats := make(map[string]nativeCompileGroupStat)
	for _, dataset := range collected {
		group := nativeCompileGroup(dataset.Dataset.Name)
		groupStat := stats[group]
		groupStat.OutputTables++
		groupStat.SourceFragments += nativeDatasetSourceFragments(dataset.Dataset)
		stats[group] = groupStat
	}
	return stats
}

func nativeDatasetSourceFragments(dataset nativeDataset) int {
	fragments := 1
	if dataset.Kind == nativeDatasetBase {
		fragments += countModuleFiles(dataset.ModulesDir)
		fragments += countModuleFiles(dataset.GeneratedDir)
	}
	return fragments
}

func countModuleFiles(dir string) int {
	if strings.TrimSpace(dir) == "" {
		return 0
	}
	paths, err := collectModulePaths(dir)
	if err != nil {
		return 0
	}
	return len(paths)
}

func discoverNativeDatasets(dataDir string) ([]nativeDataset, error) {
	var datasets []nativeDataset
	appendPlainDataset := func(rootPath, filePath, relName string) error {
		tableData, err := loadJSONObject(filePath)
		if err != nil {
			return err
		}
		outputName, _ := tableData["output"].(string)
		if strings.TrimSpace(outputName) == "" {
			outputName = nativeDatasetDefaultOutputName(filePath, nativeDatasetPlain)
		}
		datasets = append(datasets, nativeDataset{
			Kind:             nativeDatasetPlain,
			Name:             filepath.ToSlash(relName),
			RootPath:         rootPath,
			BasePath:         filePath,
			LockPath:         filepath.Join(rootPath, "lock.json"),
			OutputName:       outputName,
			Spec:             specForDataset(filepath.ToSlash(relName)),
			CompareReference: optionalBool(tableData["compare_reference"], true),
		})
		return nil
	}
	err := filepath.WalkDir(dataDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if !d.IsDir() {
			return nil
		}
		if d.Name() == "modules" || d.Name() == "generated" {
			return filepath.SkipDir
		}

		rel, err := filepath.Rel(dataDir, path)
		if err != nil {
			return err
		}
		if rel == "." {
			entries, err := os.ReadDir(path)
			if err != nil {
				return err
			}
			for _, entry := range entries {
				if entry.IsDir() {
					continue
				}
				name := entry.Name()
				if !strings.HasSuffix(strings.ToLower(name), ".json") || name == "lock.json" || name == "base.json" {
					continue
				}
				filePath := filepath.Join(path, name)
				if err := appendPlainDataset(path, filePath, strings.TrimSuffix(name, filepath.Ext(name))); err != nil {
					return err
				}
			}
			return nil
		}
		if filepath.ToSlash(rel) == "parts/overrides" {
			return filepath.SkipDir
		}
		if filepath.ToSlash(rel) == itempropsRegistryDirName || filepath.ToSlash(rel) == damagetypesRegistryDirName || filepath.ToSlash(rel) == racialtypesRegistryDirName {
			return filepath.SkipDir
		}

		basePath := filepath.Join(path, "base.json")
		if _, err := os.Stat(basePath); err == nil {
			baseData, err := loadJSONObject(basePath)
			if err != nil {
				return err
			}
			outputName, _ := baseData["output"].(string)
			if strings.TrimSpace(outputName) == "" {
				outputName = nativeDatasetDefaultOutputName(path, nativeDatasetBase)
			}
			datasets = append(datasets, nativeDataset{
				Kind:             nativeDatasetBase,
				Name:             filepath.ToSlash(rel),
				RootPath:         path,
				BasePath:         basePath,
				LockPath:         filepath.Join(path, "lock.json"),
				ModulesDir:       filepath.Join(path, "modules"),
				GeneratedDir:     filepath.Join(path, "generated"),
				OutputName:       outputName,
				Spec:             specForDataset(filepath.ToSlash(rel)),
				CompareReference: optionalBool(baseData["compare_reference"], true),
			})
			return nil
		}

		entries, err := os.ReadDir(path)
		if err != nil {
			return err
		}
		for _, entry := range entries {
			if entry.IsDir() {
				continue
			}
			name := entry.Name()
			if strings.HasSuffix(strings.ToLower(name), ".json") && name != "lock.json" {
				filePath := filepath.Join(path, name)
				if err := appendPlainDataset(path, filePath, filepath.Join(rel, strings.TrimSuffix(name, filepath.Ext(name)))); err != nil {
					return err
				}
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	slices.SortFunc(datasets, func(a, b nativeDataset) int {
		return strings.Compare(a.Name, b.Name)
	})
	return datasets, nil
}

func discoverNativeOutputCatalog(dataDir string) (map[string]string, error) {
	datasets, err := discoverNativeDatasets(dataDir)
	if err != nil {
		return nil, err
	}
	registryDatasets, err := collectGeneratedRegistryDatasets(dataDir)
	if err != nil {
		return nil, err
	}
	catalog := make(map[string]string, len(datasets)+len(registryDatasets))
	for _, dataset := range datasets {
		if !dataset.CompareReference {
			continue
		}
		catalog[dataset.OutputName] = dataset.Name
	}
	for _, dataset := range registryDatasets {
		catalog[dataset.Dataset.OutputName] = dataset.Dataset.Name
	}
	return catalog, nil
}

func missingNativeOutputs(projectOutputs, referenceOutputs map[string]string) []string {
	missing := make([]string, 0)
	for outputName, datasetName := range referenceOutputs {
		if _, ok := projectOutputs[outputName]; ok {
			continue
		}
		missing = append(missing, fmt.Sprintf("%s (%s)", outputName, datasetName))
	}
	slices.Sort(missing)
	return missing
}

func collectNativeDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
	switch dataset.Kind {
	case nativeDatasetBase:
		return collectBaseDataset(dataset)
	case nativeDatasetPlain:
		return collectPlainDataset(dataset)
	default:
		return nativeCollectedDataset{}, fmt.Errorf("unsupported native dataset kind %q", dataset.Kind)
	}
}

func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
	baseData, err := loadJSONObject(dataset.BasePath)
	if err != nil {
		return nativeCollectedDataset{}, err
	}

	columns, err := parseColumns(baseData, dataset.Name)
	if err != nil {
		return nativeCollectedDataset{}, err
	}
	modulePaths, err := collectModulePaths(dataset.ModulesDir)
	if err != nil {
		return nativeCollectedDataset{}, err
	}
	moduleData := make([]nativeGeneratedModule, 0, len(modulePaths))
	for _, path := range modulePaths {
		obj, err := loadJSONObject(path)
		if err != nil {
			return nativeCollectedDataset{}, err
		}
		moduleData = append(moduleData, nativeGeneratedModule{
			Name: path,
			Data: obj,
		})
	}
	for _, module := range moduleData {
		columns, err = extendColumns(columns, module.Data, dataset.Name, module.Name)
		if err != nil {
			return nativeCollectedDataset{}, err
		}
	}
	rawBaseRows, ok := baseData["rows"].([]any)
	if !ok {
		return nativeCollectedDataset{}, fmt.Errorf("dataset %s: rows must be an array", dataset.Name)
	}
	baseBoundaryID := -1
	baseRowKeys := map[string]struct{}{}
	baseRowKeyCounts := map[string]int{}
	for index, raw := range rawBaseRows {
		rowObj, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		rowID := index
		if rawID, ok := rowObj["id"]; ok {
			parsed, err := asInt(rawID)
			if err == nil {
				rowID = parsed
			}
		}
		baseBoundaryID = rowID
		key, ok := rowObj["key"].(string)
		if !ok || key == "" {
			continue
		}
		baseRowKeys[key] = struct{}{}
		baseRowKeyCounts[key]++
	}

	lockData, err := loadLockfile(dataset.LockPath)
	if err != nil {
		return nativeCollectedDataset{}, fmt.Errorf("dataset %s: %w", dataset.Name, err)
	}

	explicitModuleIDs := map[string]int{}
	for _, module := range moduleData {
		explicitIDs, err := collectExplicitModuleIDs(dataset.Name, module.Name, module.Data)
		if err != nil {
			return nativeCollectedDataset{}, err
		}
		for key, rowID := range explicitIDs {
			explicitModuleIDs[key] = rowID
		}
	}

	rows := make([]map[string]any, 0, len(rawBaseRows))
	rowByID := map[int]map[string]any{}
	rowByKey := map[string]map[string]any{}
	usedIDs := map[int]struct{}{}
	lockModified := false
	lockAdded := 0
	lockPruned := 0
	retiredKeys := map[string]struct{}{}
	featFamilyAliases := generatedFamilyAliases{}
	if dataset.Name == "feat" {
		var err error
		featFamilyAliases, err = collectGeneratedFamilyAliases(dataset.GeneratedDir)
		if err != nil {
			return nativeCollectedDataset{}, err
		}
	}

	for key, rowID := range lockData {
		if rowID <= baseBoundaryID {
			if _, ok := baseRowKeys[key]; ok {
				continue
			}
			if explicitID, ok := explicitModuleIDs[key]; ok && explicitID == rowID {
				continue
			}
			delete(lockData, key)
			lockModified = true
			lockPruned++
		}
	}
	for _, rowID := range lockData {
		usedIDs[rowID] = struct{}{}
	}
	for index, raw := range rawBaseRows {
		rowObj, ok := raw.(map[string]any)
		if !ok {
			return nativeCollectedDataset{}, fmt.Errorf("dataset %s: row %d must be an object", dataset.Name, index)
		}
		row, err := canonicalizeBaseRow(dataset, columns, rowObj, index)
		if err != nil {
			return nativeCollectedDataset{}, fmt.Errorf("dataset %s: %w", dataset.Name, err)
		}
		rows = append(rows, row)
		rowID := row["id"].(int)
		if key, ok := row["key"].(string); ok && key != "" {
			if baseRowKeyCounts[key] > 1 {
				if lockedID, ok := lockData[key]; !ok || lockedID != rowID {
					lockData[key] = rowID
					lockModified = true
				}
			} else if _, ok := lockData[key]; !ok {
				lockData[key] = rowID
				lockModified = true
				lockAdded++
			}
		}
		rowByID[rowID] = row
		if key, ok := row["key"].(string); ok && key != "" {
			rowByKey[key] = row
		}
		usedIDs[rowID] = struct{}{}
	}
	for rowID := 0; rowID <= baseBoundaryID; rowID++ {
		usedIDs[rowID] = struct{}{}
	}
	for _, rowID := range explicitModuleIDs {
		usedIDs[rowID] = struct{}{}
	}

	nextID := nextAvailableID(usedIDs)
	allocateNextID := func() int {
		rowID := nextID
		usedIDs[rowID] = struct{}{}
		nextID = nextAvailableID(usedIDs)
		return rowID
	}
	lockedIDForKey := func(key string) (int, bool) {
		lockedID, ok := lockData[key]
		if !ok {
			return 0, false
		}
		return lockedID, true
	}
	stripMismatchedLockedOverrideID := func(override map[string]any) (map[string]any, error) {
		rawKey, hasKey := override["key"].(string)
		if !hasKey || rawKey == "" {
			return override, nil
		}
		lockedID, hasLockedID := lockData[rawKey]
		if !hasLockedID {
			return override, nil
		}
		rawID, hasID := override["id"]
		if !hasID {
			return override, nil
		}
		parsedID, err := asInt(rawID)
		if err != nil {
			return nil, fmt.Errorf("dataset %s: override id %v is not numeric", dataset.Name, rawID)
		}
		if parsedID == lockedID {
			return override, nil
		}
		if dataset.Name == "feat" && generatedCanonicalLockCanMove(rawKey, parsedID, lockData, featFamilyAliases) {
			delete(lockData, rawKey)
			lockModified = true
			return override, nil
		}
		return override, nil
	}
	validExplicitIDForNewKey := func(key string, rowID int) bool {
		if key == "" {
			return true
		}
		for lockedKey, lockedID := range lockData {
			if lockedKey != key && lockedID == rowID {
				if dataset.Name == "feat" && generatedAliasLockForKey(key, lockedKey, featFamilyAliases) {
					delete(lockData, lockedKey)
					lockModified = true
					continue
				}
				return false
			}
		}
		return true
	}
	applyModuleData := func(sourceLabel string, moduleData map[string]any) error {
		if rawEntries, ok := moduleData["entries"]; ok {
			entries, ok := rawEntries.(map[string]any)
			if !ok {
				return fmt.Errorf("dataset %s: entries in %s must be an object", dataset.Name, sourceLabel)
			}
			for _, key := range sortedKeys(entries) {
				rawEntry, ok := entries[key].(map[string]any)
				if !ok {
					return fmt.Errorf("dataset %s: entry %q in %s must be an object", dataset.Name, key, sourceLabel)
				}
				if existing, ok := rowByKey[key]; ok {
					candidate := map[string]any{}
					for field, value := range existing {
						candidate[field] = value
					}
					for field, value := range rawEntry {
						if field == "id" || field == "key" || field == "_tlk" {
							continue
						}
						if isMetadataField(field) {
							meta, err := parseRowMetadata(value)
							if err != nil {
								return fmt.Errorf("dataset %s: %w", dataset.Name, err)
							}
							if meta != nil {
								existingMeta, _ := parseExistingMetadata(candidate)
								candidate["meta"] = mergeMetadataMaps(existingMeta, meta)
							}
							continue
						}
						if isDeprecatedAuthoringOnlyField(dataset.Name, field) {
							if err := setLegacyWikiMetadata(candidate, field, value); err != nil {
								return fmt.Errorf("dataset %s: %w", dataset.Name, err)
							}
							continue
						}
						columnName, ok := canonicalColumn(columns, field)
						if !ok {
							return fmt.Errorf("dataset %s: entry field %q is not a known column", dataset.Name, field)
						}
						candidate[columnName] = value
					}
					expanded, err := expandRowAuthoringSugar(dataset, columns, rawEntry, candidate)
					if err != nil {
						return err
					}
					keyChanged, retiredKey, err := updateOverrideRowKey(dataset.Name, existing, expanded, rowByKey, lockData)
					if err != nil {
						return err
					}
					if keyChanged {
						lockModified = true
					}
					if retiredKey != "" {
						retiredKeys[retiredKey] = struct{}{}
					}
					for field, value := range expanded {
						if field == "id" || field == "_tlk" {
							continue
						}
						existing[field] = value
					}
					continue
				}
				rowID, ok := lockedIDForKey(key)
				if !ok {
					rowID = allocateNextID()
					lockData[key] = rowID
					lockModified = true
					lockAdded++
				}
				row, err := canonicalizeEntry(dataset, columns, rowID, key, rawEntry)
				if err != nil {
					return fmt.Errorf("dataset %s: %w", dataset.Name, err)
				}
				rows = append(rows, row)
				rowByID[rowID] = row
				rowByKey[key] = row
			}
		}

		if rawOverrides, ok := moduleData["overrides"]; ok {
			overrideList, ok := rawOverrides.([]any)
			if !ok {
				return fmt.Errorf("dataset %s: overrides in %s must be an array", dataset.Name, sourceLabel)
			}
			for index, rawOverride := range overrideList {
				override, ok := rawOverride.(map[string]any)
				if !ok {
					return fmt.Errorf("dataset %s: override %d in %s must be an object", dataset.Name, index, sourceLabel)
				}
				override, err = stripMismatchedLockedOverrideID(override)
				if err != nil {
					return err
				}
				row, err := resolveOverrideTarget(dataset.Name, override, rowByID, rowByKey)
				if err != nil {
					rawKey, hasKey := override["key"].(string)
					rowID := 0
					hasRowID := false
					if rawID, hasID := override["id"]; hasID {
						parsedID, parseErr := asInt(rawID)
						if parseErr != nil {
							return fmt.Errorf("dataset %s: override id %v is not numeric", dataset.Name, rawID)
						}
						rawKey, _ := override["key"].(string)
						_, lockedIDExists := lockData[rawKey]
						if !lockedIDExists {
							if !validExplicitIDForNewKey(rawKey, parsedID) {
								return fmt.Errorf("dataset %s: override key %q id %d collides with an existing lockfile id", dataset.Name, rawKey, parsedID)
							}
							rowID = parsedID
							hasRowID = true
						}
					}
					if hasKey && rawKey != "" {
						if lockedID, hasLockedID := lockedIDForKey(rawKey); hasLockedID {
							rowID = lockedID
							hasRowID = true
						} else {
							if !hasRowID {
								rowID = allocateNextID()
								hasRowID = true
							}
							lockData[rawKey] = rowID
							lockModified = true
							lockAdded++
						}
					}
					if !hasRowID {
						return err
					}
					usedIDs[rowID] = struct{}{}
					row = map[string]any{"id": rowID}
					if hasKey && rawKey != "" {
						row["key"] = rawKey
					}
					rows = append(rows, row)
					rowByID[rowID] = row
					if hasKey && rawKey != "" {
						rowByKey[rawKey] = row
					}
				}
				if overrideRequestsNullRow(override) {
					retireRowIdentity(row, lockData, retiredKeys)
					nullifyOverrideRow(row, columns, rowByKey)
					continue
				}
				candidate := map[string]any{}
				for field, value := range row {
					candidate[field] = value
				}
				for field, value := range override {
					if field == "id" || field == "key" || field == "_tlk" {
						continue
					}
					if isMetadataField(field) {
						meta, err := parseRowMetadata(value)
						if err != nil {
							return fmt.Errorf("dataset %s: %w", dataset.Name, err)
						}
						if meta != nil {
							existingMeta, _ := parseExistingMetadata(candidate)
							candidate["meta"] = mergeMetadataMaps(existingMeta, meta)
						}
						continue
					}
					if isDeprecatedAuthoringOnlyField(dataset.Name, field) {
						if err := setLegacyWikiMetadata(candidate, field, value); err != nil {
							return fmt.Errorf("dataset %s: %w", dataset.Name, err)
						}
						continue
					}
					columnName, ok := canonicalColumn(columns, field)
					if !ok {
						if isDeprecatedAuthoringOnlyField(dataset.Name, field) {
							continue
						}
						return fmt.Errorf("dataset %s: override field %q is not a known column", dataset.Name, field)
					}
					candidate[columnName] = value
				}
				if rawKey, ok := override["key"]; ok {
					candidate["key"] = rawKey
				}
				expanded, err := expandRowAuthoringSugar(dataset, columns, override, candidate)
				if err != nil {
					return err
				}
				keyChanged, retiredKey, err := updateOverrideRowKey(dataset.Name, row, expanded, rowByKey, lockData)
				if err != nil {
					return err
				}
				if keyChanged {
					lockModified = true
				}
				if retiredKey != "" {
					retiredKeys[retiredKey] = struct{}{}
				}
				for field, value := range expanded {
					if field == "id" || field == "_tlk" {
						continue
					}
					row[field] = value
				}
			}
		}
		return nil
	}

	generatedModules, err := collectGeneratedModules(dataset, lockData)
	if err != nil {
		return nativeCollectedDataset{}, err
	}
	for _, module := range generatedModules {
		updatedColumns, err := extendColumns(columns, module.Data, dataset.Name, module.Name)
		if err != nil {
			return nativeCollectedDataset{}, err
		}
		if len(updatedColumns) != len(columns) {
			columns = updatedColumns
			ensureRowsExposeColumns(rows, columns)
		}
	}
	for _, module := range generatedModules {
		if module.ApplyAfterModules {
			continue
		}
		if err := applyModuleData(module.Name, module.Data); err != nil {
			return nativeCollectedDataset{}, err
		}
	}

	for _, module := range moduleData {
		if err := applyModuleData(module.Name, module.Data); err != nil {
			return nativeCollectedDataset{}, err
		}
	}
	for _, module := range generatedModules {
		if !module.ApplyAfterModules {
			continue
		}
		if err := applyModuleData(module.Name, module.Data); err != nil {
			return nativeCollectedDataset{}, err
		}
	}
	if dataset.Name == "spells" {
		normalizeCollectedSpellImpactScripts(rows, baseRowKeys)
	}
	referencedKeys, err := collectReferencedLockKeys(nativeDatasetSourceDir(dataset))
	if err != nil {
		return nativeCollectedDataset{}, fmt.Errorf("dataset %s: collect referenced keys: %w", dataset.Name, err)
	}
	if pruned, updated := pruneLockDataToActiveRows(lockData, rows, referencedKeys, retiredKeys); pruned > 0 || updated > 0 {
		lockModified = true
		lockPruned += pruned
	}

	rows = dedupeCollectedRows(rows)
	slices.SortFunc(rows, func(a, b map[string]any) int {
		return a["id"].(int) - b["id"].(int)
	})
	return nativeCollectedDataset{
		Dataset:      dataset,
		Columns:      columns,
		Rows:         rows,
		LockData:     lockData,
		RetiredKeys:  retiredKeys,
		LockAdded:    lockAdded,
		LockPruned:   lockPruned,
		LockModified: lockModified,
	}, nil
}

func pruneLockDataToActiveRows(lockData map[string]int, rows []map[string]any, referencedKeys map[string]struct{}, retiredKeys map[string]struct{}) (int, int) {
	if len(lockData) == 0 {
		return 0, 0
	}
	activeKeys := make(map[string]int, len(rows))
	for _, row := range rows {
		if key, ok := row["key"].(string); ok && key != "" {
			rowID, _ := row["id"].(int)
			activeKeys[key] = rowID
		}
	}
	updated := 0
	for key, rowID := range activeKeys {
		if lockedID, ok := lockData[key]; !ok || lockedID != rowID {
			lockData[key] = rowID
			updated++
		}
	}
	pruned := 0
	for key := range lockData {
		if _, ok := activeKeys[key]; ok {
			continue
		}
		if _, retired := retiredKeys[key]; retired {
			delete(lockData, key)
			pruned++
			continue
		}
		if _, ok := referencedKeys[key]; ok {
			continue
		}
		delete(lockData, key)
		pruned++
	}
	return pruned, updated
}

func collectReferencedLockKeys(sourceDir string) (map[string]struct{}, error) {
	dataDir := filepath.Join(sourceDir, "data")
	referenced := map[string]struct{}{}
	if _, err := os.Stat(dataDir); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return referenced, nil
		}
		return nil, err
	}
	err := filepath.WalkDir(dataDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		if filepath.Base(path) == "lock.json" || !strings.EqualFold(filepath.Ext(path), ".json") {
			return nil
		}
		obj, err := loadJSONObject(path)
		if err != nil {
			return err
		}
		collectReferencedLockKeysFromValue(obj, referenced)
		return nil
	})
	return referenced, err
}

func nativeDatasetSourceDir(dataset nativeDataset) string {
	dir := filepath.Clean(dataset.RootPath)
	for {
		if filepath.Base(dir) == "data" {
			return filepath.Dir(dir)
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return filepath.Dir(filepath.Dir(dataset.RootPath))
		}
		dir = parent
	}
}

func collectReferencedLockKeysFromValue(value any, referenced map[string]struct{}) {
	collectReferencedLockKeysFromValueWithExpansion(value, referenced, false)
}

func collectReferencedLockKeysFromValueWithExpansion(value any, referenced map[string]struct{}, inExpansionSubtree bool) {
	switch typed := value.(type) {
	case string:
		if strings.Contains(typed, ":") {
			referenced[typed] = struct{}{}
		}
	case []any:
		for _, item := range typed {
			collectReferencedLockKeysFromValueWithExpansion(item, referenced, inExpansionSubtree)
		}
	case map[string]any:
		_, hasValue := typed["value"]
		_, hasData := typed["data"]
		isExpansion := hasValue && hasData

		for key, item := range typed {
			if strings.Contains(key, ":") {
				referenced[key] = struct{}{}
			}
			if key == "key" {
				if inExpansionSubtree {
					if s, ok := item.(string); ok && strings.Contains(s, ":") {
						referenced[s] = struct{}{}
					}
				}
				continue
			}
			collectReferencedLockKeysFromValueWithExpansion(item, referenced, inExpansionSubtree || isExpansion)
		}
	}
}

func dedupeCollectedRows(rows []map[string]any) []map[string]any {
	if len(rows) <= 1 {
		return rows
	}
	seen := make(map[string]int, len(rows))
	out := make([]map[string]any, 0, len(rows))
	for _, row := range rows {
		rowID, _ := row["id"].(int)
		rowKey, _ := row["key"].(string)
		if rowKey == "" {
			out = append(out, row)
			continue
		}
		identity := strconv.Itoa(rowID) + "\x00" + rowKey
		if index, ok := seen[identity]; ok {
			out[index] = row
			continue
		}
		seen[identity] = len(out)
		out = append(out, row)
	}
	return out
}

type nativeGeneratedModule struct {
	Name              string
	Data              map[string]any
	ApplyAfterModules bool
}

func collectGeneratedModules(dataset nativeDataset, lockData map[string]int) ([]nativeGeneratedModule, error) {
	if dataset.Name != "feat" {
		return nil, nil
	}
	ctx, err := newFeatGeneratedContext(dataset, lockData)
	if err != nil {
		return nil, err
	}
	paths, err := collectModulePaths(dataset.GeneratedDir)
	if err != nil {
		return nil, err
	}
	type generatedSource struct {
		path string
		obj  map[string]any
	}
	sources := make([]generatedSource, 0, len(paths))
	for _, path := range paths {
		obj, err := loadJSONObject(path)
		if err != nil {
			return nil, err
		}
		if isFamilyExpansionObject(obj) {
			spec, err := parseFamilyExpansionSpec(path, obj)
			if err != nil {
				return nil, err
			}
			ctx.familySpecs[spec.FamilyKey] = spec
		}
		sources = append(sources, generatedSource{path: path, obj: obj})
	}
	modules := make([]nativeGeneratedModule, 0, len(paths))
	for _, source := range sources {
		applyAfterModules := false
		if isFamilyExpansionObject(source.obj) {
			spec, err := parseFamilyExpansionSpec(source.path, source.obj)
			if err != nil {
				return nil, err
			}
			applyAfterModules = spec.ApplyAfterModules
		}
		moduleData, err := buildFeatGeneratedModule(source.path, source.obj, ctx)
		if err != nil {
			return nil, err
		}
		modules = append(modules, nativeGeneratedModule{
			Name:              source.path,
			Data:              moduleData,
			ApplyAfterModules: applyAfterModules,
		})
	}
	racialtypesModule, err := buildRacialtypesSkillAffinityModule(ctx)
	if err != nil {
		return nil, err
	}
	if racialtypesModule != nil {
		modules = append(modules, nativeGeneratedModule{
			Name: filepath.Join(dataset.RootPath, "generated", "skill_affinity<racialtypes>"),
			Data: racialtypesModule,
		})
	}
	return modules, nil
}

type featGeneratedContext struct {
	sourceDir      string
	dataDir        string
	lockData       map[string]int
	supplementalID map[string]int
	tlkStateKeys   map[string]struct{}
	existingFeat   map[string]struct{}
	datasets       map[string]nativeDataset
	rowsByDataset  map[string]map[string]map[string]any
	familySpecs    map[string]familyExpansionSpec
}

type compactSourceOverride = map[string]map[string]any

type affinityGrant struct {
	featKey string
	races   []string
}

func newFeatGeneratedContext(dataset nativeDataset, lockData map[string]int) (*featGeneratedContext, error) {
	sourceDir := filepath.Dir(filepath.Dir(dataset.RootPath))
	dataDir := filepath.Join(sourceDir, "data")
	allDatasets, err := discoverNativeDatasets(dataDir)
	if err != nil {
		return nil, err
	}
	datasetMap := make(map[string]nativeDataset, len(allDatasets))
	for _, item := range allDatasets {
		datasetMap[item.Name] = item
	}
	supplementalID, err := collectSupplementalLockIDs(sourceDir, "")
	if err != nil {
		return nil, err
	}
	tlkState, err := loadTLKState(filepath.Join(sourceDir, tlkStateFile))
	if err != nil {
		return nil, err
	}
	tlkStateKeys := make(map[string]struct{}, len(tlkState.Entries))
	for key := range tlkState.Entries {
		tlkStateKeys[key] = struct{}{}
	}
	lockCopy := make(map[string]int, len(lockData))
	for key, value := range lockData {
		lockCopy[key] = value
	}
	existingFeat, err := collectExistingFeatKeys(filepath.Join(dataDir, "feat"))
	if err != nil {
		return nil, err
	}
	retiredFeatKeys, err := collectRetiredFeatKeys(filepath.Join(dataDir, "feat"))
	if err != nil {
		return nil, err
	}
	for key, retiredID := range retiredFeatKeys {
		if lockedID, ok := lockCopy[key]; ok && lockedID == retiredID {
			delete(lockCopy, key)
		}
		delete(existingFeat, key)
	}
	return &featGeneratedContext{
		sourceDir:      sourceDir,
		dataDir:        dataDir,
		lockData:       lockCopy,
		supplementalID: supplementalID,
		tlkStateKeys:   tlkStateKeys,
		existingFeat:   existingFeat,
		datasets:       datasetMap,
		rowsByDataset:  map[string]map[string]map[string]any{},
		familySpecs:    map[string]familyExpansionSpec{},
	}, nil
}

func collectExistingFeatKeys(featDir string) (map[string]struct{}, error) {
	keys := map[string]struct{}{}
	baseObj, err := loadJSONObject(filepath.Join(featDir, "base.json"))
	if err != nil {
		return nil, err
	}
	if rawRows, ok := baseObj["rows"].([]any); ok {
		for _, raw := range rawRows {
			row, ok := raw.(map[string]any)
			if !ok {
				continue
			}
			if key, ok := row["key"].(string); ok && key != "" {
				keys[key] = struct{}{}
			}
		}
	}
	modulePaths, err := collectModulePaths(filepath.Join(featDir, "modules"))
	if err != nil {
		return nil, err
	}
	for _, path := range modulePaths {
		obj, err := loadJSONObject(path)
		if err != nil {
			return nil, err
		}
		if rawEntries, ok := obj["entries"].(map[string]any); ok {
			for _, key := range sortedKeys(rawEntries) {
				if key != "" {
					keys[key] = struct{}{}
				}
			}
		}
		if rawOverrides, ok := obj["overrides"].([]any); ok {
			for _, raw := range rawOverrides {
				override, ok := raw.(map[string]any)
				if !ok {
					continue
				}
				if key, ok := override["key"].(string); ok && key != "" {
					keys[key] = struct{}{}
				}
			}
		}
	}
	return keys, nil
}

func (c *featGeneratedContext) featKeyExists(key string) bool {
	_, ok := c.existingFeat[key]
	return ok
}

func collectRetiredFeatKeys(featDir string) (map[string]int, error) {
	baseObj, err := loadJSONObject(filepath.Join(featDir, "base.json"))
	if err != nil {
		return nil, err
	}
	rowIDToKey := map[int]string{}
	rowKeyToID := map[string]int{}
	usedIDs := map[int]struct{}{}
	if rawRows, ok := baseObj["rows"].([]any); ok {
		for index, raw := range rawRows {
			row, ok := raw.(map[string]any)
			if !ok {
				continue
			}
			rowID := index
			if rawID, ok := row["id"]; ok {
				parsed, err := asInt(rawID)
				if err == nil {
					rowID = parsed
				}
			}
			usedIDs[rowID] = struct{}{}
			if key, ok := row["key"].(string); ok && key != "" {
				rowIDToKey[rowID] = key
				rowKeyToID[key] = rowID
			}
		}
	}
	nextID := nextAvailableID(usedIDs)
	allocateNextID := func() int {
		rowID := nextID
		usedIDs[rowID] = struct{}{}
		nextID = nextAvailableID(usedIDs)
		return rowID
	}
	retired := map[string]int{}
	modulePaths, err := collectModulePaths(filepath.Join(featDir, "modules"))
	if err != nil {
		return nil, err
	}
	for _, path := range modulePaths {
		obj, err := loadJSONObject(path)
		if err != nil {
			return nil, err
		}
		if rawEntries, ok := obj["entries"].(map[string]any); ok {
			for _, key := range sortedKeys(rawEntries) {
				rawEntry := rawEntries[key]
				entry, ok := rawEntry.(map[string]any)
				if !ok || key == "" {
					continue
				}
				if _, exists := rowKeyToID[key]; exists {
					continue
				}
				rowID := 0
				if rawID, ok := entry["id"]; ok {
					parsed, err := asInt(rawID)
					if err != nil {
						return nil, fmt.Errorf("entry %q in %s has non-numeric id %v", key, path, rawID)
					}
					rowID = parsed
					usedIDs[rowID] = struct{}{}
					nextID = nextAvailableID(usedIDs)
				} else {
					rowID = allocateNextID()
				}
				rowIDToKey[rowID] = key
				rowKeyToID[key] = rowID
			}
		}
		rawOverrides, ok := obj["overrides"].([]any)
		if !ok {
			continue
		}
		for index, rawOverride := range rawOverrides {
			override, ok := rawOverride.(map[string]any)
			if !ok {
				return nil, fmt.Errorf("override %d in %s must be an object", index, path)
			}
			rowID := 0
			hasRowID := false
			if rawID, ok := override["id"]; ok {
				parsed, err := asInt(rawID)
				if err != nil {
					return nil, fmt.Errorf("override %d in %s has non-numeric id %v", index, path, rawID)
				}
				rowID = parsed
				hasRowID = true
			} else if key, ok := override["key"].(string); ok && key != "" {
				if mappedID, exists := rowKeyToID[key]; exists {
					rowID = mappedID
					hasRowID = true
				}
			}
			if !hasRowID {
				continue
			}
			currentKey := rowIDToKey[rowID]
			if overrideRequestsNullRow(override) {
				if currentKey != "" {
					retired[currentKey] = rowID
					delete(rowKeyToID, currentKey)
					delete(rowIDToKey, rowID)
				}
				continue
			}
			if rawKey, present := override["key"]; present {
				if rawKey == nil {
					if currentKey != "" {
						retired[currentKey] = rowID
						delete(rowKeyToID, currentKey)
						delete(rowIDToKey, rowID)
					}
					continue
				}
				newKey, ok := rawKey.(string)
				if !ok || newKey == "" || strings.TrimSpace(newKey) == nullValue {
					if currentKey != "" {
						retired[currentKey] = rowID
						delete(rowKeyToID, currentKey)
						delete(rowIDToKey, rowID)
					}
					continue
				}
				if currentKey != "" && currentKey != newKey {
					retired[currentKey] = rowID
					delete(rowKeyToID, currentKey)
				}
				if previousID, exists := rowKeyToID[newKey]; exists && previousID != rowID {
					delete(rowIDToKey, previousID)
				}
				rowKeyToID[newKey] = rowID
				rowIDToKey[rowID] = newKey
			}
		}
	}
	return retired, nil
}

func (c *featGeneratedContext) familyHasExistingRows(familyKey string) bool {
	prefix := "feat:" + familyKey + "_"
	for key := range c.existingFeat {
		if strings.HasPrefix(key, prefix) {
			return true
		}
	}
	return false
}

func (c *featGeneratedContext) datasetRows(name string) (map[string]map[string]any, error) {
	if rows, ok := c.rowsByDataset[name]; ok {
		return rows, nil
	}
	dataset, ok := c.datasets[name]
	if !ok {
		return nil, fmt.Errorf("generated feat family requires canonical dataset %q", name)
	}
	collected, err := collectNativeDataset(dataset)
	if err != nil {
		return nil, err
	}
	rows := make(map[string]map[string]any, len(collected.Rows))
	for _, row := range collected.Rows {
		if key, ok := row["key"].(string); ok && key != "" {
			rows[key] = row
		}
	}
	c.rowsByDataset[name] = rows
	return rows, nil
}

func (c *featGeneratedContext) featID(key string, explicit any) (int, bool, error) {
	if explicit != nil && explicit != nullValue {
		parsed, err := featSourceID(explicit)
		if err != nil {
			return 0, false, err
		}
		if parsed > 0 {
			return parsed, true, nil
		}
	}
	if value, ok := c.lockData[key]; ok && value >= 0 {
		return value, true, nil
	}
	if value, ok := c.supplementalID[key]; ok && value > 0 {
		return value, true, nil
	}
	return 0, false, nil
}

func (c *featGeneratedContext) preferredGeneratedFeatKey(familyKey, sourceSlug string) string {
	exact := "feat:" + familyKey + "_" + sourceSlug
	target := normalizeKeyIdentity(sourceSlug)
	for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
		matches := make([]string, 0)
		for key, value := range candidates {
			if value < 0 {
				continue
			}
			if !strings.HasPrefix(key, "feat:"+familyKey+"_") {
				continue
			}
			suffix := strings.TrimPrefix(key, "feat:"+familyKey+"_")
			if normalizeKeyIdentity(suffix) == target {
				matches = append(matches, key)
			}
		}
		if best, ok := preferredExpandedKey(matches); ok {
			return best
		}
	}
	for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
		if value, ok := candidates[exact]; ok && value > 0 {
			return exact
		}
	}
	stateMatches := make([]string, 0)
	for key := range c.tlkStateKeys {
		if !strings.HasPrefix(key, "feat:"+familyKey+"_") {
			continue
		}
		suffix := strings.TrimPrefix(key, "feat:"+familyKey+"_")
		suffix = strings.TrimSuffix(strings.TrimSuffix(suffix, ".name"), ".description")
		if normalizeKeyIdentity(suffix) == target {
			stateMatches = append(stateMatches, "feat:"+familyKey+"_"+suffix)
		}
	}
	if best, ok := preferredExpandedKey(stateMatches); ok {
		return best
	}
	return exact
}

func preferredExpandedKey(matches []string) (string, bool) {
	if len(matches) == 0 {
		return "", false
	}
	best := matches[0]
	for _, candidate := range matches[1:] {
		best = betterExpandedKey(best, candidate)
	}
	return best, true
}

func betterExpandedKey(a, b string) string {
	aUnderscores := strings.Count(a, "_")
	bUnderscores := strings.Count(b, "_")
	if aUnderscores != bUnderscores {
		if aUnderscores < bUnderscores {
			return a
		}
		return b
	}
	if len(a) != len(b) {
		if len(a) < len(b) {
			return a
		}
		return b
	}
	if a <= b {
		return a
	}
	return b
}

func collectExplicitModuleIDs(datasetName, sourceLabel string, moduleData map[string]any) (map[string]int, error) {
	ids := map[string]int{}
	if rawEntries, ok := moduleData["entries"]; ok {
		entries, ok := rawEntries.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("dataset %s: entries in %s must be an object", datasetName, sourceLabel)
		}
		for key, rawEntry := range entries {
			entry, ok := rawEntry.(map[string]any)
			if !ok {
				return nil, fmt.Errorf("dataset %s: entry %q in %s must be an object", datasetName, key, sourceLabel)
			}
			if rawID, ok := entry["id"]; ok {
				rowID, err := asInt(rawID)
				if err != nil {
					return nil, fmt.Errorf("dataset %s: entry %q id %v is not numeric", datasetName, key, rawID)
				}
				ids[key] = rowID
			}
		}
	}
	if rawOverrides, ok := moduleData["overrides"]; ok {
		switch overrides := rawOverrides.(type) {
		case []any:
			for index, rawOverride := range overrides {
				override, ok := rawOverride.(map[string]any)
				if !ok {
					return nil, fmt.Errorf("dataset %s: override %d in %s must be an object", datasetName, index, sourceLabel)
				}
				if rawID, ok := override["id"]; ok {
					rowID, err := asInt(rawID)
					if err != nil {
						return nil, fmt.Errorf("dataset %s: override id %v is not numeric", datasetName, rawID)
					}
					if key, ok := override["key"].(string); ok && key != "" {
						ids[key] = rowID
					}
				}
			}
		case map[string]any:
			for key, rawOverride := range overrides {
				override, ok := rawOverride.(map[string]any)
				if !ok {
					return nil, fmt.Errorf("dataset %s: override %q in %s must be an object", datasetName, key, sourceLabel)
				}
				if rawID, ok := override["id"]; ok {
					rowID, err := asInt(rawID)
					if err != nil {
						return nil, fmt.Errorf("dataset %s: override id %v is not numeric", datasetName, rawID)
					}
					ids[key] = rowID
				}
			}
		default:
			return nil, fmt.Errorf("dataset %s: overrides in %s must be an array or object", datasetName, sourceLabel)
		}
	}
	return ids, nil
}

func buildFeatGeneratedModule(path string, obj map[string]any, ctx *featGeneratedContext) (map[string]any, error) {
	family, _ := obj["family"].(string)
	if strings.TrimSpace(family) == "" {
		return nil, fmt.Errorf("generated feat file %s is missing family", path)
	}
	if isFamilyExpansionObject(obj) {
		return buildFamilyExpansionGeneratedModule(path, obj, ctx)
	}
	switch family {
	case "special_attacks":
		return buildLegacyFeatGeneratedModule(path, obj)
	default:
		return buildLegacyFeatGeneratedModule(path, obj)
	}
}

func buildLegacyFeatGeneratedModule(path string, obj map[string]any) (map[string]any, error) {
	module := map[string]any{}
	if rawEntries, ok := obj["entries"]; ok {
		entries, ok := rawEntries.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("generated feat file %s entries must be an object", path)
		}
		module["entries"] = entries
	}
	if rawOverrides, ok := obj["overrides"]; ok {
		overrides, ok := rawOverrides.([]any)
		if !ok {
			return nil, fmt.Errorf("generated feat file %s overrides must be an array", path)
		}
		module["overrides"] = overrides
	}
	if len(module) == 0 {
		return nil, fmt.Errorf("generated feat file %s must contain entries and/or overrides", path)
	}
	return module, nil
}

func buildFamilyExpansionGeneratedModule(path string, obj map[string]any, ctx *featGeneratedContext) (map[string]any, error) {
	spec, err := parseFamilyExpansionSpec(path, obj)
	if err != nil {
		return nil, err
	}
	overrides, err := parseCompactSourceOverrides(obj["overrides"])
	if err != nil {
		return nil, fmt.Errorf("generated feat file %s: %w", path, err)
	}
	sourceRows, err := ctx.datasetRows(spec.ChildSource.Dataset)
	if err != nil {
		return nil, err
	}
	overrideList := make([]any, 0)
	familyAllowlist := spec.AllowExistingOnly && ctx.familyHasExistingRows(spec.FamilyKey)
	for _, sourceKey := range sortedStringMapKeys(sourceRows) {
		row := sourceRows[sourceKey]
		include, err := familyExpansionSourceEligible(spec, row)
		if err != nil {
			return nil, fmt.Errorf("generated feat file %s: %w", path, err)
		}
		if !include {
			continue
		}
		slug := strings.TrimPrefix(sourceKey, spec.ChildSource.Dataset+":")
		featKey, rowID, hasID, err := ctx.resolveGeneratedFeatIdentity(spec, slug, row)
		if err != nil {
			return nil, fmt.Errorf("generated feat file %s: %w", path, err)
		}
		childToken := childTokenFromExpandedKey(featKey, spec.FamilyKey)
		displayName := displayNameForGeneratedSource(spec.ChildSource.Dataset, row)
		if spec.IdentitySource == "child_source_value" && childToken != "" && normalizeKeyIdentity(childToken) != normalizeKeyIdentity(slug) {
			displayName = displayNameFromSlug(childToken)
		}
		labelSuffix := upperSnake(displayName)
		override := map[string]any{
			"key":        featKey,
			"MASTERFEAT": map[string]any{"id": spec.Template},
			"meta":       familyMetadata(spec.FamilyKey, childToken, sourceKey, spec.Template),
		}
		if hasID {
			override["id"] = rowID
		}
		if familyAllowlist && !ctx.featKeyExists(featKey) {
			continue
		}
		if spec.ChildRefField != "" {
			override[spec.ChildRefField] = map[string]any{"id": sourceKey}
		}
		for field, prereqFamily := range spec.AutoPrereqFields {
			if prereqKey, ok := generatedFamilyPrereqKey(ctx, row, slug, prereqFamily); ok {
				override[field] = map[string]any{"id": prereqKey}
			}
		}
		if !ctx.featKeyExists(featKey) {
			if spec.LabelPrefix != "" {
				override["LABEL"] = spec.LabelPrefix + "_" + labelSuffix
			}
			if spec.NamePrefix != "" {
				override["FEAT"] = map[string]any{
					"tlk": map[string]any{
						"key":  featKey + ".name",
						"text": fmt.Sprintf("%s (%s)", spec.NamePrefix, displayName),
					},
				}
			}
			for _, field := range spec.TemplateFields {
				override[field] = map[string]any{"field": field, "ref": spec.Template}
			}
			if spec.ConstantPrefix != "" {
				override["Constant"] = spec.ConstantPrefix + "_" + labelSuffix
			}
		}
		for field, value := range spec.DefaultFields {
			override[field] = value
		}
		mergeGeneratedOverride(override, overrides[sourceKey])
		overrideList = append(overrideList, override)
	}
	return map[string]any{"overrides": overrideList}, nil
}

func buildRacialtypesSkillAffinityModule(ctx *featGeneratedContext) (map[string]any, error) {
	racesDir := filepath.Join(ctx.dataDir, "racialtypes", "registry", "races")
	entries, err := os.ReadDir(racesDir)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, err
	}
	grants := map[string]*affinityGrant{}
	for _, entry := range entries {
		if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
			continue
		}
		path := filepath.Join(racesDir, entry.Name())
		obj, err := loadJSONObject(path)
		if err != nil {
			return nil, err
		}
		raceName := displayNameForRaceFile(obj)
		rawFeats, ok := obj["feats"].([]any)
		if !ok {
			continue
		}
		for _, rawFeat := range rawFeats {
			featObj, ok := rawFeat.(map[string]any)
			if !ok {
				continue
			}
			featKey, ok := extractRaceFeatKey(featObj)
			if !ok || !isSkillAffinityFeatKey(featKey) {
				continue
			}
			item := grants[featKey]
			if item == nil {
				item = &affinityGrant{featKey: featKey}
				grants[featKey] = item
			}
			item.races = append(item.races, raceName)
		}
	}
	if len(grants) == 0 {
		return nil, nil
	}
	skillRows, err := ctx.datasetRows("skills")
	if err != nil {
		return nil, err
	}
	overrideList := make([]any, 0, len(grants))
	for _, featKey := range sortedGrantKeys(grants) {
		grant := grants[featKey]
		skillKey, bonus, err := parseSkillAffinityKey(featKey)
		if err != nil {
			return nil, err
		}
		skillKey = resolveAffinitySkillKey(skillRows, skillKey)
		skillRow, ok := skillRows[skillKey]
		skillName := affinitySkillDisplayName(featKey, skillKey)
		if ok {
			skillName = displayNameForSkill(skillRow)
		}
		rowID, hasID, err := ctx.featID(featKey, nil)
		if err != nil {
			return nil, err
		}
		races := dedupeAndSort(grant.races)
		title := affinityTitleFromKey(featKey, skillName)
		description := affinityDescription(skillName, bonus, races)
		label := affinityLabel(featKey)
		parent := "skillaffinity"
		if bonus == 1 {
			parent = "partialskillaffinity"
		}
		child := childTokenFromExpandedKey(featKey, parent)
		override := map[string]any{
			"key":   featKey,
			"LABEL": label,
			"FEAT": map[string]any{
				"tlk": map[string]any{
					"key":  featKey + ".name",
					"text": title,
				},
			},
			"DESCRIPTION": map[string]any{
				"tlk": map[string]any{
					"key":  featKey + ".description",
					"text": description,
				},
			},
			"ICON":             "ife_racial",
			"ALLCLASSESCANUSE": "0",
			"TOOLSCATEGORIES":  "6",
			"PreReqEpic":       "0",
			"ReqAction":        "1",
			"Constant":         label,
			"meta":             familyMetadata(parent, child, skillKey, ""),
		}
		if hasID {
			override["id"] = rowID
		}
		overrideList = append(overrideList, override)
	}
	return map[string]any{"overrides": overrideList}, nil
}

func affinitySkillDisplayName(featKey, skillKey string) string {
	slug := strings.TrimPrefix(skillKey, "skills:")
	if slug == "" {
		slug = strings.TrimPrefix(strings.TrimPrefix(featKey, "feat:skillaffinity"), "feat:partialskillaffinity")
	}
	parts := strings.Fields(strings.ReplaceAll(strings.ReplaceAll(slug, "_", " "), "-", " "))
	for i, part := range parts {
		if part == "" {
			continue
		}
		parts[i] = strings.ToUpper(part[:1]) + part[1:]
	}
	return strings.Join(parts, " ")
}

func requiredStringField(obj map[string]any, field string) (string, error) {
	raw, ok := obj[field]
	if !ok {
		return "", fmt.Errorf("%s is required", field)
	}
	text, ok := raw.(string)
	if !ok || strings.TrimSpace(text) == "" {
		return "", fmt.Errorf("%s must be a non-empty string", field)
	}
	return strings.TrimSpace(text), nil
}

func parseCompactSourceOverrides(raw any) (compactSourceOverride, error) {
	if raw == nil {
		return compactSourceOverride{}, nil
	}
	obj, ok := raw.(map[string]any)
	if !ok {
		return nil, fmt.Errorf("overrides must be an object keyed by source dataset key")
	}
	result := make(compactSourceOverride, len(obj))
	for key, value := range obj {
		item, ok := value.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("override %q must be an object", key)
		}
		result[key] = item
	}
	return result, nil
}

func sortedStringMapKeys[T any](items map[string]T) []string {
	keys := make([]string, 0, len(items))
	for key := range items {
		keys = append(keys, key)
	}
	slices.Sort(keys)
	return keys
}

func mergeGeneratedOverride(dst map[string]any, src map[string]any) {
	if len(src) == 0 {
		return
	}
	if name, ok := src["name"].(string); ok && strings.TrimSpace(name) != "" {
		key, _ := dst["key"].(string)
		dst["FEAT"] = map[string]any{
			"tlk": map[string]any{
				"key":  key + ".name",
				"text": strings.TrimSpace(name),
			},
		}
	}
	if description, ok := src["description_text"].(string); ok && strings.TrimSpace(description) != "" {
		key, _ := dst["key"].(string)
		dst["DESCRIPTION"] = map[string]any{
			"tlk": map[string]any{
				"key":  key + ".description",
				"text": strings.TrimSpace(description),
			},
		}
	}
	if icon, ok := src["icon"]; ok {
		dst["ICON"] = icon
	}
	for field, value := range src {
		switch field {
		case "name", "description_text", "icon":
			continue
		default:
			dst[field] = value
		}
	}
}

func isHiddenSkill(row map[string]any) bool {
	value, ok := lookupField(row, "HideFromLevelUp")
	if !ok {
		return false
	}
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed) == "1"
	case int:
		return typed == 1
	case float64:
		return typed == 1
	default:
		return false
	}
}

func displayNameForSkill(row map[string]any) string {
	if text, ok := displayTextFromValue(row["Name"]); ok {
		return text
	}
	if label, ok := row["Label"].(string); ok && strings.TrimSpace(label) != "" {
		return normalizeDisplayName(strings.TrimSpace(label))
	}
	if key, ok := row["key"].(string); ok {
		return displayNameFromSlug(strings.TrimPrefix(key, "skills:"))
	}
	return "Unknown Skill"
}

func displayNameForBaseitem(row map[string]any) string {
	if text, ok := displayTextFromValue(row["Name"]); ok {
		return text
	}
	if label, ok := row["label"].(string); ok && strings.TrimSpace(label) != "" {
		return normalizeDisplayName(strings.TrimSpace(label))
	}
	if key, ok := row["key"].(string); ok {
		return displayNameFromSlug(strings.TrimPrefix(key, "baseitems:"))
	}
	return "Unknown Weapon"
}

func displayNameForGeneratedSource(dataset string, row map[string]any) string {
	switch dataset {
	case "skills":
		return displayNameForSkill(row)
	case "baseitems":
		return displayNameForBaseitem(row)
	default:
		if text, ok := displayTextFromValue(row["Name"]); ok {
			return text
		}
		if label, ok := row["Label"].(string); ok && strings.TrimSpace(label) != "" {
			return normalizeDisplayName(strings.TrimSpace(label))
		}
		if label, ok := row["label"].(string); ok && strings.TrimSpace(label) != "" {
			return normalizeDisplayName(strings.TrimSpace(label))
		}
		if key, ok := row["key"].(string); ok {
			return displayNameFromSlug(strings.TrimPrefix(key, dataset+":"))
		}
		return "Unknown Entry"
	}
}

func displayNameForRaceFile(obj map[string]any) string {
	core, ok := obj["core"].(map[string]any)
	if ok {
		if text, ok := displayTextFromValue(core["Name"]); ok {
			return text
		}
		if label, ok := core["Label"].(string); ok && strings.TrimSpace(label) != "" {
			return normalizeDisplayName(strings.TrimSpace(label))
		}
	}
	if key, ok := obj["key"].(string); ok {
		return displayNameFromSlug(strings.TrimPrefix(key, "racialtypes:"))
	}
	return "Unknown Race"
}

func displayTextFromValue(value any) (string, bool) {
	switch typed := value.(type) {
	case string:
		if strings.TrimSpace(typed) == "" || isNumericText(typed) || typed == nullValue {
			return "", false
		}
		return normalizeDisplayName(strings.TrimSpace(typed)), true
	case map[string]any:
		if tlk, ok := typed["tlk"].(map[string]any); ok {
			if text, ok := tlk["text"].(string); ok && strings.TrimSpace(text) != "" {
				return strings.TrimSpace(text), true
			}
		}
	}
	return "", false
}

func isNumericText(text string) bool {
	if strings.TrimSpace(text) == "" {
		return false
	}
	for _, r := range text {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}

func displayNameFromSlug(slug string) string {
	text := strings.TrimSpace(slug)
	text = strings.TrimPrefix(text, "knowledge")
	if strings.HasPrefix(slug, "knowledge") {
		remainder := strings.TrimPrefix(slug, "knowledge")
		if remainder == "" {
			return "Knowledge"
		}
		return "Knowledge (" + normalizeDisplayName(remainder) + ")"
	}
	if strings.HasPrefix(slug, "craft_") {
		remainder := strings.TrimPrefix(slug, "craft_")
		return "Craft (" + normalizeDisplayName(remainder) + ")"
	}
	return normalizeDisplayName(text)
}

func normalizeDisplayName(text string) string {
	text = strings.ReplaceAll(text, "_", " ")
	text = splitCamelText(text)
	parts := strings.Fields(text)
	for index, part := range parts {
		lower := strings.ToLower(part)
		switch lower {
		case "of", "and", "vs":
			parts[index] = lower
		default:
			runes := []rune(lower)
			if len(runes) == 0 {
				continue
			}
			runes[0] = []rune(strings.ToUpper(string(runes[0])))[0]
			parts[index] = string(runes)
		}
	}
	return strings.Join(parts, " ")
}

func splitCamelText(text string) string {
	var out []rune
	var prev rune
	for index, r := range text {
		if index > 0 && isCamelBoundary(prev, r) {
			out = append(out, ' ')
		}
		out = append(out, r)
		prev = r
	}
	return string(out)
}

func isCamelBoundary(prev, current rune) bool {
	return prev >= 'a' && prev <= 'z' && current >= 'A' && current <= 'Z'
}

func upperSnake(text string) string {
	normalized := normalizeDisplayName(text)
	normalized = strings.ReplaceAll(normalized, "(", " ")
	normalized = strings.ReplaceAll(normalized, ")", " ")
	normalized = strings.ReplaceAll(normalized, "-", " ")
	normalized = strings.Join(strings.Fields(normalized), "_")
	return strings.ToUpper(normalized)
}

func isNullishValue(value any) bool {
	switch typed := value.(type) {
	case nil:
		return true
	case string:
		return strings.TrimSpace(typed) == "" || typed == nullValue
	default:
		return false
	}
}

func featSourceID(value any) (int, error) {
	if isNullishValue(value) {
		return 0, nil
	}
	switch typed := value.(type) {
	case int:
		return typed, nil
	case float64:
		return int(typed), nil
	case string:
		return asInt(typed)
	default:
		return 0, fmt.Errorf("cannot derive feat id from %T", value)
	}
}

func (c *featGeneratedContext) resolveGeneratedFeatIdentityBySource(familyKey, slug string, rawSource any) (string, int, bool, error) {
	if ref, ok := rawSource.(string); ok && strings.HasPrefix(ref, "feat:") {
		return c.resolveGeneratedFeatIdentityFromRef(familyKey, ref)
	}
	if rawSource != nil && rawSource != nullValue {
		if parsedID, err := featSourceID(rawSource); err == nil && parsedID > 0 {
			if generatedKey, ok := c.generatedFeatKeyForID(familyKey, parsedID); ok {
				return generatedKey, parsedID, true, nil
			}
			return c.preferredGeneratedFeatKey(familyKey, slug), parsedID, true, nil
		}
	}
	if rawMap, ok := rawSource.(map[string]any); ok {
		if ref, ok := rawMap["id"].(string); ok && strings.HasPrefix(ref, "feat:") {
			return c.resolveGeneratedFeatIdentityFromRef(familyKey, ref)
		}
	}
	featKey := c.preferredGeneratedFeatKey(familyKey, slug)
	rowID, hasID, err := c.featID(featKey, rawSource)
	return featKey, rowID, hasID, err
}

func (c *featGeneratedContext) resolveGeneratedFeatIdentityFromRef(familyKey, ref string) (string, int, bool, error) {
	if generatedKey, rowID, ok := c.generatedFeatKeyFromCanonicalAlias(ref); ok {
		return generatedKey, rowID, true, nil
	}
	if rowID, hasID, err := c.featID(ref, nil); err != nil {
		return "", 0, false, err
	} else if hasID {
		if generatedKey, ok := c.generatedFeatKeyForID(familyKey, rowID); ok {
			return generatedKey, rowID, true, nil
		}
	}
	rowID, hasID, err := c.featID(ref, nil)
	return ref, rowID, hasID, err
}

func (c *featGeneratedContext) resolveGeneratedFeatIdentity(spec familyExpansionSpec, slug string, sourceRow map[string]any) (string, int, bool, error) {
	if spec.IdentitySource == "child_source_value" {
		rawSource, ok := lookupField(sourceRow, spec.ChildSource.Column)
		if !ok {
			rawSource = nil
		}
		return c.resolveGeneratedFeatIdentityBySource(spec.FamilyKey, slug, rawSource)
	}
	featKey := c.preferredGeneratedFeatKey(spec.FamilyKey, slug)
	if legacyKey, legacyID, ok := c.generatedFeatKeyFromLegacyAlias(spec.FamilyKey, slug); ok {
		return legacyKey, legacyID, true, nil
	}
	rowID, hasID, err := c.featID(featKey, nil)
	return featKey, rowID, hasID, err
}

func familyExpansionSourceEligible(spec familyExpansionSpec, row map[string]any) (bool, error) {
	if spec.ChildSource.Column != "" {
		rawValue, ok := lookupField(row, spec.ChildSource.Column)
		if !ok || isNullishValue(rawValue) {
			return false, nil
		}
	}
	switch spec.ChildSource.Predicate {
	case "":
		return true, nil
	case "accessible":
		return !isHiddenSkill(row), nil
	default:
		return false, fmt.Errorf("unsupported child_source.predicate %q", spec.ChildSource.Predicate)
	}
}

func generatedFamilyPrereqKey(ctx *featGeneratedContext, sourceRow map[string]any, slug, familyKey string) (string, bool) {
	if familyKey == "" {
		return "", false
	}
	spec, ok := ctx.familySpecs[familyKey]
	if !ok {
		return "", false
	}
	featKey, _, _, err := ctx.resolveGeneratedFeatIdentity(spec, slug, sourceRow)
	if err != nil {
		return "", false
	}
	return featKey, true
}

type generatedFamilyAliases map[string][]string

func collectGeneratedFamilyAliases(generatedDir string) (generatedFamilyAliases, error) {
	aliases := generatedFamilyAliases{}
	if generatedDir == "" {
		return aliases, nil
	}
	paths, err := collectModulePaths(generatedDir)
	if err != nil {
		return nil, err
	}
	for _, path := range paths {
		obj, err := loadJSONObject(path)
		if err != nil {
			return nil, err
		}
		if !isFamilyExpansionObject(obj) {
			continue
		}
		spec, err := parseFamilyExpansionSpec(path, obj)
		if err != nil {
			return nil, err
		}
		if len(spec.LegacyFamilyKeys) > 0 {
			aliases[spec.FamilyKey] = spec.LegacyFamilyKeys
		}
	}
	return aliases, nil
}

func configuredGeneratedFamilyLegacyAliases(familyKey string, aliases generatedFamilyAliases) []string {
	normalized := normalizeKeyIdentity(familyKey)
	for configuredFamilyKey, configured := range aliases {
		if normalizeKeyIdentity(configuredFamilyKey) == normalized && len(configured) > 0 {
			return configured
		}
	}
	return compatibilityGeneratedFamilyLegacyAliases(familyKey)
}

func compatibilityGeneratedFamilyLegacyAliases(familyKey string) []string {
	switch normalizeKeyIdentity(familyKey) {
	case "skillfocus":
		return []string{"skillfocus"}
	case "greaterskillfocus":
		return []string{"epicskillfocus"}
	case "greaterweaponfocus":
		return []string{"epicweaponfocus"}
	case "greaterweaponspecialization":
		return []string{"epicweaponspecialization"}
	case "overwhelmingcritical":
		return []string{"epicoverwhelmingcritical"}
	default:
		return nil
	}
}

func knownGeneratedFamilyKeys(aliases generatedFamilyAliases) []string {
	seen := map[string]struct{}{}
	keys := make([]string, 0, 32)
	add := func(key string) {
		key = strings.TrimSpace(key)
		if key == "" {
			return
		}
		if _, ok := seen[key]; ok {
			return
		}
		seen[key] = struct{}{}
		keys = append(keys, key)
	}
	for _, key := range []string{
		"greater_weapon_specialization",
		"epic_weapon_specialization",
		"greaterweaponspecialization",
		"epicweaponspecialization",
		"overwhelming_critical",
		"epic_overwhelming_critical",
		"overwhelmingcritical",
		"epicoverwhelmingcritical",
		"greater_weapon_focus",
		"epic_weapon_focus",
		"greaterweaponfocus",
		"epicweaponfocus",
		"greater_skill_focus",
		"epic_skill_focus",
		"greaterskillfocus",
		"epicskillfocus",
		"weapon_specialization",
		"weaponspecialization",
		"improved_critical",
		"improvedcritical",
		"weapon_of_choice",
		"weaponofchoice",
		"weapon_focus",
		"weaponfocus",
		"skill_focus",
		"skillfocus",
	} {
		add(key)
	}
	for familyKey, legacyKeys := range aliases {
		add(familyKey)
		for _, legacyKey := range legacyKeys {
			add(legacyKey)
		}
	}
	slices.SortFunc(keys, func(a, b string) int {
		if len(a) == len(b) {
			return strings.Compare(a, b)
		}
		return len(b) - len(a)
	})
	return keys
}

func splitKnownGeneratedFamilyIdentity(text string, aliases generatedFamilyAliases) familyIdentity {
	text = strings.TrimPrefix(strings.TrimSpace(text), "feat:")
	for _, parent := range knownGeneratedFamilyKeys(aliases) {
		if text == parent {
			return familyIdentity{Parent: parent}
		}
		prefix := parent + "_"
		if strings.HasPrefix(text, prefix) {
			return familyIdentity{
				Parent: parent,
				Child:  strings.TrimPrefix(text, prefix),
			}
		}
	}
	return splitFamilyExpansionIdentity(text)
}

func generatedAliasLockForKey(canonicalKey, lockedKey string, aliases generatedFamilyAliases) bool {
	canonicalKey = strings.TrimPrefix(strings.TrimSpace(canonicalKey), "feat:")
	lockedKey = strings.TrimPrefix(strings.TrimSpace(lockedKey), "feat:")
	if canonicalKey == "" || lockedKey == "" {
		return false
	}
	identity := splitKnownGeneratedFamilyIdentity(canonicalKey, aliases)
	if identity.Parent == "" || identity.Child == "" {
		return false
	}
	legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
	for _, alias := range configuredGeneratedFamilyLegacyAliases(identity.Parent, aliases) {
		prefix := alias + "_"
		if !strings.HasPrefix(lockedKey, prefix) {
			continue
		}
		lockedChild := strings.TrimPrefix(lockedKey, prefix)
		for _, legacyChild := range legacyChildren {
			if normalizeKeyIdentity(lockedChild) == normalizeKeyIdentity(legacyChild) {
				return true
			}
		}
	}
	return false
}

func generatedCanonicalLockCanMove(canonicalKey string, rowID int, lockData map[string]int, aliases generatedFamilyAliases) bool {
	if rowID <= 0 {
		return false
	}
	for lockedKey, lockedID := range lockData {
		if lockedID == rowID && generatedAliasLockForKey(canonicalKey, lockedKey, aliases) {
			return true
		}
	}
	return false
}

func (c *featGeneratedContext) generatedFamilyAliases() generatedFamilyAliases {
	aliases := generatedFamilyAliases{}
	for familyKey, spec := range c.familySpecs {
		if len(spec.LegacyFamilyKeys) > 0 {
			aliases[familyKey] = spec.LegacyFamilyKeys
		}
	}
	return aliases
}

func (c *featGeneratedContext) generatedFamilyLegacyAliases(familyKey string) []string {
	normalized := normalizeKeyIdentity(familyKey)
	for specFamilyKey, spec := range c.familySpecs {
		if normalizeKeyIdentity(specFamilyKey) == normalized && len(spec.LegacyFamilyKeys) > 0 {
			return spec.LegacyFamilyKeys
		}
	}
	return compatibilityGeneratedFamilyLegacyAliases(familyKey)
}

func (c *featGeneratedContext) generatedFeatKeyForID(familyKey string, rowID int) (string, bool) {
	if rowID <= 0 {
		return "", false
	}
	for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
		matches := make([]string, 0)
		prefix := "feat:" + familyKey + "_"
		for key, candidateID := range candidates {
			if candidateID == rowID && strings.HasPrefix(key, prefix) {
				matches = append(matches, key)
			}
		}
		if best, ok := preferredExpandedKey(matches); ok {
			return best, true
		}
	}
	for _, alias := range c.generatedFamilyLegacyAliases(familyKey) {
		for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
			matches := make([]string, 0)
			prefix := "feat:" + alias + "_"
			for key, candidateID := range candidates {
				if candidateID != rowID || !strings.HasPrefix(key, prefix) {
					continue
				}
				child := strings.TrimPrefix(key, prefix)
				matches = append(matches, "feat:"+familyKey+"_"+child)
			}
			if best, ok := preferredExpandedKey(matches); ok {
				return best, true
			}
		}
	}
	return "", false
}

func (c *featGeneratedContext) generatedFeatKeyFromLegacyAlias(familyKey, sourceSlug string) (string, int, bool) {
	targets := legacyFamilyChildAliases(familyKey, sourceSlug)
	canonicalKey := c.preferredGeneratedFeatKey(familyKey, sourceSlug)
	for _, alias := range c.generatedFamilyLegacyAliases(familyKey) {
		for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
			prefix := "feat:" + alias + "_"
			for _, target := range targets {
				for key, rowID := range candidates {
					if rowID <= 0 || !strings.HasPrefix(key, prefix) {
						continue
					}
					child := strings.TrimPrefix(key, prefix)
					if normalizeKeyIdentity(child) == normalizeKeyIdentity(target) {
						return canonicalKey, rowID, true
					}
				}
			}
		}
	}
	return "", 0, false
}

func legacyFamilyChildAliases(familyKey, sourceSlug string) []string {
	aliases := []string{}
	normalizedFamilyKey := normalizeKeyIdentity(familyKey)
	if normalizedFamilyKey != "greaterskillfocus" && normalizedFamilyKey != "skillfocus" {
		return []string{sourceSlug}
	}
	switch normalizeKeyIdentity(sourceSlug) {
	case "acrobatics":
		aliases = append(aliases, "tumble")
	case "animalhandling":
		aliases = append(aliases, "animalempathy")
	case "craftarmorsmithing":
		aliases = append(aliases, "craftarmor")
	case "crafttinkering":
		aliases = append(aliases, "settrap")
	case "craftweaponsmithing":
		aliases = append(aliases, "craftweapon")
	case "craftwoodworking":
		aliases = append(aliases, "crafttrap")
	case "disabledevice":
		aliases = append(aliases, "disabletrap")
	case "influence":
		aliases = append(aliases, "persuade")
	case "sleightofhand":
		aliases = append(aliases, "pickpocket")
	}
	aliases = append(aliases, sourceSlug)
	return aliases
}

func (c *featGeneratedContext) generatedFeatKeyFromCanonicalAlias(canonicalKey string) (string, int, bool) {
	stripped := strings.TrimPrefix(strings.TrimSpace(canonicalKey), "feat:")
	identity := splitKnownGeneratedFamilyIdentity(stripped, c.generatedFamilyAliases())
	if identity.Parent == "" || identity.Child == "" {
		return "", 0, false
	}
	legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
	for _, alias := range c.generatedFamilyLegacyAliases(identity.Parent) {
		for _, legacyChild := range legacyChildren {
			if _, rowID, ok := c.featKeyForFamilyChild(alias, legacyChild); ok {
				return "feat:" + identity.Parent + "_" + identity.Child, rowID, true
			}
		}
	}
	return "", 0, false
}

func (c *featGeneratedContext) featKeyForFamilyChild(familyKey, child string) (string, int, bool) {
	key := "feat:" + familyKey + "_" + child
	for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
		if rowID, ok := candidates[key]; ok && rowID > 0 {
			return key, rowID, true
		}
	}
	return "", 0, false
}

func (c *featGeneratedContext) preferredFeatKeyForID(familyKey string, rowID int) (string, bool) {
	prefix := "feat:" + familyKey
	type candidateBuckets struct {
		familyExisting []string
		otherExisting  []string
		familyLegacy   []string
		otherLegacy    []string
		familyOther    []string
		otherOther     []string
	}

	buckets := candidateBuckets{}
	appendMatches := func(candidates map[string]int, legacy bool) {
		for key, candidateID := range candidates {
			if candidateID != rowID {
				continue
			}
			inFamily := strings.HasPrefix(key, prefix)
			exists := c.featKeyExists(key)
			switch {
			case exists && inFamily:
				buckets.familyExisting = append(buckets.familyExisting, key)
			case exists:
				buckets.otherExisting = append(buckets.otherExisting, key)
			case legacy && inFamily:
				buckets.familyLegacy = append(buckets.familyLegacy, key)
			case legacy:
				buckets.otherLegacy = append(buckets.otherLegacy, key)
			case inFamily:
				buckets.familyOther = append(buckets.familyOther, key)
			default:
				buckets.otherOther = append(buckets.otherOther, key)
			}
		}
	}

	appendMatches(c.lockData, false)
	appendMatches(c.supplementalID, true)

	for _, group := range [][]string{
		buckets.familyExisting,
		buckets.otherExisting,
		buckets.otherLegacy,
		buckets.familyLegacy,
		buckets.familyOther,
		buckets.otherOther,
	} {
		if best, ok := preferredExpandedKey(group); ok {
			return best, true
		}
	}
	return "", false
}

func extractRaceFeatKey(obj map[string]any) (string, bool) {
	value, ok := obj["FeatIndex"].(map[string]any)
	if !ok {
		return "", false
	}
	key, ok := value["id"].(string)
	if !ok || !strings.HasPrefix(key, "feat:") {
		return "", false
	}
	return key, true
}

func isSkillAffinityFeatKey(key string) bool {
	return strings.HasPrefix(key, "feat:skillaffinity") || strings.HasPrefix(key, "feat:partialskillaffinity")
}

func parseSkillAffinityKey(featKey string) (string, int, error) {
	switch {
	case strings.HasPrefix(featKey, "feat:partialskillaffinity"):
		return "skills:" + strings.TrimPrefix(featKey, "feat:partialskillaffinity"), 1, nil
	case strings.HasPrefix(featKey, "feat:skillaffinity"):
		return "skills:" + strings.TrimPrefix(featKey, "feat:skillaffinity"), 2, nil
	default:
		return "", 0, fmt.Errorf("unsupported skill affinity key %q", featKey)
	}
}

func resolveAffinitySkillKey(skillRows map[string]map[string]any, skillKey string) string {
	if _, ok := skillRows[skillKey]; ok {
		return skillKey
	}
	target := normalizeKeyIdentity(strings.TrimPrefix(skillKey, "skills:"))
	for _, key := range sortedStringMapKeys(skillRows) {
		if normalizeKeyIdentity(strings.TrimPrefix(key, "skills:")) == target {
			return key
		}
	}
	return skillKey
}

func normalizeKeyIdentity(text string) string {
	text = strings.ToLower(strings.TrimSpace(text))
	text = strings.ReplaceAll(text, "_", "")
	text = strings.ReplaceAll(text, " ", "")
	return text
}

func resolveCanonicalDatasetKey(key string, keyToID map[string]int) string {
	key = strings.TrimSpace(key)
	if key == "" {
		return key
	}
	if _, ok := keyToID[key]; ok {
		return key
	}
	prefix, suffix, ok := strings.Cut(key, ":")
	if !ok || prefix == "" || suffix == "" {
		return key
	}
	normalizedSuffix := normalizeKeyIdentity(suffix)
	match := ""
	for candidate := range keyToID {
		candidatePrefix, candidateSuffix, ok := strings.Cut(candidate, ":")
		if !ok || candidatePrefix != prefix {
			continue
		}
		if normalizeKeyIdentity(candidateSuffix) != normalizedSuffix {
			continue
		}
		if match == "" || candidate < match {
			match = candidate
		}
	}
	if match != "" {
		return match
	}
	return key
}

func affinityTitleFromKey(featKey, skillName string) string {
	if strings.HasPrefix(featKey, "feat:partialskillaffinity") {
		return fmt.Sprintf("Partial Skill Affinity (%s)", skillName)
	}
	return fmt.Sprintf("Skill Affinity (%s)", skillName)
}

func affinityDescription(skillName string, bonus int, races []string) string {
	return fmt.Sprintf("Gain a +%d racial bonus to %s checks. Prerequisites: %s.", bonus, skillName, strings.Join(races, ", "))
}

func affinityLabel(featKey string) string {
	skillKey, _, err := parseSkillAffinityKey(featKey)
	if err != nil {
		return "FEAT_UNKNOWN_SKILL_AFFINITY"
	}
	skillSlug := strings.TrimPrefix(skillKey, "skills:")
	if strings.HasPrefix(featKey, "feat:partialskillaffinity") {
		return "FEAT_PARTIAL_SKILL_AFFINITY_" + upperSnake(displayNameFromSlug(skillSlug))
	}
	return "FEAT_SKILL_AFFINITY_" + upperSnake(displayNameFromSlug(skillSlug))
}

func dedupeAndSort(items []string) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(items))
	for _, item := range items {
		item = strings.TrimSpace(item)
		if item == "" {
			continue
		}
		if _, ok := seen[item]; ok {
			continue
		}
		seen[item] = struct{}{}
		out = append(out, item)
	}
	slices.Sort(out)
	return out
}

func sortedGrantKeys(items map[string]*affinityGrant) []string {
	keys := make([]string, 0, len(items))
	for key := range items {
		keys = append(keys, key)
	}
	slices.Sort(keys)
	return keys
}

func collectPlainDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
	tableData, err := loadJSONObject(dataset.BasePath)
	if err != nil {
		return nativeCollectedDataset{}, err
	}

	columns, err := parseColumns(tableData, dataset.Name)
	if err != nil {
		return nativeCollectedDataset{}, err
	}
	rawRows, ok := tableData["rows"].([]any)
	if !ok {
		return nativeCollectedDataset{}, fmt.Errorf("dataset %s: rows must be an array", dataset.Name)
	}

	lockData, err := loadLockfile(dataset.LockPath)
	if err != nil {
		return nativeCollectedDataset{}, fmt.Errorf("dataset %s: %w", dataset.Name, err)
	}
	usedIDs := map[int]struct{}{}
	for _, rowID := range lockData {
		usedIDs[rowID] = struct{}{}
	}

	rows := make([]map[string]any, 0, len(rawRows))
	explicitID := make([]bool, 0, len(rawRows))
	for index, raw := range rawRows {
		rowObj, ok := raw.(map[string]any)
		if !ok {
			return nativeCollectedDataset{}, fmt.Errorf("dataset %s: row %d must be an object", dataset.Name, index)
		}
		row, err := canonicalizeBaseRow(dataset, columns, rowObj, index)
		if err != nil {
			return nativeCollectedDataset{}, fmt.Errorf("dataset %s: %w", dataset.Name, err)
		}
		rows = append(rows, row)
		_, hasExplicitID := rowObj["id"]
		explicitID = append(explicitID, hasExplicitID)
		if hasExplicitID {
			usedIDs[row["id"].(int)] = struct{}{}
		}
	}

	nextID := nextAvailableID(usedIDs)
	for index, row := range rows {
		key, hasKey := row["key"].(string)
		if hasKey && key != "" {
			if lockedID, ok := lockData[key]; ok {
				row["id"] = lockedID
				continue
			}
			if explicitID[index] {
				lockData[key] = row["id"].(int)
			} else {
				row["id"] = nextID
				lockData[key] = nextID
				usedIDs[nextID] = struct{}{}
				nextID = nextAvailableID(usedIDs)
			}
			continue
		}
		if !explicitID[index] {
			row["id"] = index
		}
	}

	slices.SortFunc(rows, func(a, b map[string]any) int {
		return a["id"].(int) - b["id"].(int)
	})
	tableKey, _ := tableData["key"].(string)
	return nativeCollectedDataset{
		Dataset:  dataset,
		Columns:  columns,
		Rows:     rows,
		LockData: lockData,
		TableKey: tableKey,
	}, nil
}

func resolveNativeDataset(dataset nativeCollectedDataset, keyToID map[string]int, globalRowByKey map[string]map[string]any, tableRegistry resolvedTableRegistry, compiler *tlkCompiler) (map[string]any, error) {
	rows := dataset.Rows
	if strings.HasPrefix(filepath.ToSlash(dataset.Dataset.Name), "classes/feats/") {
		classKey := "classes:" + dataset.Dataset.Name[strings.LastIndex(dataset.Dataset.Name, "/")+1:]
		featSuccessors := buildFeatSuccessorsIndex(globalRowByKey, keyToID)
		classSkills := buildClassSkillsIndex(tableRegistry, classKey)
		expanded, err := expandClassesFeatRows(rows, keyToID, globalRowByKey, featSuccessors, classSkills, globalRowByKey, classKey)
		if err != nil {
			return nil, fmt.Errorf("dataset %s: %w", dataset.Dataset.Name, err)
		}
		rows = expanded
	}

	dataset.Dataset.Columns = dataset.Columns
	resolver := valueResolver{
		dataset:       dataset.Dataset,
		keyToID:       keyToID,
		rowByKey:      globalRowByKey,
		tableRegistry: tableRegistry,
		compiler:      compiler,
		cache:         map[string]map[string]tlkCompiledValue{},
	}

	resolvedRows := make([]map[string]any, 0, len(rows))
	for _, row := range rows {
		resolved, err := resolver.resolveRow(row, nil)
		if err != nil {
			return nil, fmt.Errorf("dataset %s: %w", dataset.Dataset.Name, err)
		}
		resolvedRows = append(resolvedRows, resolved)
	}
	return map[string]any{
		"columns": dataset.Columns,
		"rows":    resolvedRows,
	}, nil
}

var (
	classFeatGlobalRows = []map[string]any{
		{"FeatIndex": map[string]any{"id": "feat:combatexpertise"}, "List": "0", "GrantedOnLevel": "-1", "OnMenu": "1"},
		{"FeatIndex": map[string]any{"id": "feat:powerattack"}, "List": "0", "GrantedOnLevel": "-1", "OnMenu": "1"},
		{"FeatIndex": map[string]any{"id": "feat:specialattacks"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "1"},
		{"FeatIndex": map[string]any{"id": "feat:throw"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "1"},
		{"FeatIndex": map[string]any{"id": "feat:grapple"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "1"},
		{"FeatIndex": map[string]any{"id": "feat:offensivefighting"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "1"},
		{"FeatIndex": map[string]any{"id": "feat:defensivefighting"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "1"},
		{"FeatIndex": map[string]any{"id": "feat:horsemenu"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "1"},
	}
	classFeatClassSkillShorthandRows = []map[string]any{
		{"FeatIndex": map[string]any{"id": "masterfeats:skill_focus", "filter": "classskills"}, "List": "1", "GrantedOnLevel": "-1", "OnMenu": "0"},
		{"FeatIndex": map[string]any{"id": "masterfeats:greater_skill_focus", "filter": "classskills"}, "List": "1", "GrantedOnLevel": "12", "OnMenu": "0"},
	}
)

func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, featSuccessors map[string]string, classSkills map[string]bool, allRowByKey map[string]map[string]any, classKey string) ([]map[string]any, error) {

	injected := make([]map[string]any, 0, len(classFeatGlobalRows)+len(classFeatClassSkillShorthandRows))
	existingRefIDs := make(map[string]struct{}, len(rows))
	for _, row := range rows {
		featRef, ok := row["FeatIndex"].(map[string]any)
		if !ok {
			continue
		}
		refID, _ := featRef["id"].(string)
		if refID == "" {
			continue
		}
		existingRefIDs[refID] = struct{}{}
	}

	for _, row := range classFeatGlobalRows {
		featKey := row["FeatIndex"].(map[string]any)["id"].(string)
		if _, exists := existingRefIDs[featKey]; exists {
			continue
		}
		if _, exists := allRowByKey[featKey]; exists {
			cloned := deepCopyValue(row).(map[string]any)
			if label := lookupReferencedFeatLabel(featKey, allRowByKey); label != "" {
				cloned["FeatLabel"] = label
			}
			injected = append(injected, cloned)
		}
	}

	hasClassSkills := classSkills != nil && len(classSkills) > 0
	for _, row := range classFeatClassSkillShorthandRows {
		if !hasClassSkills {
			continue
		}
		refID := row["FeatIndex"].(map[string]any)["id"].(string)
		if _, exists := existingRefIDs[refID]; exists {
			continue
		}
		rowItems, err := expandClassesFeatRow(row, keyToID, rowByKey, featSuccessors, classSkills, classKey)
		if err != nil {
			return nil, err
		}
		if len(rowItems) > 0 {
			injected = append(injected, rowItems...)
		}
	}

	expanded := make([]map[string]any, 0, len(rows))
	for _, row := range rows {
		rowItems, err := expandClassesFeatRow(row, keyToID, rowByKey, featSuccessors, classSkills, classKey)
		if err != nil {
			return nil, err
		}
		expanded = append(expanded, rowItems...)
	}

	combined := make([]map[string]any, 0, len(injected)+len(expanded))
	combined = append(combined, injected...)
	combined = append(combined, expanded...)

	for index, row := range combined {
		row["id"] = index
	}
	return combined, nil
}

func expandClassesFeatRow(row map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, featSuccessors map[string]string, classSkills map[string]bool, classKey string) ([]map[string]any, error) {
	featRef, ok := row["FeatIndex"].(map[string]any)
	if !ok {
		return []map[string]any{row}, nil
	}
	refKey, _ := featRef["id"].(string)
	filterMode, _ := featRef["filter"].(string)

	switch {
	case strings.HasPrefix(refKey, "feat:"):
		if filterMode == "successors" {
			return expandFeatSuccessors(refKey, row, featSuccessors, rowByKey)
		}
		cloned := deepCopyValue(row).(map[string]any)
		if label := lookupReferencedFeatLabel(refKey, rowByKey); label != "" && isNullLikeValue(cloned["FeatLabel"]) {
			cloned["FeatLabel"] = label
		}
		return []map[string]any{cloned}, nil
	case strings.HasPrefix(refKey, "masterfeats:"):
		masterfeatKey := resolveCanonicalDatasetKey(refKey, keyToID)
		children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey, classKey)
		if err != nil {
			return nil, err
		}
		if filterMode == "classskills" {
			if classSkills == nil || len(classSkills) == 0 {
				return []map[string]any{}, nil
			}
			filtered := make([]map[string]any, 0, len(children))
			for _, child := range children {
				featRef, ok := child["FeatIndex"].(map[string]any)
				if !ok {
					continue
				}
				featKey, _ := featRef["id"].(string)
				featRow := rowByKey[featKey]
				if featRow == nil {
					continue
				}
				reqSkillKey := resolveReqSkill(featRow, keyToID)
				if reqSkillKey == "" {
					continue
				}
				if classSkills[reqSkillKey] {
					filtered = append(filtered, child)
				}
			}
			if len(filtered) == 0 {
				return []map[string]any{}, nil
			}
			return filtered, nil
		}
		if len(children) == 0 {
			return []map[string]any{row}, nil
		}
		return children, nil
	default:
		return []map[string]any{row}, nil
	}
}

func expandMasterfeatChildren(masterfeatKey string, row map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, classKey string) ([]map[string]any, error) {
	masterfeatKey = resolveCanonicalDatasetKey(masterfeatKey, keyToID)
	if _, ok := keyToID[masterfeatKey]; !ok {
		return nil, fmt.Errorf("unknown masterfeat reference %q", masterfeatKey)
	}
	type match struct {
		key string
		id  int
	}
	matches := make([]match, 0)
	for key, featRow := range rowByKey {
		if !strings.HasPrefix(key, "feat:") {
			continue
		}
		if !rowReferencesMasterfeat(featRow, masterfeatKey, keyToID) {
			continue
		}
		if !classFeatExpansionCanUseFeat(featRow, classKey, keyToID) {
			continue
		}
		featID, ok := keyToID[key]
		if !ok {
			continue
		}
		matches = append(matches, match{key: key, id: featID})
	}
	slices.SortFunc(matches, func(a, b match) int {
		return a.id - b.id
	})
	expanded := make([]map[string]any, 0, len(matches))
	for _, item := range matches {
		cloned := deepCopyValue(row).(map[string]any)
		cloned["FeatIndex"] = map[string]any{"id": item.key}
		if label := lookupReferencedFeatLabel(item.key, rowByKey); label != "" {
			cloned["FeatLabel"] = label
		}
		expanded = append(expanded, cloned)
	}
	return expanded, nil
}

func classFeatExpansionCanUseFeat(featRow map[string]any, classKey string, keyToID map[string]int) bool {
	value, ok := lookupField(featRow, "ALLCLASSESCANUSE")
	if !ok || isNullishValue(value) {
		return true
	}
	if strings.TrimSpace(fmt.Sprint(value)) != "0" {
		return true
	}
	if classKey == "" {
		return false
	}
	minLevelClass, ok := lookupField(featRow, "MinLevelClass")
	if !ok || isNullishValue(minLevelClass) {
		return false
	}
	return rowRefersToKey(minLevelClass, classKey, keyToID)
}

func rowRefersToKey(value any, targetKey string, keyToID map[string]int) bool {
	targetKey = resolveCanonicalDatasetKey(targetKey, keyToID)
	targetID, hasTargetID := keyToID[targetKey]
	switch typed := value.(type) {
	case string:
		text := resolveCanonicalDatasetKey(strings.TrimSpace(typed), keyToID)
		if text == "" || text == nullValue || text == "****" {
			return false
		}
		if text == targetKey {
			return true
		}
		if hasTargetID {
			if id, err := strconv.Atoi(text); err == nil {
				return id == targetID
			}
		}
		return false
	case int:
		return hasTargetID && typed == targetID
	case float64:
		return hasTargetID && int(typed) == targetID
	case map[string]any:
		if id, ok := typed["id"].(string); ok {
			return rowRefersToKey(id, targetKey, keyToID)
		}
		if key, ok := typed["key"].(string); ok {
			return rowRefersToKey(key, targetKey, keyToID)
		}
		return false
	default:
		return false
	}
}

func rowReferencesMasterfeat(row map[string]any, masterfeatKey string, keyToID map[string]int) bool {
	masterfeatKey = resolveCanonicalDatasetKey(masterfeatKey, keyToID)
	masterID, ok := keyToID[masterfeatKey]
	if !ok {
		return false
	}
	value, ok := row["MASTERFEAT"]
	if !ok {
		return false
	}
	switch typed := value.(type) {
	case string:
		return typed == strconv.Itoa(masterID)
	case int:
		return typed == masterID
	case float64:
		return int(typed) == masterID
	case map[string]any:
		if id, ok := typed["id"].(string); ok {
			id = resolveCanonicalDatasetKey(id, keyToID)
			if id == masterfeatKey {
				return true
			}
			if refID, ok := keyToID[id]; ok {
				return refID == masterID
			}
			return id == "masterfeats:"+strconv.Itoa(masterID)
		}
		return false
	default:
		return false
	}
}

func lookupReferencedFeatLabel(featKey string, rowByKey map[string]map[string]any) string {
	row, ok := rowByKey[featKey]
	if !ok {
		return ""
	}
	label, _ := row["LABEL"].(string)
	return strings.TrimSpace(label)
}

func isNullLikeValue(value any) bool {
	switch typed := value.(type) {
	case nil:
		return true
	case string:
		trimmed := strings.TrimSpace(typed)
		return trimmed == "" || trimmed == nullValue
	default:
		return false
	}
}

func buildFeatSuccessorsIndex(rowByKey map[string]map[string]any, keyToID map[string]int) map[string]string {
	successors := make(map[string]string)
	for key, row := range rowByKey {
		if !strings.HasPrefix(key, "feat:") {
			continue
		}
		successorKey := resolveFeatSuccessor(row, keyToID)
		if successorKey != "" {
			successors[key] = successorKey
		}
	}
	return successors
}

func resolveFeatSuccessor(row map[string]any, keyToID map[string]int) string {
	value, ok := row["SUCCESSOR"]
	if !ok {
		return ""
	}
	switch typed := value.(type) {
	case map[string]any:
		if idValue, ok := typed["id"]; ok {
			switch idTyped := idValue.(type) {
			case string:
				trimmed := strings.TrimSpace(idTyped)
				if trimmed == "" || trimmed == nullValue || trimmed == "****" {
					return ""
				}
				if id, err := strconv.Atoi(trimmed); err == nil {
					for k, v := range keyToID {
						if v == id && strings.HasPrefix(k, "feat:") {
							return k
						}
					}
					return ""
				}
				if strings.HasPrefix(trimmed, "feat:") {
					return trimmed
				}
				return "feat:" + trimmed
			case int:
				for k, v := range keyToID {
					if v == idTyped && strings.HasPrefix(k, "feat:") {
						return k
					}
				}
				return ""
			case float64:
				targetID := int(idTyped)
				for k, v := range keyToID {
					if v == targetID && strings.HasPrefix(k, "feat:") {
						return k
					}
				}
				return ""
			}
		}
		return ""
	case string:
		trimmed := strings.TrimSpace(typed)
		if trimmed == "" || trimmed == nullValue || trimmed == "****" {
			return ""
		}
		if id, err := strconv.Atoi(trimmed); err == nil {
			for k, v := range keyToID {
				if v == id && strings.HasPrefix(k, "feat:") {
					return k
				}
			}
			return ""
		}
		if strings.HasPrefix(trimmed, "feat:") {
			return trimmed
		}
		return "feat:" + trimmed
	case int:
		for k, v := range keyToID {
			if v == typed && strings.HasPrefix(k, "feat:") {
				return k
			}
		}
		return ""
	case float64:
		targetID := int(typed)
		for k, v := range keyToID {
			if v == targetID && strings.HasPrefix(k, "feat:") {
				return k
			}
		}
		return ""
	default:
		return ""
	}
}

func buildClassSkillsIndex(tableRegistry resolvedTableRegistry, classKey string) map[string]bool {
	skillDatasetKey := classKey
	if !strings.HasPrefix(skillDatasetKey, "classes/skills:") {
		skillDatasetKey = "classes/skills:" + strings.TrimPrefix(classKey, "classes:")
	}

	table, ok := tableRegistry.resolveTableByKey(skillDatasetKey)
	if !ok {
		return nil
	}

	skills := make(map[string]bool)
	for _, rowMap := range table.Rows {
		classSkill, _ := rowMap["ClassSkill"].(string)
		if classSkill != "1" {
			continue
		}
		skillIndex, ok := rowMap["SkillIndex"].(map[string]any)
		if !ok {
			continue
		}
		skillKey, _ := skillIndex["id"].(string)
		if skillKey != "" {
			skills[skillKey] = true
		}
	}

	if len(skills) == 0 {
		return nil
	}
	return skills
}

func expandFeatSuccessors(rootFeatKey string, row map[string]any, featSuccessors map[string]string, rowByKey map[string]map[string]any) ([]map[string]any, error) {
	current := rootFeatKey
	visited := make(map[string]bool)
	var results []map[string]any
	for {
		if current == "" {
			break
		}
		if visited[current] {
			return nil, fmt.Errorf("successor chain cycles at %q", current)
		}
		visited[current] = true
		if rowByKey[current] == nil {
			break
		}
		cloned := deepCopyValue(row).(map[string]any)
		cloned["FeatIndex"] = map[string]any{"id": current}
		if label := lookupReferencedFeatLabel(current, rowByKey); label != "" {
			cloned["FeatLabel"] = label
		}
		results = append(results, cloned)
		current = featSuccessors[current]
	}
	if len(results) == 0 {
		return []map[string]any{row}, nil
	}
	return results, nil
}

func expandMasterfeatChildrenFiltered(masterfeatKey string, row map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, classSkills map[string]bool) ([]map[string]any, error) {
	if classSkills == nil || len(classSkills) == 0 {
		return []map[string]any{}, nil
	}
	children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey, "")
	if err != nil {
		return nil, err
	}
	var filtered []map[string]any
	for _, child := range children {
		featRef, ok := child["FeatIndex"].(map[string]any)
		if !ok {
			continue
		}
		featKey, _ := featRef["id"].(string)
		featRow := rowByKey[featKey]
		if featRow == nil {
			continue
		}
		reqSkillKey := resolveReqSkill(featRow, keyToID)
		if reqSkillKey == "" {
			continue
		}
		if classSkills[reqSkillKey] {
			filtered = append(filtered, child)
		}
	}
	return filtered, nil
}

func extractClassKeyFromDatasetName(rowByKey map[string]map[string]any) string {
	for key := range rowByKey {
		if strings.HasPrefix(key, "classes:") {
			return key
		}
	}
	return ""
}

func resolveReqSkill(row map[string]any, keyToID map[string]int) string {
	value, ok := row["REQSKILL"]
	if !ok {
		return ""
	}
	switch typed := value.(type) {
	case string:
		trimmed := strings.TrimSpace(typed)
		if trimmed == "" || trimmed == nullValue || trimmed == "****" {
			return ""
		}
		if strings.HasPrefix(trimmed, "skills:") {
			return trimmed
		}
		if resolved := lookupKeyByIDPrefix(keyToID, "skills:", trimmed); resolved != "" {
			return resolved
		}
		return "skills:" + trimmed
	case map[string]any:
		if id, ok := typed["id"].(string); ok {
			if id == "" || id == nullValue || id == "****" {
				return ""
			}
			if strings.HasPrefix(id, "skills:") {
				return id
			}
			if resolved := lookupKeyByIDPrefix(keyToID, "skills:", id); resolved != "" {
				return resolved
			}
			return "skills:" + id
		}
		return ""
	default:
		return ""
	}
}

func lookupKeyByIDPrefix(keyToID map[string]int, prefix string, rawID string) string {
	idValue, err := strconv.Atoi(strings.TrimSpace(rawID))
	if err != nil {
		return ""
	}
	for key, existingID := range keyToID {
		if existingID == idValue && strings.HasPrefix(key, prefix) {
			return key
		}
	}
	return ""
}

type valueResolver struct {
	dataset       nativeDataset
	keyToID       map[string]int
	rowByKey      map[string]map[string]any
	tableRegistry resolvedTableRegistry
	compiler      *tlkCompiler
	cache         map[string]map[string]tlkCompiledValue
}

func (r *valueResolver) resolveTableByKey(key string) (resolvedTable, bool) {
	return r.tableRegistry.resolveTableByKey(key)
}

func (r *valueResolver) resolveRow(row map[string]any, stack []string) (map[string]any, error) {
	effectiveRow, err := r.resolveInheritedObject(row, rowIdentity(r.dataset.Name, row), row, stack)
	if err != nil {
		return nil, err
	}
	out := map[string]any{
		"id": effectiveRow["id"],
	}
	for _, column := range rowColumns(effectiveRow, r.dataset.Columns) {
		resolved, err := r.resolveField(effectiveRow, column, stack)
		if err != nil {
			return nil, err
		}
		out[column] = resolved.Value
	}
	return out, nil
}

func (r *valueResolver) resolveField(row map[string]any, field string, stack []string) (tlkCompiledValue, error) {
	rowKey := rowCacheIdentity(r.dataset.Name, row)
	if r.cache[rowKey] == nil {
		r.cache[rowKey] = map[string]tlkCompiledValue{}
	}
	if cached, ok := r.cache[rowKey][field]; ok {
		return cached, nil
	}

	stackKey := rowKey + "." + field
	for _, existing := range stack {
		if existing == stackKey {
			return tlkCompiledValue{}, fmt.Errorf("cyclic row/field reference detected at %s", stackKey)
		}
	}

	value, ok := lookupField(row, field)
	if !ok {
		resolved := tlkCompiledValue{Value: nullValue}
		r.cache[rowKey][field] = resolved
		return resolved, nil
	}
	resolved, err := r.resolveValue(row, field, value, append(stack, stackKey))
	if err != nil {
		return tlkCompiledValue{}, err
	}
	r.cache[rowKey][field] = resolved
	return resolved, nil
}

func (r *valueResolver) resolveValue(row map[string]any, field string, value any, stack []string) (tlkCompiledValue, error) {
	if typed, ok := value.(map[string]any); ok {
		merged, err := r.resolveInheritedObject(row, rowCacheIdentity(r.dataset.Name, row)+"."+field, typed, stack)
		if err != nil {
			return tlkCompiledValue{}, err
		}
		value = merged
	}
	if literal, ok := r.resolveLiteralFeatObject(row, field, value); ok {
		return tlkCompiledValue{Value: literal}, nil
	}
	allowBare := columnMatchesSpec(r.dataset.Spec, field)
	if payload, ok, err := parseTLKPayload(value, allowBare); err != nil {
		return tlkCompiledValue{}, err
	} else if ok {
		return r.resolveTLKPayload(row, field, payload, stack)
	}

	switch typed := value.(type) {
	case map[string]any:
		if refID, ok := typed["id"]; ok {
			if refKey, ok := refID.(string); ok && strings.Contains(refKey, ":") {
				id, ok := r.keyToID[refKey]
				if !ok {
					return tlkCompiledValue{}, fmt.Errorf("unknown key reference: %s (row=%s field=%s)", refKey, rowCacheIdentity(r.dataset.Name, row), field)
				}
				return tlkCompiledValue{Value: id}, nil
			}
		}
		if refKey, ok := typed["ref"]; ok {
			fieldValue, hasField := typed["field"]
			refKeyString, okKey := refKey.(string)
			fieldString, okField := fieldValue.(string)
			if hasField && okKey && okField {
				targetRow, ok := r.rowByKey[refKeyString]
				if !ok {
					return tlkCompiledValue{}, fmt.Errorf("unknown row reference: %s", refKeyString)
				}
				return r.resolveField(targetRow, fieldString, stack)
			}
		}
		if tableValue, ok := typed["table"]; ok {
			tableKey, ok := tableValue.(string)
			if ok {
				outputName, ok := r.tableRegistry.stemByKey[tableKey]
				if !ok {
					return tlkCompiledValue{}, fmt.Errorf("unknown table reference: %s", tableKey)
				}
				return tlkCompiledValue{Value: outputName}, nil
			}
		}

		resolved := map[string]any{}
		for _, key := range sortedKeys(typed) {
			item, err := r.resolveValue(row, key, typed[key], stack)
			if err != nil {
				return tlkCompiledValue{}, err
			}
			resolved[key] = item.Value
		}
		return tlkCompiledValue{Value: resolved}, nil
	case []any:
		resolved := make([]any, 0, len(typed))
		for _, item := range typed {
			value, err := r.resolveValue(row, field, item, stack)
			if err != nil {
				return tlkCompiledValue{}, err
			}
			resolved = append(resolved, value.Value)
		}
		return tlkCompiledValue{Value: resolved}, nil
	default:
		return tlkCompiledValue{Value: typed}, nil
	}
}

func (r *valueResolver) resolveLiteralFeatObject(row map[string]any, field string, value any) (string, bool) {
	if r.dataset.Name != "feat" {
		return "", false
	}
	typed, ok := value.(map[string]any)
	if !ok {
		return "", false
	}
	if refID, ok := typed["id"].(string); ok && len(typed) == 1 && strings.Contains(refID, ":") {
		if !shouldPreserveLiteralFeatIDObject(row, field, refID) {
			return "", false
		}
		if _, exists := r.keyToID[refID]; !exists {
			return "", false
		}
		return "{'id': '" + refID + "'}", true
	}
	if refKey, ok := typed["key"].(string); ok && len(typed) == 1 && strings.Contains(refKey, ":") {
		if !shouldPreserveLiteralFeatKeyObject(field) {
			return "", false
		}
		if _, exists := r.keyToID[refKey]; !exists && !isLegacyLiteralFeatKeyException(field, refKey) {
			return "", false
		}
		return "{'key': '" + refKey + "'}", true
	}
	return "", false
}

func shouldPreserveLiteralFeatKeyObject(field string) bool {
	switch field {
	case "SUCCESSOR", "PREREQFEAT1", "PREREQFEAT2":
		return true
	default:
		return false
	}
}

func shouldPreserveLiteralFeatIDObject(row map[string]any, field, refID string) bool {
	switch field {
	case "MinLevelClass":
		return shouldPreserveFeatMinLevelClass(row)
	case "SUCCESSOR":
		switch refID {
		case "feat:weaponproficiencysimple":
			return true
		default:
			return false
		}
	default:
		return false
	}
}

func shouldPreserveFeatMinLevelClass(row map[string]any) bool {
	rowKey, _ := row["key"].(string)
	if prefix, suffix, ok := strings.Cut(strings.TrimSpace(rowKey), ":"); ok && prefix == "masterfeats" {
		switch normalizeKeyIdentity(suffix) {
		case "weaponfocus", "weaponspecialization", "greaterweaponfocus", "greaterweaponspecialization", "improvedcritical", "overwhelmingcritical":
			return true
		}
	}
	switch {
	case strings.HasPrefix(rowKey, "feat:weaponfocus_"):
		return true
	case strings.HasPrefix(rowKey, "feat:weaponspecialization_"):
		return true
	case strings.HasPrefix(rowKey, "feat:greaterweaponfocus_"):
		return true
	case strings.HasPrefix(rowKey, "feat:greaterweaponspecialization_"):
		return true
	case strings.HasPrefix(rowKey, "feat:improvedcritical_"):
		return true
	case strings.HasPrefix(rowKey, "feat:overwhelmingcritical_"):
		return true
	case strings.HasPrefix(rowKey, "feat:epicweaponfocus_"):
		return true
	case strings.HasPrefix(rowKey, "feat:epicweaponspecialization_"):
		return true
	default:
		return false
	}
}

func isLegacyLiteralFeatKeyException(field, refKey string) bool {
	if field != "SUCCESSOR" {
		return false
	}
	switch refKey {
	case "feat:improveddodge", "feat:improveduncannydodge":
		return true
	default:
		return false
	}
}

func (r *valueResolver) resolveTLKPayload(row map[string]any, field string, payload parsedTLKPayload, stack []string) (tlkCompiledValue, error) {
	if payload.Ref != "" {
		targetRow, ok := r.rowByKey[payload.Ref]
		if !ok {
			return tlkCompiledValue{}, fmt.Errorf("unknown TLK ref row %s", payload.Ref)
		}
		resolved, err := r.resolveField(targetRow, payload.RefField, stack)
		if err != nil {
			return tlkCompiledValue{}, err
		}
		if resolved.TLKKey == "" {
			switch resolved.Value.(type) {
			case string, int, float64:
				return resolved, nil
			default:
				return tlkCompiledValue{}, fmt.Errorf("TLK ref %s.%s does not resolve to TLK-backed text", payload.Ref, payload.RefField)
			}
		}
		return resolved, nil
	}
	if payload.Text != "" {
		key := payload.Key
		if key == "" {
			key = deriveAutoTLKKey(rowIdentity(r.dataset.Name, row), field)
		}
		return r.compiler.registerInline(key, tlkEntryData{
			Text:        payload.Text,
			SoundResRef: payload.SoundResRef,
			SoundLength: payload.SoundLength,
		})
	}
	return r.compiler.resolveLegacyKey(payload.Key)
}

func rowColumns(row map[string]any, ordered []string) []string {
	columns := make([]string, 0, len(row))
	seen := map[string]struct{}{}
	for _, key := range ordered {
		if _, ok := row[key]; ok {
			columns = append(columns, key)
			seen[key] = struct{}{}
		}
	}
	for key := range row {
		if key == "id" || key == "key" || key == "__columns" || key == "inherit" || isMetadataField(key) {
			continue
		}
		if _, ok := seen[key]; ok {
			continue
		}
		columns = append(columns, key)
	}
	return columns
}

func columnMatchesSpec(spec datasetTextSpec, field string) bool {
	return roleForField(spec, field) == textRoleName || roleForField(spec, field) == textRoleDescription
}

func rowIdentity(datasetName string, row map[string]any) string {
	if key, ok := row["key"].(string); ok && key != "" {
		return key
	}
	return fmt.Sprintf("%s:id:%d", datasetName, row["id"].(int))
}

func rowCacheIdentity(datasetName string, row map[string]any) string {
	return rowIdentity(datasetName, row)
}

func lookupField(row map[string]any, field string) (any, bool) {
	if value, ok := row[field]; ok {
		return value, true
	}
	for key, value := range row {
		if strings.EqualFold(key, field) {
			return value, true
		}
	}
	return nil, false
}

func parseColumns(baseData map[string]any, datasetName string) ([]string, error) {
	rawColumns, ok := baseData["columns"].([]any)
	if !ok {
		return nil, fmt.Errorf("dataset %s: columns must be an array", datasetName)
	}
	columns := make([]string, 0, len(rawColumns))
	for _, raw := range rawColumns {
		column, ok := raw.(string)
		if !ok {
			return nil, fmt.Errorf("dataset %s: columns must contain strings", datasetName)
		}
		if isDeprecatedAuthoringOnlyField(datasetName, column) || isMetadataField(column) {
			continue
		}
		columns = append(columns, column)
	}
	return columns, nil
}

func extendColumns(columns []string, obj map[string]any, datasetName, sourceLabel string) ([]string, error) {
	rawColumns, ok := obj["columns"]
	if !ok {
		return columns, nil
	}
	columnList, ok := rawColumns.([]any)
	if !ok {
		return nil, fmt.Errorf("dataset %s: columns in %s must be an array", datasetName, sourceLabel)
	}
	extended := append([]string(nil), columns...)
	for _, raw := range columnList {
		column, ok := raw.(string)
		if !ok {
			return nil, fmt.Errorf("dataset %s: columns in %s must contain strings", datasetName, sourceLabel)
		}
		if isDeprecatedAuthoringOnlyField(datasetName, column) || isMetadataField(column) {
			continue
		}
		if _, exists := canonicalColumn(extended, column); exists {
			continue
		}
		extended = append(extended, column)
	}
	return extended, nil
}

func ensureRowsExposeColumns(rows []map[string]any, columns []string) {
	for _, row := range rows {
		for _, column := range columns {
			if _, ok := row[column]; !ok {
				row[column] = nullValue
			}
		}
	}
}

func canonicalizeBaseRow(dataset nativeDataset, columns []string, raw map[string]any, index int) (map[string]any, error) {
	rowID := index
	if value, ok := raw["id"]; ok {
		parsed, err := asInt(value)
		if err != nil {
			return nil, fmt.Errorf("row id %v is not numeric", value)
		}
		rowID = parsed
	}

	row := map[string]any{"id": rowID}
	for _, column := range columns {
		row[column] = nullValue
	}
	for key, value := range raw {
		switch key {
		case "id", "_tlk":
		case "key":
			if value == nil {
				continue
			}
			text, ok := value.(string)
			if !ok {
				return nil, fmt.Errorf("row key must be a string")
			}
			row["key"] = text
		case "inherit":
			row["inherit"] = value
		default:
			if isMetadataField(key) {
				meta, err := parseRowMetadata(value)
				if err != nil {
					return nil, err
				}
				if meta != nil {
					row["meta"] = meta
				}
				continue
			}
			if isDeprecatedAuthoringOnlyField(dataset.Name, key) {
				if err := setLegacyWikiMetadata(row, key, value); err != nil {
					return nil, err
				}
				continue
			}
			columnName, ok := canonicalColumn(columns, key)
			if !ok {
				if isDeprecatedAuthoringOnlyField(dataset.Name, key) {
					continue
				}
				return nil, fmt.Errorf("unknown column %q", key)
			}
			row[columnName] = value
		}
	}

	return expandRowAuthoringSugar(dataset, columns, raw, row)
}

func canonicalizeEntry(dataset nativeDataset, columns []string, rowID int, key string, raw map[string]any) (map[string]any, error) {
	row := map[string]any{
		"id":  rowID,
		"key": key,
	}
	for _, column := range columns {
		row[column] = nullValue
	}
	for field, value := range raw {
		if field == "_tlk" {
			continue
		}
		if field == "inherit" {
			row["inherit"] = value
			continue
		}
		if isMetadataField(field) {
			meta, err := parseRowMetadata(value)
			if err != nil {
				return nil, err
			}
			if meta != nil {
				row["meta"] = meta
			}
			continue
		}
		if isDeprecatedAuthoringOnlyField(dataset.Name, field) {
			if err := setLegacyWikiMetadata(row, field, value); err != nil {
				return nil, err
			}
			continue
		}
		columnName, ok := canonicalColumn(columns, field)
		if !ok {
			if isDeprecatedAuthoringOnlyField(dataset.Name, field) {
				continue
			}
			return nil, fmt.Errorf("entry %q references unknown column %q", key, field)
		}
		row[columnName] = value
	}
	return expandRowAuthoringSugar(dataset, columns, raw, row)
}

func isAuthoringOnlyField(name string) bool {
	return strings.HasPrefix(strings.ToUpper(strings.TrimSpace(name)), "WIKI")
}

func preserveLegacyWikiColumn(datasetName, field string) bool {
	return filepath.ToSlash(strings.TrimSpace(datasetName)) == "classes/core" &&
		strings.EqualFold(strings.TrimSpace(field), "WIKIGENERATE")
}

func isDeprecatedAuthoringOnlyField(datasetName, field string) bool {
	return isAuthoringOnlyField(field) && !preserveLegacyWikiColumn(datasetName, field)
}

func isMetadataField(name string) bool {
	return strings.EqualFold(strings.TrimSpace(name), "meta")
}

func expandRowAuthoringSugar(dataset nativeDataset, columns []string, source map[string]any, row map[string]any) (map[string]any, error) {
	expanded, err := expandRowTLKSugar(dataset, columns, source, row)
	if err != nil {
		return nil, err
	}
	return expandRowInheritanceSugar(dataset, columns, source, expanded)
}

func expandRowTLKSugar(dataset nativeDataset, columns []string, source map[string]any, row map[string]any) (map[string]any, error) {
	rawTLK, ok := source["_tlk"]
	if !ok {
		return row, nil
	}
	tlkMap, ok := rawTLK.(map[string]any)
	if !ok {
		return nil, fmt.Errorf("dataset %s: _tlk must be an object", dataset.Name)
	}

	out := map[string]any{}
	for key, value := range row {
		out[key] = value
	}
	for roleName, rawValue := range tlkMap {
		var role textRole
		switch strings.ToLower(roleName) {
		case "name":
			role = textRoleName
		case "description":
			role = textRoleDescription
		default:
			return nil, fmt.Errorf("dataset %s: unsupported _tlk role %q", dataset.Name, roleName)
		}
		column := columnForRole(columns, dataset.Spec, role)
		if column == "" {
			return nil, fmt.Errorf("dataset %s: _tlk.%s has no matching text column", dataset.Name, roleName)
		}
		if _, hasField := source[column]; hasField {
			return nil, fmt.Errorf("dataset %s: cannot use _tlk.%s and field %s together", dataset.Name, roleName, column)
		}

		switch typed := rawValue.(type) {
		case string:
			out[column] = map[string]any{"tlk": map[string]any{"text": typed}}
		case map[string]any:
			if _, ok := typed["tlk"]; ok {
				out[column] = typed
			} else {
				out[column] = map[string]any{"tlk": typed}
			}
		default:
			return nil, fmt.Errorf("dataset %s: _tlk.%s must be a string or object", dataset.Name, roleName)
		}
	}
	return out, nil
}

func expandRowInheritanceSugar(dataset nativeDataset, columns []string, source map[string]any, row map[string]any) (map[string]any, error) {
	spec, ok, err := parseRowInheritanceSpec(source)
	if err != nil {
		return nil, fmt.Errorf("dataset %s: %w", dataset.Name, err)
	}
	if !ok {
		return row, nil
	}

	parentKey, err := resolveInheritanceParentKey(row, spec.From)
	if err != nil {
		return nil, fmt.Errorf("dataset %s: %w", dataset.Name, err)
	}

	out := map[string]any{}
	for key, value := range row {
		out[key] = value
	}
	delete(out, "inherit")
	for _, field := range spec.Fields {
		columnName, ok := canonicalColumn(columns, field)
		if !ok {
			return nil, fmt.Errorf("inherit field %q is not a known column", field)
		}
		if rowFieldWasAuthored(source, columns, columnName, dataset.Spec) {
			continue
		}
		if current, ok := lookupField(out, columnName); ok && current != nullValue {
			continue
		}
		out[columnName] = map[string]any{
			"ref":   parentKey,
			"field": columnName,
		}
	}
	return out, nil
}

type rowInheritanceSpec struct {
	From   string
	Fields []string
}

type genericInheritanceSpec struct {
	Ref   string
	Field string
}

func parseRowInheritanceSpec(source map[string]any) (rowInheritanceSpec, bool, error) {
	raw, ok := source["inherit"]
	if !ok {
		return rowInheritanceSpec{}, false, nil
	}
	obj, ok := raw.(map[string]any)
	if !ok {
		return rowInheritanceSpec{}, false, fmt.Errorf("inherit must be an object")
	}
	if _, hasRef := obj["ref"]; hasRef {
		if _, hasFrom := obj["from"]; hasFrom {
			return rowInheritanceSpec{}, false, fmt.Errorf("inherit cannot mix row sugar (from/fields) with object inheritance (ref)")
		}
		return rowInheritanceSpec{}, false, nil
	}
	rawFrom, ok := obj["from"]
	if !ok {
		return rowInheritanceSpec{}, false, fmt.Errorf("inherit.from is required")
	}
	from, ok := rawFrom.(string)
	if !ok || strings.TrimSpace(from) == "" {
		return rowInheritanceSpec{}, false, fmt.Errorf("inherit.from must be a non-empty string")
	}
	rawFields, ok := obj["fields"]
	if !ok {
		return rowInheritanceSpec{}, false, fmt.Errorf("inherit.fields is required")
	}
	fieldList, ok := rawFields.([]any)
	if !ok || len(fieldList) == 0 {
		return rowInheritanceSpec{}, false, fmt.Errorf("inherit.fields must be a non-empty array")
	}
	fields := make([]string, 0, len(fieldList))
	seen := map[string]struct{}{}
	for _, rawField := range fieldList {
		field, ok := rawField.(string)
		if !ok || strings.TrimSpace(field) == "" {
			return rowInheritanceSpec{}, false, fmt.Errorf("inherit.fields must contain non-empty strings")
		}
		normalized := strings.ToLower(field)
		if _, exists := seen[normalized]; exists {
			continue
		}
		seen[normalized] = struct{}{}
		fields = append(fields, field)
	}
	return rowInheritanceSpec{From: from, Fields: fields}, true, nil
}

func parseGenericInheritanceSpec(source map[string]any) (genericInheritanceSpec, bool, error) {
	raw, ok := source["inherit"]
	if !ok {
		return genericInheritanceSpec{}, false, nil
	}
	obj, ok := raw.(map[string]any)
	if !ok {
		return genericInheritanceSpec{}, false, fmt.Errorf("inherit must be an object")
	}
	if _, hasFrom := obj["from"]; hasFrom {
		if _, hasRef := obj["ref"]; hasRef {
			return genericInheritanceSpec{}, false, fmt.Errorf("inherit cannot mix row sugar (from/fields) with object inheritance (ref)")
		}
		return genericInheritanceSpec{}, false, nil
	}
	rawRef, ok := obj["ref"]
	if !ok {
		return genericInheritanceSpec{}, false, fmt.Errorf("inherit.ref is required")
	}
	ref, ok := rawRef.(string)
	if !ok || strings.TrimSpace(ref) == "" {
		return genericInheritanceSpec{}, false, fmt.Errorf("inherit.ref must be a non-empty string")
	}
	spec := genericInheritanceSpec{Ref: strings.TrimSpace(ref)}
	if rawField, ok := obj["field"]; ok {
		field, ok := rawField.(string)
		if !ok || strings.TrimSpace(field) == "" {
			return genericInheritanceSpec{}, false, fmt.Errorf("inherit.field must be a non-empty string when present")
		}
		spec.Field = strings.TrimSpace(field)
	}
	return spec, true, nil
}

func resolveInheritanceParentKey(row map[string]any, from string) (string, error) {
	value, ok := lookupField(row, from)
	if !ok {
		return "", fmt.Errorf("inherit.from field %q was not found on row", from)
	}
	switch typed := value.(type) {
	case string:
		if strings.Contains(typed, ":") {
			return typed, nil
		}
	case map[string]any:
		if rawID, ok := typed["id"]; ok {
			if key, ok := rawID.(string); ok && strings.Contains(key, ":") {
				return key, nil
			}
		}
		if rawRef, ok := typed["ref"]; ok {
			if key, ok := rawRef.(string); ok && strings.Contains(key, ":") {
				if _, hasField := typed["field"]; !hasField {
					return key, nil
				}
			}
		}
	}
	return "", fmt.Errorf("inherit.from field %q must resolve to a keyed parent row reference", from)
}

func (r *valueResolver) resolveInheritedObject(ownerRow map[string]any, identity string, obj map[string]any, stack []string) (map[string]any, error) {
	spec, ok, err := parseGenericInheritanceSpec(obj)
	if err != nil {
		return nil, err
	}

	out := map[string]any{}
	for key, value := range obj {
		if key == "inherit" {
			continue
		}
		out[key] = cloneAuthoringValue(value)
	}
	if !ok {
		return out, nil
	}

	marker := "inherit:" + identity
	for _, existing := range stack {
		if existing == marker {
			return nil, fmt.Errorf("cyclic inheritance detected at %s", identity)
		}
	}

	parent, err := r.resolveGenericInheritanceTarget(ownerRow, spec, append(stack, marker))
	if err != nil {
		return nil, err
	}
	return mergeInheritedMaps(parent, out), nil
}

func (r *valueResolver) resolveGenericInheritanceTarget(ownerRow map[string]any, spec genericInheritanceSpec, stack []string) (map[string]any, error) {
	targetRow, ok := r.rowByKey[spec.Ref]
	if !ok {
		return nil, fmt.Errorf("inherit target %q was not found", spec.Ref)
	}
	if spec.Field == "" {
		return r.resolveInheritedObject(targetRow, spec.Ref, targetRow, stack)
	}
	value, ok := lookupField(targetRow, spec.Field)
	if !ok {
		return nil, fmt.Errorf("inherit target field %q was not found on %s", spec.Field, spec.Ref)
	}
	targetObj, ok := value.(map[string]any)
	if !ok {
		return nil, fmt.Errorf("inherit target %s.%s must resolve to an object", spec.Ref, spec.Field)
	}
	return r.resolveInheritedObject(targetRow, spec.Ref+"."+spec.Field, targetObj, stack)
}

func mergeInheritedMaps(parent, local map[string]any) map[string]any {
	out := map[string]any{}
	for key, value := range parent {
		out[key] = cloneAuthoringValue(value)
	}
	for key, value := range local {
		if key == "inherit" {
			continue
		}
		parentValue, hasParent := out[key]
		if !hasParent {
			out[key] = cloneAuthoringValue(value)
			continue
		}
		out[key] = mergeInheritedValues(parentValue, value)
	}
	return out
}

func mergeInheritedValues(parent, local any) any {
	if isNullLike(local) {
		return cloneAuthoringValue(parent)
	}
	localMap, localIsMap := local.(map[string]any)
	parentMap, parentIsMap := parent.(map[string]any)
	if localIsMap && parentIsMap && !isAtomicAuthoringMap(localMap) && !isAtomicAuthoringMap(parentMap) {
		return mergeInheritedMaps(parentMap, localMap)
	}
	return cloneAuthoringValue(local)
}

func isAtomicAuthoringMap(value map[string]any) bool {
	for _, key := range []string{"tlk", "ref", "id", "table"} {
		if _, ok := value[key]; ok {
			return true
		}
	}
	return false
}

func cloneAuthoringValue(value any) any {
	switch typed := value.(type) {
	case map[string]any:
		out := map[string]any{}
		for key, item := range typed {
			out[key] = cloneAuthoringValue(item)
		}
		return out
	case []any:
		out := make([]any, 0, len(typed))
		for _, item := range typed {
			out = append(out, cloneAuthoringValue(item))
		}
		return out
	default:
		return typed
	}
}

func rowFieldWasAuthored(source map[string]any, columns []string, field string, spec datasetTextSpec) bool {
	if _, ok := source[field]; ok {
		return true
	}
	for key := range source {
		if canonical, ok := canonicalColumn(columns, key); ok && canonical == field {
			return true
		}
	}
	rawTLK, ok := source["_tlk"].(map[string]any)
	if !ok {
		return false
	}
	for roleName := range rawTLK {
		var role textRole
		switch strings.ToLower(roleName) {
		case "name":
			role = textRoleName
		case "description":
			role = textRoleDescription
		default:
			continue
		}
		column := columnForRole(columns, spec, role)
		if column == field {
			return true
		}
	}
	return false
}

func resolveOverrideTarget(datasetName string, override map[string]any, rowByID map[int]map[string]any, rowByKey map[string]map[string]any) (map[string]any, error) {
	if rawID, ok := override["id"]; ok {
		rowID, err := asInt(rawID)
		if err != nil {
			return nil, fmt.Errorf("dataset %s: override id %v is not numeric", datasetName, rawID)
		}
		row, ok := rowByID[rowID]
		if !ok {
			return nil, fmt.Errorf("dataset %s: override target id %d was not found", datasetName, rowID)
		}
		return row, nil
	}
	if rawKey, ok := override["key"]; ok {
		key, ok := rawKey.(string)
		if !ok || key == "" {
			return nil, fmt.Errorf("dataset %s: override key must be a string", datasetName)
		}
		if key != "" {
			row, ok := rowByKey[key]
			if ok {
				return row, nil
			}
			return nil, fmt.Errorf("dataset %s: override target key %q was not found", datasetName, key)
		}
	}
	return nil, fmt.Errorf("dataset %s: override must specify id or key", datasetName)
}

func overrideRequestsNullRow(override map[string]any) bool {
	value, ok := override["null"]
	if !ok {
		return false
	}
	switch typed := value.(type) {
	case bool:
		return typed
	case string:
		trimmed := strings.TrimSpace(strings.ToLower(typed))
		return trimmed == "true" || trimmed == "1" || trimmed == "yes"
	case int:
		return typed != 0
	case float64:
		return typed != 0
	default:
		return false
	}
}

func nullifyOverrideRow(row map[string]any, columns []string, rowByKey map[string]map[string]any) {
	if oldKey, ok := row["key"].(string); ok && oldKey != "" {
		if mapped, exists := rowByKey[oldKey]; exists {
			mappedID, mappedHasID := mapped["id"].(int)
			rowID, rowHasID := row["id"].(int)
			if mappedHasID && rowHasID && mappedID == rowID {
				delete(rowByKey, oldKey)
			}
		}
	}
	for field := range row {
		if field != "id" {
			delete(row, field)
		}
	}
	for _, column := range columns {
		row[column] = nullValue
	}
}

func retireRowIdentity(row map[string]any, lockData map[string]int, retiredKeys map[string]struct{}) {
	if row == nil {
		return
	}
	oldKey, ok := row["key"].(string)
	if !ok || oldKey == "" {
		return
	}
	rowID, _ := row["id"].(int)
	if lockedID, ok := lockData[oldKey]; ok && lockedID == rowID {
		delete(lockData, oldKey)
	}
	if retiredKeys != nil {
		retiredKeys[oldKey] = struct{}{}
	}
}

func updateOverrideRowKey(datasetName string, row map[string]any, expanded map[string]any, rowByKey map[string]map[string]any, lockData map[string]int) (bool, string, error) {
	oldKey, _ := row["key"].(string)
	newKeyValue, hasKey := expanded["key"]
	if !hasKey {
		return false, "", nil
	}
	rowID, _ := row["id"].(int)
	changed := false
	retiredKey := ""
	if oldKey != "" {
		if mapped, ok := rowByKey[oldKey]; ok {
			mappedID, mappedHasID := mapped["id"].(int)
			rowHasID := rowID != 0 || row["id"] != nil
			if mappedHasID && rowHasID && mappedID == rowID {
				delete(rowByKey, oldKey)
			}
		}
	}
	if newKeyValue == nil {
		delete(row, "key")
		if oldKey != "" {
			retiredKey = oldKey
			if lockedID, ok := lockData[oldKey]; ok && lockedID == rowID {
				delete(lockData, oldKey)
			}
			changed = true
		}
		return changed, retiredKey, nil
	}
	newKey, ok := newKeyValue.(string)
	if !ok || newKey == "" || strings.TrimSpace(newKey) == nullValue {
		delete(row, "key")
		if oldKey != "" {
			retiredKey = oldKey
			if lockedID, ok := lockData[oldKey]; ok && lockedID == rowID {
				delete(lockData, oldKey)
			}
			changed = true
		}
		return changed, retiredKey, nil
	}
	if oldKey == newKey {
		row["key"] = newKey
		rowByKey[newKey] = row
		if _, ok := lockData[newKey]; !ok {
			lockData[newKey] = rowID
			changed = true
		}
		return changed, "", nil
	}
	conflictingID := -1
	if conflicting, ok := rowByKey[newKey]; ok && conflicting != nil {
		var conflictingHasID bool
		conflictingID, conflictingHasID = conflicting["id"].(int)
		rowHasID := rowID != 0 || row["id"] != nil
		if !conflictingHasID || !rowHasID || conflictingID != rowID {
			delete(conflicting, "key")
			changed = true
		}
	}
	row["key"] = newKey
	rowByKey[newKey] = row
	if oldKey != "" && oldKey != newKey {
		retiredKey = oldKey
		if lockedID, ok := lockData[oldKey]; ok && lockedID == rowID {
			delete(lockData, oldKey)
		}
		changed = true
	}
	if existingID, ok := lockData[newKey]; ok {
		if existingID != rowID {
			if conflictingID >= 0 && existingID == conflictingID {
				lockData[newKey] = rowID
				changed = true
			} else {
				return false, "", fmt.Errorf("dataset %s: key %q is locked to id %d and cannot be rewritten to id %d", datasetName, newKey, existingID, rowID)
			}
		}
	} else {
		lockData[newKey] = rowID
		changed = true
	}
	return changed, retiredKey, nil
}

func normalizeCollectedSpellImpactScripts(rows []map[string]any, baseRowKeys map[string]struct{}) {
	for _, row := range rows {
		rowKey, ok := row["key"].(string)
		if !ok || rowKey == "" {
			continue
		}
		if _, ok := baseRowKeys[rowKey]; ok {
			continue
		}
		rowID, ok := row["id"].(int)
		if !ok {
			continue
		}
		row["ImpactScript"] = fmt.Sprintf("ss_%d", rowID)
	}
}

func write2DA(data map[string]any, path string, denseRows bool) error {
	columns, ok := data["columns"].([]string)
	if !ok {
		return fmt.Errorf("2da columns must be []string")
	}
	rows, ok := data["rows"].([]map[string]any)
	if !ok {
		return fmt.Errorf("2da rows must be []map[string]any")
	}

	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("create 2da output parent: %w", err)
	}

	var builder strings.Builder
	builder.WriteString("2DA V2.0\n\n")
	builder.WriteString(strings.Join(columns, "\t"))
	builder.WriteByte('\n')
	nextID := 0
	for _, row := range rows {
		rowID := row["id"].(int)
		if denseRows {
			for nextID < rowID {
				builder.WriteString(strconv.Itoa(nextID))
				for range columns {
					builder.WriteByte('\t')
					builder.WriteString(nullValue)
				}
				builder.WriteByte('\n')
				nextID++
			}
		}
		builder.WriteString(strconv.Itoa(rowID))
		for _, column := range columns {
			builder.WriteByte('\t')
			builder.WriteString(format2DAValue(row[column]))
		}
		builder.WriteByte('\n')
		if denseRows {
			nextID = rowID + 1
		}
	}
	return os.WriteFile(path, []byte(builder.String()), 0o644)
}

func format2DAValue(value any) string {
	switch typed := value.(type) {
	case nil:
		return nullValue
	case string:
		if typed == nullValue {
			return typed
		}
		if strings.ContainsAny(typed, " \t\r\n") {
			return strconv.Quote(typed)
		}
		return typed
	case int:
		return strconv.Itoa(typed)
	case float64:
		if typed == float64(int64(typed)) {
			return strconv.FormatInt(int64(typed), 10)
		}
		return strconv.FormatFloat(typed, 'f', -1, 64)
	case bool:
		if typed {
			return "1"
		}
		return "0"
	default:
		raw, err := json.Marshal(typed)
		if err != nil {
			return fmt.Sprint(typed)
		}
		text := string(raw)
		if strings.ContainsAny(text, " \t\r\n") {
			return strconv.Quote(text)
		}
		return text
	}
}

func outputStem(outputName string) string {
	return strings.TrimSuffix(outputName, filepath.Ext(outputName))
}

func nativeDatasetDefaultOutputName(path string, kind nativeDatasetKind) string {
	name := filepath.Base(path)
	if kind == nativeDatasetPlain {
		name = strings.TrimSuffix(name, filepath.Ext(name))
	}
	return name + ".2da"
}

func loadJSONObject(path string) (map[string]any, error) {
	raw, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read %s: %w", path, err)
	}
	var payload any
	if err := json.Unmarshal(raw, &payload); err != nil {
		return nil, fmt.Errorf("parse %s: %w", path, err)
	}
	obj, ok := payload.(map[string]any)
	if !ok {
		return nil, fmt.Errorf("%s must contain a JSON object", path)
	}
	return obj, nil
}

func loadLockfile(path string) (map[string]int, error) {
	lockData := map[string]int{}
	if _, err := os.Stat(path); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return lockData, nil
		}
		return nil, err
	}

	obj, err := loadJSONObject(path)
	if err != nil {
		return nil, err
	}
	for key, value := range obj {
		rowID, err := asInt(value)
		if err != nil {
			return nil, fmt.Errorf("lock entry %q is not numeric", key)
		}
		lockData[key] = rowID
	}
	return lockData, nil
}

func saveLockfile(path string, lockData map[string]int) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("create lockfile parent: %w", err)
	}
	keys, err := orderedLockfileKeys(path, lockData)
	if err != nil {
		return fmt.Errorf("read lockfile order: %w", err)
	}
	raw, err := marshalOrderedLockfile(keys, lockData)
	if err != nil {
		return fmt.Errorf("marshal lockfile: %w", err)
	}
	current, err := os.ReadFile(path)
	if err == nil && bytes.Equal(current, raw) {
		return nil
	}
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("read lockfile %s: %w", path, err)
	}
	if err := os.WriteFile(path, raw, 0o644); err != nil {
		return fmt.Errorf("write lockfile %s: %w", path, err)
	}
	return nil
}

func orderedLockfileKeys(path string, lockData map[string]int) ([]string, error) {
	existingKeys, err := readLockfileKeyOrder(path)
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return nil, err
	}

	keys := make([]string, 0, len(lockData))
	seen := make(map[string]struct{}, len(lockData))
	for _, key := range existingKeys {
		if _, ok := lockData[key]; !ok {
			continue
		}
		keys = append(keys, key)
		seen[key] = struct{}{}
	}

	extraKeys := make([]string, 0, len(lockData)-len(keys))
	for key := range lockData {
		if _, ok := seen[key]; ok {
			continue
		}
		extraKeys = append(extraKeys, key)
	}
	slices.Sort(extraKeys)
	keys = append(keys, extraKeys...)
	return keys, nil
}

func readLockfileKeyOrder(path string) ([]string, error) {
	raw, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	decoder := json.NewDecoder(bytes.NewReader(raw))
	token, err := decoder.Token()
	if err != nil {
		return nil, err
	}
	delim, ok := token.(json.Delim)
	if !ok || delim != '{' {
		return nil, fmt.Errorf("%s must contain a JSON object", path)
	}

	keys := []string{}
	for decoder.More() {
		token, err := decoder.Token()
		if err != nil {
			return nil, err
		}
		key, ok := token.(string)
		if !ok {
			return nil, fmt.Errorf("%s contains a non-string object key", path)
		}
		var value json.RawMessage
		if err := decoder.Decode(&value); err != nil {
			return nil, err
		}
		keys = append(keys, key)
	}

	token, err = decoder.Token()
	if err != nil {
		return nil, err
	}
	delim, ok = token.(json.Delim)
	if !ok || delim != '}' {
		return nil, fmt.Errorf("%s must contain a JSON object", path)
	}
	return keys, nil
}

func marshalOrderedLockfile(keys []string, lockData map[string]int) ([]byte, error) {
	if len(keys) == 0 {
		return []byte("{}\n"), nil
	}

	var buffer bytes.Buffer
	buffer.WriteString("{\n")
	for index, key := range keys {
		keyRaw, err := json.Marshal(key)
		if err != nil {
			return nil, err
		}
		valueRaw, err := json.Marshal(lockData[key])
		if err != nil {
			return nil, err
		}
		buffer.WriteString("  ")
		buffer.Write(keyRaw)
		buffer.WriteString(": ")
		buffer.Write(valueRaw)
		if index < len(keys)-1 {
			buffer.WriteByte(',')
		}
		buffer.WriteByte('\n')
	}
	buffer.WriteString("}\n")
	return buffer.Bytes(), nil
}

func loadLegacyTLK(root string) (*legacyTLKData, error) {
	info, err := os.Stat(root)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	if !info.IsDir() {
		return nil, fmt.Errorf("legacy tlk root must be a directory: %s", root)
	}

	result := &legacyTLKData{
		Language: "en",
		Entries:  map[string]tlkEntryData{},
		Lock:     map[string]int{},
	}

	lockPath := filepath.Join(root, "lock.json")
	lockData, err := loadLockfile(lockPath)
	if err != nil && !os.IsNotExist(err) {
		return nil, err
	}
	for key, id := range lockData {
		result.Lock[key] = id
	}

	basePath := filepath.Join(root, "base.json")
	if _, err := os.Stat(basePath); err == nil {
		base, err := loadJSONObject(basePath)
		if err != nil {
			return nil, err
		}
		if language, ok := base["language"].(string); ok && language != "" {
			result.Language = language
		}
		if entries, ok := base["entries"].(map[string]any); ok {
			normalized, err := normalizeLegacyTLKEntries(entries)
			if err != nil {
				return nil, err
			}
			for key, entry := range normalized {
				result.Entries[key] = entry
			}
		}
	}

	modulesDir := filepath.Join(root, "modules")
	modulePaths, err := collectModulePaths(modulesDir)
	if err != nil {
		if os.IsNotExist(err) {
			return result, nil
		}
		return nil, err
	}
	for _, path := range modulePaths {
		obj, err := loadJSONObject(path)
		if err != nil {
			return nil, err
		}
		entries, ok := obj["entries"].(map[string]any)
		if !ok {
			continue
		}
		normalized, err := normalizeLegacyTLKEntries(entries)
		if err != nil {
			return nil, fmt.Errorf("%s: %w", path, err)
		}
		for key, entry := range normalized {
			result.Entries[key] = entry
		}
	}
	return result, nil
}

func normalizeLegacyTLKEntries(entries map[string]any) (map[string]tlkEntryData, error) {
	normalized := map[string]tlkEntryData{}
	for _, key := range sortedKeys(entries) {
		value := entries[key]
		switch typed := value.(type) {
		case string:
			normalized[key] = tlkEntryData{Text: typed}
		case map[string]any:
			if shouldExpandLegacyKey(key, typed) {
				for _, subKey := range sortedKeys(typed) {
					subObj, ok := typed[subKey].(map[string]any)
					if !ok {
						continue
					}
					entry, err := parseLegacyTLKLeaf(subObj)
					if err != nil {
						return nil, err
					}
					normalized[key+"."+subKey] = entry
				}
				continue
			}
			entry, err := parseLegacyTLKLeaf(typed)
			if err != nil {
				return nil, err
			}
			normalized[key] = entry
		default:
			return nil, fmt.Errorf("legacy TLK entry %q must be an object or string", key)
		}
	}
	return normalized, nil
}

func shouldExpandLegacyKey(key string, obj map[string]any) bool {
	if strings.Contains(key[strings.LastIndex(key, ":")+1:], ".") {
		return false
	}
	for _, value := range obj {
		if _, ok := value.(map[string]any); ok {
			return true
		}
	}
	return false
}

func parseLegacyTLKLeaf(obj map[string]any) (tlkEntryData, error) {
	entry := tlkEntryData{}
	if rawText, ok := obj["text"]; ok {
		text, ok := rawText.(string)
		if !ok {
			return entry, fmt.Errorf("legacy TLK text must be a string")
		}
		entry.Text = text
	}
	if rawSound, ok := obj["sound_resref"]; ok {
		sound, ok := rawSound.(string)
		if !ok {
			return entry, fmt.Errorf("legacy TLK sound_resref must be a string")
		}
		entry.SoundResRef = sound
	}
	if rawLength, ok := obj["sound_length"]; ok {
		switch typed := rawLength.(type) {
		case float64:
			entry.SoundLength = float32(typed)
		case int:
			entry.SoundLength = float32(typed)
		default:
			return entry, fmt.Errorf("legacy TLK sound_length must be numeric")
		}
	}
	return entry, nil
}

func collectModulePaths(modulesDir string) ([]string, error) {
	if _, err := os.Stat(modulesDir); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, err
	}
	var paths []string
	err := filepath.WalkDir(modulesDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		if strings.HasSuffix(strings.ToLower(d.Name()), ".json") {
			paths = append(paths, path)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	slices.Sort(paths)
	return paths, nil
}

func canonicalColumn(columns []string, name string) (string, bool) {
	for _, column := range columns {
		if strings.EqualFold(column, name) {
			return column, true
		}
	}
	return "", false
}

func optionalBool(value any, fallback bool) bool {
	switch typed := value.(type) {
	case bool:
		return typed
	case nil:
		return fallback
	default:
		return fallback
	}
}

func asInt(value any) (int, error) {
	switch typed := value.(type) {
	case int:
		return typed, nil
	case int64:
		return int(typed), nil
	case float64:
		return int(typed), nil
	case json.Number:
		n, err := typed.Int64()
		return int(n), err
	case string:
		return strconv.Atoi(typed)
	default:
		return 0, fmt.Errorf("not an int")
	}
}

func nextAvailableID(used map[int]struct{}) int {
	next := 0
	for {
		if _, ok := used[next]; !ok {
			return next
		}
		next++
	}
}

func hasJSONFiles(root string) (bool, error) {
	found := false
	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			return nil
		}
		if strings.HasSuffix(strings.ToLower(d.Name()), ".json") {
			found = true
			return fs.SkipAll
		}
		return nil
	})
	if err != nil && !errors.Is(err, fs.SkipAll) {
		return false, err
	}
	return found, nil
}

func collectReferenceLockIDs(root string) (map[string]int, error) {
	result := map[string]int{}
	if _, err := os.Stat(root); err != nil {
		if os.IsNotExist(err) {
			return result, nil
		}
		return nil, err
	}
	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() || d.Name() != "lock.json" {
			return nil
		}
		lockData, err := loadLockfile(path)
		if err != nil {
			return err
		}
		for key, rowID := range lockData {
			if _, exists := result[key]; !exists {
				result[key] = rowID
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return result, nil
}

func collectSupplementalLockIDs(sourceDir, referenceBuilderDir string) (map[string]int, error) {
	result := map[string]int{}
	snapshotIDs, err := collectReferenceLockIDs(filepath.Join(sourceDir, "migration_snapshot", "data"))
	if err != nil {
		return nil, err
	}
	for key, rowID := range snapshotIDs {
		result[key] = rowID
	}
	if strings.TrimSpace(referenceBuilderDir) == "" {
		return result, nil
	}
	referenceIDs, err := collectReferenceLockIDs(filepath.Join(referenceBuilderDir, "data"))
	if err != nil {
		return nil, err
	}
	for key, rowID := range referenceIDs {
		if _, ok := result[key]; ok {
			continue
		}
		result[key] = rowID
	}
	return result, nil
}
