package topdata

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"strings"
)

const (
	racialtypesRegistryDirName = "racialtypes/registry"
	racialtypesRegistryLock    = "lock.json"
)

type racialtypesRegistry struct {
	RootPath    string
	LockPath    string
	LockData    map[string]int
	BaseColumns []string
	BaseRows    []map[string]any
	Races       []map[string]any
}

func loadRacialtypesRegistry(dataDir string) (*racialtypesRegistry, error) {
	root := filepath.Join(dataDir, filepath.FromSlash(racialtypesRegistryDirName))
	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("racialtypes registry root must be a directory: %s", root)
	}

	lockPath := filepath.Join(root, racialtypesRegistryLock)
	lockData, err := loadLockfile(lockPath)
	if err != nil {
		return nil, err
	}

	basePath := filepath.Join(root, "base.json")
	baseObj, err := loadJSONObject(basePath)
	if err != nil {
		return nil, err
	}
	baseColumns, err := parseColumns(baseObj, "racialtypes/registry/core")
	if err != nil {
		return nil, err
	}
	rawBaseRows, ok := baseObj["rows"].([]any)
	if !ok {
		return nil, fmt.Errorf("%s: rows must be an array", basePath)
	}
	baseRows := make([]map[string]any, 0, len(rawBaseRows))
	for index, raw := range rawBaseRows {
		row, ok := raw.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("%s: row %d must be an object", basePath, index)
		}
		baseRows = append(baseRows, row)
	}

	raceRows, err := loadRacialtypesRegistryRaceRows(filepath.Join(root, "races"))
	if err != nil {
		return nil, err
	}

	return &racialtypesRegistry{
		RootPath:    root,
		LockPath:    lockPath,
		LockData:    lockData,
		BaseColumns: baseColumns,
		BaseRows:    baseRows,
		Races:       raceRows,
	}, nil
}

