const fs = require('fs');
const path = require('path');
const express = require('express');
const { WebSocketServer } = require('ws');
const chalk = require('chalk');
const open = require('open');

const { connectToTikTok } = require('./tiktokConnector');
const { startSimulation } = require('./simulator');
const { loadGiftLegend, normalizeKey } = require('./giftLegend');
const { creerStats } = require('./stats');

/**
 * Demarre le serveur HTTP (fichiers statiques du jeu choisi) + WebSocket (evenements TikTok)
 * puis ouvre le navigateur sur le jeu.
 */
function startGameServer({ game, tiktokUsername, port, simulateWhenOffline }) {
  const app = express();

  // Statistiques cumulees sur tous les lives (tableau de bord prive).
  const stats = creerStats();
  stats.demarrerLive({ jeu: game.id, pseudoTikTok: tiktokUsername || null });

  app.use(express.json());
  app.get('/api/stats', (req, res) => res.json(stats.resume()));
  app.post('/api/stats/reset', (req, res) => { stats.reinitialiser(); res.json({ ok: true }); });

  // ---- Reglages du jeu, modifiables en direct depuis le tableau de bord ----
  const CHEMIN_CONFIG_JEU = path.join(__dirname, '..', 'public', 'overlay', 'game-config.json');

  function lireConfigJeu() {
    try { return JSON.parse(fs.readFileSync(CHEMIN_CONFIG_JEU, 'utf-8')); } catch (e) { return {}; }
  }

  app.get('/api/game-config', (req, res) => res.json(lireConfigJeu()));

  app.post('/api/game-config', (req, res) => {
    const cfg = lireConfigJeu();
    const corps = req.body || {};
    const palier = Math.floor(Number(corps.likeMilestone));
    const etages = Math.floor(Number(corps.likeMilestoneFloors));
    if (palier > 0) cfg.likeMilestone = palier;
    if (etages > 0) cfg.likeMilestoneFloors = etages;

    try {
      fs.writeFileSync(CHEMIN_CONFIG_JEU, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
    } catch (e) {
      return res.status(500).json({ erreur: e.message });
    }

    // Diffuse aux pages ouvertes : le changement s'applique sans rien recharger.
    broadcast({
      type: 'config',
      likeMilestone: cfg.likeMilestone,
      likeMilestoneFloors: cfg.likeMilestoneFloors
    });
    console.log(chalk.cyan(`[reglages] palier de likes = ${cfg.likeMilestone}, etages detruits = ${cfg.likeMilestoneFloors}`));
    res.json(cfg);
  });
  app.use('/shared', express.static(path.join(__dirname, '..', 'public', 'shared')));
  app.use('/overlay', express.static(path.join(__dirname, '..', 'public', 'overlay')));
  app.use('/simulator', express.static(path.join(__dirname, '..', 'public', 'simulator')));
  app.use('/', express.static(path.join(__dirname, '..', 'public', 'games', game.folder)));

  const server = app.listen(port, () => {
    console.log(chalk.green(`Serveur pret : http://localhost:${port}`));
  });

  // Port deja pris (le plus souvent : une partie precedente tourne encore).
  // On explique quoi faire au lieu d'afficher une pile d'erreurs Node.
  server.on('error', (err) => {
    if (err.code === 'EADDRINUSE') {
      console.log(chalk.red(`\nLe port ${port} est deja utilise.`));
      console.log(chalk.yellow('C\'est presque toujours une partie precedente restee ouverte dans un autre terminal.'));
      console.log(chalk.yellow('Deux solutions :'));
      console.log(chalk.yellow(`  1) Ferme l'autre fenetre (Ctrl+C dedans), ou libere le port :`));
      console.log(chalk.cyan(`     npx kill-port ${port}`));
      console.log(chalk.yellow('  2) Relance et choisis un autre port quand il te le demande.\n'));
    } else {
      console.log(chalk.red(`\nImpossible de demarrer le serveur : ${err.message}\n`));
    }
    process.exit(1);
  });

  const wss = new WebSocketServer({ server, path: '/ws' });
  const clients = new Set();

  wss.on('connection', (ws) => {
    clients.add(ws);
    ws.on('close', () => clients.delete(ws));

    // Relai generique : un client (le jeu, l'overlay) peut envoyer un message
    // qui est retransmis a tous les autres (etage courant, objectif, etc.),
    // meme si le jeu et l'overlay tournent dans deux navigateurs differents
    // (ex: OBS Browser Source pour l'overlay).
    // IMPORTANT : `raw` est un Buffer. Le renvoyer tel quel enverrait une trame
    // BINAIRE, que les pages recoivent comme un Blob -> JSON.parse echoue et
    // l'evenement est perdu en silence. On rediffuse donc toujours du TEXTE.
    ws.on('message', (raw, isBinary) => {
      if (isBinary) return;
      let text = raw.toString();
      // Les evenements simules depuis la page /simulator comptent aussi dans
      // les statistiques et dans le compteur de likes (ils servent notamment a
      // rattraper un don qui n'est pas parti).
      try {
        const event = JSON.parse(text);
        if (followEnDoublon(event)) return; // meme regle des 5 min qu'en vrai live
        marquerLikesSession(event);
        stats.enregistrer(event);
        text = JSON.stringify(event);
      } catch (e) { /* pas du JSON : simple relai */ }
      for (const client of clients) {
        if (client !== ws && client.readyState === 1) client.send(text);
      }
    });
  });

  // Signale dans le terminal tout don dont le nom ne correspond a aucune entree
  // de gift-legend.json, pour pouvoir recuperer le vrai nom TikTok et l'ajouter.
  const { knownKeys } = loadGiftLegend();
  function checkKnownGift(event) {
    if (event.type !== 'gift') return;
    const key = normalizeKey(event.giftName);
    if (!knownKeys.has(key)) {
      console.log(chalk.yellow(
        `[dons] Nom de don inconnu : "${event.giftName}"` +
        (event.giftId ? ` (giftId TikTok ${event.giftId})` : '') +
        ` (${event.diamondCount || 0} diamants x${event.repeatCount || 1}) ` +
        `- ajoute-le dans public/overlay/gift-legend.json si besoin.`
      ));
    }
  }

  // Compteur de likes de CETTE session, seule reference commune au jeu et a
  // l'overlay. (totalLikeCount de TikTok est le total a vie du salon : il ne
  // veut rien dire pour "toutes les 500 likes = une explosion", et le jeu
  // comptait de son cote -> la barre et l'explosion n'etaient jamais d'accord.)
  let likesSession = 0;   // likes recus depuis le lancement du serveur
  let likesTotalLive = 0; // total affiche par TikTok (inclut l'avant-connexion)

  function marquerLikesSession(event) {
    if (!event || event.type !== 'like') return event;
    const taps = Number(event.likeCount) > 0 ? Number(event.likeCount) : 1;
    likesSession += taps;

    // TikTok fournit le total du salon depuis le debut du live : on le prend
    // tel quel pour afficher le MEME chiffre que le live. Sans lui (mode
    // simulation), on avance notre propre compteur.
    const totalTikTok = Number(event.totalLikeCount) || 0;
    if (totalTikTok > 0) likesTotalLive = Math.max(likesTotalLive, totalTikTok);
    else likesTotalLive += taps;

    event.likesSession = likesSession;
    event.likesTotal = likesTotalLive;
    return event;
  }

  // Anti-refollow : quelqu'un qui se desabonne puis se reabonne (ou dont TikTok
  // renvoie l'evenement) ne redeclenche pas l'effet pendant 5 minutes.
  const DELAI_REFOLLOW_MS = 5 * 60 * 1000;
  const derniersFollows = new Map(); // uniqueId -> horodatage

  function followEnDoublon(event) {
    if (!event || event.type !== 'follow' || !event.uniqueId) return false;
    const maintenant = Date.now();
    const precedent = derniersFollows.get(event.uniqueId);
    if (precedent && maintenant - precedent < DELAI_REFOLLOW_MS) {
      const reste = Math.ceil((DELAI_REFOLLOW_MS - (maintenant - precedent)) / 1000);
      console.log(chalk.gray(`[follow] ${event.uniqueId} ignore (deja suivi il y a moins de 5 min, ${reste}s restantes)`));
      return true;
    }
    derniersFollows.set(event.uniqueId, maintenant);
    return false;
  }

  function broadcast(event) {
    if (followEnDoublon(event)) return;
    marquerLikesSession(event);
    checkKnownGift(event);
    stats.enregistrer(event);
    if (event && event.type) etatLive.dernierEvenementLe = new Date().toISOString();
    const payload = JSON.stringify(event);
    for (const client of clients) {
      if (client.readyState === 1) client.send(payload);
    }
  }

  let stopSimulation = null;
  let usingSimulation = false;

  function fallbackToSimulationIfNeeded(reason) {
    if (!simulateWhenOffline || usingSimulation) return;
    usingSimulation = true;
    console.log(chalk.yellow(`${reason} -> passage en mode simulation (evenements factices).`));
    stopSimulation = startSimulation(broadcast);
  }

  // ---- Etat de la connexion au live, expose au tableau de bord ----
  let connexionTikTok = null;
  const etatLive = {
    pseudo: (tiktokUsername || '').trim() || null,
    statut: 'aucun-pseudo', // aucun-pseudo | connexion | connecte | erreur | ended | disconnected
    message: 'Aucun pseudo TikTok : mode manuel (simulateur uniquement).',
    depuis: new Date().toISOString(),
    dernierEvenementLe: null
  };

  function majEtatLive(statut, message) {
    etatLive.statut = statut;
    etatLive.message = message;
    etatLive.depuis = new Date().toISOString();
  }

  async function connecterAuLive(pseudoDemande) {
    const nom = String(pseudoDemande || etatLive.pseudo || '').trim().replace(/^@/, '');
    if (!nom) {
      majEtatLive('aucun-pseudo', 'Aucun pseudo TikTok : indique-en un pour te connecter.');
      return etatLive;
    }
    etatLive.pseudo = nom;
    majEtatLive('connexion', `Connexion au live de @${nom}...`);

    if (connexionTikTok) {
      try { connexionTikTok.disconnect(); } catch (e) { /* deja fermee */ }
      connexionTikTok = null;
    }

    connexionTikTok = await connectToTikTok(nom, broadcast, (status, message) => {
      if (status === 'connected') {
        majEtatLive('connecte', message);
        console.log(chalk.green(message));
      } else {
        majEtatLive(status === 'error' ? 'erreur' : status, message);
        console.log(chalk.red(message));
        if (status === 'error' || status === 'ended' || status === 'disconnected') {
          fallbackToSimulationIfNeeded(message);
        }
      }
    });
    return etatLive;
  }

  app.get('/api/live-status', (req, res) => {
    res.json({ ...etatLive, enLigne: etatLive.statut === 'connecte' });
  });

  app.post('/api/live/reconnect', async (req, res) => {
    try {
      const etat = await connecterAuLive(req.body && req.body.pseudo);
      res.json({ ...etat, enLigne: etat.statut === 'connecte' });
    } catch (err) {
      majEtatLive('erreur', `Echec de la reconnexion : ${err.message}`);
      res.json({ ...etatLive, enLigne: false });
    }
  });

  if (etatLive.pseudo) {
    connecterAuLive(etatLive.pseudo);
  } else {
    fallbackToSimulationIfNeeded('Aucun pseudo TikTok fourni');
  }

  const url = `http://localhost:${port}`;
  open(url);
  console.log(chalk.cyan(`Overlay OBS (source Navigateur, fond transparent) : ${url}/overlay?profile=${game.id}`));
  console.log(chalk.cyan('Format recommande pour cette source OBS : 1080 x 2340 (telephone)'));
  console.log(chalk.cyan(`Simulateur de dons (page web a toi, pas pour OBS)   : ${url}/simulator`));

  // Le simulateur s'ouvre toujours, meme en vrai live : il sert aussi de
  // secours si un don ne se declenche pas, et a montrer les effets aux viewers.
  open(`${url}/simulator`);

  return {
    server,
    broadcast,
    stop: () => {
      if (stopSimulation) stopSimulation();
      wss.close();
      server.close();
    }
  };
}

module.exports = { startGameServer };
