Source: config/migrations/20260926-runner-companion-into-member.js

export const id = '20260926-runner-companion-into-member';
export const name = 'Fold the runner companion (RunnerData) into Member';

/**
 * `RunnerData` war eine eigene Companion-Tabelle für den heißen Runner-Zustand
 * (20260907-runner-companion) — dabei ist `Member` im Schema längst die
 * **generische Companion-Tabelle** nach dem Member-Muster: gleiche UID wie das
 * `ObjectBase`-Objekt, nicht system-versioniert, `Data`-JSON trägt den heißen
 * Zustand, nur der gesuchte Lookup liegt als Spalte daneben. Gruppen, Events,
 * Locations, Familien und E-Mails liegen dort schon — Runner brauchen keine
 * zweite Tabelle.
 *
 * Diese Migration zieht den Runner-Zustand nach `Member`:
 *
 *   - `credential_id` (UNIQUE)  → neue Lookup-Spalte an `Member` (Runner-Auth;
 *                                 inverser Lookup „Credential-ID → Runner")
 *   - `public_key`              → entfällt als Spalte. Der Key steht bereits in
 *                                 `ObjectBase.Data.publicKey` (von `createRunner`
 *                                 gesetzt) und ist damit die eine Quelle. Für
 *                                 Bestands-Runner, die vor 20260907 entstanden
 *                                 sind, wird er hier einmalig nachgezogen —
 *                                 sonst könnten sie sich nach dem Drop nicht
 *                                 mehr authentifizieren.
 *   - `Data` (heartbeat/capacity/lastSeenAt) → `Member.Data` des Runner-Objekts
 *
 * Danach wird `RunnerData` gedroppt. Idempotent: ein frischer Boot (Member hat
 * die Spalte schon aus `initTables.sql`, `RunnerData` existiert nicht) und eine
 * laufende dev-DB (Spalte fehlt, `RunnerData` ist da) landen im selben Zustand.
 *
 * Hinweis: `20260907-runner-companion` legt `RunnerData` weiterhin an — auf einem
 * frischen Boot entsteht sie leer und wird hier wieder entfernt. Die Migration
 * bleibt unangetastet, weil sie auf laufenden DBs bereits appliziert ist.
 */

const hasColumn = async (query, table, column) => {
    const rows = await query(
        `SELECT COUNT(*) AS c FROM information_schema.columns
         WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?`,
        [table, column],
    );
    return Number(rows[0]?.c) > 0;
};

const hasIndex = async (query, table, indexName) => {
    const rows = await query(
        `SELECT COUNT(*) AS c FROM information_schema.statistics
         WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?`,
        [table, indexName],
    );
    return Number(rows[0]?.c) > 0;
};

const hasTable = async (query, table) => {
    const rows = await query(
        `SELECT COUNT(*) AS c FROM information_schema.tables
         WHERE table_schema = DATABASE() AND table_name = ?`,
        [table],
    );
    return Number(rows[0]?.c) > 0;
};

export const migrate = async ({ query }) => {
    // ----------------------------------------- 1. Lookup-Spalte an Member
    if (!(await hasColumn(query, 'Member', 'credential_id'))) {
        await query(
            `ALTER TABLE \`Member\` ADD COLUMN \`credential_id\` varchar(64) DEFAULT NULL`,
            [],
        );
    }
    if (!(await hasIndex(query, 'Member', 'member_credential_id_uq'))) {
        await query(
            `ALTER TABLE \`Member\` ADD UNIQUE KEY \`member_credential_id_uq\` (\`credential_id\`)`,
            [],
        );
    }

    if (await hasTable(query, 'RunnerData')) {
        // ------------------------------------ 2. Bestands-Companion → Member
        // Eine Member-Zeile pro Runner. `ON DUPLICATE KEY UPDATE` fasst nur
        // credential_id an: ein bereits vorhandenes Member-Data (Heartbeat)
        // darf nicht von einer leeren Zeile überschrieben werden.
        await query(`
            INSERT INTO Member (UID, Display, SortName, FullTextIndex, Data, credential_id)
            SELECT ObjectBase.UID,
                   LEFT(COALESCE(ObjectBase.Title, ObjectBase.Display, 'Runner'), 128),
                   '', '',
                   COALESCE(RunnerData.Data, JSON_OBJECT()),
                   RunnerData.credential_id
            FROM ObjectBase
            LEFT JOIN RunnerData ON (RunnerData.UID = ObjectBase.UID)
            WHERE ObjectBase.Type = 'runner'
            ON DUPLICATE KEY UPDATE credential_id = VALUES(credential_id)
        `, []);

        // --------------------------- 3. publicKey an ObjectBase.Data nachziehen
        // Altbestand (vor 20260907) hatte den Key nur in RunnerData.public_key.
        // ObjectBase ist system-versioniert, daher nur die echten Abweichungen
        // schreiben (im Regelfall keine).
        const legacyKeys = await query(
            `SELECT ObjectBase.UID, ObjectBase.Data, RunnerData.public_key
             FROM ObjectBase
             INNER JOIN RunnerData ON (RunnerData.UID = ObjectBase.UID)
             WHERE ObjectBase.Type = 'runner' AND RunnerData.public_key IS NOT NULL`,
            [],
        );
        for (const row of legacyKeys) {
            const data = typeof row.Data === 'string'
                ? JSON.parse(row.Data || '{}')
                : (row.Data || {});
            if (data.publicKey === row.public_key) continue;
            data.publicKey = row.public_key;
            await query(
                `UPDATE ObjectBase SET Data = ? WHERE UID = ? AND Type = 'runner'`,
                [JSON.stringify(data), row.UID],
            );
        }

        // ------------------------------------------- 4. Companion-Tabelle weg
        await query(`DROP TABLE IF EXISTS \`RunnerData\``, []);
        return;
    }

    // --------- 2b. Frischer Boot: keine Companion-Tabelle vorhanden
    // Bereits existierende Runner (z. B. während eines Deploys entstanden)
    // bekommen ihre Member-Zeile, damit der Service einheitlich liest.
    await query(`
        INSERT IGNORE INTO Member (UID, Display, SortName, FullTextIndex, Data)
        SELECT ObjectBase.UID,
               LEFT(COALESCE(ObjectBase.Title, ObjectBase.Display, 'Runner'), 128),
               '', '', JSON_OBJECT()
        FROM ObjectBase
        WHERE ObjectBase.Type = 'runner'
    `, []);
};