/**
 * Se connecte au live TikTok d'un pseudo et remonte des evenements normalises
 * via onEvent({type, uniqueId, ...}). Types: 'like', 'gift', 'follow', 'comment', 'share'.
 *
 * tiktok-live-connector v2 est distribue en ESM pur (plus de require()), d'ou
 * l'import() dynamique depuis notre code CommonJS. On utilise la classe de
 * base TikTokLiveConnection (pas le wrapper "legacy") car ce dernier contient
 * un bug qui fait planter tout le process des le premier message "roomUser"
 * (frequent) : `getTopViewerAttributes` y appelle `.map()` sur `ranksList`
 * meme quand ce champ est absent.
 */
async function connectToTikTok(username, onEvent, onStatus) {
  let TikTokLiveConnection;
  try {
    ({ TikTokLiveConnection } = await import('tiktok-live-connector'));
  } catch (err) {
    onStatus('error', `Impossible de charger tiktok-live-connector : ${err.message}`);
    return null;
  }

  // Le constructeur exige un objet d'options explicite (meme vide) : passer
  // undefined fait planter la connexion des la creation.
  const connection = new TikTokLiveConnection(username, {});

  // Un evenement 'error' sans listener fait planter tout le process Node
  // (comportement standard des EventEmitter). On le capte toujours.
  connection.on('error', (err) => {
    onStatus('error', `Erreur de connexion au live de @${username} : ${err && err.message ? err.message : err}`);
  });

  connection.connect()
    .then((state) => {
      onStatus('connected', `Connecte au live de @${username} (roomId ${state.roomId})`);
    })
    .catch((err) => {
      onStatus('error', `Impossible de se connecter au live de @${username}: ${err.message}`);
    });

  function avatarOf(user) {
    return user && user.avatarThumb && user.avatarThumb.urlList && user.avatarThumb.urlList[0];
  }

  function nameOf(user) {
    return user && user.displayId;
  }

  /**
   * IMPORTANT : cette version de la librairie decode les messages avec le
   * schema `tiktok-live-proto/v3`, ou les champs ont ete RENOMMES :
   *   like    : likeCount -> count      | totalLikeCount -> total (une chaine)
   *   don     : giftDetails -> gift
   *   comment : comment -> content
   * Lire les anciens noms renvoyait undefined : un tapotement de 100 likes
   * etait compte 1. On lit donc les deux, v3 d'abord.
   */
  function nombre(v) {
    const n = Number(v);
    return Number.isFinite(n) ? n : 0;
  }

  // TikTok peut livrer deux fois le meme message (reconnexion, accuse de
  // reception rejoue...). Chaque message porte un identifiant unique
  // `common.msgId` : on ignore ceux deja traites, sinon un seul don en
  // declenchait deux.
  const messagesVus = new Set();
  const ordreMessagesVus = [];
  const MAX_MESSAGES_MEMORISES = 2000;

  function dejaTraite(data) {
    const id = data && data.common && data.common.msgId;
    if (!id) return false;
    if (messagesVus.has(id)) return true;
    messagesVus.add(id);
    ordreMessagesVus.push(id);
    if (ordreMessagesVus.length > MAX_MESSAGES_MEMORISES) {
      messagesVus.delete(ordreMessagesVus.shift());
    }
    return false;
  }

  let likesTraces = 0;
  let dernierTotalLikes = 0;

  connection.on('like', (data) => {
    if (dejaTraite(data)) return;
    // v3 : `count`. v1/v2 : `likeCount`.
    let taps = nombre(data.count) || nombre(data.likeCount);
    const total = nombre(data.total) || nombre(data.totalLikeCount);

    // Filet de securite : si le nombre de tapotements manque mais que le total
    // du salon progresse, on deduit le lot depuis cette progression.
    if (!taps && total > dernierTotalLikes && dernierTotalLikes > 0) {
      taps = total - dernierTotalLikes;
    }
    if (total > 0) dernierTotalLikes = total;
    if (!taps) taps = 1;

    if (likesTraces < 10) {
      likesTraces++;
      console.log(
        `[likes] ${nameOf(data.user)} : count=${data.count} likeCount=${data.likeCount} ` +
        `total=${data.total} -> retenu ${taps} tapotement(s)`
      );
    }

    onEvent({
      type: 'like',
      uniqueId: nameOf(data.user),
      likeCount: taps,
      totalLikeCount: total,
      profilePictureUrl: avatarOf(data.user)
    });
  });

  // ATTENTION : dans l'API v2, `giftDetails` est OPTIONNEL (la lib elle-meme y
  // accede avec `?.`). Quand il manque, il faut retomber sur les champs de la
  // racine, sinon le nom arrive a "undefined" et le don n'est pas reconnu.
  // v3 : les details sont dans `gift`. v1/v2 : dans `giftDetails`.
  function detailsDon(data) {
    return data.gift || data.giftDetails || {};
  }

  function giftNameOf(data) {
    const d = detailsDon(data);
    return d.name || d.giftName || data.giftName || d.describe || null;
  }

  function giftDiamondsOf(data) {
    const d = detailsDon(data);
    const v = d.diamondCount !== undefined ? d.diamondCount
      : d.diamond_count !== undefined ? d.diamond_count
      : data.diamondCount;
    return v === undefined ? 0 : v;
  }

  let donsTraces = 0;

  connection.on('gift', (data) => {
    if (dejaTraite(data)) return; // meme message livre deux fois
    const details = detailsDon(data);
    const giftName = giftNameOf(data);

    // ATTENTION : `repeatEnd` est un NOMBRE (0 ou 1), pas un booleen, et il
    // vaut 0 pour tous les dons SANS combo. Filtrer sur repeatEnd seul jetait
    // donc silencieusement ces dons-la (Confettis, Coeur dans les mains...),
    // qui n'arrivaient jamais jusqu'au jeu.
    // Regle correcte : seul un don "streakable" (giftType 1) attend la fin de
    // sa serie ; tous les autres sont emis immediatement.
    const giftType = details.giftType !== undefined ? details.giftType : details.type;
    const serieEnCours = giftType === 1 && !data.repeatEnd;

    // Trace des premiers dons : permet de diagnostiquer sans deviner si un don
    // ne declenche toujours rien.
    if (donsTraces < 15) {
      donsTraces++;
      console.log(
        `[dons] recu "${giftName}" giftId=${data.giftId} giftType=${giftType} ` +
        `repeatEnd=${data.repeatEnd} repeatCount=${data.repeatCount} ` +
        `diamants=${giftDiamondsOf(data)}${serieEnCours ? ' -> serie en cours, ignore' : ''}`
      );
    }

    if (serieEnCours) return;

    if (!giftName) {
      // On n'a vraiment pas le nom : on trace ce qu'on a recu pour pouvoir le
      // rattraper (l'id du don est stable et suffit a l'identifier).
      console.log(
        `[dons] Don sans nom lisible (giftId=${data.giftId}). Champs recus : ` +
        Object.keys(data).join(', ') +
        (details && Object.keys(details).length ? ` | giftDetails : ${Object.keys(details).join(', ')}` : '')
      );
    }

    onEvent({
      type: 'gift',
      uniqueId: nameOf(data.user),
      giftName,
      giftId: data.giftId,
      diamondCount: giftDiamondsOf(data),
      repeatCount: data.repeatCount || 1,
      profilePictureUrl: avatarOf(data.user)
    });
  });

  connection.on('follow', (data) => {
    onEvent({ type: 'follow', uniqueId: nameOf(data.user), profilePictureUrl: avatarOf(data.user) });
  });

  connection.on('share', (data) => {
    onEvent({ type: 'share', uniqueId: nameOf(data.user), profilePictureUrl: avatarOf(data.user) });
  });

  connection.on('chat', (data) => {
    // v3 : `content`. v1/v2 : `comment`.
    onEvent({ type: 'comment', uniqueId: nameOf(data.user), comment: data.content || data.comment || '' });
  });

  connection.on('streamEnd', () => {
    onStatus('ended', 'Le live est termine.');
  });

  connection.on('disconnected', () => {
    onStatus('disconnected', 'Deconnecte du live.');
  });

  return connection;
}

module.exports = { connectToTikTok };
