#include "CharacterSlotSystem.hpp"
#include "CharacterRegistry.hpp"
#include "PrefabSystem.hpp"
#include "EditorUISystem.hpp"
#include "../components/CharacterIdentity.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/CharacterClassDatabase.hpp"
#include "../components/Transform.hpp"
#include "../components/EntityName.hpp"
#include "../components/EditorMarker.hpp"
#include "../components/RuntimeMarker.hpp"
#include <OgreLogManager.h>
#include <OgreSceneManager.h>
#include <fstream>
#include <imgui.h>
#include <algorithm>
#include <filesystem>

/* ===================================================================== */
/*  Singleton                                                            */
/* ===================================================================== */

CharacterRegistry *CharacterRegistry::ms_singleton = nullptr;

CharacterRegistry &CharacterRegistry::getSingleton()
{
	OgreAssert(ms_singleton, "CharacterRegistry not created");
	return *ms_singleton;
}

CharacterRegistry *CharacterRegistry::getSingletonPtr()
{
	return ms_singleton;
}

/* ===================================================================== */
/*  Construction / init                                                  */
/* ===================================================================== */

CharacterRegistry::CharacterRegistry()
{
	ms_singleton = this;
	m_autoSavePath = "character_registry.json";
}

void CharacterRegistry::initialize()
{
	if (!std::filesystem::exists(m_autoSavePath))
		return;
	if (!loadFromFile(m_autoSavePath)) {
		Ogre::LogManager::getSingleton().logMessage(
			"CharacterRegistry: auto-load failed: " + m_lastError);
	}
	learnNamesFromRegistry();
	scanTemplates();
}

void CharacterRegistry::autoSave()
{
	if (!m_autoSavePath.empty())
		saveToFile(m_autoSavePath);
}

std::string CharacterRegistry::generatePrefabPath(uint64_t id,
						  const std::string &,
						  const std::string &) const
{
	return "prefabs/char_" + std::to_string(id) + ".json";
}

/* ===================================================================== */
/*  Prefab helpers                                                       */
/* ===================================================================== */

bool CharacterRegistry::isValidCharacterPrefab(const std::string &filepath)
{
	try {
		std::ifstream file(filepath);
		if (!file.is_open())
			return false;
		nlohmann::json j;
		file >> j;
		return j.contains("characterSlots");
	} catch (...) {
		return false;
	}
}

bool CharacterRegistry::readPrefabSlots(const std::string &filepath,
					std::string &age, std::string &sex,
					int &outfitLevel)
{
	try {
		std::ifstream file(filepath);
		if (!file.is_open())
			return false;
		nlohmann::json j;
		file >> j;
		if (!j.contains("characterSlots"))
			return false;
		auto &cs = j["characterSlots"];
		age = cs.value("age", "adult");
		sex = cs.value("sex", "male");
		outfitLevel = cs.value("outfitLevel", 2);
		return true;
	} catch (...) {
		return false;
	}
}

bool CharacterRegistry::writePrefabSlots(const std::string &filepath,
					 const std::string &age,
					 const std::string &sex,
					 int outfitLevel)
{
	try {
		nlohmann::json j;
		{
			std::ifstream file(filepath);
			if (file.is_open())
				file >> j;
		}
		j["characterSlots"]["age"] = age;
		j["characterSlots"]["sex"] = sex;
		j["characterSlots"]["outfitLevel"] = outfitLevel;
		std::ofstream out(filepath);
		if (!out.is_open())
			return false;
		out << j.dump(4);
		return true;
	} catch (...) {
		return false;
	}
}

bool CharacterRegistry::copyPrefab(const std::string &src,
				   const std::string &dst)
{
	try {
		std::filesystem::path dp(dst);
		std::filesystem::create_directories(dp.parent_path());
		std::filesystem::copy_file(
			src, dst,
			std::filesystem::copy_options::overwrite_existing);
		return true;
	} catch (...) {
		return false;
	}
}

bool CharacterRegistry::deletePrefabFile(const std::string &filepath)
{
	try {
		if (std::filesystem::exists(filepath))
			return std::filesystem::remove(filepath);
		return true;
	} catch (...) {
		return false;
	}
}

/* ===================================================================== */
/*  Templates                                                            */
/* ===================================================================== */

void CharacterRegistry::scanTemplates()
{
	m_templates.clear();
	std::string dir = PrefabSystem::getPrefabsDirectory();
	if (!std::filesystem::exists(dir))
		return;
	for (const auto &entry : std::filesystem::directory_iterator(dir)) {
		if (entry.is_regular_file() &&
		    entry.path().extension() == ".json") {
			std::string p = entry.path().string();
			if (isValidCharacterPrefab(p))
				m_templates.push_back(p);
		}
	}
}

/* ===================================================================== */
/*  Name Generation                                                      */
/* ===================================================================== */

static std::string
getCharacterSexForLearning(const CharacterRegistry::CharacterRecord &c)
{
	if (c.inlineSex == "female")
		return "female";
	/* If inlineSex is default male, try prefab for accuracy */
	if (c.inlineSex == "male" && std::filesystem::exists(c.prefabPath)) {
		std::string age, sex;
		int outfit;
		if (CharacterRegistry::readPrefabSlots(c.prefabPath, age, sex,
						       outfit) &&
		    sex == "female")
			return "female";
	}
	return "male";
}

void CharacterRegistry::rebuildNameGenerators()
{
	m_maleFirstNameGen.clear();
	m_femaleFirstNameGen.clear();
	m_lastNameGen.clear();

	/* Feed seeded names first */
	for (const auto &name : m_maleFirstNameSeeds)
		m_maleFirstNameGen.learn(name);
	for (const auto &name : m_femaleFirstNameSeeds)
		m_femaleFirstNameGen.learn(name);
	for (const auto &name : m_lastNameSeeds)
		m_lastNameGen.learn(name);

	/* Then feed registered character names */
	for (const auto &pair : m_characters) {
		const CharacterRecord &c = pair.second;
		if (!c.firstName.empty()) {
			if (getCharacterSexForLearning(c) == "female")
				m_femaleFirstNameGen.learn(c.firstName);
			else
				m_maleFirstNameGen.learn(c.firstName);
		}
		if (!c.lastName.empty())
			m_lastNameGen.learn(c.lastName);
	}
}

void CharacterRegistry::learnNamesFromRegistry()
{
	rebuildNameGenerators();
}

std::string CharacterRegistry::generateFirstName(const std::string &sex) const
{
	std::unordered_set<std::string> existing;
	for (const auto &pair : m_characters)
		existing.insert(pair.second.firstName);
	if (sex == "female")
		return m_femaleFirstNameGen.generate(3, 12, &existing);
	return m_maleFirstNameGen.generate(3, 12, &existing);
}

std::string CharacterRegistry::generateLastName() const
{
	std::unordered_set<std::string> existing;
	for (const auto &pair : m_characters)
		existing.insert(pair.second.lastName);
	return m_lastNameGen.generate(3, 12, &existing);
}

/* ------------------------------------------------------------------ */
/*  Family / Birth                                                    */
/* ------------------------------------------------------------------ */

std::vector<uint64_t> CharacterRegistry::getParents(uint64_t childId) const
{
	std::vector<uint64_t> result;
	auto range = m_relByTarget.equal_range(childId);
	for (auto it = range.first; it != range.second; ++it) {
		const Relationship &r = m_relationships[it->second];
		if (!r.sourceIsGroup && r.tags.find("parent") != r.tags.end())
			result.push_back(r.sourceId);
	}
	return result;
}

std::vector<uint64_t> CharacterRegistry::getChildren(uint64_t parentId) const
{
	std::vector<uint64_t> result;
	auto range = m_relBySource.equal_range(parentId);
	for (auto it = range.first; it != range.second; ++it) {
		const Relationship &r = m_relationships[it->second];
		if (!r.targetIsGroup && r.tags.find("parent") != r.tags.end())
			result.push_back(r.targetId);
	}
	return result;
}

/* ------------------------------------------------------------------ */
/*  Prefab appearance reader                                          */
/* ------------------------------------------------------------------ */

static bool
readPrefabAppearance(const std::string &path, std::string &outSex,
		     std::unordered_map<std::string, SlotSelection> &outSelections,
		     std::unordered_map<std::string, float> &shapeKeys,
		     std::string &outAge,
		     Ogre::Vector3 &outFrontAxis)
{
	try {
		std::ifstream file(path);
		if (!file.is_open())
			return false;
		nlohmann::json j;
		file >> j;
		if (j.contains("characterSlots")) {
			auto &s = j["characterSlots"];
			outSex = s.value("sex", "male");
			outAge = s.value("age", "adult");
			if (s.contains("frontAxis") && s["frontAxis"].is_array() &&
			    s["frontAxis"].size() >= 3) {
				outFrontAxis = Ogre::Vector3(
					s["frontAxis"][0].get<float>(),
					s["frontAxis"][1].get<float>(),
					s["frontAxis"][2].get<float>());
				if (outFrontAxis.squaredLength() > 0.0001f)
					outFrontAxis.normalise();
			}
			if (s.contains("slotSelections")) {
				for (auto &[slot, selJson] :
				     s["slotSelections"].items()) {
					SlotSelection sel;
					sel.layer0Mesh =
						selJson.value("layer0Mesh", "");
					sel.layer1Mesh =
						selJson.value("layer1Mesh", "");
					sel.layer2Mesh =
						selJson.value("layer2Mesh", "");
					sel.explicitMesh = selJson.value(
						"explicitMesh", "");
					outSelections[slot] = sel;
				}
			}
			/* Backward compat: old flat slots map */
			if (s.contains("slots") && s["slots"].is_object()) {
				for (auto &[slot, mesh] : s["slots"].items()) {
					SlotSelection sel;
					sel.explicitMesh = mesh.get<std::string>();
					outSelections[slot] = sel;
				}
			}
		}
		if (j.contains("characterShapeKeys")) {
			auto &sk = j["characterShapeKeys"];
			if (sk.contains("weights")) {
				for (auto &[k, v] : sk["weights"].items())
					shapeKeys[k] = v.get<float>();
			}
		}
		return true;
	} catch (...) {
		return false;
	}
}

uint64_t CharacterRegistry::createChild(uint64_t parentA, uint64_t parentB)
{
	const CharacterRecord *pA = findCharacter(parentA);
	const CharacterRecord *pB = findCharacter(parentB);
	if (!pA || !pB || parentA == parentB)
		return 0;

	/* Ensure generators are up to date */
	learnNamesFromRegistry();

	/* Determine actual sex of both parents (prefab overrides inline) */
	std::string sexA = pA->inlineSex;
	std::string sexB = pB->inlineSex;
	if (std::filesystem::exists(pA->prefabPath)) {
		std::string a, s;
		int o;
		if (readPrefabSlots(pA->prefabPath, a, s, o))
			sexA = s;
	}
	if (std::filesystem::exists(pB->prefabPath)) {
		std::string a, s;
		int o;
		if (readPrefabSlots(pB->prefabPath, a, s, o))
			sexB = s;
	}

	/* Random sex for child */
	std::string childSex = (rand() % 2 == 0) ? "male" : "female";

	std::string first = generateFirstName(childSex);
	if (first.empty())
		first = "Child";

	uint64_t id = createCharacter(first, pA->lastName, "", false);
	CharacterRecord *child = findCharacter(id);
	if (!child)
		return 0;

	child->ageYears = 0;
	child->inlineSex = childSex;
	const CharacterRecord *sameSexParent = (child->inlineSex == sexA) ? pA :
									    pB;

	child->age = "adult"; /* default; can be changed */
	child->inlineOutfitLevel = 2;

	/* Inherit class from same-sex parent, fallback to other */
	if (!sameSexParent->className.empty())
		child->className = sameSexParent->className;
	else if (!pA->className.empty())
		child->className = pA->className;
	else if (!pB->className.empty())
		child->className = pB->className;

	/* Base stats/skills/needs from class */
	initializeFromClass(id);

	/* Stat remixing from both parents */
	auto &db = CharacterClassDatabase::getSingleton();
	for (const auto &name : db.getStatNames()) {
		int valA = 0, valB = 0;
		auto itA = pA->stats.find(name.c_str());
		if (itA != pA->stats.end())
			valA = itA->second;
		auto itB = pB->stats.find(name.c_str());
		if (itB != pB->stats.end())
			valB = itB->second;

		/* Randomly pick from one parent, then DNA variation */
		int base = (rand() % 2 == 0) ? valA : valB;
		int dna = (rand() % 5) - 2; /* -2 .. +2 */
		int result = base + dna;

		/* Clamp to class base minimum */
		const auto *cls = db.findClass(child->className);
		if (cls) {
			auto itBase = cls->baseStats.find(name.c_str());
			if (itBase != cls->baseStats.end() &&
			    result < itBase->second)
				result = itBase->second;
		}
		const auto *statDef = db.findStat(name);
		if (statDef) {
			if (result < statDef->minValue)
				result = statDef->minValue;
			if (result > statDef->maxValue)
				result = statDef->maxValue;
		}

		child->stats[name.c_str()] = result;
	}

	/* Needs: class base (already set by initializeFromClass) */
	/* Skills: class base (already set by initializeFromClass) */

	/* Copy appearance from same-sex parent */
	if (sameSexParent->inlineSlotSelections.empty() &&
	    std::filesystem::exists(sameSexParent->prefabPath)) {
		std::string prefabSex = "male";
		std::string prefabAge;
		std::unordered_map<std::string, SlotSelection> prefabSelections;
		Ogre::Vector3 prefabFrontAxis = sameSexParent->frontAxis;
		readPrefabAppearance(sameSexParent->prefabPath, prefabSex,
				     prefabSelections,
				     child->inlineShapeKeyWeights, prefabAge,
				     prefabFrontAxis);
		child->age = prefabAge;
		child->inlineSex = prefabSex;
		child->inlineOutfitLevel = sameSexParent->inlineOutfitLevel;
		child->inlineSlotSelections = prefabSelections;
		child->frontAxis = sameSexParent->frontAxis;
	} else {
		child->age = sameSexParent->age;
		child->inlineSex = sameSexParent->inlineSex;
		child->inlineOutfitLevel = sameSexParent->inlineOutfitLevel;
		child->inlineSlotSelections =
			sameSexParent->inlineSlotSelections;
		child->inlineShapeKeyWeights =
			sameSexParent->inlineShapeKeyWeights;
		child->frontAxis = sameSexParent->frontAxis;
	}

	/* Randomize configured birth shape keys */
	if (!m_birthRandomizableShapeKeys.empty()) {
		for (const auto &key : m_birthRandomizableShapeKeys) {
			/* Skip excluded keys */
			if (std::find(m_birthExcludedShapeKeys.begin(),
				      m_birthExcludedShapeKeys.end(),
				      key) != m_birthExcludedShapeKeys.end())
				continue;
			float r = static_cast<float>(rand()) / RAND_MAX;
			child->inlineShapeKeyWeights[key] = r;
		}
	}

	/* Relationships */
	addRelationshipTag(parentA, false, id, false, "parent");
	addRelationshipTag(id, false, parentA, false, "child");
	addRelationshipTag(parentB, false, id, false, "parent");
	addRelationshipTag(id, false, parentB, false, "child");

	autoSave();
	learnNamesFromRegistry();
	return id;
}

