import BaseService from './BaseService.js';
import { Sequelize, Op } from 'sequelize';
import { sequelize } from '../utils/sequelize.js';
import FalukantPredefineFirstname from '../models/falukant/predefine/firstname.js';
import FalukantPredefineLastname from '../models/falukant/predefine/lastname.js';
import FalukantUser from '../models/falukant/data/user.js';
import FalukantCharacter from '../models/falukant/data/character.js';
import RegionData from '../models/falukant/data/region.js';
import RegionType from '../models/falukant/type/region.js';
import FalukantStock from '../models/falukant/data/stock.js';
import FalukantStockType from '../models/falukant/type/stock.js';
import TitleOfNobility from '../models/falukant/type/title_of_nobility.js';
import Branch from '../models/falukant/data/branch.js';
import BranchType from '../models/falukant/type/branch.js';
import Production from '../models/falukant/data/production.js';
import ProductType from '../models/falukant/type/product.js';
import Knowledge from '../models/falukant/data/product_knowledge.js';
import Inventory from '../models/falukant/data/inventory.js';
import MoneyFlow from '../models/falukant/log/moneyflow.js';
import User from '../models/community/user.js';
import { notifyUser } from '../utils/socket.js';
import { differenceInDays } from 'date-fns';
import { updateFalukantUserMoney } from '../utils/sequelize.js';
import BuyableStock from '../models/falukant/data/buyable_stock.js';
import DirectorProposal from '../models/falukant/data/director_proposal.js';
import Director from '../models/falukant/data/director.js';
import DaySell from '../models/falukant/log/daysell.js';
import MarriageProposal from '../models/falukant/data/marriage_proposal.js';
import RelationshipType from '../models/falukant/type/relationship.js';
import Relationship from '../models/falukant/data/relationship.js';
import PromotionalGift from '../models/falukant/type/promotional_gift.js';
import PromotionalGiftCharacterTrait from '../models/falukant/predefine/promotional_gift_character_trait.js';
import PromotionalGiftMood from '../models/falukant/predefine/promotional_gift_mood.js';
import PromotionalGiftLog from '../models/falukant/log/promotional_gift.js';
import CharacterTrait from '../models/falukant/type/character_trait.js';
import FalukantCharacterTrait from '../models/falukant/data/falukant_character_trait.js';
import Mood from '../models/falukant/type/mood.js';
import UserHouse from '../models/falukant/data/user_house.js';
import HouseType from '../models/falukant/type/house.js';
import BuyableHouse from '../models/falukant/data/buyable_house.js';
import PartyType from '../models/falukant/type/party.js';
import MusicType from '../models/falukant/type/music.js';
import BanquetteType from '../models/falukant/type/banquette.js';
import Party from '../models/falukant/data/party.js';
import ChildRelation from '../models/falukant/data/child_relation.js';
import Learning from '../models/falukant/data/learning.js';
import LearnRecipient from '../models/falukant/type/learn_recipient.js';
import Credit from '../models/falukant/data/credit.js';
import TitleRequirement from '../models/falukant/type/title_requirement.js';
import HealthActivity from '../models/falukant/log/health_activity.js';
import Election from '../models/falukant/data/election.js';
import PoliticalOfficeType from '../models/falukant/type/political_office_type.js';
import Candidate from '../models/falukant/data/candidate.js';
import Vote from '../models/falukant/data/vote.js';
import PoliticalOfficePrerequisite from '../models/falukant/predefine/political_office_prerequisite.js';
import PoliticalOfficeHistory from '../models/falukant/log/political_office_history.js';
import UndergroundType from '../models/falukant/type/underground.js';
import Notification from '../models/falukant/log/notification.js';
import PoliticalOffice from '../models/falukant/data/political_office.js';
import Underground from '../models/falukant/data/underground.js';
import VehicleType from '../models/falukant/type/vehicle.js';
import Vehicle from '../models/falukant/data/vehicle.js';
import Transport from '../models/falukant/data/transport.js';
import RegionDistance from '../models/falukant/data/region_distance.js';
import Weather from '../models/falukant/data/weather.js';
import TownProductWorth from '../models/falukant/data/town_product_worth.js';
import ProductWeatherEffect from '../models/falukant/type/product_weather_effect.js';
import WeatherType from '../models/falukant/type/weather.js';
import ReputationActionType from '../models/falukant/type/reputation_action.js';
import ReputationActionLog from '../models/falukant/log/reputation_action.js';

function calcAge(birthdate) {
    const b = new Date(birthdate); b.setHours(0, 0);
    const now = new Date(); now.setHours(0, 0);
    return differenceInDays(now, b);
}

async function getFalukantUserOrFail(hashedId, options = {}) {
    const user = await FalukantUser.findOne({
        include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }],
        transaction: options.transaction
    });
    if (!user) throw new Error('User not found');
    return user;
}

async function getBranchOrFail(userId, branchId) {
    const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: userId } });
    if (!branch) throw new Error('Branch not found');
    return branch;
}

function calcSellPrice(product, knowledgeFactor = 0) {
    const max = product.sellCost;
    const min = max * 0.6;
    return min + (max - min) * (knowledgeFactor / 100);
}

// Synchron version für Batch-Operationen (ohne DB-Query)
function calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent = 50) {
    const basePrice = product.sellCost * (worthPercent / 100);
    const min = basePrice * 0.6;
    const max = basePrice;
    return min + (max - min) * (knowledgeFactor / 100);
}

async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
    // Wenn worthPercent nicht übergeben wurde, hole es aus der Datenbank
    if (worthPercent === null) {
        const townWorth = await TownProductWorth.findOne({
            where: { productId: product.id, regionId: regionId }
        });
        worthPercent = townWorth?.worthPercent || 50; // Default 50% wenn nicht gefunden
    }
    
    // Basispreis basierend auf regionalem worthPercent
    const basePrice = product.sellCost * (worthPercent / 100);
    
    // Dann Knowledge-Faktor anwenden
    const min = basePrice * 0.6;
    const max = basePrice;
    return min + (max - min) * (knowledgeFactor / 100);
}

    // Sum tax_percent for a region and its ancestors (upwards). Returns numeric percent (e.g. 7.5)
    async function getCumulativeTaxPercent(regionId) {
        if (!regionId) return 0;
        const rows = await sequelize.query(
            `WITH RECURSIVE ancestors AS (
                SELECT id, parent_id, tax_percent
                FROM falukant_data.region r
                WHERE id = :id
              UNION ALL
                SELECT reg.id, reg.parent_id, reg.tax_percent
                FROM falukant_data.region reg
                JOIN ancestors a ON reg.id = a.parent_id
            )
            SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;`,
            {
                replacements: { id: regionId },
                type: sequelize.QueryTypes.SELECT
            }
        );
        const val = rows?.[0]?.total ?? 0;
        return parseFloat(val) || 0;
    }

    // Returns cumulative tax percent for a region, but excludes regions where the user holds
    // a political office that grants tax exemption according to the rules.
    // exemptionsMap maps political office.name -> array of regionType labelTr that are exempted
    const POLITICAL_TAX_EXEMPTIONS = {
        'council': ['city'],
        'taxman': ['city', 'county'],
        'treasurerer': ['city', 'county', 'shire'],
        'super-state-administrator': ['city', 'county', 'shire', 'markgrave', 'duchy'],
        'chancellor': ['city','county','shire','markgrave','duchy','duchy'] // chancellor = all types; we'll handle as wildcard
    };

    async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
        if (!regionId) return 0;
        // fetch user's political offices (active) and their region types
        // PoliticalOffice hat keine userId Spalte, daher müssen wir zuerst den Character finden
        const character = await FalukantCharacter.findOne({
            where: { userId },
            attributes: ['id']
        });
        if (!character) {
            // Wenn kein Character existiert, gibt es keine politischen Ämter
            return 0;
        }
        
        const offices = await PoliticalOffice.findAll({
            where: { characterId: character.id },
            include: [
                {
                    model: PoliticalOfficeType,
                    as: 'type',
                    attributes: ['name']
                },
                {
                    model: RegionData,
                    as: 'region',
                    include: [{
                        model: RegionType,
                        as: 'regionType',
                        attributes: ['labelTr']
                    }]
                }
            ]
        });

        // build set of exempt region type labels from user's offices
        const exemptTypes = new Set();
        let hasChancellor = false;
        for (const o of offices) {
            const name = o.type?.name;
            if (!name) continue;
            if (name === 'chancellor') { hasChancellor = true; break; }
            const allowed = POLITICAL_TAX_EXEMPTIONS[name];
            if (allowed && Array.isArray(allowed)) {
                for (const t of allowed) exemptTypes.add(t);
            }
        }

        // If chancellor, exempt all region types -> tax = 0
        if (hasChancellor) return 0;

        // Now compute cumulative tax but exclude regions whose regionType.labelTr is in exemptTypes
        // Konvertiere exemptTypes Set zu einem PostgreSQL-Array-String
        const exemptTypesArray = Array.from(exemptTypes);
        const exemptTypesString = exemptTypesArray.length > 0 
            ? `ARRAY[${exemptTypesArray.map(t => `'${t.replace(/'/g, "''")}'`).join(',')}]`
            : `ARRAY[]::text[]`;
        
        const rows = await sequelize.query(
            `WITH RECURSIVE ancestors AS (
                SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
                FROM falukant_data.region r
                JOIN falukant_type.region rt ON rt.id = r.region_type_id
                WHERE r.id = :id
              UNION ALL
                SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
                FROM falukant_data.region reg
                JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
                JOIN ancestors a ON reg.id = a.parent_id
            )
            SELECT COALESCE(SUM(CASE WHEN ${exemptTypesString} && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;`,
            {
                replacements: { id: regionId },
                type: sequelize.QueryTypes.SELECT
            }
        );
        const val = rows?.[0]?.total ?? 0;
        return parseFloat(val) || 0;
    }

function calculateMarriageCost(titleOfNobility, age) {
    const minTitle = 1;
    const adjustedTitle = titleOfNobility - minTitle + 1;
    const baseCost = 500;
    return baseCost * Math.pow(adjustedTitle, 1.3) - (age - 12) * 20;
}

async function computeShortestRoute(transportMode, sourceRegionId, targetRegionId) {
    const src = parseInt(sourceRegionId, 10);
    const tgt = parseInt(targetRegionId, 10);
    if (Number.isNaN(src) || Number.isNaN(tgt)) {
        throw new Error('Invalid region ids for route calculation');
    }
    if (src === tgt) {
        return { distance: 0, path: [src] };
    }

    const rows = await RegionDistance.findAll({
        where: { transportMode },
        attributes: ['sourceRegionId', 'targetRegionId', 'distance'],
    });

    if (!rows.length) {
        return null;
    }

    const adj = new Map();
    for (const r of rows) {
        const a = r.sourceRegionId;
        const b = r.targetRegionId;
        const d = r.distance;
        if (!adj.has(a)) adj.set(a, []);
        if (!adj.has(b)) adj.set(b, []);
        adj.get(a).push({ to: b, w: d });
        adj.get(b).push({ to: a, w: d });
    }

    if (!adj.has(src) || !adj.has(tgt)) {
        return null;
    }

    const dist = new Map();
    const prev = new Map();
    const visited = new Set();

    for (const node of adj.keys()) {
        dist.set(node, Number.POSITIVE_INFINITY);
    }
    dist.set(src, 0);

    while (true) {
        let u = null;
        let best = Number.POSITIVE_INFINITY;
        for (const [node, d] of dist.entries()) {
            if (!visited.has(node) && d < best) {
                best = d;
                u = node;
            }
        }
        if (u === null || u === tgt) break;
        visited.add(u);
        const neighbors = adj.get(u) || [];
        for (const { to, w } of neighbors) {
            if (visited.has(to)) continue;
            const alt = dist.get(u) + w;
            if (alt < dist.get(to)) {
                dist.set(to, alt);
                prev.set(to, u);
            }
        }
    }

    if (!prev.has(tgt) && tgt !== src) {
        return null;
    }

    const path = [];
    let cur = tgt;
    while (cur !== undefined) {
        path.unshift(cur);
        cur = prev.get(cur);
    }

    return { distance: dist.get(tgt), path };
}

class PreconditionError extends Error {
    constructor(label) {
        super(label);
        this.name = 'PreconditionError';
        this.status = 412;
    }
}

class FalukantService extends BaseService {
    static KNOWLEDGE_MAX = 99;
    static REPUTATION_ACTION_DAILY_CAP = Number(process.env.FALUKANT_REPUTATION_ACTION_DAILY_CAP ?? 10);
    static REPUTATION_ACTION_COOLDOWN_MINUTES = Number(process.env.FALUKANT_REPUTATION_ACTION_COOLDOWN_MINUTES ?? 60);
    static RANDOM_EVENT_DAILY_ENABLED = String(process.env.FALUKANT_RANDOM_EVENT_DAILY_ENABLED ?? '1') === '1';
    static COST_CONFIG = {
        one: { min: 50, max: 5000 },
        all: { min: 400, max: 40000 }
    };
    static HEALTH_ACTIVITIES = [
        { tr: "barber", method: "healthBarber", cost: 10 },
        { tr: "doctor", method: "healthDoctor", cost: 50 },
        { tr: "witch", method: "healthWitch", cost: 500 },
        { tr: "pill", method: "healthPill", cost: 5000 },
        { tr: "drunkOfLife", method: "healthDrunkOfLife", cost: 5000000 }
    ];

    static RECURSIVE_REGION_SEARCH = `
        WITH RECURSIVE ancestors AS (
          SELECT
            r.id,
            r.parent_id
          FROM falukant_data.region r
          join falukant_data."character" c 
            on c.region_id = r.id
          WHERE c.user_id = :user_id
          UNION ALL
          SELECT
            r.id,
            r.parent_id
          FROM falukant_data.region r
          JOIN ancestors a ON r.id = a.parent_id
        )
        SELECT id
        FROM ancestors;
    `;

