core/recommendation-engine.js

/**
 * Recommendation-Engine — Personalisierte Inhaltsempfehlungen.
 * 
 * Orchestriert UserProfile (Assessment-Ergebnisse, Fortschritt)
 * und ContentRegistry (Inhalte, Beziehungen) zu kontextbewussten
 * Empfehlungen für den jeweiligen Nutzer.
 * 
 * @fileoverview
 * @author Alexander Wolf
 * @version 1.0
 * @see docs/specifications/ONBOARDING_AND_CONTENT_ARCHITECTURE.md §3
 */

// ContentRegistry und UserProfile werden als globale Window-Exporte geladen
// (per <script src="..."> in HTML, analog zu iframe-bridge.js).

/**
 * Mapping: Assessment-Kategorie → Content-Registry Topics.
 * Schwache Kategorien werden auf inhaltlich passende Ressourcen abgebildet.
 * 
 * @type {Object<string, string[]>}
 */
const CATEGORY_TOPIC_MAP = {
    'history':     ['ai-history', 'general-ai'],
    'current-ai':  ['general-ai', 'neural-networks', 'machine-learning'],
    'general-ai':  ['general-ai', 'agent-types', 'rule-based-ai'],
    'math':        ['probability', 'graph-theory', 'neural-networks', 'machine-learning'],
    'cs-practice': ['search-trees', 'algorithms', 'minimax', 'heuristics', 'data-structures']
};

/**
 * Empfehlungs-Typen für die UI.
 * 
 * @enum {string}
 */
const RECOMMENDATION_TYPE = {
    /** Basierend auf schwachen Assessment-Kategorien */
    WEAKNESS:   'weakness',
    /** Nächster logischer Schritt nach Abschlüssen */
    NEXT_STEP:  'next-step',
    /** Passend zu Interessen */
    INTEREST:   'interest',
    /** Allgemeine Einsteigerempfehlung */
    STARTER:    'starter'
};

/**
 * Erstellt personalisierte Empfehlungen.
 * 
 * @example
 * import { RecommendationEngine } from './js/core/recommendation-engine.js';
 * 
 * const engine = await RecommendationEngine.create('../../');
 * const recs = engine.getRecommendations();
 * // → [{ resource, type, reason, priority }, ...]
 */
class RecommendationEngine {

    /**
     * @param {ContentRegistry} registry
     * @param {UserProfile} profile
     */
    constructor(registry, profile) {
        /** @type {ContentRegistry} */
        this._registry = registry;

        /** @type {UserProfile} */
        this._profile = profile;
    }

    /**
     * Erstellt eine Engine-Instanz (async Factory).
     * 
     * @param {string} [basePath=''] - Basis-Pfad für ContentRegistry-Fetch
     * @returns {Promise<RecommendationEngine>}
     */
    static async create(basePath = '') {
        const registry = await ContentRegistry.load(basePath);
        const profile = UserProfile.load();
        return new RecommendationEngine(registry, profile);
    }

    // ── Public API ──────────────────────────────────────────────

    /**
     * Gibt priorisierte Empfehlungen zurück.
     * 
     * Strategie:
     * 1. Schwache Assessment-Kategorien → passende Ressourcen
     * 2. Nächste logische Schritte (Prerequisites erfüllt)
     * 3. Interessen des Nutzers
     * 4. Starter-Empfehlungen für neue Nutzer
     * 
     * @param {Object} [options]
     * @param {number} [options.maxResults=6] - Maximale Anzahl
     * @param {string[]} [options.dismissed=[]] - Ausgeblendete IDs
     * @returns {Array<{resource: Object, type: string, reason: string, priority: number}>}
     */
    getRecommendations({ maxResults = 6, dismissed = [] } = {}) {
        const completedIds = this._profile.completedResources || [];
        const interests = this._profile.interests || [];
        const level = this._profile.level || 'beginner';
        const scores = this._profile.assessmentScores || {};
        const dismissedSet = new Set(dismissed);
        const usedIds = new Set();

        /** @type {Array<{resource: Object, type: string, reason: string, priority: number}>} */
        const recommendations = [];

        // ── 1. Schwächen-basierte Empfehlungen ──────────────────
        const weakCategories = this._getWeakCategories(scores);
        for (const cat of weakCategories) {
            const topics = CATEGORY_TOPIC_MAP[cat] || [];
            for (const topic of topics) {
                const resources = this._registry.getByTopic(topic);
                for (const r of resources) {
                    if (this._isCandidate(r, completedIds, dismissedSet, usedIds, level)) {
                        recommendations.push({
                            resource: r,
                            type: RECOMMENDATION_TYPE.WEAKNESS,
                            reason: `Stärke dein Wissen in "${this._getCategoryLabel(cat)}"`,
                            priority: 100 - (scores[cat] || 0) * 100
                        });
                        usedIds.add(r.id);
                        break; // Max 1 pro Topic
                    }
                }
            }
        }

        // ── 2. Nächste logische Schritte ────────────────────────
        const completedSet = new Set(completedIds);
        for (const r of this._registry.resources) {
            if (!this._isCandidate(r, completedIds, dismissedSet, usedIds, level)) continue;
            if (r.prerequisites.length === 0) continue;

            const allMet = r.prerequisites.every(id => completedSet.has(id));
            if (allMet) {
                recommendations.push({
                    resource: r,
                    type: RECOMMENDATION_TYPE.NEXT_STEP,
                    reason: 'Nächster logischer Schritt',
                    priority: 80
                });
                usedIds.add(r.id);
            }
        }

        // ── 3. Interessen-basierte Empfehlungen ─────────────────
        for (const topic of interests) {
            const resources = this._registry.getByTopic(topic);
            for (const r of resources) {
                if (this._isCandidate(r, completedIds, dismissedSet, usedIds, level)) {
                    recommendations.push({
                        resource: r,
                        type: RECOMMENDATION_TYPE.INTEREST,
                        reason: `Passt zu deinem Interesse: ${topic}`,
                        priority: 60
                    });
                    usedIds.add(r.id);
                    break;
                }
            }
        }

        // ── 4. Starter-Empfehlungen ─────────────────────────────
        if (recommendations.length < maxResults && completedIds.length < 3) {
            const starters = this._registry.resources
                .filter(r => this._isCandidate(r, completedIds, dismissedSet, usedIds, level))
                .filter(r => r.difficulty <= 2 && r.prerequisites.length === 0)
                .sort((a, b) => a.difficulty - b.difficulty);

            for (const r of starters) {
                if (recommendations.length >= maxResults) break;
                recommendations.push({
                    resource: r,
                    type: RECOMMENDATION_TYPE.STARTER,
                    reason: 'Guter Einstiegspunkt',
                    priority: 40
                });
                usedIds.add(r.id);
            }
        }

        // Sortieren nach Priorität, limitieren
        return recommendations
            .sort((a, b) => b.priority - a.priority)
            .slice(0, maxResults);
    }