/* ------------------------------------------------------------------ */
/*  Pregnancy helpers                                                 */
/* ------------------------------------------------------------------ */

bool CharacterRegistry::conceive(uint64_t motherId, uint64_t fatherId)
{
	CharacterRecord *mother = findCharacter(motherId);
	CharacterRecord *father = findCharacter(fatherId);
	if (!mother || !father || motherId == fatherId)
		return false;
	if (mother->pregnantByFatherId != 0)
		return false;

	mother->pregnantByFatherId = fatherId;
	mother->pregnancyProgress = 0.0f;

	float base = (mother->basePregnancyDuration > 0.0f) ?
			     mother->basePregnancyDuration :
			     m_basePregnancyDuration;
	float randomFactor = 0.8f + static_cast<float>(rand() % 41) / 100.0f;
	mother->pregnancyMaxProgress = base * randomFactor;

	autoSave();
	return true;
}

void CharacterRegistry::abortPregnancy(uint64_t motherId)
{
	CharacterRecord *mother = findCharacter(motherId);
	if (!mother)
		return;
	mother->pregnantByFatherId = 0;
	mother->pregnancyProgress = 0.0f;
	mother->pregnancyMaxProgress = 0.0f;
	autoSave();
}

bool CharacterRegistry::isPregnant(uint64_t motherId) const
{
	const CharacterRecord *mother = findCharacter(motherId);
	if (!mother)
		return false;
	return mother->pregnantByFatherId != 0;
}

/* ------------------------------------------------------------------ */
/*  Inline spawn (no prefab file)                                     */
/* ------------------------------------------------------------------ */

flecs::entity CharacterRegistry::spawnInlineCharacter(const CharacterRecord &c,
						      const Ogre::Vector3 &pos)
{
	if (!m_world || !m_sceneMgr)
		return flecs::entity::null();

	flecs::entity inst = m_world->entity();
	inst.add<EditorMarkerComponent>();
	inst.set<EntityNameComponent>(
		EntityNameComponent(c.firstName + " " + c.lastName));

	/* Transform */
	Ogre::SceneNode *node =
		m_sceneMgr->getRootSceneNode()->createChildSceneNode();
	node->setPosition(pos);
	TransformComponent transform;
	transform.node = node;
	transform.position = pos;
	transform.rotation = c.rotation;
	transform.scale = Ogre::Vector3(1, 1, 1);
	transform.applyToNode();
	inst.set<TransformComponent>(transform);

	/* Character visual data is now read from the registry record at
	 * build time; no component storage is needed. */
	if (m_slotSystem)
		m_slotSystem->markEntityDirty(inst.id());

	return inst;
}

/* ===================================================================== */
/*  Spawn / Save                                                         */
/* ===================================================================== */

void CharacterRegistry::initializeFromClass(uint64_t id)
{
	CharacterRecord *c = findCharacter(id);
	if (!c || c->className.empty())
		return;

	const auto *cls =
		CharacterClassDatabase::getSingleton().findClass(c->className);
	if (!cls)
		return;

	int targetLevel = c->level;
	if (targetLevel < 1)
		targetLevel = 1;

	c->level = 1;
	c->availablePoints = 0;
	c->stats = cls->baseStats;
	c->skills = cls->baseSkills;
	c->needs = cls->baseNeeds;

	auto &db = CharacterClassDatabase::getSingleton();
	for (const auto &name : db.getStatNames()) {
		if (c->stats.find(name.c_str()) == c->stats.end()) {
			const auto *def = db.findStat(name);
			c->stats[name.c_str()] = def ? def->minValue : 1;
		}
	}
	for (const auto &name : db.getSkillNames()) {
		if (c->skills.find(name.c_str()) == c->skills.end())
			c->skills[name.c_str()] = 0;
	}
	for (const auto &name : db.getNeedNames()) {
		if (c->needs.find(name.c_str()) == c->needs.end())
			c->needs[name.c_str()] = 0;
	}

	/* Simulate level-ups */
	for (int lvl = 2; lvl <= targetLevel; ++lvl) {
		c->level = lvl;
		for (const auto &pair : cls->statGrowth) {
			int growth = db.computeStatGrowth(pair.first, c->level,
							  *cls);
			c->stats[pair.first.c_str()] += growth;
		}
		for (const auto &pair : cls->skillGrowth) {
			int growth = db.computeSkillGrowth(pair.first, c->level,
							   *cls);
			c->skills[pair.first.c_str()] += growth;
			const auto *skillDef = db.findSkill(pair.first);
			if (skillDef &&
			    c->skills[pair.first.c_str()] > skillDef->maxValue)
				c->skills[pair.first.c_str()] =
					skillDef->maxValue;
		}
		c->availablePoints += db.computePointsForLevel(c->level, *cls);
	}

	/* Distribute points AI-style */
	int points = c->availablePoints;
	int idx = 0;
	int safety = 0;
	while (points > 0 && !cls->primaryStats.empty() && safety < 1000) {
		safety++;
		const auto &statName =
			cls->primaryStats[idx % cls->primaryStats.size()];
		int current = c->stats.count(statName.c_str()) ?
				      c->stats[statName.c_str()] :
				      0;
		int cost = db.computeStatCost(current, *cls);
		if (points >= cost) {
			c->stats[statName.c_str()] = current + 1;
			points -= cost;
			idx++;
		} else {
			idx++;
			if (idx >= (int)cls->primaryStats.size() * 2)
				break;
		}
	}
	if (points > 0 && !c->stats.empty()) {
		std::vector<std::string> statNames;
		for (const auto &pair : c->stats)
			statNames.push_back(pair.first);
		int randomSafety = 0;
		while (points > 0 && !statNames.empty() &&
		       randomSafety < 1000) {
			randomSafety++;
			size_t r = rand() % statNames.size();
			const auto &statName = statNames[r];
			int current = c->stats.count(statName) ?
					      c->stats[statName] :
					      0;
			int cost = db.computeStatCost(current, *cls);
			if (points >= cost) {
				c->stats[statName] = current + 1;
				points -= cost;
			} else {
				statNames.erase(statNames.begin() + r);
			}
		}
	}
	c->availablePoints = points;

	/* Initialize resource pools to full */
	for (const auto &name : db.getStatNames()) {
		const auto *def = db.findStat(name);
		if (def &&
		    def->kind ==
			    CharacterClassDatabase::StatKind::ResourcePool) {
			int maxVal = c->stats.count(name.c_str()) ?
					     c->stats[name.c_str()] :
					     def->minValue;
			if (maxVal < def->minValue)
				maxVal = def->minValue;
			if (maxVal > def->maxValue)
				maxVal = def->maxValue;
			c->currentPools[name.c_str()] = maxVal;
		}
	}
}

flecs::entity CharacterRegistry::findSpawnedEntity(uint64_t id) const
{
	if (!m_world)
		return flecs::entity::null();
	flecs::entity result = flecs::entity::null();
	m_world->query<CharacterIdentityComponent>().each(
		[&](flecs::entity e, CharacterIdentityComponent &ci) {
			if (ci.registryId == id)
				result = e;
		});
	return result;
}

void CharacterRegistry::markCharacterDirty(uint64_t id)
{
	CharacterRecord *rec = findCharacter(id);
	if (rec)
		rec->version++;

	flecs::entity e = findSpawnedEntity(id);
	if (!e.is_alive())
		return;
	if (m_slotSystem)
		m_slotSystem->markEntityDirty(e.id());
}

uint64_t CharacterRegistry::getCharacterVersion(uint64_t id) const
{
	const CharacterRecord *rec = findCharacter(id);
	return rec ? rec->version : 0;
}

bool CharacterRegistry::despawnCharacter(uint64_t id)
{
	if (!m_world)
		return false;
	flecs::entity e = findSpawnedEntity(id);
	if (!e.is_alive())
		return false;
	e.destruct();
	return true;
}

flecs::entity CharacterRegistry::spawnCharacter(uint64_t id)
{
	if (!m_world || !m_sceneMgr || !m_uiSystem)
		return flecs::entity::null();

	CharacterRecord *c = findCharacter(id);
	if (!c)
		return flecs::entity::null();

	/* Singleton: despawn any existing copy first */
	flecs::entity existing = findSpawnedEntity(id);
	if (existing.is_alive())
		existing.destruct();

	/* Migrate prefab appearance data into the registry record if the
	 * record has no inline appearance data yet. */
	if (c->inlineSlotSelections.empty() &&
	    std::filesystem::exists(c->prefabPath)) {
		std::string prefabAge;
		readPrefabAppearance(c->prefabPath, c->inlineSex,
				     c->inlineSlotSelections,
				     c->inlineShapeKeyWeights, prefabAge,
				     c->frontAxis);
		if (!prefabAge.empty())
			c->age = prefabAge;
		autoSave();
	}

	Ogre::Vector3 pos = c->position;
	flecs::entity inst;
	if (std::filesystem::exists(c->prefabPath)) {
		PrefabSystem prefabSys(*m_world, m_sceneMgr);
		inst = prefabSys.createInstance(
			c->prefabPath, flecs::entity::null(), pos,
			c->firstName + " " + c->lastName, m_uiSystem);
		if (inst.is_alive() && inst.has<TransformComponent>()) {
			auto &t = inst.get_mut<TransformComponent>();
			t.position = c->position;
			t.rotation = c->rotation;
			t.applyToNode();
		}
	} else {
		inst = spawnInlineCharacter(*c, pos);
		if (inst.is_alive() && inst.has<TransformComponent>()) {
			auto &t = inst.get_mut<TransformComponent>();
			t.rotation = c->rotation;
			t.applyToNode();
		}
	}

	if (inst.is_alive()) {
		inst.set<CharacterIdentityComponent>(
			CharacterIdentityComponent{ id });
		inst.add<RuntimeMarkerComponent>();
		m_uiSystem->addEntity(inst);
	}
	return inst;
}

