import crypto from 'crypto';
import User from '../models/community/user.js';
import VocabCourse from '../models/community/vocab_course.js';
import VocabCourseLesson from '../models/community/vocab_course_lesson.js';
import VocabCourseEnrollment from '../models/community/vocab_course_enrollment.js';
import VocabCourseProgress from '../models/community/vocab_course_progress.js';
import VocabGrammarExerciseType from '../models/community/vocab_grammar_exercise_type.js';
import VocabGrammarExercise from '../models/community/vocab_grammar_exercise.js';
import VocabGrammarExerciseProgress from '../models/community/vocab_grammar_exercise_progress.js';
import { sequelize } from '../utils/sequelize.js';
import { notifyUser } from '../utils/socket.js';
import { Op } from 'sequelize';

export default class VocabService {
  async _getUserByHashedId(hashedUserId) {
    const user = await User.findOne({ where: { hashedId: hashedUserId } });
    if (!user) {
      const err = new Error('User not found');
      err.status = 404;
      throw err;
    }
    return user;
  }

  _normalizeLexeme(text) {
    return String(text || '')
      .trim()
      .toLowerCase()
      .replace(/\s+/g, ' ');
  }

  async _getLanguageAccess(userId, languageId) {
    const id = Number.parseInt(languageId, 10);
    if (!Number.isFinite(id)) {
      const err = new Error('Invalid language id');
      err.status = 400;
      throw err;
    }

    const [row] = await sequelize.query(
      `
      SELECT
        l.id,
        (l.owner_user_id = :userId) AS "isOwner"
      FROM community.vocab_language l
      WHERE l.id = :languageId
        AND (
          l.owner_user_id = :userId
          OR EXISTS (
            SELECT 1
            FROM community.vocab_language_subscription s
            WHERE s.user_id = :userId AND s.language_id = l.id
          )
        )
      LIMIT 1
      `,
      {
        replacements: { userId, languageId: id },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    if (!row) {
      const err = new Error('Language not found or no access');
      err.status = 404;
      throw err;
    }

    return row;
  }

  async _getChapterAccess(userId, chapterId) {
    const id = Number.parseInt(chapterId, 10);
    if (!Number.isFinite(id)) {
      const err = new Error('Invalid chapter id');
      err.status = 400;
      throw err;
    }

    const [row] = await sequelize.query(
      `
      SELECT
        c.id,
        c.language_id AS "languageId",
        c.title,
        (l.owner_user_id = :userId) AS "isOwner"
      FROM community.vocab_chapter c
      JOIN community.vocab_language l ON l.id = c.language_id
      WHERE c.id = :chapterId
        AND (
          l.owner_user_id = :userId
          OR EXISTS (
            SELECT 1
            FROM community.vocab_language_subscription s
            WHERE s.user_id = :userId AND s.language_id = l.id
          )
        )
      LIMIT 1
      `,
      {
        replacements: { userId, chapterId: id },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    if (!row) {
      const err = new Error('Chapter not found or no access');
      err.status = 404;
      throw err;
    }

    return row;
  }

  async listLanguages(hashedUserId) {
    const user = await this._getUserByHashedId(hashedUserId);

    const rows = await sequelize.query(
      `
      SELECT
        l.id,
        l.name,
        l.share_code AS "shareCode",
        TRUE AS "isOwner"
      FROM community.vocab_language l
      WHERE l.owner_user_id = :userId

      UNION ALL

      SELECT
        l.id,
        l.name,
        NULL::text AS "shareCode",
        FALSE AS "isOwner"
      FROM community.vocab_language_subscription s
      JOIN community.vocab_language l ON l.id = s.language_id
      WHERE s.user_id = :userId

      ORDER BY name ASC
      `,
      {
        replacements: { userId: user.id },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    return { languages: rows };
  }

  async listAllLanguages() {
    // Gibt alle verfügbaren Sprachen zurück (für Kursliste)
    const rows = await sequelize.query(
      `
      SELECT
        id,
        name
      FROM community.vocab_language
      ORDER BY name ASC
      `,
      {
        type: sequelize.QueryTypes.SELECT,
      }
    );

    return { languages: rows };
  }

  async listLanguagesForMenu(userId) {
    // userId ist die numerische community.user.id
    const rows = await sequelize.query(
      `
      SELECT l.id, l.name
      FROM community.vocab_language l
      WHERE l.owner_user_id = :userId
      UNION
      SELECT l.id, l.name
      FROM community.vocab_language_subscription s
      JOIN community.vocab_language l ON l.id = s.language_id
      WHERE s.user_id = :userId
      ORDER BY name ASC
      `,
      {
        replacements: { userId },
        type: sequelize.QueryTypes.SELECT,
      }
    );
    return rows;
  }

  async createLanguage(hashedUserId, { name }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const cleanName = typeof name === 'string' ? name.trim() : '';
    if (!cleanName || cleanName.length < 2 || cleanName.length > 60) {
      const err = new Error('Invalid language name');
      err.status = 400;
      throw err;
    }

    // 16 hex chars => ausreichend kurz, gut teilbar
    const shareCode = crypto.randomBytes(8).toString('hex');

    const [created] = await sequelize.query(
      `
      INSERT INTO community.vocab_language (owner_user_id, name, share_code)
      VALUES (:ownerUserId, :name, :shareCode)
      RETURNING id, name, share_code AS "shareCode"
      `,
      {
        replacements: { ownerUserId: user.id, name: cleanName, shareCode },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    // Menü dynamisch nachladen (bei allen offenen Tabs/Clients)
    try {
      notifyUser(user.hashedId, 'reloadmenu', {});
    } catch (_) {}

    return created;
  }

  async subscribeByShareCode(hashedUserId, { shareCode }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const code = typeof shareCode === 'string' ? shareCode.trim() : '';
    if (!code || code.length < 6 || code.length > 128) {
      const err = new Error('Invalid share code');
      err.status = 400;
      throw err;
    }

    const [lang] = await sequelize.query(
      `
      SELECT id, owner_user_id AS "ownerUserId", name
      FROM community.vocab_language
      WHERE share_code = :shareCode
      LIMIT 1
      `,
      {
        replacements: { shareCode: code },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    if (!lang) {
      const err = new Error('Language not found');
      err.status = 404;
      throw err;
    }

    // Owner braucht kein Abo
    if (lang.ownerUserId === user.id) {
      return { subscribed: false, message: 'Already owner', languageId: lang.id };
    }

    await sequelize.query(
      `
      INSERT INTO community.vocab_language_subscription (user_id, language_id)
      VALUES (:userId, :languageId)
      ON CONFLICT (user_id, language_id) DO NOTHING
      `,
      {
        replacements: { userId: user.id, languageId: lang.id },
        type: sequelize.QueryTypes.INSERT,
      }
    );

    try {
      notifyUser(user.hashedId, 'reloadmenu', {});
    } catch (_) {}

    return { subscribed: true, languageId: lang.id, name: lang.name };
  }

  async getLanguage(hashedUserId, languageId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const id = Number.parseInt(languageId, 10);
    if (!Number.isFinite(id)) {
      const err = new Error('Invalid language id');
      err.status = 400;
      throw err;
    }

    const [row] = await sequelize.query(
      `
      SELECT
        l.id,
        l.name,
        CASE WHEN l.owner_user_id = :userId THEN l.share_code ELSE NULL END AS "shareCode",
        (l.owner_user_id = :userId) AS "isOwner"
      FROM community.vocab_language l
      WHERE l.id = :languageId
        AND (
          l.owner_user_id = :userId
          OR EXISTS (
            SELECT 1
            FROM community.vocab_language_subscription s
            WHERE s.user_id = :userId AND s.language_id = l.id
          )
        )
      LIMIT 1
      `,
      {
        replacements: { userId: user.id, languageId: id },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    if (!row) {
      const err = new Error('Language not found or no access');
      err.status = 404;
      throw err;
    }

    return row;
  }

  async listChapters(hashedUserId, languageId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const access = await this._getLanguageAccess(user.id, languageId);

    const rows = await sequelize.query(
      `
      SELECT
        c.id,
        c.title,
        c.created_at AS "createdAt",
        (
          SELECT COUNT(*)
          FROM community.vocab_chapter_lexeme cl
          WHERE cl.chapter_id = c.id
        )::int AS "vocabCount"
      FROM community.vocab_chapter c
      WHERE c.language_id = :languageId
      ORDER BY c.title ASC
      `,
      {
        replacements: { languageId: access.id },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    return { chapters: rows, isOwner: access.isOwner };
  }

  async createChapter(hashedUserId, languageId, { title }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const access = await this._getLanguageAccess(user.id, languageId);
    if (!access.isOwner) {
      const err = new Error('Only owner can create chapters');
      err.status = 403;
      throw err;
    }

    const cleanTitle = typeof title === 'string' ? title.trim() : '';
    if (!cleanTitle || cleanTitle.length < 2 || cleanTitle.length > 80) {
      const err = new Error('Invalid chapter title');
      err.status = 400;
      throw err;
    }

    const [created] = await sequelize.query(
      `
      INSERT INTO community.vocab_chapter (language_id, title, created_by_user_id)
      VALUES (:languageId, :title, :userId)
      RETURNING id, title, created_at AS "createdAt"
      `,
      {
        replacements: { languageId: access.id, title: cleanTitle, userId: user.id },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    return created;
  }

  async getChapter(hashedUserId, chapterId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const ch = await this._getChapterAccess(user.id, chapterId);
    return { id: ch.id, languageId: ch.languageId, title: ch.title, isOwner: ch.isOwner };
  }

  async listChapterVocabs(hashedUserId, chapterId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const ch = await this._getChapterAccess(user.id, chapterId);

    const rows = await sequelize.query(
      `
      SELECT
        cl.id,
        l1.text AS "learning",
        l2.text AS "reference",
        cl.created_at AS "createdAt"
      FROM community.vocab_chapter_lexeme cl
      JOIN community.vocab_lexeme l1 ON l1.id = cl.learning_lexeme_id
      JOIN community.vocab_lexeme l2 ON l2.id = cl.reference_lexeme_id
      WHERE cl.chapter_id = :chapterId
      ORDER BY l1.text ASC, l2.text ASC
      `,
      {
        replacements: { chapterId: ch.id },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    return { chapter: { id: ch.id, title: ch.title, languageId: ch.languageId, isOwner: ch.isOwner }, vocabs: rows };
  }

  async listLanguageVocabs(hashedUserId, languageId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const access = await this._getLanguageAccess(user.id, languageId);

    const rows = await sequelize.query(
      `
      SELECT
        cl.id,
        c.id AS "chapterId",
        c.title AS "chapterTitle",
        l1.text AS "learning",
        l2.text AS "reference",
        cl.created_at AS "createdAt"
      FROM community.vocab_chapter_lexeme cl
      JOIN community.vocab_chapter c ON c.id = cl.chapter_id
      JOIN community.vocab_lexeme l1 ON l1.id = cl.learning_lexeme_id
      JOIN community.vocab_lexeme l2 ON l2.id = cl.reference_lexeme_id
      WHERE c.language_id = :languageId
      ORDER BY c.title ASC, l1.text ASC, l2.text ASC
      `,
      {
        replacements: { languageId: access.id },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    return { languageId: access.id, isOwner: access.isOwner, vocabs: rows };
  }

  async searchVocabs(hashedUserId, languageId, { q = '', learning = '', motherTongue = '' } = {}) {
    const user = await this._getUserByHashedId(hashedUserId);
    const access = await this._getLanguageAccess(user.id, languageId);

    const query = typeof q === 'string' ? q.trim() : '';
    // Abwärtskompatibel: falls alte Parameter genutzt werden, zusammenfassen
    const learningTerm = typeof learning === 'string' ? learning.trim() : '';
    const motherTerm = typeof motherTongue === 'string' ? motherTongue.trim() : '';
    const effective = query || learningTerm || motherTerm;

    if (!effective) {
      const err = new Error('Missing search term');
      err.status = 400;
      throw err;
    }

    const like = `%${effective}%`;

    const rows = await sequelize.query(
      `
      SELECT
        cl.id,
        c.id AS "chapterId",
        c.title AS "chapterTitle",
        l1.text AS "learning",
        l2.text AS "motherTongue"
      FROM community.vocab_chapter_lexeme cl
      JOIN community.vocab_chapter c ON c.id = cl.chapter_id
      JOIN community.vocab_lexeme l1 ON l1.id = cl.learning_lexeme_id
      JOIN community.vocab_lexeme l2 ON l2.id = cl.reference_lexeme_id
      WHERE c.language_id = :languageId
        AND (l1.text ILIKE :like OR l2.text ILIKE :like)
      ORDER BY l2.text ASC, l1.text ASC, c.title ASC
      LIMIT 200
      `,
      {
        replacements: {
          languageId: access.id,
          like,
        },
        type: sequelize.QueryTypes.SELECT,
      }
    );

    return { languageId: access.id, results: rows };
  }

  async addVocabToChapter(hashedUserId, chapterId, { learning, reference }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const ch = await this._getChapterAccess(user.id, chapterId);
    if (!ch.isOwner) {
      const err = new Error('Only owner can add vocab');
      err.status = 403;
      throw err;
    }

    const learningText = typeof learning === 'string' ? learning.trim() : '';
    const referenceText = typeof reference === 'string' ? reference.trim() : '';
    if (!learningText || !referenceText) {
      const err = new Error('Invalid vocab');
      err.status = 400;
      throw err;
    }

    const learningNorm = this._normalizeLexeme(learningText);
    const referenceNorm = this._normalizeLexeme(referenceText);

    // Transaktion: Lexeme upserten + Zuordnung setzen
    return await sequelize.transaction(async (t) => {
      const [learningLex] = await sequelize.query(
        `
        INSERT INTO community.vocab_lexeme (language_id, text, normalized, created_by_user_id)
        VALUES (:languageId, :text, :normalized, :userId)
        ON CONFLICT (language_id, normalized) DO UPDATE SET text = EXCLUDED.text
        RETURNING id
        `,
        {
          replacements: { languageId: ch.languageId, text: learningText, normalized: learningNorm, userId: user.id },
          type: sequelize.QueryTypes.SELECT,
          transaction: t,
        }
      );

      const [referenceLex] = await sequelize.query(
        `
        INSERT INTO community.vocab_lexeme (language_id, text, normalized, created_by_user_id)
        VALUES (:languageId, :text, :normalized, :userId)
        ON CONFLICT (language_id, normalized) DO UPDATE SET text = EXCLUDED.text
        RETURNING id
        `,
        {
          replacements: { languageId: ch.languageId, text: referenceText, normalized: referenceNorm, userId: user.id },
          type: sequelize.QueryTypes.SELECT,
          transaction: t,
        }
      );

      const [mapping] = await sequelize.query(
        `
        INSERT INTO community.vocab_chapter_lexeme (chapter_id, learning_lexeme_id, reference_lexeme_id, created_by_user_id)
        VALUES (:chapterId, :learningId, :referenceId, :userId)
        ON CONFLICT (chapter_id, learning_lexeme_id, reference_lexeme_id) DO NOTHING
        RETURNING id
        `,
        {
          replacements: {
            chapterId: ch.id,
            learningId: learningLex.id,
            referenceId: referenceLex.id,
            userId: user.id,
          },
          type: sequelize.QueryTypes.SELECT,
          transaction: t,
        }
      );

      return { created: Boolean(mapping?.id) };
    });
  }

  // ========== COURSE METHODS ==========

  async createCourse(hashedUserId, { title, description, languageId, nativeLanguageId, difficultyLevel = 1, isPublic = false }) {
    const user = await this._getUserByHashedId(hashedUserId);
    
    // Prüfe Zugriff auf Sprache
    await this._getLanguageAccess(user.id, languageId);

    const shareCode = isPublic ? crypto.randomBytes(8).toString('hex') : null;

    const course = await VocabCourse.create({
      ownerUserId: user.id,
      title,
      description,
      languageId: Number(languageId),
      nativeLanguageId: nativeLanguageId ? Number(nativeLanguageId) : null,
      difficultyLevel: Number(difficultyLevel) || 1,
      isPublic: Boolean(isPublic),
      shareCode
    });

    return course.get({ plain: true });
  }

  async getCourses(hashedUserId, { includePublic = true, includeOwn = true, languageId, nativeLanguageId, search } = {}) {
    const user = await this._getUserByHashedId(hashedUserId);

    // Konvertiere String-Parameter zu Booleans
    const includePublicBool = includePublic === 'true' || includePublic === true;
    const includeOwnBool = includeOwn === 'true' || includeOwn === true;

    const where = {};
    const andConditions = [];
    
    // Zugriffsbedingungen
    if (includeOwnBool && includePublicBool) {
      andConditions.push({
        [Op.or]: [
          { ownerUserId: user.id },
          { isPublic: true }
        ]
      });
    } else if (includeOwnBool) {
      where.ownerUserId = user.id;
    } else if (includePublicBool) {
      where.isPublic = true;
    }

    // Filter nach Zielsprache (die zu lernende Sprache)
    if (languageId) {
      where.languageId = Number(languageId);
    }

    // Filter nach Muttersprache (die Sprache des Lerners)
    // Wenn nativeLanguageId nicht gesetzt ist (undefined), zeige alle Kurse (kein Filter)
    // Wenn nativeLanguageId === null oder 'null' (aus Frontend), zeige alle Kurse (kein Filter)
    // Wenn nativeLanguageId eine Zahl ist, zeige nur Kurse für diese Muttersprache
    if (nativeLanguageId !== undefined && nativeLanguageId !== null && nativeLanguageId !== 'null') {
      where.nativeLanguageId = Number(nativeLanguageId);
    }
    // Wenn nativeLanguageId null/undefined/'null' ist, wird kein Filter angewendet = alle Kurse

    // Suche nach Titel oder Beschreibung
    if (search && search.trim()) {
      const searchTerm = `%${search.trim()}%`;
      andConditions.push({
        [Op.or]: [
          { title: { [Op.iLike]: searchTerm } },
          { description: { [Op.iLike]: searchTerm } }
        ]
      });
    }

    // Kombiniere alle AND-Bedingungen
    // Wenn sowohl andConditions als auch direkte where-Eigenschaften existieren,
    // müssen sie kombiniert werden
    // WICHTIG: directWhereProps muss NACH dem Setzen aller direkten Eigenschaften berechnet werden
    const directWhereProps = Object.keys(where).filter(key => {
      // Filtere Op.and und Op.or heraus (sind Symbol-Keys)
      return key !== Op.and && key !== Op.or && typeof key === 'string';
    });
    
    if (andConditions.length > 0) {
      // Wenn where bereits direkte Eigenschaften hat, füge sie zu andConditions hinzu
      if (directWhereProps.length > 0) {
        const directWhere = {};
        for (const key of directWhereProps) {
          directWhere[key] = where[key];
          delete where[key];
        }
        andConditions.push(directWhere);
      }
      // Entferne leere Objekte aus andConditions
      const filteredConditions = andConditions.filter(cond => {
        return cond && typeof cond === 'object' && Object.keys(cond).length > 0;
      });
      // Setze andConditions nur, wenn sie nicht leer sind
      if (filteredConditions.length > 0) {
        where[Op.and] = filteredConditions;
      }
    }
    // Wenn nur direkte Eigenschaften existieren (andConditions.length === 0),
    // bleiben sie in where (nichts zu tun, sie sind bereits dort)

    const courses = await VocabCourse.findAll({
      where,
      order: [['createdAt', 'DESC']]
    });

    // Debug-Logging (kann später entfernt werden)
    console.log(`[getCourses] Gefunden: ${courses.length} Kurse`, {
      userId: user.id,
      languageId,
      nativeLanguageId,
      search,
      whereBefore: JSON.stringify(where, null, 2),
      includePublic: includePublicBool,
      includeOwn: includeOwnBool,
      andConditionsLength: andConditions.length,
      directWherePropsBefore: Object.keys(where).filter(key => key !== Op.and && key !== Op.or),
      whereAfter: JSON.stringify(where, null, 2)
    });

    const coursesData = courses.map(c => c.get({ plain: true }));
    
    // Lade Sprachnamen für alle Kurse
    const languageIds = [...new Set(coursesData.map(c => c.languageId))];
    if (languageIds.length > 0) {
      const languages = await sequelize.query(
        `SELECT id, name FROM community.vocab_language WHERE id IN (:languageIds)`,
        {
          replacements: { languageIds },
          type: sequelize.QueryTypes.SELECT
        }
      );
      if (Array.isArray(languages)) {
        const languageMap = new Map(languages.map(l => [l.id, l.name]));
        coursesData.forEach(c => {
          c.languageName = languageMap.get(c.languageId) || null;
        });
      }
    }
    
    // Lade Muttersprachen-Namen für alle Kurse
    const nativeLanguageIds = [...new Set(coursesData.map(c => c.nativeLanguageId).filter(id => id !== null))];
    if (nativeLanguageIds.length > 0) {
      const nativeLanguages = await sequelize.query(
        `SELECT id, name FROM community.vocab_language WHERE id IN (:nativeLanguageIds)`,
        {
          replacements: { nativeLanguageIds },
          type: sequelize.QueryTypes.SELECT
        }
      );
      if (Array.isArray(nativeLanguages)) {
        const nativeLanguageMap = new Map(nativeLanguages.map(l => [l.id, l.name]));
        coursesData.forEach(c => {
          c.nativeLanguageName = c.nativeLanguageId ? nativeLanguageMap.get(c.nativeLanguageId) || null : null;
        });
      }
    }

    return coursesData;
  }

  async getCourseByShareCode(hashedUserId, shareCode) {
    const user = await this._getUserByHashedId(hashedUserId);
    const code = typeof shareCode === 'string' ? shareCode.trim() : '';
    
    if (!code || code.length < 6 || code.length > 128) {
      const err = new Error('Invalid share code');
      err.status = 400;
      throw err;
    }

    const course = await VocabCourse.findOne({
      where: { shareCode: code }
    });

    if (!course) {
      const err = new Error('Course not found');
      err.status = 404;
      throw err;
    }

    // Prüfe Zugriff (öffentlich oder Besitzer)
    if (course.ownerUserId !== user.id && !course.isPublic) {
      const err = new Error('Course is not public');
      err.status = 403;
      throw err;
    }

    return course.get({ plain: true });
  }

  async getCourse(hashedUserId, courseId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const course = await VocabCourse.findByPk(courseId, {
      include: [
        {
          model: VocabCourseLesson,
          as: 'lessons',
          order: [['lessonNumber', 'ASC']]
        }
      ]
    });

    if (!course) {
      const err = new Error('Course not found');
      err.status = 404;
      throw err;
    }

    // Prüfe Zugriff
    if (course.ownerUserId !== user.id && !course.isPublic) {
      const err = new Error('Access denied');
      err.status = 403;
      throw err;
    }

    const courseData = course.get({ plain: true });
    courseData.lessons = courseData.lessons || [];
    
    // Sortiere Lektionen nach Woche, Tag, dann Nummer
    courseData.lessons.sort((a, b) => {
      if (a.weekNumber !== b.weekNumber) {
        return (a.weekNumber || 999) - (b.weekNumber || 999);
      }
      if (a.dayNumber !== b.dayNumber) {
        return (a.dayNumber || 999) - (b.dayNumber || 999);
      }
      return a.lessonNumber - b.lessonNumber;
    });

    return courseData;
  }

  async updateCourse(hashedUserId, courseId, { title, description, languageId, nativeLanguageId, difficultyLevel, isPublic }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const course = await VocabCourse.findByPk(courseId);

    if (!course) {
      const err = new Error('Course not found');
      err.status = 404;
      throw err;
    }

    if (course.ownerUserId !== user.id) {
      const err = new Error('Only the owner can update the course');
      err.status = 403;
      throw err;
    }

    const updates = {};
    if (title !== undefined) updates.title = title;
    if (description !== undefined) updates.description = description;
    if (languageId !== undefined) updates.languageId = Number(languageId);
    if (nativeLanguageId !== undefined) updates.nativeLanguageId = nativeLanguageId ? Number(nativeLanguageId) : null;
    if (difficultyLevel !== undefined) updates.difficultyLevel = Number(difficultyLevel);
    if (isPublic !== undefined) {
      updates.isPublic = Boolean(isPublic);
      // Generiere Share-Code wenn Kurs öffentlich wird
      if (isPublic && !course.shareCode) {
        updates.shareCode = crypto.randomBytes(8).toString('hex');
      } else if (!isPublic) {
        updates.shareCode = null;
      }
    }

    await course.update(updates);
    return course.get({ plain: true });
  }

  async deleteCourse(hashedUserId, courseId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const course = await VocabCourse.findByPk(courseId);

    if (!course) {
      const err = new Error('Course not found');
      err.status = 404;
      throw err;
    }

    if (course.ownerUserId !== user.id) {
      const err = new Error('Only the owner can delete the course');
      err.status = 403;
      throw err;
    }

    await course.destroy();
    return { success: true };
  }

  async getLesson(hashedUserId, lessonId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const lesson = await VocabCourseLesson.findByPk(lessonId, {
      include: [
        {
          model: VocabCourse,
          as: 'course'
        },
        {
          model: VocabGrammarExercise,
          as: 'grammarExercises',
          include: [
            {
              model: VocabGrammarExerciseType,
              as: 'exerciseType'
            }
          ],
          required: false,
          separate: true,
          order: [['exerciseNumber', 'ASC']]
        }
      ]
    });

    if (!lesson) {
      const err = new Error('Lesson not found');
      err.status = 404;
      throw err;
    }

    // Prüfe Zugriff
    if (lesson.course.ownerUserId !== user.id && !lesson.course.isPublic) {
      const err = new Error('Access denied');
      err.status = 403;
      throw err;
    }

    const plainLesson = lesson.get({ plain: true });
    console.log(`[getLesson] Lektion ${lessonId} geladen:`, {
      id: plainLesson.id,
      title: plainLesson.title,
      exerciseCount: plainLesson.grammarExercises ? plainLesson.grammarExercises.length : 0,
      exercises: plainLesson.grammarExercises
    });
    return plainLesson;
  }

  async addLessonToCourse(hashedUserId, courseId, { chapterId, lessonNumber, title, description, weekNumber, dayNumber, lessonType, audioUrl, culturalNotes, targetMinutes, targetScorePercent, requiresReview }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const course = await VocabCourse.findByPk(courseId);

    if (!course) {
      const err = new Error('Course not found');
      err.status = 404;
      throw err;
    }

    if (course.ownerUserId !== user.id) {
      const err = new Error('Only the owner can add lessons');
      err.status = 403;
      throw err;
    }

    // Prüfe, ob Kapitel zur gleichen Sprache gehört (nur wenn chapterId angegeben)
    if (chapterId) {
      const [chapter] = await sequelize.query(
        `SELECT language_id FROM community.vocab_chapter WHERE id = :chapterId`,
        {
          replacements: { chapterId: Number(chapterId) },
          type: sequelize.QueryTypes.SELECT
        }
      );

      if (!chapter || chapter.language_id !== course.languageId) {
        const err = new Error('Chapter does not belong to the course language');
        err.status = 400;
        throw err;
      }
    }

    const lesson = await VocabCourseLesson.create({
      courseId: course.id,
      chapterId: chapterId ? Number(chapterId) : null,
      lessonNumber: Number(lessonNumber),
      title,
      description,
      weekNumber: weekNumber ? Number(weekNumber) : null,
      dayNumber: dayNumber ? Number(dayNumber) : null,
      lessonType: lessonType || 'vocab',
      audioUrl: audioUrl || null,
      culturalNotes: culturalNotes || null,
      targetMinutes: targetMinutes ? Number(targetMinutes) : null,
      targetScorePercent: targetScorePercent ? Number(targetScorePercent) : 80,
      requiresReview: requiresReview !== undefined ? Boolean(requiresReview) : false
    });

    return lesson.get({ plain: true });
  }

  async updateLesson(hashedUserId, lessonId, { title, description, lessonNumber, weekNumber, dayNumber, lessonType, audioUrl, culturalNotes, targetMinutes, targetScorePercent, requiresReview }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const lesson = await VocabCourseLesson.findByPk(lessonId, {
      include: [{ model: VocabCourse, as: 'course' }]
    });

    if (!lesson) {
      const err = new Error('Lesson not found');
      err.status = 404;
      throw err;
    }

    if (lesson.course.ownerUserId !== user.id) {
      const err = new Error('Only the owner can update lessons');
      err.status = 403;
      throw err;
    }

    const updates = {};
    if (title !== undefined) updates.title = title;
    if (description !== undefined) updates.description = description;
    if (lessonNumber !== undefined) updates.lessonNumber = Number(lessonNumber);
    if (weekNumber !== undefined) updates.weekNumber = weekNumber ? Number(weekNumber) : null;
    if (dayNumber !== undefined) updates.dayNumber = dayNumber ? Number(dayNumber) : null;
    if (lessonType !== undefined) updates.lessonType = lessonType;
    if (audioUrl !== undefined) updates.audioUrl = audioUrl;
    if (culturalNotes !== undefined) updates.culturalNotes = culturalNotes;
    if (targetMinutes !== undefined) updates.targetMinutes = targetMinutes ? Number(targetMinutes) : null;
    if (targetScorePercent !== undefined) updates.targetScorePercent = Number(targetScorePercent);
    if (requiresReview !== undefined) updates.requiresReview = Boolean(requiresReview);

    await lesson.update(updates);
    return lesson.get({ plain: true });
  }

  async deleteLesson(hashedUserId, lessonId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const lesson = await VocabCourseLesson.findByPk(lessonId, {
      include: [{ model: VocabCourse, as: 'course' }]
    });

    if (!lesson) {
      const err = new Error('Lesson not found');
      err.status = 404;
      throw err;
    }

    if (lesson.course.ownerUserId !== user.id) {
      const err = new Error('Only the owner can delete lessons');
      err.status = 403;
      throw err;
    }

    await lesson.destroy();
    return { success: true };
  }

  async enrollInCourse(hashedUserId, courseId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const course = await VocabCourse.findByPk(courseId);

    if (!course) {
      const err = new Error('Course not found');
      err.status = 404;
      throw err;
    }

    // Prüfe Zugriff
    if (course.ownerUserId !== user.id && !course.isPublic) {
      const err = new Error('Course is not public');
      err.status = 403;
      throw err;
    }

    const [enrollment, created] = await VocabCourseEnrollment.findOrCreate({
      where: { userId: user.id, courseId: course.id },
      defaults: { userId: user.id, courseId: course.id }
    });

    if (!created) {
      const err = new Error('Already enrolled in this course');
      err.status = 400;
      throw err;
    }

    return enrollment.get({ plain: true });
  }

  async unenrollFromCourse(hashedUserId, courseId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const enrollment = await VocabCourseEnrollment.findOne({
      where: { userId: user.id, courseId: Number(courseId) }
    });

    if (!enrollment) {
      const err = new Error('Not enrolled in this course');
      err.status = 404;
      throw err;
    }

    await enrollment.destroy();
    return { success: true };
  }

  async getMyCourses(hashedUserId) {
    const user = await this._getUserByHashedId(hashedUserId);

    const enrollments = await VocabCourseEnrollment.findAll({
      where: { userId: user.id },
      include: [{ model: VocabCourse, as: 'course' }],
      order: [['enrolledAt', 'DESC']]
    });

    return enrollments.map(e => ({
      ...e.course.get({ plain: true }),
      enrolledAt: e.enrolledAt
    }));
  }

  async getCourseProgress(hashedUserId, courseId) {
    const user = await this._getUserByHashedId(hashedUserId);
    
    // Prüfe Einschreibung
    const enrollment = await VocabCourseEnrollment.findOne({
      where: { userId: user.id, courseId: Number(courseId) }
    });

    if (!enrollment) {
      const err = new Error('Not enrolled in this course');
      err.status = 403;
      throw err;
    }

    const progress = await VocabCourseProgress.findAll({
      where: { userId: user.id, courseId: Number(courseId) },
      include: [{ model: VocabCourseLesson, as: 'lesson' }],
      order: [[{ model: VocabCourseLesson, as: 'lesson' }, 'lessonNumber', 'ASC']]
    });

    return progress.map(p => p.get({ plain: true }));
  }

  async updateLessonProgress(hashedUserId, lessonId, { completed, score, timeSpentMinutes }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const lesson = await VocabCourseLesson.findByPk(lessonId, {
      include: [{ model: VocabCourse, as: 'course' }]
    });

    if (!lesson) {
      const err = new Error('Lesson not found');
      err.status = 404;
      throw err;
    }

    // Prüfe Einschreibung
    const enrollment = await VocabCourseEnrollment.findOne({
      where: { userId: user.id, courseId: lesson.courseId }
    });

    if (!enrollment) {
      const err = new Error('Not enrolled in this course');
      err.status = 403;
      throw err;
    }

    const lessonData = await VocabCourseLesson.findByPk(lesson.id);
    const targetScore = lessonData.targetScorePercent || 80;
    const actualScore = Number(score) || 0;
    const hasReachedTarget = actualScore >= targetScore;
    
    // Prüfe, ob Lektion als abgeschlossen gilt (nur wenn Ziel erreicht oder explizit completed=true)
    const isCompleted = Boolean(completed) || (hasReachedTarget && lessonData.requiresReview === false);

    const [progress, created] = await VocabCourseProgress.findOrCreate({
      where: { userId: user.id, lessonId: lesson.id },
      defaults: {
        userId: user.id,
        courseId: lesson.courseId,
        lessonId: lesson.id,
        completed: isCompleted,
        score: actualScore,
        lastAccessedAt: new Date()
      }
    });

    if (!created) {
      const updates = { lastAccessedAt: new Date() };
      if (score !== undefined) {
        updates.score = Math.max(progress.score, actualScore);
        // Prüfe, ob Ziel jetzt erreicht wurde
        if (updates.score >= targetScore && !progress.completed) {
          if (!lessonData.requiresReview) {
            updates.completed = true;
            updates.completedAt = new Date();
          }
        }
      }
      if (completed !== undefined) {
        updates.completed = Boolean(completed);
        if (completed && !progress.completedAt) {
          updates.completedAt = new Date();
        }
      }
      await progress.update(updates);
    } else if (isCompleted) {
      progress.completed = true;
      progress.completedAt = new Date();
      await progress.save();
    }

    const progressData = progress.get({ plain: true });
    progressData.targetScore = targetScore;
    progressData.hasReachedTarget = progressData.score >= targetScore;
    progressData.needsReview = lessonData.requiresReview && !progressData.hasReachedTarget;

    return progressData;
  }

  // ========== GRAMMAR EXERCISE METHODS ==========

  async getExerciseTypes() {
    const types = await VocabGrammarExerciseType.findAll({
      order: [['name', 'ASC']]
    });
    return types.map(t => t.get({ plain: true }));
  }

  async createGrammarExercise(hashedUserId, lessonId, { exerciseTypeId, exerciseNumber, title, instruction, questionData, answerData, explanation }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const lesson = await VocabCourseLesson.findByPk(lessonId, {
      include: [{ model: VocabCourse, as: 'course' }]
    });

    if (!lesson) {
      const err = new Error('Lesson not found');
      err.status = 404;
      throw err;
    }

    // Prüfe, ob User Besitzer des Kurses ist
    if (lesson.course.ownerUserId !== user.id) {
      const err = new Error('Only the owner can add grammar exercises');
      err.status = 403;
      throw err;
    }

    const exercise = await VocabGrammarExercise.create({
      lessonId: lesson.id,
      exerciseTypeId: Number(exerciseTypeId),
      exerciseNumber: Number(exerciseNumber),
      title,
      instruction,
      questionData,
      answerData,
      explanation,
      createdByUserId: user.id
    });

    return exercise.get({ plain: true });
  }

  async getGrammarExercisesForLesson(hashedUserId, lessonId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const lesson = await VocabCourseLesson.findByPk(lessonId, {
      include: [{ model: VocabCourse, as: 'course' }]
    });

    if (!lesson) {
      const err = new Error('Lesson not found');
      err.status = 404;
      throw err;
    }

    // Prüfe Zugriff
    if (lesson.course.ownerUserId !== user.id && !lesson.course.isPublic) {
      const err = new Error('Access denied');
      err.status = 403;
      throw err;
    }

    const exercises = await VocabGrammarExercise.findAll({
      where: { lessonId: lesson.id },
      include: [{ model: VocabGrammarExerciseType, as: 'exerciseType' }],
      order: [['exerciseNumber', 'ASC']]
    });

    return exercises.map(e => e.get({ plain: true }));
  }

  async getGrammarExercise(hashedUserId, exerciseId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const exercise = await VocabGrammarExercise.findByPk(exerciseId, {
      include: [
        { model: VocabCourseLesson, as: 'lesson', include: [{ model: VocabCourse, as: 'course' }] },
        { model: VocabGrammarExerciseType, as: 'exerciseType' }
      ]
    });

    if (!exercise) {
      const err = new Error('Exercise not found');
      err.status = 404;
      throw err;
    }

    // Prüfe Zugriff
    if (exercise.lesson.course.ownerUserId !== user.id && !exercise.lesson.course.isPublic) {
      const err = new Error('Access denied');
      err.status = 403;
      throw err;
    }

    return exercise.get({ plain: true });
  }

  async checkGrammarExerciseAnswer(hashedUserId, exerciseId, userAnswer) {
    const user = await this._getUserByHashedId(hashedUserId);
    const exercise = await VocabGrammarExercise.findByPk(exerciseId, {
      include: [
        { model: VocabCourseLesson, as: 'lesson', include: [{ model: VocabCourse, as: 'course' }] }
      ]
    });

    if (!exercise) {
      const err = new Error('Exercise not found');
      err.status = 404;
      throw err;
    }

    // Prüfe Einschreibung
    const enrollment = await VocabCourseEnrollment.findOne({
      where: { userId: user.id, courseId: exercise.lesson.courseId }
    });

    if (!enrollment) {
      const err = new Error('Not enrolled in this course');
      err.status = 403;
      throw err;
    }

    // Überprüfe Antwort (vereinfachte Logik - kann je nach Übungstyp erweitert werden)
    const isCorrect = this._checkAnswer(exercise.answerData, exercise.questionData, userAnswer, exercise.exerciseTypeId);

    // Speichere Fortschritt
    const [progress, created] = await VocabGrammarExerciseProgress.findOrCreate({
      where: { userId: user.id, exerciseId: exercise.id },
      defaults: {
        userId: user.id,
        exerciseId: exercise.id,
        attempts: 1,
        correctAttempts: isCorrect ? 1 : 0,
        lastAttemptAt: new Date(),
        completed: false
      }
    });

    if (!created) {
      progress.attempts += 1;
      if (isCorrect) {
        progress.correctAttempts += 1;
        if (!progress.completed) {
          progress.completed = true;
          progress.completedAt = new Date();
        }
      }
      progress.lastAttemptAt = new Date();
      await progress.save();
    } else if (isCorrect) {
      progress.completed = true;
      progress.completedAt = new Date();
      await progress.save();
    }

    // Extrahiere richtige Antwort und Alternativen
    const answerData = typeof exercise.answerData === 'string' 
      ? JSON.parse(exercise.answerData) 
      : exercise.answerData;
    const questionData = typeof exercise.questionData === 'string'
      ? JSON.parse(exercise.questionData)
      : exercise.questionData;
    
    let correctAnswer = null;
    let alternatives = [];

    // Für Multiple Choice: Extrahiere die richtige Antwort aus dem Index
    if (exercise.exerciseTypeId === 2 && answerData.correctAnswer !== undefined) {
      const options = questionData?.options || [];
      const correctIndex = answerData.correctAnswer;
      if (options[correctIndex] !== undefined) {
        correctAnswer = options[correctIndex];
      }
      // Alternativen sind alle anderen Optionen
      alternatives = options.filter((opt, idx) => idx !== correctIndex);
    }
    // Für Gap Fill: Extrahiere aus answers Array
    else if (exercise.exerciseTypeId === 1 && answerData.answers) {
      correctAnswer = Array.isArray(answerData.answers) 
        ? answerData.answers.join(', ') 
        : answerData.answers;
    }
    // Fallback: Versuche correct oder correctAnswer
    else {
      correctAnswer = Array.isArray(answerData.correct) 
        ? answerData.correct[0] 
        : (answerData.correct || answerData.correctAnswer);
      alternatives = answerData.alternatives || [];
    }

    return {
      correct: isCorrect,
      correctAnswer: correctAnswer,
      alternatives: alternatives,
      explanation: exercise.explanation,
      progress: progress.get({ plain: true })
    };
  }

  _checkAnswer(answerData, questionData, userAnswer, exerciseTypeId) {
    // Vereinfachte Antwortprüfung - kann je nach Übungstyp erweitert werden
    if (!answerData || userAnswer === undefined || userAnswer === null) return false;

    // Parse JSON strings
    const parsedAnswerData = typeof answerData === 'string' ? JSON.parse(answerData) : answerData;
    const parsedQuestionData = typeof questionData === 'string' ? JSON.parse(questionData) : questionData;

    // Für Multiple Choice: Prüfe ob userAnswer (Index) mit correctAnswer (Index) übereinstimmt
    if (exerciseTypeId === 2) { // multiple_choice
      const correctIndex = parsedAnswerData.correctAnswer !== undefined 
        ? parsedAnswerData.correctAnswer 
        : (parsedAnswerData.correct !== undefined ? parsedAnswerData.correct : null);
      if (correctIndex === null) return false;
      // userAnswer ist der Index (0, 1, 2, ...)
      return Number(userAnswer) === Number(correctIndex);
    }

    // Für Lückentext: Normalisiere und vergleiche
    if (exerciseTypeId === 1) { // gap_fill
      const normalize = (str) => String(str || '').trim().toLowerCase();
      const correctAnswers = parsedAnswerData.answers || parsedAnswerData.correct || [];
      const correctAnswersArray = Array.isArray(correctAnswers) ? correctAnswers : [correctAnswers];
      
      // userAnswer ist ein Array von Antworten
      if (Array.isArray(userAnswer)) {
        if (userAnswer.length !== correctAnswersArray.length) return false;
        return userAnswer.every((ans, idx) => {
          const correct = correctAnswersArray[idx];
          return normalize(ans) === normalize(correct);
        });
      } else {
        // Fallback: Einzelne Antwort
        const normalizedUserAnswer = normalize(userAnswer);
        return correctAnswersArray.some(correct => normalize(correct) === normalizedUserAnswer);
      }
    }

    // Für andere Typen: einfacher String-Vergleich (kann später erweitert werden)
    const normalize = (str) => String(str || '').trim().toLowerCase();
    const correctAnswers = parsedAnswerData.correct || parsedAnswerData.correctAnswer || [];
    const correctAnswersArray = Array.isArray(correctAnswers) ? correctAnswers : [correctAnswers];
    const normalizedUserAnswer = normalize(userAnswer);
    return correctAnswersArray.some(correct => normalize(correct) === normalizedUserAnswer);
  }

  async getGrammarExerciseProgress(hashedUserId, lessonId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const exercises = await this.getGrammarExercisesForLesson(hashedUserId, lessonId);
    
    const exerciseIds = exercises.map(e => e.id);
    const progress = await VocabGrammarExerciseProgress.findAll({
      where: {
        userId: user.id,
        exerciseId: { [Op.in]: exerciseIds }
      }
    });

    const progressMap = new Map(progress.map(p => [p.exerciseId, p.get({ plain: true })]));
    
    return exercises.map(exercise => ({
      ...exercise,
      progress: progressMap.get(exercise.id) || null
    }));
  }

  async updateGrammarExercise(hashedUserId, exerciseId, { title, instruction, questionData, answerData, explanation, exerciseNumber }) {
    const user = await this._getUserByHashedId(hashedUserId);
    const exercise = await VocabGrammarExercise.findByPk(exerciseId, {
      include: [
        { model: VocabCourseLesson, as: 'lesson', include: [{ model: VocabCourse, as: 'course' }] }
      ]
    });

    if (!exercise) {
      const err = new Error('Exercise not found');
      err.status = 404;
      throw err;
    }

    if (exercise.lesson.course.ownerUserId !== user.id) {
      const err = new Error('Only the owner can update exercises');
      err.status = 403;
      throw err;
    }

    const updates = {};
    if (title !== undefined) updates.title = title;
    if (instruction !== undefined) updates.instruction = instruction;
    if (questionData !== undefined) updates.questionData = questionData;
    if (answerData !== undefined) updates.answerData = answerData;
    if (explanation !== undefined) updates.explanation = explanation;
    if (exerciseNumber !== undefined) updates.exerciseNumber = Number(exerciseNumber);

    await exercise.update(updates);
    return exercise.get({ plain: true });
  }

  async deleteGrammarExercise(hashedUserId, exerciseId) {
    const user = await this._getUserByHashedId(hashedUserId);
    const exercise = await VocabGrammarExercise.findByPk(exerciseId, {
      include: [
        { model: VocabCourseLesson, as: 'lesson', include: [{ model: VocabCourse, as: 'course' }] }
      ]
    });

    if (!exercise) {
      const err = new Error('Exercise not found');
      err.status = 404;
      throw err;
    }

    if (exercise.lesson.course.ownerUserId !== user.id) {
      const err = new Error('Only the owner can delete exercises');
      err.status = 403;
      throw err;
    }

    await exercise.destroy();
    return { success: true };
  }
}


