// features/humeurs — analyse IA de l'humeur (début/fin d'appel)
// ---------------------------------------------------------------
// N'analyse QUE les appels affichés à l'écran, par lots, avec cache
// par appel persisté (étiquettes seules — jamais de contenu clinique,
// purgé à la déconnexion via window.LIVANA_PURGE_CACHES).
// Le prompt et ses invariants vivent ici, à côté de leur usage.

// ── Analyse IA de l'humeur : un niveau par appel des 7 derniers jours ──
// Les humeurs déduites de `isAlert` sont binaires (apaisé/anxieux) et donnent
// une courbe plate. On demande à Mistral de qualifier chaque appel à partir de
// son résumé. Convention du projet : JSON strict, extraction par regex,
// repli local obligatoire si l'IA est indisponible.
const MOODS_VALIDES = ['agitated', 'anxious', 'sad', 'calm', 'happy'];
// Cache PAR APPEL (et non par période) : passer de 7 j à 30 j ne réanalyse que
// les appels encore inconnus. Un appel n'est donc jamais envoyé deux fois à l'IA.
// Conservé d'une session à l'autre pour ne pas repayer l'analyse à chaque
// rechargement. Ne contient que des étiquettes d'humeur associées à des
// identifiants d'appel — aucun résumé ni transcription. Vidé à la déconnexion.
const CLE_CACHE_HUMEUR = 'livana.humeurs';
const CACHE_HUMEUR_APPEL = (() => {
  try { return JSON.parse(window.localStorage.getItem(CLE_CACHE_HUMEUR)) || {}; }
  catch (e) { return {}; }
})(); // { callId: { debut, fin } }

let sauvegardeCachePrevue = null;
function sauverCacheHumeur() {
  if (sauvegardeCachePrevue) return; // regroupe les écritures rapprochées
  sauvegardeCachePrevue = setTimeout(() => {
    sauvegardeCachePrevue = null;
    try { window.localStorage.setItem(CLE_CACHE_HUMEUR, JSON.stringify(CACHE_HUMEUR_APPEL)); }
    catch (e) { /* quota plein ou stockage refusé : le cache reste en mémoire */ }
  }, 800);
}

// Appelé par signOut() : aucune donnée d'un compte ne doit survivre au suivant
window.LIVANA_PURGE_CACHES = function () {
  Object.keys(CACHE_HUMEUR_APPEL).forEach((k) => delete CACHE_HUMEUR_APPEL[k]);
  try { window.localStorage.removeItem(CLE_CACHE_HUMEUR); } catch (e) {}
};
const LOT_ANALYSE = 25;        // appels par requête (transcriptions = plus volumineux)
const MSG_DEBUT = 10;          // premiers messages envoyés pour juger l'état initial
const MSG_FIN = 15;            // derniers messages envoyés pour juger l'état final

// Extrait le début et la fin d'une transcription : c'est ce qui permet de juger
// l'évolution de l'état du résident pendant l'appel. Si la conversation est
// courte, elle est envoyée en entier plutôt que découpée en deux morceaux.
function extraitsAppel(c) {
  const blocs = (c.transcript || []).map((b) =>
    `${b.speaker === 'livana' ? 'Livana' : 'Résident'}: ${b.text}`);
  if (!blocs.length) return { debut: '', fin: '', entier: true };
  if (blocs.length <= MSG_DEBUT + MSG_FIN) return { debut: blocs.join('\n'), fin: '', entier: true };
  return {
    debut: blocs.slice(0, MSG_DEBUT).join('\n'),
    fin: blocs.slice(-MSG_FIN).join('\n'),
    entier: false,
  };
}

// Humeurs d'un appel : analysées si disponibles, sinon valeurs brutes (isAlert).
// Exposées globalement : le tiroir de transcription (screens-shell) les utilise aussi.
const humeurDebut = (c) => (CACHE_HUMEUR_APPEL[c.id] || {}).debut || c.moodStart;
const humeurFin = (c) => (CACHE_HUMEUR_APPEL[c.id] || {}).fin || c.moodEnd;
window.humeurDebut = humeurDebut;
window.humeurFin = humeurFin;