bool CharacterRegistry::savePrefabForCharacter(uint64_t id)
{
	const CharacterRecord *c = findCharacter(id);
	if (!c)
		return false;

	try {
		nlohmann::json j;
		{
			std::ifstream file(c->prefabPath);
			if (file.is_open())
				file >> j;
		}

		j["characterSlots"]["age"] = c->age;
		j["characterSlots"]["sex"] = c->inlineSex;
		j["characterSlots"]["outfitLevel"] = c->inlineOutfitLevel;
		nlohmann::json selections = nlohmann::json::object();
		for (const auto &kv : c->inlineSlotSelections) {
			nlohmann::json s;
			s["layer0Mesh"] = kv.second.layer0Mesh;
			s["layer1Mesh"] = kv.second.layer1Mesh;
			s["layer2Mesh"] = kv.second.layer2Mesh;
			s["explicitMesh"] = kv.second.explicitMesh;
			selections[kv.first] = s;
		}
		j["characterSlots"]["slotSelections"] = selections;

		if (!c->inlineShapeKeyWeights.empty()) {
			nlohmann::json weights = nlohmann::json::object();
			for (const auto &kv : c->inlineShapeKeyWeights)
				weights[kv.first] = kv.second;
			j["characterShapeKeys"]["weights"] = weights;
		}

		std::filesystem::path p(c->prefabPath);
		std::filesystem::create_directories(p.parent_path());
		std::ofstream out(c->prefabPath);
		if (!out.is_open()) {
			m_lastError = "Cannot open " + c->prefabPath;
			return false;
		}
		out << j.dump(4);
		return true;
	} catch (const std::exception &e) {
		m_lastError = std::string("Save prefab error: ") + e.what();
		return false;
	}
}

/* ===================================================================== */
/*  Indexes                                                              */
/* ===================================================================== */

void CharacterRegistry::rebuildIndexes()
{
	m_relBySource.clear();
	m_relByTarget.clear();
	for (size_t i = 0; i < m_relationships.size(); ++i)
		addToIndex(i);
}

void CharacterRegistry::addToIndex(size_t relIndex)
{
	const Relationship &r = m_relationships[relIndex];
	m_relBySource.insert({ r.sourceId, relIndex });
	m_relByTarget.insert({ r.targetId, relIndex });
}

void CharacterRegistry::removeFromIndex(uint64_t sourceId, uint64_t targetId,
					size_t relIndex)
{
	auto srcRange = m_relBySource.equal_range(sourceId);
	for (auto it = srcRange.first; it != srcRange.second; ++it) {
		if (it->second == relIndex) {
			m_relBySource.erase(it);
			break;
		}
	}
	auto tgtRange = m_relByTarget.equal_range(targetId);
	for (auto it = tgtRange.first; it != tgtRange.second; ++it) {
		if (it->second == relIndex) {
			m_relByTarget.erase(it);
			break;
		}
	}
}

/* ===================================================================== */
/*  Characters                                                           */
/* ===================================================================== */

uint64_t CharacterRegistry::createCharacter(const std::string &firstName,
					    const std::string &lastName,
					    const std::string &templatePath,
					    bool persistent)
{
	uint64_t id = persistent ? m_nextId++ : m_nextRuntimeId++;
	/* Safety: id must never be 0 */
	if (id == 0) {
		id = persistent ? m_nextId++ : m_nextRuntimeId++;
		Ogre::LogManager::getSingleton().logMessage(
			"CharacterRegistry: id wrapped to 0, "
			"incremented to " +
			std::to_string(id));
	}
	CharacterRecord rec;
	rec.id = id;
	rec.firstName = firstName;
	rec.lastName = lastName;
	rec.persistent = persistent;

	if (persistent) {
		rec.prefabPath = generatePrefabPath(id, firstName, lastName);
		if (!templatePath.empty() &&
		    std::filesystem::exists(templatePath)) {
			copyPrefab(templatePath, rec.prefabPath);
		}
	}

	m_characters[id] = rec;
	if (persistent)
		autoSave();
	return id;
}

void CharacterRegistry::deleteCharacter(uint64_t id)
{
	auto it = m_characters.find(id);
	if (it != m_characters.end()) {
		deletePrefabFile(it->second.prefabPath);
		m_characters.erase(it);
	}
	purgeRelationships(id);
	autoSave();
}

CharacterRegistry::CharacterRecord *
CharacterRegistry::findCharacter(uint64_t id)
{
	auto it = m_characters.find(id);
	return it != m_characters.end() ? &it->second : nullptr;
}

const CharacterRegistry::CharacterRecord *
CharacterRegistry::findCharacter(uint64_t id) const
{
	auto it = m_characters.find(id);
	return it != m_characters.end() ? &it->second : nullptr;
}

/* ===================================================================== */
/*  Groups                                                               */
/* ===================================================================== */

uint64_t CharacterRegistry::createGroup(const std::string &name)
{
	uint64_t id = m_nextId++;
	GroupRecord rec;
	rec.id = id;
	rec.name = name;
	m_groups[id] = rec;
	autoSave();
	return id;
}

void CharacterRegistry::deleteGroup(uint64_t id)
{
	m_groups.erase(id);
	purgeRelationships(id);
	autoSave();
}

void CharacterRegistry::addToGroup(uint64_t groupId, uint64_t characterId)
{
	GroupRecord *g = findGroup(groupId);
	if (!g)
		return;
	if (std::find(g->memberIds.begin(), g->memberIds.end(), characterId) ==
	    g->memberIds.end()) {
		g->memberIds.push_back(characterId);
		autoSave();
	}
}

void CharacterRegistry::removeFromGroup(uint64_t groupId, uint64_t characterId)
{
	GroupRecord *g = findGroup(groupId);
	if (!g)
		return;
	auto it = std::find(g->memberIds.begin(), g->memberIds.end(),
			    characterId);
	if (it != g->memberIds.end()) {
		g->memberIds.erase(it);
		autoSave();
	}
}

CharacterRegistry::GroupRecord *CharacterRegistry::findGroup(uint64_t id)
{
	auto it = m_groups.find(id);
	return it != m_groups.end() ? &it->second : nullptr;
}

const CharacterRegistry::GroupRecord *
CharacterRegistry::findGroup(uint64_t id) const
{
	auto it = m_groups.find(id);
	return it != m_groups.end() ? &it->second : nullptr;
}

/* ===================================================================== */
/*  Relationships                                                        */
/* ===================================================================== */

CharacterRegistry::Relationship *
CharacterRegistry::findRelationship(uint64_t sourceId, bool sourceIsGroup,
				    uint64_t targetId, bool targetIsGroup)
{
	auto srcRange = m_relBySource.equal_range(sourceId);
	for (auto it = srcRange.first; it != srcRange.second; ++it) {
		Relationship &r = m_relationships[it->second];
		if (r.sourceIsGroup == sourceIsGroup &&
		    r.targetId == targetId && r.targetIsGroup == targetIsGroup)
			return &r;
	}
	return nullptr;
}

const CharacterRegistry::Relationship *
CharacterRegistry::findRelationship(uint64_t sourceId, bool sourceIsGroup,
				    uint64_t targetId, bool targetIsGroup) const
{
	auto srcRange = m_relBySource.equal_range(sourceId);
	for (auto it = srcRange.first; it != srcRange.second; ++it) {
		const Relationship &r = m_relationships[it->second];
		if (r.sourceIsGroup == sourceIsGroup &&
		    r.targetId == targetId && r.targetIsGroup == targetIsGroup)
			return &r;
	}
	return nullptr;
}

void CharacterRegistry::setRelationshipStat(
	uint64_t sourceId, bool sourceIsGroup, uint64_t targetId,
	bool targetIsGroup, const std::string &stat, float value)
{
	Relationship *r = findRelationship(sourceId, sourceIsGroup, targetId,
					   targetIsGroup);
	if (!r) {
		Relationship nr;
		nr.sourceId = sourceId;
		nr.sourceIsGroup = sourceIsGroup;
		nr.targetId = targetId;
		nr.targetIsGroup = targetIsGroup;
		m_relationships.push_back(nr);
		addToIndex(m_relationships.size() - 1);
		r = &m_relationships.back();
	}
	r->stats[stat] = value;
	autoSave();
}

float CharacterRegistry::getRelationshipStat(uint64_t sourceId,
					     bool sourceIsGroup,
					     uint64_t targetId,
					     bool targetIsGroup,
					     const std::string &stat) const
{
	const Relationship *r = findRelationship(sourceId, sourceIsGroup,
						 targetId, targetIsGroup);
	if (!r)
		return 0.0f;
	auto it = r->stats.find(stat);
	return it != r->stats.end() ? it->second : 0.0f;
}

void CharacterRegistry::addRelationshipTag(uint64_t sourceId,
					   bool sourceIsGroup,
					   uint64_t targetId,
					   bool targetIsGroup,
					   const std::string &tag)
{
	Relationship *r = findRelationship(sourceId, sourceIsGroup, targetId,
					   targetIsGroup);
	if (!r) {
		Relationship nr;
		nr.sourceId = sourceId;
		nr.sourceIsGroup = sourceIsGroup;
		nr.targetId = targetId;
		nr.targetIsGroup = targetIsGroup;
		m_relationships.push_back(nr);
		addToIndex(m_relationships.size() - 1);
		r = &m_relationships.back();
	}
	r->tags.insert(tag);
	autoSave();
}

void CharacterRegistry::removeRelationshipTag(uint64_t sourceId,
					      bool sourceIsGroup,
					      uint64_t targetId,
					      bool targetIsGroup,
					      const std::string &tag)
{
	Relationship *r = findRelationship(sourceId, sourceIsGroup, targetId,
					   targetIsGroup);
	if (r) {
		r->tags.erase(tag);
		autoSave();
	}
}

bool CharacterRegistry::hasRelationshipTag(uint64_t sourceId,
					   bool sourceIsGroup,
					   uint64_t targetId,
					   bool targetIsGroup,
					   const std::string &tag) const
{
	const Relationship *r = findRelationship(sourceId, sourceIsGroup,
						 targetId, targetIsGroup);
	if (!r)
		return false;
	return r->tags.find(tag) != r->tags.end();
}

std::vector<const CharacterRegistry::Relationship *>
CharacterRegistry::getRelationships(uint64_t id) const
{
	std::vector<const Relationship *> result;
	auto srcRange = m_relBySource.equal_range(id);
	for (auto it = srcRange.first; it != srcRange.second; ++it)
		result.push_back(&m_relationships[it->second]);
	auto tgtRange = m_relByTarget.equal_range(id);
	for (auto it = tgtRange.first; it != tgtRange.second; ++it)
		result.push_back(&m_relationships[it->second]);
	return result;
}

void CharacterRegistry::purgeRelationships(uint64_t id)
{
	std::vector<size_t> toRemove;
	auto srcRange = m_relBySource.equal_range(id);
	for (auto it = srcRange.first; it != srcRange.second; ++it)
		toRemove.push_back(it->second);
	auto tgtRange = m_relByTarget.equal_range(id);
	for (auto it = tgtRange.first; it != tgtRange.second; ++it)
		if (std::find(toRemove.begin(), toRemove.end(), it->second) ==
		    toRemove.end())
			toRemove.push_back(it->second);

	std::sort(toRemove.begin(), toRemove.end(), std::greater<size_t>());
	for (size_t idx : toRemove) {
		removeFromIndex(m_relationships[idx].sourceId,
				m_relationships[idx].targetId, idx);
		m_relationships.erase(m_relationships.begin() + idx);
	}
	rebuildIndexes();
}

/* ===================================================================== */
/*  Custom columns                                                       */
/* ===================================================================== */

void CharacterRegistry::addCharacterColumn(const std::string &name,
					   ColumnDef::Type type)
{
	m_characterColumns.push_back({ type, name });
	autoSave();
}

void CharacterRegistry::removeCharacterColumn(const std::string &name)
{
	m_characterColumns.erase(std::remove_if(m_characterColumns.begin(),
						m_characterColumns.end(),
						[&](const ColumnDef &c) {
							return c.name == name;
						}),
				 m_characterColumns.end());
	autoSave();
}

void CharacterRegistry::addGroupColumn(const std::string &name,
				       ColumnDef::Type type)
{
	m_groupColumns.push_back({ type, name });
	autoSave();
}

void CharacterRegistry::removeGroupColumn(const std::string &name)
{
	m_groupColumns.erase(std::remove_if(m_groupColumns.begin(),
					    m_groupColumns.end(),
					    [&](const ColumnDef &c) {
						    return c.name == name;
					    }),
			     m_groupColumns.end());
	autoSave();
}

/* ===================================================================== */
/*  Serialization                                                        */
/* ===================================================================== */

