import Club from "../models/Club.js";
import Member from "../models/Member.js";
import Tournament from "../models/Tournament.js";
import TournamentGroup from "../models/TournamentGroup.js";
import TournamentMatch from "../models/TournamentMatch.js";
import TournamentMember from "../models/TournamentMember.js";
import TournamentResult from "../models/TournamentResult.js";
import ExternalTournamentParticipant from "../models/ExternalTournamentParticipant.js";
import TournamentClass from "../models/TournamentClass.js";
import TournamentPairing from "../models/TournamentPairing.js";
import TournamentStage from "../models/TournamentStage.js";
import TournamentStageAdvancement from "../models/TournamentStageAdvancement.js";
import { checkAccess } from '../utils/userUtils.js';
import { Op, literal } from 'sequelize';


import { devLog } from '../utils/logger.js';

function normalizeJsonConfig(value, label = 'config') {
    if (value == null) return {};
    if (typeof value === 'object') return value;
    if (typeof value === 'string') {
        const trimmed = value.trim();
        if (!trimmed) return {};
        try {
            const parsed = JSON.parse(trimmed);
            return (parsed && typeof parsed === 'object') ? parsed : {};
        } catch (e) {
            throw new Error(`${label} ist ungültig (konnte JSON nicht parsen).`);
        }
    }
    return {};
}
function getRoundName(size) {
    switch (size) {
        case 2: return "Finale";
        case 3: return "Halbfinale (3)";
        case 4: return "Halbfinale";
        case 5: return "Viertelfinale (5)";
        case 6: return "Viertelfinale (6)";
        case 7: return "Viertelfinale (7)";
        case 8: return "Viertelfinale";
        case 9: return "Achtelfinale (9)";
        case 10: return "Achtelfinale (10)";
        case 11: return "Achtelfinale (11)";
        case 12: return "Achtelfinale (12)";
        case 13: return "Achtelfinale (13)";
        case 14: return "Achtelfinale (14)";
        case 15: return "Achtelfinale (15)";
        case 16: return "Achtelfinale";
        default: return `${size}-Runde`;
    }
}

function nextRoundName(currentName) {
    // Erkenne Rundennamen auch mit Suffixen wie "Achtelfinale (9)", "Viertelfinale (5)", etc.
    if (currentName && typeof currentName === 'string') {
        if (currentName.includes("Achtelfinale")) {
            return "Viertelfinale";
        }
        if (currentName.includes("Viertelfinale")) {
            return "Halbfinale";
        }
        if (currentName.includes("Halbfinale")) {
            return "Finale";
        }
        // Für "Finale" gibt es keine nächste Runde
        if (currentName.includes("Finale")) {
            return null;
        }
    }
    return null;
}

function getLoserId(match) {
    if (!match || !match.result) return null;
    const [w1, w2] = String(match.result).split(":").map(n => +n);
    if (!Number.isFinite(w1) || !Number.isFinite(w2)) return null;
    if (w1 === w2) return null;
    return (w1 > w2) ? match.player2Id : match.player1Id;
}

