package topdata

import (
	"encoding/json"
	"os"
	"path/filepath"
	"slices"
	"strings"
)

var legacyClassesPlainFamilies = []string{"feats", "skills", "savthr", "bfeat", "pres"}

func importLegacyClasses(referenceBuilderDir, dataDir string, legacyTLK *legacyTLKData) (int, error) {
	legacyRoot := filepath.Join(referenceBuilderDir, "data", "classes")
	if _, err := os.Stat(legacyRoot); err != nil {
		if os.IsNotExist(err) {
			return 0, nil
		}
		return 0, err
	}

	targetRoot := filepath.Join(dataDir, "classes")
	if canonicalClassesPresent(targetRoot) {
		return 0, nil
	}

	coreCollected, plainCollected, err := collectLegacyClassesDatasets(legacyRoot)
	if err != nil {
		return 0, err
	}

	tableKeyByOutput := map[string]string{}
	for _, dataset := range plainCollected {
		if dataset.TableKey == "" {
			continue
		}
		registerLegacyClassesTableKey(tableKeyByOutput, dataset.TableKey, dataset.Dataset.OutputName)
	}

	coreRows := make([]map[string]any, 0, len(coreCollected.Rows))
	for _, rawRow := range coreCollected.Rows {
		row, ok := deepCopyValue(rawRow).(map[string]any)
		if !ok {
			continue
		}
		if legacyTLK != nil {
			if _, _, err := inlineLegacyTLKValue(row, legacyTLK); err != nil {
				return 0, err
			}
		}
		rewriteLegacyClassesCoreRow(row, tableKeyByOutput)
		coreRows = append(coreRows, row)
	}

	if err := removePathIfExists(targetRoot); err != nil {
		return 0, err
	}
	if err := os.MkdirAll(filepath.Join(targetRoot, "core"), 0o755); err != nil {
		return 0, err
	}

	writes := []struct {
		path string
		obj  map[string]any
	}{
		{
			path: filepath.Join(targetRoot, "core", "base.json"),
			obj: map[string]any{
				"output":  coreCollected.Dataset.OutputName,
				"columns": stringSliceToAny(coreCollected.Columns),
				"rows":    rowsToAny(coreRows),
			},
		},
		{
			path: filepath.Join(targetRoot, "core", "lock.json"),
			obj:  anyMapInt(coreCollected.LockData),
		},
	}

	for _, collected := range plainCollected {
		obj := map[string]any{
			"output":  collected.Dataset.OutputName,
			"columns": stringSliceToAny(collected.Columns),
			"rows":    rowsToAny(collected.Rows),
		}
		if strings.TrimSpace(collected.TableKey) != "" {
			obj["key"] = collected.TableKey
		}
		writes = append(writes, struct {
			path string
			obj  map[string]any
		}{
			path: filepath.Join(dataDir, collected.Dataset.Name+".json"),
			obj:  obj,
		})
	}

	for _, write := range writes {
		if err := os.MkdirAll(filepath.Dir(write.path), 0o755); err != nil {
			return 0, err
		}
		raw, err := json.MarshalIndent(write.obj, "", "  ")
		if err != nil {
			return 0, err
		}
		raw = append(raw, '\n')
		if err := os.WriteFile(write.path, raw, 0o644); err != nil {
			return 0, err
		}
	}

	return len(writes), nil
}

func canonicalClassesPresent(targetRoot string) bool {
	if !fileExists(filepath.Join(targetRoot, "core", "base.json")) || !fileExists(filepath.Join(targetRoot, "core", "lock.json")) {
		return false
	}
	for _, family := range legacyClassesPlainFamilies {
		ok, err := hasJSONFiles(filepath.Join(targetRoot, family))
		if err != nil || !ok {
			return false
		}
	}
	return true
}

func collectLegacyClassesDatasets(legacyRoot string) (nativeCollectedDataset, []nativeCollectedDataset, error) {
	coreCollected, err := collectBaseDataset(nativeDataset{
		Name:       "classes/core",
		BasePath:   filepath.Join(legacyRoot, "core", "base.json"),
		LockPath:   filepath.Join(legacyRoot, "core", "lock.json"),
		ModulesDir: filepath.Join(legacyRoot, "core", "modules"),
		OutputName: "classes.2da",
		Spec:       specForDataset("classes"),
	})
	if err != nil {
		return nativeCollectedDataset{}, nil, err
	}
	coreCollected.Dataset.OutputName = "classes.2da"

	plainCollected := make([]nativeCollectedDataset, 0)
	for _, family := range legacyClassesPlainFamilies {
		familyDir := filepath.Join(legacyRoot, family)
		entries, err := os.ReadDir(familyDir)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return nativeCollectedDataset{}, nil, err
		}
		for _, entry := range entries {
			if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" || strings.HasPrefix(entry.Name(), ".") || entry.Name() == "lock.json" {
				continue
			}
			filePath := filepath.Join(familyDir, entry.Name())
			tableData, err := loadJSONObject(filePath)
			if err != nil {
				return nativeCollectedDataset{}, nil, err
			}
			outputName, _ := tableData["output"].(string)
			if strings.TrimSpace(outputName) == "" {
				outputName = strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())) + ".2da"
			}
			collected, err := collectPlainDataset(nativeDataset{
				Name:       filepath.ToSlash(filepath.Join("classes", family, strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())))),
				BasePath:   filePath,
				LockPath:   filepath.Join(familyDir, "lock.json"),
				OutputName: outputName,
				Spec:       specForDataset(filepath.ToSlash(filepath.Join("classes", family, strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))))),
			})
			if err != nil {
				return nativeCollectedDataset{}, nil, err
			}
			plainCollected = append(plainCollected, collected)
		}
	}

	slices.SortFunc(plainCollected, func(a, b nativeCollectedDataset) int {
		return strings.Compare(a.Dataset.Name, b.Dataset.Name)
	})
	return coreCollected, plainCollected, nil
}

func registerLegacyClassesTableKey(tableKeyByOutput map[string]string, tableKey, outputName string) {
	trimmedOutput := strings.TrimSpace(outputName)
	if trimmedOutput != "" {
		tableKeyByOutput[trimmedOutput] = tableKey
	}
	trimmedStem := strings.TrimSpace(outputStem(outputName))
	if trimmedStem != "" {
		tableKeyByOutput[trimmedStem] = tableKey
	}
}

func rewriteLegacyClassesCoreRow(row map[string]any, tableKeyByOutput map[string]string) {
	for _, field := range []string{"FeatsTable", "SavingThrowTable", "SkillsTable", "BonusFeatsTable", "PreReqTable"} {
		tableKey := legacyClassesTableKeyForValue(row[field], tableKeyByOutput)
		if tableKey == "" {
			continue
		}
		row[field] = map[string]any{"table": tableKey}
	}
}

func legacyClassesTableKeyForValue(value any, tableKeyByOutput map[string]string) string {
	switch typed := value.(type) {
	case map[string]any:
		tableKey, _ := typed["table"].(string)
		return strings.TrimSpace(tableKey)
	case string:
		trimmed := strings.TrimSpace(typed)
		if trimmed == "" || trimmed == nullValue {
			return ""
		}
		if trimmed != strings.ToLower(trimmed) {
			return ""
		}
		if tableKey, ok := tableKeyByOutput[trimmed]; ok {
			return tableKey
		}
		return ""
	default:
		return ""
	}
}