nlohmann::json CharacterRegistry::serialize() const
{
	nlohmann::json j;
	j["version"] = "1.0";
	j["nextId"] = m_nextId;

	for (const auto &name : m_maleFirstNameSeeds)
		j["maleFirstNameSeeds"].push_back(name);
	for (const auto &name : m_femaleFirstNameSeeds)
		j["femaleFirstNameSeeds"].push_back(name);
	for (const auto &name : m_lastNameSeeds)
		j["lastNameSeeds"].push_back(name);

	for (const auto &key : m_birthRandomizableShapeKeys)
		j["birthRandomizableShapeKeys"].push_back(key);
	for (const auto &key : m_birthExcludedShapeKeys)
		j["birthExcludedShapeKeys"].push_back(key);

	j["basePregnancyDuration"] = m_basePregnancyDuration;
	j["pregnancyTimeScale"] = m_pregnancyTimeScale;

	for (const auto &c : m_characterColumns) {
		nlohmann::json col;
		col["name"] = c.name;
		col["type"] = (c.type == ColumnDef::Int)   ? "int" :
			      (c.type == ColumnDef::Float) ? "float" :
							     "string";
		j["characterColumns"].push_back(col);
	}
	for (const auto &c : m_groupColumns) {
		nlohmann::json col;
		col["name"] = c.name;
		col["type"] = (c.type == ColumnDef::Int)   ? "int" :
			      (c.type == ColumnDef::Float) ? "float" :
							     "string";
		j["groupColumns"].push_back(col);
	}

	for (const auto &pair : m_characters) {
		const CharacterRecord &c = pair.second;
		if (!c.persistent)
			continue;
		/* Belt-and-suspenders: never serialize id == 0 */
		if (c.id == 0)
			continue;
		nlohmann::json rec;

		rec["id"] = c.id;
		rec["persistent"] = c.persistent;
		rec["firstName"] = c.firstName;
		rec["lastName"] = c.lastName;
		rec["prefabPath"] = c.prefabPath;
		rec["className"] = c.className;
		rec["level"] = c.level;
		rec["currentXP"] = c.currentXP;
		rec["availablePoints"] = c.availablePoints;
		rec["levelUpPending"] = c.levelUpPending;
		rec["ageYears"] = c.ageYears;
		rec["age"] = c.age;
		rec["inlineSex"] = c.inlineSex;
		rec["inlineOutfitLevel"] = c.inlineOutfitLevel;
		{
			nlohmann::json fa;
			fa["x"] = c.frontAxis.x;
			fa["y"] = c.frontAxis.y;
			fa["z"] = c.frontAxis.z;
			rec["frontAxis"] = fa;
		}
		if (!c.inlineSlotSelections.empty()) {
			nlohmann::json selJson;
			for (const auto &kv : c.inlineSlotSelections) {
				nlohmann::json s;
				s["layer0Mesh"] = kv.second.layer0Mesh;
				s["layer1Mesh"] = kv.second.layer1Mesh;
				s["layer2Mesh"] = kv.second.layer2Mesh;
				s["explicitMesh"] = kv.second.explicitMesh;
				selJson[kv.first] = s;
			}
			rec["inlineSlotSelections"] = selJson;
		}
		if (!c.inlineShapeKeyWeights.empty()) {
			nlohmann::json skJson;
			for (const auto &kv : c.inlineShapeKeyWeights)
				skJson[kv.first] = kv.second;
			rec["inlineShapeKeys"] = skJson;
		}
		for (const auto &kv : c.stats)
			rec["stats"][kv.first] = kv.second;
		for (const auto &kv : c.skills)
			rec["skills"][kv.first] = kv.second;
		for (const auto &kv : c.needs)
			rec["needs"][kv.first] = kv.second;
		for (const auto &kv : c.currentPools)
			rec["currentPools"][kv.first] = kv.second;
		for (const auto &t : c.tags)
			rec["tags"].push_back(t);
		for (const auto &kv : c.intColumns)
			rec["intColumns"][kv.first] = kv.second;
		for (const auto &kv : c.floatColumns)
			rec["floatColumns"][kv.first] = kv.second;
		for (const auto &kv : c.stringColumns)
			rec["stringColumns"][kv.first] = kv.second;
		rec["pregnantByFatherId"] = c.pregnantByFatherId;
		rec["pregnancyProgress"] = c.pregnancyProgress;
		rec["pregnancyMaxProgress"] = c.pregnancyMaxProgress;
		rec["basePregnancyDuration"] = c.basePregnancyDuration;
		{
			nlohmann::json pos;
			pos["x"] = c.position.x;
			pos["y"] = c.position.y;
			pos["z"] = c.position.z;
			rec["position"] = pos;
		}
		{
			nlohmann::json rot;
			rot["w"] = c.rotation.w;
			rot["x"] = c.rotation.x;
			rot["y"] = c.rotation.y;
			rot["z"] = c.rotation.z;
			rec["rotation"] = rot;
		}
		j["characters"].push_back(rec);
	}

	for (const auto &pair : m_groups) {
		const GroupRecord &g = pair.second;
		nlohmann::json rec;
		rec["id"] = g.id;
		rec["name"] = g.name;
		rec["memberIds"] = g.memberIds;
		for (const auto &kv : g.intColumns)
			rec["intColumns"][kv.first] = kv.second;
		for (const auto &kv : g.floatColumns)
			rec["floatColumns"][kv.first] = kv.second;
		for (const auto &kv : g.stringColumns)
			rec["stringColumns"][kv.first] = kv.second;
		j["groups"].push_back(rec);
	}

	for (const auto &r : m_relationships) {
		nlohmann::json rec;
		rec["sourceId"] = r.sourceId;
		rec["sourceIsGroup"] = r.sourceIsGroup;
		rec["targetId"] = r.targetId;
		rec["targetIsGroup"] = r.targetIsGroup;
		for (const auto &kv : r.stats)
			rec["stats"][kv.first] = kv.second;
		for (const auto &tag : r.tags)
			rec["tags"].push_back(tag);
		j["relationships"].push_back(rec);
	}

	return j;
}

void CharacterRegistry::deserialize(const nlohmann::json &j)
{
	m_characters.clear();
	m_groups.clear();
	m_relationships.clear();
	m_characterColumns.clear();
	m_groupColumns.clear();

	m_nextId = j.value("nextId", 1);

	if (j.contains("maleFirstNameSeeds")) {
		for (const auto &name : j["maleFirstNameSeeds"])
			m_maleFirstNameSeeds.push_back(name.get<std::string>());
	}
	if (j.contains("femaleFirstNameSeeds")) {
		for (const auto &name : j["femaleFirstNameSeeds"])
			m_femaleFirstNameSeeds.push_back(
				name.get<std::string>());
	}
	if (j.contains("lastNameSeeds")) {
		for (const auto &name : j["lastNameSeeds"])
			m_lastNameSeeds.push_back(name.get<std::string>());
	}
	if (j.contains("birthRandomizableShapeKeys")) {
		for (const auto &key : j["birthRandomizableShapeKeys"])
			m_birthRandomizableShapeKeys.push_back(
				key.get<std::string>());
	}
	if (j.contains("birthExcludedShapeKeys")) {
		for (const auto &key : j["birthExcludedShapeKeys"])
			m_birthExcludedShapeKeys.push_back(
				key.get<std::string>());
	}

	m_basePregnancyDuration = j.value("basePregnancyDuration", 300.0f);
	m_pregnancyTimeScale = j.value("pregnancyTimeScale", 1.0f);

	if (j.contains("characterColumns")) {
		for (const auto &col : j["characterColumns"]) {
			ColumnDef::Type t = ColumnDef::String;
			std::string ts = col.value("type", "string");
			if (ts == "int")
				t = ColumnDef::Int;
			else if (ts == "float")
				t = ColumnDef::Float;
			m_characterColumns.push_back(
				{ t, col.value("name", "") });
		}
	}
	if (j.contains("groupColumns")) {
		for (const auto &col : j["groupColumns"]) {
			ColumnDef::Type t = ColumnDef::String;
			std::string ts = col.value("type", "string");
			if (ts == "int")
				t = ColumnDef::Int;
			else if (ts == "float")
				t = ColumnDef::Float;
			m_groupColumns.push_back({ t, col.value("name", "") });
		}
	}

	if (j.contains("characters")) {
		for (const auto &rec : j["characters"]) {
			CharacterRecord c;
			c.id = rec.value("id", 0);
			c.persistent = rec.value("persistent", true);
			c.firstName = rec.value("firstName", "");
			c.lastName = rec.value("lastName", "");
			c.pregnantByFatherId =
				rec.value("pregnantByFatherId", 0);
			c.pregnancyProgress =
				rec.value("pregnancyProgress", 0.0f);
			c.pregnancyMaxProgress =
				rec.value("pregnancyMaxProgress", 0.0f);
			c.basePregnancyDuration =
				rec.value("basePregnancyDuration", 0.0f);
			c.prefabPath = rec.value("prefabPath", "");
			c.className = rec.value("className", "");
			c.level = rec.value("level", 1);
			c.currentXP = rec.value("currentXP", 0);
			c.availablePoints = rec.value("availablePoints", 0);
			c.levelUpPending = rec.value("levelUpPending", false);
			c.ageYears = rec.value("ageYears", 0);
			c.age = rec.value("age", "adult");
			c.inlineSex = rec.value("inlineSex", "male");
			c.inlineOutfitLevel = rec.value("inlineOutfitLevel", 2);
			if (rec.contains("frontAxis") &&
			    rec["frontAxis"].is_object()) {
				auto &fa = rec["frontAxis"];
				c.frontAxis = Ogre::Vector3(
					fa.value("x", 0.0f), fa.value("y", 0.0f),
					fa.value("z", 0.0f));
				if (c.frontAxis.squaredLength() > 0.0001f)
					c.frontAxis.normalise();
			}
			if (rec.contains("position") &&
			    rec["position"].is_object()) {
				auto &p = rec["position"];
				c.position = Ogre::Vector3(p.value("x", 0.0f),
							   p.value("y", 0.0f),
							   p.value("z", 0.0f));
			}
			if (rec.contains("rotation") &&
			    rec["rotation"].is_object()) {
				auto &r = rec["rotation"];
				c.rotation = Ogre::Quaternion(
					r.value("w", 1.0f), r.value("x", 0.0f),
					r.value("y", 0.0f), r.value("z", 0.0f));
			}
			if (rec.contains("inlineSlotSelections")) {
				for (auto &[slot, s] :
				     rec["inlineSlotSelections"].items()) {
					SlotSelection sel;
					sel.layer0Mesh =
						s.value("layer0Mesh", "");
					sel.layer1Mesh =
						s.value("layer1Mesh", "");
					sel.layer2Mesh =
						s.value("layer2Mesh", "");
					sel.explicitMesh =
						s.value("explicitMesh", "");
					c.inlineSlotSelections[slot] = sel;
				}
			}
			if (rec.contains("inlineShapeKeys")) {
				for (auto &[k, v] :
				     rec["inlineShapeKeys"].items())
					c.inlineShapeKeyWeights[k] =
						v.get<float>();
			}
			if (rec.contains("stats")) {
				for (auto &[k, v] : rec["stats"].items())
					c.stats[k] = v.get<int>();
			}
			if (rec.contains("skills")) {
				for (auto &[k, v] : rec["skills"].items())
					c.skills[k] = v.get<int>();
			}
			if (rec.contains("needs")) {
				for (auto &[k, v] : rec["needs"].items())
					c.needs[k] = v.get<int>();
			}
			if (rec.contains("currentPools")) {
				for (auto &[k, v] : rec["currentPools"].items())
					c.currentPools[k] = v.get<int>();
			}
			if (rec.contains("tags")) {
				for (const auto &t : rec["tags"])
					c.tags.push_back(t.get<std::string>());
			}
			if (rec.contains("intColumns")) {
				for (auto &[k, v] : rec["intColumns"].items())
					c.intColumns[k] = v.get<int64_t>();
			}
			if (rec.contains("floatColumns")) {
				for (auto &[k, v] : rec["floatColumns"].items())
					c.floatColumns[k] = v.get<double>();
			}
			if (rec.contains("stringColumns")) {
				for (auto &[k, v] :
				     rec["stringColumns"].items())
					c.stringColumns[k] =
						v.get<std::string>();
			}

			/* ---- Safeguard: reject id == 0 ---- */
			if (c.id == 0) {
				Ogre::LogManager::getSingleton().logMessage(
					"CharacterRegistry: skipping corrupt "
					"record with id=0 (firstName=" +
					c.firstName +
					" lastName=" + c.lastName + ")");
				continue;
			}

			m_characters[c.id] = c;
		}
	}

	if (j.contains("groups")) {
		for (const auto &rec : j["groups"]) {
			GroupRecord g;
			g.id = rec.value("id", 0);
			g.name = rec.value("name", "");
			if (rec.contains("memberIds"))
				for (const auto &m : rec["memberIds"])
					g.memberIds.push_back(
						m.get<uint64_t>());
			if (rec.contains("intColumns")) {
				for (auto &[k, v] : rec["intColumns"].items())
					g.intColumns[k] = v.get<int64_t>();
			}
			if (rec.contains("floatColumns")) {
				for (auto &[k, v] : rec["floatColumns"].items())
					g.floatColumns[k] = v.get<double>();
			}
			if (rec.contains("stringColumns")) {
				for (auto &[k, v] :
				     rec["stringColumns"].items())
					g.stringColumns[k] =
						v.get<std::string>();
			}
			m_groups[g.id] = g;
		}
	}

	if (j.contains("relationships")) {
		for (const auto &rec : j["relationships"]) {
			Relationship r;
			r.sourceId = rec.value("sourceId", 0);
			r.sourceIsGroup = rec.value("sourceIsGroup", false);
			r.targetId = rec.value("targetId", 0);
			r.targetIsGroup = rec.value("targetIsGroup", false);
			if (rec.contains("stats")) {
				for (auto &[k, v] : rec["stats"].items())
					r.stats[k] = v.get<float>();
			}
			if (rec.contains("tags")) {
				for (const auto &tag : rec["tags"])
					r.tags.insert(tag.get<std::string>());
			}
			m_relationships.push_back(r);
		}
	}

	rebuildIndexes();
}