// Appels dont l'analyse a échoué : on retombe alors sur les humeurs brutes
// plutôt que d'attendre indéfiniment.
const ECHECS_HUMEUR = new Set();

// ── Taux d'apaisement ──
// Part des appels qui SE TERMINENT dans un état positif. Seule l'humeur de fin
// compte : c'est l'état dans lequel Livana laisse le résident. Calculé sur les
// 7 derniers jours, à partir des humeurs analysées (jamais des humeurs brutes,
// qui vaudraient « apaisé » presque partout et gonfleraient artificiellement
// le taux). Renvoie null tant qu'aucun appel de la période n'est analysé.
const JOURS_APAISEMENT = 7;
const FINS_APAISEES = ['calm', 'happy'];

function tauxApaisement(residents, jours) {
  const appels = [];
  [].concat(residents).forEach((r) => appelsDerniersJours(r, jours || JOURS_APAISEMENT)
    .forEach((c) => appels.push(c)));
  const analyses = appels.filter((c) => CACHE_HUMEUR_APPEL[c.id]);
  if (!analyses.length) return null;
  const apaises = analyses.filter((c) => FINS_APAISEES.indexOf(CACHE_HUMEUR_APPEL[c.id].fin) !== -1);
  return {
    pourcent: Math.round(apaises.length / analyses.length * 100),
    analyses: analyses.length,
    total: appels.length,
  };
}

// Appels des N derniers jours d'une liste de résidents (pour lancer l'analyse)
const appels7jDe = (residents, jours) => {
  const liste = [];
  [].concat(residents).forEach((r) => appelsDerniersJours(r, jours || JOURS_APAISEMENT)
    .forEach((c) => liste.push(c)));
  return liste;
};

// Vrai tant que l'humeur réelle de cet appel n'est pas connue : on préfère
// n'afficher aucune humeur plutôt qu'une valeur par défaut qui sera démentie
// une seconde plus tard (contresens clinique).
const humeurEnAttente = (c) =>
  !CACHE_HUMEUR_APPEL[c.id] && !ECHECS_HUMEUR.has(c.id) && !!c.summary;

// Les deux pastilles d'un appel, ou un indicateur d'attente pendant l'analyse
function MoodFlow({ call }) {
  if (humeurEnAttente(call)) {
    return (
      <span className="mood-flow-attente" title="Analyse de l'humeur en cours">
        <span className="mfa-pill"></span>
        <span className="mfa-arrow"><Icon name="arrow-right" size={14} /></span>
        <span className="mfa-pill"></span>
      </span>);
  }
  return (
    <React.Fragment>
      <MoodPill mood={humeurDebut(call)} />
      <span className="call-arrow"><Icon name="arrow-right" size={14} /></span>
      <MoodPill mood={humeurFin(call)} />
    </React.Fragment>);
}
window.MoodFlow = MoodFlow;
window.humeurEnAttente = humeurEnAttente;

// jours : un nombre de jours, `null` pour tout l'historique, `0` pour rien
// (utilisé quand le graphique agrandi est fermé — on n'analyse pas l'invisible).
const appelsDerniersJours = (r, jours) => {
  if (jours === 0) return [];
  const tous = (r.calls || []).filter((c) => c.date instanceof Date && !isNaN(c.date));
  if (jours == null) return tous;
  const limite = new Date();
  limite.setHours(0, 0, 0, 0);
  limite.setDate(limite.getDate() - (jours - 1));
  return tous.filter((c) => c.date >= limite);
};