function shuffleInPlace(arr) {
    for (let i = arr.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr;
}

function nextPowerOfTwo(n) {
    let p = 1;
    while (p < n) p *= 2;
    return p;
}

function knockoutEntrantKey(entrant) {
    return `${entrant.isExternal ? 'E' : 'M'}:${entrant.id}`;
}

function compareKnockoutEntrants(a, b) {
    const comparisons = [
        ['place', 1],
        ['points', -1],
        ['setDiff', -1],
        ['setsWon', -1],
        ['pointsDiff', -1],
        ['pointsWon', -1],
    ];

    for (const [property, direction] of comparisons) {
        const aValue = Number(a[property] ?? 0);
        const bValue = Number(b[property] ?? 0);
        if (aValue !== bValue) {
            return direction * (aValue - bValue);
        }
    }

    if (!!a.isExternal !== !!b.isExternal) {
        return a.isExternal ? 1 : -1;
    }
    return Number(a.id) - Number(b.id);
}

const THIRD_PLACE_ROUND = 'Spiel um Platz 3';
class TournamentService {
    async createRoundRobinMatchesForGroups(tournamentId, groups, stageId = null) {
        for (const group of groups) {
            const internalMembers = await TournamentMember.findAll({ where: { groupId: group.id } });
            const externalMembers = await ExternalTournamentParticipant.findAll({ where: { groupId: group.id } });

            const allGroupMembers = [
                ...internalMembers.map(m => ({ id: m.id, key: `internal-${m.id}` })),
                ...externalMembers.map(m => ({ id: m.id, key: `external-${m.id}` })),
            ];
            if (allGroupMembers.length < 2) continue;

            const memberMap = new Map(allGroupMembers.map(m => [m.key, m]));
            const rounds = this.generateRoundRobinSchedule(allGroupMembers.map(m => ({ id: m.key })));
            for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
                for (const [p1Key, p2Key] of rounds[roundIndex]) {
                    const p1 = memberMap.get(p1Key);
                    const p2 = memberMap.get(p2Key);
                    if (!p1 || !p2) continue;
                    await TournamentMatch.create({
                        tournamentId,
                        stageId,
                        groupId: group.id,
                        round: 'group',
                        player1Id: p1.id,
                        player2Id: p2.id,
                        groupRound: roundIndex + 1,
                        classId: group.classId,
                    });
                }
            }
        }
    }

    async assignEntrantsToStageGroups(tournamentId, items, groups, classId) {
        if (!groups.length || !items.length) return;
        const orderedItems = [...items].sort(compareKnockoutEntrants);
        const assignments = groups.map(() => []);
        orderedItems.forEach((item, index) => {
            assignments[index % groups.length].push(item);
        });

        for (let groupIndex = 0; groupIndex < assignments.length; groupIndex++) {
            const group = groups[groupIndex];
            for (const item of assignments[groupIndex]) {
                if (item.isExternal) {
                    const original = await ExternalTournamentParticipant.findByPk(Number(item.id));
                    if (!original) continue;
                    await ExternalTournamentParticipant.create({
                        tournamentId,
                        groupId: group.id,
                        firstName: original.firstName,
                        lastName: original.lastName,
                        club: original.club,
                        email: original.email,
                        address: original.address,
                        birthDate: original.birthDate,
                        gender: original.gender,
                        seeded: original.seeded || false,
                        classId,
                        outOfCompetition: original.outOfCompetition || false,
                        gaveUp: original.gaveUp || false,
                    });
                } else {
                    const clubMemberId = Number(item.clubMemberId ?? item.member?.id ?? item.id);
                    const member = Number.isFinite(clubMemberId) ? await Member.findByPk(clubMemberId) : null;
                    if (!member) continue;
                    await TournamentMember.create({
                        tournamentId,
                        groupId: group.id,
                        clubMemberId,
                        seeded: item.seeded || false,
                        classId,
                        outOfCompetition: item.outOfCompetition || false,
                        gaveUp: item.gaveUp || false,
                    });
                }
            }
        }
    }

    // -------- Multi-Stage (Runden) V1 --------
    async getTournamentStages(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);
        const t = await Tournament.findOne({ where: { id: tournamentId, clubId } });
        if (!t) throw new Error('Turnier nicht gefunden');

        const stages = await TournamentStage.findAll({
            where: { tournamentId },
            order: [['index', 'ASC']],
        });
        const advancements = await TournamentStageAdvancement.findAll({
            where: { tournamentId },
            order: [['id', 'ASC']],
        });

        // MariaDB kann JSON-Felder als String liefern -> für Frontend/Advance normalisieren
        const normalizedAdvancements = advancements.map(a => {
            const plain = a.toJSON ? a.toJSON() : a;
            return {
                ...plain,
                config: normalizeJsonConfig(plain.config, 'Advancement-Konfiguration'),
            };
        });

        return { stages, advancements: normalizedAdvancements };
    }

    async upsertTournamentStages(userToken, clubId, tournamentId, stages = [], advancement = null, advancements = null) {
        await checkAccess(userToken, clubId);
        const t = await Tournament.findOne({ where: { id: tournamentId, clubId } });
        if (!t) throw new Error('Turnier nicht gefunden');

        // Minimal-V1: ersetzen (einfach zu testen; später inkrementell)
        await TournamentStageAdvancement.destroy({ where: { tournamentId } });
        await TournamentStage.destroy({ where: { tournamentId } });

        const createdStages = [];
        for (const s of stages) {
            if (!s || s.index == null || !s.type) continue;
            createdStages.push(await TournamentStage.create({
                tournamentId,
                index: Number(s.index),
                name: s.name ?? null,
                type: s.type,
                numberOfGroups: s.numberOfGroups != null ? Number(s.numberOfGroups) : null,
                advancingPerGroup: s.advancingPerGroup != null ? Number(s.advancingPerGroup) : null,
                maxGroupSize: s.maxGroupSize != null ? Number(s.maxGroupSize) : null,
            }));
        }

        // Unterstütze sowohl neues `advancements: []` als auch legacy `advancement: {}`
        const advList = Array.isArray(advancements)
            ? advancements
            : (advancement ? [advancement] : []);

        const createdAdvs = [];
        for (const adv of advList) {
            if (!adv || adv.fromStageIndex == null || adv.toStageIndex == null) continue;

            const fromIndex = Number(adv.fromStageIndex);
            const toIndex = Number(adv.toStageIndex);
            const from = createdStages.find(x => x.index === fromIndex);
            const to = createdStages.find(x => x.index === toIndex);
            if (!from || !to) throw new Error('Advancement verweist auf unbekannte Stages');

            const mode = adv.mode || 'pools';
            const config = normalizeJsonConfig(adv.config, 'Advancement-Konfiguration');
            if (mode === 'pools') {
                const pools = Array.isArray(config.pools) ? config.pools : [];
                if (pools.length === 0) throw new Error('Advancement-Konfiguration ist leer');
            }

            createdAdvs.push(await TournamentStageAdvancement.create({
                tournamentId,
                fromStageId: from.id,
                toStageId: to.id,
                mode,
                config,
            }));
        }

        // Response-Shape an GET anpassen
        return { stages: createdStages, advancements: createdAdvs };
    }

    // V1: advance von StageIndex -> StageIndex+1. Pool-Regeln basieren auf Gruppenplatz pro Gruppe.
    async advanceTournamentStage(userToken, clubId, tournamentId, fromStageIndex = 1, toStageIndex = null) {
        await checkAccess(userToken, clubId);
        const t = await Tournament.findOne({ where: { id: tournamentId, clubId } });
        if (!t) throw new Error('Turnier nicht gefunden');

        const stages = await TournamentStage.findAll({ where: { tournamentId }, order: [['index', 'ASC']] });
        const fromStage = stages.find(s => s.index === Number(fromStageIndex));
        if (!fromStage) {
            const existing = stages.map(s => s.index).sort((a, b) => a - b);
            const expectedFrom = Number(fromStageIndex);
            throw new Error(
                `Stage nicht gefunden (erwartet fromStageIndex=${expectedFrom}; vorhanden: ${existing.join(',') || '(keine)'}). ` +
                `Tipp: Speichere die Runden-Konfiguration und prüfe, dass Stage ${expectedFrom} existiert.`
            );
        }

        // Ziel-Stage kommt aus der Advancement-Konfiguration.
        // Damit ist sowohl 1->2 (mit Zwischenrunde) als auch 1->3 (direkt Endrunde) möglich.
        const advWhere = { tournamentId, fromStageId: fromStage.id };
        if (toStageIndex != null) {
            const toIndexNum = Number(toStageIndex);
            const to = stages.find(s => s.index === toIndexNum);
            if (!to) {
                const existing = stages.map(s => s.index).sort((a, b) => a - b);
                throw new Error(
                    `Stage nicht gefunden (erwartet toStageIndex=${toIndexNum}; vorhanden: ${existing.join(',') || '(keine)'}).`
                );
            }
            advWhere.toStageId = to.id;
        }

        const adv = await TournamentStageAdvancement.findOne({
            where: advWhere,
            order: [['id', 'ASC']],
        });
        if (!adv) throw new Error('Keine Advancement-Konfiguration gefunden');

        const toStage = stages.find(s => s.id === adv.toStageId);
        if (!toStage) {
            const existing = stages.map(s => `${s.index}(id=${s.id})`).join(',') || '(keine)';
            throw new Error(
                `Stage nicht gefunden (Advancement zeigt auf toStageId=${adv.toStageId}; vorhanden: ${existing}). ` +
                `Tipp: Speichere die Runden erneut (Stages + Advancement).`
            );
        }

    const config = normalizeJsonConfig(adv.config, 'Advancement-Konfiguration');
    let pools = Array.isArray(config.pools) ? config.pools : [];
        if (pools.length === 0) {
            const advancingCount = Math.max(1, Number(toStage.advancingPerGroup || t.advancingPerGroup || 2));
            const fromPlaces = Array.from({ length: advancingCount }, (_, index) => index + 1);
            pools = [{
                fromPlaces,
                target: toStage.type === 'knockout'
                    ? { type: 'knockout', singleField: true }
                    : { type: 'groups', groupCount: Math.max(1, Number(toStage.numberOfGroups || 1)) },
            }];
        }

        // Cleanup Zielrunde inklusive dort duplizierter Teilnehmer.
        const existingTargetGroups = await TournamentGroup.findAll({ where: { tournamentId, stageId: toStage.id } });
        const existingTargetGroupIds = existingTargetGroups.map(g => g.id);
        await TournamentMatch.destroy({ where: { tournamentId, stageId: toStage.id } });
        if (existingTargetGroupIds.length > 0) {
            await TournamentMember.destroy({ where: { tournamentId, groupId: { [Op.in]: existingTargetGroupIds } } });
            await ExternalTournamentParticipant.destroy({ where: { tournamentId, groupId: { [Op.in]: existingTargetGroupIds } } });
        }
        await TournamentGroup.destroy({ where: { tournamentId, stageId: toStage.id } });

        // Stage1 Gruppen mit Teilnehmern (Ranking aus existierender Logik)
        const stage1Groups = await this.getGroupsWithParticipants(userToken, clubId, tournamentId);
        const relevantStage1Groups = stage1Groups.filter(g => {
            if (Number(fromStage.index) === 1 && g.stageId == null) return true;
            return Number(g.stageId) === Number(fromStage.id);
        });
        if (relevantStage1Groups.length === 0) throw new Error('Keine Gruppen in der Ausgangsrunde gefunden');

        const perGroupRanked = relevantStage1Groups.map(g => ({
            groupId: g.groupId,
            classId: g.classId ?? null,
            // WICHTIG:
            // - Für interne Teilnehmer brauchen wir die ClubMember-ID (Member.id / TournamentMember.clubMemberId),
            //   nicht die TournamentMember.id.
            // - Für externe Teilnehmer ist `id` die ExternalTournamentParticipant.id (bestehende Logik).
            participants: (g.participants || []).map((p, index) => ({
                id: p.isExternal ? p.id : (p.clubMemberId ?? p.member?.id ?? p.id),
                isExternal: !!p.isExternal,
                place: Number(p.position ?? index + 1),
                points: Number(p.points ?? 0),
                setDiff: Number(p.setDiff ?? 0),
                setsWon: Number(p.setsWon ?? 0),
                pointsDiff: Number(p.pointsDiff ?? 0),
                pointsWon: Number(p.pointsWon ?? 0),
            })),
        }));

        const getByPlace = (grp, place) => grp.participants[place - 1];

        const poolItems = [];
        for (const rule of pools) {
            const fromPlaces = Array.isArray(rule.fromPlaces) ? rule.fromPlaces : [];
            if (fromPlaces.length === 0) continue;
            const target = rule.target || {};

            // Wenn Klassen verwendet werden, sollen Advancements je Klasse unabhängig sein.
            // D.h. eine Pool-Regel wird pro Klasse ausgewertet (oder nur für classless, falls keine Klasse).
            const classIdsInStage1 = [...new Set(perGroupRanked.map(g => (g.classId ?? null)))];

            for (const classId of classIdsInStage1) {
                // Zähle die Gruppen für diese Klasse
                const groupsForClass = perGroupRanked.filter(g => (g.classId ?? null) === classId);
                const numberOfGroupsForClass = groupsForClass.length;
                
                // Nur Klassen mit mehr als einer Gruppe dürfen weitere Runden haben
                if (numberOfGroupsForClass <= 1) {
                    // Überspringe diese Klasse - keine Zwischen-/Endrunde für Klassen mit nur einer Gruppe
                    continue;
                }
                
                const items = [];
                for (const grp of perGroupRanked) {
                    if ((grp.classId ?? null) !== classId) continue;
                    for (const place of fromPlaces) {
                        const p = getByPlace(grp, Number(place));
                        if (p) items.push({ ...p, classId });
                    }
                }

                // Keine leeren Regeln weiterreichen: verhindert, dass eine Klasse ohne fertige Gruppen
                // "nichts" erzeugt und dadurch andere Regeln/klassen beeinflusst.
                if (items.length > 0) {
                    poolItems.push({ target, items, classId });
                }
            }
        }

    const createdGroups = [];
    const singleFieldKoItems = [];
    let wantsThirdPlace = false;
        for (const pool of poolItems) {
            let target = pool.target || {};
            const items = pool.items || [];
            const poolClassId = pool.classId ?? null;
            if (items.length === 0) continue;

            // Wenn Endrunde K.O. sein soll, aber target.type === 'groups' ist,
            // dann sollte es K.O. sein statt Gruppen.
            // Die Stage-Konfiguration (toStage.type) ist die Quelle der Wahrheit
            if (toStage.type === 'knockout' && target.type === 'groups') {
                // Erzwinge K.O. wenn die Stage-Konfiguration K.O. ist
                // singleField sollte true sein für Endrunden (K.O. als ein einziges Feld)
                target = { ...target, type: 'knockout', singleField: true };
            }

            if (target.type === 'groups') {
                const groupCount = Math.max(1, Number(target.groupCount || toStage.numberOfGroups || 1));
                const poolGroups = [];
                for (let i = 0; i < groupCount; i++) {
                    poolGroups.push(await TournamentGroup.create({
                        tournamentId,
                        stageId: toStage.id,
                        classId: poolClassId,
                    }));
                }

                const shuffled = [...items];
                for (let i = shuffled.length - 1; i > 0; i--) {
                    const j = Math.floor(Math.random() * (i + 1));
                    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
                }

                await this.assignEntrantsToStageGroups(tournamentId, shuffled, poolGroups, poolClassId);
                await this.createRoundRobinMatchesForGroups(tournamentId, poolGroups, toStage.id);

                createdGroups.push(...poolGroups);
            } else if (target.type === 'knockout') {
                if (target.thirdPlace === true) wantsThirdPlace = true;
                if (target.singleField === true) {
                    singleFieldKoItems.push(...items);
                    continue;
                }
                // KO-Bracket pro Pool-Regel
                // Wir legen eine "Container-Gruppe" an, damit Matches logisch zusammengehören.
                // (groupId ist optional; viele Stellen nutzen groupId als Filter.)
                const containerGroup = await TournamentGroup.create({
                    tournamentId,
                    stageId: toStage.id,
                    classId: poolClassId,
                });
                createdGroups.push(containerGroup);

                const entrants = items.map(p => ({
                    // p.id wurde oben bereits als clubMemberId (intern) bzw. ExternalParticipant.id (extern) gemappt
                    id: Number(p.id),
                    isExternal: !!p.isExternal,
                }));
                if (entrants.length < 2) {
                    // Für KO brauchen wir mindestens 2.
                    continue;
                }

                shuffleInPlace(entrants);
                const bracketSize = nextPowerOfTwo(entrants.length);
                const byes = bracketSize - entrants.length;
                const roundName = getRoundName(bracketSize);
                // Erzeuge zunächst BYE‑Matches für die ersten 'byes' Teilnehmer
                let remaining = [...entrants];
                if (byes > 0) {
                    const byePlayers = remaining.slice(0, Math.min(byes, remaining.length));
                    remaining = remaining.slice(byePlayers.length);
                    for (const p of byePlayers) {
                        if (!p) continue;
                        await TournamentMatch.create({
                            tournamentId,
                            stageId: toStage.id,
                            groupId: containerGroup.id,
                            classId: poolClassId,
                            groupRound: null,
                            round: roundName,
                            player1Id: Number(p.id),
                            player2Id: null,
                            isFinished: true,
                            isActive: false,
                            result: 'BYE',
                        });
                    }
                }
                // Paare die verbleibenden Teilnehmer normal
                for (let i = 0; i < remaining.length; i += 2) {
                    const a = remaining[i];
                    const b = remaining[i + 1];
                    if (!a || !b) continue;
                    await TournamentMatch.create({
                        tournamentId,
                        stageId: toStage.id,
                        groupId: containerGroup.id,
                        classId: poolClassId,
                        groupRound: null,
                        round: roundName,
                        player1Id: Number(a.id),
                        player2Id: Number(b.id),
                        isFinished: false,
                        isActive: true,
                        result: null,
                    });
                }
            }
        }

        // KO als "ein einziges Feld" über alle Regeln
        if (singleFieldKoItems.length > 0) {
            // singleField: bisher war das "alle Regeln zu einem KO-Feld mischen".
            // Mit Klassen ist es praktisch, pro Klasse ein eigenes singleField-KO zu bauen.
            const itemsByClass = {};
            for (const it of singleFieldKoItems) {
                const key = (it.classId ?? null) === null ? 'null' : String(it.classId);
                (itemsByClass[key] ||= []).push(it);
            }

            for (const [classKey, classItems] of Object.entries(itemsByClass)) {
                const classId = classKey === 'null' ? null : Number(classKey);

                const containerGroup = await TournamentGroup.create({
                    tournamentId,
                    stageId: toStage.id,
                    classId,
                });
                createdGroups.push(containerGroup);

                const entrants = classItems.map(p => ({
                    id: Number(p.id),
                    isExternal: !!p.isExternal,
                    place: p.place || 999,
                    points: p.points ?? 0,
                    setDiff: p.setDiff ?? 0,
                    setsWon: p.setsWon ?? 0,
                    pointsDiff: p.pointsDiff ?? 0,
                    pointsWon: p.pointsWon ?? 0,
                }));

                // Dedupliziere (falls jemand in mehreren Regeln landet)
                // Wenn jemand mehrfach vorkommt, nehme den besten Platz
                const seen = new Map();
                for (const e of entrants) {
                    const key = knockoutEntrantKey(e);
                    const existing = seen.get(key);
                    if (!existing || compareKnockoutEntrants(e, existing) < 0) {
                        seen.set(key, e);
                    }
                }
                const uniqueEntrants = Array.from(seen.values());

                const thirdPlace = wantsThirdPlace;
                if (uniqueEntrants.length >= 2) {
                    // Ein KO-Feld wird voll besetzt, sofern weitere Gruppenteilnehmer vorhanden sind.
                    // Beispiel: 3 Gruppen x 2 Qualifikanten ergeben sechs Starter; die beiden
                    // besten Drittplatzierten vervollständigen das Viertelfinale.
                    const targetBracketSize = nextPowerOfTwo(uniqueEntrants.length);
                    const requiredFillers = targetBracketSize - uniqueEntrants.length;
                    if (requiredFillers > 0) {
                        const selectedKeys = new Set(uniqueEntrants.map(knockoutEntrantKey));
                        const remainingEntrants = perGroupRanked
                            .filter(group => (group.classId ?? null) === classId)
                            .flatMap(group => group.participants)
                            .filter(entrant => !selectedKeys.has(knockoutEntrantKey(entrant)))
                            .sort(compareKnockoutEntrants);
                        uniqueEntrants.push(...remainingEntrants.slice(0, requiredFillers));
                    }

                    uniqueEntrants.sort(compareKnockoutEntrants);
                    
                    // Paare: Bester gegen Schlechtesten, Zweiter gegen Vorletzten, etc.
                    // Reverse die zweite Hälfte, um das gewünschte Pairing zu erreichen
                    const bracketSize = nextPowerOfTwo(uniqueEntrants.length);
                    const byes = bracketSize - uniqueEntrants.length;
                    const playersNeeded = bracketSize - byes;
                    
                    // Teile in zwei Hälften für das Pairing
                    const firstHalf = uniqueEntrants.slice(0, Math.ceil(playersNeeded / 2));
                    const secondHalf = uniqueEntrants.slice(Math.ceil(playersNeeded / 2), playersNeeded);
                    const reversedSecondHalf = [...secondHalf].reverse(); // Umgekehrte Reihenfolge für Pairing
                    
                    const roundName = getRoundName(bracketSize);
                    // BYE‑Matches zuerst (für die ersten Teilnehmer, wenn nötig)
                    if (byes > 0) {
                        // Die besten Spieler bekommen BYEs
                        for (let i = 0; i < byes && i < uniqueEntrants.length; i++) {
                            const p = uniqueEntrants[i];
                            if (!p) continue;
                            await TournamentMatch.create({
                                tournamentId,
                                stageId: toStage.id,
                                groupId: containerGroup.id,
                                classId,
                                groupRound: null,
                                round: roundName,
                                player1Id: Number(p.id),
                                player2Id: null,
                                isFinished: true,
                                isActive: false,
                                result: 'BYE',
                            });
                        }
                    }
                    
                    // Paare: Erster gegen Letzten, Zweiter gegen Vorletzten, etc.
                    const maxPairs = Math.min(firstHalf.length, reversedSecondHalf.length);
                    for (let i = 0; i < maxPairs; i++) {
                        const a = firstHalf[i];
                        const b = reversedSecondHalf[i];
                        if (!a || !b) continue;
                        await TournamentMatch.create({
                            tournamentId,
                            stageId: toStage.id,
                            groupId: containerGroup.id,
                            classId,
                            groupRound: null,
                            round: roundName,
                            player1Id: Number(a.id),
                            player2Id: Number(b.id),
                            isFinished: false,
                            isActive: true,
                            result: null,
                        });
                    }
                }
            }

            return {
                fromStageId: fromStage.id,
                toStageId: toStage.id,
                createdGroupIds: createdGroups.map(g => g.id),
            };
        }

        return {
            fromStageId: fromStage.id,
            toStageId: toStage.id,
            createdGroupIds: createdGroups.map(g => g.id),
        };
    }

    // 1. Turniere listen
    async getTournaments(userToken, clubId) {
        await checkAccess(userToken, clubId);
        const tournaments = await Tournament.findAll({
            where: { clubId },
            order: [['date', 'DESC']],
            attributes: ['id', 'name', 'date', 'allowsExternal']
        });
        return JSON.parse(JSON.stringify(tournaments));
    }

    // 2. Neues Turnier anlegen (prüft Duplikat)
    async addTournament(userToken, clubId, tournamentName, date, winningSets, allowsExternal) {
        await checkAccess(userToken, clubId);
        const existing = await Tournament.findOne({ where: { clubId, date } });
        if (existing) {
            throw new Error('Ein Turnier mit diesem Datum existiert bereits');
        }
        const t = await Tournament.create({
            name: tournamentName,
            date,
            clubId: +clubId,
            bestOfEndroundSize: 0,
            type: '',
            winningSets: winningSets || 3, // Default: 3 Sätze
            allowsExternal: allowsExternal || false
        });
        return JSON.parse(JSON.stringify(t));
    }

    // 3. Teilnehmer hinzufügen (kein Duplikat) - klassengebunden
    async addParticipant(userToken, clubId, classId, participantId, tournamentId = null) {
        await checkAccess(userToken, clubId);

        // Normalize case: the third parameter may be either a classId or a tournamentId
        if (classId) {
            const maybeClass = await TournamentClass.findByPk(classId);
            if (!maybeClass) {
                const maybeTournament = await Tournament.findByPk(classId);
                if (maybeTournament) {
                    // caller passed tournamentId in this slot
                    tournamentId = classId;
                    classId = null;
                } else {
                    throw new Error('Klasse nicht gefunden');
                }
            }
        }

        // If classId is provided (and resolved to a valid class), do class-specific validations and add
        if (classId) {
            const tournamentClass = await TournamentClass.findByPk(classId);
            if (!tournamentClass) throw new Error('Klasse nicht gefunden');
            const tournament = await Tournament.findByPk(tournamentClass.tournamentId);
            if (!tournament || tournament.clubId != clubId) {
                throw new Error('Turnier nicht gefunden');
            }

            // Prüfe Geschlecht: Lade Mitglied
            const member = await Member.findByPk(participantId);
            if (!member) {
                throw new Error('Mitglied nicht gefunden');
            }
            const memberGender = member.gender || 'unknown';

            // Validierung: Geschlecht muss zur Klasse passen
            if (tournamentClass.gender) {
                if (tournamentClass.gender === 'male' && memberGender !== 'male') {
                    throw new Error('Dieser Teilnehmer kann nicht in einer männlichen Klasse spielen');
                }
                if (tournamentClass.gender === 'female' && memberGender !== 'female') {
                    throw new Error('Dieser Teilnehmer kann nicht in einer weiblichen Klasse spielen');
                }
                if (tournamentClass.gender === 'mixed' && memberGender === 'unknown') {
                    throw new Error('Teilnehmer mit unbekanntem Geschlecht können nicht in einer Mixed-Klasse spielen');
                }
            }

            // Validierung: Geburtsjahr muss zur Klasse passen (geboren im Jahr X oder später, also >=)
            if (tournamentClass.minBirthYear && member.birthDate) {
                // Parse das Geburtsdatum (Format: YYYY-MM-DD oder DD.MM.YYYY)
                let birthYear = null;
                if (member.birthDate.includes('-')) {
                    birthYear = parseInt(member.birthDate.split('-')[0]);
                } else if (member.birthDate.includes('.')) {
                    const parts = member.birthDate.split('.');
                    if (parts.length === 3) {
                        birthYear = parseInt(parts[2]);
                    }
                }
                if (birthYear && !isNaN(birthYear)) {
                    if (birthYear < tournamentClass.minBirthYear) {
                        throw new Error(`Dieser Teilnehmer ist zu alt für diese Klasse. Erlaubt: geboren ${tournamentClass.minBirthYear} oder später`);
                    }
                }
            }

            // Prüfe, ob Teilnehmer bereits in dieser Klasse ist
            const exists = await TournamentMember.findOne({ where: { classId, clubMemberId: participantId } });
            if (exists) {
                throw new Error('Teilnehmer bereits hinzugefügt');
            }
            await TournamentMember.create({
                tournamentId: tournamentClass.tournamentId,
                classId,
                clubMemberId: participantId,
                groupId: null
            });
            return;
        }

        // If classId is not provided, allow adding participant to the tournament without class
        // tournamentId must be provided in this case
        if (!tournamentId) {
            throw new Error('Klasse oder Turnier-ID ist erforderlich');
        }
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }

        // Verify member exists
        const member = await Member.findByPk(participantId);
        if (!member) {
            throw new Error('Mitglied nicht gefunden');
        }

        // Prüfe, ob Teilnehmer bereits im Turnier (ohne Klasse) ist
        const existsNoClass = await TournamentMember.findOne({
            where: { tournamentId, classId: null, clubMemberId: participantId }
        });
        if (existsNoClass) {
            throw new Error('Teilnehmer bereits hinzugefügt');
        }

        await TournamentMember.create({
            tournamentId,
            classId: null,
            clubMemberId: participantId,
            groupId: null
        });
    }

    // 4. Teilnehmerliste - nach Klasse oder Turnier
    async getParticipants(userToken, clubId, tournamentId, classId = null) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const whereClause = { tournamentId };
        if (classId !== null) {
            whereClause.classId = classId;
        }
        const stageGroups = await TournamentGroup.findAll({
            where: { tournamentId, stageId: { [Op.ne]: null } },
            attributes: ['id'],
        });
        const stageGroupIds = new Set(stageGroups.map(g => Number(g.id)));
        const participants = await TournamentMember.findAll({
            where: whereClause,
            include: [{
                model: Member,
                as: 'member',
                attributes: ['id', 'firstName', 'lastName', 'ttr', 'qttr', 'gender'],
            }],
            order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']]
        });
        return participants.filter(p => !p.groupId || !stageGroupIds.has(Number(p.groupId)));
    }

    // 5. Modus setzen (Gruppen / KO‑Runde)
    async setModus(userToken, clubId, tournamentId, type, numberOfGroups, advancingPerGroup) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        await tournament.update({ type, numberOfGroups, advancingPerGroup });
    }

    // 6. Leere Gruppen anlegen
    async createGroups(userToken, clubId, tournamentId, numberOfGroups = null) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        // Semantik:
        // - null/undefined: nimm den im Turnier gespeicherten Wert
        // - 0: UI-Default -> normalisiere auf mindestens 1
        // - <0 oder nicht-integer: Fehler
        let desired = tournament.numberOfGroups;
        if (numberOfGroups !== null && numberOfGroups !== undefined) {
            const n = Number(numberOfGroups);
            if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
                throw new Error('numberOfGroups muss eine ganze Zahl >= 0 sein');
            }
            desired = Math.max(1, n);
        }

        // DEBUG: Gruppen-Planung sichtbar machen (temporär)
        console.log('[tournamentService.createGroups] input:', {
            tournamentId,
            clubId,
            numberOfGroups,
            tournamentStoredNumberOfGroups: tournament.numberOfGroups,
            desired,
        });
        const existing = await TournamentGroup.findAll({ 
            where: { 
                tournamentId,
                classId: null // Nur Gruppen ohne Klasse
            } 
        });

        console.log('[tournamentService.createGroups] existing groups (classId=null):', existing.map(g => g.id));

        // zu viele Gruppen löschen
        if (existing.length > desired) {
            const toRemove = existing.slice(desired);
            console.log('[tournamentService.createGroups] removing groups:', toRemove.map(g => g.id));
            await Promise.all(toRemove.map(g => g.destroy()));
        }
        // fehlende Gruppen anlegen
        let createdCount = 0;
        for (let i = existing.length; i < desired; i++) {
            await TournamentGroup.create({ tournamentId, classId: null });
            createdCount++;
        }

        const finalGroups = await TournamentGroup.findAll({
            where: { tournamentId, classId: null },
            order: [['id', 'ASC']],
        });
        console.log('[tournamentService.createGroups] createdCount:', createdCount, 'finalCount:', finalGroups.length, 'finalIds:', finalGroups.map(g => g.id));
    }

    async createGroupsPerClass(userToken, clubId, tournamentId, groupsPerClass) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }

        // Nur die angegebenen Klassen anpassen; keine globale Löschung
        for (const [classIdStr, numberOfGroups] of Object.entries(groupsPerClass || {})) {
            const classId = (classIdStr === 'null' || classIdStr === 'undefined') ? null : parseInt(classIdStr);
            const desired = Number.parseInt(numberOfGroups);
            if (!Number.isFinite(desired) || desired < 0) {
                // Überspringe ungültige Werte
                continue;
            }

            // Hole bestehende Gruppen für diese Klasse
            const where = { tournamentId };
            if (classId === null) {
                where.classId = null;
            } else {
                where.classId = classId;
            }
            const existing = await TournamentGroup.findAll({ where, order: [['id', 'ASC']] });

            // Wenn desired == 0: lösche nur Gruppen dieser Klasse, nicht die anderen
            if (desired === 0) {
                if (existing.length > 0) {
                    await Promise.all(existing.map(g => g.destroy()));
                }
                continue;
            }

            // Ansonsten auf gewünschte Anzahl bringen
            if (existing.length > desired) {
                const toRemove = existing.slice(desired);
                await Promise.all(toRemove.map(g => g.destroy()));
            } else if (existing.length < desired) {
                const toCreate = desired - existing.length;
                for (let i = 0; i < toCreate; i++) {
                    await TournamentGroup.create({ tournamentId, classId });
                }
            }
        }
    }

    // 7. Gruppen zufällig füllen & Spiele anlegen
    generateRoundRobinSchedule(players) {
        const list = [...players];
        const n = list.length;
        const hasBye = n % 2 === 1;
        if (hasBye) list.push(null);        // füge Bye hinzu
        const total = list.length;          // jetzt gerade Zahl
        const rounds = [];

        for (let round = 0; round < total - 1; round++) {
            const pairs = [];
            for (let i = 0; i < total / 2; i++) {
                const p1 = list[i];
                const p2 = list[total - 1 - i];
                if (p1 && p2) {
                    pairs.push([p1.id, p2.id]);
                }
            }
            rounds.push(pairs);
            // Rotation (Fixpunkt list[0]):
            list.splice(1, 0, list.pop());
        }

        return rounds;
    }


    // services/tournamentService.js
    async fillGroups(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);

        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }

        // Lade alle Gruppen (mit classId)
        let allGroups = await TournamentGroup.findAll({ 
            where: { tournamentId },
            order: [['id', 'ASC']]
        });

        // Lade alle Teilnehmer (interne und externe)
        const members = await TournamentMember.findAll({ where: { tournamentId } });
        const externalParticipants = await ExternalTournamentParticipant.findAll({ where: { tournamentId } });
        
        if (!members.length && !externalParticipants.length) {
            throw new Error('Keine Teilnehmer vorhanden.');
        }

        // 2) Alte Matches löschen
        await TournamentMatch.destroy({ where: { tournamentId } });

        // 3) Alle Zuordnungen löschen und zufällig neu verteilen
        await TournamentMember.update(
            { groupId: null },
            { where: { tournamentId } }
        );
        await ExternalTournamentParticipant.update(
            { groupId: null },
            { where: { tournamentId } }
        );
        // Setze auch groupId bei Paarungen auf null
        await TournamentPairing.update(
            { groupId: null },
            { where: { tournamentId } }
        );

        // 4) Gruppiere Teilnehmer und Gruppen nach Klassen
        const groupsByClass = {};
        allGroups.forEach(g => {
            const classKey = g.classId || 'null';
            if (!groupsByClass[classKey]) {
                groupsByClass[classKey] = [];
            }
            groupsByClass[classKey].push(g);
        });

        const membersByClass = {};
        members.forEach(m => {
            const classKey = (m.classId || 'null').toString();
            if (!membersByClass[classKey]) {
                membersByClass[classKey] = [];
            }
            membersByClass[classKey].push({ ...m.toJSON(), isExternal: false });
        });
        
        externalParticipants.forEach(e => {
            const classKey = (e.classId || 'null').toString();
            if (!membersByClass[classKey]) {
                membersByClass[classKey] = [];
            }
            membersByClass[classKey].push({ ...e.toJSON(), isExternal: true });
        });

        // 5) Für jede Klasse separat verteilen
        const allUpdatePromises = [];
        
        for (const [classKey, groups] of Object.entries(groupsByClass)) {
            const classMembers = membersByClass[classKey] || [];
            if (classMembers.length === 0 || groups.length === 0) continue;

            // Prüfe, ob diese Klasse ein Doppel ist
            const classId = classKey === 'null' ? null : parseInt(classKey);
            const tournamentClass = classId ? await TournamentClass.findOne({
                where: { id: classId, tournamentId }
            }) : null;
            const isDoubles = tournamentClass ? tournamentClass.isDoubles : false;

            let itemsToDistribute = [];
            
            if (isDoubles) {
                // Bei Doppel: Lade Paarungen und verteile diese
                const pairings = await TournamentPairing.findAll({
                    where: { tournamentId, classId: classId },
                    include: [
                        { model: TournamentMember, as: 'member1', include: [{ model: Member, as: 'member' }] },
                        { model: TournamentMember, as: 'member2', include: [{ model: Member, as: 'member' }] },
                        { model: ExternalTournamentParticipant, as: 'external1' },
                        { model: ExternalTournamentParticipant, as: 'external2' }
                    ]
                });
                
                // Erstelle Items für jede Paarung
                itemsToDistribute = pairings.map(pairing => ({
                    type: 'pairing',
                    pairingId: pairing.id,
                    member1Id: pairing.member1Id,
                    external1Id: pairing.external1Id,
                    member2Id: pairing.member2Id,
                    external2Id: pairing.external2Id
                }));
            } else {
                // Bei Einzel: Verteile einzelne Spieler
                itemsToDistribute = classMembers.map(m => ({
                    type: 'member',
                    memberId: m.id,
                    isExternal: m.isExternal
                }));
            }

            if (itemsToDistribute.length === 0) continue;

            // Alle Items zufällig mischen
            for (let i = itemsToDistribute.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [itemsToDistribute[i], itemsToDistribute[j]] = [itemsToDistribute[j], itemsToDistribute[i]];
            }
            
            // Berechne gleichmäßige Verteilung
            const numGroups = groups.length;
            const itemsPerGroup = Math.floor(itemsToDistribute.length / numGroups);
            const remainder = itemsToDistribute.length % numGroups;
            
            // Verteile gleichmäßig
            const groupAssignments = groups.map(() => []);
            let itemIndex = 0;
            
            for (let groupIdx = 0; groupIdx < numGroups; groupIdx++) {
                const itemsForThisGroup = itemsPerGroup + (groupIdx < remainder ? 1 : 0);
                for (let i = 0; i < itemsForThisGroup; i++) {
                    if (itemIndex < itemsToDistribute.length) {
                        groupAssignments[groupIdx].push(itemsToDistribute[itemIndex]);
                        itemIndex++;
                    }
                }
            }
            
            // Bei Einzel-Klassen: Optimiere die Verteilung der gesetzten Spieler
            if (!isDoubles) {
                // Lade Member-Daten für gesetzte Spieler
                const memberIds = itemsToDistribute.filter(item => item.type === 'member').map(item => item.memberId);
                const membersData = await TournamentMember.findAll({
                    where: { id: { [Op.in]: memberIds }, tournamentId }
                });
                const externalIds = itemsToDistribute.filter(item => item.type === 'member' && item.isExternal).map(item => item.memberId);
                const externalsData = externalIds.length > 0 ? await ExternalTournamentParticipant.findAll({
                    where: { id: { [Op.in]: externalIds }, tournamentId }
                }) : [];
                
                const memberDataMap = new Map();
                membersData.forEach(m => memberDataMap.set(m.id, m));
                externalsData.forEach(e => memberDataMap.set(e.id, e));
                
                const seededCounts = groupAssignments.map(group => 
                    group.filter(item => {
                        if (item.type !== 'member') return false;
                        const memberData = memberDataMap.get(item.memberId);
                        return memberData && memberData.seeded;
                    }).length
                );
                
                // Wichtig: Gruppengrößen dürfen dabei NICHT aus dem Gleichgewicht geraten.
                // Deshalb nur 1:1 Swaps (seeded <-> non-seeded) zwischen Gruppen.
                for (let round = 0; round < 20; round++) {
                    let changed = false;
                    let maxSeededIdx = 0;
                    let maxSeededCount = seededCounts[0];
                    for (let i = 1; i < numGroups; i++) {
                        if (seededCounts[i] > maxSeededCount) {
                            maxSeededCount = seededCounts[i];
                            maxSeededIdx = i;
                        }
                    }
                    
                    let minSeededIdx = 0;
                    let minSeededCount = seededCounts[0];
                    for (let i = 1; i < numGroups; i++) {
                        if (seededCounts[i] < minSeededCount) {
                            minSeededCount = seededCounts[i];
                            minSeededIdx = i;
                        }
                    }
                    
                    if (maxSeededCount - minSeededCount > 1) {
                        const seededInMax = groupAssignments[maxSeededIdx].filter(item => {
                            if (item.type !== 'member') return false;
                            const memberData = memberDataMap.get(item.memberId);
                            return memberData && memberData.seeded;
                        });
                        const nonSeededInMin = groupAssignments[minSeededIdx].filter(item => {
                            if (item.type !== 'member') return false;
                            const memberData = memberDataMap.get(item.memberId);
                            return memberData && !memberData.seeded;
                        });

                        if (seededInMax.length > 0 && nonSeededInMin.length > 0) {
                            const seededToSwap = seededInMax[0];
                            const nonSeededToSwap = nonSeededInMin[0];

                            // Swap: seeded wandert in Min, non-seeded wandert in Max
                            groupAssignments[maxSeededIdx] = groupAssignments[maxSeededIdx].map(item =>
                                (item.type === 'member' && item.memberId === seededToSwap.memberId) ? nonSeededToSwap : item
                            );
                            groupAssignments[minSeededIdx] = groupAssignments[minSeededIdx].map(item =>
                                (item.type === 'member' && item.memberId === nonSeededToSwap.memberId) ? seededToSwap : item
                            );

                            seededCounts[maxSeededIdx] -= 1;
                            seededCounts[minSeededIdx] += 1;
                            changed = true;
                        } else {
                            // Ohne passenden Swap-Kandidaten können wir die Seeded-Verteilung nicht weiter verbessern.
                            break;
                        }
                    }
                    
                    if (!changed) break;
                }
            }
            
            // Speichere Zuordnungen
            groupAssignments.forEach((groupItems, groupIdx) => {
                groupItems.forEach(item => {
                    if (item.type === 'pairing') {
                        // Bei Paarungen: Aktualisiere die Paarung selbst und beide Spieler
                        allUpdatePromises.push(
                            TournamentPairing.update(
                                { groupId: groups[groupIdx].id },
                                { where: { id: item.pairingId } }
                            )
                        );
                        // Aktualisiere auch beide Spieler der Paarung
                        if (item.member1Id) {
                            allUpdatePromises.push(
                                TournamentMember.update(
                                    { groupId: groups[groupIdx].id },
                                    { where: { id: item.member1Id } }
                                )
                            );
                        }
                        if (item.external1Id) {
                            allUpdatePromises.push(
                                ExternalTournamentParticipant.update(
                                    { groupId: groups[groupIdx].id },
                                    { where: { id: item.external1Id } }
                                )
                            );
                        }
                        if (item.member2Id) {
                            allUpdatePromises.push(
                                TournamentMember.update(
                                    { groupId: groups[groupIdx].id },
                                    { where: { id: item.member2Id } }
                                )
                            );
                        }
                        if (item.external2Id) {
                            allUpdatePromises.push(
                                ExternalTournamentParticipant.update(
                                    { groupId: groups[groupIdx].id },
                                    { where: { id: item.external2Id } }
                                )
                            );
                        }
                    } else {
                        // Bei einzelnen Spielern
                        if (item.isExternal) {
                            allUpdatePromises.push(
                                ExternalTournamentParticipant.update(
                                    { groupId: groups[groupIdx].id },
                                    { where: { id: item.memberId } }
                                )
                            );
                        } else {
                            allUpdatePromises.push(
                                TournamentMember.update(
                                    { groupId: groups[groupIdx].id },
                                    { where: { id: item.memberId } }
                                )
                            );
                        }
                    }
                });
            });
        }
        
        await Promise.all(allUpdatePromises);

        // 6) Round‑Robin anlegen - NUR innerhalb jeder Gruppe
        // Lade alle Klassen, um zu prüfen, ob es sich um Doppel-Klassen handelt
        const tournamentClasses = await TournamentClass.findAll({ where: { tournamentId } });
        const classIsDoublesMap = tournamentClasses.reduce((map, cls) => {
            map[cls.id] = cls.isDoubles;
            return map;
        }, {});

        for (const g of allGroups) {
            const classId = g.classId;
            const isDoubles = classId ? (classIsDoublesMap[classId] || false) : false;
            
            if (isDoubles) {
                // Bei Doppel: Lade Paarungen für diese Gruppe
                const pairings = await TournamentPairing.findAll({
                    where: { tournamentId, classId: classId, groupId: g.id },
                    include: [
                        { model: TournamentMember, as: 'member1', include: [{ model: Member, as: 'member' }] },
                        { model: TournamentMember, as: 'member2', include: [{ model: Member, as: 'member' }] },
                        { model: ExternalTournamentParticipant, as: 'external1' },
                        { model: ExternalTournamentParticipant, as: 'external2' }
                    ]
                });
                
                if (pairings.length < 2) {
                    continue;
                }
                
                // Erstelle Round-Robin zwischen Paarungen
                // Verwende die erste Spieler-ID jeder Paarung als Repräsentant für das Match
                const pairingItems = pairings.map(p => ({
                    pairingId: p.id,
                    player1Id: p.member1Id || p.external1Id,
                    player2Id: p.member2Id || p.external2Id,
                    key: `pairing-${p.id}`
                }));
                
                const rounds = this.generateRoundRobinSchedule(pairingItems.map(p => ({ id: p.key })));
                
                for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
                    for (const [p1Key, p2Key] of rounds[roundIndex]) {
                        if (p1Key && p2Key) {
                            const pairing1 = pairingItems.find(p => p.key === p1Key);
                            const pairing2 = pairingItems.find(p => p.key === p2Key);
                            
                            if (pairing1 && pairing2) {
                                // Erstelle Match zwischen den beiden Paarungen
                                // Verwende die erste Spieler-ID jeder Paarung
                                // Die Match-Erkennung in getGroupsWithParticipants prüft, ob player1Id oder player2Id
                                // mit den Spieler-IDs der Paarungen übereinstimmen
                                await TournamentMatch.create({
                                    tournamentId,
                                    groupId: g.id,
                                    round: 'group',
                                    player1Id: pairing1.player1Id,
                                    player2Id: pairing2.player1Id,
                                    groupRound: roundIndex + 1,
                                    classId: g.classId
                                });
                            }
                        }
                    }
                }
            } else {
                // Bei Einzel: Normale Logik mit einzelnen Spielern
                // Lade interne und externe Teilnehmer der Gruppe
                const internalMembers = await TournamentMember.findAll({ where: { groupId: g.id } });
                const externalMembers = await ExternalTournamentParticipant.findAll({ where: { groupId: g.id } });
                
                // Kombiniere beide Listen mit eindeutigen zusammengesetzten Schlüsseln
                // um ID-Kollisionen zwischen TournamentMember und ExternalTournamentParticipant zu vermeiden
                const allGroupMembers = [
                    ...internalMembers.map(m => ({ id: m.id, isExternal: false, key: `internal-${m.id}` })),
                    ...externalMembers.map(m => ({ id: m.id, isExternal: true, key: `external-${m.id}` }))
                ];
                
                // Erstelle eine Map für schnelle Suche nach zusammengesetztem Schlüssel
                const memberMap = new Map();
                allGroupMembers.forEach(m => {
                    memberMap.set(m.key, m);
                });
                
                if (allGroupMembers.length < 2) {
                    continue;
                }
                
                // Für Round-Robin verwenden wir zusammengesetzte Schlüssel, um ID-Kollisionen zu vermeiden
                const rounds = this.generateRoundRobinSchedule(allGroupMembers.map(m => ({ id: m.key })));
                
                for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
                    for (const [p1Key, p2Key] of rounds[roundIndex]) {
                        if (p1Key && p2Key) {
                            // Finde die tatsächlichen Teilnehmer mit zusammengesetztem Schlüssel
                            const p1 = memberMap.get(p1Key);
                            const p2 = memberMap.get(p2Key);
                            
                            if (p1 && p2) {
                                await TournamentMatch.create({
                                    tournamentId,
                                    groupId: g.id,
                                    round: 'group',
                                    player1Id: p1.id,
                                    player2Id: p2.id,
                                    groupRound: roundIndex + 1,
                                    classId: g.classId
                                });
                            }
                        }
                    }
                }
            }
        }

        // Rückgabe: Lade sowohl interne als auch externe Teilnehmer
        // Ähnlich wie getGroupsWithParticipants, aber ohne Gruppierung
        const internalMembers = await TournamentMember.findAll({ 
            where: { tournamentId },
            include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName', 'gender'] }]
        });
        
        const externalMembers = await ExternalTournamentParticipant.findAll({
            where: { tournamentId }
        });
        
        // Kombiniere beide Listen für Rückgabe
        const allParticipants = [
            ...internalMembers.map(m => ({
                id: m.id,
                tournamentId: m.tournamentId,
                groupId: m.groupId,
                clubMemberId: m.clubMemberId,
                seeded: m.seeded,
                classId: m.classId,
                member: m.member,
                isExternal: false
            })),
            ...externalMembers.map(m => ({
                id: m.id,
                tournamentId: m.tournamentId,
                groupId: m.groupId,
                firstName: m.firstName,
                lastName: m.lastName,
                club: m.club,
                birthDate: m.birthDate,
                seeded: m.seeded,
                classId: m.classId,
                isExternal: true
            }))
        ];
        
        return allParticipants;
    }

    // Erstellt nur die Matches für bestehende Gruppen, ohne Gruppenzuordnungen zu ändern
    async createGroupMatches(userToken, clubId, tournamentId, classId = null) {
        await checkAccess(userToken, clubId);

        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }

        // Lade alle Gruppen, optional gefiltert nach classId
        const whereClause = { tournamentId };
        if (classId !== null && classId !== undefined) {
            whereClause.classId = classId;
        }
        
        const allGroups = await TournamentGroup.findAll({ 
            where: whereClause,
            order: [['id', 'ASC']]
        });

        if (allGroups.length === 0) {
            throw new Error('Keine Gruppen vorhanden.');
        }

        // Lösche nur Matches für die betroffenen Gruppen (optional gefiltert nach classId)
        const matchWhereClause = { 
            tournamentId, 
            round: 'group'
        };
        if (classId !== null && classId !== undefined) {
            matchWhereClause.classId = classId;
        }
        await TournamentMatch.destroy({ where: matchWhereClause });

        // Lade alle Klassen, um zu prüfen, ob es sich um Doppel-Klassen handelt
        const tournamentClasses = await TournamentClass.findAll({ where: { tournamentId } });
        const classIsDoublesMap = tournamentClasses.reduce((map, cls) => {
            map[cls.id] = cls.isDoubles;
            return map;
        }, {});

        for (const g of allGroups) {
            const groupClassId = g.classId;
            const isDoubles = groupClassId ? (classIsDoublesMap[groupClassId] || false) : false;
            
            if (isDoubles) {
                // Bei Doppel: Lade Paarungen für diese Gruppe
                const pairings = await TournamentPairing.findAll({
                    where: { tournamentId, classId: groupClassId, groupId: g.id },
                    include: [
                        { model: TournamentMember, as: 'member1', include: [{ model: Member, as: 'member' }] },
                        { model: TournamentMember, as: 'member2', include: [{ model: Member, as: 'member' }] },
                        { model: ExternalTournamentParticipant, as: 'external1' },
                        { model: ExternalTournamentParticipant, as: 'external2' }
                    ]
                });
                
                if (pairings.length < 2) {
                    continue;
                }
                
                // Erstelle Round-Robin zwischen Paarungen
                const pairingItems = pairings.map(p => ({
                    pairingId: p.id,
                    player1Id: p.member1Id || p.external1Id,
                    player2Id: p.member2Id || p.external2Id,
                    key: `pairing-${p.id}`
                }));
                
                const rounds = this.generateRoundRobinSchedule(pairingItems.map(p => ({ id: p.key })));
                
                for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
                    for (const [p1Key, p2Key] of rounds[roundIndex]) {
                        if (p1Key && p2Key) {
                            const pairing1 = pairingItems.find(p => p.key === p1Key);
                            const pairing2 = pairingItems.find(p => p.key === p2Key);
                            
                            if (pairing1 && pairing2) {
                                await TournamentMatch.create({
                                    tournamentId,
                                    groupId: g.id,
                                    round: 'group',
                                    player1Id: pairing1.player1Id,
                                    player2Id: pairing2.player1Id,
                                    groupRound: roundIndex + 1,
                                    classId: g.classId
                                });
                            }
                        }
                    }
                }
            } else {
                // Bei Einzel: Normale Logik mit einzelnen Spielern
                const internalMembers = await TournamentMember.findAll({ where: { groupId: g.id } });
                const externalMembers = await ExternalTournamentParticipant.findAll({ where: { groupId: g.id } });
                
                const allGroupMembers = [
                    ...internalMembers.map(m => ({ id: m.id, isExternal: false, key: `internal-${m.id}` })),
                    ...externalMembers.map(m => ({ id: m.id, isExternal: true, key: `external-${m.id}` }))
                ];
                
                const memberMap = new Map();
                allGroupMembers.forEach(m => {
                    memberMap.set(m.key, m);
                });
                
                if (allGroupMembers.length < 2) {
                    continue;
                }
                
                const rounds = this.generateRoundRobinSchedule(allGroupMembers.map(m => ({ id: m.key })));
                
                for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
                    for (const [p1Key, p2Key] of rounds[roundIndex]) {
                        if (p1Key && p2Key) {
                            const p1 = memberMap.get(p1Key);
                            const p2 = memberMap.get(p2Key);
                            
                            if (p1 && p2) {
                                await TournamentMatch.create({
                                    tournamentId,
                                    groupId: g.id,
                                    round: 'group',
                                    player1Id: p1.id,
                                    player2Id: p2.id,
                                    groupRound: roundIndex + 1,
                                    classId: g.classId
                                });
                            }
                        }
                    }
                }
            }
        }
    }

    async getGroups(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        return await TournamentGroup.findAll({ where: { tournamentId } });
    }

    // 9. Gruppen mit ihren Teilnehmern
    // services/tournamentService.js

    async getGroupsWithParticipants(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
    let groups = await TournamentGroup.findAll({
            where: { tournamentId },
            include: [{
                model: TournamentMember,
                as: 'tournamentGroupMembers',
                include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName', 'gender'] }]
            }, {
                model: ExternalTournamentParticipant,
                as: 'externalGroupMembers'
            }],
            order: [
                [literal('CASE WHEN `TournamentGroup`.`class_id` IS NULL THEN 1 ELSE 0 END'), 'ASC'],
                [literal('`TournamentGroup`.`class_id`'), 'ASC'],
                ['id', 'ASC']
            ]
        });

        // Lade alle Gruppen-Matches mit Results für Rankings
        const groupMatches = await TournamentMatch.findAll({
            where: { tournamentId, round: 'group' },
            include: [{ model: TournamentResult, as: 'tournamentResults' }]
        });

        // Filtere nur KO‑Container‑Gruppen heraus: erkenne sie über Stage‑Typen.
        // Vorrunden‑Gruppen bleiben sichtbar, auch wenn (noch) keine Matches existieren.
        const stages = await TournamentStage.findAll({ where: { tournamentId } });
        const stageById = new Map(stages.map(s => [Number(s.id), s]));
        const knockoutStageIds = new Set(stages.filter(s => s.type === 'knockout').map(s => s.id));
        groups = groups.filter(g => !(g.stageId && knockoutStageIds.has(g.stageId)));

        // Gruppiere nach Klassen und nummeriere Gruppen pro Klasse
        const groupsByClass = {};
        groups.forEach(g => {
            const classKey = g.classId || 'null';
            if (!groupsByClass[classKey]) {
                groupsByClass[classKey] = [];
            }
            groupsByClass[classKey].push(g);
        });

        const result = [];
        for (const [classKey, classGroups] of Object.entries(groupsByClass)) {
            // Prüfe, ob diese Klasse ein Doppel ist
            const classId = classKey === 'null' ? null : parseInt(classKey);
            const tournamentClass = classId ? await TournamentClass.findOne({
                where: { id: classId, tournamentId }
            }) : null;
            const isDoubles = tournamentClass ? tournamentClass.isDoubles : false;
            
            // Lade Paarungen für diese Klasse, falls Doppel
            let pairingsByGroup = {};
            if (isDoubles && classId) {
                const pairings = await TournamentPairing.findAll({
                    where: { tournamentId, classId: classId },
                    include: [
                        { model: TournamentMember, as: 'member1', include: [{ model: Member, as: 'member' }] },
                        { model: TournamentMember, as: 'member2', include: [{ model: Member, as: 'member' }] },
                        { model: ExternalTournamentParticipant, as: 'external1' },
                        { model: ExternalTournamentParticipant, as: 'external2' }
                    ]
                });
                
                // Gruppiere Paarungen nach groupId
                pairings.forEach(pairing => {
                    if (pairing.groupId) {
                        if (!pairingsByGroup[pairing.groupId]) {
                            pairingsByGroup[pairing.groupId] = [];
                        }
                        pairingsByGroup[pairing.groupId].push(pairing);
                    }
                });
            }
            
            classGroups.forEach((g, idx) => {
                // Berechne Rankings für diese Gruppe
                const stats = {};
                
                if (isDoubles && pairingsByGroup[g.id]) {
                    // Bei Doppel: Verwende Paarungen
                    for (const pairing of pairingsByGroup[g.id]) {
                        const player1Name = pairing.member1?.member 
                            ? `${pairing.member1.member.firstName} ${pairing.member1.member.lastName}`
                            : pairing.external1 
                                ? `${pairing.external1.firstName} ${pairing.external1.lastName}`
                                : 'Unbekannt';
                        const player2Name = pairing.member2?.member 
                            ? `${pairing.member2.member.firstName} ${pairing.member2.member.lastName}`
                            : pairing.external2 
                                ? `${pairing.external2.firstName} ${pairing.external2.lastName}`
                                : 'Unbekannt';
                        
                        stats[`pairing_${pairing.id}`] = {
                            id: `pairing_${pairing.id}`,
                            pairingId: pairing.id,
                            name: `${player1Name} / ${player2Name}`,
                            seeded: pairing.seeded || false,
                            isExternal: false,
                            isPairing: true,
                            player1Id: pairing.member1Id || pairing.external1Id,
                            player2Id: pairing.member2Id || pairing.external2Id,
                            points: 0,
                            setsWon: 0,
                            setsLost: 0,
                            pointsWon: 0,
                            pointsLost: 0,
                            pointRatio: 0,
                            matchesWon: 0,
                            matchesLost: 0
                        };
                    }
                } else {
                    // Bei Einzel: Verwende einzelne Spieler
                    // Interne Teilnehmer
                    for (const tm of g.tournamentGroupMembers || []) {
                        stats[tm.id] = {
                            id: tm.id,
                            clubMemberId: tm.clubMemberId,
                            name: `${tm.member.firstName} ${tm.member.lastName}`,
                            seeded: tm.seeded || false,
                            isExternal: false,
                            points: 0,
                            setsWon: 0,
                            setsLost: 0,
                            pointsWon: 0,
                            pointsLost: 0,
                            pointRatio: 0,
                            matchesWon: 0,
                            matchesLost: 0
                        };
                    }
                    
                    // Externe Teilnehmer
                    for (const ext of g.externalGroupMembers || []) {
                        stats[ext.id] = {
                            id: ext.id,
                            name: `${ext.firstName} ${ext.lastName}`,
                            seeded: ext.seeded || false,
                            isExternal: true,
                            points: 0,
                            setsWon: 0,
                            setsLost: 0,
                            pointsWon: 0,
                            pointsLost: 0,
                            pointRatio: 0,
                            matchesWon: 0,
                            matchesLost: 0
                        };
                    }
                }
                
                // Berechne Statistiken aus Matches
                for (const m of groupMatches.filter(m => m.groupId === g.id)) {
                    if (isDoubles) {
                        // Bei Doppel: Finde die Paarungen für player1Id und player2Id
                        const pairing1Key = Object.keys(stats).find(key => 
                            stats[key].isPairing && 
                            (stats[key].player1Id === m.player1Id || stats[key].player2Id === m.player1Id)
                        );
                        const pairing2Key = Object.keys(stats).find(key => 
                            stats[key].isPairing && 
                            (stats[key].player1Id === m.player2Id || stats[key].player2Id === m.player2Id)
                        );
                        
                        if (!pairing1Key || !pairing2Key) continue;
                        // Ergebnis kann null/undefiniert oder in anderem Format sein -> defensiv prüfen
                        if (!m.result || typeof m.result !== 'string' || !m.result.includes(':')) continue;
                        const [s1, s2] = m.result.split(':').map(n => parseInt(n, 10));
                        
                        if (s1 > s2) {
                            stats[pairing1Key].points += 1;
                            stats[pairing1Key].matchesWon += 1;
                            stats[pairing2Key].points -= 1;
                            stats[pairing2Key].matchesLost += 1;
                        } else if (s2 > s1) {
                            stats[pairing2Key].points += 1;
                            stats[pairing2Key].matchesWon += 1;
                            stats[pairing1Key].points -= 1;
                            stats[pairing1Key].matchesLost += 1;
                        }
                        
                        stats[pairing1Key].setsWon += s1;
                        stats[pairing1Key].setsLost += s2;
                        stats[pairing2Key].setsWon += s2;
                        stats[pairing2Key].setsLost += s1;
                        
                        // Berechne gespielte Punkte aus tournamentResults
                        if (m.tournamentResults && m.tournamentResults.length > 0) {
                            let p1Points = 0, p2Points = 0;
                            for (const r of m.tournamentResults) {
                                // Verwende absolute Werte, falls negative Werte gespeichert wurden
                                p1Points += Math.abs(r.pointsPlayer1 || 0);
                                p2Points += Math.abs(r.pointsPlayer2 || 0);
                            }
                            stats[pairing1Key].pointsWon += p1Points;
                            stats[pairing1Key].pointsLost += p2Points;
                            stats[pairing2Key].pointsWon += p2Points;
                            stats[pairing2Key].pointsLost += p1Points;
                        }
                    } else {
                        // Bei Einzel: Normale Logik
                        if (!stats[m.player1Id] || !stats[m.player2Id]) continue;
                        // Ergebnis kann null/undefiniert oder in anderem Format sein -> defensiv prüfen
                        if (!m.result || typeof m.result !== 'string' || !m.result.includes(':')) continue;
                        const [s1, s2] = m.result.split(':').map(n => parseInt(n, 10));
                        
                        if (s1 > s2) {
                            stats[m.player1Id].points += 1;
                            stats[m.player1Id].matchesWon += 1;
                            stats[m.player2Id].points -= 1;
                            stats[m.player2Id].matchesLost += 1;
                        } else if (s2 > s1) {
                            stats[m.player2Id].points += 1;
                            stats[m.player2Id].matchesWon += 1;
                            stats[m.player1Id].points -= 1;
                            stats[m.player1Id].matchesLost += 1;
                        }
                        
                        stats[m.player1Id].setsWon += s1;
                        stats[m.player1Id].setsLost += s2;
                        stats[m.player2Id].setsWon += s2;
                        stats[m.player2Id].setsLost += s1;
                        
                        // Berechne gespielte Punkte aus tournamentResults
                        if (m.tournamentResults && m.tournamentResults.length > 0) {
                            let p1Points = 0, p2Points = 0;
                            for (const r of m.tournamentResults) {
                                // Verwende absolute Werte, falls negative Werte gespeichert wurden
                                p1Points += Math.abs(r.pointsPlayer1 || 0);
                                p2Points += Math.abs(r.pointsPlayer2 || 0);
                            }
                            stats[m.player1Id].pointsWon += p1Points;
                            stats[m.player1Id].pointsLost += p2Points;
                            stats[m.player2Id].pointsWon += p2Points;
                            stats[m.player2Id].pointsLost += p1Points;
                        }
                    }
                }
                
                // Berechne Punktverhältnis und absolute Differenz für jeden Spieler
                Object.values(stats).forEach(s => {
                    const totalPoints = s.pointsWon + s.pointsLost;
                    s.pointRatio = totalPoints > 0 ? s.pointsWon / totalPoints : 0;
                    s.setDiff = s.setsWon - s.setsLost;
                    s.pointsDiff = s.pointsWon - s.pointsLost; // Absolute Differenz der gespielten Punkte
                });
                
                // Sortiere nach: Punkte -> Satzverhältnis -> mehr gewonnene Sätze -> absolute Punktdifferenz -> mehr erzielte Spielpunkte -> direkter Vergleich
                const ranked = Object.values(stats).sort((a, b) => {
                    // 1. Beste Punkte
                    if (b.points !== a.points) return b.points - a.points;
                    // 2. Besseres Satzverhältnis
                    if (b.setDiff !== a.setDiff) return b.setDiff - a.setDiff;
                    // 3. Bei Satzgleichheit: Wer mehr Sätze gewonnen hat
                    if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon;
                    // 4. Bessere absolute Differenz der gespielten Punkte (höhere Differenz zuerst)
                    if (b.pointsDiff !== a.pointsDiff) return b.pointsDiff - a.pointsDiff;
                    // 5. Bei Spielpunktgleichheit: Wer mehr Spielpunkte erzielt hat
                    if (b.pointsWon !== a.pointsWon) return b.pointsWon - a.pointsWon;
                    // 6. Direkter Vergleich (Sieger weiter oben)
                    let directMatch;
                    if (isDoubles && a.isPairing && b.isPairing) {
                        // Bei Doppel: Finde Match zwischen den Paarungen
                        directMatch = groupMatches.find(m => {
                            const aPlayer1 = a.player1Id === m.player1Id || a.player2Id === m.player1Id;
                            const aPlayer2 = a.player1Id === m.player2Id || a.player2Id === m.player2Id;
                            const bPlayer1 = b.player1Id === m.player1Id || b.player2Id === m.player1Id;
                            const bPlayer2 = b.player1Id === m.player2Id || b.player2Id === m.player2Id;
                            return m.groupId === g.id && ((aPlayer1 && bPlayer2) || (aPlayer2 && bPlayer1));
                        });
                        if (directMatch) {
                            if (directMatch.result && typeof directMatch.result === 'string' && directMatch.result.includes(':')) {
                                const [s1, s2] = directMatch.result.split(':').map(n => parseInt(n, 10));
                                const aPlayer1 = a.player1Id === directMatch.player1Id || a.player2Id === directMatch.player1Id;
                                const aWon = aPlayer1 ? (s1 > s2) : (s2 > s1);
                                if (aWon) return -1;
                                return 1;
                            }
                        }
                    } else if (!isDoubles) {
                        directMatch = groupMatches.find(m =>
                            m.groupId === g.id &&
                            ((m.player1Id === a.id && m.player2Id === b.id) ||
                             (m.player1Id === b.id && m.player2Id === a.id))
                        );
                        if (directMatch) {
                            if (directMatch.result && typeof directMatch.result === 'string' && directMatch.result.includes(':')) {
                                const [s1, s2] = directMatch.result.split(':').map(n => parseInt(n, 10));
                                const aWon = (directMatch.player1Id === a.id && s1 > s2) ||
                                             (directMatch.player2Id === a.id && s2 > s1);
                                if (aWon) return -1; // a hat gewonnen -> a kommt weiter oben
                                return 1; // b hat gewonnen -> b kommt weiter oben
                            }
                        }
                    }
                    // Fallback: Alphabetisch nach Name
                    return a.name.localeCompare(b.name);
                });
                
                
                // Weise Positionen zu, wobei Spieler mit identischen Werten den gleichen Platz bekommen
                let currentPosition = 1;
                const participantsWithPosition = ranked.map((p, i) => {
                    if (i > 0) {
                        const prev = ranked[i - 1];
                        // Prüfe, ob alle Sortierkriterien identisch sind
                        const pointsEqual = prev.points === p.points;
                        const setDiffEqual = prev.setDiff === p.setDiff;
                        const setsWonEqual = prev.setsWon === p.setsWon;
                        const pointsDiffEqual = prev.pointsDiff === p.pointsDiff;
                        const pointsWonEqual = prev.pointsWon === p.pointsWon;
                        
                        if (pointsEqual && setDiffEqual && setsWonEqual && pointsDiffEqual && pointsWonEqual) {
                            // Prüfe direkten Vergleich
                            let directMatch;
                            if (isDoubles && prev.isPairing && p.isPairing) {
                                // Bei Doppel: Finde Match zwischen den Paarungen
                                directMatch = groupMatches.find(m => {
                                    const prevPlayer1 = prev.player1Id === m.player1Id || prev.player2Id === m.player1Id;
                                    const prevPlayer2 = prev.player1Id === m.player2Id || prev.player2Id === m.player2Id;
                                    const pPlayer1 = p.player1Id === m.player1Id || p.player2Id === m.player1Id;
                                    const pPlayer2 = p.player1Id === m.player2Id || p.player2Id === m.player2Id;
                                    return m.groupId === g.id && ((prevPlayer1 && pPlayer2) || (prevPlayer2 && pPlayer1));
                                });
                            } else if (!isDoubles) {
                                directMatch = groupMatches.find(m =>
                                    m.groupId === g.id &&
                                    ((m.player1Id === prev.id && m.player2Id === p.id) ||
                                     (m.player1Id === p.id && m.player2Id === prev.id))
                                );
                            }
                            if (!directMatch ||
                                !directMatch.result || typeof directMatch.result !== 'string' || !directMatch.result.includes(':') ||
                                directMatch.result.split(':').map(n => +n)[0] === directMatch.result.split(':').map(n => +n)[1]) {
                                // Gleicher Platz wie Vorgänger (unentschieden oder kein direktes Match)
                                return {
                                    ...p,
                                    position: currentPosition,
                                    setDiff: p.setDiff,
                                    pointsDiff: p.pointsDiff
                                };
                            }
                        }
                    }
                    // Neuer Platz
                    currentPosition = i + 1;
                    return {
                        ...p,
                        position: currentPosition,
                        setDiff: p.setDiff,
                        pointsDiff: p.pointsDiff
                    };
                });

                const stage = g.stageId ? stageById.get(Number(g.stageId)) : null;
                const sameStageGroups = classGroups.filter(other => (other.stageId ?? null) === (g.stageId ?? null));
                const stageGroupIndex = sameStageGroups.findIndex(other => Number(other.id) === Number(g.id));
                const groupNumber = stageGroupIndex >= 0 ? stageGroupIndex + 1 : idx + 1;
                const stageLabel = stage
                    ? (stage.name || (Number(stage.index) === 3 ? 'Endrunde' : 'Zwischenrunde'))
                    : null;

                result.push({
                    groupId: g.id,
                    classId: g.classId,
                    stageId: g.stageId ?? null,
                    stageIndex: stage?.index ?? null,
                    stageName: stageLabel,
                    groupNumber,
                    groupLabel: stageLabel ? `${stageLabel}-Gruppe ${groupNumber}` : null,
                    participants: participantsWithPosition
                });
            });
        }

        return result;
    }


    // 10. Einzelnes Turnier
    async getTournament(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);
        const t = await Tournament.findOne({ where: { id: tournamentId, clubId } });
        if (!t) throw new Error('Turnier nicht gefunden');
        // Stelle sicher, dass winningSets vorhanden ist (für alte Turniere)
        // Nur setzen, wenn es wirklich null/undefined ist (nicht 0 oder andere Werte)
        if (t.winningSets == null) {
            t.winningSets = 3;
            await t.save();
        }
        return JSON.parse(JSON.stringify(t));
    }

    // Update Turnier (Name, Datum, Gewinnsätze und Tischanzahl)
    async updateTournament(userToken, clubId, tournamentId, name, date, winningSets, numberOfTables) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findOne({ where: { id: tournamentId, clubId } });
        if (!tournament) {
            throw new Error('Turnier nicht gefunden');
        }
        
        // Prüfe auf Duplikat, wenn Datum geändert wird
        if (date && date !== tournament.date) {
            const existing = await Tournament.findOne({ where: { clubId, date, id: { [Op.ne]: tournamentId } } });
            if (existing) {
                throw new Error('Ein Turnier mit diesem Datum existiert bereits');
            }
        }
        
        if (name !== undefined) tournament.name = name;
        if (date !== undefined) tournament.date = date;
        if (winningSets !== undefined) {
            if (winningSets < 1) {
                throw new Error('Anzahl der Gewinnsätze muss mindestens 1 sein');
            }
            tournament.winningSets = winningSets;
        }
        if (numberOfTables !== undefined) {
            if (numberOfTables !== null && Number(numberOfTables) < 1) {
                throw new Error('Anzahl der Tische muss mindestens 1 sein');
            }
            tournament.numberOfTables = numberOfTables != null ? Number(numberOfTables) : null;
        }
        
        await tournament.save();
        return JSON.parse(JSON.stringify(tournament));
    }

    // 11. Spiele eines Turniers
    async getTournamentMatches(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);
        const t = await Tournament.findOne({ where: { id: tournamentId, clubId } });
        if (!t) throw new Error('Turnier nicht gefunden');
        // 1) Matches ohne Spieler laden (damit Sequelize nicht automatisch falsche TournamentMember matched)
        let matches = await TournamentMatch.findAll({
            where: { tournamentId },
            include: [
                { model: TournamentResult, as: 'tournamentResults' }
            ],
            order: [
                ['group_round', 'ASC'],  // Zuerst nach Runde sortieren
                // group_id wird nicht sortiert, da die logische groupNumber im Frontend verwendet wird
                ['id', 'ASC'],
                [{ model: TournamentResult, as: 'tournamentResults' }, 'set', 'ASC']
            ]
        });

        // DB-Order kann die gewünschte KO-Reihenfolge (Platz 3 vor Finale) nicht sauber ausdrücken.
        // Wir sortieren deshalb nur innerhalb derselben group_round entsprechend nach.
        matches = [...matches].sort((a, b) => {
            const grA = a.groupRound ?? a.group_round ?? -1;
            const grB = b.groupRound ?? b.group_round ?? -1;
            if (grA !== grB) return grA - grB;

            const rank = (m) => {
                if (m.round === THIRD_PLACE_ROUND) return 98;
                if (m.round === 'Finale') return 99;
                return 0;
            };

            const rA = rank(a);
            const rB = rank(b);
            if (rA !== rB) return rA - rB;
            return (a.id ?? 0) - (b.id ?? 0);
        });

        // 2) Alle im Turnier verwendeten Spieler-IDs einsammeln
        const allPlayerIds = new Set();
        for (const m of matches) {
            if (m.player1Id) allPlayerIds.add(m.player1Id);
            if (m.player2Id) allPlayerIds.add(m.player2Id);
        }

        const idsArray = Array.from(allPlayerIds);

        // Wenn gar keine Spieler-IDs vorhanden sind, können wir direkt zurückgeben
        if (idsArray.length === 0) {
            const plain = matches.map(m => m.toJSON());
            return plain;
        }

        // 3) Passende interne Turnierteilnehmer + Member laden
        const internalMembers = await TournamentMember.findAll({
            where: {
                tournamentId,
                id: { [Op.in]: idsArray }
            },
            include: [{ model: Member, as: 'member' }]
        });
        const internalById = new Map(internalMembers.map(m => [m.id, m]));

        // 4) Passende externe Teilnehmer laden
        const externalParticipants = await ExternalTournamentParticipant.findAll({
            where: {
                tournamentId,
                id: { [Op.in]: idsArray }
            }
        });
        const externalById = new Map(externalParticipants.map(e => [e.id, e]));

        // 5) Finale, saubere Struktur erzeugen
        const finalMatches = matches.map(m => {
            const plain = m.toJSON();

            // player1
            const p1Id = plain.player1Id;
            if (p1Id && internalById.has(p1Id)) {
                plain.player1 = internalById.get(p1Id).toJSON();
            } else if (p1Id && externalById.has(p1Id)) {
                const ep = externalById.get(p1Id);
                plain.player1 = {
                    id: ep.id,
                    firstName: ep.firstName,
                    lastName: ep.lastName,
                    club: ep.club,
                    gender: ep.gender,
                    birthDate: ep.birthDate
                };
            } else {
                plain.player1 = null;
            }

            // player2
            const p2Id = plain.player2Id;
            if (p2Id && internalById.has(p2Id)) {
                plain.player2 = internalById.get(p2Id).toJSON();
            } else if (p2Id && externalById.has(p2Id)) {
                const ep = externalById.get(p2Id);
                plain.player2 = {
                    id: ep.id,
                    firstName: ep.firstName,
                    lastName: ep.lastName,
                    club: ep.club,
                    gender: ep.gender,
                    birthDate: ep.birthDate
                };
            } else {
                plain.player2 = null;
            }

            return plain;
        });

        return finalMatches;
    }

    // 12. Satz-Ergebnis hinzufügen/überschreiben
    async addMatchResult(userToken, clubId, tournamentId, matchId, set, result) {
        await checkAccess(userToken, clubId);
        
        // Lade Turnier, um winningSets zu erhalten
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament) throw new Error('Turnier nicht gefunden');
        const winningSets = tournament.winningSets || 3; // Default: 3 Sätze
        
        // Lade Match, um isFinished zu prüfen
        const match = await TournamentMatch.findByPk(matchId);
        if (!match || match.tournamentId != tournamentId) throw new Error('Match nicht gefunden');
        
        const existing = await TournamentResult.findOne({ where: { matchId, set } });
        if (existing) {
            existing.pointsPlayer1 = +result.split(':')[0];
            existing.pointsPlayer2 = +result.split(':')[1];
            await existing.save();
        } else {
            const [p1, p2] = result.split(':').map(Number);
            await TournamentResult.create({ matchId, set, pointsPlayer1: p1, pointsPlayer2: p2 });
        }
        
        // Prüfe, ob ein Spieler die Gewinnsätze erreicht hat
        // Lade Match neu, um sicherzustellen, dass wir den aktuellen Status haben
        await match.reload();
        if (!match.isFinished) {
            const allResults = await TournamentResult.findAll({
                where: { matchId },
                order: [['set', 'ASC']]
            });
            
            let setsWonPlayer1 = 0;
            let setsWonPlayer2 = 0;
            
            for (const r of allResults) {
                if (r.pointsPlayer1 > r.pointsPlayer2) {
                    setsWonPlayer1++;
                } else if (r.pointsPlayer2 > r.pointsPlayer1) {
                    setsWonPlayer2++;
                }
            }
            
            // Wenn ein Spieler die Gewinnsätze erreicht hat, schließe das Match automatisch ab
            if (setsWonPlayer1 >= winningSets || setsWonPlayer2 >= winningSets) {
                await this.finishMatch(userToken, clubId, tournamentId, matchId);
            }
        }
    }

    async finishMatch(userToken, clubId, tournamentId, matchId) {
        await checkAccess(userToken, clubId);
        const match = await TournamentMatch.findByPk(matchId, {
            include: [{ model: TournamentResult, as: "tournamentResults" }]
        });
        if (!match) throw new Error("Match nicht gefunden");

        let win = 0, lose = 0;
        for (const r of match.tournamentResults) {
            if (r.pointsPlayer1 > r.pointsPlayer2) win++;
            else lose++;
        }
        match.isFinished = true;
        match.result = `${win}:${lose}`;
        // Wenn ein Match abgeschlossen wird, darf es nicht mehr als aktiv gelten
        match.isActive = false;
        // Freigebe den Tisch, damit er für neue Zuweisungen verfügbar ist
        match.tableNumber = null;
        await match.save();
        console.log(`[finishMatch] match ${matchId} finished, result=${match.result}, isActive set to false`);

        // Platz-3-Spiel (Legacy-KO ohne Stages): erst erzeugen, wenn beide Halbfinals fertig sind.
        // Keine Placeholders beim KO-Start.
        if (!match.stageId && match.round && String(match.round).includes('Halbfinale')) {
            const allKoMatchesSameClass = await TournamentMatch.findAll({
                where: {
                    tournamentId,
                    classId: match.classId,
                }
            });

            const semiMatches = allKoMatchesSameClass.filter(
                m => m.round && String(m.round).includes('Halbfinale')
            );
            const finishedSemis = semiMatches.filter(m => m.isFinished && m.result);

            if (finishedSemis.length >= 2) {
                const losers = finishedSemis
                    .map(getLoserId)
                    .filter(id => Number.isFinite(id) && id > 0);

                if (losers.length === 2) {
                    const existingThirdPlace = await TournamentMatch.findOne({
                        where: {
                            tournamentId,
                            classId: match.classId,
                            round: THIRD_PLACE_ROUND,
                        }
                    });

                    if (existingThirdPlace) {
                        if ((existingThirdPlace.player1Id == null || existingThirdPlace.player1Id === 0) && (existingThirdPlace.player2Id == null || existingThirdPlace.player2Id === 0)) {
                            existingThirdPlace.player1Id = losers[0];
                            existingThirdPlace.player2Id = losers[1];
                            existingThirdPlace.stageId = existingThirdPlace.stageId || match.stageId || null;
                            existingThirdPlace.groupId = existingThirdPlace.groupId || match.groupId || null;
                            existingThirdPlace.isActive = true;
                            await existingThirdPlace.save();
                        }
                    } else {
                        await TournamentMatch.create({
                            tournamentId,
                            stageId: match.stageId || null,
                            groupId: match.groupId || null,
                            round: THIRD_PLACE_ROUND,
                            player1Id: losers[0],
                            player2Id: losers[1],
                            classId: match.classId,
                            isFinished: false,
                            isActive: true,
                            result: null,
                        });
                    }
                }
            }
        }

    // Platz-3-Spiel automatisch setzen, sobald beide Halbfinals einer KO-Gruppe fertig sind.
    // Gilt nur für neue Stage-KO-Matches (stageId + groupId gesetzt).
    // Wichtig: Halbfinal-Rundennamen können Suffixe haben (z.B. "Halbfinale (3)").
    if (match.stageId && match.groupId && match.round && String(match.round).includes('Halbfinale')) {
            let thirdPlaceMatch = await TournamentMatch.findOne({
                where: {
                    tournamentId,
                    stageId: match.stageId,
                    groupId: match.groupId,
                    round: THIRD_PLACE_ROUND,
                }
            });

            const semifinals = await TournamentMatch.findAll({
                where: {
                    tournamentId,
                    stageId: match.stageId,
                    groupId: match.groupId,
                }
            });

            const semiMatches = semifinals.filter(m => m.round && String(m.round).includes('Halbfinale'));
            const finishedSemis = semiMatches.filter(m => m.isFinished && m.result);
            if (finishedSemis.length >= 2) {
                const losers = finishedSemis
                    .map(getLoserId)
                    .filter(id => Number.isFinite(id) && id > 0);

                if (losers.length === 2) {
                    if (!thirdPlaceMatch) {
                        thirdPlaceMatch = await TournamentMatch.create({
                            tournamentId,
                            stageId: match.stageId,
                            groupId: match.groupId,
                            classId: match.classId,
                            groupRound: null,
                            round: THIRD_PLACE_ROUND,
                            player1Id: losers[0],
                            player2Id: losers[1],
                            isFinished: false,
                            isActive: true,
                            result: null,
                        });
                    } else if ((thirdPlaceMatch.player1Id == null || thirdPlaceMatch.player1Id === 0) && (thirdPlaceMatch.player2Id == null || thirdPlaceMatch.player2Id === 0)) {
                        thirdPlaceMatch.player1Id = losers[0];
                        thirdPlaceMatch.player2Id = losers[1];
                        thirdPlaceMatch.isActive = true;
                        await thirdPlaceMatch.save();
                    }
                }
            }
        }

        // Prüfe, ob alle Matches dieser Runde UND Klasse abgeschlossen sind
        const roundWhere = { tournamentId, round: match.round, classId: match.classId };
        if (match.stageId) roundWhere.stageId = match.stageId;
        if (match.groupId) roundWhere.groupId = match.groupId;
        const allFinished = await TournamentMatch.count({
            where: { ...roundWhere, isFinished: false }
        }) === 0;

        if (allFinished) {
            // Lade alle Matches dieser Runde UND Klasse innerhalb desselben KO-Felds
            const sameRound = await TournamentMatch.findAll({
                where: roundWhere
            });
            
            // Gruppiere nach Klasse
            const winnersByClass = {};
            sameRound.forEach(m => {
                if (!m || !m.result) return;
                let winnerId = null;
                // BYE: Spieler1 ist automatisch Sieger
                if (String(m.result).toUpperCase() === 'BYE') {
                    winnerId = Number(m.player1Id) || null;
                } else if (m.result.includes(':')) {
                    const parts = m.result.split(':');
                    const w1 = Number(parts[0]);
                    const w2 = Number(parts[1]);
                    if (Number.isFinite(w1) && Number.isFinite(w2)) {
                        winnerId = w1 > w2 ? m.player1Id : m.player2Id;
                    }
                }
                if (!winnerId) return;
                const classKey = m.classId || 'null';
                (winnersByClass[classKey] ||= []).push(Number(winnerId));
            });

            const nextName = nextRoundName(match.round);
            if (nextName) {
                // Drittplatz wird (Legacy) nach beiden Halbfinals als echtes Match erzeugt.
                // Kein Placeholder beim Übergang zum Finale.
                // (Stage-KO behandelt Drittplatz separat.)
                const shouldEnsureThirdPlace = false;

                // Erstelle nächste Runde pro Klasse
                for (const [classKey, winners] of Object.entries(winnersByClass)) {
                    const filtered = (winners || []).filter(id => Number.isFinite(id) && id > 0);
                    if (filtered.length < 2) continue; // zu wenige Gewinner

                    const classId = classKey !== 'null' ? parseInt(classKey) : null;
                    // Paarungen sequentiell (0-1, 2-3, ...) statt außen-innen, um Ordnung zu bewahren
                    for (let i = 0; i + 1 < filtered.length; i += 2) {
                        const p1 = filtered[i];
                        const p2 = filtered[i + 1];
                        if (!p1 || !p2 || p1 === p2) {
                            devLog(`[finishMatch] Skip invalid next-round pairing in ${nextName} for class ${classKey}: ${p1} vs ${p2}`);
                            continue;
                        }
                        await TournamentMatch.create({
                            tournamentId,
                            stageId: match.stageId || null,
                            groupId: match.groupId || null,
                            round: nextName,
                            player1Id: p1,
                            player2Id: p2,
                            classId
                        });
                    }
                }
            }
        }
    }

    async _determineQualifiers(tournamentId, tournament) {
        const groups = await TournamentGroup.findAll({
            where: { tournamentId },
            include: [
                { model: TournamentMember, as: "tournamentGroupMembers" },
                { model: ExternalTournamentParticipant, as: "externalGroupMembers" }
            ]
        });
        const groupMatches = await TournamentMatch.findAll({
            where: { tournamentId, round: "group", isFinished: true },
            include: [{ model: TournamentResult, as: "tournamentResults" }]
        });

        // Lade alle Klassen, um zu prüfen, ob es sich um Doppel-Klassen handelt
        const tournamentClasses = await TournamentClass.findAll({ where: { tournamentId } });
        const classIsDoublesMap = tournamentClasses.reduce((map, cls) => {
            map[cls.id] = cls.isDoubles;
            return map;
        }, {});

        const qualifiers = [];
        for (const g of groups) {
            const classId = g.classId;
            const isDoubles = classId ? (classIsDoublesMap[classId] || false) : false;
            const stats = {};
            // Interne Teilnehmer
            for (const tm of g.tournamentGroupMembers || []) {
                stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: false, matchesWon: 0, matchesLost: 0 };
            }
            // Externe Teilnehmer
            for (const ext of g.externalGroupMembers || []) {
                stats[ext.id] = { member: ext, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: true, matchesWon: 0, matchesLost: 0 };
            }
            for (const m of groupMatches.filter(m => m.groupId === g.id)) {
                if (!stats[m.player1Id] || !stats[m.player2Id]) continue;
                const [p1, p2] = m.result.split(":").map(n => parseInt(n, 10));
                if (p1 > p2) {
                    stats[m.player1Id].points += 1;  // Sieger bekommt +1
                    stats[m.player1Id].matchesWon += 1;
                    stats[m.player2Id].points -= 1;  // Verlierer bekommt -1
                    stats[m.player2Id].matchesLost += 1;
                } else {
                    stats[m.player2Id].points += 1;  // Sieger bekommt +1
                    stats[m.player2Id].matchesWon += 1;
                    stats[m.player1Id].points -= 1;  // Verlierer bekommt -1
                    stats[m.player1Id].matchesLost += 1;
                }
                stats[m.player1Id].setsWon += p1;
                stats[m.player1Id].setsLost += p2;
                stats[m.player2Id].setsWon += p2;
                stats[m.player2Id].setsLost += p1;
                
                // Berechne gespielte Punkte aus tournamentResults
                if (m.tournamentResults && m.tournamentResults.length > 0) {
                    let p1Points = 0, p2Points = 0;
                    for (const r of m.tournamentResults) {
                        // Verwende absolute Werte, falls negative Werte gespeichert wurden
                        p1Points += Math.abs(r.pointsPlayer1 || 0);
                        p2Points += Math.abs(r.pointsPlayer2 || 0);
                    }
                    stats[m.player1Id].pointsWon += p1Points;
                    stats[m.player1Id].pointsLost += p2Points;
                    stats[m.player2Id].pointsWon += p2Points;
                    stats[m.player2Id].pointsLost += p1Points;
                }
            }
            
            // Berechne Punktverhältnis und absolute Differenz für jeden Spieler
            Object.values(stats).forEach(s => {
                const totalPoints = s.pointsWon + s.pointsLost;
                s.pointRatio = totalPoints > 0 ? s.pointsWon / totalPoints : 0;
                s.pointsDiff = s.pointsWon - s.pointsLost; // Absolute Differenz der gespielten Punkte
            });
            
            const ranked = Object.values(stats).sort((a, b) => {
                const diffA = a.setsWon - a.setsLost;
                const diffB = b.setsWon - b.setsLost;
                // 1. Beste Punkte
                if (b.points !== a.points) return b.points - a.points;
                // 2. Besseres Satzverhältnis
                if (diffB !== diffA) return diffB - diffA;
                // 3. Bei Satzgleichheit: Wer mehr Sätze gewonnen hat
                if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon;
                // 4. Bessere absolute Differenz der gespielten Punkte (höhere Differenz zuerst)
                if (b.pointsDiff !== a.pointsDiff) return b.pointsDiff - a.pointsDiff;
                // 5. Bei Spielpunktgleichheit: Wer mehr Spielpunkte erzielt hat
                if (b.pointsWon !== a.pointsWon) return b.pointsWon - a.pointsWon;
                // 6. Direkter Vergleich (Sieger weiter oben)
                const directMatch = groupMatches.find(m => 
                    m.groupId === g.id &&
                    ((m.player1Id === a.member.id && m.player2Id === b.member.id) ||
                     (m.player1Id === b.member.id && m.player2Id === a.member.id))
                );
                if (directMatch) {
                    const [s1, s2] = directMatch.result.split(":").map(n => parseInt(n, 10));
                    const aWon = (directMatch.player1Id === a.member.id && s1 > s2) || 
                                 (directMatch.player2Id === a.member.id && s2 > s1);
                    if (aWon) return -1; // a hat gewonnen -> a kommt weiter oben
                    return 1; // b hat gewonnen -> b kommt weiter oben
                }
                // Fallback: Nach ID
                return a.member.id - b.member.id;
            });
            // Füge classId und groupId zur Gruppe hinzu
            // r.member ist entweder TournamentMember oder ExternalTournamentParticipant
            qualifiers.push(...ranked.slice(0, tournament.advancingPerGroup).map((r, position) => {
                const member = r.member;
                // Stelle sicher, dass id vorhanden ist
                if (!member || !member.id) {
                    devLog(`[_determineQualifiers] Warning: Member without id found in group ${g.id}`);
                    return null;
                }
                return {
                    id: member.id,
                    classId: g.classId,
                    groupId: g.id,
                    isExternal: r.isExternal || false,
                    position: position + 1 // 1-basierte Position innerhalb der Gruppe (1., 2., 3., etc.)
                };
            }).filter(q => q !== null));
        }
        return qualifiers;
    }

    async startKnockout(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);
        const t = await Tournament.findByPk(tournamentId);
        if (!t || t.clubId != clubId) throw new Error("Tournament not found");

    // Legacy-KO hat kein eigenes Persistenzfeld für Platz-3.
    // Wir erzeugen das Spiel automatisch, sobald ein KO mit mindestens Halbfinale (>=4 Qualifier) gestartet wird.

        if (t.type === "groups") {
            const unfinished = await TournamentMatch.count({
                where: { tournamentId, round: "group", isFinished: false }
            });
            if (unfinished > 0) {
                throw new Error(
                    "Turnier ist im Gruppenmodus, K.o.-Runde kann erst nach Abschluss aller Gruppenspiele gestartet werden."
                );
            }
        }

        const qualifiers = await this._determineQualifiers(tournamentId, t);
        if (qualifiers.length < 2) throw new Error("Zu wenige Qualifikanten für K.O.-Runde");

        devLog(`[startKnockout] Found ${qualifiers.length} qualifiers`);
        qualifiers.forEach((q, idx) => {
            devLog(`[startKnockout] Qualifier ${idx}: id=${q.id}, classId=${q.classId}, isExternal=${q.isExternal}`);
        });

        await TournamentMatch.destroy({
            where: { tournamentId, round: { [Op.ne]: "group" } }
        });

        // Gruppiere Qualifiers nach Klasse
        const qualifiersByClass = {};
        qualifiers.forEach(q => {
            const classKey = q.classId || 'null';
            if (!qualifiersByClass[classKey]) {
                qualifiersByClass[classKey] = [];
            }
            qualifiersByClass[classKey].push(q);
        });
        
        devLog(`[startKnockout] Qualifiers grouped by class:`, Object.keys(qualifiersByClass).map(k => `${k}: ${qualifiersByClass[k].length}`));
        
        // Erstelle Matches pro Klasse
        for (const [classKey, classQualifiers] of Object.entries(qualifiersByClass)) {
            const roundSize = classQualifiers.length;
            if (roundSize < 2) continue; // Überspringe Klassen mit weniger als 2 Qualifizierten
            
            const rn = getRoundName(roundSize);
            const classId = classKey !== 'null' ? parseInt(classKey) : null;

            // Sonderfall: 3 Qualifier => genau EIN "Halbfinale (3)"-Match + 1 Freilos.
            // Die generische Gruppen-Pairing-Logik versucht sonst ggf. 2 Matches zu erstellen.
            if (roundSize === 3) {
                // Deterministisch: Darf nicht von Gruppen abhängen.
                // Bye = bester Qualifier (Position 1 zuerst, dann kleinere ID als stabiler Tiebreaker)
                const sorted = [...classQualifiers].sort((a, b) => {
                    const pa = Number(a.position ?? 999);
                    const pb = Number(b.position ?? 999);
                    if (pa !== pb) return pa - pb;
                    return Number(a.id) - Number(b.id);
                });

                const bye = sorted[0];
                const p1 = sorted[1];
                const p2 = sorted[2];

                devLog(`[startKnockout] roundSize=3: assigning bye=${bye?.id} and creating 1 match ${p1?.id} vs ${p2?.id} for class ${classKey}`);

                if (p1?.id && p2?.id && p1.id !== p2.id) {
                    await TournamentMatch.create({
                        tournamentId,
                        round: rn,
                        player1Id: p1.id,
                        player2Id: p2.id,
                        classId: classId
                    });
                } else {
                    devLog(`[startKnockout] Warning: Could not create Halbfinale (3) match for class ${classKey}`);
                }

                // Bye wird nicht als Match persistiert (keine Placeholders).
                continue;
            }

            // Drittplatz wird erst nach beiden Halbfinals mit fixen Spielern erzeugt.
            
            // Gruppiere Qualifiers nach Gruppen
            const qualifiersByGroup = {};
            classQualifiers.forEach(q => {
                const groupKey = q.groupId || 'null';
                if (!qualifiersByGroup[groupKey]) {
                    qualifiersByGroup[groupKey] = [];
                }
                qualifiersByGroup[groupKey].push(q);
            });
            
            // Sortiere Qualifiers innerhalb jeder Gruppe nach Position (1., 2., 3., etc.)
            Object.keys(qualifiersByGroup).forEach(groupKey => {
                qualifiersByGroup[groupKey].sort((a, b) => a.position - b.position);
            });
            
            // Erstelle eine flache Liste aller Qualifiers, gruppiert nach Gruppen
            const groups = Object.keys(qualifiersByGroup);
            const advancingPerGroup = t.advancingPerGroup;
            
            // Erstelle Paarungen: 1. gegen letzter weitergekommener, 2. gegen vorletzter, etc.
            // Wichtig: Niemand darf gegen jemanden aus der eigenen Gruppe spielen
            const matches = [];
            const usedQualifiers = new Set();
            
            // Für jede Position (1., 2., 3., etc.)
            for (let pos = 1; pos <= advancingPerGroup; pos++) {
                // Finde alle Qualifiers mit dieser Position
                const qualifiersAtPosition = [];
                groups.forEach(groupKey => {
                    const groupQualifiers = qualifiersByGroup[groupKey];
                    const qualifierAtPos = groupQualifiers.find(q => q.position === pos);
                    if (qualifierAtPos && !usedQualifiers.has(qualifierAtPos.id)) {
                        qualifiersAtPosition.push(qualifierAtPos);
                    }
                });
                
                // Paare jeden Qualifier dieser Position mit dem entsprechenden Gegner
                // 1. Platz spielt gegen letzter weitergekommener Platz (z.B. bei 2 weiterkommenden: 1. gegen 2.)
                // 2. Platz spielt gegen vorletzter weitergekommener Platz (z.B. bei 2 weiterkommenden: 2. gegen 1.)
                const opponentPosition = advancingPerGroup - pos + 1;
                
                qualifiersAtPosition.forEach(qualifier => {
                    // Finde Gegner mit opponentPosition aus einer anderen Gruppe
                    let opponent = null;
                    for (const groupKey of groups) {
                        if (groupKey === qualifier.groupId.toString()) continue; // Nicht aus derselben Gruppe
                        
                        const groupQualifiers = qualifiersByGroup[groupKey];
                        const opponentCandidate = groupQualifiers.find(q => 
                            q.position === opponentPosition && !usedQualifiers.has(q.id)
                        );
                        
                        if (opponentCandidate) {
                            opponent = opponentCandidate;
                            break;
                        }
                    }
                    
                    // Falls kein Gegner gefunden, suche nach einem beliebigen Gegner aus einer anderen Gruppe
                    if (!opponent) {
                        for (const groupKey of groups) {
                            if (groupKey === qualifier.groupId.toString()) continue;
                            
                            const groupQualifiers = qualifiersByGroup[groupKey];
                            const opponentCandidate = groupQualifiers.find(q => !usedQualifiers.has(q.id));
                            
                            if (opponentCandidate) {
                                opponent = opponentCandidate;
                                break;
                            }
                        }
                    }
                    
                    if (opponent) {
                        matches.push({ player1: qualifier, player2: opponent });
                        usedQualifiers.add(qualifier.id);
                        usedQualifiers.add(opponent.id);
                    }
                });
            }

            // Falls Qualifiers übrig bleiben (ungerade Teilnehmerzahl / keine gültige Paarung möglich):
            // Freilos vergeben. Wir erzeugen KEIN Match mit doppelten Spielern.
            // Der Spieler mit Freilos wird in späteren Runden berücksichtigt, sobald dort (durch Ergebnisse)
            // echte Gegner feststehen. (Passt zur Vorgabe: keine Placeholder-Matches ohne bekannte Spieler.)
            const unusedQualifiers = classQualifiers.filter(q => q && q.id && !usedQualifiers.has(q.id));
            if (unusedQualifiers.length > 0) {
                devLog(`[startKnockout] Assigning ${unusedQualifiers.length} bye(s) for class ${classKey}:`, unusedQualifiers.map(q => q.id));
            }
            
            // Erstelle die Matches in der Datenbank
            for (const match of matches) {
                if (!match.player1 || !match.player2 || !match.player1.id || !match.player2.id) {
                    devLog(`[startKnockout] Warning: Invalid match pair for class ${classKey}`);
                    continue;
                }

                // Safety: niemals Spieler gegen sich selbst.
                if (match.player1.id === match.player2.id) {
                    devLog(`[startKnockout] Warning: Prevented self-match for class ${classKey}: ${match.player1.id}`);
                    continue;
                }
                
                try {
                    await TournamentMatch.create({
                        tournamentId,
                        round: rn,
                        player1Id: match.player1.id,
                        player2Id: match.player2.id,
                        classId: classId
                    });
                } catch (error) {
                    devLog(`[startKnockout] Error creating match: ${error.message}`);
                    throw error;
                }
            }

            // (kein Placeholder beim KO-Start)
        }
    }
    async manualAssignGroups(
        userToken,
        clubId,
        tournamentId,
        assignments,
        numberOfGroups,
        maxGroupSize
    ) {
        await checkAccess(userToken, clubId);

        // 1) Turnier und Teilnehmerzahl validieren
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const totalMembers = assignments.length;
        if (totalMembers === 0) { 
            throw new Error('Keine Teilnehmer zum Verteilen');
        }

        // 2) Hole bestehende Gruppen und erstelle nur neue, wenn nötig
        const existingGroups = await TournamentGroup.findAll({
            where: { tournamentId },
            order: [['id', 'ASC']]
        });
        
        // Berechne die maximale groupNumber aus den assignments
        const maxGroupNumber = assignments.reduce((max, a) => {
            if (a.groupNumber != null && a.groupNumber > max) {
                return a.groupNumber;
            }
            return max;
        }, 0);
        
        // Erstelle nur neue Gruppen, wenn die maximale groupNumber größer ist als die Anzahl der bestehenden Gruppen
        const existingGroupCount = existingGroups.length;
        let groupCount = Math.max(existingGroupCount, maxGroupNumber);
        
        // Wenn numberOfGroups oder maxGroupSize gesetzt sind, verwende diese
        if (numberOfGroups != null) {
            groupCount = Number(numberOfGroups);
            if (isNaN(groupCount) || groupCount < 1) {
                throw new Error('Ungültige Anzahl Gruppen');
            }
        } else if (maxGroupSize != null) {
            const sz = Number(maxGroupSize);
            if (isNaN(sz) || sz < 1) {
                throw new Error('Ungültige maximale Gruppengröße');
            }
            groupCount = Math.max(groupCount, Math.ceil(totalMembers / sz));
        }
        
        console.log(`[manualAssignGroups] Existing groups: ${existingGroupCount}, maxGroupNumber: ${maxGroupNumber}, target groupCount: ${groupCount}`);

        // 3) Erstelle nur fehlende Gruppen
        const createdGroups = [...existingGroups];
        for (let i = existingGroupCount; i < groupCount; i++) {
            const grp = await TournamentGroup.create({ tournamentId });
            createdGroups.push(grp);
            console.log(`[manualAssignGroups] Created new group ${i + 1} with id ${grp.id}`);
        }

        // 4) Mapping von UI‑Nummer (1…groupCount) auf reale DB‑ID
        const groupMap = {};
        createdGroups.forEach((grp, idx) => {
            groupMap[idx + 1] = grp.id;
        });
        
        console.log(`[manualAssignGroups] Group mapping:`, groupMap);

        // 5) Teilnehmer updaten (sowohl interne als auch externe)
        await Promise.all(
            assignments.map(async ({ participantId, groupNumber, isExternal }) => {
                console.log(`[manualAssignGroups] Processing assignment: participantId=${participantId}, groupNumber=${groupNumber}, isExternal=${isExternal}`);
                console.log(`[manualAssignGroups] Group map:`, JSON.stringify(groupMap));
                
                // Wenn groupNumber null ist, entferne den Teilnehmer aus der Gruppe (groupId = null)
                const dbGroupId = groupNumber != null ? groupMap[groupNumber] : null;
                console.log(`[manualAssignGroups] Calculated dbGroupId: ${dbGroupId} (from groupNumber ${groupNumber})`);
                
                if (groupNumber != null && !dbGroupId) {
                    throw new Error(`Ungültige Gruppen‑Nummer: ${groupNumber}`);
                }
                
                console.log(`[manualAssignGroups] Updating participant ${participantId}, isExternal: ${isExternal}, groupNumber: ${groupNumber}, dbGroupId: ${dbGroupId}`);
                
                // Prüfe zuerst, ob es ein interner Teilnehmer ist (wenn isExternal nicht explizit true ist)
                if (isExternal !== true) {
                    const internalMember = await TournamentMember.findOne({
                        where: { id: participantId, tournamentId }
                    });
                    
                    if (internalMember) {
                        // Interner Teilnehmer
                        console.log(`[manualAssignGroups] Updating internal member id=${participantId} (memberId=${internalMember.memberId}) to groupId ${dbGroupId}`);
                        await TournamentMember.update(
                            { groupId: dbGroupId },
                            { where: { id: participantId, tournamentId } }
                        );
                        return;
                    }
                }
                
                // Versuche externen Teilnehmer
                const externalMember = await ExternalTournamentParticipant.findOne({
                    where: { id: participantId, tournamentId }
                });
                
                if (externalMember) {
                    // Externer Teilnehmer
                    console.log(`[manualAssignGroups] Updating external member id=${participantId} to groupId ${dbGroupId}`);
                    await ExternalTournamentParticipant.update(
                        { groupId: dbGroupId },
                        { where: { id: participantId, tournamentId } }
                    );
                } else {
                    throw new Error(`Teilnehmer mit ID ${participantId} nicht gefunden`);
                }
            })
        );

        // 6) Ergebnis zurückliefern wie getGroupsWithParticipants (mit vollständigen Rankings)
        // Verwende die gleiche Methode wie getGroupsWithParticipants, um konsistente Daten zu liefern
        return await this.getGroupsWithParticipants(userToken, clubId, tournamentId);
    }

    /**
     * Ordne einen einzelnen Teilnehmer einer Gruppe zu
     * @param {string} userToken - User authentication token
     * @param {number} clubId - Club ID
     * @param {number} tournamentId - Tournament ID
     * @param {number} participantId - Participant ID
     * @param {number|null} groupNumber - Group number (1-based) or null to remove from group
     * @param {boolean} isExternal - Whether the participant is external
     * @returns {Promise<Object>} Updated groups with participants
     */
    async assignParticipantToGroup(userToken, clubId, tournamentId, participantId, groupNumber, isExternal = false) {
        console.log(`[assignParticipantToGroup] Called with: participantId=${participantId}, groupNumber=${groupNumber}, isExternal=${isExternal}, tournamentId=${tournamentId}, clubId=${clubId}`);
        await checkAccess(userToken, clubId);
        
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }

        // Hole zunächst den Teilnehmer, um seine classId zu erhalten
        let participantClassId = null;
        let internalMember = null;
        let externalMember = null;
        
        if (isExternal) {
            externalMember = await ExternalTournamentParticipant.findOne({
                where: { id: participantId, tournamentId }
            });
            
            if (!externalMember) {
                throw new Error(`Externer Teilnehmer mit ID ${participantId} nicht gefunden`);
            }
            participantClassId = externalMember.classId;
        } else {
            internalMember = await TournamentMember.findOne({
                where: { id: participantId, tournamentId }
            });
            
            if (!internalMember) {
                throw new Error(`Interner Teilnehmer mit ID ${participantId} nicht gefunden`);
            }
            participantClassId = internalMember.classId;
        }

        // Hole bestehende Gruppen für diese Klasse (oder ohne Klasse, wenn classId null)
        const whereClause = { tournamentId };
        if (participantClassId === null) {
            whereClause.classId = { [Op.is]: null };
        } else {
            whereClause.classId = participantClassId;
        }
        const existingGroups = await TournamentGroup.findAll({
            where: whereClause,
            order: [['id', 'ASC']]
        });
        console.log(`[assignParticipantToGroup] Found ${existingGroups.length} existing groups for classId ${participantClassId}`);

        // Berechne dbGroupId
        let dbGroupId = null;
        if (groupNumber != null && groupNumber > 0) {
            // Stelle sicher, dass genug Gruppen existieren
            if (groupNumber > existingGroups.length) {
                console.log(`[assignParticipantToGroup] Creating ${groupNumber - existingGroups.length} new groups for classId ${participantClassId}`);
                // Erstelle fehlende Gruppen
                for (let i = existingGroups.length; i < groupNumber; i++) {
                    const grp = await TournamentGroup.create({ 
                        tournamentId,
                        classId: participantClassId
                    });
                    existingGroups.push(grp);
                }
            }
            // Mapping von groupNumber (1-based) zu groupId innerhalb dieser Klasse
            if (existingGroups.length >= groupNumber) {
                dbGroupId = existingGroups[groupNumber - 1].id;
                console.log(`[assignParticipantToGroup] Mapped groupNumber ${groupNumber} to dbGroupId ${dbGroupId} for classId ${participantClassId}`);
            } else {
                throw new Error(`Ungültiges groupNumber ${groupNumber} - es gibt nur ${existingGroups.length} Gruppen für classId ${participantClassId}`);
            }
        }

        // Aktualisiere den Teilnehmer
        if (isExternal) {
            console.log(`[assignParticipantToGroup] Updating external member ${participantId} (classId=${participantClassId}) to groupId ${dbGroupId}`);
            await ExternalTournamentParticipant.update(
                { groupId: dbGroupId },
                { where: { id: participantId, tournamentId } }
            );
        } else {
            console.log(`[assignParticipantToGroup] Updating internal member ${participantId} (memberId=${internalMember.clubMemberId || 'N/A'}, classId=${participantClassId}) to groupId ${dbGroupId}`);
            await TournamentMember.update(
                { groupId: dbGroupId },
                { where: { id: participantId, tournamentId } }
            );
        }

        console.log(`[assignParticipantToGroup] Successfully updated participant, loading groups with participants...`);
        // Lade aktualisierte Gruppen mit Teilnehmern zurück
        return await this.getGroupsWithParticipants(userToken, clubId, tournamentId);
    }

    // services/tournamentService.js
    async resetGroups(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);
        // löscht alle Gruppen … (inkl. CASCADE oder manuell TournamentMatch.destroy)
        await TournamentMatch.destroy({ where: { tournamentId } });
        await TournamentGroup.destroy({ where: { tournamentId } });
    }
    async resetMatches(userToken, clubId, tournamentId, classId = null) {
        await checkAccess(userToken, clubId);
        const where = {
            tournamentId,
            round: 'group'
        };
        if (classId != null) {
            where.classId = Number(classId);
        }
        await TournamentMatch.destroy({ where });
    }

    async removeParticipant(userToken, clubId, tournamentId, participantId) {
        await checkAccess(userToken, clubId);
        
        // Prüfe, ob der Teilnehmer existiert
        const participant = await TournamentMember.findOne({
            where: { id: participantId, tournamentId }
        });
        if (!participant) {
            throw new Error('Teilnehmer nicht gefunden');
        }
        
        // Lösche alle Matches, die auf diesen Teilnehmer verweisen (player1_id oder player2_id)
        await TournamentMatch.destroy({
            where: {
                tournamentId,
                [Op.or]: [
                    { player1Id: participantId },
                    { player2Id: participantId }
                ]
            }
        });
        
        // Jetzt kann der Teilnehmer gelöscht werden
        await TournamentMember.destroy({
            where: { id: participantId, tournamentId }
        });
    }

    async updateParticipantSeeded(userToken, clubId, tournamentId, participantId, seeded) {
        await checkAccess(userToken, clubId);
        const participant = await TournamentMember.findOne({
            where: { id: participantId, tournamentId }
        });
        if (!participant) {
            throw new Error('Teilnehmer nicht gefunden');
        }
        participant.seeded = seeded;
        await participant.save();
    }

    // services/tournamentService.js
    async deleteMatchResult(userToken, clubId, tournamentId, matchId, setToDelete) {
        await checkAccess(userToken, clubId);

        // Match existiert?
        const match = await TournamentMatch.findOne({ where: { id: matchId, tournamentId } });
        if (!match) throw new Error('Match nicht gefunden');

        // Satz löschen
        await TournamentResult.destroy({ where: { matchId, set: setToDelete } });

        // verbleibende Sätze neu durchnummerieren
        const remaining = await TournamentResult.findAll({
            where: { matchId },
            order: [['set', 'ASC']]
        });
        for (let i = 0; i < remaining.length; i++) {
            const r = remaining[i];
            const newSet = i + 1;
            if (r.set !== newSet) {
                r.set = newSet;
                await r.save();
            }
        }
    }

    async reopenMatch(userToken, clubId, tournamentId, matchId) {
        await checkAccess(userToken, clubId);

        const match = await TournamentMatch.findOne({
            where: { id: matchId, tournamentId }
        });
        if (!match) {
            throw new Error("Match nicht gefunden");
        }

        // Nur den Abschluss‑Status zurücksetzen, nicht die Einzelsätze
        match.isFinished = false;
        match.result = null;  // optional: entfernt die zusammengefasste Ergebnis‑Spalte
        await match.save();
    }

    async setMatchActive(userToken, clubId, tournamentId, matchId, isActive) {
        await checkAccess(userToken, clubId);

        const match = await TournamentMatch.findOne({
            where: { id: matchId, tournamentId }
        });
        if (!match) {
            throw new Error("Match nicht gefunden");
        }

        // Setze den Status für dieses Match (mehrere Matches können gleichzeitig aktiv sein)
        match.isActive = isActive;
        await match.save();
    }

    async setMatchTable(userToken, clubId, tournamentId, matchId, tableNumber) {
        await checkAccess(userToken, clubId);

        const match = await TournamentMatch.findOne({ where: { id: matchId, tournamentId } });
        if (!match) throw new Error('Match nicht gefunden');

        match.tableNumber = (tableNumber == null) ? null : Number(tableNumber);
        await match.save();
        return match.toJSON ? match.toJSON() : match;
    }

    async distributeTables(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);

        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) throw new Error('Turnier nicht gefunden');

        const numTables = tournament.numberOfTables ? Number(tournament.numberOfTables) : 0;
        if (!numTables || numTables < 1) throw new Error('Anzahl der Tische nicht gesetzt');

        console.log(`[distributeTables] clubId=${clubId}, tournamentId=${tournamentId}, numberOfTables=${numTables}`);

        // Ermittle aktuell laufende Matches, damit wir nur Spiele verteilen, bei denen beide Spieler frei sind
        const activeMatches = await TournamentMatch.findAll({ where: { tournamentId, isActive: true, isFinished: false } });

        // Nur Spieler aus aktiv laufenden Matches mit einem zugewiesenen Tisch blockieren.
        const occupiedPlayerIds = new Set();
        activeMatches.forEach(am => {
            if (am.tableNumber != null) {
                if (am.player1Id) occupiedPlayerIds.add(Number(am.player1Id));
                if (am.player2Id) occupiedPlayerIds.add(Number(am.player2Id));
            }
        });

        console.log(`[distributeTables] activeMatches=${activeMatches.length} occupiedPlayers=${[...occupiedPlayerIds].slice(0,10).join(',')}`);

        // Lade nur noch die nicht abgeschlossenen, noch nicht zugewiesenen Matches
        const matches = await TournamentMatch.findAll({
            where: { tournamentId, isFinished: false, tableNumber: null },
            order: [ ['group_round', 'ASC'], ['id', 'ASC'] ]
        });

        console.log(`[distributeTables] candidateMatches=${matches.length}`);

        // Ermittle aktuell belegte Tischnummern basierend auf laufenden Matches
        const occupiedTables = new Set();
        activeMatches.forEach(am => {
            if (am.tableNumber != null) occupiedTables.add(Number(am.tableNumber));
        });

        console.log(`[distributeTables] occupiedTables=${[...occupiedTables].join(',')}`);

        // Erzeuge Liste verfügbarer Tische (1..numTables) minus belegte Tische
        const availableTables = [];
        for (let t = 1; t <= numTables; t++) {
            if (!occupiedTables.has(t)) availableTables.push(t);
        }

        if (availableTables.length === 0) {
            console.log('[distributeTables] no available tables (all occupied by active matches)');
            return [];
        }

        const updated = [];
        // Verwende `busyPlayers` (initial aus occupiedPlayerIds) und erweitere es bei Zuweisungen,
        // damit ein Spieler nicht mehrere Tische in einer Verteilung bekommt.
        const busyPlayers = new Set(occupiedPlayerIds);

        for (const m of matches) {
            // Spiele mit BYE (fehlendem Spieler) überspringen
            if (!m.player1Id || !m.player2Id) {
                console.log(`[distributeTables] skipping match ${m.id} (BYE or missing player) p1=${m.player1Id} p2=${m.player2Id}`);
                continue;
            }
            // Nur vergeben, wenn beide Spieler aktuell keinen Tisch belegen (busyPlayers)
            if (busyPlayers.has(Number(m.player1Id)) || busyPlayers.has(Number(m.player2Id))) {
                console.log(`[distributeTables] skipping match ${m.id} because player occupies table p1=${m.player1Id} p2=${m.player2Id}`);
                continue;
            }

            // Wenn keine freien Tische mehr vorhanden, abbrechen
            if (availableTables.length === 0) {
                console.log('[distributeTables] no more available tables to assign');
                break;
            }

            // Nimm den nächsten freien Tisch (FIFO) und markiere ihn als belegt
            const assign = availableTables.shift();
            m.tableNumber = assign;
            m.isActive = true; // Markiere Spiel als gestartet
            await m.save();
            updated.push(m.toJSON ? m.toJSON() : m);
            console.log(`[distributeTables] assigned match ${m.id} -> table ${assign}`);

            // Markiere Spieler als busy, damit sie in dieser Verteilung keinen weiteren Tisch bekommen
            busyPlayers.add(Number(m.player1Id));
            busyPlayers.add(Number(m.player2Id));
        }

        console.log(`[distributeTables] finished: assignedCount=${updated.length}`);

        return updated;
    }

    async resetKnockout(userToken, clubId, tournamentId, classId = null) {
        await checkAccess(userToken, clubId);

        const stages = await TournamentStage.findAll({ where: { tournamentId } });
        const nonPreliminaryStageIds = stages
            .filter(stage => Number(stage.index) !== 1)
            .map(stage => Number(stage.id));

        const stageGroupWhere = { tournamentId };
        if (nonPreliminaryStageIds.length > 0) {
            stageGroupWhere.stageId = { [Op.in]: nonPreliminaryStageIds };
        } else {
            stageGroupWhere.stageId = { [Op.ne]: null };
        }
        if (classId != null) {
            stageGroupWhere.classId = Number(classId);
        }

        const stageGroups = await TournamentGroup.findAll({ where: stageGroupWhere });
        const stageGroupIds = stageGroups.map(group => Number(group.id));

        const knockoutWhere = {
            tournamentId,
            round: { [Op.ne]: "group" }
        };
        if (classId != null) {
            knockoutWhere.classId = Number(classId);
        }
        await TournamentMatch.destroy({ where: knockoutWhere });

        if (stageGroupIds.length > 0) {
            const stageMatchWhere = {
                tournamentId,
                [Op.or]: [
                    { groupId: { [Op.in]: stageGroupIds } },
                    { stageId: { [Op.in]: nonPreliminaryStageIds } },
                ],
            };
            if (classId != null) {
                stageMatchWhere.classId = Number(classId);
            }
            await TournamentMatch.destroy({ where: stageMatchWhere });
            await TournamentMember.destroy({ where: { tournamentId, groupId: { [Op.in]: stageGroupIds } } });
            await ExternalTournamentParticipant.destroy({ where: { tournamentId, groupId: { [Op.in]: stageGroupIds } } });
            await TournamentGroup.destroy({ where: { id: { [Op.in]: stageGroupIds } } });
        }
    }

    // Externe Teilnehmer hinzufügen
    async addExternalParticipant(userToken, clubId, classId, firstName, lastName, club, birthDate, gender) {
        await checkAccess(userToken, clubId);
        if (!classId) {
            throw new Error('Klasse ist erforderlich');
        }
        const tournamentClass = await TournamentClass.findByPk(classId);
        if (!tournamentClass) {
            throw new Error('Klasse nicht gefunden');
        }
        const tournament = await Tournament.findByPk(tournamentClass.tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        if (!tournament.allowsExternal) {
            throw new Error('Dieses Turnier erlaubt keine externen Teilnehmer');
        }
        
        // Validierung: Geschlecht muss zur Klasse passen
        const participantGender = gender || 'unknown';
        if (tournamentClass.gender) {
            if (tournamentClass.gender === 'male' && participantGender !== 'male') {
                throw new Error('Dieser Teilnehmer kann nicht in einer männlichen Klasse spielen');
            }
            if (tournamentClass.gender === 'female' && participantGender !== 'female') {
                throw new Error('Dieser Teilnehmer kann nicht in einer weiblichen Klasse spielen');
            }
            if (tournamentClass.gender === 'mixed' && participantGender === 'unknown') {
                throw new Error('Teilnehmer mit unbekanntem Geschlecht können nicht in einer Mixed-Klasse spielen');
            }
            // mixed erlaubt alle Geschlechter (male, female, diverse)
        }
        
        // Validierung: Geburtsjahr muss zur Klasse passen (geboren im Jahr X oder später, also >=)
        if (tournamentClass.minBirthYear && birthDate) {
            // Parse das Geburtsdatum (Format: YYYY-MM-DD oder DD.MM.YYYY)
            let birthYear = null;
            if (birthDate.includes('-')) {
                // Format: YYYY-MM-DD
                birthYear = parseInt(birthDate.split('-')[0]);
            } else if (birthDate.includes('.')) {
                // Format: DD.MM.YYYY
                const parts = birthDate.split('.');
                if (parts.length === 3) {
                    birthYear = parseInt(parts[2]);
                }
            }
            
            if (birthYear && !isNaN(birthYear)) {
                // Geboren im Jahr X oder später bedeutet: birthYear >= minBirthYear
                if (birthYear < tournamentClass.minBirthYear) {
                    throw new Error(`Dieser Teilnehmer ist zu alt für diese Klasse. Erlaubt: geboren ${tournamentClass.minBirthYear} oder später`);
                }
            }
        }
        
        await ExternalTournamentParticipant.create({
            tournamentId: tournamentClass.tournamentId,
            classId,
            firstName,
            lastName,
            club: club || null,
            birthDate: birthDate || null,
            gender: participantGender,
            groupId: null
        });
    }

    // Externe Teilnehmer abrufen - nach Klasse oder Turnier
    async getExternalParticipants(userToken, clubId, tournamentId, classId = null) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const whereClause = { tournamentId };
        if (classId !== null) {
            whereClause.classId = classId;
        }
        const stageGroups = await TournamentGroup.findAll({
            where: { tournamentId, stageId: { [Op.ne]: null } },
            attributes: ['id'],
        });
        const stageGroupIds = new Set(stageGroups.map(g => Number(g.id)));
        const participants = await ExternalTournamentParticipant.findAll({
            where: whereClause,
            order: [['firstName', 'ASC'], ['lastName', 'ASC']]
        });
        return participants.filter(p => !p.groupId || !stageGroupIds.has(Number(p.groupId)));
    }

    // Externe Teilnehmer löschen
    async removeExternalParticipant(userToken, clubId, tournamentId, participantId) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const participant = await ExternalTournamentParticipant.findOne({
            where: { id: participantId, tournamentId }
        });
        if (!participant) {
            throw new Error('Externer Teilnehmer nicht gefunden');
        }
        await participant.destroy();
    }

    // Gesetzt-Status für externe Teilnehmer aktualisieren
    async updateExternalParticipantSeeded(userToken, clubId, tournamentId, participantId, seeded) {
        await checkAccess(userToken, clubId);
        const participant = await ExternalTournamentParticipant.findOne({
            where: { id: participantId, tournamentId }
        });
        if (!participant) {
            throw new Error('Externer Teilnehmer nicht gefunden');
        }
        participant.seeded = seeded;
        await participant.save();
    }

    // Tournament Classes
    async getTournamentClasses(userToken, clubId, tournamentId) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        return await TournamentClass.findAll({
            where: { tournamentId },
            order: [['sortOrder', 'ASC'], ['name', 'ASC']]
        });
    }

    async addTournamentClass(userToken, clubId, tournamentId, name, isDoubles = false, gender = null, minBirthYear = null) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        // Finde die höchste sortOrder
        const maxSortOrder = await TournamentClass.max('sortOrder', {
            where: { tournamentId }
        }) || 0;
        return await TournamentClass.create({
            tournamentId,
            name,
            sortOrder: maxSortOrder + 1,
            isDoubles: isDoubles || false,
            gender: gender || null,
            minBirthYear: minBirthYear || null
        });
    }

    async updateTournamentClass(userToken, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const tournamentClass = await TournamentClass.findOne({
            where: { id: classId, tournamentId }
        });
        if (!tournamentClass) {
            throw new Error('Klasse nicht gefunden');
        }
        console.log('[updateTournamentClass] Before update:', {
            id: tournamentClass.id,
            name: tournamentClass.name,
            isDoubles: tournamentClass.isDoubles,
            gender: tournamentClass.gender,
            minBirthYear: tournamentClass.minBirthYear
        });
        console.log('[updateTournamentClass] New values:', { name, sortOrder, isDoubles, gender, minBirthYear });
        
        // Verwende update() statt direkter Zuweisung für bessere Kontrolle
        const updateData = {};
        if (name !== undefined) updateData.name = name;
        if (sortOrder !== undefined) updateData.sortOrder = sortOrder;
        if (isDoubles !== undefined) updateData.isDoubles = isDoubles;
        if (gender !== undefined) updateData.gender = gender;
        if (minBirthYear !== undefined) updateData.minBirthYear = minBirthYear;
        
        console.log('[updateTournamentClass] Update data:', updateData);
        
        await tournamentClass.update(updateData);
        
        // Lade die aktualisierte Instanz neu, um sicherzustellen, dass wir die aktuellen DB-Werte haben
        await tournamentClass.reload();
        
        console.log('[updateTournamentClass] After update and reload:', {
            id: tournamentClass.id,
            name: tournamentClass.name,
            isDoubles: tournamentClass.isDoubles,
            gender: tournamentClass.gender,
            minBirthYear: tournamentClass.minBirthYear
        });
        return tournamentClass;
    }

    async deleteTournamentClass(userToken, clubId, tournamentId, classId) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const tournamentClass = await TournamentClass.findOne({
            where: { id: classId, tournamentId }
        });
        if (!tournamentClass) {
            throw new Error('Klasse nicht gefunden');
        }
        // Setze classId bei allen Teilnehmern auf null
        await TournamentMember.update(
            { classId: null },
            { where: { tournamentId, classId } }
        );
        await ExternalTournamentParticipant.update(
            { classId: null },
            { where: { tournamentId, classId } }
        );
        await tournamentClass.destroy();
    }

    async updateParticipantClass(userToken, clubId, tournamentId, participantId, classId, isExternal = false) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        if (classId !== null) {
            const tournamentClass = await TournamentClass.findOne({
                where: { id: classId, tournamentId }
            });
            if (!tournamentClass) {
                throw new Error('Klasse nicht gefunden');
            }
        }
        if (isExternal) {
            const participant = await ExternalTournamentParticipant.findOne({
                where: { id: participantId, tournamentId }
            });
            if (!participant) {
                throw new Error('Externer Teilnehmer nicht gefunden');
            }
            participant.classId = classId;
            await participant.save();
        } else {
            const participant = await TournamentMember.findOne({
                where: { id: participantId, tournamentId }
            });
            if (!participant) {
                throw new Error('Teilnehmer nicht gefunden');
            }
            participant.classId = classId;
            await participant.save();
        }
    }

    // Tournament Pairings
    async getPairings(userToken, clubId, tournamentId, classId) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const tournamentClass = await TournamentClass.findOne({
            where: { id: classId, tournamentId }
        });
        if (!tournamentClass) {
            throw new Error('Klasse nicht gefunden');
        }
        if (!tournamentClass.isDoubles) {
            throw new Error('Klasse ist keine Doppel-Klasse');
        }
        return await TournamentPairing.findAll({
            where: { tournamentId, classId },
            include: [
                { model: TournamentMember, as: 'member1', include: [{ model: Member, as: 'member' }] },
                { model: TournamentMember, as: 'member2', include: [{ model: Member, as: 'member' }] },
                { model: ExternalTournamentParticipant, as: 'external1' },
                { model: ExternalTournamentParticipant, as: 'external2' }
            ],
            order: [['id', 'ASC']]
        });
    }

    async createPairing(userToken, clubId, tournamentId, classId, player1Type, player1Id, player2Type, player2Id, seeded = false, groupId = null) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const tournamentClass = await TournamentClass.findOne({
            where: { id: classId, tournamentId }
        });
        if (!tournamentClass) {
            throw new Error('Klasse nicht gefunden');
        }
        if (!tournamentClass.isDoubles) {
            throw new Error('Klasse ist keine Doppel-Klasse');
        }
        // Validiere, dass beide Spieler existieren
        if (player1Type === 'member') {
            const member1 = await TournamentMember.findOne({
                where: { id: player1Id, tournamentId }
            });
            if (!member1) {
                throw new Error('Spieler 1 (Mitglied) nicht gefunden');
            }
        } else if (player1Type === 'external') {
            const external1 = await ExternalTournamentParticipant.findOne({
                where: { id: player1Id, tournamentId }
            });
            if (!external1) {
                throw new Error('Spieler 1 (extern) nicht gefunden');
            }
        }
        if (player2Type === 'member') {
            const member2 = await TournamentMember.findOne({
                where: { id: player2Id, tournamentId }
            });
            if (!member2) {
                throw new Error('Spieler 2 (Mitglied) nicht gefunden');
            }
        } else if (player2Type === 'external') {
            const external2 = await ExternalTournamentParticipant.findOne({
                where: { id: player2Id, tournamentId }
            });
            if (!external2) {
                throw new Error('Spieler 2 (extern) nicht gefunden');
            }
        }
        // Prüfe auf Duplikate: gleiche Paarung (in beiden Richtungen)
        // Baue die Bedingungen für beide Richtungen
        const condition1 = {
            tournamentId,
            classId,
            member1Id: player1Type === 'member' ? player1Id : null,
            external1Id: player1Type === 'external' ? player1Id : null,
            member2Id: player2Type === 'member' ? player2Id : null,
            external2Id: player2Type === 'external' ? player2Id : null
        };
        const condition2 = {
            tournamentId,
            classId,
            member1Id: player2Type === 'member' ? player2Id : null,
            external1Id: player2Type === 'external' ? player2Id : null,
            member2Id: player1Type === 'member' ? player1Id : null,
            external2Id: player1Type === 'external' ? player1Id : null
        };
        
        // Prüfe beide Bedingungen
        const existingPairing1 = await TournamentPairing.findOne({ where: condition1 });
        const existingPairing2 = await TournamentPairing.findOne({ where: condition2 });
        const existingPairing = existingPairing1 || existingPairing2;
        if (existingPairing) {
            throw new Error('Diese Paarung existiert bereits');
        }
        return await TournamentPairing.create({
            tournamentId,
            classId,
            groupId,
            member1Id: player1Type === 'member' ? player1Id : null,
            external1Id: player1Type === 'external' ? player1Id : null,
            member2Id: player2Type === 'member' ? player2Id : null,
            external2Id: player2Type === 'external' ? player2Id : null,
            seeded
        });
    }

    async updatePairing(userToken, clubId, tournamentId, pairingId, player1Type, player1Id, player2Type, player2Id, seeded, groupId) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const pairing = await TournamentPairing.findOne({
            where: { id: pairingId, tournamentId }
        });
        if (!pairing) {
            throw new Error('Paarung nicht gefunden');
        }
        // Validiere Spieler, wenn sie geändert werden
        if (player1Type !== undefined && player1Id !== undefined) {
            if (player1Type === 'member') {
                const member1 = await TournamentMember.findOne({
                    where: { id: player1Id, tournamentId }
                });
                if (!member1) {
                    throw new Error('Spieler 1 (Mitglied) nicht gefunden');
                }
                pairing.member1Id = player1Id;
                pairing.external1Id = null;
            } else if (player1Type === 'external') {
                const external1 = await ExternalTournamentParticipant.findOne({
                    where: { id: player1Id, tournamentId }
                });
                if (!external1) {
                    throw new Error('Spieler 1 (extern) nicht gefunden');
                }
                pairing.external1Id = player1Id;
                pairing.member1Id = null;
            }
        }
        if (player2Type !== undefined && player2Id !== undefined) {
            if (player2Type === 'member') {
                const member2 = await TournamentMember.findOne({
                    where: { id: player2Id, tournamentId }
                });
                if (!member2) {
                    throw new Error('Spieler 2 (Mitglied) nicht gefunden');
                }
                pairing.member2Id = player2Id;
                pairing.external2Id = null;
            } else if (player2Type === 'external') {
                const external2 = await ExternalTournamentParticipant.findOne({
                    where: { id: player2Id, tournamentId }
                });
                if (!external2) {
                    throw new Error('Spieler 2 (extern) nicht gefunden');
                }
                pairing.external2Id = player2Id;
                pairing.member2Id = null;
            }
        }
        if (seeded !== undefined) pairing.seeded = seeded;
        if (groupId !== undefined) pairing.groupId = groupId;
        await pairing.save();
        return pairing;
    }

    async deletePairing(userToken, clubId, tournamentId, pairingId) {
        await checkAccess(userToken, clubId);
        const tournament = await Tournament.findByPk(tournamentId);
        if (!tournament || tournament.clubId != clubId) {
            throw new Error('Turnier nicht gefunden');
        }
        const pairing = await TournamentPairing.findOne({
            where: { id: pairingId, tournamentId }
        });
        if (!pairing) {
            throw new Error('Paarung nicht gefunden');
        }
        await pairing.destroy();
    }

}

export default new TournamentService();