bool CharacterRegistry::saveToFile(const std::string &filepath)
{
	try {
		std::ofstream file(filepath);
		if (!file.is_open()) {
			m_lastError = "Cannot open " + filepath;
			return false;
		}
		file << serialize().dump(4);
		return true;
	} catch (const std::exception &e) {
		m_lastError = std::string("Save error: ") + e.what();
		return false;
	}
}

bool CharacterRegistry::loadFromFile(const std::string &filepath)
{
	try {
		std::ifstream file(filepath);
		if (!file.is_open()) {
			m_lastError = "Cannot open " + filepath;
			return false;
		}
		nlohmann::json j;
		file >> j;
		deserialize(j);
		return true;
	} catch (const std::exception &e) {
		m_lastError = std::string("Load error: ") + e.what();
		return false;
	}
}
/* ===================================================================== */
/*  ImGui helpers                                                        */
/* ===================================================================== */

static void labelBuf(char *buf, size_t bufSize,
		     const CharacterRegistry::CharacterRecord *c)
{
	if (!c)
		snprintf(buf, bufSize, "(unknown)");
	else
		snprintf(buf, bufSize, "%s %s (#%lu)", c->firstName.c_str(),
			 c->lastName.c_str(), (unsigned long)c->id);
}

static void labelBuf(char *buf, size_t bufSize,
		     const CharacterRegistry::GroupRecord *g)
{
	if (!g)
		snprintf(buf, bufSize, "(unknown)");
	else
		snprintf(buf, bufSize, "%s (#%lu)", g->name.c_str(),
			 (unsigned long)g->id);
}

static void drawColumnsEditor(const char *title,
			      std::vector<CharacterRegistry::ColumnDef> &cols,
			      CharacterRegistry &reg)
{
	if (ImGui::CollapsingHeader(title)) {
		ImGui::Indent();
		for (size_t i = 0; i < cols.size(); ++i) {
			ImGui::PushID(static_cast<int>(i));
			ImGui::Text(
				"%s (%s)", cols[i].name.c_str(),
				cols[i].type == CharacterRegistry::ColumnDef::Int ?
					"int" :
				cols[i].type == CharacterRegistry::ColumnDef::
							Float ?
					"float" :
					"string");
			ImGui::SameLine();
			if (ImGui::SmallButton("Del")) {
				cols.erase(cols.begin() + i--);
				reg.autoSave();
			}
			ImGui::PopID();
		}

		static char newColName[64] = "";
		static int newColType = 0;
		ImGui::InputText("Name", newColName, sizeof(newColName));
		const char *types[] = { "int", "float", "string" };
		ImGui::Combo("Type", &newColType, types, 3);
		if (ImGui::Button("Add Column") && strlen(newColName) > 0) {
			CharacterRegistry::ColumnDef::Type t =
				CharacterRegistry::ColumnDef::Int;
			if (newColType == 1)
				t = CharacterRegistry::ColumnDef::Float;
			else if (newColType == 2)
				t = CharacterRegistry::ColumnDef::String;
			cols.push_back({ t, newColName });
			newColName[0] = '\0';
			reg.autoSave();
		}
		ImGui::Unindent();
	}
}

/* ===================================================================== */
/*  Character appearance editor (moved from CharacterSlotsEditor)        */
/* ===================================================================== */

static void drawCharacterAppearanceEditor(CharacterRegistry &reg,
					  CharacterRegistry::CharacterRecord *c)
{
	if (!c)
		return;

	CharacterSlotSystem::loadCatalog();

	const Ogre::String currentAge = c->age;
	const int outfitLevel = c->inlineOutfitLevel;

	/* Sex selector */
	std::vector<Ogre::String> sexes =
		CharacterSlotSystem::getSexes(currentAge);
	Ogre::String currentSex = c->inlineSex;
	if (ImGui::BeginCombo("Sex", currentSex.c_str())) {
		for (const auto &sex : sexes) {
			bool isSelected = (currentSex == sex);
			if (ImGui::Selectable(sex.c_str(), isSelected)) {
				c->inlineSex = sex;
				reg.autoSave();
				reg.markCharacterDirty(c->id);
			}
			if (isSelected)
				ImGui::SetItemDefaultFocus();
		}
		ImGui::EndCombo();
	}

	ImGui::Separator();

	/* Front-facing axis */
	{
		Ogre::Vector3 axis = c->frontAxis;
		float axisVals[3] = { axis.x, axis.y, axis.z };
		ImGui::Text("Front Axis:");
		ImGui::SameLine();
		ImGui::TextDisabled(
			"(direction character faces, used by path following)");
		if (ImGui::DragFloat3("##frontAxis", axisVals, 0.1f, -1.0f,
				      1.0f)) {
			c->frontAxis = Ogre::Vector3(axisVals[0], axisVals[1],
						     axisVals[2]);
			if (c->frontAxis.squaredLength() > 0.0001f)
				c->frontAxis.normalise();
			reg.autoSave();
		}
		ImGui::SameLine();
		if (ImGui::SmallButton("-Z")) {
			c->frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z;
			reg.autoSave();
		}
		ImGui::SameLine();
		if (ImGui::SmallButton("+Z")) {
			c->frontAxis = Ogre::Vector3::UNIT_Z;
			reg.autoSave();
		}
	}

	ImGui::Separator();

	/* Shape Keys */
	if (ImGui::CollapsingHeader("Shape Keys")) {
		auto shapeKeys = CharacterSlotSystem::getShapeKeyNames(
			currentAge, c->inlineSex);
		if (shapeKeys.empty()) {
			ImGui::TextDisabled("No shape keys available.");
		} else {
			for (const auto &name : shapeKeys) {
				float val = c->inlineShapeKeyWeights[name];
				if (ImGui::SliderFloat(name.c_str(), &val, 0.0f,
						       1.0f)) {
					c->inlineShapeKeyWeights[name] = val;
					reg.autoSave();
					reg.markCharacterDirty(c->id);
				}
			}
		}
	}

	ImGui::Separator();

	/* Slot selections */
	std::vector<Ogre::String> availableSlots =
		CharacterSlotSystem::getSlots(currentAge, c->inlineSex);
	for (const auto &pair : c->inlineSlotSelections) {
		if (std::find(availableSlots.begin(), availableSlots.end(),
			      pair.first) == availableSlots.end())
			availableSlots.push_back(pair.first);
	}
	std::sort(availableSlots.begin(), availableSlots.end());

	for (const auto &slot : availableSlots) {
		if (c->inlineSlotSelections.find(slot) ==
		    c->inlineSlotSelections.end()) {
			c->inlineSlotSelections[slot] = SlotSelection();
		}

		SlotSelection &sel = c->inlineSlotSelections[slot];

		if (ImGui::TreeNode(slot.c_str())) {
			/* Resolved mesh preview */
			Ogre::String resolved = CharacterSlotSystem::resolveMesh(
				currentAge, c->inlineSex, slot, sel,
				outfitLevel);
			ImGui::TextDisabled("Resolved: %s",
					    resolved.empty() ?
						    "(none)" :
						    resolved.c_str());

			/* Explicit mesh override */
			bool useExplicit = !sel.explicitMesh.empty();
			if (ImGui::Checkbox("Lock explicit mesh", &useExplicit)) {
				if (!useExplicit) {
					sel.explicitMesh.clear();
				} else {
					sel.explicitMesh = CharacterSlotSystem::resolveMesh(
						currentAge, c->inlineSex, slot, sel,
						outfitLevel);
				}
				reg.autoSave();
				reg.markCharacterDirty(c->id);
			}

			if (useExplicit) {
				std::vector<Ogre::String> meshes =
					CharacterSlotSystem::getMeshes(
						currentAge, c->inlineSex, slot);
				Ogre::String preview = sel.explicitMesh.empty() ?
							       "(none)" :
							       sel.explicitMesh;
				if (ImGui::BeginCombo("Mesh", preview.c_str())) {
					if (ImGui::Selectable(
						    "(none)",
						    sel.explicitMesh.empty())) {
						sel.explicitMesh.clear();
						reg.autoSave();
						reg.markCharacterDirty(
							c->id);
					}
					for (const auto &m : meshes) {
						bool isSelected =
							(sel.explicitMesh == m);
						if (ImGui::Selectable(m.c_str(),
								      isSelected)) {
							sel.explicitMesh = m;
							reg.autoSave();
							reg.markCharacterDirty(
								c->id);
						}
					}
					ImGui::EndCombo();
				}
			} else {
				/* Layer 0 combo (base meshes like hair) */
				if (slot != "hair") {
					std::vector<Ogre::String> layer0Meshes =
						CharacterSlotSystem::
							getMeshesForLayer(
								currentAge,
								c->inlineSex, slot,
								0);
					if (!layer0Meshes.empty()) {
						Ogre::String l0Preview = "auto";
						if (!sel.layer0Mesh.empty() &&
						    sel.layer0Mesh != "none")
							l0Preview = CharacterSlotSystem::
								getMeshLabel(
									currentAge,
									c->inlineSex,
									slot,
									sel.layer0Mesh);
						if (ImGui::BeginCombo(
							    "Base (Layer 0)",
							    l0Preview.c_str())) {
							if (ImGui::Selectable(
								    "auto",
								    sel.layer0Mesh.empty() ||
									    sel.layer0Mesh ==
										    "none")) {
								sel.layer0Mesh =
									"none";
								reg.autoSave();
								reg.markCharacterDirty(
									c->id);
							}
							for (const auto &m :
							     layer0Meshes) {
								Ogre::String label =
									CharacterSlotSystem::
										getMeshLabel(
											currentAge,
											c->inlineSex,
											slot,
											m);
								bool isSelected =
									(sel.layer0Mesh ==
									 m);
								if (ImGui::Selectable(
									    label.c_str(),
									    isSelected)) {
									sel.layer0Mesh =
										m;
									reg.autoSave();
									reg.markCharacterDirty(
										c->id);
								}
							}
							ImGui::EndCombo();
						}
					}
				} else {
					ImGui::TextDisabled(
						"Base: auto (stub hair)");
				}
				/* Layer 1 combo */
				std::vector<Ogre::String> layer1Meshes =
					CharacterSlotSystem::
						getMeshesForLayer(
							currentAge,
							c->inlineSex, slot,
							1);
				Ogre::String l1Preview = "none";
				if (!sel.layer1Mesh.empty())
					l1Preview = CharacterSlotSystem::
						getMeshLabel(
							currentAge,
							c->inlineSex, slot,
							sel.layer1Mesh);
				if (ImGui::BeginCombo("Lingerie (Layer 1)",
						      l1Preview.c_str())) {
					if (ImGui::Selectable(
						    "none",
						    sel.layer1Mesh.empty() ||
							    sel.layer1Mesh ==
								    "none")) {
						sel.layer1Mesh = "none";
						reg.autoSave();
						reg.markCharacterDirty(c->id);
					}
					for (const auto &m : layer1Meshes) {
						Ogre::String label =
							CharacterSlotSystem::
								getMeshLabel(
									currentAge,
									c->inlineSex,
									slot,
									m);
						bool isSelected =
							(sel.layer1Mesh == m);
						if (ImGui::Selectable(
							    label.c_str(),
							    isSelected)) {
							sel.layer1Mesh = m;
							reg.autoSave();
							reg.markCharacterDirty(
								c->id);
						}
					}
					ImGui::EndCombo();
				}

				/* Layer 2 combo */
				std::vector<Ogre::String> layer2Meshes =
					CharacterSlotSystem::
						getMeshesForLayer(
							currentAge,
							c->inlineSex, slot,
							2);
				Ogre::String l2Preview = "none";
				if (!sel.layer2Mesh.empty())
					l2Preview = CharacterSlotSystem::
						getMeshLabel(
							currentAge,
							c->inlineSex, slot,
							sel.layer2Mesh);
				if (ImGui::BeginCombo("Clothing (Layer 2)",
						      l2Preview.c_str())) {
					if (ImGui::Selectable(
						    "none",
						    sel.layer2Mesh.empty() ||
							    sel.layer2Mesh ==
								    "none")) {
						sel.layer2Mesh = "none";
						reg.autoSave();
						reg.markCharacterDirty(c->id);
					}
					for (const auto &m : layer2Meshes) {
						Ogre::String label =
							CharacterSlotSystem::
								getMeshLabel(
									currentAge,
									c->inlineSex,
									slot,
									m);
						bool isSelected =
							(sel.layer2Mesh == m);
						if (ImGui::Selectable(
							    label.c_str(),
							    isSelected)) {
							sel.layer2Mesh = m;
							reg.autoSave();
							reg.markCharacterDirty(
								c->id);
						}
					}
					ImGui::EndCombo();
				}
			}

			ImGui::TreePop();
		}
	}

	/* Rebuild button */
	if (ImGui::Button("Rebuild")) {
		reg.markCharacterDirty(c->id);
	}
}

