core/user-profile.js

/**
 * Zentrales User-Profil — localStorage-basiert.
 * 
 * Konsolidiert alle lernbezogenen Daten (Fortschritt, Assessment-Ergebnisse,
 * Interessen) in einem einzigen localStorage-Key. Bestehende Legacy-Keys
 * (lp_progress, ai_unboxed_universe_progress) werden beim ersten Laden
 * eingelesen und in das neue Schema migriert.
 * 
 * @fileoverview
 * @author Alexander Wolf
 * @version 1.0
 * @see docs/conventions/DATA_MODEL_CONVENTIONS.md §2
 */

const STORAGE_KEY = 'aiu_user_profile';
const CURRENT_VERSION = 1;

/** Legacy-Keys, die beim ersten Laden eingelesen werden. */
const LEGACY_KEYS = {
    LP_PROGRESS: 'lp_progress',
    UNIVERSE_PROGRESS: 'ai_unboxed_universe_progress',
    INTRO_SEEN: 'aiu_intro_seen'
};

/**
 * Default-Profil-Daten. Wird als Basis für neue Profile verwendet.
 * @returns {Object} Frisches Profil-Objekt.
 */
function createDefaults() {
    const now = new Date().toISOString();
    return {
        version: CURRENT_VERSION,
        onboardingCompleted: null,
        level: 'unknown',
        interests: [],
        assessmentScores: {},
        completedResources: [],
        badges: [],
        lastVisited: null,
        recommendationsDismissed: [],
        createdAt: now,
        updatedAt: now
    };
}

/**
 * Zentrales User-Profil für die ai-unboxed Plattform.
 * 
 * Speichert ausschließlich lernbezogene Daten (keine PII).
 * Verwendet localStorage als Persistenzschicht.
 * 
 * @example
 * const profile = UserProfile.load();
 * profile.completeResource('game_rotatebox');
 * profile.setAssessmentScore('math', 0.65);
 * profile.save();
 */
class UserProfile {

    /**
     * @param {Object} data - Profildaten (siehe Schema in DATA_MODEL_CONVENTIONS.md §2.2)
     */
    constructor(data) {
        /** @type {number} Schema-Version */
        this.version = data.version;

        /** @type {string|null} Art des abgeschlossenen Onboardings */
        this.onboardingCompleted = data.onboardingCompleted;

        /** @type {string} Einstufung: 'unknown'|'beginner'|'intermediate'|'advanced' */
        this.level = data.level;

        /** @type {string[]} Topic-Tags (identisch mit Content-Registry Topics) */
        this.interests = data.interests;

        /** @type {Object<string, number>} Kategorie → Score (0.0–1.0) */
        this.assessmentScores = data.assessmentScores;

        /** @type {string[]} Content-Registry IDs abgeschlossener Ressourcen */
        this.completedResources = data.completedResources;

        /** @type {string[]} Badge-IDs */
        this.badges = data.badges;

        /** @type {string|null} Letzte besuchte Ressource-ID */
        this.lastVisited = data.lastVisited;

        /** @type {string[]} Weggeklickte Empfehlungs-IDs */
        this.recommendationsDismissed = data.recommendationsDismissed;

        /** @type {string} ISO 8601 Timestamp */
        this.createdAt = data.createdAt;

        /** @type {string} ISO 8601 Timestamp */
        this.updatedAt = data.updatedAt;
    }

    // ── Factory Methods ─────────────────────────────────────────

    /**
     * Lädt das Profil aus localStorage oder erstellt ein neues.
     * Bei erstem Laden werden Legacy-Keys eingelesen und konsolidiert.
     * 
     * @returns {UserProfile} Das geladene oder neu erstellte Profil.
     */
    static load() {
        try {
            const raw = localStorage.getItem(STORAGE_KEY);
            if (raw) {
                const data = JSON.parse(raw);
                if (data.version !== CURRENT_VERSION) {
                    return UserProfile._migrate(data);
                }
                return new UserProfile(data);
            }
        } catch (err) {
            console.warn('[UserProfile] Fehler beim Laden, erstelle neues Profil:', err);
        }

        // Kein gespeichertes Profil → neues erstellen und Legacy migrieren
        const profile = new UserProfile(createDefaults());
        UserProfile._importLegacy(profile);
        profile.save();
        return profile;
    }

    /**
     * Löscht alle Profildaten und alle Legacy-Keys aus localStorage.
     */
    static reset() {
        try {
            localStorage.removeItem(STORAGE_KEY);
            Object.values(LEGACY_KEYS).forEach(key => localStorage.removeItem(key));
        } catch (err) {
            console.warn('[UserProfile] Fehler beim Zurücksetzen:', err);
        }
    }

