Source: Router/guest/controller.js

/**
 * Guest Controller
 * 
 * Contains all the business logic for guest operations:
 * - Adding individual guests to groups
 * - Bulk adding guests to groups
 * - Creating guest groups from existing groups
 * - Retrieving guests from groups
 * - Deleting guests and guest groups
 * 
 * All functions handle both regular guests (person) and guest groups (ggroup).
 */
// @ts-check
/**
 * @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
 */

import { query, transaction, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates, configs } from '../../utils/compileTemplates.js';
import { getUID, isValidUID } from '../../utils/UUIDs.js';
import { queueAdd, queueAddArray } from '../../tree/treeQueue/treeQueue.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { publishEvent } from '../../utils/events.js';
import { errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js';

/**
 * Fügt mehrere Gäste oder Gastgruppen zu einer Gruppe hinzu.
 * @param {Array} newGuests - Array mit UIDs der Personen/Gruppen, die als Gäste hinzugefügt werden sollen
 * @param {Buffer} UIDgroup - Zielgruppe (als Hex)
 * @param {string} type - 'guest' oder 'ggroup'
 * @param {string} root - Root-Organisation
 * @param {number|null} timestamp - Optionaler Zeitstempel für Backdating
 * @param {string|null} UIDuser - Optional: User, der die Aktion ausführt
 * @returns {Promise<Array>} Liste der neu angelegten Gäste/Gastgruppen
 */
export const addGuests = async (newGuests, UIDgroup, type, root, timestamp = null, UIDuser = null) => {
    try {
        // Lade das passende Template für Gäste oder Gastgruppen
        const template = Templates[HEX2uuid(root)][type];
        let objects;

        if (type === 'guest') {
            // Für jeden neuen Gast: Prüfe, ob schon ein Gast existiert, sonst UID generieren
            objects = await Promise.all(newGuests.map(guest => {
                const asOf = timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : '';
                return query(`SELECT Member.UID,Member.Data,IF(guest.UID,guest.UID,UIDV1()) AS UIDnew , ? AS timestamp,
                    guest.UID AS UIDguest 
                    FROM Member 
                    LEFT JOIN (ObjectBase ${asOf} AS guest 
                            INNER JOIN Links ${asOf} ON (Links.UID=guest.UID AND Links.Type IN ('member','memberA') AND Links.UIDTarget=?)
                        ) ON (guest.UIDBelongsTo=Member.UID AND guest.Type='guest')
                        WHERE Member.UID =? 
                `, [timestamp, UIDgroup, guest], { log: true, cast: ['json'] });
            }));
        }

        if (type === 'ggroup') {
            // Für Gastgruppen analog, aber Typ 'ggroup'
            objects = await Promise.all(newGuests.map(guest => {
                const asOf = timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : '';
                return query(`SELECT Member.UID,Member.Data,IF(guest.UID,guest.UID,UIDV1()) AS UIDnew , ? AS timestamp,
                    guest.UID AS UIDguest 
                    FROM Member 
                    LEFT JOIN (ObjectBase ${asOf} AS guest 
                    INNER JOIN Links ${asOf} ON (Links.UID=guest.UID AND Links.Type IN ('member','memberA') AND Links.UIDTarget=?)
                ) ON (guest.UIDBelongsTo=Member.UID AND guest.Type='ggroup')
                WHERE Member.UID =?
                `, [timestamp, UIDgroup, guest], { cast: ['json'] });
            }));
        }

        // Flache Liste der gefundenen/angelegten Objekte
        objects = objects.map(guest => guest[0]);

        // Lade die Gruppendaten für die Zielgruppe
        const [groupDB] = await query(`SELECT Member.Data FROM Member
            INNER JOIN ObjectBase ON (ObjectBase.UIDBelongsTo=Member.UID)
            WHERE ObjectBase.UID=? `, [UIDgroup], { cast: ['json'] });

        // Bereite die Daten für das Rendern vor
        const guestsDB = objects.map(p => {
            const result = { ...p.Data, UIDguest: p.UIDguest, timestamp: p.timestamp, UID: p.UID, UIDnew: p.UIDnew };
            if (type === 'guest') {
                result.hierarchie = groupDB.Data.hierarchie;
            }
            return result;
        });

        // Rendere die Gäste-Daten aus den Originaldaten
        const guests = await Promise.all(guestsDB.map((p, Index) => (new Promise(async (fullfill, reject) => {
             const rendered=await renderObject(template,{...p,group:groupDB.Data},{session:{root:HEX2uuid(root)}} /*mocked req*/)
             fullfill({
                ...rendered,
                UIDnew: p.UIDnew,
                UIDguest: p.UIDguest,
                Data: type === 'guest' ? JSON.stringify({}) : objects[Index].Data,
                timestamp: p.timestamp
            });
        }))));

        // Schreibe alle neuen Gäste/Gastgruppen in die Datenbank (Transaktion)
        await Promise.all(guests.map(guest =>
            transaction(async (connection) => {
                if (!guest.UIDguest) {
                    // Lege neuen Gast/Gastgruppe an
                    await connection.query(`
                        INSERT INTO ObjectBase(UID,UIDuser,Type,UIDBelongsTo,Title,Display,SortName,FullTextIndex, dindex,hierarchie,stage,gender,Data)
                        VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
                        `,
                        [guest.UIDnew, UUID2hex(UIDuser), type, guest.UID, guest.Title, guest.Display, guest.SortBase, guest.FullTextIndex, guest.dindex,
                            guest.hierarchie, guest.stage, guest.gender, guest.Data])
                } else {
                    // Gast existiert schon, UID übernehmen
                    guest.UIDnew = guest.UIDguest;
                    guest.UIDguest = null;
                }
                // Verknüpfe Gast/Gastgruppe mit der Zielgruppe
                await connection.query(`
                    INSERT IGNORE INTO Links(UID, Type, UIDTarget) VALUES (?,?,?)`, [guest.UIDnew, type === 'ggroup' ? 'memberGA' : 'memberA', UIDgroup]);
            }, { backDate: timestamp ? Math.max(guest.timestamp, timestamp) : null }
            )));

        // Gib die neuen Gäste/Gastgruppen zurück
        return guests.filter(o => !o.UIDguest).map(o => ({ UID: o.UIDnew, UIDBelongsTo: o.UID, timestamp: o.timestamp }));
    } catch (e) {
        // Fehlerbehandlung: Fehler loggen
        errorLoggerUpdate(e);
        throw e;
    }
};

/**
 * Erstellt eine Gastgruppe für eine Zielgruppe und übernimmt Mitglieder/Jobs als Gäste.
 * @param {Object} req - Express request object
 * @param {Buffer} UIDgroup - Zielgruppe (als Hex)
 * @param {Buffer} UIDparent - Eltern-Gruppe (als Hex)
 * @param {number|null} timestamp - Optionaler Zeitstempel
 * @returns {Promise<Object>} Erfolg oder Fehlermeldung
 */
export const addGroupGuest = async (req, UIDgroup, UIDparent, timestamp) => {
    try {
        // 1. Prüfe, ob die Zielgruppe existiert
        const group = await query(`SELECT ObjectBase.Type,ObjectBase.UIDBelongsTo, ObjectBase.UID
            FROM ObjectBase 
            WHERE UID=?`, [UIDgroup]);
        if (!group.length) {
            return { success: false, message: 'group not found' };
        }

        // 2. Prüfe, ob die Eltern-Gruppe existiert und vom richtigen Typ ist
        const parents = await query(`SELECT Type,UIDBelongsTo,UID FROM ObjectBase WHERE UID=? AND Type IN ('group','ggroup')`, [UIDparent]);
        if (!parents.length) {
            return { success: false, message: 'parent group not found' };
        }
        const parent = parents[0].UID;

        // 3. Erstelle die Gastgruppe (ggroup) für die Zielgruppe und verknüpfe sie mit der Eltern-Gruppe
        const guestGroups = await addGuests([UIDgroup], parent, 'ggroup', req.session.root);
        const guestGroup = guestGroups[0];
        queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'ggroup', guestGroup.UID, guestGroup.UIDBelongsTo, null, parent);

        // 4. Ermittle alle Personen und Gäste der Zielgruppe
        const objects = await query(`SELECT ObjectBase.Type,ObjectBase.UIDBelongsTo, ObjectBase.UID
            FROM Links 
            INNER JOIN ObjectBase ON (Links.UID=ObjectBase.UID AND Links.Type IN('member','memberA'))
            WHERE (Links.UIDTarget=?) AND ObjectBase.Type IN ('person','guest','job')
            GROUP BY ObjectBase.UID `, [UIDgroup]);

        // 5. Füge alle Personen und Gäste als Gäste zur neuen Gastgruppe hinzu
        const gpersons = objects.filter(o => o.Type === 'person' || o.Type === 'guest').map(o => o.UID);
        if (gpersons.length) {
            const guestPersons = await addGuests(gpersons, guestGroup.UID, 'guest', req.session.root);
            queueAddArray(req, guestPersons.map(g => ({
                type: 'guest',
                UID: g.UID,
                UIDBelongsTo: g.UIDBelongsTo,
                oldTarget: null,
                newTarget: parents[0].UID,
            })));
        }

        // 6. Verknüpfe alle Jobs als "tree"-Elemente mit der Eltern-Gruppe
        const jobs = objects.filter(o => o.Type === 'job');
        if (jobs.length) {
            queueAddArray(req, jobs.map(j => ({
                type: 'tree',
                UID: j.UID,
                UIDBelongsTo: j.UIDBelongsTo,
                oldTarget: null,
                newTarget: parents[0].UID,
            })));
        }

        // 7. Erfolg zurückgeben
        return { success: true };
    } catch (e) {
        // Fehlerbehandlung: Fehler loggen
        errorLoggerUpdate(e);
        throw e;
    }
};