func loadRacialtypesRegistryRaceRows(dir string) ([]map[string]any, error) {
	info, err := os.Stat(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	if !info.IsDir() {
		return nil, fmt.Errorf("racialtypes registry races path must be a directory: %s", dir)
	}

	paths := make([]string, 0)
	err = filepath.WalkDir(dir, 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)

	rows := make([]map[string]any, 0, len(paths))
	for _, path := range paths {
		obj, err := loadJSONObject(path)
		if err != nil {
			return nil, err
		}
		rows = append(rows, obj)
	}
	return rows, nil
}

func collectRacialtypesRegistryDatasets(dataDir string) ([]nativeCollectedDataset, error) {
	registry, err := loadRacialtypesRegistry(dataDir)
	if err != nil {
		return nil, err
	}
	if registry == nil {
		return nil, nil
	}

	lockModified, err := syncRacialtypesBaseLockData(registry.BaseRows, registry.LockData)
	if err != nil {
		return nil, err
	}
	raceIDs, raceLockModified, err := assignRegistryIDs(registry.Races, registry.LockData, "racialtypes:")
	if err != nil {
		return nil, err
	}
	lockModified = lockModified || raceLockModified
	if lockModified {
		if err := saveLockfile(registry.LockPath, registry.LockData); err != nil {
			return nil, err
		}
	}

	coreDataset := nativeDataset{
		Kind:       nativeDatasetBase,
		Name:       "racialtypes/registry/core",
		OutputName: "racialtypes.2da",
		Spec:       specForDataset("racialtypes"),
	}
	featDataset := nativeDataset{
		Kind: nativeDatasetPlain,
		Name: "racialtypes/registry/feats",
		Spec: specForDataset("racialtypes"),
	}

	coreRows := make([]map[string]any, 0, len(registry.BaseRows)+len(registry.Races))
	rowIDSeen := map[int]string{}
	for index, raw := range registry.BaseRows {
		row, err := canonicalizeBaseRow(coreDataset, registry.BaseColumns, raw, index)
		if err != nil {
			return nil, fmt.Errorf("racialtypes registry base row %d: %w", index, err)
		}
		rowID := row["id"].(int)
		if existing, ok := rowIDSeen[rowID]; ok {
			return nil, fmt.Errorf("racialtypes registry base row id %d conflicts with %s", rowID, existing)
		}
		rowIDSeen[rowID] = "base"
		coreRows = append(coreRows, row)
	}

	datasets := make([]nativeCollectedDataset, 0, 16)
	for _, race := range registry.Races {
		key := stringField(race, "key")
		if key == "" {
			return nil, fmt.Errorf("racialtypes registry row is missing key")
		}
		rowID, ok := raceIDs[key]
		if !ok {
			return nil, fmt.Errorf("racialtypes registry row %s is missing lock id", key)
		}
		if existing, ok := rowIDSeen[rowID]; ok {
			return nil, fmt.Errorf("racialtypes registry row id %d for %s conflicts with %s", rowID, key, existing)
		}
		rowIDSeen[rowID] = key

		coreRaw, ok := race["core"].(map[string]any)
		if !ok {
			return nil, fmt.Errorf("%s: core must be an object", key)
		}
		row, err := canonicalizeEntry(coreDataset, registry.BaseColumns, rowID, key, coreRaw)
		if err != nil {
			return nil, fmt.Errorf("%s: %w", key, err)
		}

		featOutput, _ := race["feat_output"].(string)
		if featOutput != "" {
			row["FeatsTable"] = outputStem(featOutput)
		}
		coreRows = append(coreRows, row)

		if featOutput == "" {
			continue
		}
		rawFeats, ok := race["feats"].([]any)
		if !ok {
			return nil, fmt.Errorf("%s: feats must be an array when feat_output is set", key)
		}
		featRows := make([]map[string]any, 0, len(rawFeats))
		for index, rawFeat := range rawFeats {
			featRow, ok := rawFeat.(map[string]any)
			if !ok {
				return nil, fmt.Errorf("%s: feat row %d must be an object", key, index)
			}
			row, err := canonicalizeBaseRow(featDataset, []string{"FeatLabel", "FeatIndex"}, featRow, index)
			if err != nil {
				return nil, fmt.Errorf("%s: feat row %d: %w", key, index, err)
			}
			featRows = append(featRows, row)
		}
		datasets = append(datasets, newGeneratedDataset(
			"racialtypes/registry/"+strings.TrimPrefix(key, "racialtypes:"),
			featOutput,
			[]string{"FeatLabel", "FeatIndex"},
			featRows,
		))
	}

	slices.SortFunc(coreRows, func(a, b map[string]any) int {
		return a["id"].(int) - b["id"].(int)
	})
	slices.SortFunc(datasets, func(a, b nativeCollectedDataset) int {
		return strings.Compare(a.Dataset.OutputName, b.Dataset.OutputName)
	})

	coreCollected := newGeneratedDataset("racialtypes/registry/core", "racialtypes.2da", registry.BaseColumns, coreRows)
	out := make([]nativeCollectedDataset, 0, len(datasets)+1)
	out = append(out, coreCollected)
	out = append(out, datasets...)
	return out, nil
}

func syncRacialtypesBaseLockData(rows []map[string]any, lockData map[string]int) (bool, error) {
	modified := false
	for _, raw := range rows {
		key := stringField(raw, "key")
		if key == "" {
			continue
		}
		rowID, ok := raw["id"]
		if !ok {
			return false, fmt.Errorf("racialtypes registry base row %s is missing id", key)
		}
		id, err := asInt(rowID)
		if err != nil {
			return false, fmt.Errorf("racialtypes registry base row %s has invalid id: %w", key, err)
		}
		if existing, ok := lockData[key]; ok {
			if existing != id {
				return false, fmt.Errorf("racialtypes registry base row %s id %d does not match lock id %d", key, id, existing)
			}
			continue
		}
		lockData[key] = id
		modified = true
	}
	return modified, nil
}