const PROMPT_HUMEUR = `Tu es l'agent clinique Livana. Pour CHAQUE appel ci-dessous, détermine DEUX humeurs du résident :
- "debut" : son état au DÉBUT de l'échange (ses premières répliques, avant que Livana n'agisse)
- "fin" : son état à la FIN de l'échange (ses dernières répliques)

Choisis STRICTEMENT une valeur parmi :
- "agitated" : agitation, colère, opposition, refus marqué
- "anxious" : inquiétude, angoisse, confusion anxiogène, désorientation pénible
- "sad" : tristesse, mélancolie, solitude, évocation douloureuse
- "calm" : apaisé, serein, échange neutre et posé
- "happy" : joie, enthousiasme, rires, plaisir manifeste

Règles :
- Juge l'état du RÉSIDENT, jamais celui de Livana.
- "debut" et "fin" peuvent être identiques si l'état n'a pas changé : ne force pas une évolution.
- Si l'extrait de fin est absent, la conversation est courte : juge la fin sur ses dernières répliques disponibles.
- En cas de doute ou d'extrait peu parlant, réponds "calm". N'invente rien.

Réponds UNIQUEMENT par un tableau JSON, sans texte autour :
[{"id":"<id de l'appel>","debut":"<valeur>","fin":"<valeur>"}]`;

// Analyse un lot d'appels et range les humeurs obtenues dans le cache
async function analyserLot(appels) {
  const liste = appels.map((c) => {
    const x = extraitsAppel(c);
    const bloc = [`[${c.id}] ${c.when}`, `Résumé : ${c.summary}`];
    if (x.debut) bloc.push(x.entier ? `Conversation :\n${x.debut}` : `Début de conversation :\n${x.debut}`);
    if (x.fin) bloc.push(`Fin de conversation :\n${x.fin}`);
    return bloc.join('\n');
  }).join('\n\n───\n\n');

  const out = await window.LivanaAPI.aiComplete({
    messages: [{ role: 'user', content: `${PROMPT_HUMEUR}\n\nAPPELS :\n${liste}` }],
    maxTokens: 3000,
  });
  const m = out.match(/\[[\s\S]*\]/);
  JSON.parse(m ? m[0] : out).forEach((o) => {
    const debut = MOODS_VALIDES.indexOf(o.debut) !== -1 ? o.debut : null;
    const fin = MOODS_VALIDES.indexOf(o.fin) !== -1 ? o.fin : null;
    if (debut || fin) CACHE_HUMEUR_APPEL[o.id] = { debut: debut || fin, fin: fin || debut };
  });
  sauverCacheHumeur();
}

// Appels actuellement envoyés à l'IA : évite que deux zones de l'écran (le
// graphique et la liste, par exemple) demandent le même appel simultanément.
const ENVOI_EN_COURS = new Set();
let abonnesHumeur = [];
const prevenirHumeur = () => abonnesHumeur.forEach((f) => { try { f(); } catch (e) {} });

// Analyse les appels FOURNIS, et eux seuls : rien n'est envoyé à l'IA pour un
// appel qui n'est pas affiché à l'écran. Les appels déjà connus (en cache) ou
// déjà en cours d'analyse ailleurs sont ignorés.
function useAnalyseHumeurs(appels) {
  const [, setVersion] = React.useState(0);
  // Se tenir informé des analyses lancées par les autres zones de l'écran
  React.useEffect(() => {
    const f = () => setVersion((v) => v + 1);
    abonnesHumeur.push(f);
    return () => { abonnesHumeur = abonnesHumeur.filter((x) => x !== f); };
  }, []);

  const aTraiter = (appels || []).filter((c) => !CACHE_HUMEUR_APPEL[c.id] && c.summary);
  const cle = aTraiter.map((c) => c.id).join(',');

  React.useEffect(() => {
    // Filtrage au moment de l'envoi (et non au rendu) : une autre zone de
    // l'écran a pu réserver ces appels entre-temps.
    const manquants = aTraiter.filter((c) => !CACHE_HUMEUR_APPEL[c.id] && !ENVOI_EN_COURS.has(c.id));
    if (!manquants.length) return;
    manquants.forEach((c) => ENVOI_EN_COURS.add(c.id));
    (async () => {
      try {
        for (let i = 0; i < manquants.length; i += LOT_ANALYSE) {
          const lot = manquants.slice(i, i + LOT_ANALYSE);
          try { await analyserLot(lot); }
          finally { lot.forEach((c) => ENVOI_EN_COURS.delete(c.id)); }
          prevenirHumeur(); // affichage progressif, lot par lot
        }
      } catch (e) {
        // Échec : on marque ces appels pour retomber sur les humeurs brutes
        console.warn('Analyse d\'humeur indisponible :', e && e.message);
        manquants.forEach((c) => { ENVOI_EN_COURS.delete(c.id); ECHECS_HUMEUR.add(c.id); });
        prevenirHumeur();
      }
    })();
  }, [cle]);

  // « en cours » = certains appels demandés ne sont pas encore connus
  return aTraiter.length > 0;
}