/**
 * Adds a single person as guest to a group
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const addSingleGuest = async (/** @type {ExpressRequestAuthorized} */ req, /** @type {ExpressResponse} */ res) => {
    try {
        const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);

        // Prüfe, ob der User Adminrechte für die Zielgruppe hat
        if (!isObjectAdmin(req, req.params.group)) {
            res.status(403).json({ success: false, message: 'user not authorized for this group' });
            return;
        }

        // Konvertiere UIDs ins interne Hex-Format
        const UIDgroup = UUID2hex(req.params.group);
        const UIDperson = UUID2hex(req.params.UID);
        const UIDguest = await getUID(req);
        const template = Templates[req.session.root].guest;

        // Lade Person und Gruppe aus der Datenbank
        const [personDB] = await query(`SELECT Data FROM Member WHERE UID=? `, [UIDperson]);
        if (!personDB) {
            res.status(404).json({ success: false, message: 'invalid person UID' });
            return;
        }
        const person = JSON.parse(personDB.Data);

        const [groupDB] = await query(`SELECT Data FROM Member WHERE UID=? `, [UIDgroup]);
        if (!groupDB) {
            res.status(404).json({ success: false, message: 'invalid group UID' });
            return;
        }
        const groupData = JSON.parse(groupDB.Data);

        // Setze Hierarchie und Stage für den Gast passend zur Gruppe
        person.hierarchie = groupData.hierarchie;
        person.stage = groupData.stage && groupData.stage > 0 ? groupData.stage : 4;

        // Prüfe, ob die Person schon als Gast oder Mitglied in der Gruppe ist
        const persons = await query(
            `SELECT ObjectBase.UID,ObjectBase.Type FROM ObjectBase
             INNER JOIN Links ON (ObjectBase.UID=Links.UID AND Links.Type IN ('member','memberA'))
             WHERE ObjectBase.UIDBelongsTo=? AND ObjectBase.Type IN ('person','guest') AND Links.UIDTarget=?`,
            [UIDperson, UIDgroup]
        );

        let guest;
        if (persons.length > 0) {
            if (persons[0].Type === 'guest') {
                guest = persons[0];
            } else {
                res.status(409).json({ success: false, message: 'person is already a member of this group' });
                return;
            }
        }

        // Rendere das Gast-Objekt
        const object = await renderObject(template, { ...person, UID: req.body.UID, group: groupData }, req);
        
        if (!guest) {
            // Lege neuen Gast an
            if (!isValidUID(req.body.UID)) {
                res.status(400).json({ success: false, message: 'invalid UID format in body.UID' });
                return;
            }
            
            await query(`
                INSERT INTO ObjectBase(UID,UIDuser,Type,UIDBelongsTo,Title,SortName,FullTextIndex, dindex,hierarchie,stage,gender,Data)
                VALUES (?,?,'guest',?,?,?,?,?,?,?,?,?)
                `,
                [object.UID, HEX2uuid(req.session.user), UIDperson, object.Title, object.SortBase, object.FullTextIndex, object.dindex,
                    object.hierarchie, object.stage, object.gender, JSON.stringify({})]);
            
            await query(`INSERT IGNORE INTO Links(UID, Type, UIDTarget) VALUES (?,'memberA',?)`, 
                [object.UID, UIDgroup]);
            
            queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'guest', object.UID, UIDperson, null, UIDgroup);
            publishEvent(`/add/group/guest/${HEX2uuid(UIDgroup)}`, {
                organization: req.session.root,
                data: [HEX2uuid(object.UID)]
            });
        } else {
            // Aktualisiere bestehenden Gast
            await query(`UPDATE ObjectBase SET 
                Title=?,SortName=?,FullTextIndex=?,dindex=?,hierarchie=?,stage=?,gender=?
                WHERE UID=?`,
                [object.Title, object.SortBase, object.FullTextIndex, object.dindex,
                    object.hierarchie, object.stage, object.gender, guest.UID]);
        }
        
        res.status(201).json({ success: true, result: { ...object, UID: HEX2uuid(object.UID) } });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Bulk add guests to a group
 * @param {ExpressRequestAuthorized} req - Express request object
 * @param {ExpressResponse} res - Express response object
 */