    // ── Read API ────────────────────────────────────────────────

    /**
     * Prüft, ob eine Ressource abgeschlossen wurde.
     * @param {string} resourceId - Content-Registry ID.
     * @returns {boolean}
     */
    hasCompleted(resourceId) {
        return this.completedResources.includes(resourceId);
    }

    /**
     * Prüft, ob das Onboarding abgeschlossen wurde.
     * @returns {boolean}
     */
    hasCompletedOnboarding() {
        return this.onboardingCompleted !== null;
    }

    /**
     * Gibt den Assessment-Score für eine Kategorie zurück.
     * @param {string} category - z.B. 'math', 'history'
     * @returns {number|null} Score (0.0–1.0) oder null falls nicht vorhanden.
     */
    getAssessmentScore(category) {
        return this.assessmentScores[category] ?? null;
    }

    /**
     * Berechnet den Durchschnittsscore über alle Assessment-Kategorien.
     * @returns {number} Durchschnitt (0.0–1.0), oder 0 wenn keine Scores vorhanden.
     */
    getAverageScore() {
        const scores = Object.values(this.assessmentScores);
        if (scores.length === 0) return 0;
        return scores.reduce((sum, s) => sum + s, 0) / scores.length;
    }

    /**
     * Gibt die Anzahl abgeschlossener Ressourcen zurück.
     * @returns {number}
     */
    getCompletedCount() {
        return this.completedResources.length;
    }

    // ── Write API ───────────────────────────────────────────────

    /**
     * Markiert eine Ressource als abgeschlossen.
     * @param {string} resourceId - Content-Registry ID.
     * @returns {UserProfile} this (für Chaining)
     */
    completeResource(resourceId) {
        if (!this.completedResources.includes(resourceId)) {
            this.completedResources.push(resourceId);
            this._touch();
        }
        return this;
    }

    /**
     * Setzt das Level des Nutzers.
     * @param {'unknown'|'beginner'|'intermediate'|'advanced'} level
     * @returns {UserProfile} this
     */
    setLevel(level) {
        const VALID = ['unknown', 'beginner', 'intermediate', 'advanced'];
        if (!VALID.includes(level)) {
            console.warn(`[UserProfile] Ungültiges Level: "${level}". Erlaubt: ${VALID.join(', ')}`);
            return this;
        }
        this.level = level;
        this._touch();
        return this;
    }

    /**
     * Setzt den Assessment-Score für eine Kategorie.
     * @param {string} category - z.B. 'math', 'history'
     * @param {number} score - Wert zwischen 0.0 und 1.0
     * @returns {UserProfile} this
     */
    setAssessmentScore(category, score) {
        this.assessmentScores[category] = Math.max(0, Math.min(1, score));
        this._touch();
        return this;
    }

    /**
     * Markiert das Onboarding als abgeschlossen.
     * @param {'test'|'skip'|'quick'} method - Art des Onboardings
     * @returns {UserProfile} this
     */
    completeOnboarding(method) {
        this.onboardingCompleted = method;
        this._touch();
        return this;
    }

    /**
     * Fügt eine Interesse/Topic hinzu.
     * @param {string} topic - Topic-Tag
     * @returns {UserProfile} this
     */
    addInterest(topic) {
        if (!this.interests.includes(topic)) {
            this.interests.push(topic);
            this._touch();
        }
        return this;
    }

    /**
     * Entfernt ein Interesse.
     * @param {string} topic - Topic-Tag
     * @returns {UserProfile} this
     */
    removeInterest(topic) {
        const idx = this.interests.indexOf(topic);
        if (idx !== -1) {
            this.interests.splice(idx, 1);
            this._touch();
        }
        return this;
    }

    /**
     * Setzt die zuletzt besuchte Ressource.
     * @param {string} resourceId - Content-Registry ID
     * @returns {UserProfile} this
     */
    setLastVisited(resourceId) {
        this.lastVisited = resourceId;
        this._touch();
        return this;
    }

    /**
     * Vergibt ein Badge.
     * @param {string} badgeId - Badge-ID
     * @returns {UserProfile} this
     */
    addBadge(badgeId) {
        if (!this.badges.includes(badgeId)) {
            this.badges.push(badgeId);
            this._touch();
        }
        return this;
    }

    /**
     * Markiert eine Empfehlung als weggeklickt.
     * @param {string} resourceId - Content-Registry ID
     * @returns {UserProfile} this
     */
    dismissRecommendation(resourceId) {
        if (!this.recommendationsDismissed.includes(resourceId)) {
            this.recommendationsDismissed.push(resourceId);
            this._touch();
        }
        return this;
    }

