package topdata

import (
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"
)

const (
	damagetypesRegistryDirName = "damagetypes/registry"
	damagetypesRegistryLock    = "lock.json"
)

type damagetypesRegistry struct {
	RootPath string
	LockPath string
	LockData map[string]int
	Types    []map[string]any
}

func collectGeneratedRegistryDatasets(dataDir string) ([]nativeCollectedDataset, error) {
	itempropsDatasets, err := collectItempropsRegistryDatasets(dataDir)
	if err != nil {
		return nil, err
	}
	damagetypeDatasets, err := collectDamagetypesRegistryDatasets(dataDir)
	if err != nil {
		return nil, err
	}
	racialtypesDatasets, err := collectRacialtypesRegistryDatasets(dataDir)
	if err != nil {
		return nil, err
	}
	out := make([]nativeCollectedDataset, 0, len(itempropsDatasets)+len(damagetypeDatasets)+len(racialtypesDatasets))
	out = append(out, itempropsDatasets...)
	out = append(out, damagetypeDatasets...)
	out = append(out, racialtypesDatasets...)
	return out, nil
}

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

	lockData, err := loadLockfile(filepath.Join(root, damagetypesRegistryLock))
	if err != nil {
		return nil, err
	}
	types, err := loadRegistryRows(filepath.Join(root, "types.json"))
	if err != nil {
		return nil, err
	}
	return &damagetypesRegistry{
		RootPath: root,
		LockPath: filepath.Join(root, damagetypesRegistryLock),
		LockData: lockData,
		Types:    types,
	}, nil
}

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

	typeIDs, lockModified, err := assignDamagetypeRegistryIDs(registry.Types, registry.LockData)
	if err != nil {
		return nil, err
	}
	if lockModified {
		if err := saveLockfile(registry.LockPath, registry.LockData); err != nil {
			return nil, err
		}
	}

	typeRowsSorted := slices.Clone(registry.Types)
	slices.SortFunc(typeRowsSorted, func(a, b map[string]any) int {
		return typeIDs[stringField(a, "key")] - typeIDs[stringField(b, "key")]
	})

	coreRows := make([]map[string]any, 0, len(typeRowsSorted))
	hitvisualRows := make([]map[string]any, 0, len(typeRowsSorted))
	groupRowsByID := map[int]map[string]any{}
	groupOrder := make([]int, 0)
	for _, row := range typeRowsSorted {
		key := stringField(row, "key")
		rowID := typeIDs[key]

		groupID, err := registryIntField(row, "damage_type_group")
		if err != nil {
			return nil, fmt.Errorf("%s: %w", key, err)
		}
		coreRows = append(coreRows, map[string]any{
			"id":                     rowID,
			"key":                    key,
			"Label":                  registryStringField(row, "label"),
			"CharsheetStrref":        deepCopyValue(row["charsheet_strref"]),
			"DamageTypeGroup":        strconv.Itoa(groupID),
			"DamageRangedProjectile": registryNullableField(row, "damage_ranged_projectile"),
		})
		hitvisualRows = append(hitvisualRows, map[string]any{
			"id":             rowID,
			"key":            strings.Replace(key, "damagetype:", "damagehitvisual:", 1),
			"Label":          registryStringField(row, "label"),
			"VisualEffectID": registryNullableField(row, "visual_effect_id"),
			"RangedEffectID": registryNullableField(row, "ranged_effect_id"),
		})

		if _, ok := groupRowsByID[groupID]; !ok {
			groupRowsByID[groupID] = map[string]any{
				"id":             groupID,
				"key":            strings.Replace(key, "damagetype:", "damagetypegroup:", 1),
				"Label":          registryStringField(row, "group_label"),
				"FeedbackStrref": deepCopyValue(row["feedback_strref"]),
				"ColorR":         registryNullableField(row, "color_r"),
				"ColorG":         registryNullableField(row, "color_g"),
				"ColorB":         registryNullableField(row, "color_b"),
			}
			groupOrder = append(groupOrder, groupID)
			continue
		}
		existing := groupRowsByID[groupID]
		for field, candidate := range map[string]any{
			"Label":          registryStringField(row, "group_label"),
			"FeedbackStrref": deepCopyValue(row["feedback_strref"]),
			"ColorR":         registryNullableField(row, "color_r"),
			"ColorG":         registryNullableField(row, "color_g"),
			"ColorB":         registryNullableField(row, "color_b"),
		} {
			if format2DAValue(existing[field]) != format2DAValue(candidate) {
				return nil, fmt.Errorf("%s: conflicting group projection for DamageTypeGroup %d field %s", key, groupID, field)
			}
		}
	}
	slices.Sort(groupOrder)
	groupRows := make([]map[string]any, 0, len(groupOrder))
	for _, id := range groupOrder {
		groupRows = append(groupRows, groupRowsByID[id])
	}

	return []nativeCollectedDataset{
		newGeneratedDataset("damagetypes/registry/core", "damagetypes.2da", []string{"Label", "CharsheetStrref", "DamageTypeGroup", "DamageRangedProjectile"}, coreRows),
		newGeneratedDataset("damagetypes/registry/groups", "damagetypegroups.2da", []string{"Label", "FeedbackStrref", "ColorR", "ColorG", "ColorB"}, groupRows),
		newGeneratedDataset("damagetypes/registry/hitvisual", "damagehitvisual.2da", []string{"Label", "VisualEffectID", "RangedEffectID"}, hitvisualRows),
	}, nil
}

func assignDamagetypeRegistryIDs(rows []map[string]any, lockData map[string]int) (map[string]int, bool, error) {
	ids := map[string]int{}
	used := map[int]struct{}{}
	for key, id := range lockData {
		if strings.HasPrefix(key, "damagetype:") {
			used[id] = struct{}{}
		}
	}
	modified := false
	for _, row := range rows {
		key := stringField(row, "key")
		if key == "" {
			return nil, false, fmt.Errorf("registry row is missing key")
		}
		if !strings.HasPrefix(key, "damagetype:") {
			return nil, false, fmt.Errorf("registry key %q must use prefix %q", key, "damagetype:")
		}
		if rawID, ok := row["id"]; ok {
			id, err := asInt(rawID)
			if err != nil {
				return nil, false, fmt.Errorf("%s: invalid id: %w", key, err)
			}
			if existing, ok := lockData[key]; ok && existing != id {
				return nil, false, fmt.Errorf("%s: explicit id %d does not match lock id %d", key, id, existing)
			}
			lockData[key] = id
			ids[key] = id
			used[id] = struct{}{}
			modified = true
			continue
		}
		if id, ok := lockData[key]; ok {
			ids[key] = id
			used[id] = struct{}{}
		}
	}
	nextID := nextAvailableID(used)
	for _, row := range rows {
		key := stringField(row, "key")
		if _, ok := ids[key]; ok {
			continue
		}
		lockData[key] = nextID
		ids[key] = nextID
		used[nextID] = struct{}{}
		nextID = nextAvailableID(used)
		modified = true
	}
	return ids, modified, nil
}

func registryStringField(row map[string]any, field string) string {
	text := stringField(row, field)
	if text == "" {
		return nullValue
	}
	return text
}

func registryNullableField(row map[string]any, field string) any {
	value, ok := row[field]
	if !ok {
		return nullValue
	}
	switch typed := value.(type) {
	case string:
		if strings.TrimSpace(typed) == "" {
			return nullValue
		}
	}
	return deepCopyValue(value)
}

func registryIntField(row map[string]any, field string) (int, error) {
	value, ok := row[field]
	if !ok {
		return 0, fmt.Errorf("missing %s", field)
	}
	return asInt(value)
}