// Moyenne d'une liste d'humeurs, ramenée à l'humeur la plus proche
function moyenneHumeurs(humeurs) {
  const moyenne = humeurs.reduce((s, m) => s + window.moodOf(m).score, 0) / humeurs.length;
  let proche = null, ecart = Infinity;
  Object.keys(window.MOODS).forEach((k) => {
    const d = Math.abs(window.MOODS[k].score - moyenne);
    if (d < ecart) { ecart = d; proche = k; }
  });
  return { mood: proche, score: moyenne };
}

const MOIS_COURTS = ['janv', 'févr', 'mars', 'avr', 'mai', 'juin', 'juil', 'août', 'sept', 'oct', 'nov', 'déc'];
const jourCourt = (d) => d.getDate() + ' ' + MOIS_COURTS[d.getMonth()];

// jours = 7, 14, 30… ou null pour tout l'historique
function useMoodAnalysis(r, jours) {
  const concernes = React.useMemo(
    () => appelsDerniersJours(r, jours), [r.id, r.calls.length, jours]);

  // Construit la courbe : UN POINT PAR JOUR, valeur = moyenne des humeurs de
  // fin d'appel de la journée (l'état dans lequel Livana laisse le résident).
  // Les jours sans appel restent présents mais vides : l'axe horizontal
  // représente vraiment le temps, et une absence de contact se voit.
  const construire = React.useCallback(() => {
    if (!concernes.length) return [];

    // Humeurs regroupées par jour (repli sur l'humeur brute tant que
    // l'analyse IA n'est pas revenue)
    const parJour = {};
    concernes.forEach((c) => {
      const cle = c.date.toDateString();
      (parJour[cle] || (parJour[cle] = [])).push(humeurFin(c));
    });

    // Seuls les jours ayant au moins un appel apparaissent : un jour vide
    // laisserait un trou dans la courbe. Les dates restent chronologiques.
    return Object.keys(parJour)
      .map((cle) => ({ jour: new Date(cle), humeurs: parJour[cle] }))
      .sort((a, b) => a.jour - b.jour)
      .map(({ jour, humeurs }) => {
        const moyenne = moyenneHumeurs(humeurs);
        return {
          date: jourCourt(jour),
          mood: moyenne.mood,
          score: moyenne.score,
          appels: humeurs.length,
        };
      });
  }, [concernes, jours]);

  // Les envois passent par le mécanisme partagé (dédoublonné, lot par lot)
  const enCours = useAnalyseHumeurs(concernes);
  const timeline = construire();

  return {
    timeline: timeline,        // un élément par JOUR (mood null = sans appel)
    nbAppels: concernes.length, // nombre d'appels réels sur la période
    analyse: enCours && !timeline.length,
    enCours: enCours,
    aucun: concernes.length === 0,
  };
}

// Exposition inter-features (convention option B : tout ce qui traverse
// un fichier passe par window)
Object.assign(window, { tauxApaisement, appels7jDe, useAnalyseHumeurs, useMoodAnalysis, appelsDerniersJours });