    // ── Persistence ─────────────────────────────────────────────

    /**
     * Speichert das Profil in localStorage.
     * @returns {UserProfile} this
     */
    save() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(this.toJSON()));
        } catch (err) {
            console.warn('[UserProfile] Fehler beim Speichern:', err);
        }
        return this;
    }

    /**
     * Serialisiert das Profil als JSON-kompatibles Objekt.
     * @returns {Object}
     */
    toJSON() {
        return {
            version: this.version,
            onboardingCompleted: this.onboardingCompleted,
            level: this.level,
            interests: [...this.interests],
            assessmentScores: { ...this.assessmentScores },
            completedResources: [...this.completedResources],
            badges: [...this.badges],
            lastVisited: this.lastVisited,
            recommendationsDismissed: [...this.recommendationsDismissed],
            createdAt: this.createdAt,
            updatedAt: this.updatedAt
        };
    }

    // ── Private ─────────────────────────────────────────────────

    /**
     * Aktualisiert den updatedAt-Timestamp.
     * @private
     */
    _touch() {
        this.updatedAt = new Date().toISOString();
    }

    /**
     * Migriert ein Profil von einer älteren Version.
     * @param {Object} data - Gespeicherte Profildaten mit veralteter Version
     * @returns {UserProfile}
     * @private
     */
    static _migrate(data) {
        // Aktuell gibt es nur Version 1. Zukünftige Migrationen hier ergänzen.
        // Beispiel für v1 → v2:
        // if (data.version === 1) { data.newField = defaultValue; data.version = 2; }
        console.info(`[UserProfile] Migration von v${data.version} zu v${CURRENT_VERSION}`);
        data.version = CURRENT_VERSION;
        const profile = new UserProfile({ ...createDefaults(), ...data });
        profile.save();
        return profile;
    }

    /**
     * Importiert Daten aus Legacy-localStorage-Keys.
     * Bestehende Keys werden NICHT gelöscht (Rückwärtskompatibilität).
     * @param {UserProfile} profile - Das Profil, in das importiert wird
     * @private
     */
    static _importLegacy(profile) {
        try {
            // Legacy: lp_progress (Learning-Path Fortschritt)
            const lpRaw = localStorage.getItem(LEGACY_KEYS.LP_PROGRESS);
            if (lpRaw) {
                const lpData = JSON.parse(lpRaw);
                // lpData hat Struktur: { courseId: { completed: [...pageIndices] } }
                if (lpData && typeof lpData === 'object') {
                    Object.keys(lpData).forEach(courseId => {
                        // Mapping: courseId → Content-Registry-ID
                        const registryId = UserProfile._mapLegacyCourseId(courseId);
                        if (registryId && lpData[courseId]?.completed?.length > 0) {
                            profile.completeResource(registryId);
                        }
                    });
                }
            }

            // Legacy: ai_unboxed_universe_progress (Challenge-Fortschritt)
            const universeRaw = localStorage.getItem(LEGACY_KEYS.UNIVERSE_PROGRESS);
            if (universeRaw) {
                const universeData = JSON.parse(universeRaw);
                if (universeData?.completedNodes) {
                    Object.keys(universeData.completedNodes).forEach(nodeId => {
                        if (universeData.completedNodes[nodeId]) {
                            profile.completeResource(nodeId);
                        }
                    });
                }
            }

            // Legacy: aiu_intro_seen
            const introSeen = localStorage.getItem(LEGACY_KEYS.INTRO_SEEN);
            if (introSeen) {
                profile.onboardingCompleted = 'legacy';
            }
        } catch (err) {
            console.warn('[UserProfile] Fehler beim Import von Legacy-Daten:', err);
        }
    }

    /**
     * Mappt Legacy-Course-IDs auf Content-Registry-IDs.
     * @param {string} courseId - z.B. 'search-trees-rotatebox'
     * @returns {string|null}
     * @private
     */
    static _mapLegacyCourseId(courseId) {
        const MAPPING = {
            'search-trees-rotatebox': 'expedition_search_trees',
            'expedition-searchtrees': 'expedition_search_trees',
            'minimax-ttt': 'expedition_minimax',
            'expedition-minimax': 'expedition_minimax'
        };
        return MAPPING[courseId] || null;
    }
}

// Global Export (analog zu iframe-bridge.js)
if (typeof window !== 'undefined') {
    window.UserProfile = UserProfile;
}