export const bulkAddGuests = async (req,  res) => {
    try {
        const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
        
        // Prüfe Admin-Rechte
        if (!isObjectAdmin(req, req.params.group)) {
            res.status(403).json({ success: false, message: 'user not authorized for this group' });
            return;
        }

        // Hole alle Personen-Objekte anhand der übergebenen UIDs
        const persons = await query(`SELECT UID,UIDBelongsTo 
            FROM ObjectBase WHERE UID IN (?)`, [req.body]);
        
        // Füge die Personen als Gäste zur Gruppe hinzu
        const guests = await addGuests(persons.map(p => p.UIDBelongsTo),
            UUID2hex(req.params.group),
            'guest',
            req.session.root,
            timestamp,
            req.session.user
        );
        
        // Trage die Änderungen für die Synchronisation ein
        queueAddArray(req, guests.map(g => ({
            UID: g.UID,
            UIDBelongsTo: g.UIDBelongsTo,
            type: 'guest',
            oldTarget: null,
            newTarget: UUID2hex(req.params.group),
            timestamp: null
        })));

        res.status(201).json({ success: true, result: guests });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Create guest group from existing group
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const createGuestGroup = async (req, res) => {
    try {
        // Extrahiere optionalen Zeitstempel für Backdating
        const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);

        // Rufe die Hilfsfunktion auf, die die Gastgruppe anlegt und Mitglieder/Jobs übernimmt
        const result = await addGroupGuest(
            req,
            UUID2hex(req.params.UID),      // Zielgruppe (als Hex)
            UUID2hex(req.params.parent),   // Eltern-Gruppe (als Hex)
            timestamp                      // Optionaler Zeitstempel
        );

        // Sende das Ergebnis zurück (Erfolg oder Fehlermeldung)
        res.status(result.success ? 201 : 400).json(result);
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Get all guests from a group
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const getGroupGuests = async (req, res) => {
    try {
        // Hole alle direkten Gäste (guest) und Gastgruppen (ggroup) der angegebenen Gruppe
        const result = await query(`
                SELECT ObjectBase.Title, Member.Display, ObjectBase.UID, ObjectBase.Type, ObjectBase.UIDBelongsTo,
                    JSON_VALUE(ObjectBase.Data,'$.banner') AS banner,ObjectBase.dindex, pGroup.Display AS pGroup
                    FROM
                    ObjectBase 
                    INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
                    INNER JOIN Links as aLinks ON (aLinks.UID=Member.UID AND aLinks.Type='memberA')
                    INNER JOIN ObjectBase pGroup ON (pGroup.UID=aLinks.UIDTarget)
                    INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type='memberA')
                    WHERE ObjectBase.Type IN ('guest','ggroup') AND Links.UIDTarget=?
                    ORDER BY ObjectBase.Type,ObjectBase.SortName,Member.SortName
            `,
            [UUID2hex(req.params.UIDgroup)],
            { cast: ['UUID', 'json'] }
        );

        // Sende die gefundenen Gäste/Gastgruppen als Ergebnis zurück
        res.json({ success: true, result: result });
    } catch (e) {
        errorLoggerRead(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};

/**
 * Delete a guest or guest group
 * @param {Object} req - Express request object
 * @param {Object} res - Express response object
 */
export const deleteGuest = async (req, res) => {
    try {
        const UID = UUID2hex(req.params.UID);
        const objects = await query(`SELECT ObjectBase.Type,ObjectBase.UID,ObjectBase.UIDBelongsTo, Links.UIDTarget AS UIDparent
            FROM ObjectBase 
            INNER JOIN Links ON (Links.UID=ObjectBase.UID AND ((Links.Type='memberA' AND ObjectBase.Type='guest') OR (Links.Type='memberGA' AND ObjectBase.Type='ggroup')))
            WHERE ObjectBase.UID=?
            GROUP BY ObjectBase.UID
            `, [UID], { log: true });

        if (objects.length === 0) {
            res.status(404).json({ success: false, message: 'guest not found' });
            return;
        }

        const object = objects[0];

        if (object.Type === 'ggroup') {
            queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'groupGuest', UUID2hex(req.params.UID), object.UIDBelongsTo, object.UIDparent, null);
        } else if (object.Type === 'guest') {
            queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'guest', UUID2hex(req.params.UID), object.UIDBelongsTo, object.UIDparent, null);
        }

        // Entferne die Verknüpfung
        await query(`DELETE FROM Links WHERE UID =? AND Type IN ('memberA', 'memberGA')`, [UID]);

        res.json({ success: true });
    } catch (e) {
        errorLoggerUpdate(e);
        res.status(500).json({ success: false, message: 'Internal server error' });
    }
};