    async getFalukantUserByHashedId(hashedId) {
        console.log('🔍 getFalukantUserByHashedId called with hashedId:', hashedId);
        const user = await FalukantUser.findOne({
            include: [
                { model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } },
                {
                    model: FalukantCharacter,
                    as: 'character',
                    include: [
                        { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                        { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
                        { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr', 'id'] },
                        { model: CharacterTrait, as: 'traits', attributes: ['id', 'tr'] }
                    ],
                    attributes: ['id', 'birthdate', 'gender', 'moodId', 'health', 'reputation']
                },
                {
                    model: UserHouse,
                    as: 'userHouse',
                    attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'],
                    include: [
                        {
                            model: HouseType,
                            as: 'houseType',
                            attributes: ['labelTr', 'position']
                        }
                    ]
                },
            ]
        });
        console.log('🔍 getFalukantUserByHashedId result:', user ? 'User found' : 'User not found');
        if (!user) throw new Error('User not found');
        return user;
    }

    async getUser(hashedUserId) {
        const u = await FalukantUser.findOne({
            include: [
                { model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId: hashedUserId } },
                {
                    model: FalukantCharacter,
                    as: 'character',
                    include: [
                        { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                        { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
                        { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
                    ],
                    attributes: ['birthdate', 'gender']
                },
                {
                    model: RegionData,
                    as: 'mainBranchRegion',
                    include: [{ model: RegionType, as: 'regionType' }],
                    attributes: ['id', 'name']
                },
                {
                    model: Branch,
                    as: 'branches',
                    include: [
                        { model: BranchType, as: 'branchType', attributes: ['labelTr'] },
                        {
                            model: RegionData,
                            as: 'region',
                            include: [{ model: RegionType, as: 'regionType' }],
                            attributes: ['name']
                        }
                    ]
                },
                {
                    model: UserHouse,
                    as: 'userHouse',
                    include: [
                        {
                            model: HouseType,
                            as: 'houseType',
                            'attributes': ['labelTr', 'position']
                        },
                    ],
                    attributes: ['roofCondition'],
                },
            ],
            attributes: ['money', 'creditAmount', 'todayCreditTaken',]
        });
        if (!u) throw new Error('User not found');
        if (u.character?.birthdate) u.character.setDataValue('age', calcAge(u.character.birthdate));
        return u;
    }

    async randomFirstName(gender) {
        const names = await FalukantPredefineFirstname.findAll({ where: { gender } });
        return names[Math.floor(Math.random() * names.length)].name;
    }

    async randomLastName() {
        const names = await FalukantPredefineLastname.findAll();
        return names[Math.floor(Math.random() * names.length)].name;
    }

    async createUser(hashedUserId, gender, firstName, lastName) {
        const user = await this.getUserByHashedId(hashedUserId);
        if (await FalukantUser.findOne({ where: { userId: user.id } })) throw new Error('User already exists in Falukant.');
        let fnObj = await FalukantPredefineFirstname.findOne({ where: { name: firstName } });
        let lnObj = await FalukantPredefineLastname.findOne({ where: { name: lastName } });
        if (!fnObj) fnObj = await FalukantPredefineFirstname.create({ name: firstName, gender });
        if (!lnObj) lnObj = await FalukantPredefineLastname.create({ name: lastName });
        const region = await RegionData.findOne({
            order: Sequelize.fn('RANDOM'),
            limit: 1,
            include: [{ model: RegionType, as: 'regionType', where: { labelTr: 'city' } }]
        });
        if (!region) throw new Error('No region found with the label "city".');
        const nobility = await TitleOfNobility.findOne({ where: { labelTr: 'noncivil' } });
        if (!nobility) throw new Error('No title of nobility found with the label "noncivil".');
        const falukantUser = await FalukantUser.create({
            userId: user.id, money: 50, creditAmount: 0, todayCreditTaken: 0, creditInterestRate: 0, mainBranchRegionId: region.id
        });
        const date = new Date(); date.setDate(date.getDate() - 14);
        const ch = await FalukantCharacter.create({
            userId: falukantUser.id, regionId: region.id, firstName: fnObj.id, lastName: lnObj.id, gender, birthdate: date, titleOfNobility: nobility.id
        });
        falukantUser.character = ch;
        const bType = await BranchType.findOne({ where: { labelTr: 'fullstack' } });
        const branch = await Branch.create({ falukantUserId: falukantUser.id, regionId: region.id, branchTypeId: bType.id });
        const stType = await FalukantStockType.findOne({ where: [{ label_tr: 'wood' }] });
        await FalukantStock.create({ branchId: branch.id, stockTypeId: stType.id, quantity: 20 });
        notifyUser(user.hashedId, 'reloadmenu', {});
        return falukantUser;
    }

    async getInfo(hashedUserId) {
        const user = await User.findOne({ where: { hashedId: hashedUserId } });
        if (!user) throw new Error('User not found');
        const falukantUser = await FalukantUser.findOne({
            include: [
                { model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } },
                {
                    model: FalukantCharacter,
                    as: 'character',
                    attributes: ['id', 'regionId', 'birthdate', 'health', 'reputation'],
                    include: [
                        {
                            model: Relationship,
                            as: 'relationshipsAsCharacter1',
                            required: false,
                            attributes: ['id', 'character2Id', 'relationshipTypeId'],
                            include: [{
                                model: RelationshipType,
                                as: 'relationshipType',
                                attributes: ['tr'],
                                where: { tr: { [Op.not]: 'lover' } }
                            }]
                        },
                        {
                            model: Relationship,
                            as: 'relationshipsAsCharacter2',
                            required: false,
                            attributes: ['id', 'character1Id', 'relationshipTypeId'],
                            include: [{
                                model: RelationshipType,
                                as: 'relationshipType',
                                attributes: ['tr'],
                                where: { tr: { [Op.not]: 'lover' } }
                            }]
                        }
                    ]
                },
            ],
            attributes: ['id', 'money']
        });
        if (!falukantUser) throw new Error('User not found');
        if (falukantUser.character?.birthdate) falukantUser.character.setDataValue('age', calcAge(falukantUser.character.birthdate));

        // Aggregate status additions: children counts and unread notifications
        try {
            const bm = (step, payload = {}) => {
                try { console.log(`[BLOCKMARKER][falukant.getInfo] ${step}`, payload); } catch (_) { /* ignore */ }
            };
            bm('aggregate.start', { userId: user.id, falukantUserId: falukantUser.id });
            // Determine all character IDs belonging to the user
            if (!falukantUser.id) {
                bm('aggregate.noFalukantUserId');
                throw new Error('Missing falukantUser.id in getInfo aggregation');
            }
            const userCharacterIdsRows = await FalukantCharacter.findAll({
                attributes: ['id'],
                where: { userId: falukantUser.id },
                raw: true
            });
            const userCharacterIds = userCharacterIdsRows.map(r => r.id);
            bm('aggregate.userCharacters', { count: userCharacterIds.length, ids: userCharacterIds.slice(0, 5) });

            // Daily random event (once per calendar day per user) -> stored as Notification random_event.*
            // Frontend already supports JSON-encoded tr: {"tr":"random_event.windfall","amount":123}
            try {
                if (FalukantService.RANDOM_EVENT_DAILY_ENABLED) {
                    await this._maybeCreateDailyRandomEvent(falukantUser, user);
                }
            } catch (e) {
                console.warn('[Falukant] daily random event failed (non-fatal):', e?.message || e);
            }

            // Count distinct children for any of the user's characters (as father or mother)
            let childrenCount = 0;
            let unbaptisedChildrenCount = 0;
            if (userCharacterIds.length > 0) {
                const childRels = await ChildRelation.findAll({
                    attributes: ['childCharacterId'],
                    where: {
                        [Op.or]: [
                            { fatherCharacterId: { [Op.in]: userCharacterIds } },
                            { motherCharacterId: { [Op.in]: userCharacterIds } },
                        ]
                    },
                    raw: true
                });
                const distinctChildIds = new Set(childRels.map(r => r.childCharacterId));
                childrenCount = distinctChildIds.size;
                bm('aggregate.children', { relations: childRels.length, distinct: childrenCount, sample: Array.from(distinctChildIds).slice(0, 5) });

                const unbaptised = await ChildRelation.findAll({
                    attributes: ['childCharacterId'],
                    where: {
                        nameSet: false,
                        [Op.or]: [
                            { fatherCharacterId: { [Op.in]: userCharacterIds } },
                            { motherCharacterId: { [Op.in]: userCharacterIds } },
                        ]
                    },
                    raw: true
                });
                const distinctUnbaptisedIds = new Set(unbaptised.map(r => r.childCharacterId));
                unbaptisedChildrenCount = distinctUnbaptisedIds.size;
                bm('aggregate.unbaptised', { relations: unbaptised.length, distinct: unbaptisedChildrenCount, sample: Array.from(distinctUnbaptisedIds).slice(0, 5) });
            }

            // Unread notifications count
            const unreadNotifications = await Notification.count({ where: { userId: falukantUser.id, shown: false } });
            bm('aggregate.unread', { unreadNotifications });

            falukantUser.setDataValue('childrenCount', childrenCount);
            falukantUser.setDataValue('unbaptisedChildrenCount', unbaptisedChildrenCount);
            falukantUser.setDataValue('unreadNotifications', unreadNotifications);
            bm('aggregate.done', { childrenCount, unbaptisedChildrenCount });
        } catch (e) {
            console.error('Error aggregating status info:', e);
            falukantUser.setDataValue('childrenCount', 0);
            falukantUser.setDataValue('unbaptisedChildrenCount', 0);
            falukantUser.setDataValue('unreadNotifications', 0);
        }

        return falukantUser;
    }

    async _maybeCreateDailyRandomEvent(falukantUser, user) {
        if (!falukantUser?.id) return null;

        // Already created today?
        const since = new Date();
        since.setHours(0, 0, 0, 0);
        const already = await Notification.count({
            where: {
                userId: falukantUser.id,
                createdAt: { [Op.gte]: since },
                [Op.or]: [
                    { tr: { [Op.like]: 'random_event.%' } },
                    { tr: { [Op.like]: '%\"tr\":\"random_event.%' } },
                ],
            },
        });
        if (already > 0) return null;

        // Choose an event (reduced frequency - not guaranteed every day)
        // Total weight: 50 (50% chance per day, or adjust as needed)
        const events = [
            { id: 'windfall', weight: 10 },
            { id: 'theft', weight: 8 },
            { id: 'character_illness', weight: 8 },
            { id: 'character_recovery', weight: 6 },
            { id: 'character_accident', weight: 4 },
            { id: 'regional_festival', weight: 4 },
            // Regionale Events sind sehr selten
            { id: 'regional_storm', weight: 1 },
            { id: 'regional_epidemic', weight: 1 },
            { id: 'earthquake', weight: 1 },
        ];
        const total = events.reduce((s, e) => s + e.weight, 0);
        // Reduzierte Wahrscheinlichkeit: Nur 30% Chance pro Tag, dass ein Event auftritt
        const eventChance = 0.3;
        if (Math.random() > eventChance) {
            return null; // Kein Event heute
        }
        
        let r = Math.random() * total;
        let chosen = events[0].id;
        for (const e of events) {
            r -= e.weight;
            if (r <= 0) { chosen = e.id; break; }
        }

        const payload = { tr: `random_event.${chosen}` };

        return await sequelize.transaction(async (t) => {
            // Reload current values inside tx
            const freshUser = await FalukantUser.findByPk(falukantUser.id, { transaction: t });
            const character = await FalukantCharacter.findOne({
                where: { userId: falukantUser.id },
                include: [
                    { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'], required: false },
                    { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'], required: false },
                ],
                transaction: t,
            });

            // Effects (keine harten Datenlöschungen)
            if (chosen === 'windfall') {
                const amount = Math.floor(Math.random() * 901) + 100; // 100..1000
                payload.amount = amount;
                await updateFalukantUserMoney(falukantUser.id, amount, 'random_event.windfall', falukantUser.id, t);
            } else if (chosen === 'theft') {
                const maxLoss = Math.max(0, Math.min(500, Math.floor(Number(freshUser?.money || 0))));
                const amount = maxLoss > 0 ? (Math.floor(Math.random() * maxLoss) + 1) : 0;
                payload.amount = amount;
                if (amount > 0) {
                    await updateFalukantUserMoney(falukantUser.id, -amount, 'random_event.theft', falukantUser.id, t);
                }
            } else if (chosen === 'character_illness' || chosen === 'character_recovery' || chosen === 'character_accident') {
                const name = [character?.definedFirstName?.name, character?.definedLastName?.name].filter(Boolean).join(' ').trim();
                payload.characterName = name || null;
                let delta = 0;
                if (chosen === 'character_illness') delta = -(Math.floor(Math.random() * 10) + 1); // -1..-10
                if (chosen === 'character_recovery') delta = (Math.floor(Math.random() * 11) + 5); // +5..+15
                if (chosen === 'character_accident') delta = -(Math.floor(Math.random() * 24) + 2); // -2..-25
                payload.healthChange = delta > 0 ? `+${delta}` : `${delta}`;
                if (character) {
                    const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta));
                    await character.update({ health: next }, { transaction: t });
                }
            } else if (chosen === 'regional_festival' || chosen === 'regional_storm' || chosen === 'regional_epidemic' || chosen === 'earthquake') {
                const regionId = character?.regionId || falukantUser?.mainBranchRegionId || null;
                if (regionId) {
                    const region = await RegionData.findByPk(regionId, { attributes: ['name'], transaction: t });
                    payload.regionName = region?.name || null;
                }
                
                // Regionale Events sollten nur einen moderaten Health-Verlust verursachen
                // NICHT alle Charaktere töten!
                if (chosen === 'regional_epidemic' && character) {
                    // Moderate Health-Reduktion: -3 bis -7 (reduziert)
                    const delta = -(Math.floor(Math.random() * 5) + 3); // -3..-7
                    payload.healthChange = `${delta}`;
                    const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta));
                    await character.update({ health: next }, { transaction: t });
                } else if (chosen === 'regional_storm' && character) {
                    // Sehr geringer Health-Verlust: -2 bis -5
                    const delta = -(Math.floor(Math.random() * 4) + 2); // -2..-5
                    payload.healthChange = `${delta}`;
                    const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta));
                    await character.update({ health: next }, { transaction: t });
                } else if (chosen === 'earthquake' && character) {
                    // Moderate Health-Reduktion: -5 bis -10 (reduziert)
                    const delta = -(Math.floor(Math.random() * 6) + 5); // -5..-10
                    payload.healthChange = `${delta}`;
                    const next = Math.min(100, Math.max(0, Number(character.health || 0) + delta));
                    await character.update({ health: next }, { transaction: t });
                }
            }

            // Store notification as JSON string so frontend can interpolate params
            await Notification.create(
                { userId: falukantUser.id, tr: JSON.stringify(payload), shown: false },
                { transaction: t }
            );

            // Make statusbar update (unread count, etc.)
            try { notifyUser(user.hashedId, 'falukantUpdateStatus', {}); } catch (_) {}
            return payload;
        });
    }

    async getBranches(hashedUserId) {
        const u = await getFalukantUserOrFail(hashedUserId);
        const bs = await Branch.findAll({
            where: { falukantUserId: u.id },
            include: [
                { model: BranchType, as: 'branchType', attributes: ['labelTr'] },
                { 
                    model: RegionData, 
                    as: 'region', 
                    attributes: ['id', 'name'],
                    include: [
                        {
                            model: Weather,
                            as: 'weather',
                            attributes: ['regionId', 'weatherTypeId'],
                            include: [
                                { model: WeatherType, as: 'weatherType', attributes: ['tr'] }
                            ],
                            required: false
                        }
                    ]
                }
            ],
            attributes: ['id', 'regionId'],
            order: [['branchTypeId', 'ASC']]
        });
        
        // Lade Wetter explizit für alle Regionen, um sicherzustellen, dass es korrekt geladen wird
        const regionIds = [...new Set(bs.map(b => b.regionId))];
        const weathers = await Weather.findAll({
            where: { regionId: { [Op.in]: regionIds } },
            include: [
                { model: WeatherType, as: 'weatherType', attributes: ['tr'] }
            ]
        });
        const weatherMap = new Map(weathers.map(w => [w.regionId, w.weatherType?.tr || null]));
        
        return bs.map(b => {
            const branchJson = b.toJSON();
            // Verwende das explizit geladene Wetter, falls vorhanden, sonst das aus der Include-Beziehung
            const weather = weatherMap.get(b.regionId) || branchJson.region?.weather?.weatherType?.tr || null;
            return { 
                ...branchJson, 
                isMainBranch: u.mainBranchRegionId === b.regionId,
                weather: weather
            };
        });
    }

    async createBranch(hashedUserId, cityId, branchTypeId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const branchType = await BranchType.findByPk(branchTypeId);
        if (!branchType) {
            throw new Error(`Unknown branchTypeId ${branchTypeId}`);
        }
        const existingCount = await Branch.count({
            where: { falukantUserId: user.id }
        });
        const exponentBase = Math.max(existingCount, 1);
        const rawCost = branchType.baseCost * Math.pow(exponentBase, 1.2);
        const cost = Math.round(rawCost * 100) / 100;
        if (user.money < cost) {
            throw new PreconditionError('insufficientFunds');
        }
        await updateFalukantUserMoney(
            user.id,
            -cost,
            'create_branch'
        );
        const branch = await Branch.create({
            branchTypeId,
            regionId: cityId,
            falukantUserId: user.id
        });
        return branch;
    }

    async getBranchTypes(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const branchTypes = await BranchType.findAll();
        return branchTypes;

    }

    async getBranch(hashedUserId, branchId) {
        const u = await getFalukantUserOrFail(hashedUserId);
        const br = await Branch.findOne({
            where: { id: branchId, falukantUserId: u.id },
            include: [
                { model: BranchType, as: 'branchType', attributes: ['labelTr'] },
                { model: RegionData, as: 'region', attributes: ['name'] },
                {
                    model: Production,
                    as: 'productions',
                    attributes: ['quantity', 'startTimestamp'],
                    include: [{ model: ProductType, as: 'productType', attributes: ['id', 'category', 'labelTr', 'sellCost', 'productionTime'] }]
                }
            ],
            attributes: ['id', 'regionId']
        });
        if (!br) throw new Error('Branch not found');
        return br;
    }

    async upgradeBranch(hashedUserId, branchId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const branch = await Branch.findOne({
            where: { id: branchId, falukantUserId: user.id },
            include: [{ model: BranchType, as: 'branchType', attributes: ['id', 'labelTr'] }],
        });
        if (!branch) {
            throw new Error('Branch not found');
        }

        const currentLabel = branch.branchType?.labelTr;

        let targetLabel = null;
        if (currentLabel === 'production') {
            targetLabel = 'fullstack';
        } else if (currentLabel === 'store') {
            targetLabel = 'fullstack';
        } else {
            // already fullstack or unknown type
            throw new PreconditionError('noUpgradeAvailable');
        }

        const targetType = await BranchType.findOne({ where: { labelTr: targetLabel } });
        if (!targetType) {
            throw new Error(`Target branch type '${targetLabel}' not found`);
        }

        // Für den Moment ohne zusätzliche Kosten – kann später erweitert werden
        branch.branchTypeId = targetType.id;
        await branch.save();

        const updated = await this.getBranch(hashedUserId, branch.id);
        return updated;
    }

    async getStock(hashedUserId, branchId) {
        const u = await getFalukantUserOrFail(hashedUserId);
        const b = await getBranchOrFail(u.id, branchId);
        return FalukantStock.findAll({ where: { regionId: b.regionId, userId: u.id } });
    }

    async getVehicleTypes(hashedUserId) {
        // Validate user existence, but we don't filter by user here
        await getFalukantUserOrFail(hashedUserId);
        return VehicleType.findAll();
    }

    async getVehicles(hashedUserId, regionId) {
        const user = await getFalukantUserOrFail(hashedUserId);

        const where = { falukantUserId: user.id };
        if (regionId) {
            where.regionId = regionId;
        }

        const vehicles = await Vehicle.findAll({
            where,
            include: [
                {
                    model: VehicleType,
                    as: 'type',
                    attributes: ['id', 'tr', 'capacity', 'transportMode', 'speed', 'buildTimeMinutes', 'cost'],
                },
                {
                    model: Transport,
                    as: 'transports',
                    attributes: ['id', 'sourceRegionId', 'targetRegionId'],
                    required: false,
                },
            ],
            order: [['availableFrom', 'ASC'], ['id', 'ASC']],
        });

        const now = new Date();
        const branchRegionId = regionId ? parseInt(regionId, 10) : undefined;

        return vehicles.map((v) => {
            const plain = v.get({ plain: true });

            const effectiveRegionId = branchRegionId ?? plain.regionId;

            const hasTransportHere = Array.isArray(plain.transports) && plain.transports.some(
                (t) =>
                    t.sourceRegionId === effectiveRegionId ||
                    t.targetRegionId === effectiveRegionId
            );

            let status;
            if (hasTransportHere) {
                // verknüpft mit Transport in dieser Region = unterwegs
                status = 'travelling';
            } else if (plain.availableFrom) {
                const availableFromTime = new Date(plain.availableFrom).getTime();
                const nowTime = now.getTime();
                // Verfügbarkeit liegt in der Zukunft = im Bau oder in Reparatur
                if (availableFromTime > nowTime) {
                    status = 'building';
                } else {
                    // Verfügbarkeit erreicht = verfügbar
                    status = 'available';
                }
            } else {
                // Kein availableFrom gesetzt = verfügbar (Fallback)
                status = 'available';
            }

            return {
                id: plain.id,
                // Defensive: legacy DB rows can have NULL -> UI would display "Unbekannt"
                condition: Math.max(0, Math.min(100, Number.isFinite(Number(plain.condition)) ? Number(plain.condition) : 100)),
                availableFrom: plain.availableFrom,
                status,
                type: {
                    id: plain.type?.id,
                    tr: plain.type?.tr,
                    capacity: plain.type?.capacity,
                    transportMode: plain.type?.transportMode,
                    speed: plain.type?.speed,
                    buildTimeMinutes: plain.type?.buildTimeMinutes,
                    cost: plain.type?.cost,
                },
            };
        });
    }

    async getTransportRoute(hashedUserId, { sourceRegionId, targetRegionId, vehicleTypeId }) {
        const user = await getFalukantUserOrFail(hashedUserId);
        if (!user) {
            throw new Error('User not found');
        }

        const type = await VehicleType.findByPk(vehicleTypeId);
        if (!type) {
            throw new Error('Vehicle type not found');
        }

        const route = await computeShortestRoute(
            type.transportMode,
            sourceRegionId,
            targetRegionId
        );

        if (!route) {
            return {
                mode: type.transportMode,
                totalDistance: null,
                regions: [],
            };
        }

        const regions = await RegionData.findAll({
            where: { id: route.path },
            attributes: ['id', 'name'],
        });
        const regionMap = new Map(regions.map((r) => [r.id, r.name]));
        const ordered = route.path.map((id) => ({
            id,
            name: regionMap.get(id) || String(id),
        }));

        return {
            mode: type.transportMode,
            totalDistance: route.distance,
            regions: ordered,
        };
    }

    async createTransport(hashedUserId, { branchId, vehicleTypeId, vehicleIds, productId, quantity, targetBranchId }) {
        const user = await getFalukantUserOrFail(hashedUserId);

        const sourceBranch = await Branch.findOne({
            where: { id: branchId, falukantUserId: user.id },
            include: [{ model: RegionData, as: 'region' }],
        });
        if (!sourceBranch) {
            throw new Error('Branch not found');
        }

        const targetBranch = await Branch.findOne({
            where: { id: targetBranchId, falukantUserId: user.id },
            include: [{ model: BranchType, as: 'branchType', attributes: ['labelTr'] }, { model: RegionData, as: 'region' }],
        });
        if (!targetBranch) {
            throw new Error('Target branch not found');
        }
        if (!['store', 'fullstack'].includes(targetBranch.branchType.labelTr)) {
            throw new PreconditionError('invalidTargetBranch');
        }

        const sourceRegionId = sourceBranch.regionId;
        const targetRegionId = targetBranch.regionId;
        const now = new Date();

        let type;
        let freeVehicles = [];

        // Wenn spezifische vehicleIds übergeben wurden, diese verwenden
        if (vehicleIds && Array.isArray(vehicleIds) && vehicleIds.length > 0) {
            const vehicles = await Vehicle.findAll({
                where: {
                    id: { [Op.in]: vehicleIds },
                    falukantUserId: user.id,
                    regionId: sourceRegionId,
                    availableFrom: { [Op.lte]: now },
                },
                include: [
                    {
                        model: Transport,
                        as: 'transports',
                        required: false,
                        attributes: ['id'],
                    },
                    {
                        model: VehicleType,
                        as: 'type',
                        required: true,
                    },
                ],
            });

            freeVehicles = vehicles.filter((v) => {
                const t = v.transports || [];
                return t.length === 0;
            });

            if (freeVehicles.length === 0) {
                throw new PreconditionError('noVehiclesAvailable');
            }

            // Alle Fahrzeuge müssen denselben Typ haben
            const vehicleTypeIds = [...new Set(freeVehicles.map(v => v.vehicleTypeId))];
            if (vehicleTypeIds.length !== 1) {
                throw new Error('All vehicles must be of the same type');
            }

            type = await VehicleType.findByPk(vehicleTypeIds[0]);
            if (!type) {
                throw new Error('Vehicle type not found');
            }
        } else {
            // Standard-Verhalten: Alle freien Fahrzeuge dieses Typs verwenden
            if (!vehicleTypeId) {
                throw new Error('Either vehicleTypeId or vehicleIds must be provided');
            }

            type = await VehicleType.findByPk(vehicleTypeId);
            if (!type) {
                throw new Error('Vehicle type not found');
            }

            // Freie Fahrzeuge dieses Typs in der Quell-Region
            const vehicles = await Vehicle.findAll({
                where: {
                    falukantUserId: user.id,
                    regionId: sourceRegionId,
                    vehicleTypeId,
                    availableFrom: { [Op.lte]: now },
                },
                include: [
                    {
                        model: Transport,
                        as: 'transports',
                        required: false,
                        attributes: ['id'],
                    },
                ],
            });

            freeVehicles = vehicles.filter((v) => {
                const t = v.transports || [];
                return t.length === 0;
            });
        }

        if (!freeVehicles.length) {
            throw new PreconditionError('noVehiclesAvailable');
        }

        const route = await computeShortestRoute(type.transportMode, sourceRegionId, targetRegionId);
        if (!route) {
            throw new PreconditionError('noRoute');
        }

        if (!freeVehicles.length) {
            throw new PreconditionError('noVehiclesAvailable');
        }

        const capacityPerVehicle = type.capacity || 0;
        if (capacityPerVehicle <= 0) {
            throw new Error('Invalid vehicle capacity');
        }
        const maxByVehicles = capacityPerVehicle * freeVehicles.length;

        // Produkt-Transport oder leerer Transport (nur Fahrzeuge bewegen)?
        const isEmptyTransport = !productId || !quantity || quantity <= 0;
        
        let inventory = [];
        let available = 0;
        let maxByInventory = 0;
        let hardMax = 0;
        let requested = 0;
        let transportCost = 0.1; // Minimale Kosten für leeren Transport

        if (!isEmptyTransport) {
            // Produkt-Transport: Inventar prüfen
            const stock = await FalukantStock.findOne({ where: { branchId: sourceBranch.id } });
            if (!stock) {
                throw new Error('Stock not found');
            }

            inventory = await Inventory.findAll({
                where: { stockId: stock.id },
                include: [
                    {
                        model: ProductType,
                        as: 'productType',
                        required: true,
                        where: { id: productId },
                    },
                ],
            });

            available = inventory.reduce((sum, i) => sum + i.quantity, 0);
            if (available <= 0) {
                throw new PreconditionError('noInventory');
            }

            maxByInventory = available;
            hardMax = Math.min(maxByVehicles, maxByInventory);

            requested = Math.max(1, parseInt(quantity, 10) || 0);
            if (requested > hardMax) {
                throw new PreconditionError('quantityTooHigh');
            }

            // Transportkosten: 1 % des Warenwerts, mindestens 0,1
            const productType = inventory[0]?.productType;
            const unitValue = productType?.sellCost || 0;
            const totalValue = unitValue * requested;
            transportCost = Math.max(0.1, totalValue * 0.01);
        } else {
            // Leerer Transport: Ein Fahrzeug wird bewegt
            requested = 1;
            hardMax = 1;
        }

        if (user.money < transportCost) {
            throw new PreconditionError('insufficientFunds');
        }

        const result = await sequelize.transaction(async (tx) => {
            // Geld für den Transport abziehen
            const moneyResult = await updateFalukantUserMoney(
                user.id,
                -transportCost,
                'transport',
                user.id
            );
            if (!moneyResult.success) {
                throw new Error('Failed to update money');
            }

            let remaining = requested;
            const transportsCreated = [];

            for (const v of freeVehicles) {
                if (remaining <= 0) break;
                const size = isEmptyTransport ? 0 : Math.min(remaining, capacityPerVehicle);
                const t = await Transport.create(
                    {
                        sourceRegionId,
                        targetRegionId,
                        productId: isEmptyTransport ? null : productId,
                        size: isEmptyTransport ? 0 : size,
                        vehicleId: v.id,
                    },
                    { transaction: tx }
                );
                transportsCreated.push(t);
                if (!isEmptyTransport) {
                    remaining -= size;
                } else {
                    // Bei leerem Transport nur ein Fahrzeug bewegen
                    remaining = 0;
                }
            }

            if (remaining > 0 && !isEmptyTransport) {
                throw new Error('Not enough vehicle capacity for requested quantity');
            }

            // Inventar in der Quell-Niederlassung reduzieren (nur bei Produkt-Transport)
            if (!isEmptyTransport && inventory.length > 0) {
                let left = requested;
                for (const inv of inventory) {
                    if (left <= 0) break;
                    if (inv.quantity <= left) {
                        left -= inv.quantity;
                        await inv.destroy({ transaction: tx });
                    } else {
                        await inv.update({ quantity: inv.quantity - left }, { transaction: tx });
                        left = 0;
                        break;
                    }
                }

                if (left > 0) {
                    throw new Error('Inventory changed during transport creation');
                }

                notifyUser(user.user.hashedId, 'stock_change', { branchId: sourceBranch.id });
            }

            notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: sourceBranch.id });

            return {
                success: true,
                maxQuantity: hardMax,
                requested,
                totalDistance: route.distance,
                totalCost: transportCost,
                transports: transportsCreated.map((t) => ({
                    id: t.id,
                    size: t.size,
                    vehicleId: t.vehicleId,
                })),
            };
        });

        return result;
    }

    async getBranchTransports(hashedUserId, branchId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const branch = await Branch.findOne({
            where: { id: branchId, falukantUserId: user.id },
            include: [{ model: RegionData, as: 'region', attributes: ['id', 'name'] }],
        });
        if (!branch) {
            throw new Error('Branch not found');
        }
        const regionId = branch.regionId;

        const transports = await Transport.findAll({
            where: {
                [Op.or]: [
                    { sourceRegionId: regionId },
                    { targetRegionId: regionId },
                ],
            },
            include: [
                { model: RegionData, as: 'sourceRegion', attributes: ['id', 'name'] },
                { model: RegionData, as: 'targetRegion', attributes: ['id', 'name'] },
                { model: ProductType, as: 'productType', required: false, attributes: ['id', 'labelTr'] },
                {
                    model: Vehicle,
                    as: 'vehicle',
                    include: [
                        {
                            model: VehicleType,
                            as: 'type',
                            attributes: ['id', 'tr', 'speed'],
                        },
                    ],
                },
            ],
            order: [['createdAt', 'DESC']],
        });

        const now = Date.now();

        return transports.map((t) => {
            const direction = t.sourceRegionId === regionId ? 'outgoing' : 'incoming';

            let eta = null;
            let durationHours = null;
            if (t.vehicle && t.vehicle.type && t.vehicle.type.speed && t.sourceRegionId && t.targetRegionId) {
                // Näherungsweise Dauer: wir haben die exakte Distanz hier nicht, deshalb nur anhand der
                // bei Erstellung verwendeten Route in der UI berechnet – für die Liste reicht createdAt + 1h.
                // TODO: Optional: Distanz persistent speichern.
                durationHours = 1;
                const createdAt = t.createdAt ? new Date(t.createdAt).getTime() : now;
                const etaMs = createdAt + durationHours * 60 * 60 * 1000;
                eta = new Date(etaMs);
            }

            return {
                id: t.id,
                direction,
                sourceRegion: t.sourceRegion,
                targetRegion: t.targetRegion,
                product: t.productType,
                size: t.size,
                vehicleId: t.vehicleId,
                vehicleType: t.vehicle?.type || null,
                createdAt: t.createdAt,
                eta,
                durationHours,
            };
        });
    }

    async buyVehicles(hashedUserId, { vehicleTypeId, quantity, regionId, mode = 'buy' }) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const qty = Math.max(1, parseInt(quantity, 10) || 0);

        const type = await VehicleType.findByPk(vehicleTypeId);
        if (!type) {
            throw new Error('Vehicle type not found');
        }

        const baseCost = type.cost;
        const unitCost = mode === 'build'
            ? Math.round(baseCost * 0.75)
            : baseCost;
        const totalCost = unitCost * qty;
        if (user.money < totalCost) {
            throw new PreconditionError('insufficientFunds');
        }

        // Ensure the region exists (and is part of Falukant map)
        const region = await RegionData.findByPk(regionId);
        if (!region) {
            throw new Error('Region not found');
        }

        // Update money and create vehicles in a transaction
        await sequelize.transaction(async (tx) => {
            await updateFalukantUserMoney(
                user.id,
                -totalCost,
                mode === 'build' ? 'build_vehicles' : 'buy_vehicles',
                user.id
            );

            const records = [];
            const now = new Date();
            const baseTime = now.getTime();
            const buildMs = mode === 'build'
                ? (type.buildTimeMinutes || 0) * 60 * 1000
                : 0;

            for (let i = 0; i < qty; i++) {
                records.push({
                    vehicleTypeId: type.id,
                    falukantUserId: user.id,
                    regionId: region.id,
                    availableFrom: new Date(baseTime + buildMs),
                    condition: 100,
                });
            }
            await Vehicle.bulkCreate(records, { transaction: tx });
        });

        return { success: true };
    }

    async repairVehicle(hashedUserId, vehicleId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const now = new Date();

        // Fahrzeug laden mit Typ-Informationen
        const vehicle = await Vehicle.findOne({
            where: {
                id: vehicleId,
                falukantUserId: user.id,
            },
            include: [
                {
                    model: VehicleType,
                    as: 'type',
                    required: true,
                },
                {
                    model: Transport,
                    as: 'transports',
                    required: false,
                    attributes: ['id'],
                },
            ],
        });

        if (!vehicle) {
            throw new Error('Vehicle not found or does not belong to user');
        }

        // Prüfen, ob Fahrzeug in Benutzung ist
        const hasActiveTransport = Array.isArray(vehicle.transports) && vehicle.transports.length > 0;
        const isBuilding = vehicle.availableFrom && new Date(vehicle.availableFrom).getTime() > now.getTime();

        if (hasActiveTransport) {
            throw new PreconditionError('vehicleInUse');
        }

        if (isBuilding) {
            throw new PreconditionError('vehicleAlreadyBuilding');
        }

        // Prüfen, ob Reparatur nötig ist (Zustand < 100)
        if (vehicle.condition >= 100) {
            throw new PreconditionError('vehicleAlreadyPerfect');
        }

        // Kosten berechnen: Baupreis * 0.8 * (100 - zustand) / 100
        const baseCost = vehicle.type.cost;
        const repairCost = Math.round(baseCost * 0.8 * (100 - vehicle.condition) / 100);

        if (user.money < repairCost) {
            throw new PreconditionError('insufficientFunds');
        }

        // Bauzeit verwenden (buildTimeMinutes)
        const buildTimeMinutes = vehicle.type.buildTimeMinutes || 0;
        const buildMs = buildTimeMinutes * 60 * 1000;
        const availableFrom = new Date(now.getTime() + buildMs);

        // Reparatur durchführen
        await sequelize.transaction(async (tx) => {
            // Geld abziehen
            const moneyResult = await updateFalukantUserMoney(
                user.id,
                -repairCost,
                'repair_vehicle',
                user.id
            );
            if (!moneyResult.success) {
                throw new Error('Failed to update money');
            }

            // Fahrzeug reparieren: Zustand auf 100, availableFrom auf jetzt + Bauzeit
            await vehicle.update({
                condition: 100,
                availableFrom: availableFrom,
            }, { transaction: tx });
        });

        return {
            success: true,
            repairCost,
            buildTimeMinutes,
            availableFrom,
        };
    }

    async repairAllVehicles(hashedUserId, vehicleIds) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const now = new Date();

        if (!vehicleIds || !Array.isArray(vehicleIds) || vehicleIds.length === 0) {
            throw new Error('Keine Fahrzeuge zum Reparieren angegeben');
        }

        // Alle Fahrzeuge laden
        const vehicles = await Vehicle.findAll({
            where: {
                id: { [Op.in]: vehicleIds },
                falukantUserId: user.id,
            },
            include: [
                {
                    model: VehicleType,
                    as: 'type',
                    required: true,
                },
                {
                    model: Transport,
                    as: 'transports',
                    required: false,
                    attributes: ['id'],
                },
            ],
        });

        if (vehicles.length !== vehicleIds.length) {
            throw new Error('Nicht alle angegebenen Fahrzeuge gefunden oder gehören nicht dem Benutzer');
        }

        // Prüfe alle Fahrzeuge
        const repairableVehicles = [];
        let totalCost = 0;

        for (const vehicle of vehicles) {
            // Prüfen, ob Fahrzeug in Benutzung ist
            const hasActiveTransport = Array.isArray(vehicle.transports) && vehicle.transports.length > 0;
            const isBuilding = vehicle.availableFrom && new Date(vehicle.availableFrom).getTime() > now.getTime();

            if (hasActiveTransport || isBuilding) {
                continue; // Überspringe Fahrzeuge in Benutzung
            }

            // Prüfen, ob Reparatur nötig ist (Zustand < 100)
            if (vehicle.condition >= 100) {
                continue; // Überspringe bereits perfekte Fahrzeuge
            }

            // Kosten berechnen: Baupreis * 0.8 * (100 - zustand) / 100
            const baseCost = vehicle.type.cost;
            const repairCost = Math.round(baseCost * 0.8 * (100 - vehicle.condition) / 100);
            totalCost += repairCost;
            repairableVehicles.push({ vehicle, repairCost });
        }

        if (repairableVehicles.length === 0) {
            throw new PreconditionError('noVehiclesToRepair');
        }

        // 10% Rabatt für Reparatur aller Fahrzeuge
        const discountedCost = Math.round(totalCost * 0.9);

        if (user.money < discountedCost) {
            throw new PreconditionError('insufficientFunds');
        }

        // Alle Reparaturen in einer Transaktion durchführen
        await sequelize.transaction(async (tx) => {
            // Geld abziehen
            const moneyResult = await updateFalukantUserMoney(
                user.id,
                -discountedCost,
                'repair_all_vehicles',
                user.id
            );
            if (!moneyResult.success) {
                throw new Error('Failed to update money');
            }

            // Alle Fahrzeuge reparieren
            for (const { vehicle, repairCost } of repairableVehicles) {
                const buildTimeMinutes = vehicle.type.buildTimeMinutes || 0;
                const buildMs = buildTimeMinutes * 60 * 1000;
                const availableFrom = new Date(now.getTime() + buildMs);

                await vehicle.update({
                    condition: 100,
                    availableFrom: availableFrom,
                }, { transaction: tx });
            }
        });

        return {
            success: true,
            repairedCount: repairableVehicles.length,
            totalCost: discountedCost,
        };
    }

    async createStock(hashedUserId, branchId, stockData) {
        const u = await getFalukantUserOrFail(hashedUserId);
        const b = await getBranchOrFail(u.id, branchId);
        return FalukantStock.create({
            userId: u.id, regionId: b.regionId, stockTypeId: stockData.stockTypeId, quantity: stockData.quantity
        });
    }

    async createProduction(hashedUserId, branchId, productId, quantity) {
        const u = await getFalukantUserOrFail(hashedUserId);
        const b = await getBranchOrFail(u.id, branchId);
        const p = await ProductType.findOne({ where: { id: productId } });
        const runningProductions = await Production.findAll({ where: { branchId: b.id } });
        if (runningProductions.length >= 2) {
            throw new Error('Too many productions');
            return; // wird später implementiert, wenn familie implementiert ist.

        }
        if (!p) throw new Error('Product not found');
        quantity = Math.min(100, quantity);
        const cost = quantity * p.category * 6;
        if (u.money < cost) throw new Error('notenoughmoney');
        const r = await updateFalukantUserMoney(u.id, -cost, 'Production cost', u.id);
        if (!r.success) throw new Error('Failed to update money');
        
        // Hole aktuelles Wetter der Region
        const currentWeather = await Weather.findOne({
            where: { regionId: b.regionId }
        });
        const weatherTypeId = currentWeather?.weatherTypeId || null;
        
        const d = await Production.create({ 
            branchId: b.id, 
            productId, 
            quantity,
            weatherTypeId
        });
        notifyUser(u.user.hashedId, 'falukantUpdateStatus', {});
        notifyUser(u.user.hashedId, 'falukantBranchUpdate', { branchId: b.id });
        return d;
    }

    async getProduction(hashedUserId, branchId) {
        const u = await getFalukantUserOrFail(hashedUserId);
        const b = await getBranchOrFail(u.id, branchId);
        const production = await Production.findOne({ 
            where: { branchId: b.id },
            include: [
                { model: ProductType, as: 'productType' },
                { model: WeatherType, as: 'weatherType' }
            ]
        });
        
        if (!production) {
            return null;
        }
        
        // Berechne Qualität basierend auf Wettereffekt
        let quality = 50; // Basisqualität (50%)
        
        if (production.weatherTypeId) {
            const weatherEffect = await ProductWeatherEffect.findOne({
                where: {
                    productId: production.productId,
                    weatherTypeId: production.weatherTypeId
                }
            });
            
            if (weatherEffect) {
                // Wettereffekt: -2 bis +2, wird auf Qualität angewendet
                // Basisqualität 50%, Effekt wird als Prozentpunkte addiert
                quality = Math.max(0, Math.min(100, 50 + (weatherEffect.qualityEffect * 10)));
            }
        }
        
        // Konvertiere zu JSON und füge berechnete Qualität hinzu
        const result = production.toJSON();
        result.quality = Math.round(quality);
        
        return result;
    }

    async getProducts(hashedUserId) {
        const u = await getFalukantUserOrFail(hashedUserId);
        const c = await FalukantCharacter.findOne({ where: { userId: u.id } });
        if (!c) {
            throw new Error(`No FalukantCharacter found for user with id ${u.id}`);
        }
        const ps = await ProductType.findAll({
            where: { category: { [Op.lte]: u.certificate } },
            include: [{ model: Knowledge, as: 'knowledges', attributes: ['knowledge'], where: { characterId: c.id } }],
            attributes: ['labelTr', 'id', 'sellCost', 'productionTime', 'category']
        });
        return ps;
    }

    async getInventory(hashedUserId, branchId) {
        // PERFORMANCE: Diese Route war langsam wegen doppelter/verschachtelter Includes (Branch->Stocks->Region + Inventory->Stock->Branch->Region).
        // Wir holen stattdessen genau die benötigten Felder in EINER aggregierenden SQL-Query.
        const u = await getFalukantUserOrFail(hashedUserId);
        const branchIdInt = branchId == null ? null : parseInt(branchId, 10);
        if (branchId != null && Number.isNaN(branchIdInt)) {
            throw new Error('Invalid branchId');
        }

        const rows = await sequelize.query(
            `
            SELECT
              r.id        AS region_id,
              r.name      AS region_name,
              rt.id       AS region_type_id,
              rt.label_tr AS region_type_label_tr,
              p.id        AS product_id,
              p.label_tr  AS product_label_tr,
              p.sell_cost AS product_sell_cost,
              i.quality   AS quality,
              SUM(i.quantity)::int AS total_quantity
            FROM falukant_data.inventory i
            JOIN falukant_data.stock s ON s.id = i.stock_id
            JOIN falukant_data.branch b ON b.id = s.branch_id
            JOIN falukant_data.region r ON r.id = b.region_id
            LEFT JOIN falukant_type.region rt ON rt.id = r.region_type_id
            JOIN falukant_type.product p ON p.id = i.product_id
            WHERE b.falukant_user_id = :falukantUserId
              AND (:branchId::int IS NULL OR b.id = :branchId::int)
            GROUP BY
              r.id, r.name, rt.id, rt.label_tr,
              p.id, p.label_tr, p.sell_cost,
              i.quality
            ORDER BY r.id, p.id, i.quality
            `,
            {
                replacements: { falukantUserId: u.id, branchId: branchIdInt },
                type: sequelize.QueryTypes.SELECT
            }
        );

        return (rows || []).map(r => ({
            region: {
                id: r.region_id,
                name: r.region_name,
                regionType: r.region_type_id
                    ? { id: r.region_type_id, labelTr: r.region_type_label_tr }
                    : null
            },
            product: {
                id: r.product_id,
                labelTr: r.product_label_tr,
                sellCost: r.product_sell_cost
            },
            quality: r.quality,
            totalQuantity: r.total_quantity
        }));
    }

    async sellProduct(hashedUserId, branchId, productId, quality, quantity) {
        // Konsistenz wie sellAll: nur aus Stocks dieses Branches verkaufen und alles atomar ausführen
        return await sequelize.transaction(async (t) => {
            const user = await getFalukantUserOrFail(hashedUserId, { transaction: t });
            const branch = await getBranchOrFail(user.id, branchId);

            const character = await FalukantCharacter.findOne({ where: { userId: user.id }, transaction: t });
            if (!character) throw new Error('No character found for user');

            const stocks = await FalukantStock.findAll({
                where: { branchId: branch.id },
                attributes: ['id'],
                transaction: t
            });
            const stockIds = stocks.map(s => s.id);
            if (!stockIds.length) throw new Error('Stock not found');

            const inventory = await Inventory.findAll({
                where: {
                    stockId: { [Op.in]: stockIds },
                    productId,
                    quality
                },
                include: [
                    {
                        model: ProductType,
                        as: 'productType',
                        required: true,
                        where: { id: productId },
                        include: [
                            {
                                model: Knowledge,
                                as: 'knowledges',
                                required: false,
                                where: { characterId: character.id }
                            }
                        ]
                    }
                ],
                order: [['producedAt', 'ASC'], ['id', 'ASC']],
                transaction: t
            });

            if (!inventory.length) {
                throw new Error('No inventory found');
            }

            const available = inventory.reduce((sum, i) => sum + i.quantity, 0);
            if (available < quantity) throw new Error('Not enough inventory available');

            const item = inventory[0].productType;
            const knowledgeVal = item.knowledges?.[0]?.knowledge || 0;
            const pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);

            // compute cumulative tax (region + ancestors) with political exemptions and inflate price so seller net is unchanged
            const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, branch.regionId);
            const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
            const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
            const revenue = quantity * adjustedPricePerUnit;

            // compute tax and net
            const taxValue = Math.round((revenue * cumulativeTax / 100) * 100) / 100;
            const net = Math.round((revenue - taxValue) * 100) / 100;

            // Book net to seller (in tx)
            const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id, t);
            if (!moneyResult.success) throw new Error('Failed to update money for seller');

            // Book tax to treasury (if configured)
            const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
            if (treasuryId && taxValue > 0) {
                const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id, t);
                if (!taxResult.success) throw new Error('Failed to update money for treasury');
            }

            let remaining = quantity;
            for (const inv of inventory) {
                if (remaining <= 0) break;
                if (inv.quantity <= remaining) {
                    remaining -= inv.quantity;
                    await inv.destroy({ transaction: t });
                } else {
                    await inv.update({ quantity: inv.quantity - remaining }, { transaction: t });
                    remaining = 0;
                    break;
                }
            }
            if (remaining !== 0) {
                throw new Error(`Inventory deduction mismatch (remaining=${remaining})`);
            }

            await this.addSellItem(branchId, user.id, productId, quantity, t);

            // notify after successful commit (we can still emit here; worst-case it's slightly early)
            notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
            notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
            return { success: true };
        });
    }

    async sellAllProducts(hashedUserId, branchId) {
        // Konsistenz-Garantie: Verkauf, DaySell-Log, Geldbuchung und Inventory-Löschung müssen atomar sein.
        // Sonst kann es (wie beobachtet) zu "teilweise verkauft/gelöscht" kommen.
        return await sequelize.transaction(async (t) => {
            const falukantUser = await getFalukantUserOrFail(hashedUserId, { transaction: t });
            const branch = await Branch.findOne({
                where: { id: branchId, falukantUserId: falukantUser.id },
                include: [{ model: FalukantStock, as: 'stocks' }],
                transaction: t
            });
            if (!branch) throw new Error('Branch not found');
            const stockIds = branch.stocks.map(s => s.id);
            const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, transaction: t });
            if (!character) throw new Error('No character for user');
            const inventory = await Inventory.findAll({
                where: { stockId: stockIds },
                include: [
                    {
                        model: ProductType,
                        as: 'productType',
                        include: [
                            {
                                model: Knowledge,
                                as: 'knowledges',
                                required: false,
                                where: {
                                    characterId: character.id
                                }
                            }
                        ]
                    },
                    {
                        model: FalukantStock,
                        as: 'stock',
                        include: [
                            {
                                model: Branch,
                                as: 'branch'
                            },
                            {
                                model: FalukantStockType,
                                as: 'stockType'
                            }
                        ]
                    }
                ],
                transaction: t
            });
            if (!inventory.length) return { success: true, revenue: 0 };

            // PERFORMANCE OPTIMIZATION: Batch-Load alle benötigten Daten
            const regionIds = [...new Set(inventory.map(item => item.stock.branch.regionId))];
            const productIds = [...new Set(inventory.map(item => item.productType.id))];

            // 1. Batch-Load alle TownProductWorth Einträge
            const townWorths = await TownProductWorth.findAll({
                where: {
                    productId: { [Op.in]: productIds },
                    regionId: { [Op.in]: regionIds }
                },
                transaction: t
            });
            const worthMap = new Map();
            townWorths.forEach(tw => {
                worthMap.set(`${tw.productId}-${tw.regionId}`, tw.worthPercent);
            });

            // 2/3. Batch-Berechne Steuern für alle Regionen
            const taxMap = new Map();
            const uniqueRegionIds = [...new Set(regionIds)];
            for (const regionId of uniqueRegionIds) {
                const tax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId);
                taxMap.set(regionId, tax);
            }

            // 4. Berechne Preise, Steuern und Einnahmen in EINER Schleife
            let total = 0;
            let totalTax = 0;
            const sellItems = [];

            for (const item of inventory) {
                const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
                const regionId = item.stock.branch.regionId;
                const worthPercent = worthMap.get(`${item.productType.id}-${regionId}`) || 50;

                const pricePerUnit = calcRegionalSellPriceSync(item.productType, knowledgeVal, worthPercent);

                const cumulativeTax = taxMap.get(regionId);
                const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
                const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
                const itemRevenue = item.quantity * adjustedPricePerUnit;
                const itemTax = Math.round((itemRevenue * cumulativeTax / 100) * 100) / 100;

                total += itemRevenue;
                totalTax += itemTax;

                sellItems.push({
                    branchId: item.stock.branch.id,
                    productId: item.productType.id,
                    quantity: item.quantity
                });
            }

            const totalNet = Math.round((total - totalTax) * 100) / 100;

            // 5. Batch-Update DaySell Einträge (innerhalb Transaktion)
            await this.addSellItemsBatch(sellItems, falukantUser.id, t);

            // 6. Inventory löschen (innerhalb Transaktion) und sicherstellen, dass alles weg ist
            const inventoryIds = inventory.map(item => item.id).filter(Boolean);
            const expected = inventoryIds.length;
            const deleted = await Inventory.destroy({ where: { id: { [Op.in]: inventoryIds } }, transaction: t });
            if (deleted !== expected) {
                throw new Error(`Inventory delete mismatch: expected ${expected}, deleted ${deleted}`);
            }

            // 7. Geld buchen (innerhalb Transaktion)
            const moneyResult = await updateFalukantUserMoney(
                falukantUser.id,
                totalNet,
                'Sell all products (net)',
                falukantUser.id,
                t
            );
            if (!moneyResult.success) throw new Error('Failed to update money for seller');

            const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
            if (treasuryId && totalTax > 0) {
                const taxResult = await updateFalukantUserMoney(
                    parseInt(treasuryId, 10),
                    Math.round(totalTax * 100) / 100,
                    `Sales tax (aggregate)`,
                    falukantUser.id,
                    t
                );
                if (!taxResult.success) throw new Error('Failed to update money for treasury');
            }

            console.log('[FalukantService.sellAllProducts] sold items', expected, 'deleted', deleted, 'revenue', total);
            notifyUser(falukantUser.user.hashedId, 'falukantUpdateStatus', {});
            notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', { branchId });
            return { success: true, revenue: total };
        });
    }

    async addSellItem(branchId, userId, productId, quantity, transaction = null) {
        const branch = await Branch.findOne({
            where: { id: branchId },
            attributes: ['id', 'regionId'],
            transaction: transaction || undefined
        });
        if (!branch) throw new Error(`Branch not found (branchId: ${branchId})`);

        const [daySell, created] = await DaySell.findOrCreate({
            where: {
                regionId: branch.regionId,
                productId: productId,
                sellerId: userId,
            },
            defaults: { quantity: quantity },
            transaction: transaction || undefined
        });
        if (!created) {
            daySell.quantity += quantity;
            await daySell.save({ transaction: transaction || undefined });
        }
    }

    async addSellItemsBatch(sellItems, userId, transaction = null) {
        if (!sellItems || sellItems.length === 0) return;
        
        // Lade alle benötigten Branches auf einmal
        const branchIds = [...new Set(sellItems.map(item => item.branchId))];
        const branches = await Branch.findAll({
            where: { id: { [Op.in]: branchIds } },
            attributes: ['id', 'regionId'],
            transaction: transaction || undefined
        });
        const branchMap = new Map(branches.map(b => [b.id, b]));
        // WICHTIG: Wie bei addSellItem muss ein fehlender Branch ein harter Fehler sein,
        // sonst entsteht ein Accounting-/Audit-Mismatch (Geld/Steuern werden gebucht, aber Sell-Logs fehlen).
        const missingBranchIds = branchIds.filter(id => !branchMap.has(id));
        if (missingBranchIds.length > 0) {
            throw new Error(
                `Branch not found for sell batch (missing branchIds: ${missingBranchIds.join(', ')})`
            );
        }

        // Gruppiere nach (regionId, productId, sellerId)
        const grouped = new Map();
        for (const item of sellItems) {
            const branch = branchMap.get(item.branchId);
            // sollte durch missingBranchIds Check oben nie passieren, aber defensiv:
            if (!branch) {
                throw new Error(`Branch not found for sell batch (branchId: ${item.branchId})`);
            }
            
            const key = `${branch.regionId}-${item.productId}-${userId}`;
            if (!grouped.has(key)) {
                grouped.set(key, {
                    regionId: branch.regionId,
                    productId: item.productId,
                    sellerId: userId,
                    quantity: 0
                });
            }
            grouped.get(key).quantity += item.quantity;
        }

        // Batch-Update oder Create für alle gruppierten Einträge
        const promises = [];
        for (const [key, data] of grouped) {
            promises.push(
                DaySell.findOrCreate({
                    where: {
                        regionId: data.regionId,
                        productId: data.productId,
                        sellerId: data.sellerId
                    },
                    defaults: { quantity: data.quantity },
                    transaction: transaction || undefined
                }).then(([daySell, created]) => {
                    if (!created) {
                        daySell.quantity += data.quantity;
                        return daySell.save({ transaction: transaction || undefined });
                    }
                })
            );
        }
        await Promise.all(promises);
    }

    // Return tax summary for a branch: total cumulative tax and breakdown per region (region -> parent chain)
    async getBranchTaxes(hashedUserId, branchId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id }, include: [{ model: RegionData, as: 'region', attributes: ['id','name','parentId','taxPercent'] }] });
        if (!branch) throw new Error('Branch not found');
        const regionId = branch.regionId;

        // gather region + ancestors with taxPercent
        const rows = await sequelize.query(`
            WITH RECURSIVE ancestors AS (
              SELECT id, parent_id, tax_percent, name FROM falukant_data.region WHERE id = :id
              UNION ALL
              SELECT r.id, r.parent_id, r.tax_percent, r.name FROM falukant_data.region r JOIN ancestors a ON r.id = a.parent_id
            ) SELECT id, name, parent_id, COALESCE(tax_percent,0) AS tax_percent FROM ancestors;
        `, { replacements: { id: regionId }, type: sequelize.QueryTypes.SELECT });

        // compute total
        const total = rows.reduce((sum, r) => sum + parseFloat(r.tax_percent || 0), 0);

        return {
            regionId,
            total: Math.round(total * 100) / 100,
            breakdown: rows.map(r => ({ id: r.id, name: r.name, parentId: r.parent_id, taxPercent: parseFloat(r.tax_percent || 0) }))
        };
    }

    async moneyHistory(hashedUserId, page = 1, filter = '') {
        const u = await getFalukantUserOrFail(hashedUserId);
        const limit = 25, offset = (page - 1) * limit;
        const w = { falukantUserId: u.id };
        if (filter) w.activity = { [Op.iLike]: `%${filter}%` };
        const { rows, count } = await MoneyFlow.findAndCountAll({
            where: w, order: [['time', 'DESC']], limit, offset
        });
        return { data: rows, total: count, currentPage: page, totalPages: Math.ceil(count / limit) };
    }

    async getStorage(hashedUserId, branchId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const branch = await getBranchOrFail(user.id, branchId);
        const stocks = await FalukantStock.findAll({
            where: { branchId: branch.id },
            include: [{ model: FalukantStockType, as: 'stockType' }],
        });
        const stockIds = stocks.map(s => s.id);
        const inventoryItems = await Inventory.findAll({
            where: { stockId: stockIds },
            include: [
                {
                    model: FalukantStock,
                    as: 'stock',
                    include: [{ model: FalukantStockType, as: 'stockType' }],
                },
            ],
        });
        let totalUsedCapacity = 0;
        const usageByType = {};
        for (const s of stocks) {
            const stId = s.stockTypeId;
            if (!usageByType[stId]) {
                usageByType[stId] = {
                    stockTypeId: stId,
                    stockTypeLabelTr: s.stockType?.labelTr || `stockType:${stId}`,
                    totalCapacity: 0,
                    used: 0
                };
            }
            usageByType[stId].totalCapacity += s.quantity;
        }
        for (const item of inventoryItems) {
            totalUsedCapacity += item.quantity;
            const stId = item.stock.stockTypeId;
            if (!usageByType[stId]) {
                usageByType[stId] = {
                    stockTypeId: stId,
                    stockTypeLabelTr: item.stock.stockType?.labelTr || `stockType:${stId}`,
                    totalCapacity: 0,
                    used: 0
                };
            }
            usageByType[stId].used += item.quantity;
        }
        const buyableStocks = await BuyableStock.findAll({
            where: { regionId: branch.regionId },
            include: [{ model: FalukantStockType, as: 'stockType' }],
        });
        const buyableByType = {};
        for (const b of buyableStocks) {
            const stId = b.stockTypeId;
            if (!buyableByType[stId]) {
                buyableByType[stId] = {
                    stockTypeId: stId,
                    stockTypeLabelTr: b.stockType?.labelTr || `stockType:${stId}`,
                    quantity: 0
                };
            }
            buyableByType[stId].quantity += b.quantity;
        }
        let maxCapacity = stocks.reduce((sum, s) => sum + s.quantity, 0);
        return {
            branchId,
            totalUsedCapacity,
            maxCapacity,
            usageByType: Object.values(usageByType),
            buyableByType: Object.values(buyableByType)
        };
    }

    async buyStorage(hashedUserId, branchId, amount, stockTypeId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const branch = await getBranchOrFail(user.id, branchId);
        const buyableStocks = await BuyableStock.findAll({
            where: { regionId: branch.regionId, stockTypeId },
            include: [{ model: FalukantStockType, as: 'stockType' }]
        });
        if (!buyableStocks || buyableStocks.length === 0) {
            throw new Error('Not enough buyable stock');
        }
        const totalAvailable = buyableStocks.reduce((sum, entry) => sum + entry.quantity, 0);
        if (totalAvailable < amount) {
            throw new Error('Not enough buyable stock');
        }
        const costPerUnit = buyableStocks[0].stockType.cost;
        const totalCost = costPerUnit * amount;
        if (user.money < totalCost) {
            throw new Error('notenoughmoney');
        }
        const moneyResult = await updateFalukantUserMoney(
            user.id,
            -totalCost,
            `Buy storage (type: ${buyableStocks[0].stockType.labelTr})`,
            user.id
        );
        if (!moneyResult.success) {
            throw new Error('Failed to update money');
        }
        let remainingToDeduct = amount;
        for (const entry of buyableStocks) {
            if (entry.quantity > remainingToDeduct) {
                entry.quantity -= remainingToDeduct;
                await entry.save();
                remainingToDeduct = 0;
                break;
            } else if (entry.quantity === remainingToDeduct) {
                await entry.destroy();
                remainingToDeduct = 0;
                break;
            } else {
                remainingToDeduct -= entry.quantity;
                await entry.destroy();
            }
        }
        let stock = await FalukantStock.findOne({
            where: { branchId: branch.id, stockTypeId },
            include: [{ model: FalukantStockType, as: 'stockType' }]
        });
        if (!stock) {
            stock = await FalukantStock.create({
                branchId: branch.id,
                stockTypeId,
                quantity: amount,
            });
        } else {
            stock.quantity += amount;
            await stock.save();
        }
        notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
        notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId });

        return {
            success: true,
            bought: amount,
            totalCost,
            stockType: buyableStocks[0].stockType.labelTr
        };
    }

    async sellStorage(hashedUserId, branchId, amount, stockTypeId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const branch = await getBranchOrFail(user.id, branchId);
        const stock = await FalukantStock.findOne({
            where: { branchId: branch.id, stockTypeId },
            include: [{ model: FalukantStockType, as: 'stockType' }]
        });
        if (!stock || stock.quantity < amount) throw new Error('Not enough stock to sell');
        const costPerUnit = stock.stockType.cost;
        const totalRevenue = costPerUnit * amount;
        const moneyResult = await updateFalukantUserMoney(
            user.id,
            totalRevenue,
            `Sell storage (type: ${stock.stockType.labelTr})`,
            user.id
        );
        if (!moneyResult.success) throw new Error('Failed to update money');
        stock.quantity -= amount;
        await stock.save();
        const buyable = await BuyableStock.findOne({
            where: { regionId: branch.regionId, stockTypeId },
            include: [{ model: FalukantStockType, as: 'stockType' }]
        });
        if (!buyable) throw new Error('No buyable record found for this region and stockType');
        buyable.quantity += amount;
        await buyable.save();
        return { success: true, sold: amount, totalRevenue, stockType: stock.stockType.labelTr };
    }

    async notifyRegionUsersAboutStockChange(regionId) {
        const users = await FalukantCharacter.findAll({
            where: {
                regionId: regionId
            },
            include: [
                {
                    model: FalukantUser,
                    as: 'user',
                    include: [
                        {
                            model: User,
                            as: 'user'
                        },
                        {
                            model: Branch,
                            as: 'branch',
                            where: {
                                regionId: regionId
                            }
                        }
                    ]
                }
            ]
        });
        for (const user of users) {
            notifyUser(user.user[0].user[0].hashedId, 'stock_change', { branchId: user.user[0].branch[0].id });
        }
    }

    async getStockTypes() {
        return FalukantStockType.findAll();
    }

    async getStockOverview() {
        const items = await Inventory.findAll({
            include: [
                {
                    model: FalukantStock,
                    as: 'stock',
                    include: [
                        {
                            model: Branch,
                            as: 'branch',
                            include: [{ model: RegionData, as: 'region' }]
                        }
                    ]
                },
                {
                    model: ProductType,
                    as: 'productType'
                }
            ]
        });
        const result = items.map(inv => ({
            regionName: inv.stock?.branch?.region?.name || '???',
            productLabelTr: inv.productType?.labelTr || '???',
            quantity: inv.quantity
        }));
        return result;
    }

    async getAllProductions(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const productions = await Production.findAll({
            include: [
                {
                    model: Branch,
                    as: 'branch',
                    include: [
                        { model: RegionData, as: 'region', attributes: ['name'] }
                    ],
                    where: { falukantUserId: user.id }
                },
                { model: ProductType, as: 'productType', attributes: ['labelTr', 'productionTime', 'id'] },
                { model: WeatherType, as: 'weatherType' }
            ],
            attributes: ['startTimestamp', 'quantity', 'productId', 'weatherTypeId'],
        });
        
        // Berechne Qualität für jede Produktion
        const formattedProductions = await Promise.all(productions.map(async (production) => {
            const startTimestamp = new Date(production.startTimestamp).getTime();
            const endTimestamp = startTimestamp + production.productType.productionTime * 60 * 1000;
            
            // Berechne Qualität basierend auf Wettereffekt
            let quality = 50; // Basisqualität (50%)
            
            if (production.weatherTypeId) {
                const weatherEffect = await ProductWeatherEffect.findOne({
                    where: {
                        productId: production.productId,
                        weatherTypeId: production.weatherTypeId
                    }
                });
                
                if (weatherEffect) {
                    // Wettereffekt: -2 bis +2, wird auf Qualität angewendet
                    quality = Math.max(0, Math.min(100, 50 + (weatherEffect.qualityEffect * 10)));
                }
            }
            
            return {
                cityName: production.branch.region.name,
                productName: production.productType.labelTr,
                quantity: production.quantity,
                quality: Math.round(quality),
                endTimestamp: new Date(endTimestamp).toISOString(),
            };
        }));
        
        formattedProductions.sort((a, b) => new Date(a.endTimestamp) - new Date(b.endTimestamp));
        return formattedProductions;
    }

    async getDirectorProposals(hashedUserId, branchId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        if (!user) {
            throw new Error('User not found');
        }
        const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id } });
        if (!branch) {
            throw new Error('Branch not found or does not belong to the user');
        }
        const { falukantUserId, regionId } = branch;
        await this.deleteExpiredProposals();
        const existingProposals = await this.fetchProposals(falukantUserId, regionId);
        if (existingProposals.length > 0) {
            return this.formatProposals(existingProposals);
        }
        await this.generateProposals(falukantUserId, regionId);
        const newProposals = await this.fetchProposals(falukantUserId, regionId);
        return this.formatProposals(newProposals);
    }

    async deleteExpiredProposals() {
        const expirationTime = new Date(Date.now() - 24 * 60 * 60 * 1000);
        await DirectorProposal.destroy({
            where: {
                createdAt: {
                    [Op.lt]: expirationTime,
                },
            },
        });
    }

    async fetchProposals(falukantUserId, regionId) {
        return DirectorProposal.findAll({
            where: { employerUserId: falukantUserId },
            include: [
                {
                    model: FalukantCharacter,
                    as: 'character',
                    attributes: ['firstName', 'lastName', 'birthdate', 'titleOfNobility', 'gender'],
                    where: { regionId },
                    include: [
                        { model: FalukantPredefineFirstname, as: 'definedFirstName' },
                        { model: FalukantPredefineLastname, as: 'definedLastName' },
                        { model: TitleOfNobility, as: 'nobleTitle' },
                        {
                            model: Knowledge,
                            as: 'knowledges',
                            include: [
                                { model: ProductType, as: 'productType' },
                            ]
                        },
                    ],
                },
            ],
        });
    }

    async generateProposals(falukantUserId, regionId) {
        try {
            const threeWeeksAgo = new Date(Date.now() - 21 * 24 * 60 * 60 * 1000);
            const proposalCount = Math.floor(Math.random() * 3) + 3;
            
            // OPTIMIERUNG: Verwende eine einzige SQL-Query mit CTEs statt mehrerer separater Queries
            // Dies ist viel schneller, da PostgreSQL die Query optimieren kann
            // Die Knowledge-Berechnung wird direkt in SQL gemacht (AVG)
            const sqlQuery = `
                WITH excluded_characters AS (
                    SELECT DISTINCT director_character_id AS id
                    FROM falukant_data.director_proposal
                    WHERE employer_user_id = :falukantUserId
                    UNION
                    SELECT DISTINCT director_character_id AS id
                    FROM falukant_data.director
                ),
                older_characters AS (
                    SELECT 
                        c.id,
                        c.title_of_nobility,
                        t.level,
                        COALESCE(AVG(k.knowledge), 0) AS avg_knowledge
                    FROM falukant_data.character c
                    LEFT JOIN falukant_type.title_of_nobility t ON t.id = c.title_of_nobility
                    LEFT JOIN falukant_data.knowledge k ON k.character_id = c.id
                    WHERE c.region_id = :regionId
                      AND c.user_id IS NULL
                      AND c.created_at < :threeWeeksAgo
                      AND c.id NOT IN (SELECT id FROM excluded_characters)
                    GROUP BY c.id, c.title_of_nobility, t.level
                    ORDER BY RANDOM()
                    LIMIT :proposalCount
                ),
                all_characters AS (
                    SELECT 
                        c.id,
                        c.title_of_nobility,
                        t.level,
                        COALESCE(AVG(k.knowledge), 0) AS avg_knowledge
                    FROM falukant_data.character c
                    LEFT JOIN falukant_type.title_of_nobility t ON t.id = c.title_of_nobility
                    LEFT JOIN falukant_data.knowledge k ON k.character_id = c.id
                    WHERE c.region_id = :regionId
                      AND c.user_id IS NULL
                      AND c.id NOT IN (SELECT id FROM excluded_characters)
                      AND c.id NOT IN (SELECT id FROM older_characters)
                    GROUP BY c.id, c.title_of_nobility, t.level
                    ORDER BY RANDOM()
                    LIMIT GREATEST(0, :proposalCount - (SELECT COUNT(*) FROM older_characters))
                )
                SELECT * FROM older_characters
                UNION ALL
                SELECT * FROM all_characters
                LIMIT :proposalCount
            `;
            
            const results = await sequelize.query(sqlQuery, {
                replacements: {
                    falukantUserId,
                    regionId,
                    threeWeeksAgo,
                    proposalCount
                },
                type: sequelize.QueryTypes.SELECT
            });
            
            if (results.length === 0) {
                console.error(`[generateProposals] No NPCs found in region ${regionId}`);
                throw new Error('No directors available for the region');
            }
            
            console.log(`[generateProposals] Found ${results.length} available NPCs`);
            
            // Erstelle alle Proposals in einem Batch
            const proposalsToCreate = results.map(row => {
                const avgKnowledge = parseFloat(row.avg_knowledge) || 0;
                const proposedIncome = Math.round(
                    row.level * Math.pow(1.231, avgKnowledge / 1.5)
                );
                
                return {
                    directorCharacterId: row.id,
                    employerUserId: falukantUserId,
                    proposedIncome,
                };
            });
            
            await DirectorProposal.bulkCreate(proposalsToCreate);
            
            console.log(`[generateProposals] Created ${proposalsToCreate.length} director proposals for region ${regionId}`);
        } catch (error) {
            console.error('[generateProposals] Error:', error.message, error.stack);
            throw new Error(error.message);
        }
    }

    async calculateAverageKnowledge(characterId) {
        const averageKnowledge = await Knowledge.findAll({
            where: { characterId },
            attributes: [[Sequelize.fn('AVG', Sequelize.col('knowledge')), 'avgKnowledge']],
            raw: true,
        });
        return parseFloat(averageKnowledge[0]?.avgKnowledge || 0);
    }

    formatProposals(proposals) {
        return proposals.map((proposal) => {
            const age = Math.floor((Date.now() - new Date(proposal.character.birthdate)) / (24 * 60 * 60 * 1000));
            const knowledge = proposal.character.knowledges?.map(k => ({
                productId: k.productId,
                value: k.knowledge,
                labelTr: k.productType.labelTr,
            })) || [];
            return {
                id: proposal.id,
                proposedIncome: proposal.proposedIncome,
                character: {
                    name: `${proposal.character.definedFirstName.name} ${proposal.character.definedLastName.name}`,
                    title: proposal.character.nobleTitle.labelTr,
                    age,
                    knowledge,
                    gender: proposal.character.gender,
                },
            };
        });
    }

    async convertProposalToDirector(hashedUserId, proposalId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        if (!user) {
            throw new Error('User not found');
        }
        const proposal = await DirectorProposal.findOne(
            {
                where: { id: proposalId },
                include: [
                    { model: FalukantCharacter, as: 'character' },
                ]
            }
        );
        if (!proposal || proposal.employerUserId !== user.id) {
            throw new Error('Proposal does not belong to the user');
        }
        const existingDirector = await Director.findOne({
            where: {
                employerUserId: user.id
            },
            include: [
                {
                    model: FalukantCharacter,
                    as: 'character',
                    where: {
                        regionId: proposal.character.regionId,
                    }
                },
            ]
        });
        if (existingDirector) {
            throw new Error('A director already exists for this region');
        }
        const newDirector = await Director.create({
            directorCharacterId: proposal.directorCharacterId,
            employerUserId: proposal.employerUserId,
            income: proposal.proposedIncome,
        });
        const regionUserDirectorProposals = await DirectorProposal.findAll({
            where: {
                employerUserId: proposal.employerUserId,
            },
            include: [
                {
                    model: FalukantCharacter,
                    as: 'character',
                    where: {
                        regionId: proposal.character.regionId,
                    }
                },
            ]
        });
        if (regionUserDirectorProposals.length > 0) {
            for (const p of regionUserDirectorProposals) {
                await p.destroy();
            }
        }
        notifyUser(hashedUserId, 'directorchanged');
        return newDirector;
    }

    async getDirectorForBranch(hashedUserId, branchId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        if (!user) {
            throw new Error('User not found');
        }
        const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id } });
        if (!branch) {
            throw new Error('Branch not found or does not belong to the user');
        }
        const director = await Director.findOne({
            where: { employerUserId: user.id },
            include: [
                {
                    model: FalukantCharacter,
                    as: 'character',
                    // Wichtige Felder explizit auswählen, damit die Joins auf Titel,
                    // Namen und Region im äußeren Select funktionieren.
                    attributes: ['id', 'birthdate', 'gender', 'titleOfNobility', 'firstName', 'lastName', 'regionId'],
                    where: {
                        regionId: branch.regionId,
                    },
                    include: [
                        {
                            model: TitleOfNobility,
                            as: 'nobleTitle',
                            attributes: ['labelTr', 'level']
                        },
                        {
                            model: FalukantPredefineFirstname,
                            as: 'definedFirstName',
                            attributes: ['name']
                        },
                        {
                            model: FalukantPredefineLastname,
                            as: 'definedLastName',
                            attributes: ['name']
                        },
                        {
                            model: Knowledge,
                            as: 'knowledges',
                            attributes: ['productId', 'knowledge'],
                            include: [
                                {
                                    model: ProductType,
                                    as: 'productType',
                                    attributes: ['labelTr'],
                                },
                            ],
                        },
                        {
                            model: RegionData,
                            as: 'region',
                            attributes: ['name']
                        }
                    ]
                },
            ],
        });
        if (!director) {
            return null;
        }
        const age = Math.floor((Date.now() - new Date(director.character.birthdate)) / (24 * 60 * 60 * 1000));

        // wishedIncome analog zu getAllDirectors() berechnen
        const knowledges = director.character.knowledges || [];
        const avgKnowledge = knowledges.length
            ? knowledges.reduce((sum, k) => sum + k.knowledge, 0) / knowledges.length
            : 0;
        const wishedIncome = Math.round(
            director.character.nobleTitle.level * Math.pow(1.231, avgKnowledge / 1.5)
        );

        return {
            director: {
                id: director.id,
                character: {
                    name: `${director.character.definedFirstName.name} ${director.character.definedLastName.name}`,
                    title: director.character.nobleTitle.labelTr,
                    age,
                    gender: director.character.gender,
                    nobleTitle: director.character.nobleTitle,
                    definedFirstName: director.character.definedFirstName,
                    definedLastName: director.character.definedLastName,
                    knowledges: director.character.knowledges,
                },
                income: director.income,
                satisfaction: director.satisfaction,
                mayProduce: director.mayProduce,
                maySell: director.maySell,
                mayStartTransport: director.mayStartTransport,
                mayRepairVehicles: director.mayRepairVehicles,
                region: director.character.region?.name || null,
                wishedIncome,
            },
        };
    }

    async getAllDirectors(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        if (!user) {
            throw new Error('User not found');
        }
        const directors = await Director.findAll({
            where: { employerUserId: user.id },
            include: [
                {
                    model: FalukantCharacter,
                    as: 'character',
                    attributes: ['id', 'birthdate', 'gender'],
                    include: [
                        {
                            model: TitleOfNobility,
                            as: 'nobleTitle',
                            attributes: ['labelTr', 'level'],
                        },
                        {
                            model: FalukantPredefineFirstname,
                            as: 'definedFirstName',
                            attributes: ['name'],
                        },
                        {
                            model: FalukantPredefineLastname,
                            as: 'definedLastName',
                            attributes: ['name'],
                        },
                        {
                            model: Knowledge,
                            as: 'knowledges',
                            attributes: ['productId', 'knowledge'],
                            include: [
                                {
                                    model: ProductType,
                                    as: 'productType',
                                    attributes: ['labelTr'],
                                },
                            ],
                        },
                        {
                            model: RegionData,
                            as: 'region',
                            attributes: ['name']
                        }
                    ]
                },
            ],
            attributes: ['id', 'satisfaction', 'income'],
        });
        return directors.map(director => {
            // 1) avgKnowledge berechnen
            const knowledges = director.character.knowledges || [];
            const avgKnowledge = knowledges.length
                ? knowledges.reduce((sum, k) => sum + k.knowledge, 0) / knowledges.length
                : 0;

            // 2) wishedIncome anhand der JS-Formel
            const wishedIncome = Math.round(
                director.character.nobleTitle.level * Math.pow(1.231, avgKnowledge / 1.5)
            );

            return {
                id: director.id,
                character: {
                    name: `${director.character.definedFirstName.name} ${director.character.definedLastName.name}`,
                    title: director.character.nobleTitle.labelTr,
                    age: Math.floor((Date.now() - new Date(director.character.birthdate)) / (24 * 60 * 60 * 1000)),
                    gender: director.character.gender,
                    nobleTitle: director.character.nobleTitle,
                    definedFirstName: director.character.definedFirstName,
                    definedLastName: director.character.definedLastName,
                    knowledges: director.character.knowledges,
                },
                satisfaction: director.satisfaction,
                income: director.income,
                region: director.character.region.name,
                wishedIncome,
            };
        });
    }

    async updateDirector(hashedUserId, directorId, income) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const director = await Director.findOne({
            where: {
                id: directorId,
                employerUserId: user.id
            }
        });
        if (!director) {
            throw new Error('Director not found');
        }
        director.update({ income: income });
        return { success: true };
    }

    async setSetting(hashedUserId, branchId, directorId, settingKey, value) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const branch = await Branch.findOne({
            where: {
                id: branchId,
                falukantUserId: user.id,
            },
        });
        if (!branch) {
            return null;
        }
        const director = await Director.findOne({
            where: {
                id: directorId,
                employerUserId: user.id,
            },
            include: [{
                model: FalukantCharacter,
                as: 'character',
                where: {
                    regionId: branch.regionId,
                }
            }]
        });
        if (!director) {
            return null;
        }
        const updateData = {};
        updateData[settingKey] = value || false;

        await Director.update(updateData, {
            where: {
                id: director.id,
            },
        });
        return { result: 'ok' };
    }

    async getFamily(hashedUserId) {
        const startTime = Date.now();
        const timings = {};
        
        try {
            // 1. User und Character laden (optimiert: nur benötigte Felder)
            const step1Start = Date.now();
            const user = await FalukantUser.findOne({
                include: [
                    { model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } },
                    {
                        model: FalukantCharacter,
                        as: 'character',
                        attributes: ['id', 'birthdate', 'gender', 'regionId', 'titleOfNobility'],
                        required: true
                    }
                ]
            });
            if (!user) throw new Error('User not found');
            const character = user.character;
            if (!character) throw new Error('Character not found for this user');
            timings.step1_user_character = Date.now() - step1Start;
            
            // 2. Relationships und Children parallel laden
            const step2Start = Date.now();
            const [relationshipsRaw, charsWithChildren] = await Promise.all([
                Relationship.findAll({
                    where: { character1Id: character.id },
                    attributes: ['createdAt', 'widowFirstName2', 'nextStepProgress', 'relationshipTypeId'],
                    include: [
                        {
                            model: FalukantCharacter, as: 'character2',
                            attributes: ['id', 'birthdate', 'gender', 'moodId', 'firstName', 'lastName', 'titleOfNobility'],
                            required: false
                        },
                        { model: RelationshipType, as: 'relationshipType', attributes: ['tr'], required: false }
                    ]
                }),
                FalukantCharacter.findAll({
                    where: { userId: user.id },
                    attributes: ['id'],
                    include: [
                        {
                            model: ChildRelation,
                            as: 'childrenFather',
                            attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'],
                            include: [{
                                model: FalukantCharacter,
                                as: 'child',
                                attributes: ['id', 'birthdate', 'gender', 'firstName'],
                                required: false
                            }],
                            required: false
                        },
                        {
                            model: ChildRelation,
                            as: 'childrenMother',
                            attributes: ['nameSet', 'isHeir', 'createdAt', 'childCharacterId'],
                            include: [{
                                model: FalukantCharacter,
                                as: 'child',
                                attributes: ['id', 'birthdate', 'gender', 'firstName'],
                                required: false
                            }],
                            required: false
                        }
                    ]
                })
            ]);
            timings.step2_relationships_children = Date.now() - step2Start;
            
            // 3. Batch-Loading für Relationship-Character-Daten
            const step3Start = Date.now();
            const relationshipCharacters = relationshipsRaw
                .filter(r => r.character2)
                .map(r => r.character2);
            const relationshipCharacterIds = relationshipCharacters.map(c => c.id);
            const childCharacters = charsWithChildren
                .flatMap(c => [
                    ...(c.childrenFather || []).map(r => r.child).filter(Boolean),
                    ...(c.childrenMother || []).map(r => r.child).filter(Boolean)
                ]);
            const childCharacterIds = childCharacters.map(c => c.id);
            
            // Sammle alle benötigten IDs
            const relationshipFirstNameIds = [...new Set(relationshipCharacters.map(c => c.firstName).filter(Boolean))];
            const relationshipLastNameIds = [...new Set(relationshipCharacters.map(c => c.lastName).filter(Boolean))];
            const relationshipTitleIds = [...new Set(relationshipCharacters.map(c => c.titleOfNobility).filter(Boolean))];
            const relationshipMoodIds = [...new Set(relationshipCharacters.map(c => c.moodId).filter(Boolean))];
            const childFirstNameIds = [...new Set(childCharacters.map(c => c.firstName).filter(Boolean))];
            const allFirstNameIds = [...new Set([...relationshipFirstNameIds, ...childFirstNameIds])];
            
            // Batch-Load alle benötigten Daten parallel
            const [firstNames, lastNames, titles, traitRelations, moods] = await Promise.all([
                allFirstNameIds.length > 0 ? FalukantPredefineFirstname.findAll({
                    where: { id: { [Op.in]: allFirstNameIds } },
                    attributes: ['id', 'name']
                }) : [],
                relationshipLastNameIds.length > 0 ? FalukantPredefineLastname.findAll({
                    where: { id: { [Op.in]: relationshipLastNameIds } },
                    attributes: ['id', 'name']
                }) : [],
                relationshipTitleIds.length > 0 ? TitleOfNobility.findAll({
                    where: { id: { [Op.in]: relationshipTitleIds } },
                    attributes: ['id', 'labelTr']
                }) : [],
                relationshipCharacterIds.length > 0 ? FalukantCharacterTrait.findAll({
                    where: { characterId: { [Op.in]: relationshipCharacterIds } },
                    attributes: ['characterId', 'traitId']
                }) : [],
                relationshipMoodIds.length > 0 ? Mood.findAll({
                    where: { id: { [Op.in]: relationshipMoodIds } },
                    attributes: ['id', 'tr']
                }) : []
            ]);
            
            // Sammle alle eindeutigen Trait-IDs und lade die Trait-Types
            const allTraitIds = [...new Set(traitRelations.map(t => t.traitId))];
            const traitTypes = allTraitIds.length > 0 ? await CharacterTrait.findAll({
                where: { id: { [Op.in]: allTraitIds } },
                attributes: ['id', 'tr']
            }) : [];
            
            // Erstelle Maps für schnellen Zugriff
            const firstNameMap = new Map(firstNames.map(fn => [fn.id, fn.name]));
            const lastNameMap = new Map(lastNames.map(ln => [ln.id, ln.name]));
            const titleMap = new Map(titles.map(t => [t.id, t.labelTr]));
            const moodMap = new Map(moods.map(m => [m.id, m.tr]));
            const traitTypeMap = new Map(traitTypes.map(t => [t.id, { id: t.id, tr: t.tr }]));
            const traitsMap = new Map();
            traitRelations.forEach(t => {
                if (!traitsMap.has(t.characterId)) {
                    traitsMap.set(t.characterId, []);
                }
                const traitObj = traitTypeMap.get(t.traitId);
                if (traitObj) {
                    traitsMap.get(t.characterId).push(traitObj);
                }
            });
            timings.step3_batch_loading = Date.now() - step3Start;
            
            // 4. Relationships mappen
            const step4Start = Date.now();
            const relationships = relationshipsRaw.map(r => {
                const char2 = r.character2;
                return {
                    createdAt: r.createdAt,
                    widowFirstName2: r.widowFirstName2,
                    progress: r.nextStepProgress,
                    character2: char2 ? {
                        id: char2.id,
                        age: calcAge(char2.birthdate),
                        gender: char2.gender,
                        firstName: firstNameMap.get(char2.firstName) || 'Unknown',
                        nobleTitle: titleMap.get(char2.titleOfNobility) || '',
                        mood: moodMap.get(char2.moodId) || null,
                        moodId: char2.moodId,
                        traits: traitsMap.get(char2.id) || []
                    } : null,
                    relationshipType: r.relationshipType?.tr || ''
                };
            }).filter(r => r.character2 !== null);
            timings.step4_map_relationships = Date.now() - step4Start;
            
            // 5. Children mappen
            const step5Start = Date.now();
            const children = [];
            for (const parentChar of charsWithChildren) {
                const allRels = [
                    ...(parentChar.childrenFather || []),
                    ...(parentChar.childrenMother || [])
                ];
                for (const rel of allRels) {
                    const kid = rel.child;
                    if (!kid) continue;
                    children.push({
                        childCharacterId: kid.id,
                        name: firstNameMap.get(kid.firstName) || 'Unknown',
                        gender: kid.gender,
                        age: calcAge(kid.birthdate),
                        hasName: rel.nameSet,
                        isHeir: rel.isHeir || false,
                        _createdAt: rel.createdAt,
                    });
                }
            }
            children.sort((a, b) => new Date(a._createdAt) - new Date(b._createdAt));
            timings.step5_map_children = Date.now() - step5Start;
            
            // 6. Family-Objekt erstellen
            const step6Start = Date.now();
            const inProgress = ['wooing', 'engaged', 'married'];
            const family = {
                relationships: relationships.filter(r => inProgress.includes(r.relationshipType)),
                lovers: relationships.filter(r => r.relationshipType === 'lover'),
                deathPartners: relationships.filter(r => r.relationshipType === 'widowed'),
                children: children.map(({ _createdAt, ...rest }) => rest),
                possiblePartners: []
            };
            timings.step6_create_family = Date.now() - step6Start;
            
            // 7. Possible Partners (nur wenn nötig, asynchron)
            const step7Start = Date.now();
            const ownAge = calcAge(character.birthdate);
            if (ownAge >= 12 && family.relationships.length === 0) {
                family.possiblePartners = await this.getPossiblePartners(character.id);
                if (family.possiblePartners.length === 0) {
                    // Asynchron erstellen, nicht blockieren
                    this.createPossiblePartners(
                        character.id,
                        character.gender,
                        character.regionId,
                        character.titleOfNobility,
                        ownAge
                    ).catch(err => console.error('[getFamily] Error creating partners (async):', err));
                }
            }
            timings.step7_possible_partners = Date.now() - step7Start;
            
            return family;
        } catch (error) {
            console.error('[getFamily] Error:', error);
            throw error;
        }
    }

    async setHeir(hashedUserId, childCharacterId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        if (!user) throw new Error('User not found');
        const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
        if (!character) throw new Error('Character not found for this user');

        // Prüfe, ob das Kind wirklich ein Kind des aktuellen Charakters ist
        const childRelation = await ChildRelation.findOne({
            where: {
                childCharacterId: childCharacterId,
                [Op.or]: [
                    { fatherCharacterId: character.id },
                    { motherCharacterId: character.id }
                ]
            }
        });

        if (!childRelation) {
            throw new Error('Child not found or not your child');
        }

        // Setze alle anderen Kinder des gleichen Elternteils auf isHeir = false
        await ChildRelation.update(
            { isHeir: false },
            {
                where: {
                    [Op.or]: [
                        { fatherCharacterId: character.id },
                        { motherCharacterId: character.id }
                    ]
                }
            }
        );

        // Setze das ausgewählte Kind als Erben
        await childRelation.update({ isHeir: true });

        return { success: true, childCharacterId };
    }

    async getPotentialHeirs(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        
        // Prüfe, ob der User bereits einen Charakter hat
        const existingCharacter = await FalukantCharacter.findOne({ where: { userId: user.id } });
        if (existingCharacter) {
            throw new Error('User already has a character');
        }

        if (!user.mainBranchRegionId) {
            throw new Error('User has no main branch region');
        }

        // Hole den noncivil Titel
        const noncivilTitle = await TitleOfNobility.findOne({ where: { labelTr: 'noncivil' } });
        if (!noncivilTitle) {
            throw new Error('Noncivil title not found');
        }

        // Berechne das Datum für 10-14 Jahre alt (in Tagen)
        const now = new Date();
        now.setHours(0, 0, 0, 0);
        const minDate = new Date(now);
        minDate.setDate(minDate.getDate() - 14); // 14 Tage = 14 Jahre
        const maxDate = new Date(now);
        maxDate.setDate(maxDate.getDate() - 10); // 10 Tage = 10 Jahre

        // Hole zufällige Charaktere aus der Hauptregion, die 10-14 Jahre alt sind
        // und keinen userId haben (also noch keinem User zugeordnet sind)
        // und den noncivil Titel haben
        let potentialHeirs = await FalukantCharacter.findAll({
            where: {
                regionId: user.mainBranchRegionId,
                userId: null,
                titleOfNobility: noncivilTitle.id,
                birthdate: {
                    [Op.between]: [minDate, maxDate]
                }
            },
            include: [
                { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }
            ],
            order: Sequelize.fn('RANDOM'),
            limit: 5
        });

        // Wenn keine Charaktere gefunden wurden, erstelle 5 neue
        if (potentialHeirs.length === 0) {
            const genders = ['male', 'female'];
            const createdHeirs = [];

            // Erstelle neue Charaktere in einer Transaktion
            await sequelize.transaction(async (t) => {
                for (let i = 0; i < 5; i++) {
                    // Zufälliges Geschlecht
                    const gender = genders[Math.floor(Math.random() * genders.length)];
                    
                    // Zufälliger Vorname für das Geschlecht
                    const firstName = await this.randomFirstName(gender);
                    let fnObj = await FalukantPredefineFirstname.findOne({ 
                        where: { name: firstName, gender },
                        transaction: t
                    });
                    if (!fnObj) {
                        fnObj = await FalukantPredefineFirstname.create({ 
                            name: firstName, gender 
                        }, { transaction: t });
                    }

                    // Zufälliger Nachname
                    const lastName = await this.randomLastName();
                    let lnObj = await FalukantPredefineLastname.findOne({ 
                        where: { name: lastName },
                        transaction: t
                    });
                    if (!lnObj) {
                        lnObj = await FalukantPredefineLastname.create({ 
                            name: lastName 
                        }, { transaction: t });
                    }

                    // Zufälliges Alter zwischen 10 und 14 Jahren (in Tagen)
                    const randomAge = Math.floor(Math.random() * 5) + 10; // 10-14
                    const birthdate = new Date(now);
                    birthdate.setDate(birthdate.getDate() - randomAge);

                    // Erstelle den Charakter
                    const newCharacter = await FalukantCharacter.create({
                        userId: null, // Wichtig: noch keinem User zugeordnet
                        regionId: user.mainBranchRegionId,
                        firstName: fnObj.id,
                        lastName: lnObj.id,
                        gender: gender,
                        birthdate: birthdate,
                        titleOfNobility: noncivilTitle.id,
                        health: 100,
                        moodId: 1
                    }, { transaction: t });

                    // Lade die Namen für die Rückgabe
                    const heirWithNames = await FalukantCharacter.findOne({
                        where: { id: newCharacter.id },
                        include: [
                            { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                            { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }
                        ],
                        transaction: t
                    });

                    createdHeirs.push(heirWithNames);
                }
            });

            potentialHeirs = createdHeirs;
        }

        // Berechne das Alter für jeden Charakter
        return potentialHeirs.map(heir => {
            const age = calcAge(heir.birthdate);
            return {
                id: heir.id,
                definedFirstName: heir.definedFirstName,
                definedLastName: heir.definedLastName,
                gender: heir.gender,
                age: age
            };
        });
    }

    async selectHeir(hashedUserId, heirId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        
        // Prüfe, ob der User bereits einen Charakter hat
        const existingCharacter = await FalukantCharacter.findOne({ where: { userId: user.id } });
        if (existingCharacter) {
            throw new Error('User already has a character');
        }

        // Hole den Erben-Charakter
        const heir = await FalukantCharacter.findOne({
            where: {
                id: heirId,
                userId: null // Stelle sicher, dass er noch keinem User zugeordnet ist
            }
        });

        if (!heir) {
            throw new Error('Heir not found or already assigned');
        }

        // Prüfe, ob der Erbe in der Hauptregion ist
        if (heir.regionId !== user.mainBranchRegionId) {
            throw new Error('Heir is not in main branch region');
        }

        // Prüfe das Alter (10-14 Jahre)
        const age = calcAge(heir.birthdate);
        if (age < 10 || age > 14) {
            throw new Error('Heir age is not between 10 and 14');
        }

        // Weise den Charakter dem User zu
        await heir.update({ userId: user.id });

        // Benachrichtige den User
        notifyUser(hashedUserId, 'falukantUserUpdated', {});

        return { success: true, characterId: heir.id };
    }

    async getPossiblePartners(requestingCharacterId) {
        const proposals = await MarriageProposal.findAll({
            where: {
                requesterCharacterId: requestingCharacterId,
            },
            include: [
                {
                    model: FalukantCharacter,
                    as: 'proposedCharacter',
                    attributes: ['id', 'firstName', 'lastName', 'gender', 'regionId', 'birthdate'],
                    include: [
                        { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                        { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
                        { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] },
                    ],
                },
            ],
        });
        return proposals.map(proposal => {
            const birthdate = new Date(proposal.proposedCharacter.birthdate);
            const age = calcAge(birthdate);
            return {
                id: proposal.id,
                requesterCharacterId: proposal.requesterCharacterId,
                proposedCharacterId: proposal.proposedCharacter.id,
                proposedCharacterName: `${proposal.proposedCharacter.definedFirstName?.name} ${proposal.proposedCharacter.definedLastName?.name}`,
                proposedCharacterGender: proposal.proposedCharacter.gender,
                proposedCharacterRegionId: proposal.proposedCharacter.regionId,
                proposedCharacterAge: age,
                proposedCharacterNobleTitle: proposal.proposedCharacter.nobleTitle.labelTr,
                cost: proposal.cost,
            };
        });
    }

    async createPossiblePartners(requestingCharacterId, requestingCharacterGender, requestingRegionId, requestingCharacterTitleOfNobility, ownAge) {
        try {
            const minTitleResult = await TitleOfNobility.findOne({
                order: [['id', 'ASC']],
                attributes: ['id'],
            });
            if (!minTitleResult) {
                throw new Error('No title of nobility found');
            }
            const minTitle = minTitleResult.id;

            // Logging für Debugging
            console.log(`[createPossiblePartners] Searching for partners:`, {
                requestingCharacterId,
                requestingCharacterGender,
                requestingRegionId,
                requestingCharacterTitleOfNobility,
                ownAge
            });

            const minAgeDate = new Date(new Date() - 12 * 24 * 60 * 60 * 1000);
            const titleMin = Math.max(1, requestingCharacterTitleOfNobility - 1);
            const titleMax = requestingCharacterTitleOfNobility + 1;

            console.log(`[createPossiblePartners] Search criteria:`, {
                excludeId: requestingCharacterId,
                gender: `not ${requestingCharacterGender}`,
                regionId: requestingRegionId,
                minAge: '12 days old',
                titleRange: `${titleMin}-${titleMax}`,
                userId: 'null (NPCs only)'
            });

            const whereClause = {
                id: { [Op.ne]: requestingCharacterId },
                gender: { [Op.ne]: requestingCharacterGender },
                regionId: requestingRegionId,
                createdAt: { [Op.lt]: minAgeDate },
                titleOfNobility: { [Op.between]: [titleMin, titleMax] },
                userId: null // Nur NPCs suchen
            };

            let potentialPartners = await FalukantCharacter.findAll({
                where: whereClause,
                order: [
                    [Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
                ],
                limit: 5,
            });

            console.log(`[createPossiblePartners] Found ${potentialPartners.length} potential partners in region ${requestingRegionId}`);

            // Fallback: Wenn keine Partner in der gleichen Region gefunden werden, suche in allen Regionen
            if (potentialPartners.length === 0) {
                console.log(`[createPossiblePartners] No partners in region ${requestingRegionId}, trying all regions...`);
                const fallbackWhereClause = {
                    id: { [Op.ne]: requestingCharacterId },
                    gender: { [Op.ne]: requestingCharacterGender },
                    createdAt: { [Op.lt]: minAgeDate },
                    titleOfNobility: { [Op.between]: [titleMin, titleMax] },
                    userId: null
                };
                
                potentialPartners = await FalukantCharacter.findAll({
                    where: fallbackWhereClause,
                    order: [
                        [Sequelize.literal(`ABS((EXTRACT(EPOCH FROM (NOW() - "birthdate")) / 86400) - ${ownAge})`), 'ASC']
                    ],
                    limit: 5,
                });
                
                console.log(`[createPossiblePartners] Found ${potentialPartners.length} potential partners in all regions`);
            }

            if (potentialPartners.length === 0) {
                console.log(`[createPossiblePartners] No partners found, creating new NPCs...`);
                // Erstelle automatisch 5 neue NPCs, die den Kriterien entsprechen
                const targetGender = requestingCharacterGender === 'male' ? 'female' : 'male';
                const createdNPCs = await this._createNPCsForMarriage(
                    requestingRegionId,
                    targetGender,
                    titleMin,
                    titleMax,
                    ownAge,
                    5
                );
                
                if (createdNPCs.length > 0) {
                    console.log(`[createPossiblePartners] Created ${createdNPCs.length} new NPCs, using them as partners`);
                    potentialPartners = createdNPCs;
                } else {
                    console.warn(`[createPossiblePartners] Failed to create NPCs. Consider creating NPCs manually with:`);
                    console.warn(`  - gender: ${targetGender}`);
                    console.warn(`  - regionId: ${requestingRegionId}`);
                    console.warn(`  - titleOfNobility: ${titleMin}-${titleMax}`);
                    console.warn(`  - age: ~${ownAge} years`);
                    return; // Keine Partner gefunden, aber kein Fehler
                }
            }

            const proposals = potentialPartners.map(partner => {
                const age = calcAge(partner.birthdate);
                return {
                    requesterCharacterId: requestingCharacterId,
                    proposedCharacterId: partner.id,
                    cost: calculateMarriageCost(partner.titleOfNobility, age),
                };
            });
            
            await MarriageProposal.bulkCreate(proposals);
            console.log(`[createPossiblePartners] Created ${proposals.length} marriage proposals`);
        } catch (error) {
            console.error('Error creating possible partners:', error);
            throw error;
        }
    }

    async _createNPCsForMarriage(regionId, gender, minTitle, maxTitle, targetAge, count = 5) {
        try {
            const sequelize = FalukantCharacter.sequelize;
            const createdNPCs = [];

            await sequelize.transaction(async (t) => {
                for (let i = 0; i < count; i++) {
                    // Zufälliger Titel im Bereich
                    const randomTitle = Math.floor(Math.random() * (maxTitle - minTitle + 1)) + minTitle;
                    
                    // Alter: ±2 Jahre um targetAge
                    const ageVariation = Math.floor(Math.random() * 5) - 2; // -2 bis +2
                    const randomAge = Math.max(12, targetAge + ageVariation); // Mindestens 12 Jahre
                    
                    // Zufälliger Vorname für das Geschlecht
                    const firstName = await FalukantPredefineFirstname.findAll({
                        where: { gender },
                        order: sequelize.fn('RANDOM'),
                        limit: 1,
                        transaction: t
                    });
                    
                    if (!firstName || firstName.length === 0) {
                        console.warn(`[_createNPCsForMarriage] No first names found for gender ${gender}`);
                        continue;
                    }
                    
                    // Zufälliger Nachname
                    const lastName = await FalukantPredefineLastname.findAll({
                        order: sequelize.fn('RANDOM'),
                        limit: 1,
                        transaction: t
                    });
                    
                    if (!lastName || lastName.length === 0) {
                        console.warn(`[_createNPCsForMarriage] No last names found`);
                        continue;
                    }
                    
                    // Geburtsdatum berechnen (Alter in Tagen)
                    const birthdate = new Date();
                    birthdate.setDate(birthdate.getDate() - randomAge);
                    
                    // Erstelle den NPC-Charakter
                    const npc = await FalukantCharacter.create({
                        userId: null, // Wichtig: null = NPC
                        regionId: regionId,
                        firstName: firstName[0].id,
                        lastName: lastName[0].id,
                        gender: gender,
                        birthdate: birthdate,
                        titleOfNobility: randomTitle,
                        health: 100,
                        moodId: 1
                    }, { transaction: t });
                    
                    createdNPCs.push(npc);
                }
            });
            
            console.log(`[_createNPCsForMarriage] Created ${createdNPCs.length} NPCs`);
            return createdNPCs;
        } catch (error) {
            console.error('[_createNPCsForMarriage] Error creating NPCs:', error);
            return []; // Bei Fehler leeres Array zurückgeben
        }
    }

    async acceptMarriageProposal(hashedUserId, proposedCharacterId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
        if (!user) {
            throw new Error('User not found');
        }
        const proposal = await MarriageProposal.findOne({
            where: {
                requesterCharacterId: character.id,
                proposedCharacterId: proposedCharacterId,
            },
        });
        if (!proposal) {
            throw new Error('Proposal not found');
        }
        if (user.money < proposal.cost) {
            throw new Error('Not enough money to accept the proposal');
        }
        const moneyResult = await updateFalukantUserMoney(user.id, -proposal.cost, 'Marriage cost', user.id);
        if (!moneyResult.success) {
            throw new Error('Failed to update money');
        }
        const marriedType = await RelationshipType.findOne({
            where: { tr: 'wooing' },
        });
        if (!marriedType) {
            throw new Error('Relationship type "married" not found');
        }
        await Relationship.create({
            character1Id: proposal.requesterCharacterId,
            character2Id: proposal.proposedCharacterId,
            relationshipTypeId: marriedType.id,
        });
        await MarriageProposal.destroy({
            where: { requesterCharacterId: character.id },
        })
            ;
        return { success: true, message: 'Marriage proposal accepted' };
    }

    async getGifts(hashedUserId) {
        const startTime = Date.now();
        const timings = {};
        
        try {
            // 1) User & Character optimiert laden (nur benötigte Felder)
            const step1Start = Date.now();
            const user = await FalukantUser.findOne({
                include: [
                    { model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } },
                    {
                        model: FalukantCharacter,
                        as: 'character',
                        attributes: ['id', 'titleOfNobility'],
                        required: true
                    }
                ]
            });
            if (!user) throw new Error('User not found');
            const myChar = user.character;
            if (!myChar) throw new Error('Character not found');
            timings.step1_user_character = Date.now() - step1Start;

            // 2) Beziehung finden (zwei separate Queries für bessere Index-Nutzung)
            const step2Start = Date.now();
            const [relAsChar1, relAsChar2] = await Promise.all([
                Relationship.findOne({
                    where: { character1Id: myChar.id },
                    attributes: ['character1Id', 'character2Id']
                }),
                Relationship.findOne({
                    where: { character2Id: myChar.id },
                    attributes: ['character1Id', 'character2Id']
                })
            ]);
            const rel = relAsChar1 || relAsChar2;
            timings.step2_relationship = Date.now() - step2Start;

            // 3) Related Character und Traits laden (nur wenn Relationship existiert)
            const step3Start = Date.now();
            let relatedTraitIds = [];
            let relatedMoodId = null;

            if (rel) {
                const relatedCharId = rel.character1Id === myChar.id ? rel.character2Id : rel.character1Id;
                
                // Parallel: Character (moodId) und Traits laden
                const [relatedChar, traitRows] = await Promise.all([
                    FalukantCharacter.findOne({
                        where: { id: relatedCharId },
                        attributes: ['id', 'moodId']
                    }),
                    FalukantCharacterTrait.findAll({
                        where: { characterId: relatedCharId },
                        attributes: ['traitId']
                    })
                ]);
                
                if (relatedChar) {
                    relatedMoodId = relatedChar.moodId;
                    relatedTraitIds = traitRows.map(t => t.traitId);
                }
            }
            timings.step3_load_character_and_traits = Date.now() - step3Start;

            // 4) Gifts laden – mit Mood/Trait-Filter nur wenn Beziehung existiert
            const step4Start = Date.now();
            const giftIncludes = [
                {
                    model: PromotionalGiftMood,
                    as: 'promotionalgiftmoods',
                    attributes: ['mood_id', 'suitability'],
                    required: false
                },
                {
                    model: PromotionalGiftCharacterTrait,
                    as: 'characterTraits',
                    attributes: ['trait_id', 'suitability'],
                    required: false
                }
            ];

            // Wenn Beziehung existiert, Filter anwenden
            if (rel && relatedMoodId) {
                giftIncludes[0].where = { mood_id: relatedMoodId };
            }
            if (rel && relatedTraitIds.length > 0) {
                giftIncludes[1].where = { trait_id: { [Op.in]: relatedTraitIds } };
            }
            timings.step4_prepare_gift_includes = Date.now() - step4Start;

            // 5) Parallel: Gifts und lowestTitleOfNobility laden
            const step5Start = Date.now();
            const [gifts, lowestTitleOfNobility] = await Promise.all([
                PromotionalGift.findAll({
                    include: giftIncludes
                }),
                TitleOfNobility.findOne({ order: [['id', 'ASC']] })
            ]);
            timings.step5_load_gifts_and_title = Date.now() - step5Start;

            // 6) Kosten berechnen (getGiftCost ist synchron)
            const step6Start = Date.now();
            const result = gifts.map(gift => ({
                id: gift.id,
                name: gift.name,
                cost: this.getGiftCost(
                    gift.value,
                    myChar.titleOfNobility,
                    lowestTitleOfNobility.id
                ),
                moodsAffects: gift.promotionalgiftmoods || [],   // nur Einträge mit relatedMoodId (wenn Filter angewendet)
                charactersAffects: gift.characterTraits || []   // nur Einträge mit relatedTraitIds (wenn Filter angewendet)
            }));
            timings.step6_calculate_costs = Date.now() - step6Start;

            const totalTime = Date.now() - startTime;
            console.log(`[getGifts] Performance: ${totalTime}ms total`, timings);
            
            return result;
        } catch (error) {
            console.error('[getGifts] Error:', error);
            throw error;
        }
    }

    async getChildren(hashedUserId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        console.log(user);
        const children = await ChildRelation.findAll({
            where: {
                [Op.or]: [
                    { fatherCharacterId: user.character.id },
                    { motherCharacterId: user.character.id }
                ]
            },
            include: [
                {
                    model: FalukantCharacter,
                    as: 'child',
                    attributes: ['id', 'birthdate'],
                    include: [
                        {
                            model: FalukantPredefineFirstname,
                            as: 'definedFirstName',
                            attributes: ['name']
                        },
                        {
                            model: Knowledge,
                            as: 'knowledges',
                            attributes: ['knowledge'],
                            include: [
                                {
                                    model: ProductType,
                                    as: 'productType',
                                    attributes: ['id', 'labelTr']
                                }
                            ]
                        }
                    ]
                },
            ]
        });
        return children.map(rel => ({
            id: rel.child.id,
            name: rel.child.definedFirstName.name,
            age: calcAge(rel.child.birthdate),
            knowledge: rel.child.knowledges.map(k => ({
                id: k.productType.id,
                tr: k.productType.labelTr,
                knowledge: k.knowledge
            }))
        }));
    }

    async sendGift(hashedUserId, giftId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const lowestTitle = await TitleOfNobility.findOne({ order: [['id', 'ASC']] });
        const currentMoodId = user.character.moodId;
        if (currentMoodId == null) {
            throw new Error('moodNotSet');
        }
        const relation = await Relationship.findOne({
            where: { character1Id: user.character.id },
            include: [{
                model: RelationshipType,
                as: 'relationshipType',
                where: { tr: 'wooing' }
            }]
        });
        if (!relation) {
            throw new Error('notRelated');
        }
        const lastGift = await PromotionalGiftLog.findOne({
            where: { senderCharacterId: user.character.id },
            order: [['createdAt', 'DESC']],
            limit: 1
        });
        if (lastGift && (lastGift.createdAt.getTime() + 3_600_000) > Date.now()) {
            throw new PreconditionError('tooOften');
        }
        const gift = await PromotionalGift.findOne({
            where: { id: giftId },
            include: [
                {
                    model: PromotionalGiftCharacterTrait,
                    as: 'characterTraits',
                    where: { trait_id: { [Op.in]: user.character.traits.map(t => t.id) } },
                    required: false
                },
                {
                    model: PromotionalGiftMood,
                    as: 'promotionalgiftmoods',
                    where: { mood_id: currentMoodId },
                    required: false
                }
            ]
        });
        if (!gift) {
            throw new Error('notFound');
        }
        const cost = this.getGiftCost(
            gift.value,
            user.character.nobleTitle.id,
            lowestTitle.id
        );
        if (user.money < cost) {
            throw new PreconditionError('insufficientFunds');
        }
        const traits = gift.characterTraits;
        if (!traits.length) {
            throw new Error('noTraits');
        }
        // Finde den höchsten Charakterwert (wie im Frontend)
        const highestCharacterValue = Math.max(...traits.map(ct => ct.suitability));
        const moodRecord = gift.promotionalgiftmoods[0];
        if (!moodRecord) {
            throw new Error('noMoodData');
        }
        const moodSuitability = moodRecord.suitability;
        // Basis-Berechnung wie im Frontend: (moodValue + highestCharacterValue) / 2
        const baseChangeValue = Math.round((moodSuitability + highestCharacterValue) / 2);
        // Zusätzlicher zufälliger Bonus zwischen 0 und 7 Punkten
        const randomBonus = Math.floor(Math.random() * 8); // 0–7 inklusive
        const changeValue = baseChangeValue + randomBonus;
        await updateFalukantUserMoney(user.id, -cost, 'Gift cost', user.id);
        await relation.update({ nextStepProgress: relation.nextStepProgress + changeValue });
        await PromotionalGiftLog.create({
            senderCharacterId: user.character.id,
            recipientCharacterId: relation.character2Id,
            giftId,
            changeValue
        });
        this.checkProposalProgress(relation);
        return { success: true, message: 'sent' };
    }

    async checkProposalProgress(relation) {
        const { nextStepProgress } = relation;
        if (nextStepProgress >= 100) {
            const engagedStatus = await RelationshipType.findOne({ where: { tr: 'engaged' } });
            await relation.update({ nextStepProgress: 0, relationshipTypeId: engagedStatus.id });
            const user = await User.findOne({
                include: [{
                    model: FalukantUser,
                    as: 'falukantData',
                    include: [{
                        model: FalukantCharacter,
                        as: 'character',
                        where: { id: relation.character1Id }
                    }]
                }]
            });
            await notifyUser(user.hashedId, 'familychanged');
        }
    }

    getGiftCost(value, titleOfNobility, lowestTitleOfNobility) {
        const titleLevel = titleOfNobility - lowestTitleOfNobility + 1;
        return Math.round(value * Math.pow(1 + titleLevel * 0.3, 1.3) * 100) / 100;
    }

    async getTitlesOfNobility() {
        return TitleOfNobility.findAll();
    }

    async getHouseTypes() {
        //        return House
    }

    async getMoodAffect() {
        return PromotionalGiftMood.findAll();
    }

    async getCharacterAffect() {
        return PromotionalGiftCharacterTrait.findAll();
    }

    async getUserHouse(hashedUserId) {
        try {
            const user = await User.findOne({
                where: { hashedId: hashedUserId },
                include: [{
                    model: FalukantUser,
                    as: 'falukantData',
                    include: [{
                        model: UserHouse,
                        as: 'userHouse',
                        include: [{
                            model: HouseType,
                            as: 'houseType',
                            attributes: ['position', 'cost']
                        }],
                        attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition']
                    }],
                }
                ]
            });
            return user.falukantData[0].userHouse ?? { position: 0, roofCondition: 100, wallCondition: 100, floorCondition: 100, windowCondition: 100 };
        } catch (error) {
            console.log(error);
            return {};
        }
    }

    async getBuyableHouses(hashedUserId) {
        try {
            const user = await this.getFalukantUserByHashedId(hashedUserId);
            const houses = await BuyableHouse.findAll({
                include: [{
                    model: HouseType,
                    as: 'houseType',
                    attributes: ['position', 'cost', 'labelTr'],
                    where: {
                        minimumNobleTitle: {
                            [Op.lte]: user.character.nobleTitle.id
                        }
                    }
                }],
                attributes: ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition', 'id'],
                order: [
                    [{ model: HouseType, as: 'houseType' }, 'position', 'DESC'],
                    ['wallCondition', 'DESC'],
                    ['roofCondition', 'DESC'],
                    ['floorCondition', 'DESC'],
                    ['windowCondition', 'DESC']
                ]
            });
            return houses;
        } catch (error) {
            console.error('Fehler beim Laden der kaufbaren Häuser:', error);
            throw error;
        }
    }

    async buyUserHouse(hashedUserId, houseId) {
        try {
            const falukantUser = await getFalukantUserOrFail(hashedUserId);
            const house = await BuyableHouse.findByPk(houseId, {
                include: [{
                    model: HouseType,
                    as: 'houseType',
                }],
            });
            if (!house) {
                throw new Error('Das Haus wurde nicht gefunden.');
            }
            const housePrice = this.housePrice(house);
            const oldHouse = await UserHouse.findOne({ where: { userId: falukantUser.id } });
            if (Number(falukantUser.money) < Number(housePrice)) {
                throw new Error('notenoughmoney.');
            }
            if (oldHouse) {
                await oldHouse.destroy();
            }
            await UserHouse.create({
                userId: falukantUser.id,
                houseTypeId: house.houseTypeId,
            });
            await house.destroy();
            await updateFalukantUserMoney(falukantUser.id, -housePrice, "housebuy", falukantUser.id);
            const user = await User.findByPk(falukantUser.userId);
            notifyUser(user.hashedId, 'falukantHouseUpdate', {});
            return {};
        } catch (error) {
            console.error('Fehler beim Kaufen des Hauses:', error);
            throw error;
        }
    }

    housePrice(house) {
        const houseQuality = (house.roofCondition + house.windowCondition + house.floorCondition + house.wallCondition) / 4;
        return (house.houseType.cost / 100 * houseQuality).toFixed(2);
    }

    async getPartyTypes(hashedUserId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        const engagedCount = await Relationship.count({
            include: [
                {
                    model: RelationshipType,
                    as: 'relationshipType',
                    where: { tr: 'engaged' },
                    required: true
                },
                {
                    model: FalukantCharacter,
                    as: 'character1',
                    where: { userId: falukantUser.id },
                    required: false
                },
                {
                    model: FalukantCharacter,
                    as: 'character2',
                    where: { userId: falukantUser.id },
                    required: false
                }
            ],
            where: {
                [Op.or]: [
                    { '$character1.user_id$': falukantUser.id },
                    { '$character2.user_id$': falukantUser.id }
                ]
            }
        });
        const orConditions = [{ forMarriage: false }];
        if (engagedCount > 0) {
            orConditions.push({ forMarriage: true });
        }
        const partyTypes = await PartyType.findAll({
            where: {
                [Op.or]: orConditions
            },
            order: [['cost', 'ASC']]
        });
        const musicTypes = await MusicType.findAll();
        const banquetteTypes = await BanquetteType.findAll();
        return { partyTypes, musicTypes, banquetteTypes };
    }

    async getReputationActions(hashedUserId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        const actionTypes = await ReputationActionType.findAll({ order: [['cost', 'ASC']] });

        // Tageslimit (global, aus Aktionen) – Anzeige im UI
        const dailyCap = FalukantService.REPUTATION_ACTION_DAILY_CAP;
        const [{ dailyUsed }] = await sequelize.query(
            `
            SELECT COALESCE(SUM(gain), 0)::int AS "dailyUsed"
            FROM falukant_log.reputation_action
            WHERE falukant_user_id = :uid
              AND action_timestamp >= date_trunc('day', now())
            `,
            { replacements: { uid: falukantUser.id }, type: sequelize.QueryTypes.SELECT }
        );
        const dailyRemaining = Math.max(0, Number(dailyCap) - Number(dailyUsed || 0));

        // Globaler Cooldown: max. 1 Aktion pro Stunde (oder konfigurierbar) unabhängig vom Typ
        const cooldownMinutes = FalukantService.REPUTATION_ACTION_COOLDOWN_MINUTES;
        const [{ lastTs }] = await sequelize.query(
            `
            SELECT MAX(action_timestamp) AS "lastTs"
            FROM falukant_log.reputation_action
            WHERE falukant_user_id = :uid
            `,
            { replacements: { uid: falukantUser.id }, type: sequelize.QueryTypes.SELECT }
        );
        let cooldownRemainingSec = 0;
        if (lastTs) {
            const last = new Date(lastTs).getTime();
            const nextAllowed = last + cooldownMinutes * 60 * 1000;
            cooldownRemainingSec = Math.max(0, Math.ceil((nextAllowed - Date.now()) / 1000));
        }

        if (!actionTypes.length) {
            return {
                dailyCap,
                dailyUsed: Number(dailyUsed || 0),
                dailyRemaining,
                cooldownMinutes,
                cooldownRemainingSec,
                actions: []
            };
        }

        // counts in einem Query – aber pro Typ in seinem "Decay-Fenster" (default 7 Tage)
        const now = Date.now();
        const actions = [];
        for (const t of actionTypes) {
            const windowDays = Number(t.decayWindowDays || 7);
            const since = new Date(now - windowDays * 24 * 3600 * 1000);
            const timesUsed = await ReputationActionLog.count({
                where: {
                    falukantUserId: falukantUser.id,
                    actionTypeId: t.id,
                    actionTimestamp: { [Op.gte]: since },
                }
            });
            const raw = Number(t.baseGain) * Math.pow(Number(t.decayFactor), Number(timesUsed));
            const gain = Math.max(Number(t.minGain || 0), Math.ceil(raw));
            actions.push({
                id: t.id,
                tr: t.tr,
                cost: t.cost,
                baseGain: t.baseGain,
                decayFactor: t.decayFactor,
                minGain: t.minGain,
                decayWindowDays: t.decayWindowDays,
                timesUsed,
                currentGain: gain,
            });
        }
        return {
            dailyCap,
            dailyUsed: Number(dailyUsed || 0),
            dailyRemaining,
            cooldownMinutes,
            cooldownRemainingSec,
            actions
        };
    }

    async executeReputationAction(hashedUserId, actionTypeId) {
        return await sequelize.transaction(async (t) => {
            const falukantUser = await getFalukantUserOrFail(hashedUserId, { transaction: t });
            const actionType = await ReputationActionType.findByPk(actionTypeId, { transaction: t });
            if (!actionType) throw new Error('Unbekannte Aktion');

            // Globaler Cooldown (unabhängig vom Aktionstyp): max. 1 pro Stunde
            const cooldownMinutes = FalukantService.REPUTATION_ACTION_COOLDOWN_MINUTES;
            const [{ lastTs }] = await sequelize.query(
                `
                SELECT MAX(action_timestamp) AS "lastTs"
                FROM falukant_log.reputation_action
                WHERE falukant_user_id = :uid
                `,
                { replacements: { uid: falukantUser.id }, type: sequelize.QueryTypes.SELECT, transaction: t }
            );
            if (lastTs) {
                const last = new Date(lastTs).getTime();
                const nextAllowed = last + cooldownMinutes * 60 * 1000;
                const remainingSec = Math.max(0, Math.ceil((nextAllowed - Date.now()) / 1000));
                if (remainingSec > 0) {
                    const remainingMin = Math.ceil(remainingSec / 60);
                    throw new Error(`Sozialstatus-Aktionen sind nur ${cooldownMinutes} Minutenweise möglich. Bitte warte noch ca. ${remainingMin} Minuten.`);
                }
            }

            const character = await FalukantCharacter.findOne({
                where: { userId: falukantUser.id },
                attributes: ['id', 'reputation'],
                transaction: t
            });
            if (!character) throw new Error('No character for user');

            // Abnutzung nur innerhalb des Fensters (default 7 Tage)
            const windowDays = Number(actionType.decayWindowDays || 7);
            const since = new Date(Date.now() - windowDays * 24 * 3600 * 1000);
            const timesUsedBefore = await ReputationActionLog.count({
                where: {
                    falukantUserId: falukantUser.id,
                    actionTypeId: actionType.id,
                    actionTimestamp: { [Op.gte]: since },
                },
                transaction: t
            });

            const raw = Number(actionType.baseGain) * Math.pow(Number(actionType.decayFactor), Number(timesUsedBefore));
            const plannedGain = Math.max(Number(actionType.minGain || 0), Math.ceil(raw));

            // Tageslimit aus Aktionen (global)
            const dailyCap = FalukantService.REPUTATION_ACTION_DAILY_CAP;
            const [{ dailyUsed }] = await sequelize.query(
                `
                SELECT COALESCE(SUM(gain), 0)::int AS "dailyUsed"
                FROM falukant_log.reputation_action
                WHERE falukant_user_id = :uid
                  AND action_timestamp >= date_trunc('day', now())
                `,
                { replacements: { uid: falukantUser.id }, type: sequelize.QueryTypes.SELECT, transaction: t }
            );
            const dailyRemaining = Math.max(0, Number(dailyCap) - Number(dailyUsed || 0));
            if (dailyRemaining <= 0) {
                throw new Error(`Tageslimit erreicht (max. ${dailyCap} Reputation pro Tag durch Aktionen)`);
            }
            const gain = Math.min(plannedGain, dailyRemaining);

            if (gain <= 0) {
                throw new Error('Diese Aktion bringt aktuell keine Reputation mehr');
            }

            const cost = Number(actionType.cost || 0);
            if (Number(falukantUser.money) < cost) {
                throw new Error('Nicht genügend Guthaben');
            }

            const moneyResult = await updateFalukantUserMoney(
                falukantUser.id,
                -cost,
                `reputationAction.${actionType.tr}`,
                falukantUser.id,
                t
            );
            if (!moneyResult.success) throw new Error('Geld konnte nicht abgezogen werden');

            await ReputationActionLog.create({
                falukantUserId: falukantUser.id,
                actionTypeId: actionType.id,
                cost,
                baseGain: actionType.baseGain,
                gain,
                timesUsedBefore: Number(timesUsedBefore),
            }, { transaction: t });

            await character.update(
                { reputation: Sequelize.literal(`LEAST(100, COALESCE(reputation,0) + ${gain})`) },
                { transaction: t }
            );

            const user = await User.findByPk(falukantUser.userId, { transaction: t });
            notifyUser(user.hashedId, 'falukantUpdateStatus', {});
            notifyUser(user.hashedId, 'falukantReputationUpdate', { gain, actionTr: actionType.tr });

            return { success: true, gain, plannedGain, dailyCap, dailyRemainingBefore: dailyRemaining, cost, actionTr: actionType.tr };
        });
    }

    async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio) {
        // Reputation-Logik: Party steigert Reputation um 1..5 (bestes Fest 5, kleinstes 1).
        // Wir leiten "Ausstattung" aus den Party-Kosten ab (linear zwischen min/max möglicher Konfiguration),
        // und deckeln Reputation bei 100.
        return await sequelize.transaction(async (t) => {
            const falukantUser = await getFalukantUserOrFail(hashedUserId);
            const since = new Date(Date.now() - 24 * 3600 * 1000);
            const already = await Party.findOne({
                where: {
                    falukantUserId: falukantUser.id,
                    partyTypeId,
                    createdAt: { [Op.gte]: since },
                },
                attributes: ['id'],
                transaction: t
            });
            if (already) {
                throw new Error('Diese Party wurde bereits innerhalb der letzten 24 Stunden bestellt');
            }

            const [ptype, music, banquette] = await Promise.all([
                PartyType.findByPk(partyTypeId, { transaction: t }),
                MusicType.findByPk(musicId, { transaction: t }),
                BanquetteType.findByPk(banquetteId, { transaction: t }),
            ]);
            if (!ptype || !music || !banquette) {
                throw new Error('Ungültige Party-, Musik- oder Bankett-Auswahl');
            }

            const nobilities = nobilityIds && nobilityIds.length
                ? await TitleOfNobility.findAll({ where: { id: { [Op.in]: nobilityIds } }, transaction: t })
                : [];

            // Prüfe, ob alle angegebenen IDs gefunden wurden
            if (nobilityIds && nobilityIds.length > 0 && nobilities.length !== nobilityIds.length) {
                throw new Error('Einige ausgewählte Adelstitel existieren nicht');
            }

            let cost = (ptype.cost || 0) + (music.cost || 0) + (banquette.cost || 0);
            cost += (50 / servantRatio - 1) * 1000;
            const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0);
            cost += nobilityCost;

            if (Number(falukantUser.money) < cost) {
                throw new Error('Nicht genügend Guthaben für diese Party');
            }

            // min/max mögliche Kosten für die Skalierung (nur für Reputation; Party-Preis bleibt wie berechnet)
            const [allPartyTypes, allMusicTypes, allBanquetteTypes, allNobilityTitles] = await Promise.all([
                PartyType.findAll({ attributes: ['cost'], transaction: t }),
                MusicType.findAll({ attributes: ['cost'], transaction: t }),
                BanquetteType.findAll({ attributes: ['cost'], transaction: t }),
                TitleOfNobility.findAll({ attributes: ['id'], transaction: t }),
            ]);
            const minParty = allPartyTypes.length ? Math.min(...allPartyTypes.map(x => Number(x.cost || 0))) : 0;
            const maxParty = allPartyTypes.length ? Math.max(...allPartyTypes.map(x => Number(x.cost || 0))) : 0;
            const minMusic = allMusicTypes.length ? Math.min(...allMusicTypes.map(x => Number(x.cost || 0))) : 0;
            const maxMusic = allMusicTypes.length ? Math.max(...allMusicTypes.map(x => Number(x.cost || 0))) : 0;
            const minBanq = allBanquetteTypes.length ? Math.min(...allBanquetteTypes.map(x => Number(x.cost || 0))) : 0;
            const maxBanq = allBanquetteTypes.length ? Math.max(...allBanquetteTypes.map(x => Number(x.cost || 0))) : 0;
            const servantsMin = 0; // servantRatio=50 => (50/50 - 1)*1000 = 0
            const servantsMax = (50 / 1 - 1) * 1000; // servantRatio=1 => 49k
            const nobilityMax = (allNobilityTitles || []).reduce((sum, n) => sum + ((Number(n.id) ^ 5) * 1000), 0);

            const minCostPossible = (minParty || 0) + (minMusic || 0) + (minBanq || 0) + servantsMin;
            const maxCostPossible = (maxParty || 0) + (maxMusic || 0) + (maxBanq || 0) + servantsMax + (nobilityMax || 0);
            const denom = Math.max(1, (maxCostPossible - minCostPossible));
            const score = Math.min(1, Math.max(0, (cost - minCostPossible) / denom));
            const reputationGain = 1 + Math.round(score * 4); // 1..5

            const character = await FalukantCharacter.findOne({
                where: { userId: falukantUser.id },
                attributes: ['id', 'reputation'],
                transaction: t
            });
            if (!character) throw new Error('No character for user');

            // Geld abziehen
            const moneyResult = await updateFalukantUserMoney(
                falukantUser.id,
                -cost,
                'partyOrder',
                falukantUser.id,
                t
            );
            if (!moneyResult.success) {
                throw new Error('Geld konnte nicht abgezogen werden');
            }

            const party = await Party.create({
                partyTypeId,
                falukantUserId: falukantUser.id,
                musicTypeId: musicId,
                banquetteTypeId: banquetteId,
                servantRatio,
                cost: cost
            }, { transaction: t });

            if (nobilities.length > 0) {
                await party.addInvitedNobilities(nobilities, { transaction: t });
            }

            // Reputation erhöhen (0..100)
            await character.update(
                { reputation: Sequelize.literal(`LEAST(100, COALESCE(reputation,0) + ${reputationGain})`) },
                { transaction: t }
            );

            const user = await User.findByPk(falukantUser.userId, { transaction: t });
            notifyUser(user.hashedId, 'falukantPartyUpdate', {
                partyId: party.id,
                cost,
                reputationGain,
            });
            // Statusbar kann sich damit ebenfalls aktualisieren
            notifyUser(user.hashedId, 'falukantUpdateStatus', {});

            return { success: true, reputationGain };
        });
    }

    async getParties(hashedUserId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        const parties = await Party.findAll({
            where: { falukantUserId: falukantUser.id },
            include: [
                {
                    model: PartyType,
                    as: 'partyType',
                    attributes: ['tr'],
                },
                {
                    model: MusicType,
                    as: 'musicType',
                    attributes: ['tr'],
                },
                {
                    model: BanquetteType,
                    as: 'banquetteType',
                    attributes: ['tr'],
                },
            ],
            order: [['createdAt', 'DESC']],
            attributes: ['id', 'createdAt', 'servantRatio', 'cost'],
        });
        return parties;
    }

    async getNotBaptisedChildren(hashedUserId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        const children = await ChildRelation.findAll({
            include: [
                {
                    model: FalukantCharacter,
                    as: 'father',
                    where: {
                        userId: falukantUser.id,
                    },
                    required: false,
                },
                {
                    model: FalukantCharacter,
                    as: 'mother',
                    where: {
                        userId: falukantUser.id,
                    },
                    required: false,
                },
                {
                    model: FalukantCharacter,
                    as: 'child',
                    required: true,
                    include: [
                        {
                            model: FalukantPredefineFirstname,
                            as: 'definedFirstName',
                            required: true,
                        },
                    ]
                },
            ],
            where: {
                nameSet: false,
            },
            order: [['createdAt', 'DESC']],
        });
        return children.map(child => {
            return {
                id: child.child.id,
                gender: child.child.gender,
                age: calcAge(child.child.birthdate),
                proposedFirstName: child.child.definedFirstName.name,
            };
        });
    }

    async baptise(hashedUserId, childId, firstName) {
        try {
            const falukantUser = await getFalukantUserOrFail(hashedUserId);
            const parentCharacter = await FalukantCharacter.findOne({
                where: {
                    userId: falukantUser.id,
                },
            });
            if (!parentCharacter) {
                throw new Error('Parent character not found');
            }
            const child = await FalukantCharacter.findOne({
                where: {
                    id: childId,
                },
            });
            if (!child) {
                throw new Error('Child not found');
            }
            const childRelation = await ChildRelation.findOne({
                where: {
                    [Op.or]: [
                        {
                            fatherCharacterId: parentCharacter.id,
                            childCharacterId: child.id,
                        },
                        {
                            motherCharacterId: parentCharacter.id,
                            childCharacterId: child.id,
                        }
                    ]
                }
            });
            if (!childRelation) {
                throw new Error('Child relation not found');
            }
            await childRelation.update({
                nameSet: true,
            });
            let firstNameObject = await FalukantPredefineFirstname.findOne({
                where: {
                    name: firstName,
                    gender: child.gender,
                },
            });
            if (!firstNameObject) {
                firstNameObject = await FalukantPredefineFirstname.create({
                    name: firstName,
                    gender: child.gender,
                });
            }
            await child.update({
                firstName: firstNameObject.id,
            });
            updateFalukantUserMoney(falukantUser.id, -50, 'Baptism', falukantUser.id);
            // Trigger status bar refresh for the user after baptism
            notifyUser(hashedUserId, 'falukantUpdateStatus', {});
            return { success: true };
        } catch (error) {
            throw new Error(error.message);
        }
    }

    async getEducation(hashedUserId) {
        try {
            const falukantUser = await getFalukantUserOrFail(hashedUserId);
            const education = await Learning.findAll({
                where: {
                    createdAt: { [Op.gt]: new Date().getTime() - 1000 * 60 * 60 * 24 },
                },
                include: [
                    {
                        model: LearnRecipient,
                        as: 'recipient',
                        attributes: ['tr']
                    },
                    {
                        model: ProductType,
                        as: 'productType',
                        attributes: ['labelTr']
                    },
                    {
                        model: FalukantUser,
                        as: 'learner',
                        where: {
                            id: falukantUser.id
                        },
                        attributes: []
                    },
                    {
                        model: FalukantCharacter,
                        as: 'learningCharacter',
                        attributes: ['id']
                    }
                ],
                attributes: ['createdAt'],
            });
            return education;
        } catch (error) {
            console.log(error);
            throw new Error(error.message);
        }
    }

    computeCost(percent, mode) {
        const cfg = FalukantService.COST_CONFIG[mode];
        // clamp percent auf [0, KNOWLEDGE_MAX]
        const p = Math.min(Math.max(percent, 0), FalukantService.KNOWLEDGE_MAX)
            / FalukantService.KNOWLEDGE_MAX;
        return Math.round(cfg.min + (cfg.max - cfg.min) * p);
    }

    async sendToSchool(hashedUserId, item, student, studentId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);

        // 1) prüfen, ob schon in Arbeit
        const education = await this.getEducation(hashedUserId);
        const already = education.some(e =>
            e.recipient.tr === student &&
            (studentId == null || e.learningCharacter?.id === studentId)
        );
        if (already) throw new Error('Already learning this character');

        // 2) Empfänger holen
        const rec = await LearnRecipient.findOne({ where: { tr: student } });
        if (!rec) throw new Error('Character not found');

        // 3) Wissens-Prozentsatz ermitteln
        let percent;
        if (item === 'all') {
            const all = await this.getKnowledgeForAll(hashedUserId, student, studentId);
            const sum = all.reduce((s, k) => s + k.knowledge, 0);
            percent = sum / all.length;
        } else {
            const single = await this.getKnowledgeSingle(hashedUserId, student, studentId, item);
            percent = single.knowledge;
        }

        // 4) Kosten berechnen
        const mode = item === 'all' ? 'all' : 'one';
        const cost = this.computeCost(percent, mode);

        // 5) Kontostand prüfen
        if (parseFloat(falukantUser.money) < cost) {
            throw new Error('Not enough money');
        }

        // 6) Learning anlegen
        await Learning.create({
            learningRecipientId: rec.id,
            associatedLearningCharacterId: studentId,
            associatedFalukantUserId: falukantUser.id,
            productId: item === 'all' ? null : item,
            learnAllProducts: (item === 'all')
        });

        // 7) Geld abziehen
        const upd = await updateFalukantUserMoney(
            falukantUser.id,
            -cost,
            item === 'all' ? 'learnAll' : `learnItem:${item}`,
            falukantUser.id
        );
        if (!upd.success) throw new Error(upd.message);

        return true;
    }

    async getBankOverview(hashedUserId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        if (!falukantUser) throw new Error('User not found');

        // 1) offene Schulden
        const totalDebt = await Credit.sum('remaining_amount', {
            where: { falukant_user_id: falukantUser.id }
        }) || 0;

        // 2) Häuser ermitteln
        const userHouses = await UserHouse.findAll({
            where: { userId: falukantUser.id },
            include: [{ model: HouseType, as: 'houseType', attributes: ['cost'] }]
        });

        // 3) Hauswert berechnen: buyCost * 0.8
        let houseValue = 0;
        for (const uh of userHouses) {
            const { roofCondition, wallCondition, floorCondition, windowCondition } = uh;
            const qualityAvg = (roofCondition + wallCondition + floorCondition + windowCondition) / 4;
            const buyWorth = (uh.houseType.cost / 100) * qualityAvg;
            houseValue += buyWorth * 0.8;
        }

        // 4) Filialwert (1000 pro Branch)
        const branchCount = await Branch.count({ where: { falukantUserId: falukantUser.id } });
        const branchValue = branchCount * 1000;

        // 5) Maximaler Kredit und verfügbare Linie
        const maxCredit = Math.floor(houseValue + branchValue);
        const availableCredit = maxCredit - totalDebt;

        // 6) aktive Kredite laden
        const activeCredits = await Credit.findAll({
            where: { falukantUserId: falukantUser.id },
            attributes: ['id', 'amount', 'remainingAmount', 'interestRate']
        });

        return {
            money: falukantUser.money,
            totalDebt,
            maxCredit,
            availableCredit,
            activeCredits,
            fee: 7
        };
    }

    async getBankCredits(hashedUserId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        if (!falukantUser) throw new Error('User not found');
        const credits = await Credit.findAll({
            where: { falukantUserId: falukantUser.id },
        });
        return credits;
    }

    async takeBankCredits(hashedUserId, height) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        if (!falukantUser) throw new Error('User not found');
        const financialData = await this.getBankOverview(hashedUserId);
        if (financialData.availableCredit < height) {
            throw new Error('Not enough credit');
        }
        const newCredit = await Credit.create({
            falukantUserId: falukantUser.id,
            amount: height,
            remainingAmount: height,
            interestRate: financialData.fee,
        });
        updateFalukantUserMoney(falukantUser.id, height, 'credit taken', falukantUser.id);
        return { height: newCredit.amount, fee: newCredit.interestRate };
    }

    async getNobility(hashedUserId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        const nobility = await TitleOfNobility.findOne({
            include: [
                {
                    model: FalukantCharacter,
                    as: 'charactersWithNobleTitle',
                    attributes: ['gender'],
                    where: {
                        userId: falukantUser.id
                    }
                },
                {
                    model: TitleRequirement,
                    as: 'requirements',
                    attributes: ['requirementType', 'requirementValue']
                }
            ],
            attributes: ['labelTr', 'level']
        });
        let nextAdvanceAt = null;
        if (falukantUser.lastNobilityAdvanceAt) {
            const last = new Date(falukantUser.lastNobilityAdvanceAt);
            const next = new Date(last.getTime());
            next.setDate(next.getDate() + 7);
            nextAdvanceAt = next.toISOString();
        }
        const currentTitleLevel = nobility.level;
        const nextTitle = await TitleOfNobility.findOne({
            where: {
                level: currentTitleLevel + 1
            },
            include: [
                {
                    model: TitleRequirement,
                    as: 'requirements',
                }
            ],
            attributes: ['labelTr']
        });
        return {
            current: nobility,
            next: nextTitle,
            nextAdvanceAt
        };
    }

    async advanceNobility(hashedUserId) {
        const nobility = await this.getNobility(hashedUserId);
        if (!nobility || !nobility.next) {
            throw new Error('User does not have a nobility');
        }
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const now = new Date();
        if (user.lastNobilityAdvanceAt) {
            const oneWeekAgo = new Date(now.getTime());
            oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
            if (user.lastNobilityAdvanceAt > oneWeekAgo) {
                throw new Error('too soon');
            }
        }
        const nextTitle = nobility.next.toJSON();
        let fulfilled = true;
        let cost = 0;
        for (const requirement of nextTitle.requirements) {
            switch (requirement.requirementType) {
                case 'money':
                    fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement);
                    break;
                case 'cost':
                    fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement);
                    cost = requirement.requirementValue;
                    break;
                case 'branches':
                    fulfilled = fulfilled && await this.checkBranchesRequirement(hashedUserId, requirement);
                    break;
                default:
                    fulfilled = false;
            };
        }
        if (!fulfilled) {
            throw new Error('Requirements not fulfilled');
        }
        const newTitle = await TitleOfNobility.findOne({
            where: { level: nobility.current.level + 1 }
        });
        const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
        await character.update({ titleOfNobility: newTitle.id });
        await user.update({ lastNobilityAdvanceAt: now });
        if (cost > 0) {
            updateFalukantUserMoney(user.id, -cost, 'new nobility title', user.id);
        }
        return {};
    }

    async checkMoneyRequirement(user, requirement) {
        return user.money >= requirement.requirementValue;
    }

    async checkBranchesRequirement(hashedUserId, requirement) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        if (!user) {
            throw new Error('User not found');
        }
        const branchCount = await Branch.count({
            where: { falukantUserId: user.id }
        });
        return branchCount >= requirement.requirementValue;
    }

    async getHealth(hashedUserId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const healthActivities = FalukantService.HEALTH_ACTIVITIES.map((activity) => { return { tr: activity.tr, cost: activity.cost } });
        const healthHistory = await HealthActivity.findAll({
            where: { characterId: user.character.id },
            order: [['createdAt', 'DESC']],
        });
        return {
            age: calcAge(user.character.birthdate),
            health: user.character.health,
            healthActivities: healthActivities,
            history: healthHistory.map((activity) => { return { tr: activity.activityTr, cost: activity.cost, createdAt: activity.createdAt, success: activity.successPercentage } }),
        };
    }

    async healthActivity(hashedUserId, activity) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const lastHealthActivity = await HealthActivity.findOne({
            where: {
                characterId: user.character.id,
                activityTr: activity,
                createdAt: {
                    [Op.gte]: new Date(new Date().setDate(new Date().getDate() - 1))
                }
            },
            order: [['createdAt', 'DESC']],
            limit: 1
        });
        if (lastHealthActivity) {
            throw new PreconditionError('tr:falukant.healthview.errors.tooClose');
        }
        const activityObject = FalukantService.HEALTH_ACTIVITIES.find((a) => a.tr === activity);
        if (!activityObject) {
            throw new Error('invalid');
        }
        if (user.money - activityObject.cost < 0) {
            throw new Error('no money');
        }
        // Hinweis: health ist ein Status (0..100) und darf nicht mit Geldkosten vermischt werden.
        const healthChange = await this[activityObject.method](user);
        await HealthActivity.create({
            characterId: user.character.id,
            activityTr: activity,
            successPercentage: healthChange,
            cost: activityObject.cost
        });
        const moneyResult = await updateFalukantUserMoney(user.id, -activityObject.cost, 'health.' + activity, user.id);
        if (!moneyResult.success) throw new Error('Failed to update money');
        
        // Status-Update Notification senden
        notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
        
        // Give client enough info to update UI without ambiguity
        const updatedChar = await FalukantCharacter.findOne({
            where: { id: user.character.id },
            attributes: ['health']
        });
        return { success: true, delta: healthChange, health: updatedChar?.health ?? null };
    }

    async healthChange(user, delta) {
        const char = await FalukantCharacter.findOne({
            where: {
                id: user.character.id
            }
        });
        await char.update({
            health: Math.min(FalukantService.HEALTH_MAX || 100, Math.max(0, char.health + delta))
        });
        return delta;
    }

    async healthBarber(user) {
        const raw = Math.floor(Math.random() * 11) - 5;
        return this.healthChange(user, raw);
    }

    async healthDoctor(user) {
        const raw = Math.floor(Math.random() * 8) - 2;
        return this.healthChange(user, raw);
    }

    async healthWitch(user) {
        const raw = Math.floor(Math.random() * 7) - 1;
        return this.healthChange(user, raw);
    }

    async healthPill(user) {
        const raw = Math.floor(Math.random() * 8);
        return this.healthChange(user, raw);
    }

    async healthDrunkOfLife(user) {
        // Erfolgschance: 90%
        const success = Math.random() < 0.9;
        
        let delta;
        if (success) {
            // Bei Erfolg: Gesundheit um 5-35% verbessern
            delta = Math.floor(Math.random() * 31) + 5; // 5-35
        } else {
            // Bei Misserfolg: Gesundheit um 1-10% verschlechtern
            delta = -(Math.floor(Math.random() * 10) + 1); // -1 bis -10
        }
        
        return this.healthChange(user, delta);
    }

    async getPoliticsOverview(hashedUserId) {
        // Liefert alle aktuell besetzten Ämter im eigenen Gebiet inklusive
        // Inhaber und berechnetem Enddatum der Amtszeit.
        const user = await getFalukantUserOrFail(hashedUserId);

        // Charakter des Users bestimmen (Region ist dort hinterlegt)
        const character = await FalukantCharacter.findOne({
            where: { userId: user.id },
            attributes: ['id', 'regionId']
        });
        if (!character) {
            return [];
        }

        // Alle relevanten Regionen (Region + Eltern) laden
        const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);

        // Aktuell besetzte Ämter in diesen Regionen laden
        const offices = await PoliticalOffice.findAll({
            where: {
                regionId: {
                    [Op.in]: relevantRegionIds
                }
            },
            include: [
                {
                    model: PoliticalOfficeType,
                    as: 'type',
                    attributes: ['name', 'termLength']
                },
                {
                    model: RegionData,
                    as: 'region',
                    attributes: ['name'],
                    include: [
                        {
                            model: RegionType,
                            as: 'regionType',
                            attributes: ['labelTr']
                        }
                    ]
                },
                {
                    model: FalukantCharacter,
                    as: 'holder',
                    attributes: ['id', 'gender'],
                    include: [
                        {
                            model: FalukantPredefineFirstname,
                            as: 'definedFirstName',
                            attributes: ['name']
                        },
                        {
                            model: FalukantPredefineLastname,
                            as: 'definedLastName',
                            attributes: ['name']
                        },
                        {
                            model: TitleOfNobility,
                            as: 'nobleTitle',
                            attributes: ['labelTr']
                        }
                    ]
                }
            ],
            order: [
                [{ model: PoliticalOfficeType, as: 'type' }, 'name', 'ASC'],
                [{ model: RegionData, as: 'region' }, 'name', 'ASC']
            ]
        });

        return offices.map(office => {
            const o = office.get({ plain: true });

            // Enddatum der Amtszeit berechnen: Start = createdAt, Dauer = termLength Jahre
            let termEnds = null;
            if (o.createdAt && o.type && typeof o.type.termLength === 'number') {
                const start = new Date(o.createdAt);
                if (!Number.isNaN(start.getTime())) {
                    const end = new Date(start);
                    end.setFullYear(end.getFullYear() + o.type.termLength);
                    termEnds = end;
                }
            }

            return {
                id: o.id,
                officeType: {
                    name: o.type?.name
                },
                region: {
                    name: o.region?.name,
                    regionType: o.region?.regionType
                        ? { labelTr: o.region.regionType.labelTr }
                        : undefined
                },
                character: o.holder
                    ? {
                        definedFirstName: o.holder.definedFirstName,
                        definedLastName: o.holder.definedLastName,
                        nobleTitle: o.holder.nobleTitle,
                        gender: o.holder.gender
                    }
                    : null,
                termEnds
            };
        });
    }

    async getOpenPolitics(hashedUserId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        if (!user || user.character.nobleTitle.labelTr === 'noncivil') {
            return [];
        }

    }

    async getElections(hashedUserId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        if (!user || user.character.nobleTitle.labelTr === 'noncivil') {
            return [];
        }
        const rows = await sequelize.query(
            FalukantService.RECURSIVE_REGION_SEARCH,
            {
                replacements: { user_id: user.id },
                type: sequelize.QueryTypes.SELECT
            }
        );
        const regionIds = rows.map(r => r.id);

        // 3) Zeitbereich "heute"
        const todayStart = new Date();
        todayStart.setHours(0, 0, 0, 0);
        const todayEnd = new Date();
        todayEnd.setHours(23, 59, 59, 999);

        // 4) Wahlen laden (inkl. Kandidaten, Stimmen und Verknüpfungen)
        const rawElections = await Election.findAll({
            where: {
                regionId: { [Op.in]: regionIds },
                date: { [Op.between]: [todayStart, todayEnd] }
            },
            include: [
                {
                    model: RegionData,
                    as: 'region',
                    attributes: ['name'],
                    include: [{
                        model: RegionType,
                        as: 'regionType',
                        attributes: ['labelTr']
                    }]
                },
                {
                    model: PoliticalOfficeType,
                    as: 'officeType',
                    attributes: ['name']
                },
                {
                    model: Candidate,
                    as: 'candidates',
                    attributes: ['id'],
                    include: [{
                        model: FalukantCharacter,
                        as: 'character',
                        attributes: ['birthdate', 'gender'],
                        include: [
                            { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                            { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
                            { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
                        ]
                    }]
                },
                {
                    model: Vote,
                    as: 'votes',
                    attributes: ['candidateId'],
                    where: {
                        falukantUserId: user.id
                    },
                    required: false
                }
            ]
        });

        return rawElections.map(election => {
            const e = election.get({ plain: true });

            const voted = Array.isArray(e.votes) && e.votes.length > 0;
            const reducedCandidates = (e.candidates || []).map(cand => {
                const ch = cand.character || {};
                const firstname = ch.definedFirstName?.name || '';
                const lastname = ch.definedLastName?.name || '';
                return {
                    id: cand.id,
                    title: ch.nobleTitle?.labelTr || null,
                    name: `${firstname} ${lastname}`.trim(),
                    age: calcAge(ch.birthdate),
                    gender: ch.gender
                };
            });

            return {
                id: e.id,
                officeType: { name: e.officeType.name },
                region: {
                    name: e.region.name,
                    regionType: { labelTr: e.region.regionType.labelTr }
                },
                date: e.date,
                postsToFill: e.postsToFill,
                candidates: reducedCandidates,
                voted: voted,
                votedFor: voted ? e.votes.map(vote => { return vote.candidateId }) : null,
            };
        });
    }

    async vote(hashedUserId, votes) {
        const elections = await this.getElections(hashedUserId);
        if (!Array.isArray(elections) || elections.length === 0) {
            throw new Error('No elections found');
        }
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        if (!user) {
            throw new Error('User not found');
        }
        const validElections = votes.filter(voteEntry => {
            const e = elections.find(el => el.id === voteEntry.electionId);
            return e && !e.voted;
        });

        if (validElections.length === 0) {
            throw new Error('No valid elections to vote for (either non‐existent or already voted)');
        }
        validElections.forEach(voteEntry => {
            const e = elections.find(el => el.id === voteEntry.electionId);
            const allowedIds = e.candidates.map(c => c.id);
            voteEntry.candidateIds.forEach(cid => {
                if (!allowedIds.includes(cid)) {
                    throw new Error(`Candidate ID ${cid} is not valid for election ${e.id}`);
                }
            });
            if (voteEntry.candidateIds.length > e.postsToFill) {
                throw new Error(`Too many candidates selected for election ${e.id}. Allowed: ${e.postsToFill}`);
            }
        });
        return await sequelize.transaction(async (tx) => {
            const toCreate = [];
            validElections.forEach(voteEntry => {
                voteEntry.candidateIds.forEach(candidateId => {
                    toCreate.push({
                        electionId: voteEntry.electionId,
                        candidateId,
                        falukantUserId: user.id
                    });
                });
            });
            await Vote.bulkCreate(toCreate, {
                transaction: tx,
                ignoreDuplicates: true,
                returning: false
            });
            return { success: true };
        });
    }

    async getOpenPolitics(hashedUserId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const characterId = user.character.id;
        const rows = await sequelize.query(
            FalukantService.RECURSIVE_REGION_SEARCH,
            {
                replacements: { user_id: user.id },
                type: sequelize.QueryTypes.SELECT
            }
        );
        const regionIds = rows.map(r => r.id);
        const histories = await PoliticalOfficeHistory.findAll({
            where: { characterId },
            attributes: ['officeTypeId', 'startDate', 'endDate']
        });
        const heldOfficeTypeIds = histories.map(h => h.officeTypeId);
        const allTypes = await PoliticalOfficeType.findAll({ attributes: ['id', 'name'] });
        const nameToId = Object.fromEntries(allTypes.map(t => [t.name, t.id]));
        const openPositions = await Election.findAll({
            where: {
                regionId: { [Op.in]: regionIds },
                date: { [Op.lt]: new Date() }
            },
            include: [
                {
                    model: RegionData,
                    as: 'region',
                    attributes: ['name'],
                    include: [
                        { model: RegionType, as: 'regionType', attributes: ['labelTr'] }
                    ]
                },
                { 
                    model: Candidate, 
                    as: 'candidates',
                    required: false
                },
                {
                    model: PoliticalOfficeType, as: 'officeType',
                    include: [{ model: PoliticalOfficePrerequisite, as: 'prerequisites' }]
                }
            ]
        });
        const result = openPositions
            .filter(election => {
                const prereqs = election.officeType.prerequisites || [];
                return prereqs.some(pr => {
                    const jobs = pr.prerequisite.jobs;
                    if (!Array.isArray(jobs) || jobs.length === 0) return true;
                    return jobs.some(jobName => {
                        const reqId = nameToId[jobName];
                        return heldOfficeTypeIds.includes(reqId);
                    });
                });
            })
            .map(election => {
                const e = election.get({ plain: true });
                const jobs = e.officeType.prerequisites[0]?.prerequisite.jobs || [];
                const matchingHistory = histories
                    .filter(h => jobs.includes(allTypes.find(t => t.id === h.officeTypeId)?.name))
                    .map(h => ({
                        officeTypeId: h.officeTypeId,
                        startDate: h.startDate,
                        endDate: h.endDate
                    }));

                const alreadyApplied = (e.candidates || []).some(
                    c => c.characterId === characterId
                );

                return {
                    ...e,
                    history: matchingHistory,
                    alreadyApplied
                };
            });
        return result;
    }

    async applyForElections(hashedUserId, electionIds) {
        // 1) Hole FalukantUser + Character
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        if (!user) {
            throw new Error('User nicht gefunden');
        }
        const character = user.character;
        if (!character) {
            throw new Error('Kein Charakter zum User gefunden');
        }

        // 2) Noncivil‐Titel aussperren
        if (character.nobleTitle.labelTr === 'noncivil') {
            return { applied: [], skipped: electionIds };
        }

        // 3) Ermittle die offenen Wahlen, auf die er zugreifen darf
        //    Verwende getOpenPolitics statt getElections, da getOpenPolitics die gleichen Wahlen
        //    zurückgibt, die im Frontend angezeigt werden
        const openPolitics = await this.getOpenPolitics(hashedUserId);
        const allowedIds = new Set(openPolitics.map(e => e.id));

        // 4) Filter alle electionIds auf gültige/erlaubte
        const toTry = electionIds.filter(id => allowedIds.has(id));
        if (toTry.length === 0) {
            return { applied: [], skipped: electionIds };
        }

        // 5) Prüfe, auf welche dieser Wahlen der Character bereits als Candidate eingetragen ist
        const existing = await Candidate.findAll({
            where: {
                electionId: { [Op.in]: toTry },
                characterId: character.id
            },
            attributes: ['electionId']
        });
        const alreadyIds = new Set(existing.map(c => c.electionId));

        // 6) Erstelle Liste der Wahlen, für die er sich noch nicht beworben hat
        const newApplications = toTry.filter(id => !alreadyIds.has(id));
        const skipped = electionIds.filter(id => !newApplications.includes(id));

        console.log(newApplications, skipped);

        // 7) Bulk-Insert aller neuen Bewerbungen
        if (newApplications.length > 0) {
            const toInsert = newApplications.map(eid => ({
                electionId: eid,
                characterId: character.id
            }));
            await Candidate.bulkCreate(toInsert);
        }

        return {
            applied: newApplications,
            skipped: skipped
        };
    }

    async getRegions(hashedUserId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const regions = await RegionData.findAll({
            attributes: ['id', 'name', 'map'],
            include: [
                {
                    model: RegionType,
                    as: 'regionType',
                    where: {
                        labelTr: 'city'
                    },
                    attributes: ['labelTr']
                },
                {
                    model: Branch,
                    as: 'branches',
                    where: {
                        falukantUserId: user.id
                    },
                    include: [
                        {
                            model: BranchType,
                            as: 'branchType',
                            attributes: ['labelTr'],
                        },
                    ],
                    attributes: ['branchTypeId'],
                    required: false,
                }
            ]
        });
        return regions;
    }

    async getProductPricesInRegionBatch(hashedUserId, productIds, regionId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
        if (!character) {
            throw new Error(`No FalukantCharacter found for user with id ${user.id}`);
        }

        if (!Array.isArray(productIds) || productIds.length === 0) {
            return {};
        }

        // Hole alle Produkte auf einmal
        const products = await ProductType.findAll({
            where: { id: { [Op.in]: productIds } }
        });

        // Hole alle Knowledge-Werte auf einmal
        const knowledges = await Knowledge.findAll({
            where: {
                characterId: character.id,
                productId: { [Op.in]: productIds }
            }
        });
        const knowledgeMap = new Map(knowledges.map(k => [k.productId, k.knowledge]));

        // Hole alle TownProductWorth-Werte auf einmal
        const townWorths = await TownProductWorth.findAll({
            where: {
                productId: { [Op.in]: productIds },
                regionId: regionId
            }
        });
        const worthMap = new Map(townWorths.map(tw => [tw.productId, tw.worthPercent]));

        // Berechne Preise für alle Produkte
        const prices = {};
        for (const product of products) {
            const knowledgeFactor = knowledgeMap.get(product.id) || 0;
            const worthPercent = worthMap.get(product.id) || 50;
            
            const basePrice = product.sellCost * (worthPercent / 100);
            const min = basePrice * 0.6;
            const max = basePrice;
            const price = min + (max - min) * (knowledgeFactor / 100);
            
            prices[product.id] = Math.round(price * 100) / 100; // Auf 2 Dezimalstellen runden
        }

        return prices;
    }

    async getProductPriceInRegion(hashedUserId, productId, regionId) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
        if (!character) {
            throw new Error(`No FalukantCharacter found for user with id ${user.id}`);
        }

        // Produkt abrufen
        const product = await ProductType.findOne({ where: { id: productId } });
        if (!product) {
            throw new Error(`Product not found with id ${productId}`);
        }

        // Knowledge für dieses Produkt abrufen
        const knowledge = await Knowledge.findOne({
            where: { characterId: character.id, productId: productId }
        });
        const knowledgeFactor = knowledge?.knowledge || 0;

        // Verwende die bereits existierende calcRegionalSellPrice Funktion
        const price = await calcRegionalSellPrice(product, knowledgeFactor, regionId);

        return { price };
    }

    async getProductPricesInCities(hashedUserId, productId, currentPrice, currentRegionId = null) {
        const user = await this.getFalukantUserByHashedId(hashedUserId);
        const character = await FalukantCharacter.findOne({ where: { userId: user.id } });
        if (!character) {
            throw new Error(`No FalukantCharacter found for user with id ${user.id}`);
        }

        // Produkt abrufen
        const product = await ProductType.findOne({ where: { id: productId } });
        if (!product) {
            throw new Error(`Product not found with id ${productId}`);
        }

        // Knowledge für dieses Produkt abrufen
        const knowledge = await Knowledge.findOne({
            where: { characterId: character.id, productId: productId }
        });
        const knowledgeFactor = knowledge?.knowledge || 0;

        // Alle Städte abrufen
        const cities = await RegionData.findAll({
            attributes: ['id', 'name'],
            include: [
                {
                    model: RegionType,
                    as: 'regionType',
                    where: { labelTr: 'city' },
                    attributes: ['labelTr']
                },
                {
                    model: Branch,
                    as: 'branches',
                    where: { falukantUserId: user.id },
                    include: [
                        {
                            model: BranchType,
                            as: 'branchType',
                            attributes: ['labelTr']
                        }
                    ],
                    attributes: ['branchTypeId'],
                    required: false
                }
            ]
        });

        // TownProductWorth für alle Städte und dieses Produkt einmalig abrufen
        // (vermeidet N+1 Query Problem)
        const townWorths = await TownProductWorth.findAll({
            where: { productId: productId },
            attributes: ['regionId', 'worthPercent']
        });
        const worthMap = new Map(townWorths.map(tw => [tw.regionId, tw.worthPercent]));

        // Berechne den regionalen Preis für die aktuelle Region (falls angegeben)
        // WICHTIG: Ignoriere den übergebenen currentPrice, da er möglicherweise nicht
        // den regionalen Faktor berücksichtigt. Berechne stattdessen immer den korrekten
        // regionalen Preis basierend auf currentRegionId.
        let currentRegionalPrice = currentPrice; // Fallback auf übergebenen Preis
        if (currentRegionId) {
            const currentWorthPercent = worthMap.get(currentRegionId) || 50;
            // Verwende calcRegionalSellPrice mit bereits geladenem worthPercent (keine DB-Query)
            currentRegionalPrice = await calcRegionalSellPrice(product, knowledgeFactor, currentRegionId, currentWorthPercent);
        }

        // Für jede Stadt den Preis berechnen und Branch-Typ bestimmen
        const results = [];
        for (const city of cities) {
            // Aktuelle Stadt ausschließen
            if (currentRegionId && city.id === currentRegionId) {
                continue;
            }
            
            const worthPercent = worthMap.get(city.id) || 50;
            // Verwende calcRegionalSellPrice mit bereits geladenem worthPercent (keine DB-Query)
            const priceInCity = await calcRegionalSellPrice(product, knowledgeFactor, city.id, worthPercent);
            
            // Nur Städte zurückgeben, wo der Preis höher ist
            // Kleine Toleranz (0.01) für Rundungsfehler bei Gleitkommaberechnungen
            const PRICE_TOLERANCE = 0.01;
            if (priceInCity > currentRegionalPrice - PRICE_TOLERANCE) {
                // Branch-Typ bestimmen
                let branchType = null; // null = kein Branch
                if (city.branches && city.branches.length > 0) {
                    // Finde den "besten" Branch-Typ (store/fullstack > production)
                    const branchTypes = city.branches.map(b => b.branchType?.labelTr).filter(Boolean);
                    if (branchTypes.includes('store') || branchTypes.includes('fullstack')) {
                        branchType = 'store'; // Grün
                    } else if (branchTypes.includes('production')) {
                        branchType = 'production'; // Orange
                    }
                }

                results.push({
                    regionId: city.id,
                    regionName: city.name,
                    price: priceInCity,
                    branchType: branchType // 'store' (grün), 'production' (orange), null (rot)
                });
            }
        }

        // Sortiere nach Preis (höchster zuerst)
        results.sort((a, b) => b.price - a.price);

        return results;
    }

    async renovate(hashedUserId, element) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const house = await UserHouse.findOne({
            where: { userId: user.id },
            include: [{ model: HouseType, as: 'houseType' }]
        });
        if (!house) throw new Error('House not found');
        const oldValue = house[element];
        if (oldValue >= 100) {
            return { cost: 0 };
        }
        const baseCost = house.houseType?.cost || 0;
        const cost = this._calculateRenovationCost(baseCost, element, oldValue);
        house[element] = 100;
        await house.save();
        await updateFalukantUserMoney(
            user.id,
            -cost,
            `renovation_${element}`
        );
        return { cost };
    }

    _calculateRenovationCost(baseCost, key, currentVal) {
        const weights = {
            roofCondition: 0.25,
            wallCondition: 0.25,
            floorCondition: 0.25,
            windowCondition: 0.25
        };
        const weight = weights[key] || 0;
        const missing = 100 - currentVal;
        const raw = (missing / 100) * baseCost * weight;
        return Math.round(raw * 100) / 100;
    }

    async renovateAll(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const house = await UserHouse.findOne({
            where: { userId: user.id },
            include: [{ model: HouseType, as: 'houseType' }]
        });
        if (!house) throw new Error('House not found');
        const baseCost = house.houseType?.cost || 0;
        const keys = ['roofCondition', 'wallCondition', 'floorCondition', 'windowCondition'];
        let rawSum = 0;
        for (const key of keys) {
            const current = house[key];
            if (current < 100) {
                rawSum += this._calculateRenovationCost(baseCost, key, current);
            }
        }
        const totalCost = Math.round(rawSum * 0.8 * 100) / 100;
        for (const key of keys) {
            house[key] = 100;
        }
        await house.save();
        await updateFalukantUserMoney(
            user.id,
            -totalCost,
            'renovation_all'
        );

        return { cost: totalCost };
    }

    async getUndergroundTypes(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const undergroundTypes = await UndergroundType.findAll();
        return undergroundTypes;
    }


    async getNotifications(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const notifications = await Notification.findAll({
            where: { userId: user.id, shown: false },
            order: [['createdAt', 'DESC']]
        });

        // Enrich notifications: parse JSON payloads and resolve character names
        await enrichNotificationsWithCharacterNames(notifications);

        return notifications;
    }

    async getAllNotifications(hashedUserId, page = 1, size = 10) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const limit = Math.max(1, Math.min(Number(size) || 10, 100));
        const offset = Math.max(0, ((Number(page) || 1) - 1) * limit);
        const { rows, count } = await Notification.findAndCountAll({
            where: { userId: user.id },
            order: [['createdAt', 'DESC']],
            offset,
            limit,
        });

        await enrichNotificationsWithCharacterNames(rows);

        return { items: rows, total: count, page: Number(page) || 1, size: limit };
    }

    async markNotificationsShown(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        
        // Use transaction to prevent deadlocks and ensure atomicity
        const transaction = await sequelize.transaction({
            isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED
        });
        
        try {
            const [count] = await Notification.update(
                { shown: true },
                { 
                    where: { userId: user.id, shown: false },
                    transaction
                }
            );
            await transaction.commit();
            
            // Send socket notification to update statusbar
            if (count > 0) {
                notifyUser(hashedUserId, 'falukantUpdateStatus', {});
            }
            
            return { updated: count };
        } catch (error) {
            await transaction.rollback();
            throw error;
        }
    }

    async getPoliticalOfficeHolders(hashedUserId) {
        const user = await getFalukantUserOrFail(hashedUserId);
        const character = await FalukantCharacter.findOne({
            where: {
                userId: user.id
            }
        });
        const relevantRegionIds = await this.getRegionAndParentIds(character.regionId);
        const now = new Date();
        const histories = await PoliticalOffice.findAll({
            where: {
                regionId: {
                    [Op.in]: relevantRegionIds
                },
            },
            include: [{
                model: FalukantCharacter,
                as: 'holder',
                include: [
                    { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                    { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
                    { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] }
                ],
                attributes: ['id', 'gender']
            }, {
                model: PoliticalOfficeType,
                as: 'type',
            }]
        });

        // Unikate nach character.id
        const map = new Map();
        const POLITICAL_TAX_EXEMPTIONS = {
            'council': ['city'],
            'taxman': ['city','county'],
            'treasurerer': ['city','county','shire'],
            'super-state-administrator': ['city','county','shire','markgrave','duchy'],
            'chancellor': ['*']
        };

        histories.forEach(h => {
            const c = h.holder;
            if (c && c.id && !map.has(c.id)) {
                const officeName = h.type?.name;
                const benefit = POLITICAL_TAX_EXEMPTIONS[officeName] || [];
                map.set(c.id, {
                    id: c.id,
                    name: `${c.definedFirstName.name} ${c.definedLastName.name}`,
                    title: c.nobleTitle.labelTr,
                    officeType: officeName,
                    gender: c.gender,
                    benefit
                });
            }
        });

        return Array.from(map.values());
    }

    async getRegionAndParentIds(regionId) {
        const relevantRegionIds = new Set();
        let currentRegionId = regionId;

        while (currentRegionId !== null) {
            relevantRegionIds.add(currentRegionId);
            const region = await RegionData.findByPk(currentRegionId, {
                attributes: ['parentId']
            });

            if (region && region.parentId) {
                currentRegionId = region.parentId;
            } else {
                currentRegionId = null; // Keine weitere Elternregion gefunden
            }
        }
        return Array.from(relevantRegionIds);
    }

    // vorher: async searchUsers(q) {
    async searchUsers(hashedUserId, q) {
        // User-Prüfung wie bei anderen Methoden
        await getFalukantUserOrFail(hashedUserId); // wir brauchen das Ergebnis hier nicht weiter, nur Validierung

        const chars = await FalukantCharacter.findAll({
            where: {
                userId: { [Op.ne]: null },
                [Op.or]: [
                    { '$user.user.username$': { [Op.iLike]: `%${q}%` } },
                    { '$definedFirstName.name$': { [Op.iLike]: `%${q}%` } },
                    { '$definedLastName.name$': { [Op.iLike]: `%${q}%` } }
                ]
            },
            include: [
                {
                    model: FalukantUser,
                    as: 'user',
                    include: [
                        {
                            model: User,
                            as: 'user',
                            attributes: ['username']
                        }
                    ]
                },
                {
                    model: FalukantPredefineFirstname,
                    as: 'definedFirstName',
                    attributes: ['name']
                },
                {
                    model: FalukantPredefineLastname,
                    as: 'definedLastName',
                    attributes: ['name']
                },
                {
                    model: RegionData,
                    as: 'region',
                    attributes: ['name']
                }
            ],
            limit: 50,
            raw: false
        });

        // Debug-Log (optional)
        console.log('FalukantService.searchUsers raw result for', q, chars);

        const mapped = chars
            .map(c => ({
                username: c.user?.user?.username || null,
                firstname: c.definedFirstName?.name || null,
                lastname: c.definedLastName?.name || null,
                town: c.region?.name || null
            }))
            .filter(u => u.username);

        console.log('FalukantService.searchUsers mapped result for', q, mapped);
        return mapped;
    }

    async createUndergroundActivity(hashedUserId, payload) {
        const { typeId, victimUsername, target, goal, politicalTargets } = payload;

        // 1) Performer auflösen
        const falukantUser = await this.getFalukantUserByHashedId(hashedUserId);
        if (!falukantUser || !falukantUser.character) {
            throw new Error('Performer not found');
        }
        const performerChar = falukantUser.character;

        // 2) Victim auflösen über Username (inner join)
        const victimChar = await FalukantCharacter.findOne({
            include: [
                {
                    model: FalukantUser,
                    as: 'user',
                    required: true, // inner join
                    attributes: [],
                    include: [
                        {
                            model: User,
                            as: 'user',
                            required: true, // inner join
                            where: { username: victimUsername },
                            attributes: []
                        }
                    ]
                }
            ]
        });

        if (!victimChar) {
            throw new PreconditionError('Victim character not found');
        }

        // 3) Selbstangriff verhindern
        if (victimChar.id === performerChar.id) {
            throw new PreconditionError('Cannot target yourself');
        }

        // 4) Typ-spezifische Validierung
        const undergroundType = await UndergroundType.findByPk(typeId);
        if (!undergroundType) {
            throw new Error('Invalid underground type');
        }

        if (undergroundType.tr === 'sabotage') {
            if (!target) {
                throw new PreconditionError('Sabotage target missing');
            }
        }

        if (undergroundType.tr === 'corrupt_politician') {
            if (!goal) {
                throw new PreconditionError('Corrupt goal missing');
            }
            // politicalTargets kann optional sein, falls benötigt prüfen
        }

        // 5) Eintrag anlegen (optional: in Transaction)
        const newEntry = await Underground.create({
            undergroundTypeId: typeId,
            performerId: performerChar.id,
            victimId: victimChar.id,
            result: null,
            parameters: {
                target: target || null,
                goal: goal || null,
                politicalTargets: politicalTargets || null
            }
        });

        return newEntry;
    }


    async getUndergroundAttacks(hashedUserId) {
        const falukantUser = await getFalukantUserOrFail(hashedUserId);
        const character = await FalukantCharacter.findOne({
            where: { userId: falukantUser.id }
        });
        if (!character) throw new Error('Character not found');

        const charId = character.id;

        const attacks = await Underground.findAll({
            where: {
                [Op.or]: [
                    { performerId: charId },
                    { victimId: charId }
                ]
            },
            include: [
                {
                    model: FalukantCharacter,
                    as: 'performer',
                    include: [
                        { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                        { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
                        {
                            model: FalukantUser,
                            as: 'user',
                            include: [{ model: User, as: 'user', attributes: ['username'] }],
                            attributes: []
                        }
                    ],
                    attributes: ['id', 'gender']
                },
                {
                    model: FalukantCharacter,
                    as: 'victim',
                    include: [
                        { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
                        { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] },
                        {
                            model: FalukantUser,
                            as: 'user',
                            include: [{ model: User, as: 'user', attributes: ['username'] }],
                            attributes: []
                        }
                    ],
                    attributes: ['id', 'gender']
                },
                {
                    model: UndergroundType,
                    as: 'undergroundType', // hier der korrekte Alias
                    attributes: ['tr']
                }
            ],
            order: [['createdAt', 'DESC']]
        });

        const formatCharacter = (c) => {
            if (!c) return null;
            return {
                id: c.id,
                username: c.user?.user?.username || null,
                firstname: c.definedFirstName?.name || null,
                lastname: c.definedLastName?.name || null,
                gender: c.gender
            };
        };

        const mapped = attacks.map(a => ({
            id: a.id,
            result: a.result,
            createdAt: a.createdAt,
            updatedAt: a.updatedAt,
            type: a.undergroundType?.tr || null, // angepasst
            parameters: a.parameters,
            performer: formatCharacter(a.performer),
            victim: formatCharacter(a.victim),
            success: !!a.result
        }));

        return {
            sent: mapped.filter(a => a.performer?.id === charId),
            received: mapped.filter(a => a.victim?.id === charId),
            all: mapped
        };
    }
}

export default new FalukantService();

// Helper: parse notifications for character references and attach characterName
async function enrichNotificationsWithCharacterNames(notifications) {
    if (!Array.isArray(notifications) || notifications.length === 0) return;

    const charIds = new Set();

    // recursive collector that extracts any character id fields
    function collectIds(obj) {
        if (!obj) return;
        if (Array.isArray(obj)) {
            for (const it of obj) collectIds(it);
            return;
        }
        if (typeof obj !== 'object') return;
        for (const [k, v] of Object.entries(obj)) {
            if (!v) continue;
            if (k === 'character_id' || k === 'characterId') {
                charIds.add(Number(v));
                continue;
            }
            if (k === 'character' && typeof v === 'object') {
                if (v.id) charIds.add(Number(v.id));
                if (v.character_id) charIds.add(Number(v.character_id));
                if (v.characterId) charIds.add(Number(v.characterId));
                collectIds(v);
                continue;
            }
            collectIds(v);
        }
    }

    // First pass: collect all referenced character ids from notifications
    for (const n of notifications) {
        // parse n.tr if it's JSON
        try {
            if (typeof n.tr === 'string' && n.tr.trim().startsWith('{')) {
                const parsed = JSON.parse(n.tr);
                collectIds(parsed);
            }
        } catch (err) { /* ignore */ }

        // parse n.effects if present
        try {
            if (n.effects) {
                const eff = typeof n.effects === 'string' && n.effects.trim().startsWith('{') ? JSON.parse(n.effects) : n.effects;
                collectIds(eff);
            }
        } catch (err) { /* ignore */ }

        // top-level fields
        if (n.character_id) charIds.add(Number(n.character_id));
        if (n.characterId) charIds.add(Number(n.characterId));
    }

    const ids = Array.from(charIds).filter(Boolean);
    if (!ids.length) return;

    // Batch load characters and their display names
    const characters = await FalukantCharacter.findAll({
        where: { id: { [Op.in]: ids } },
        include: [
            { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] },
            { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }
        ],
        attributes: ['id']
    });

    const nameMap = new Map();
    for (const c of characters) {
        const first = c.definedFirstName?.name || '';
        const last = c.definedLastName?.name || '';
        const display = `${first} ${last}`.trim() || null;
        nameMap.set(Number(c.id), display || `#${c.id}`);
    }

    // helper to find first character id in an object
    function findFirstId(obj) {
        if (!obj) return null;
        if (Array.isArray(obj)) {
            for (const it of obj) {
                const r = findFirstId(it);
                if (r) return r;
            }
            return null;
        }
        if (typeof obj !== 'object') return null;
        for (const [k, v] of Object.entries(obj)) {
            if (!v) continue;
            if (k === 'character_id' || k === 'characterId') return Number(v);
            if (k === 'character' && typeof v === 'object') {
                if (v.id) return Number(v.id);
                const r = findFirstId(v);
                if (r) return r;
            }
            const r = findFirstId(v);
            if (r) return r;
        }
        return null;
    }

    // Attach resolved name to notifications (set both characterName and character_name)
    for (const n of notifications) {
        let foundId = null;
        try {
            if (typeof n.tr === 'string' && n.tr.trim().startsWith('{')) {
                const parsed = JSON.parse(n.tr);
                foundId = findFirstId(parsed) || foundId;
            }
        } catch (err) { /* ignore */ }

        try {
            if (n.effects) {
                const eff = typeof n.effects === 'string' && n.effects.trim().startsWith('{') ? JSON.parse(n.effects) : n.effects;
                foundId = findFirstId(eff) || foundId;
            }
        } catch (err) { /* ignore */ }

        if (!foundId && n.character_id) foundId = Number(n.character_id);
        if (!foundId && n.characterId) foundId = Number(n.characterId);

        if (foundId && nameMap.has(Number(foundId))) {
            const resolved = nameMap.get(Number(foundId));
            n.characterName = resolved;
            n.character_name = resolved;
        }
    }
}