    /**
     * Gibt den empfohlenen nächsten Inhalt zurück (Top-1).
     * @returns {{resource: Object, type: string, reason: string}|null}
     */
    getNextRecommendation() {
        const recs = this.getRecommendations({ maxResults: 1 });
        return recs[0] || null;
    }

    /**
     * Gibt Empfehlungen als gruppierte Sektionen zurück (für UI-Rendering).
     * 
     * @returns {Array<{title: string, type: string, items: Object[]}>}
     */
    getGroupedRecommendations() {
        const all = this.getRecommendations({ maxResults: 12 });

        const groups = new Map();
        const groupTitles = {
            [RECOMMENDATION_TYPE.WEAKNESS]:  'Empfohlen für dich',
            [RECOMMENDATION_TYPE.NEXT_STEP]: 'Nächste Schritte',
            [RECOMMENDATION_TYPE.INTEREST]:  'Basierend auf deinen Interessen',
            [RECOMMENDATION_TYPE.STARTER]:   'Gute Einstiegspunkte'
        };

        for (const rec of all) {
            if (!groups.has(rec.type)) {
                groups.set(rec.type, {
                    title: groupTitles[rec.type] || rec.type,
                    type: rec.type,
                    items: []
                });
            }
            groups.get(rec.type).items.push(rec);
        }

        return [...groups.values()];
    }

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

    /**
     * Gibt Assessment-Kategorien zurück, in denen der Nutzer schwach ist.
     * @param {Object<string, number>} scores - Kategorie → Score (0.0 – 1.0)
     * @returns {string[]} Schwache Kategorien, sortiert (schwächste zuerst)
     * @private
     */
    _getWeakCategories(scores) {
        if (!scores || Object.keys(scores).length === 0) return [];

        return Object.entries(scores)
            .filter(([, score]) => score < 0.5)
            .sort(([, a], [, b]) => a - b)
            .map(([cat]) => cat);
    }

    /**
     * Prüft, ob eine Ressource als Empfehlung in Frage kommt.
     * @param {Object} resource
     * @param {string[]} completedIds
     * @param {Set<string>} dismissedSet
     * @param {Set<string>} usedIds - Bereits empfohlene IDs (Duplikatvermeidung)
     * @param {string} level
     * @returns {boolean}
     * @private
     */
    _isCandidate(resource, completedIds, dismissedSet, usedIds, level) {
        if (completedIds.includes(resource.id)) return false;
        if (dismissedSet.has(resource.id)) return false;
        if (usedIds.has(resource.id)) return false;

        // Level-Filter: Beginner nicht mit Difficulty 4/5 überfordern
        const maxDifficulty = { beginner: 3, intermediate: 4, advanced: 5 };
        const max = maxDifficulty[level] || 5;
        if (resource.difficulty > max) return false;

        return true;
    }

    /**
     * Gibt den deutschen Label-Text für eine Assessment-Kategorie zurück.
     * @param {string} category
     * @returns {string}
     * @private
     */
    _getCategoryLabel(category) {
        const labels = {
            'history':     'Geschichte der KI',
            'current-ai':  'Aktuelle KI',
            'general-ai':  'Allgemeinwissen KI',
            'math':        'Mathematik',
            'cs-practice': 'Informatik & Algorithmik'
        };
        return labels[category] || category;
    }
}

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