/* ===================================================================== */
/*  ImGui editor                                                         */
/* ===================================================================== */

void CharacterRegistry::drawEditor(bool *p_open)
{
	scanTemplates();

	if (!ImGui::Begin("Character Registry", p_open,
			  ImGuiWindowFlags_MenuBar)) {
		ImGui::End();
		return;
	}

	/* Menu bar */
	if (ImGui::BeginMenuBar()) {
		if (ImGui::BeginMenu("File")) {
			if (ImGui::MenuItem("Save Now")) {
				autoSave();
			}
			if (ImGui::MenuItem("Rescan Templates")) {
				scanTemplates();
			}
			ImGui::EndMenu();
		}
		ImGui::EndMenuBar();
	}

	const char *tabs[] = { "Characters", "Groups", "Relationships", "Names",
			       "Columns" };
	if (ImGui::BeginTabBar("RegistryTabs")) {
		for (int i = 0; i < 5; ++i) {
			if (ImGui::BeginTabItem(tabs[i])) {
				m_editorTab = i;
				ImGui::EndTabItem();
			}
		}
		ImGui::EndTabBar();
	}

	switch (m_editorTab) {
	case 0:
		/* ---------- Characters ---------- */
		static bool addRuntime = false;
		if (ImGui::Button("Add Character")) {
			uint64_t id = createCharacter("New", "Character", "",
						      addRuntime);
			m_selectedCharacterId = id;
		}
		ImGui::SameLine();
		ImGui::Checkbox("Runtime", &addRuntime);
		ImGui::SameLine();
		if (ImGui::Button("Delete") && m_selectedCharacterId != 0) {
			deleteCharacter(m_selectedCharacterId);
			m_selectedCharacterId = 0;
		}
		ImGui::Separator();

		/* Table */
		if (ImGui::BeginTable(
			    "CharactersTable", 8 + m_characterColumns.size(),
			    ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
				    ImGuiTableFlags_ScrollY,
			    ImVec2(0, 200))) {
			ImGui::TableSetupColumn(
				"ID", ImGuiTableColumnFlags_WidthFixed, 40);
			ImGui::TableSetupColumn("First Name");
			ImGui::TableSetupColumn("Last Name");
			ImGui::TableSetupColumn(
				"Age(Yrs)", ImGuiTableColumnFlags_WidthFixed,
				50);
			ImGui::TableSetupColumn(
				"Sex", ImGuiTableColumnFlags_WidthFixed, 50);
			ImGui::TableSetupColumn("Class");
			ImGui::TableSetupColumn("Prefab");
			ImGui::TableSetupColumn(
				"Actions", ImGuiTableColumnFlags_WidthFixed,
				120);
			for (const auto &c : m_characterColumns)
				ImGui::TableSetupColumn(c.name.c_str());
			ImGui::TableHeadersRow();

			for (auto &pair : m_characters) {
				CharacterRecord &c = pair.second;
				ImGui::TableNextRow();
				bool selected = (m_selectedCharacterId == c.id);
				ImGui::TableSetColumnIndex(0);
				if (ImGui::Selectable(
					    Ogre::StringConverter::toString(c.id)
						    .c_str(),
					    selected))
					m_selectedCharacterId = c.id;

				ImGui::TableSetColumnIndex(1);
				ImGui::Text("%s%s", c.persistent ? "" : "[R] ",
					    c.firstName.c_str());
				ImGui::TableSetColumnIndex(2);
				ImGui::Text("%s", c.lastName.c_str());

				std::string age = "?", sex = "?";
				int outfit = 2;
				if (!readPrefabSlots(c.prefabPath, age, sex,
						     outfit)) {
					age = c.age;
					sex = c.inlineSex;
					outfit = c.inlineOutfitLevel;
				}
				ImGui::TableSetColumnIndex(3);
				ImGui::Text("%d", c.ageYears);
				ImGui::TableSetColumnIndex(4);
				ImGui::Text("%s", sex.c_str());

				ImGui::TableSetColumnIndex(5);
				ImGui::Text("%s", c.className.c_str());

				ImGui::TableSetColumnIndex(6);
				ImGui::Text("%s", c.prefabPath.c_str());

				ImGui::TableSetColumnIndex(7);
				ImGui::PushID(static_cast<int>(c.id * 10 + 7));
				bool spawned =
					findSpawnedEntity(c.id).is_alive();
				if (spawned) {
					if (ImGui::SmallButton("Despawn"))
						despawnCharacter(c.id);
				} else {
					if (ImGui::SmallButton("Spawn"))
						spawnCharacter(c.id);
				}
				ImGui::SameLine();
				if (ImGui::SmallButton("Save")) {
					if (!savePrefabForCharacter(c.id))
						Ogre::LogManager::getSingleton()
							.logMessage(
								"CharacterRegistry: save prefab failed: " +
								m_lastError);
				}
				ImGui::PopID();

				int colIdx = 8;
				for (const auto &col : m_characterColumns) {
					ImGui::TableSetColumnIndex(colIdx++);
					switch (col.type) {
					case ColumnDef::Int: {
						auto it = c.intColumns.find(
							col.name);
						if (it != c.intColumns.end())
							ImGui::Text(
								"%lld",
								(long long)it
									->second);
						break;
					}
					case ColumnDef::Float: {
						auto it = c.floatColumns.find(
							col.name);
						if (it != c.floatColumns.end())
							ImGui::Text("%.3f",
								    it->second);
						break;
					}
					case ColumnDef::String: {
						auto it = c.stringColumns.find(
							col.name);
						if (it != c.stringColumns.end())
							ImGui::Text(
								"%s",
								it->second
									.c_str());
						break;
					}
					}
				}
			}
			ImGui::EndTable();
		}

		/* Selected character detail */
		if (m_selectedCharacterId != 0) {
			CharacterRecord *c =
				findCharacter(m_selectedCharacterId);
			if (c) {
				static char fnBuf[128] = "";
				static char lnBuf[128] = "";
				static uint64_t lastId = 0;
				if (lastId != c->id) {
					snprintf(fnBuf, sizeof(fnBuf), "%s",
						 c->firstName.c_str());
					snprintf(lnBuf, sizeof(lnBuf), "%s",
						 c->lastName.c_str());
					lastId = c->id;
				}
				ImGui::Separator();
				ImGui::Text("Character #%lu %s",
					    (unsigned long)c->id,
					    c->persistent ? "" : "[RUNTIME]");
				if (!c->persistent) {
					ImGui::SameLine();
					if (ImGui::SmallButton(
						    "Promote to Roster")) {
						/* Assign a proper persistent
						 * ID */
						uint64_t newId = m_nextId++;
						CharacterRecord promoted = *c;
						promoted.id = newId;
						promoted.persistent = true;
						promoted.prefabPath =
							generatePrefabPath(
								newId,
								c->firstName,
								c->lastName);
						m_characters.erase(c->id);
						m_characters[newId] = promoted;
						m_selectedCharacterId = newId;
						autoSave();
					}
				}

				if (ImGui::InputText("First Name", fnBuf,
						     sizeof(fnBuf))) {
					c->firstName = fnBuf;
					autoSave();
				}
				if (ImGui::InputText("Last Name", lnBuf,
						     sizeof(lnBuf))) {
					c->lastName = lnBuf;
					autoSave();
				}

				/* Name generation */
				ImGui::Separator();
				if (ImGui::Button("Generate Male First Name")) {
					std::string g =
						generateFirstName("male");
					if (!g.empty()) {
						snprintf(fnBuf, sizeof(fnBuf),
							 "%s", g.c_str());
						c->firstName = fnBuf;
						autoSave();
						learnNamesFromRegistry();
					}
				}
				ImGui::SameLine();
				if (ImGui::Button(
					    "Generate Female First Name")) {
					std::string g =
						generateFirstName("female");
					if (!g.empty()) {
						snprintf(fnBuf, sizeof(fnBuf),
							 "%s", g.c_str());
						c->firstName = fnBuf;
						autoSave();
						learnNamesFromRegistry();
					}
				}
				ImGui::SameLine();
				if (ImGui::Button("Generate Last Name")) {
					std::string g = generateLastName();
					if (!g.empty()) {
						snprintf(lnBuf, sizeof(lnBuf),
							 "%s", g.c_str());
						c->lastName = lnBuf;
						autoSave();
						learnNamesFromRegistry();
					}
				}
				ImGui::SameLine();
				if (ImGui::Button("Re-learn Names"))
					learnNamesFromRegistry();

				/* Age */
				int ageYrs = c->ageYears;
				if (ImGui::InputInt("Age (years)", &ageYrs)) {
					c->ageYears = ageYrs;
					autoSave();
				}

				/* Age category from catalog */
				CharacterSlotSystem::loadCatalog();
				std::string currentAgeCat = c->age;
				std::vector<Ogre::String> ageCats =
					CharacterSlotSystem::getAges();
				if (ImGui::BeginCombo("Age Category",
						      currentAgeCat.c_str())) {
					for (const auto &ac : ageCats) {
						bool isSelected =
							(currentAgeCat == ac);
						if (ImGui::Selectable(
							    ac.c_str(),
							    isSelected)) {
							c->age = ac;
							autoSave();
							markCharacterDirty(
								c->id);
						}
						if (isSelected)
							ImGui::SetItemDefaultFocus();
					}
					ImGui::EndCombo();
				}

				/* Outfit level */
				{
					const char *outfitLabels[] = {
						"Nude", "Lingerie", "Clothed"
					};
					int outfit = c->inlineOutfitLevel;
					if (outfit < 0)
						outfit = 0;
					if (outfit > 2)
						outfit = 2;
					if (ImGui::Combo("Outfit Level",
							 &outfit, outfitLabels,
							 3)) {
						c->inlineOutfitLevel = outfit;
						autoSave();
						markCharacterDirty(c->id);
					}
				}

				/* Appearance (formerly CharacterSlots editor) */
				ImGui::Separator();
				if (ImGui::CollapsingHeader("Appearance",
							    ImGuiTreeNodeFlags_DefaultOpen)) {
					ImGui::Indent();
					drawCharacterAppearanceEditor(*this, c);
					ImGui::Unindent();
				}

				/* Family */
				ImGui::Separator();
				ImGui::Text("Family");
				auto parents = getParents(c->id);
				if (!parents.empty()) {
					ImGui::Text("Parents:");
					for (uint64_t pid : parents) {
						const CharacterRecord *p =
							findCharacter(pid);
						if (!p)
							continue;
						char lbl[256];
						snprintf(lbl, sizeof(lbl),
							 "#%lu %s %s",
							 (unsigned long)pid,
							 p->firstName.c_str(),
							 p->lastName.c_str());
						if (ImGui::SmallButton(lbl))
							m_selectedCharacterId =
								pid;
						ImGui::SameLine();
					}
					ImGui::NewLine();
				}
				auto children = getChildren(c->id);
				if (!children.empty()) {
					ImGui::Text("Children:");
					for (uint64_t cid : children) {
						const CharacterRecord *ch =
							findCharacter(cid);
						if (!ch)
							continue;
						char lbl[256];
						snprintf(lbl, sizeof(lbl),
							 "#%lu %s %s",
							 (unsigned long)cid,
							 ch->firstName.c_str(),
							 ch->lastName.c_str());
						if (ImGui::SmallButton(lbl))
							m_selectedCharacterId =
								cid;
						ImGui::SameLine();
					}
					ImGui::NewLine();
				}
				/* Two-parent birth */
				static uint64_t partnerId = 0;
				char partnerLbl[256] = "(select partner)";
				if (partnerId != 0) {
					const CharacterRecord *pc =
						findCharacter(partnerId);
					if (pc)
						labelBuf(partnerLbl,
							 sizeof(partnerLbl),
							 pc);
					else
						partnerId = 0;
				}
				if (ImGui::BeginCombo("Partner", partnerLbl)) {
					for (const auto &pair : m_characters) {
						if (pair.second.id == c->id)
							continue;
						char lbl[256];
						labelBuf(lbl, sizeof(lbl),
							 &pair.second);
						if (ImGui::Selectable(
							    lbl,
							    partnerId ==
								    pair.second
									    .id))
							partnerId =
								pair.second.id;
					}
					ImGui::EndCombo();
				}
				if (partnerId != 0 &&
				    ImGui::Button("Create Child")) {
					uint64_t childId =
						createChild(c->id, partnerId);
					if (childId != 0)
						m_selectedCharacterId = childId;
				}

				/* Pregnancy */
				ImGui::Separator();
				ImGui::Text("Pregnancy");
				if (c->pregnantByFatherId != 0) {
					const CharacterRecord *father =
						findCharacter(
							c->pregnantByFatherId);
					ImGui::Text("Pregnant by: %s",
						    father ?
							    (father->firstName +
							     " " +
							     father->lastName)
								    .c_str() :
							    "unknown");
					float pct = 0.0f;
					if (c->pregnancyMaxProgress > 0.0f)
						pct = c->pregnancyProgress /
						      c->pregnancyMaxProgress;
					if (pct > 1.0f)
						pct = 1.0f;
					ImGui::ProgressBar(pct,
							   ImVec2(0.0f, 0.0f));
					ImGui::Text("%.1f / %.1f s",
						    c->pregnancyProgress,
						    c->pregnancyMaxProgress);
					if (ImGui::Button("Abort")) {
						abortPregnancy(c->id);
					}
					float prog = c->pregnancyProgress;
					if (ImGui::SliderFloat(
						    "Progress", &prog, 0.0f,
						    c->pregnancyMaxProgress)) {
						c->pregnancyProgress = prog;
						autoSave();
					}
				} else {
					static uint64_t fatherId = 0;
					char fatherLbl[256] = "(select father)";
					if (fatherId != 0) {
						const CharacterRecord *fc =
							findCharacter(fatherId);
						if (fc)
							labelBuf(
								fatherLbl,
								sizeof(fatherLbl),
								fc);
						else
							fatherId = 0;
					}
					if (ImGui::BeginCombo("Father",
							      fatherLbl)) {
						for (const auto &pair :
						     m_characters) {
							if (pair.second.id ==
							    c->id)
								continue;
							char lbl[256];
							labelBuf(lbl,
								 sizeof(lbl),
								 &pair.second);
							if (ImGui::Selectable(
								    lbl,
								    fatherId ==
									    pair.second
										    .id))
								fatherId =
									pair.second
										.id;
						}
						ImGui::EndCombo();
					}
					if (fatherId != 0 &&
					    ImGui::Button("Make Pregnant")) {
						conceive(c->id, fatherId);
						fatherId = 0;
					}
				}

				ImGui::Text("Prefab: %s",
					    c->prefabPath.c_str());
				bool exists =
					std::filesystem::exists(c->prefabPath);
				ImGui::Text("Status: %s",
					    exists ? "exists" : "missing");

				/* If prefab missing, offer template creation */
				if (!exists && !m_templates.empty()) {
					static int tmplIdx = 0;
					if (tmplIdx >=
					    static_cast<int>(
						    m_templates.size()))
						tmplIdx = 0;
					char comboLabel[256];
					snprintf(comboLabel, sizeof(comboLabel),
						 "%s",
						 std::filesystem::path(
							 m_templates[tmplIdx])
							 .filename()
							 .c_str());
					if (ImGui::BeginCombo("Template",
							      comboLabel)) {
						for (size_t i = 0;
						     i < m_templates.size();
						     ++i) {
							char lbl[256];
							snprintf(
								lbl,
								sizeof(lbl),
								"%s",
								std::filesystem::path(
									m_templates
										[i])
									.filename()
									.c_str());
							if (ImGui::Selectable(
								    lbl,
								    tmplIdx ==
									    static_cast<
										    int>(
										    i)))
								tmplIdx = static_cast<
									int>(i);
						}
						ImGui::EndCombo();
					}
					if (ImGui::Button(
						    "Create from Template")) {
						copyPrefab(m_templates[tmplIdx],
							   c->prefabPath);
					}
				}

				/* ---- RPG Data ---- */
				ImGui::Separator();
				ImGui::Text("RPG Data");

				/* Class selector */
				auto &db =
					CharacterClassDatabase::getSingleton();
				const auto &classNames = db.getClassNames();
				if (!classNames.empty()) {
					int clsIdx = -1;
					for (size_t i = 0;
					     i < classNames.size(); ++i) {
						if (classNames[i] ==
						    c->className) {
							clsIdx =
								static_cast<int>(
									i);
							break;
						}
					}
					char clsLbl[256];
					snprintf(clsLbl, sizeof(clsLbl), "%s",
						 clsIdx >= 0 ?
							 classNames[clsIdx]
								 .c_str() :
							 "(none)");
					if (ImGui::BeginCombo("Class",
							      clsLbl)) {
						if (ImGui::Selectable(
							    "(none)",
							    clsIdx < 0)) {
							c->className.clear();
							autoSave();
						}
						for (size_t i = 0;
						     i < classNames.size();
						     ++i) {
							if (ImGui::Selectable(
								    classNames[i]
									    .c_str(),
								    clsIdx ==
									    static_cast<
										    int>(
										    i))) {
								bool hadClass =
									!c->className
										 .empty();
								c->className =
									classNames[i]
										.c_str();
								if (!hadClass ||
								    c->stats.empty())
									initializeFromClass(
										c->id);
								autoSave();
							}
						}
						ImGui::EndCombo();
					}
				}

				int lvl = c->level;
				if (ImGui::InputInt("Level", &lvl)) {
					c->level = lvl;
					autoSave();
				}
				long long xp = c->currentXP;
				if (ImGui::InputScalar(
					    "Current XP", ImGuiDataType_S64,
					    &xp, nullptr, nullptr, "%lld")) {
					c->currentXP = xp;
					autoSave();
				}
				int pts = c->availablePoints;
				if (ImGui::InputInt("Available Points", &pts)) {
					c->availablePoints = pts;
					autoSave();
				}

				/* Stats */
				if (ImGui::TreeNode("Stats")) {
					for (const auto &name :
					     db.getStatNames()) {
						int val = 0;
						auto it = c->stats.find(
							name.c_str());
						if (it != c->stats.end())
							val = it->second;
						if (ImGui::InputInt(name.c_str(),
								    &val)) {
							c->stats[name.c_str()] =
								val;
							autoSave();
						}
					}
					ImGui::TreePop();
				}

				/* Skills */
				if (ImGui::TreeNode("Skills")) {
					for (const auto &name :
					     db.getSkillNames()) {
						int val = 0;
						auto it = c->skills.find(
							name.c_str());
						if (it != c->skills.end())
							val = it->second;
						if (ImGui::InputInt(name.c_str(),
								    &val)) {
							c->skills[name.c_str()] =
								val;
							autoSave();
						}
					}
					ImGui::TreePop();
				}

				/* Needs */
				if (ImGui::TreeNode("Needs")) {
					for (const auto &name :
					     db.getNeedNames()) {
						int val = 0;
						auto it = c->needs.find(
							name.c_str());
						if (it != c->needs.end())
							val = it->second;
						if (ImGui::InputInt(name.c_str(),
								    &val)) {
							c->needs[name.c_str()] =
								val;
							autoSave();
						}
					}
					ImGui::TreePop();
				}

				/* Current Pools */
				if (ImGui::TreeNode("Current Pools")) {
					for (const auto &name :
					     db.getStatNames()) {
						const auto *def =
							db.findStat(name);
						if (!def ||
						    def->kind !=
							    CharacterClassDatabase::
								    StatKind::ResourcePool)
							continue;
						int val = 0;
						auto it = c->currentPools.find(
							name.c_str());
						if (it != c->currentPools.end())
							val = it->second;
						if (ImGui::InputInt(name.c_str(),
								    &val)) {
							c->currentPools
								[name.c_str()] =
								val;
							autoSave();
						}
					}
					ImGui::TreePop();
				}

				/* Tags */
				ImGui::Separator();
				ImGui::Text("Tags");
				for (size_t i = 0; i < c->tags.size();) {
					ImGui::PushID(static_cast<int>(i));
					ImGui::Text("%s", c->tags[i].c_str());
					ImGui::SameLine();
					if (ImGui::SmallButton("x")) {
						c->tags.erase(c->tags.begin() +
							      i);
						autoSave();
					} else {
						++i;
					}
					ImGui::PopID();
				}
				static char tagBuf[64] = "";
				ImGui::InputText("New tag", tagBuf,
						 sizeof(tagBuf));
				ImGui::SameLine();
				if (ImGui::SmallButton("Add") &&
				    tagBuf[0] != '\0') {
					c->tags.push_back(tagBuf);
					tagBuf[0] = '\0';
					autoSave();
				}
			}
		}
		break;

	case 1:
		/* ---------- Groups ---------- */
		if (ImGui::Button("Add Group")) {
			uint64_t id = createGroup("New Group");
			m_selectedGroupId = id;
		}
		ImGui::SameLine();
		if (ImGui::Button("Delete") && m_selectedGroupId != 0) {
			deleteGroup(m_selectedGroupId);
			m_selectedGroupId = 0;
		}
		ImGui::Separator();

		if (ImGui::BeginTable("GroupsTable", 3 + m_groupColumns.size(),
				      ImGuiTableFlags_Borders |
					      ImGuiTableFlags_RowBg |
					      ImGuiTableFlags_ScrollY,
				      ImVec2(0, 200))) {
			ImGui::TableSetupColumn(
				"ID", ImGuiTableColumnFlags_WidthFixed, 40);
			ImGui::TableSetupColumn("Name");
			ImGui::TableSetupColumn("Members");
			for (const auto &c : m_groupColumns)
				ImGui::TableSetupColumn(c.name.c_str());
			ImGui::TableHeadersRow();

			for (auto &pair : m_groups) {
				GroupRecord &g = pair.second;
				ImGui::TableNextRow();
				bool selected = (m_selectedGroupId == g.id);
				ImGui::TableSetColumnIndex(0);
				if (ImGui::Selectable(
					    Ogre::StringConverter::toString(g.id)
						    .c_str(),
					    selected,
					    ImGuiSelectableFlags_SpanAllColumns))
					m_selectedGroupId = g.id;

				ImGui::TableSetColumnIndex(1);
				ImGui::Text("%s", g.name.c_str());
				ImGui::TableSetColumnIndex(2);
				ImGui::Text("%zu", g.memberIds.size());

				int colIdx = 3;
				for (const auto &col : m_groupColumns) {
					ImGui::TableSetColumnIndex(colIdx++);
					switch (col.type) {
					case ColumnDef::Int: {
						auto it = g.intColumns.find(
							col.name);
						if (it != g.intColumns.end())
							ImGui::Text(
								"%lld",
								(long long)it
									->second);
						break;
					}
					case ColumnDef::Float: {
						auto it = g.floatColumns.find(
							col.name);
						if (it != g.floatColumns.end())
							ImGui::Text("%.3f",
								    it->second);
						break;
					}
					case ColumnDef::String: {
						auto it = g.stringColumns.find(
							col.name);
						if (it != g.stringColumns.end())
							ImGui::Text(
								"%s",
								it->second
									.c_str());
						break;
					}
					}
				}
			}
			ImGui::EndTable();
		}

		if (m_selectedGroupId != 0) {
			GroupRecord *g = findGroup(m_selectedGroupId);
			if (g) {
				static char nameBuf[128] = "";
				static uint64_t lastGId = 0;
				if (lastGId != g->id) {
					snprintf(nameBuf, sizeof(nameBuf), "%s",
						 g->name.c_str());
					lastGId = g->id;
				}
				ImGui::Separator();
				ImGui::Text("Group #%lu", (unsigned long)g->id);
				if (ImGui::InputText("Name", nameBuf,
						     sizeof(nameBuf))) {
					g->name = nameBuf;
					autoSave();
				}

				ImGui::Text("Members:");
				ImGui::Indent();
				for (size_t i = 0; i < g->memberIds.size();
				     ++i) {
					uint64_t mid = g->memberIds[i];
					const CharacterRecord *mc =
						findCharacter(mid);
					char lbl[256];
					labelBuf(lbl, sizeof(lbl), mc);
					ImGui::PushID(static_cast<int>(mid));
					ImGui::Text("%s", lbl);
					ImGui::SameLine();
					if (ImGui::SmallButton("Remove")) {
						removeFromGroup(g->id, mid);
					}
					ImGui::PopID();
				}
				ImGui::Unindent();

				if (!m_characters.empty()) {
					if (ImGui::BeginCombo("Add Member",
							      "")) {
						for (const auto &pair :
						     m_characters) {
							const CharacterRecord &
								c = pair.second;
							bool inGroup =
								std::find(
									g->memberIds
										.begin(),
									g->memberIds
										.end(),
									c.id) !=
								g->memberIds
									.end();
							if (!inGroup) {
								char lbl[256];
								labelBuf(
									lbl,
									sizeof(lbl),
									&c);
								if (ImGui::Selectable(
									    lbl))
									addToGroup(
										g->id,
										c.id);
							}
						}
						ImGui::EndCombo();
					}
				}
			}
		}
		break;

	case 2:
		/* ---------- Relationships ---------- */
		ImGui::Text("Source:");
		ImGui::RadioButton("Character", &m_relSourceIsGroup, 0);
		ImGui::SameLine();
		ImGui::RadioButton("Group", &m_relSourceIsGroup, 1);

		{
			char srcLbl[256] = "(none)";
			if (m_relSourceId != 0) {
				if (m_relSourceIsGroup)
					labelBuf(srcLbl, sizeof(srcLbl),
						 findGroup(m_relSourceId));
				else
					labelBuf(srcLbl, sizeof(srcLbl),
						 findCharacter(m_relSourceId));
			}
			if (ImGui::BeginCombo("Select Source", srcLbl)) {
				if (!m_relSourceIsGroup) {
					for (const auto &pair : m_characters) {
						char lbl[256];
						labelBuf(lbl, sizeof(lbl),
							 &pair.second);
						if (ImGui::Selectable(lbl))
							m_relSourceId =
								pair.second.id;
					}
				} else {
					for (const auto &pair : m_groups) {
						char lbl[256];
						labelBuf(lbl, sizeof(lbl),
							 &pair.second);
						if (ImGui::Selectable(lbl))
							m_relSourceId =
								pair.second.id;
					}
				}
				ImGui::EndCombo();
			}
		}

		if (m_relSourceId != 0) {
			ImGui::Separator();
			ImGui::Text("Relationships:");

			auto rels = getRelationships(m_relSourceId);
			for (const Relationship *r : rels) {
				bool isOutgoing =
					(r->sourceId == m_relSourceId &&
					 r->sourceIsGroup ==
						 (m_relSourceIsGroup != 0));
				uint64_t otherId = isOutgoing ? r->targetId :
								r->sourceId;
				bool otherIsGroup = isOutgoing ?
							    r->targetIsGroup :
							    r->sourceIsGroup;
				char otherLbl[256];
				if (otherIsGroup)
					labelBuf(otherLbl, sizeof(otherLbl),
						 findGroup(otherId));
				else
					labelBuf(otherLbl, sizeof(otherLbl),
						 findCharacter(otherId));

				ImGui::PushID(static_cast<int>(
					r->sourceId * 1000000 + r->targetId));
				ImGui::Text("%s %s %s", isOutgoing ? "→" : "←",
					    otherLbl,
					    otherIsGroup ? "[group]" : "");

				for (auto &kv : r->stats) {
					float v = kv.second;
					if (ImGui::SliderFloat(kv.first.c_str(),
							       &v, -1.0f,
							       1.0f)) {
						setRelationshipStat(
							r->sourceId,
							r->sourceIsGroup,
							r->targetId,
							r->targetIsGroup,
							kv.first, v);
					}
				}

				ImGui::Text("Tags:");
				ImGui::SameLine();
				for (const auto &tag : r->tags) {
					ImGui::Text("%s", tag.c_str());
					ImGui::SameLine();
				}
				ImGui::NewLine();

				static char newTag[64] = "";
				ImGui::InputText("Tag", newTag, sizeof(newTag));
				ImGui::SameLine();
				if (ImGui::SmallButton("Add Tag") &&
				    strlen(newTag) > 0) {
					addRelationshipTag(r->sourceId,
							   r->sourceIsGroup,
							   r->targetId,
							   r->targetIsGroup,
							   newTag);
					if (strcmp(newTag, "wife") == 0 ||
					    strcmp(newTag, "husband") == 0) {
						addRelationshipTag(
							r->targetId,
							r->targetIsGroup,
							r->sourceId,
							r->sourceIsGroup,
							strcmp(newTag,
							       "wife") == 0 ?
								"husband" :
								"wife");
					} else if (strcmp(newTag, "sibling") ==
						   0) {
						addRelationshipTag(
							r->targetId,
							r->targetIsGroup,
							r->sourceId,
							r->sourceIsGroup,
							"sibling");
					} else if (strcmp(newTag, "parent") ==
						   0) {
						addRelationshipTag(
							r->targetId,
							r->targetIsGroup,
							r->sourceId,
							r->sourceIsGroup,
							"child");
					} else if (strcmp(newTag, "child") ==
						   0) {
						addRelationshipTag(
							r->targetId,
							r->targetIsGroup,
							r->sourceId,
							r->sourceIsGroup,
							"parent");
					}
					newTag[0] = '\0';
				}

				if (ImGui::SmallButton("Delete Relationship")) {
					std::vector<Relationship> newRels;
					for (const auto &existing :
					     m_relationships) {
						if (!(existing.sourceId ==
							      r->sourceId &&
						      existing.sourceIsGroup ==
							      r->sourceIsGroup &&
						      existing.targetId ==
							      r->targetId &&
						      existing.targetIsGroup ==
							      r->targetIsGroup))
							newRels.push_back(
								existing);
					}
					m_relationships = std::move(newRels);
					rebuildIndexes();
				}
				ImGui::PopID();
				ImGui::Separator();
			}

			/* Add new relationship */
			ImGui::Text("Add new relationship:");
			static int newTargetIsGroup = 0;
			static uint64_t newTargetId = 0;
			ImGui::RadioButton("To Character", &newTargetIsGroup,
					   0);
			ImGui::SameLine();
			ImGui::RadioButton("To Group", &newTargetIsGroup, 1);

			{
				char tgtLbl[256] = "(select)";
				if (newTargetId != 0) {
					if (newTargetIsGroup)
						labelBuf(
							tgtLbl, sizeof(tgtLbl),
							findGroup(newTargetId));
					else
						labelBuf(tgtLbl, sizeof(tgtLbl),
							 findCharacter(
								 newTargetId));
				}
				if (ImGui::BeginCombo("Target", tgtLbl)) {
					if (!newTargetIsGroup) {
						for (const auto &pair :
						     m_characters) {
							if (pair.second.id ==
							    m_relSourceId)
								continue;
							char lbl[256];
							labelBuf(lbl,
								 sizeof(lbl),
								 &pair.second);
							if (ImGui::Selectable(
								    lbl))
								newTargetId =
									pair.second
										.id;
						}
					} else {
						for (const auto &pair :
						     m_groups) {
							char lbl[256];
							labelBuf(lbl,
								 sizeof(lbl),
								 &pair.second);
							if (ImGui::Selectable(
								    lbl))
								newTargetId =
									pair.second
										.id;
						}
					}
					ImGui::EndCombo();
				}
			}

			if (newTargetId != 0 &&
			    ImGui::Button("Create Relationship")) {
				setRelationshipStat(m_relSourceId,
						    m_relSourceIsGroup,
						    newTargetId,
						    newTargetIsGroup,
						    "friendship", 0.0f);
				newTargetId = 0;
			}
		}
		break;

	case 3: {
		/* ---------- Names ---------- */
		if (ImGui::Button("Re-learn All Names"))
			rebuildNameGenerators();
		ImGui::Separator();

		/* Preview samples - cached to avoid flickering */
		{
			static float lastSampleTime = -1.0f;
			static std::vector<std::string> maleSamples;
			static std::vector<std::string> femaleSamples;
			static std::vector<std::string> lastSamples;
			if (ImGui::GetTime() - lastSampleTime > 1.0f) {
				lastSampleTime = ImGui::GetTime();
				maleSamples = m_maleFirstNameGen.generateMany(
					5, 3, 12, nullptr);
				femaleSamples =
					m_femaleFirstNameGen.generateMany(
						5, 3, 12, nullptr);
				lastSamples = m_lastNameGen.generateMany(
					5, 3, 12, nullptr);
			}
			if (!m_maleFirstNameGen.empty()) {
				ImGui::TextDisabled("Male first name samples:");
				for (size_t i = 0; i < maleSamples.size();
				     ++i) {
					if (i > 0)
						ImGui::SameLine();
					ImGui::TextDisabled(
						"%s", maleSamples[i].c_str());
				}
			}
			if (!m_femaleFirstNameGen.empty()) {
				ImGui::TextDisabled(
					"Female first name samples:");
				for (size_t i = 0; i < femaleSamples.size();
				     ++i) {
					if (i > 0)
						ImGui::SameLine();
					ImGui::TextDisabled(
						"%s", femaleSamples[i].c_str());
				}
			}
			if (!m_lastNameGen.empty()) {
				ImGui::TextDisabled("Last name samples:");
				for (size_t i = 0; i < lastSamples.size();
				     ++i) {
					if (i > 0)
						ImGui::SameLine();
					ImGui::TextDisabled(
						"%s", lastSamples[i].c_str());
				}
			}
		}

		/* Seed name lists with stored seeds */
		auto drawSeedList = [&](const char *label,
					std::vector<std::string> &seeds) {
			ImGui::Text("%s (%zu):", label, seeds.size());
			for (size_t i = 0; i < seeds.size();) {
				ImGui::PushID(static_cast<int>(i + 5000));
				ImGui::Text("%s", seeds[i].c_str());
				ImGui::SameLine();
				if (ImGui::SmallButton("x")) {
					seeds.erase(seeds.begin() + i);
					autoSave();
				} else {
					++i;
				}
				ImGui::PopID();
			}
		};

		drawSeedList("Male first name seeds", m_maleFirstNameSeeds);
		drawSeedList("Female first name seeds", m_femaleFirstNameSeeds);
		drawSeedList("Last name seeds", m_lastNameSeeds);

		/* Bulk add seeds */
		static char maleSeedBuf[4096] = "";
		static char femaleSeedBuf[4096] = "";
		static char lastSeedBuf[4096] = "";
		ImGui::InputTextMultiline("Male first names", maleSeedBuf,
					  sizeof(maleSeedBuf), ImVec2(0, 60));
		ImGui::InputTextMultiline("Female first names", femaleSeedBuf,
					  sizeof(femaleSeedBuf), ImVec2(0, 60));
		ImGui::InputTextMultiline("Last names", lastSeedBuf,
					  sizeof(lastSeedBuf), ImVec2(0, 60));
		if (ImGui::Button("Add Seeds")) {
			auto parseLines = [](const char *buf) {
				std::vector<std::string> lines;
				const char *p = buf;
				while (*p) {
					const char *end = p;
					while (*end && *end != '\n' &&
					       *end != '\r')
						++end;
					std::string line(p, end - p);
					size_t start =
						line.find_first_not_of(" \t");
					if (start != std::string::npos) {
						size_t last =
							line.find_last_not_of(
								" \t");
						lines.push_back(line.substr(
							start,
							last - start + 1));
					}
					p = end;
					while (*p == '\n' || *p == '\r')
						++p;
				}
				return lines;
			};
			for (const auto &name : parseLines(maleSeedBuf))
				m_maleFirstNameSeeds.push_back(name);
			for (const auto &name : parseLines(femaleSeedBuf))
				m_femaleFirstNameSeeds.push_back(name);
			for (const auto &name : parseLines(lastSeedBuf))
				m_lastNameSeeds.push_back(name);
			maleSeedBuf[0] = '\0';
			femaleSeedBuf[0] = '\0';
			lastSeedBuf[0] = '\0';
			rebuildNameGenerators();
			autoSave();
		}

		/* Pregnancy global config */
		ImGui::Separator();
		ImGui::Text("Pregnancy");
		float baseDur = m_basePregnancyDuration;
		if (ImGui::InputFloat("Base Duration (s)", &baseDur)) {
			m_basePregnancyDuration = baseDur;
			autoSave();
		}
		float ts = m_pregnancyTimeScale;
		if (ImGui::InputFloat("Time Scale", &ts)) {
			m_pregnancyTimeScale = ts;
			autoSave();
		}

		/* Birth configuration */
		ImGui::Separator();
		ImGui::Text("Birth Config");
		ImGui::TextDisabled("Shape keys randomized at birth:");
		for (size_t i = 0; i < m_birthRandomizableShapeKeys.size();) {
			ImGui::PushID(static_cast<int>(i));
			ImGui::Text("%s",
				    m_birthRandomizableShapeKeys[i].c_str());
			ImGui::SameLine();
			if (ImGui::SmallButton("x")) {
				m_birthRandomizableShapeKeys.erase(
					m_birthRandomizableShapeKeys.begin() +
					i);
				autoSave();
			} else {
				++i;
			}
			ImGui::PopID();
		}
		static char rkBuf[64] = "";
		ImGui::InputText("Add key", rkBuf, sizeof(rkBuf));
		ImGui::SameLine();
		if (ImGui::SmallButton("Add") && rkBuf[0] != '\0') {
			m_birthRandomizableShapeKeys.push_back(rkBuf);
			rkBuf[0] = '\0';
			autoSave();
		}

		ImGui::TextDisabled(
			"Excluded keys (never copied or randomized):");
		for (size_t i = 0; i < m_birthExcludedShapeKeys.size();) {
			ImGui::PushID(static_cast<int>(i + 1000));
			ImGui::Text("%s", m_birthExcludedShapeKeys[i].c_str());
			ImGui::SameLine();
			if (ImGui::SmallButton("x")) {
				m_birthExcludedShapeKeys.erase(
					m_birthExcludedShapeKeys.begin() + i);
				autoSave();
			} else {
				++i;
			}
			ImGui::PopID();
		}
		static char ekBuf[64] = "";
		ImGui::InputText("Add excluded", ekBuf, sizeof(ekBuf));
		ImGui::SameLine();
		if (ImGui::SmallButton("Add") && ekBuf[0] != '\0') {
			m_birthExcludedShapeKeys.push_back(ekBuf);
			ekBuf[0] = '\0';
			autoSave();
		}
		break;
	}

	case 4:
		/* ---------- Columns ---------- */
		drawColumnsEditor("Character Columns", m_characterColumns,
				  *this);
		ImGui::Separator();
		drawColumnsEditor("Group Columns", m_groupColumns, *this);
		break;
	}

	ImGui::End();
}
