import UserClub from "../models/UserClub.js";
import Club from "../models/Club.js";
import { checkAccess, getUserByToken, hasUserClubAccess } from "../utils/userUtils.js";
import Member from "../models/Member.js";
import MemberImage from "../models/MemberImage.js";
import MemberTtrHistory from "../models/MemberTtrHistory.js";
import MemberPlayInterest from "../models/MemberPlayInterest.js";
import ClubSepaMandate from "../models/ClubSepaMandate.js";
import Participant from "../models/Participant.js";
import DiaryDate from "../models/DiaryDates.js";
import { Op, fn, col } from 'sequelize';
import path from 'path';
import fs from 'fs';
import sharp from 'sharp';

import { devLog } from '../utils/logger.js';
import { standardizePhoneNumber } from '../utils/phoneUtils.js';
class MemberService {
    normalizeSepaMandatePayload(payload = {}) {
        const normalizeText = (value, maxLength = null) => {
            if (value === null || value === undefined) return null;
            const trimmed = String(value).trim();
            if (!trimmed) return null;
            return maxLength ? trimmed.slice(0, maxLength) : trimmed;
        };
        const normalizeDate = (value) => {
            const normalized = normalizeText(value, 10);
            return normalized || null;
        };

        const status = normalizeText(payload.status, 32) || 'active';

        return {
            debtorName: normalizeText(payload.debtorName, 255),
            iban: normalizeText(payload.iban, 34),
            bic: normalizeText(payload.bic, 11),
            mandateReference: normalizeText(payload.mandateReference, 80),
            signedOn: normalizeDate(payload.signedOn),
            validFrom: normalizeDate(payload.validFrom),
            status,
            historyNote: normalizeText(payload.historyNote),
            revokedAt: status === 'revoked'
                ? (normalizeText(payload.revokedAt) || new Date().toISOString())
                : null
        };
    }

    async getMemberSepaMandate(userToken, clubId, memberId) {
        await checkAccess(userToken, clubId);
        const member = await Member.findOne({ where: { id: memberId, clubId } });
        if (!member) {
            return {
                status: 404,
                response: { success: false, error: 'membernotfound' }
            };
        }

        const mandate = await ClubSepaMandate.findOne({
            where: { clubId, memberId },
            order: [['updatedAt', 'DESC'], ['id', 'DESC']]
        });

        return {
            status: 200,
            response: {
                success: true,
                mandate: mandate ? mandate.toJSON() : null
            }
        };
    }

    async saveMemberSepaMandate(userToken, clubId, memberId, payload = {}) {
        await checkAccess(userToken, clubId);
        const member = await Member.findOne({ where: { id: memberId, clubId } });
        if (!member) {
            return {
                status: 404,
                response: { success: false, error: 'membernotfound' }
            };
        }

        const normalizedPayload = this.normalizeSepaMandatePayload(payload);
        const hasContent = Boolean(
            normalizedPayload.debtorName
            || normalizedPayload.iban
            || normalizedPayload.bic
            || normalizedPayload.mandateReference
            || normalizedPayload.signedOn
            || normalizedPayload.validFrom
            || normalizedPayload.historyNote
        );

        let mandate = await ClubSepaMandate.findOne({
            where: { clubId, memberId },
            order: [['updatedAt', 'DESC'], ['id', 'DESC']]
        });

        if (!mandate && !hasContent) {
            return {
                status: 200,
                response: { success: true, mandate: null }
            };
        }

        if (!mandate) {
            if (!normalizedPayload.debtorName || !normalizedPayload.iban || !normalizedPayload.mandateReference) {
                return {
                    status: 400,
                    response: {
                        success: false,
                        code: 'missingrequiredsepafields',
                        error: 'Bitte Kontoinhaber, IBAN und Mandatsreferenz angeben.'
                    }
                };
            }
            mandate = await ClubSepaMandate.create({
                clubId,
                memberId,
                ...normalizedPayload
            });
        } else {
            mandate.debtorName = normalizedPayload.debtorName;
            mandate.iban = normalizedPayload.iban;
            mandate.bic = normalizedPayload.bic;
            mandate.mandateReference = normalizedPayload.mandateReference;
            mandate.signedOn = normalizedPayload.signedOn;
            mandate.validFrom = normalizedPayload.validFrom;
            mandate.status = normalizedPayload.status;
            mandate.historyNote = normalizedPayload.historyNote;
            mandate.revokedAt = normalizedPayload.revokedAt;
            await mandate.save();
        }

        await mandate.reload();

        return {
            status: 200,
            response: {
                success: true,
                mandate: mandate.toJSON()
            }
        };
    }

    async getMemberPlayInterests(userToken, clubId, seasonId, lineupHalf) {
        await checkAccess(userToken, clubId);
        if (!seasonId || !['first_half', 'second_half'].includes(String(lineupHalf || ''))) {
            return {
                status: 400,
                response: { error: 'invalidplayinterestparams' }
            };
        }

        const rows = await MemberPlayInterest.findAll({
            where: {
                clubId,
                seasonId,
                lineupHalf,
                interested: true
            },
            attributes: ['memberId', 'seasonId', 'lineupHalf', 'interested']
        });

        return {
            status: 200,
            response: rows.map((row) => row.toJSON())
        };
    }

    async setMemberPlayInterest(userToken, clubId, memberId, seasonId, lineupHalf, interested = true) {
        await checkAccess(userToken, clubId);
        if (!memberId || !seasonId || !['first_half', 'second_half'].includes(String(lineupHalf || ''))) {
            return {
                status: 400,
                response: { error: 'invalidplayinterestparams' }
            };
        }

        const member = await Member.findOne({ where: { id: memberId, clubId } });
        if (!member) {
            return {
                status: 404,
                response: { error: 'membernotfound' }
            };
        }

        const [row] = await MemberPlayInterest.findOrCreate({
            where: { clubId, memberId, seasonId, lineupHalf },
            defaults: { interested: !!interested }
        });

        if (row.interested !== !!interested) {
            row.interested = !!interested;
            await row.save();
        }

        return {
            status: 200,
            response: { result: 'success' }
        };
    }

    async getApprovalRequests(userToken, clubId) {
        await checkAccess(userToken, clubId);
        const user = await getUserByToken(userToken);
        return await UserClub.findAll({
            where: {
                clubId: clubId,
                approved: false,
                userId: user.id 
            }
        }); 
    }

    async getClubMembers(userToken, clubId, showAll) {
        await checkAccess(userToken, clubId);
        const where = {
            clubId: clubId
        };
        if (!showAll || showAll === 'false') {
            where['active'] = true;
        }
        const MemberContact = (await import('../models/MemberContact.js')).default;
        try {
            const members = await Member.findAll({
                where,
                include: [
                    {
                        model: MemberContact,
                        as: 'contacts',
                        required: false,
                        attributes: ['id', 'memberId', 'type', 'value', 'isParent', 'parentName', 'isPrimary', 'createdAt', 'updatedAt']
                    },
                    {
                        model: MemberImage,
                        as: 'images',
                        required: false,
                        attributes: ['id', 'memberId', 'fileName', 'sortOrder', 'createdAt', 'updatedAt']
                    }
                ],
                raw: false
            });

            const results = [];
            for (const member of members) {
                const memberJson = member.toJSON();

                if (member.contacts && Array.isArray(member.contacts)) {
                    memberJson.contacts = member.contacts.map(contact => ({
                        id: contact.id,
                        memberId: contact.memberId,
                        type: contact.type,
                        value: contact.value,
                        isParent: contact.isParent,
                        parentName: contact.parentName,
                        isPrimary: contact.isPrimary,
                        createdAt: contact.createdAt,
                        updatedAt: contact.updatedAt
                    }));
                }

                // Berechne Trainingsteilnahmen
                const trainingParticipations = await Participant.count({
                    include: [{
                        model: DiaryDate,
                        as: 'diaryDate',
                        where: {
                            clubId: clubId
                        }
                    }],
                    where: {
                        memberId: member.id
                    }
                });
                memberJson.trainingParticipations = trainingParticipations;

                const imageData = await this._prepareMemberImages(member, { forceReload: true });
                memberJson.images = imageData.images;
                memberJson.primaryImageId = imageData.primaryImageId;
                memberJson.primaryImageUrl = imageData.primaryImageUrl;
                memberJson.imageUrl = imageData.primaryImageUrl;
                memberJson.hasImage = imageData.hasImage;

                results.push(memberJson);
            }

            return results;
        } catch (error) {
                console.error('[getClubMembers] - Error:', error);
                throw error;
        }
    }

