import crypto from 'crypto';
import User from '../models/community/user.js';
import { sequelize } from '../utils/sequelize.js';
import { notifyUser } from '../utils/socket.js';

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 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) };
    });
  }
}