    async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate, phone, email, active = true, testMembership = false,
        picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null, memberFormHandedOver = false, adultReleaseApproved = false, adultReserveApproved = false,
        contributionGroupCode = null, contacts = []) {
        try {
            await checkAccess(userToken, clubId);
            let member = null;
            if (memberId) {
                member = await Member.findOne({ where: { id: memberId } });
            }
            const MemberContact = (await import('../models/MemberContact.js')).default;
            const normalizedContributionGroupCode = String(contributionGroupCode || '').trim() || null;
            if (member) {
                member.firstName = firstName;
                member.lastName = lastName;
                member.street = street;
                member.city = city;
                if (postalCode !== undefined) member.postalCode = postalCode;
                member.birthDate = birthdate;
                member.phone = phone ? standardizePhoneNumber(phone) : phone;
                member.email = email;
                member.active = active; 
                member.testMembership = testMembership;
                member.picsInInternetAllowed = picsInInternetAllowed;
                if (gender) member.gender = gender;
                if (ttr !== undefined) member.ttr = ttr;
                if (qttr !== undefined) member.qttr = qttr;
                member.memberFormHandedOver = !!memberFormHandedOver;
                member.adultReleaseApproved = !!adultReleaseApproved;
                member.adultReserveApproved = !!adultReserveApproved;
                member.contributionGroupCode = normalizedContributionGroupCode;
                await member.save();
                
                // Update contacts if provided
                if (Array.isArray(contacts)) {
                    // Delete existing contacts
                    await MemberContact.destroy({ where: { memberId: member.id } });
                    // Create new contacts
                    for (const contact of contacts) {
                        if (contact.value && contact.type) {
                            // Standardisiere Telefonnummern
                            let standardizedValue = contact.value;
                            if (contact.type === 'phone') {
                                standardizedValue = standardizePhoneNumber(contact.value);
                            }
                            await MemberContact.create({
                                memberId: member.id,
                                type: contact.type,
                                value: standardizedValue,
                                isParent: contact.isParent || false,
                                parentName: contact.parentName || null,
                                isPrimary: contact.isPrimary || false
                            });
                        }
                    }
                }
            } else {
                const newMember = await Member.create({
                    firstName: firstName,
                    lastName: lastName,
                    street: street,
                    city: city,
                    postalCode: postalCode || null,
                    birthDate: birthdate,
                    phone: phone ? standardizePhoneNumber(phone) : phone,
                    email: email,
                    clubId: clubId,
                    active: active,
                    testMembership: testMembership,
                    picsInInternetAllowed: picsInInternetAllowed,
                    gender: gender || 'unknown',
                    ttr: ttr,
                    qttr: qttr,
                    memberFormHandedOver: !!memberFormHandedOver,
                    adultReleaseApproved: !!adultReleaseApproved,
                    adultReserveApproved: !!adultReserveApproved,
                    contributionGroupCode: normalizedContributionGroupCode,
                });
                
                // Create contacts if provided
                if (Array.isArray(contacts)) {
                    for (const contact of contacts) {
                        if (contact.value && contact.type) {
                            // Standardisiere Telefonnummern
                            let standardizedValue = contact.value;
                            if (contact.type === 'phone') {
                                standardizedValue = standardizePhoneNumber(contact.value);
                            }
                            await MemberContact.create({
                                memberId: newMember.id,
                                type: contact.type,
                                value: standardizedValue,
                                isParent: contact.isParent || false,
                                parentName: contact.parentName || null,
                                isPrimary: contact.isPrimary || false
                            });
                        }
                    }
                }
            }
            return {
                status: 200,
                response: { result: "success" },
            }
        } catch (error) {
            devLog(error);
            return {
                status: error.statusCode || 500,
                response: { error: "nocreation" }
            }
        }
    }

    async uploadMemberImage(userToken, clubId, memberId, imageBuffer, options = {}) {
        const { makePrimary = false } = options;
        let transaction = null;
        try {
            await checkAccess(userToken, clubId);
            const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
            if (!member) {
                return { status: 404, response: { success: false, error: 'Member not found in this club' } };
            }

            await this._migrateLegacyImage(memberId);

            transaction = await MemberImage.sequelize.transaction();

            const maxSortOrder = await MemberImage.max('sortOrder', {
                where: { memberId },
                transaction
            });
            const sortOrder = Number.isFinite(maxSortOrder) ? (maxSortOrder + 1) : 1;

            const imageRecord = await MemberImage.create({
                memberId,
                fileName: '',
                sortOrder
            }, { transaction });

            const directoryPath = await this._ensureImageDirectory(memberId);
            const fileName = `${imageRecord.id}.jpg`;
            const targetPath = path.join(directoryPath, fileName);

            await sharp(imageBuffer)
                .resize(600, 600, { fit: 'inside' })
                .jpeg({ quality: 80 })
                .toFile(targetPath);

            imageRecord.fileName = fileName;
            await imageRecord.save({ transaction });

            if (makePrimary) {
                await this._setPrimaryImage(memberId, imageRecord.id, { transaction });
            }

            await transaction.commit();
            transaction = null;

            const imageData = await this._prepareMemberImages(member, { forceReload: true });
            const createdImage = imageData.images.find(img => img.id === imageRecord.id) || null;

            return {
                status: 200,
                response: {
                    success: true,
                    image: createdImage,
                    images: imageData.images,
                    primaryImageId: imageData.primaryImageId,
                    primaryImageUrl: imageData.primaryImageUrl
                }
            };
        } catch (error) {
            if (transaction) {
                try {
                    await transaction.rollback();
                } catch (rollbackError) {
                    console.error('[uploadMemberImage] - Rollback failed:', rollbackError);
                }
            }
            console.error('[uploadMemberImage] - Error:', error);
            return { status: 500, response: { success: false, error: 'Failed to upload image' } };
        }
    }

    async getMemberImage(userToken, clubId, memberId, imageId = null) {
        try {
            await checkAccess(userToken, clubId);
            const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
            if (!member) {
                return { status: 404, error: 'Member not found in this club' };
            }
            const migratedImage = await this._migrateLegacyImage(memberId);
            if (migratedImage && (!imageId || imageId === migratedImage.id)) {
                imageId = migratedImage.id;
            }

            let imageRecord = null;
            if (imageId) {
                imageRecord = await MemberImage.findOne({
                    where: { id: imageId, memberId }
                });
            }

            if (!imageRecord) {
                // Ohne explizite imageId immer das Primärbild liefern (kleinste sortOrder).
                imageRecord = await MemberImage.findOne({
                    where: { memberId },
                    order: [['sortOrder', 'ASC'], ['id', 'ASC']]
                });
            }

            if (!imageRecord) {
                const legacyPath = this._findLegacyImagePath(memberId);
                if (legacyPath) {
                    return { status: 200, imagePath: path.resolve(legacyPath) };
                }
                return { status: 404, error: 'Image not found' };
            }

            const directoryPath = path.join('images', 'members', String(memberId));
            const imagePath = path.join(directoryPath, imageRecord.fileName);

            if (!fs.existsSync(imagePath)) {
                return { status: 404, error: 'Image not found' };
            }

            return { status: 200, imagePath: path.resolve(imagePath) };
        } catch (error) {
            console.error('[getMemberImage] - Error:', error);
            return { status: 500, error: 'Failed to retrieve image' };
        }
    }

    async _updateRatingsInternal(userId, clubId) {
        const myTischtennisService = (await import('./myTischtennisService.js')).default;
        const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
        try {
            // 1. myTischtennis-Session abrufen oder Login durchführen
            let session;
            try {
                session = await myTischtennisService.getSession(userId);
            } catch (sessionError) {
                
                // Versuche automatischen Login mit gespeicherten Credentials
                try {
                    await myTischtennisService.verifyLogin(userId);
                    const freshSession = await myTischtennisService.getSession(userId);
                    session = {
                        cookie: freshSession.cookie,
                        accessToken: freshSession.accessToken,
                        refreshToken: freshSession.refreshToken,
                        expiresAt: freshSession.expiresAt,
                        userData: freshSession.userData
                    };
                } catch (loginError) {
                    console.error('[updateRatingsFromMyTischtennis] - Automatic login failed:', loginError.message);
                    return {
                        status: 401,
                        response: { 
                            message: 'Session abgelaufen und automatischer Login fehlgeschlagen. Bitte einmal in myTischtennis einloggen.',
                            updated: 0,
                            errors: [loginError.message],
                            needsReauth: true
                        }
                    };
                }
            }
            
            const account = await myTischtennisService.getAccount(userId);
            
            if (!account) {
                console.error('[updateRatingsFromMyTischtennis] - No account found!');
                return {
                    status: 400,
                    response: { 
                        message: 'Kein myTischtennis-Account gefunden.',
                        updated: 0,
                        errors: [],
                        debug: { userId }
                    }
                };
            }

            const club = await Club.findByPk(clubId);
            if (!club) {
                return {
                    status: 404,
                    response: { message: 'Verein nicht gefunden.', updated: 0, errors: [] }
                };
            }

            if (!club.autoFetchRankings) {
                return {
                    status: 200,
                    response: {
                        message: 'Ranglisten-Abruf für diesen Verein ist deaktiviert.',
                        updated: 0,
                        errors: []
                    }
                };
            }

            const effectiveClubId = (club.associationMemberNumber || '').trim() || account.clubId;
            const effectiveFedNickname = (club.myTischtennisFedNickname || '').trim() || account.fedNickname;

            if (!effectiveClubId || !effectiveFedNickname) {
                console.error('[updateRatingsFromMyTischtennis] - Missing clubId or fedNickname:', {
                    clubId: effectiveClubId,
                    fedNickname: effectiveFedNickname,
                    fromClub: !!(club.associationMemberNumber && club.myTischtennisFedNickname)
                });
                return {
                    status: 400,
                    response: { 
                        message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte in den Vereinseinstellungen hinterlegen oder einmal in myTischtennis einloggen.',
                        updated: 0,
                        errors: [],
                        debug: {
                            hasClubId: !!effectiveClubId,
                            hasFedNickname: !!effectiveFedNickname
                        }
                    }
                };
            }
            
            // 2. Mitglieder laden und prüfen, ob wir zusätzlich die TTR-History-ID auflösen müssen
            const members = await Member.findAll({ where: { clubId } });
            const shouldResolveHistoryPlayerIds = members.some((member) =>
                !member.myTischtennisHistoryPlayerId && (
                    member.myTischtennisPlayerId ||
                    member.ttr != null ||
                    member.qttr != null
                )
            );

            // 3. Ranglisten vom Verein abrufen
            // TTR (aktuell)
            try {
                await (await import('./apiLogService.js')).default.logRequest({
                    userId,
                    method: 'GET',
                    path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}&current=yes`,
                    statusCode: null,
                    requestBody: null,
                    responseBody: null,
                    executionTime: null,
                    errorMessage: 'TTR-Rangliste wird abgerufen...',
                    logType: 'api_request',
                    schedulerJobType: 'mytischtennis_rankings'
                });
            } catch {}
            const ttrStart = Date.now();
            const rankingsCurrent = await myTischtennisClient.getClubRankings(
                session.cookie,
                effectiveClubId,
                effectiveFedNickname,
                'yes',
                { includeHistoryPlayerIds: shouldResolveHistoryPlayerIds }
            );
            try {
                await (await import('./apiLogService.js')).default.logRequest({
                    userId,
                    method: 'GET',
                    path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}&current=yes`,
                    statusCode: rankingsCurrent.success ? 200 : 500,
                    requestBody: null,
                    responseBody: JSON.stringify(rankingsCurrent),
                    executionTime: Date.now() - ttrStart,
                    errorMessage: rankingsCurrent.success ? null : 'TTR-Rangliste Fehler',
                    logType: 'api_request',
                    schedulerJobType: 'mytischtennis_rankings'
                });
            } catch {}

            // QTTR (Quartalswert)
            try {
                await (await import('./apiLogService.js')).default.logRequest({
                    userId,
                    method: 'GET',
                    path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}&current=no`,
                    statusCode: null,
                    requestBody: null,
                    responseBody: null,
                    executionTime: null,
                    errorMessage: 'QTTR-Rangliste wird abgerufen...',
                    logType: 'api_request',
                    schedulerJobType: 'mytischtennis_rankings'
                });
            } catch {}
            const qttrStart = Date.now();
            const rankingsQuarter = await myTischtennisClient.getClubRankings(
                session.cookie,
                effectiveClubId,
                effectiveFedNickname,
                'no',
                { includeHistoryPlayerIds: shouldResolveHistoryPlayerIds }
            );
            let qttrWarning = null;
            try {
                await (await import('./apiLogService.js')).default.logRequest({
                    userId,
                    method: 'GET',
                    path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}&current=no`,
                    statusCode: rankingsQuarter.success ? 200 : 500,
                    requestBody: null,
                    responseBody: JSON.stringify(rankingsQuarter),
                    executionTime: Date.now() - qttrStart,
                    errorMessage: rankingsQuarter.success ? null : 'QTTR-Rangliste Fehler',
                    logType: 'api_request',
                    schedulerJobType: 'mytischtennis_rankings'
                });
            } catch {}
            
            if (!rankingsCurrent.success) {
                return {
                    status: 500,
                    response: { 
                        message: rankingsCurrent.error || 'Fehler beim Abrufen der Rangliste (aktuell)',
                        updated: 0,
                        errors: [],
                        debug: {
                            clubId: effectiveClubId,
                            fedNickname: effectiveFedNickname,
                            rankingsError: rankingsCurrent.error
                        }
                    }
                };
            }
            if (!rankingsQuarter.success) {
                // QTTR optional; nicht hart abbrechen, aber vermerken
                qttrWarning = rankingsQuarter.error || 'QTTR Abruf fehlgeschlagen';
            }
            
            let updated = 0;
            const errors = [];
            if (qttrWarning) {
                errors.push({
                    type: 'warning',
                    message: qttrWarning
                });
            }
            const notFound = [];
            const matched = [];
            
            // Indizes für schnelleres Matching (nach personId und Name)
            const createIndex = (entries) => {
                const byName = new Map();
                const byPersonId = new Map();
                for (const e of (entries || [])) {
                    const key = `${(e.firstname||'').toLowerCase().trim()}|${(e.lastname||'').toLowerCase().trim()}`;
                    if (!byName.has(key)) byName.set(key, e);
                    if (e.personId && !byPersonId.has(String(e.personId))) {
                        byPersonId.set(String(e.personId), e);
                    }
                }
                return { byName, byPersonId };
            };

            const currentIdx = createIndex(rankingsCurrent.entries);
            const quarterIdx = rankingsQuarter.success ? createIndex(rankingsQuarter.entries) : { byName: new Map(), byPersonId: new Map() };

            // 4. Für jedes Mitglied TTR und QTTR aktualisieren
            let updatedTtr = 0;
            let updatedQttr = 0;
            for (const member of members) {
                const firstName = member.firstName;
                const lastName = member.lastName;
                const key = `${(firstName||'').toLowerCase().trim()}|${(lastName||'').toLowerCase().trim()}`;
                const personId = member.myTischtennisPlayerId ? String(member.myTischtennisPlayerId).trim() : null;
                const rankingEntry = personId ? (currentIdx.byPersonId.get(personId) || currentIdx.byName.get(key)) : currentIdx.byName.get(key);
                const rankingQuarterEntry = personId ? (quarterIdx.byPersonId.get(personId) || quarterIdx.byName.get(key)) : quarterIdx.byName.get(key);

                if (rankingEntry || rankingQuarterEntry) {
                    try {
                        const oldTtr = member.ttr;
                        const oldQttr = member.qttr;
                        const historyPlayerId = this._extractTtrHistoryPlayerId(rankingEntry) || this._extractTtrHistoryPlayerId(rankingQuarterEntry);
                        if (rankingEntry && typeof rankingEntry.fedRank === 'number') {
                            member.ttr = rankingEntry.fedRank;
                            if (member.ttr !== oldTtr) updatedTtr++;
                        }
                        if (rankingQuarterEntry && typeof rankingQuarterEntry.fedRank === 'number') {
                            member.qttr = rankingQuarterEntry.fedRank;
                            if (member.qttr !== oldQttr) updatedQttr++;
                        } else if (!rankingsQuarter.success && (member.qttr == null)) {
                            // Fallback: wenn QTTR-Abruf fehlgeschlagen ist und kein Wert vorhanden war,
                            // setze QTTR ersatzweise auf aktuellen TTR, damit die Anzeige nicht leer bleibt
                            if (member.ttr != null) {
                                member.qttr = member.ttr;
                                if (member.qttr !== oldQttr) updatedQttr++;
                            }
                        }
                        if (historyPlayerId && member.myTischtennisHistoryPlayerId !== historyPlayerId) {
                            member.myTischtennisHistoryPlayerId = historyPlayerId;
                        }
                        await member.save();
                        updated++;
                        matched.push({
                            name: `${firstName} ${lastName}`,
                            oldTtr: oldTtr,
                            newTtr: member.ttr,
                            oldQttr: oldQttr,
                            newQttr: member.qttr,
                            historyPlayerId: member.myTischtennisHistoryPlayerId || null
                        });
                    } catch (error) {
                        console.error(`[updateRatingsFromMyTischtennis] - Error updating ${firstName} ${lastName}:`, error);
                        errors.push({
                            member: `${firstName} ${lastName}`,
                            error: error.message
                        });
                    }
                } else {
                    notFound.push(`${firstName} ${lastName}`);
                }
            }
            
            devLog(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
            
            let message = `${updated} Mitglied(er) aktualisiert. (TTR: ${updatedTtr}, QTTR: ${updatedQttr})`;
            if (notFound.length > 0) {
                message += ` ${notFound.length} nicht in myTischtennis-Rangliste gefunden.`;
            }
            if (errors.length > 0) {
                message += ` ${errors.length} Fehler beim Speichern.`;
            }
            
            return {
                status: 200,
                response: { 
                    message: message,
                    updated: updated,
                    matched: matched,
                    notFound: notFound,
                    errors: errors,
                    totalEntries: rankingsCurrent.entries.length,
                    totalMembers: members.length
                }
            };
        } catch (error) {
            console.error('[updateRatingsFromMyTischtennis] - Error:', error);
            return {
                status: 500,
                response: { 
                    message: error.message || 'Fehler beim Aktualisieren',
                    updated: 0,
                    errors: [error.message]
                }
            };
        }
    }

    async updateRatingsFromMyTischtennis(userToken, clubId) {
        await checkAccess(userToken, clubId);
        const user = await getUserByToken(userToken);
        return this._updateRatingsInternal(user.id, clubId);
    }

    async updateRatingsFromMyTischtennisByUserId(userId, clubId) {
        // Zugriff prüfen: User muss Zugriff auf den Club haben
        const allowed = await hasUserClubAccess(userId, clubId);
        if (!allowed) {
            return {
                status: 403,
                response: {
                    message: 'noaccess',
                    updated: 0,
                    errors: ['noaccess']
                }
            };
        }
        return this._updateRatingsInternal(userId, clubId);
    }

    async getMemberTtrHistory(userToken, clubId, memberId) {
        await checkAccess(userToken, clubId);

        const member = await Member.findOne({
            where: { id: memberId, clubId }
        });

        if (!member) {
            return {
                status: 404,
                response: { success: false, error: 'Mitglied nicht gefunden.' }
            };
        }

        const history = await MemberTtrHistory.findAll({
            where: { memberId: member.id, clubId },
            order: [['sourceDate', 'DESC'], ['createdAt', 'DESC']]
        });

        const entries = history.map(entry => ({
            id: entry.id,
            sourceDate: entry.sourceDate,
            ttr: entry.ttr,
            qttr: entry.qttr,
            label: entry.label,
            sourceType: entry.sourceType,
            fetchedAt: entry.fetchedAt
        }));

        const latestFetchedAt = history.reduce((latest, entry) => {
            if (!entry.fetchedAt) {
                return latest;
            }
            const fetchedAt = new Date(entry.fetchedAt);
            if (!latest || fetchedAt > latest) {
                return fetchedAt;
            }
            return latest;
        }, null);
        const resolvedHistoryPlayerId = member.myTischtennisHistoryPlayerId || history.find(entry => entry.playerId)?.playerId || null;

        return {
            status: 200,
            response: {
                success: true,
                member: {
                    id: member.id,
                    firstName: member.firstName,
                    lastName: member.lastName,
                    ttr: member.ttr,
                    qttr: member.qttr,
                    myTischtennisPlayerId: member.myTischtennisPlayerId || null,
                    myTischtennisHistoryPlayerId: resolvedHistoryPlayerId
                },
                history: entries,
                meta: {
                    count: entries.length,
                    lastFetchedAt: latestFetchedAt ? latestFetchedAt.toISOString() : null,
                    refreshPolicy: this._buildTtrHistoryRefreshPolicy(latestFetchedAt)
                }
            }
        };
    }

    async refreshMemberTtrHistory(userToken, clubId, memberId) {
        await checkAccess(userToken, clubId);

        const user = await getUserByToken(userToken);

        const member = await Member.findOne({
            where: { id: memberId, clubId }
        });

        if (!member) {
            return {
                status: 404,
                response: { success: false, error: 'Mitglied nicht gefunden.' }
            };
        }

        const latestHistoryEntry = await MemberTtrHistory.findOne({
            where: { memberId: member.id, clubId },
            order: [['fetchedAt', 'DESC'], ['createdAt', 'DESC']]
        });

        const historyPlayerId = member.myTischtennisHistoryPlayerId || latestHistoryEntry?.playerId || null;
        const refreshPolicy = this._buildTtrHistoryRefreshPolicy(latestHistoryEntry?.fetchedAt || null);

        if (!refreshPolicy.canRefresh) {
            return {
                status: 200,
                response: {
                    success: true,
                    skipped: true,
                    message: refreshPolicy.message,
                    member: {
                        id: member.id,
                        myTischtennisPlayerId: member.myTischtennisPlayerId || null,
                        myTischtennisHistoryPlayerId: historyPlayerId
                    },
                    meta: {
                        lastFetchedAt: latestHistoryEntry?.fetchedAt ? new Date(latestHistoryEntry.fetchedAt).toISOString() : null,
                        refreshPolicy
                    }
                }
            };
        }

        if (!historyPlayerId) {
            return {
                status: 409,
                response: {
                    success: false,
                    error: 'Für dieses Mitglied ist noch keine myTischtennis-TTR-History-ID vorhanden. Bitte zuerst die myTischtennis-Ranglisten aktualisieren.'
                }
            };
        }

        const myTischtennisService = (await import('./myTischtennisService.js')).default;
        const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;

        let session;
        try {
            session = await myTischtennisService.getSession(user.id);
        } catch (sessionError) {
            try {
                await myTischtennisService.verifyLogin(user.id);
                session = await myTischtennisService.getSession(user.id);
            } catch (loginError) {
                return {
                    status: 401,
                    response: {
                        success: false,
                        needsReauth: true,
                        error: 'myTischtennis-Session abgelaufen. Bitte einmal neu einloggen.'
                    }
                };
            }
        }

        const rootEndpoint = `/rankings/ttr-historie?player-id=${encodeURIComponent(historyPlayerId)}&_data=root`;
        const historyEndpoint = `/rankings/ttr-historie?player-id=${encodeURIComponent(historyPlayerId)}&show=everything&_data=routes%2F%24`;

        const [rootResult, historyResult] = await Promise.all([
            myTischtennisClient.authenticatedRequest(rootEndpoint, session.cookie, { method: 'GET' }),
            myTischtennisClient.authenticatedRequest(historyEndpoint, session.cookie, { method: 'GET' })
        ]);

        if (!rootResult.success) {
            return {
                status: 502,
                response: {
                    success: false,
                    error: rootResult.error || 'myTischtennis-Root-Endpunkt konnte nicht geladen werden.'
                }
            };
        }

        if (!historyResult.success) {
            return {
                status: 502,
                response: {
                    success: false,
                    error: historyResult.error || 'myTischtennis-TTR-Historie konnte nicht geladen werden.'
                }
            };
        }

        const parsedHistory = this._parseMyTischtennisHistoryResponse(historyResult.data);
        if (!parsedHistory.success) {
            return {
                status: 502,
                response: {
                    success: false,
                    error: parsedHistory.error || 'Die myTischtennis-TTR-Historie konnte nicht verarbeitet werden.'
                }
            };
        }

        const fetchedAt = new Date();
        const historyEntries = this._mapTtrHistoryEventsToRows({
            clubId,
            memberId: member.id,
            playerId: historyPlayerId,
            fetchedAt,
            historyData: parsedHistory.historyData
        });

        await MemberTtrHistory.destroy({
            where: { memberId: member.id, clubId }
        });

        if (historyEntries.length > 0) {
            await MemberTtrHistory.bulkCreate(historyEntries);
        }

        if (member.myTischtennisHistoryPlayerId !== historyPlayerId) {
            member.myTischtennisHistoryPlayerId = historyPlayerId;
            await member.save();
        }

        return {
            status: 200,
            response: {
                success: true,
                message: `${historyEntries.length} TTR-Historien-Einträge aktualisiert.`,
                member: {
                    id: member.id,
                    myTischtennisPlayerId: member.myTischtennisPlayerId || null,
                    myTischtennisHistoryPlayerId: historyPlayerId
                },
                meta: {
                    count: historyEntries.length,
                    lastFetchedAt: fetchedAt.toISOString(),
                    refreshPolicy: this._buildTtrHistoryRefreshPolicy(fetchedAt)
                }
            }
        };
    }

    _extractTtrHistoryPlayerId(entry) {
        if (!entry || typeof entry !== 'object') {
            return null;
        }

        const directCandidates = [
            entry.myTischtennisHistoryPlayerId,
            entry.historyPlayerId,
            entry.history_player_id,
            entry.ttrHistoryPlayerId,
            entry.ttr_history_player_id
        ];

        for (const candidate of directCandidates) {
            if (typeof candidate === 'string' && /^P[A-Z0-9]+$/i.test(candidate.trim())) {
                return candidate.trim();
            }
        }

        const serialized = JSON.stringify(entry);
        const urlMatch = serialized.match(/player-id=([P][A-Z0-9]+)/i);
        if (urlMatch?.[1]) {
            return urlMatch[1];
        }

        const fieldMatch = serialized.match(/"(?:historyPlayerId|history_player_id|ttrHistoryPlayerId|ttr_history_player_id|playerId|player_id)"\s*:\s*"(P[A-Z0-9]+)"/i);
        return fieldMatch?.[1] || null;
    }

    _getBerlinDateParts(value = new Date()) {
        const formatter = new Intl.DateTimeFormat('en-CA', {
            timeZone: 'Europe/Berlin',
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit',
            hourCycle: 'h23'
        });

        const parts = formatter.formatToParts(value).reduce((acc, part) => {
            if (part.type !== 'literal') {
                acc[part.type] = part.value;
            }
            return acc;
        }, {});

        return {
            year: parts.year,
            month: parts.month,
            day: parts.day,
            hour: Number(parts.hour),
            minute: Number(parts.minute),
            second: Number(parts.second),
            dateKey: `${parts.year}-${parts.month}-${parts.day}`
        };
    }

    _buildTtrHistoryRefreshPolicy(lastFetchedAt) {
        const now = new Date();
        const nowParts = this._getBerlinDateParts(now);
        const hour = nowParts.hour;

        if (hour < 7) {
            return {
                canRefresh: false,
                reason: 'before_7am',
                message: 'Die TTR-Historie wird nur einmal täglich und erst nach 07:00 Uhr aktualisiert.',
                nextRefreshDate: nowParts.dateKey
            };
        }

        if (!lastFetchedAt) {
            return {
                canRefresh: true,
                reason: 'no_cache',
                message: 'Die TTR-Historie kann jetzt aktualisiert werden.',
                nextRefreshDate: nowParts.dateKey
            };
        }

        const fetchedParts = this._getBerlinDateParts(new Date(lastFetchedAt));
        if (fetchedParts.dateKey === nowParts.dateKey) {
            return {
                canRefresh: false,
                reason: 'already_refreshed_today',
                message: 'Die TTR-Historie wurde heute bereits aktualisiert.',
                nextRefreshDate: nowParts.dateKey
            };
        }

        return {
            canRefresh: true,
            reason: 'stale_cache',
            message: 'Die TTR-Historie kann jetzt aktualisiert werden.',
            nextRefreshDate: nowParts.dateKey
        };
    }

    _parseMyTischtennisHistoryResponse(rawPayload) {
        try {
            const parsed = this._splitDeferredJsonPayload(rawPayload);
            const deferredData = Object.values(parsed.deferred || {}).find((value) => value && Array.isArray(value.event));

            if (!deferredData) {
                return {
                    success: false,
                    error: 'Keine TTR-Historien-Daten in der myTischtennis-Antwort gefunden.'
                };
            }

            return {
                success: true,
                root: parsed.root,
                historyData: deferredData
            };
        } catch (error) {
            return {
                success: false,
                error: error.message || 'TTR-Historien-Antwort konnte nicht geparst werden.'
            };
        }
    }

    _splitDeferredJsonPayload(rawPayload) {
        if (rawPayload && typeof rawPayload === 'object' && !Array.isArray(rawPayload)) {
            return { root: rawPayload, deferred: {} };
        }

        const text = typeof rawPayload === 'string' ? rawPayload : String(rawPayload || '');
        const lines = text.split(/\r?\n/);
        let root = null;
        const deferred = {};

        for (const rawLine of lines) {
            const line = rawLine.trim();
            if (!line) {
                continue;
            }

            if (line.startsWith('data:')) {
                const payload = line.slice(5).trim();
                if (!payload) {
                    continue;
                }
                const parsed = JSON.parse(payload);
                Object.assign(deferred, parsed);
                continue;
            }

            if (!root && line.startsWith('{')) {
                root = JSON.parse(line);
            }
        }

        return { root, deferred };
    }

    _mapTtrHistoryEventsToRows({ clubId, memberId, playerId, fetchedAt, historyData }) {
        const events = Array.isArray(historyData?.event) ? historyData.event : [];
        const rows = [];
        const seenKeys = new Set();

        for (const event of events) {
            const sourceDate = this._normalizeTtrHistoryDate(event?.event_date_time || event?.formattedEventDate);
            if (!sourceDate) {
                continue;
            }

            const key = `${event?.event_id || 'event'}:${sourceDate}:${event?.ttr_after ?? ''}`;
            if (seenKeys.has(key)) {
                continue;
            }
            seenKeys.add(key);

            rows.push({
                memberId,
                clubId,
                playerId,
                sourceDate,
                ttr: Number.isFinite(Number(event?.ttr_after)) ? Number(event.ttr_after) : null,
                qttr: null,
                label: event?.event_name || null,
                sourceType: event?.type || null,
                fetchedAt,
                rawPayload: event
            });
        }

        return rows.sort((a, b) => String(b.sourceDate).localeCompare(String(a.sourceDate)));
    }

    _normalizeTtrHistoryDate(value) {
        if (!value) {
            return null;
        }

        if (typeof value === 'string') {
            const isoMatch = value.match(/^(\d{4}-\d{2}-\d{2})/);
            if (isoMatch?.[1]) {
                return isoMatch[1];
            }

            const germanMatch = value.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
            if (germanMatch) {
                return `${germanMatch[3]}-${germanMatch[2]}-${germanMatch[1]}`;
            }
        }

        const date = new Date(value);
        if (Number.isNaN(date.getTime())) {
            return null;
        }

        return date.toISOString().slice(0, 10);
    }

    async rotateMemberImage(userToken, clubId, memberId, imageId, direction) {
        try {
            await checkAccess(userToken, clubId);
            const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
            
            if (!member) {
                return { status: 404, response: { success: false, error: 'Member not found in this club' } };
            }
            
            await this._migrateLegacyImage(memberId);

            const imageRecord = await MemberImage.findOne({
                where: { id: imageId, memberId }
            });

            if (!imageRecord) {
                return { status: 404, response: { success: false, error: 'Image not found' } };
            }
            
            const imagePath = path.join('images', 'members', String(memberId), imageRecord.fileName);
            if (!fs.existsSync(imagePath)) {
                return { status: 404, response: { success: false, error: 'Image file not found on disk' } };
            }

            const imageBuffer = await fs.promises.readFile(imagePath);
            const rotationAngle = direction === 'left' ? -90 : 90;
            
            const rotatedBuffer = await sharp(imageBuffer)
                .rotate(rotationAngle)
                .jpeg({ quality: 80 })
                .toBuffer();
            
            await fs.promises.writeFile(imagePath, rotatedBuffer);

            await imageRecord.update({ updatedAt: new Date() });

            const imageData = await this._prepareMemberImages(member, { forceReload: true });
            const updatedImage = imageData.images.find(img => img.id === imageRecord.id) || null;
            
            return { 
                status: 200, 
                response: { 
                    success: true, 
                    message: `Bild um ${rotationAngle}° gedreht`,
                    direction,
                    rotation: rotationAngle,
                    image: updatedImage,
                    images: imageData.images,
                    primaryImageId: imageData.primaryImageId,
                    primaryImageUrl: imageData.primaryImageUrl
                } 
            };
        } catch (error) {
            console.error('[rotateMemberImage] - Error:', error);
            return { 
                status: 500, 
                response: { 
                    success: false, 
                    error: 'Fehler beim Drehen des Bildes: ' + error.message 
                } 
            };
        }
    }

    async deleteMemberImage(userToken, clubId, memberId, imageId) {
        let transaction = null;
        try {
            await checkAccess(userToken, clubId);
            const member = await Member.findOne({ where: { id: memberId, clubId } });
            if (!member) {
                return { status: 404, response: { success: false, error: 'Member not found in this club' } };
            }

            await this._migrateLegacyImage(memberId);

            transaction = await MemberImage.sequelize.transaction();
            const imageRecord = await MemberImage.findOne({
                where: { id: imageId, memberId },
                transaction,
                lock: transaction.LOCK.UPDATE
            });

            if (!imageRecord) {
                await transaction.rollback();
                transaction = null;
                return { status: 404, response: { success: false, error: 'Image not found' } };
            }

            const imagePath = path.join('images', 'members', String(memberId), imageRecord.fileName);

            await imageRecord.destroy({ transaction });
            await this._resequenceMemberImages(memberId, { transaction });

            await transaction.commit();
            transaction = null;

            if (fs.existsSync(imagePath)) {
                await fs.promises.unlink(imagePath).catch(err => {
                    console.warn('[deleteMemberImage] - Failed to remove image file:', err);
                });
            }

            const imageData = await this._prepareMemberImages(member, { forceReload: true });

            return {
                status: 200,
                response: {
                    success: true,
                    images: imageData.images,
                    primaryImageId: imageData.primaryImageId,
                    primaryImageUrl: imageData.primaryImageUrl
                }
            };
        } catch (error) {
            if (transaction) {
                try {
                    await transaction.rollback();
                } catch (rollbackError) {
                    console.error('[deleteMemberImage] - Rollback failed:', rollbackError);
                }
            }
            console.error('[deleteMemberImage] - Error:', error);
            return { status: 500, response: { success: false, error: 'Failed to delete image' } };
        }
    }

    async setPrimaryMemberImage(userToken, clubId, memberId, imageId) {
        let transaction = null;
        try {
            await checkAccess(userToken, clubId);
            const member = await Member.findOne({ where: { id: memberId, clubId } });
            if (!member) {
                return { status: 404, response: { success: false, error: 'Member not found in this club' } };
            }

            await this._migrateLegacyImage(memberId);

            transaction = await MemberImage.sequelize.transaction();
            const imageRecord = await MemberImage.findOne({
                where: { id: imageId, memberId },
                transaction,
                lock: transaction.LOCK.UPDATE
            });

            if (!imageRecord) {
                await transaction.rollback();
                transaction = null;
                return { status: 404, response: { success: false, error: 'Image not found' } };
            }

            await this._setPrimaryImage(memberId, imageId, { transaction });

            await transaction.commit();
            transaction = null;

            const imageData = await this._prepareMemberImages(member, { forceReload: true });

            return {
                status: 200,
                response: {
                    success: true,
                    images: imageData.images,
                    primaryImageId: imageData.primaryImageId,
                    primaryImageUrl: imageData.primaryImageUrl
                }
            };
        } catch (error) {
            if (transaction) {
                try {
                    await transaction.rollback();
                } catch (rollbackError) {
                    console.error('[setPrimaryMemberImage] - Rollback failed:', rollbackError);
                }
            }
            console.error('[setPrimaryMemberImage] - Error:', error);
            return { status: 500, response: { success: false, error: 'Failed to set primary image' } };
        }
    }

    async quickUpdateTestMembership(userToken, clubId, memberId) {
        try {
            await checkAccess(userToken, clubId);
            const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
            if (!member) {
                return { status: 404, response: { error: 'Member not found in this club' } };
            }
            
            if (!member.testMembership) {
                return { status: 400, response: { error: 'Member is not a test member' } };
            }
            
            member.testMembership = false;
            await member.save();
            
            return {
                status: 200,
                response: { success: true, message: 'Testmitgliedschaft entfernt' }
            };
        } catch (error) {
            console.error('[quickUpdateTestMembership] - Error:', error);
            return { status: 500, response: { error: 'Failed to update test membership' } };
        }
    }

    async quickUpdateMemberFormHandedOver(userToken, clubId, memberId) {
        try {
            await checkAccess(userToken, clubId);
            const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
            if (!member) {
                return { status: 404, response: { error: 'Member not found in this club' } };
            }
            
            if (member.memberFormHandedOver) {
                return { status: 400, response: { error: 'Member form already handed over' } };
            }
            
            member.memberFormHandedOver = true;
            await member.save();
            
            return {
                status: 200,
                response: { success: true, message: 'Mitgliedsformular als ausgehändigt markiert' }
            };
        } catch (error) {
            console.error('[quickUpdateMemberFormHandedOver] - Error:', error);
            return { status: 500, response: { error: 'Failed to update member form status' } };
        }
    }

    async quickDeactivateMember(userToken, clubId, memberId) {
        try {
            await checkAccess(userToken, clubId);
            const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
            if (!member) {
                return { status: 404, response: { error: 'Member not found in this club' } };
            }
            
            if (!member.active) {
                return { status: 400, response: { error: 'Member is already inactive' } };
            }
            
            member.active = false;
            await member.save();
            
            return {
                status: 200,
                response: { success: true, message: 'Mitglied deaktiviert' }
            };
        } catch (error) {
            console.error('[quickDeactivateMember] - Error:', error);
            return { status: 500, response: { error: 'Failed to deactivate member' } };
        }
    }

    async _ensureImageDirectory(memberId) {
        const directoryPath = path.join('images', 'members', String(memberId));
        await fs.promises.mkdir(directoryPath, { recursive: true });
        return directoryPath;
    }

    async _fetchMemberImages(memberId, transaction = null) {
        return MemberImage.findAll({
            where: { memberId },
            order: [['sortOrder', 'ASC'], ['id', 'ASC']],
            transaction
        });
    }

    async _prepareMemberImages(member, options = {}) {
        const { forceReload = false } = options;
        let images = [];

        if (!forceReload && Array.isArray(member.images) && member.images.length > 0) {
            images = member.images;
        } else {
            images = await this._fetchMemberImages(member.id);
        }

        if (!images || images.length === 0) {
            const migrated = await this._migrateLegacyImage(member.id);
            if (migrated) {
                images = await this._fetchMemberImages(member.id);
            }
        }

        if (!images || images.length === 0) {
            return {
                images: [],
                primaryImageId: null,
                primaryImageUrl: null,
                hasImage: false
            };
        }

        const mappedImages = images
            .map(image => this._mapMemberImageRecord(member, image))
            .sort((a, b) => {
                const sortA = Number.isFinite(a.sortOrder) ? a.sortOrder : parseInt(a.sortOrder || '0', 10);
                const sortB = Number.isFinite(b.sortOrder) ? b.sortOrder : parseInt(b.sortOrder || '0', 10);
                if (sortA !== sortB) {
                    return sortA - sortB;
                }
                return (a.id || 0) - (b.id || 0);
            });

        const primary = mappedImages[0] || null;

        mappedImages.forEach((image, index) => {
            image.isPrimary = index === 0;
        });

        return {
            images: mappedImages,
            primaryImageId: primary ? primary.id : null,
            primaryImageUrl: primary ? primary.url : null,
            hasImage: mappedImages.length > 0
        };
    }

    _mapMemberImageRecord(member, image) {
        const data = image && typeof image.toJSON === 'function' ? image.toJSON() : image;
        const updatedAt = data.updatedAt ? new Date(data.updatedAt) : null;
        const cacheParam = updatedAt && !Number.isNaN(updatedAt.getTime()) ? updatedAt.getTime() : Date.now();

        return {
            id: data.id,
            memberId: member.id,
            fileName: data.fileName,
            sortOrder: data.sortOrder,
            createdAt: data.createdAt,
            updatedAt: data.updatedAt,
            url: `/api/clubmembers/image/${member.clubId}/${member.id}/${data.id}?t=${cacheParam}`
        };
    }

    async _setPrimaryImage(memberId, imageId, options = {}) {
        const { transaction = null } = options;
        const images = await this._fetchMemberImages(memberId, transaction);
        if (!images || images.length === 0) {
            return;
        }

        let nextOrder = 2;
        for (const image of images) {
            if (image.id === imageId) {
                image.sortOrder = 1;
            } else {
                image.sortOrder = nextOrder;
                nextOrder += 1;
            }
            await image.save({ transaction });
        }
    }

    async _resequenceMemberImages(memberId, options = {}) {
        const { transaction = null } = options;
        const images = await this._fetchMemberImages(memberId, transaction);
        let order = 1;
        for (const image of images) {
            if (image.sortOrder !== order) {
                image.sortOrder = order;
                await image.save({ transaction });
            }
            order += 1;
        }
        return images;
    }

    async _migrateLegacyImage(memberId) {
        const legacyPath = this._findLegacyImagePath(memberId);
        if (!legacyPath) {
            return null;
        }

        const existing = await MemberImage.findOne({ where: { memberId } });
        const directoryPath = await this._ensureImageDirectory(memberId);

        if (existing) {
            const fileName = existing.fileName || `${existing.id}.jpg`;
            const targetPath = path.join(directoryPath, fileName);
            await this._processLegacyImage(legacyPath, targetPath);
            if (!existing.fileName || existing.fileName !== fileName) {
                existing.fileName = fileName;
                await existing.save();
            }
            return existing;
        }

        let transaction = null;
        try {
            transaction = await MemberImage.sequelize.transaction();
            const imageRecord = await MemberImage.create({
                memberId,
                fileName: '',
                sortOrder: 1
            }, { transaction });

            const fileName = `${imageRecord.id}.jpg`;
            const targetPath = path.join(directoryPath, fileName);
            await this._processLegacyImage(legacyPath, targetPath);

            imageRecord.fileName = fileName;
            await imageRecord.save({ transaction });
            await transaction.commit();
            transaction = null;
            return imageRecord;
        } catch (error) {
            if (transaction) {
                try {
                    await transaction.rollback();
                } catch (rollbackError) {
                    console.error('[MemberService] - Legacy migration rollback failed:', rollbackError);
                }
            }
            console.error('[MemberService] - Legacy image migration failed:', error);
            return null;
        }
    }

    _findLegacyImagePath(memberId) {
        const directory = path.join('images', 'members');
        const extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
        for (const ext of extensions) {
            const lowerPath = path.join(directory, `${memberId}.${ext}`);
            if (fs.existsSync(lowerPath)) {
                return lowerPath;
            }
            const upperPath = path.join(directory, `${memberId}.${ext.toUpperCase()}`);
            if (fs.existsSync(upperPath)) {
                return upperPath;
            }
        }
        return null;
    }

    async _processLegacyImage(sourcePath, targetPath) {
        try {
            await sharp(sourcePath)
                .resize(600, 600, { fit: 'inside' })
                .jpeg({ quality: 80 })
                .toFile(targetPath);
        } catch (error) {
            console.error('[MemberService] - Failed to process legacy image, falling back to copy:', error);
            await fs.promises.copyFile(sourcePath, targetPath);
        }

        try {
            await fs.promises.unlink(sourcePath);
        } catch (unlinkError) {
            // Datei konnte nicht gelöscht werden – loggen, aber nicht blockieren
            console.warn('[MemberService] - Unable to remove legacy image after migration:', unlinkError.message || unlinkError);
        }
    }

    async generateMemberGallery(userToken, clubId, size = 200, createImage = true) {
        try {
            await checkAccess(userToken, clubId);

            // Validiere und setze tileDimension (nur 100, 150, 200 erlaubt)
            const validSizes = [100, 150, 200];
            const tileDimension = validSizes.includes(size) ? size : 200;

            const members = await Member.findAll({
                where: {
                    clubId,
                    active: true
                },
                include: [
                    {
                        model: MemberImage,
                        as: 'images',
                        required: false,
                        attributes: ['id', 'fileName', 'sortOrder', 'updatedAt']
                    }
                ],
                order: [['lastName', 'ASC'], ['firstName', 'ASC']]
            });

            const galleryEntries = [];

            for (const member of members) {
                let images = [];
                if (Array.isArray(member.images) && member.images.length > 0) {
                    images = member.images.map(img => img.toJSON ? img.toJSON() : img);
                } else {
                    const migrated = await this._migrateLegacyImage(member.id);
                    if (migrated) {
                        images = [migrated.toJSON ? migrated.toJSON() : migrated];
                    }
                }

                if (!images || images.length === 0) {
                    continue;
                }

                const primaryImage = this._selectPrimaryImage(images);
                if (!primaryImage) {
                    continue;
                }

                const fileName = primaryImage.fileName || `${primaryImage.id}.jpg`;
                const filePath = path.join('images', 'members', String(member.id), fileName);
                if (!fs.existsSync(filePath)) {
                    continue;
                }

                galleryEntries.push({
                    filePath,
                    fullName: `${member.firstName || ''} ${member.lastName || ''}`.trim() || member.lastName || member.firstName || 'Unbekannt',
                    memberId: member.id,
                    firstName: member.firstName || '',
                    lastName: member.lastName || ''
                });
            }

            if (galleryEntries.length === 0) {
                return {
                    status: 404,
                    error: 'Keine aktiven Mitglieder mit Bildern gefunden'
                };
            }
            
            // Wenn kein Bild erstellt werden soll (z.B. bei format=json), nur die Liste zurückgeben
            if (!createImage) {
                return {
                    status: 200,
                    galleryEntries
                };
            }
            
            // Maximale Breite für die Galerie (Dialog-Breite 900px - 32px Padding = 868px)
            const maxGalleryWidth = 868;
            // Berechne maximale Anzahl Spalten, die in die Breite passen
            // Formel: n * tileDimension <= maxGalleryWidth
            const maxColumns = Math.max(1, Math.floor(maxGalleryWidth / tileDimension));
            const columns = Math.min(maxColumns, galleryEntries.length);
            const rows = Math.ceil(galleryEntries.length / columns);
            
            // Canvas-Breite: columns * tileDimension (kein Abstand zwischen Bildern)
            const canvasWidth = columns * tileDimension;
            // Canvas-Höhe: rows * tileDimension (kein Abstand zwischen Zeilen)
            const canvasHeight = rows * tileDimension;
            
            const backgroundColor = '#101010';
            const composites = [];

            let index = 0;
            for (const entry of galleryEntries) {
                const row = Math.floor(index / columns);
                const col = index % columns;
                // Linke Position: col * tileDimension (kein Abstand)
                const left = col * tileDimension;
                // Obere Position: row * tileDimension (kein Abstand, y += 200 für nächste Zeile)
                const top = row * tileDimension;

                const resizedBuffer = await sharp(entry.filePath)
                    .resize(tileDimension, tileDimension, { fit: 'cover' })
                    .toBuffer();

                composites.push({
                    input: resizedBuffer,
                    top,
                    left
                });

                const textHeight = Math.max(36, Math.round(tileDimension * 0.18));
                const nameOverlay = this._buildNameOverlay(tileDimension, textHeight, entry.fullName);
                composites.push({
                    input: Buffer.from(nameOverlay),
                    top: top + tileDimension - textHeight,
                    left
                });

                index += 1;
            }

            const buffer = await sharp({
                create: {
                    width: canvasWidth,
                    height: canvasHeight,
                    channels: 3,
                    background: backgroundColor
                }
            })
                .composite(composites)
                .png()
                .toBuffer();

            return {
                status: 200,
                buffer,
                galleryEntries // Für interaktive Galerie
            };
        } catch (error) {
            console.error('[generateMemberGallery] - Error:', error);
            return {
                status: error.statusCode || 500,
                error: 'Failed to generate member gallery'
            };
        }
    }

    async cleanupInactiveMembersWithoutRecentTraining(options = {}) {
        const { clubId = null, inactiveDays = 365 } = options;
        const cutoffDate = new Date();
        cutoffDate.setHours(0, 0, 0, 0);
        cutoffDate.setDate(cutoffDate.getDate() - inactiveDays);

        const diaryDateWhere = {};
        if (clubId) {
            diaryDateWhere.clubId = clubId;
        }

        const lastTrainingRows = await Participant.findAll({
            attributes: [
                'memberId',
                [fn('MAX', col('diaryDate.date')), 'lastTrainingDate']
            ],
            include: [
                {
                    model: DiaryDate,
                    as: 'diaryDate',
                    attributes: [],
                    required: true,
                    where: diaryDateWhere
                }
            ],
            where: {
                attendanceStatus: 'present'
            },
            group: ['memberId'],
            raw: true
        });

        const staleMemberIds = lastTrainingRows
            .filter((row) => {
                const lastTrainingDate = row.lastTrainingDate ? new Date(row.lastTrainingDate) : null;
                return lastTrainingDate && !Number.isNaN(lastTrainingDate.getTime()) && lastTrainingDate < cutoffDate;
            })
            .map((row) => row.memberId);

        if (staleMemberIds.length === 0) {
            return {
                success: true,
                deletedCount: 0,
                deletedMembers: []
            };
        }

        const memberWhere = {
            id: { [Op.in]: staleMemberIds },
            active: false
        };

        if (clubId) {
            memberWhere.clubId = clubId;
        }

        const membersToDelete = await Member.findAll({
            where: memberWhere
        });

        const deletedMembers = [];

        for (const member of membersToDelete) {
            const result = await this._deleteMemberWithRelations(member);
            deletedMembers.push(result);
        }

        return {
            success: true,
            deletedCount: deletedMembers.length,
            deletedMembers
        };
    }

    _selectPrimaryImage(images) {
        if (!Array.isArray(images) || images.length === 0) {
            return null;
        }
        return images
            .slice()
            .sort((a, b) => {
                const sortA = Number.isFinite(a.sortOrder) ? a.sortOrder : parseInt(a.sortOrder || '0', 10);
                const sortB = Number.isFinite(b.sortOrder) ? b.sortOrder : parseInt(b.sortOrder || '0', 10);
                if (sortA !== sortB) {
                    return sortA - sortB;
                }
                return (a.id || 0) - (b.id || 0);
            })[0];
    }

    _computeGalleryGrid(count, canvasWidth, canvasHeight, gap) {
        const tileDimension = 200;
        let best = {
            columns: 1,
            rows: count,
            tileWidth: tileDimension,
            tileHeight: tileDimension
        };

        for (let columns = 1; columns <= count; columns += 1) {
            const rows = Math.ceil(count / columns);
            const availableWidth = canvasWidth - (columns + 1) * gap;
            const availableHeight = canvasHeight - (rows + 1) * gap;
            if (availableWidth <= 0 || availableHeight <= 0) {
                continue;
            }

            const tileWidth = Math.min(tileDimension, Math.floor(availableWidth / columns));
            const tileHeight = Math.min(tileDimension, Math.floor(availableHeight / rows));
            if (tileWidth <= 0 || tileHeight <= 0) {
                continue;
            }

            if (columns > best.columns || (columns === best.columns && rows < best.rows)) {
                best = {
                    columns,
                    rows,
                    tileWidth,
                    tileHeight
                };
            }
        }

        return best;
    }

    _buildNameOverlay(width, height, name) {
        const safeName = this._escapeSvgText(name || '');
        const fontSize = Math.max(24, Math.round(height * 0.6));
        return `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
  <rect x="0" y="0" width="${width}" height="${height}" fill="rgba(0,0,0,0.55)" />
  <text x="50%" y="60%" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="${fontSize}" fill="#ff3333" font-weight="600">
    ${safeName}
  </text>
</svg>`;
    }

    _escapeSvgText(text) {
        return String(text || '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/\"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    async _deleteMemberWithRelations(member) {
        const sequelize = Member.sequelize;
        const transaction = await sequelize.transaction();

        try {
            const {
                default: DiaryMemberActivity
            } = await import('../models/DiaryMemberActivity.js');
            const {
                default: DiaryMemberNote
            } = await import('../models/DiaryMemberNote.js');
            const {
                default: DiaryMemberTag
            } = await import('../models/DiaryMemberTag.js');
            const {
                default: DiaryNote
            } = await import('../models/DiaryNote.js');
            const {
                default: MemberNote
            } = await import('../models/MemberNote.js');
            const {
                default: MemberContact
            } = await import('../models/MemberContact.js');
            const {
                default: MemberTrainingGroup
            } = await import('../models/MemberTrainingGroup.js');
            const {
                default: TournamentMember
            } = await import('../models/TournamentMember.js');
            const {
                default: OfficialCompetitionMember
            } = await import('../models/OfficialCompetitionMember.js');
            const {
                default: Accident
            } = await import('../models/Accident.js');

            const participantIds = (await Participant.findAll({
                where: { memberId: member.id },
                attributes: ['id'],
                raw: true,
                transaction
            })).map((participant) => participant.id);

            if (participantIds.length > 0) {
                await DiaryMemberActivity.destroy({
                    where: { participantId: { [Op.in]: participantIds } },
                    transaction
                });
            }

            await DiaryMemberNote.destroy({ where: { memberId: member.id }, transaction });
            await DiaryMemberTag.destroy({ where: { memberId: member.id }, transaction });
            await DiaryNote.destroy({ where: { memberId: member.id }, transaction });
            await MemberNote.destroy({ where: { memberId: member.id }, transaction });
            await MemberTtrHistory.destroy({ where: { memberId: member.id }, transaction });
            await MemberContact.destroy({ where: { memberId: member.id }, transaction });
            await MemberTrainingGroup.destroy({ where: { memberId: member.id }, transaction });
            await OfficialCompetitionMember.destroy({ where: { memberId: member.id }, transaction });
            await Accident.destroy({ where: { memberId: member.id }, transaction });
            await TournamentMember.destroy({ where: { clubMemberId: member.id }, transaction });
            await Participant.destroy({ where: { memberId: member.id }, transaction });
            await MemberImage.destroy({ where: { memberId: member.id }, transaction });
            await Member.destroy({ where: { id: member.id }, transaction });

            await transaction.commit();

            const imageDirectory = path.join('images', 'members', String(member.id));
            try {
                await fs.promises.rm(imageDirectory, { recursive: true, force: true });
            } catch (fileError) {
                console.warn('[cleanupInactiveMembersWithoutRecentTraining] - Failed to remove image directory:', fileError.message);
            }

            return {
                id: member.id,
                clubId: member.clubId,
                firstName: member.firstName,
                lastName: member.lastName
            };
        } catch (error) {
            await transaction.rollback();
            throw error;
        }
    }
}

export default new MemberService();
