"""MBIOWsMonitorLLMMixin — Routes et UI pour l'assistant LLM.

Mixin pur : s'exécute toujours sur une instance MBIOWsMonitor.
Méthodes utilisées depuis wsmonitor.py via self.*
"""

import json
import queue
import time

from .wsmonitor_html import HtmlPage


class MBIOWsMonitorLLMMixin:

    # ------------------------------------------------------------------
    # Helpers internes
    # ------------------------------------------------------------------

    def _getLlmTask(self):
        """Retourne la première instance MBIOTaskLLM, ou None."""
        try:
            from .llmassistant import MBIOTaskLLM
        except ImportError:
            return None
        mbio = self.getMBIO()
        if not mbio:
            return None
        for t in mbio.tasks.all():
            if isinstance(t, MBIOTaskLLM):
                return t
        return None

    def _llmNavHtml(self):
        """Retourne le bouton nav vers /assistant, ou chaîne vide si pas de tâche LLM."""
        if self._getLlmTask() is not None:
            return '<span class="ms-2"><a href="/assistant" class="btn btn-sm btn-primary">IA</a></span>'
        return ''

    # ------------------------------------------------------------------
    # Routes API
    # ------------------------------------------------------------------

    def cb_llm_chat(self, handler, params, data=None):
        """POST /api/v1/llm/chat — Envoie un message à l'assistant.
        Body JSON : {session_id, message}
        Réponse   : {ok: true}
        """
        data = data or {}
        session_id = str(data.get('session_id', '')).strip()
        message = str(data.get('message', '')).strip()
        images = data.get('images') or []  # [{media_type, data}]

        if not session_id or not message:
            self.cb_write_json(handler, {'error': 'session_id et message requis'}, 400)
            return True

        task = self._getLlmTask()
        if task is None:
            self.cb_write_json(handler, {'error': 'Assistant IA non configuré'}, 503)
            return True

        task.post_message(session_id, message, images=images if images else None)
        self.cb_write_json(handler, {'ok': True})
        return True

    def cb_llm_stream(self, handler, params):
        """GET /api/v1/llm/stream?session_id=SID — SSE stream de réponse.
        Pattern identique à wserver._sse_logstream.
        """
        session_id = params.get('session_id', '')
        task = self._getLlmTask()
        if task is None or not session_id:
            handler.send_response(503)
            handler.end_headers()
            return True

        session = task.get_or_create_session(session_id)

        handler.send_response(200)
        handler.send_header('Content-Type', 'text/event-stream')
        handler.send_header('Cache-Control', 'no-cache')
        handler.send_header('X-Accel-Buffering', 'no')
        handler.end_headers()

        _SSE_HEARTBEAT_INTERVAL = 10   # secondes entre deux pings
        _SSE_GLOBAL_TIMEOUT     = 120  # abandon si aucun 'done' après N secondes
        t_start = time.time()
        try:
            while True:
                if time.time() - t_start > _SSE_GLOBAL_TIMEOUT:
                    # Timeout serveur : forcer la fermeture propre
                    timeout_event = json.dumps({'type': 'error', 'message': 'Timeout serveur : aucune réponse après %ds.' % _SSE_GLOBAL_TIMEOUT})
                    handler.wfile.write(('data: %s\n\n' % timeout_event).encode('utf-8'))
                    handler.wfile.flush()
                    break
                try:
                    event = session.output_queue.get(timeout=_SSE_HEARTBEAT_INTERVAL)
                    data = 'data: %s\n\n' % json.dumps(event)
                    handler.wfile.write(data.encode('utf-8'))
                    handler.wfile.flush()
                    if event.get('type') in ('done', 'error'):
                        break
                except queue.Empty:
                    # Ping comme vrai événement SSE → déclenche onmessage côté client
                    ping = json.dumps({'type': 'ping'})
                    handler.wfile.write(('data: %s\n\n' % ping).encode('utf-8'))
                    handler.wfile.flush()
        except (BrokenPipeError, ConnectionResetError, OSError):
            pass
        return True

    def cb_llm_confirm(self, handler, params, data=None):
        """POST /api/v1/llm/confirm — Confirme ou annule une écriture en attente.
        Body JSON : {session_id, action_id, confirmed (bool)}
        """
        data = data or {}
        session_id = str(data.get('session_id', '')).strip()
        action_id = str(data.get('action_id', '')).strip()
        confirmed = bool(data.get('confirmed', False))

        task = self._getLlmTask()
        if task is None:
            self.cb_write_json(handler, {'error': 'Assistant IA non configuré'}, 503)
            return True

        result = task.execute_confirm(session_id, action_id, confirmed)
        self.cb_write_json(handler, result)
        return True

    def cb_llm_tools(self, handler, params):
        """GET /api/v1/llm/tools — Liste des tools disponibles."""
        task = self._getLlmTask()
        if task is None:
            self.cb_write_json(handler, {'data': []})
            return True
        self.cb_write_json(handler, {'data': task.get_tools_info()})
        return True

    def cb_llm_history(self, handler, params):
        """GET /api/v1/llm/history?session_id=SID — Historique de la session."""
        session_id = params.get('session_id', '')
        task = self._getLlmTask()
        if task is None or not session_id:
            self.cb_write_json(handler, {'messages': [], 'audit': []})
            return True
        session = task.get_or_create_session(session_id)
        # Filtre les messages pour l'UI : user (text+images) + assistant (text)
        messages = []
        for m in session.history:
            role = m.get('role', '')
            content = m.get('content', '')
            if isinstance(content, str):
                messages.append({'role': role, 'content': content})
            elif isinstance(content, list):
                # Reconstruire un message UI avec texte et blocs image
                has_text_or_image = any(
                    b.get('type') in ('text', 'image') for b in content if isinstance(b, dict)
                )
                if has_text_or_image:
                    messages.append({'role': role, 'content': content})
        self.cb_write_json(handler, {
            'messages': messages,
            'audit': session.audit,
            'title': session.title,
        })
        return True

    def cb_llm_clear(self, handler, params, data=None):
        """POST /api/v1/llm/clear — Efface l'historique d'une session."""
        data = data or {}
        session_id = str(data.get('session_id', '')).strip()

        task = self._getLlmTask()
        if task is None or not session_id:
            self.cb_write_json(handler, {'ok': True})
            return True

        session = task.get_or_create_session(session_id)
        task._delete_session_downloads(session)
        session.history.clear()
        session.audit.clear()
        session.title = ''
        session.last_processed_len = 0
        task._save_session(session)
        self.cb_write_json(handler, {'ok': True})
        return True

    def cb_llm_abort(self, handler, params, data=None):
        """POST /api/v1/llm/abort — Interrompt la requête en cours pour une session."""
        data = data or {}
        session_id = str(data.get('session_id', '')).strip()
        task = self._getLlmTask()
        if task is None or not session_id:
            self.cb_write_json(handler, {'ok': True})
            return True
        with task._sessions_lock:
            session = task._sessions.get(session_id)
        if session is not None:
            session.abort_current = True
            session.stream_id += 1
        self.cb_write_json(handler, {'ok': True})
        return True

    def cb_llm_ping(self, handler, params):
        """GET /api/v1/llm/ping — Retourne le numéro de génération et le statut de la clé API.
        Permet au browser de détecter un reset et de recharger la page."""
        task = self._getLlmTask()
        generation = task.get_generation() if task else 0
        apikey_ok = task.has_valid_apikey() if task else False
        ratelimit = task.get_ratelimit() if task else {}
        self.cb_write_json(handler, {
            'generation': generation,
            'ok': task is not None,
            'apikey_ok': apikey_ok,
            'ratelimit': ratelimit,
        })
        return True

    def cb_llm_setapikey(self, handler, params, data=None):
        """POST /api/v1/llm/setapikey — Définit la clé API Anthropic.
        Body JSON : {apikey: str}
        La clé est persistée et la tâche est redémarrée.
        """
        data = data or {}
        apikey = str(data.get('apikey', '')).strip()
        task = self._getLlmTask()
        if task is None:
            self.cb_write_json(handler, {'error': 'Assistant IA non configuré'}, 503)
            return True
        task.set_apikey(apikey)
        self.cb_write_json(handler, {'ok': True})
        return True

    def cb_llm_files(self, handler, params):
        """GET /api/v1/llm/files?name=<stored_name> — Sert un fichier généré par l'assistant."""
        import os
        stored_name = (params.get('name', '') or '').strip()
        self.logger.debug('cb_llm_files: stored_name=%r params=%r path=%r' % (stored_name, params, getattr(handler, 'path', '?')))

        task = self._getLlmTask()
        if task is None:
            self.logger.warning('cb_llm_files: tâche LLM introuvable')
            handler.send_response(503)
            handler.end_headers()
            return True

        if not stored_name:
            self.logger.warning('cb_llm_files: paramètre name manquant')
            handler.send_response(400)
            handler.end_headers()
            return True

        fpath = task.get_download_file_path(stored_name)
        self.logger.debug('cb_llm_files: fpath=%r downloads_dir=%r' % (fpath, task._downloads_dir))
        if fpath is None:
            self.logger.warning('cb_llm_files: fichier introuvable: %s (downloads_dir=%s)' % (stored_name, task._downloads_dir))
            handler.send_response(404)
            handler.end_headers()
            return True

        # Déterminer le Content-Type selon l'extension
        ext = os.path.splitext(stored_name)[1].lower()
        content_types = {
            '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            '.csv':  'text/csv',
            '.pdf':  'application/pdf',
            '.json': 'application/json',
            '.txt':  'text/plain',
        }
        ct = content_types.get(ext, 'application/octet-stream')

        # Nom de fichier sans le préfixe UUID (8 chars + _)
        display_name = stored_name
        if len(stored_name) > 9 and stored_name[8] == '_':
            display_name = stored_name[9:]

        try:
            fsize = os.path.getsize(fpath)
            self.logger.info('cb_llm_files: envoi %s (%d bytes, ct=%s)' % (display_name, fsize, ct))
            with open(fpath, 'rb') as f:
                data_bytes = f.read()
            handler.send_response(200)
            handler.send_header('Content-Type', ct)
            handler.send_header('Content-Length', str(len(data_bytes)))
            handler.send_header('Content-Disposition',
                'attachment; filename="%s"' % display_name.replace('"', '_'))
            handler.end_headers()
            handler.wfile.write(data_bytes)
            self.logger.debug('cb_llm_files: envoi terminé')
        except (BrokenPipeError, ConnectionResetError, OSError) as e:
            self.logger.warning('cb_llm_files: erreur réseau: %s' % e)
        except Exception as e:
            self.logger.exception('cb_llm_files: erreur inattendue: %s' % e)
            try:
                handler.send_response(500)
                handler.end_headers()
            except Exception:
                pass
        return True

    # ------------------------------------------------------------------
    # Page HTML /assistant
    # ------------------------------------------------------------------

    def cb_assistant(self, handler, params):
        """GET /assistant — Page de l'assistant conversationnel IA."""
        task = self._getLlmTask()
        mbio = self.getMBIO()
        h = HtmlPage('Assistant IA — MBIO', handler)

        raw_model = task._model if task else 'N/A'
        # claude-haiku-4-5-20251001 → haiku
        m = raw_model.lower()
        model_name = m[7:].split('-')[0] if m.startswith('claude-') else m
        suggestions_json = json.dumps(task.suggestions if task else [])

        h.variable('mbio.version', mbio.version if mbio else '')
        h.variable('main.nav', self._navHtml('/assistant'))
        h.variable('bacnet.nav', self._bacnetNavHtml() + self._modbusNavHtml() + self._llmNavHtml() + self._logoutHtml())
        h.variable('llm.model', model_name)
        h.variable('llm.suggestions', suggestions_json)
        h.variable('llm.welcome', json.dumps(task._welcome_message if task else 'Bonjour, je suis votre assistant IA Digimat.'))

        h.header('<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>')
        h.header('<style>html, body { overflow: hidden; height: 100%; } .page.container-fluid { overflow: hidden; } .card.p-4 { padding: 1rem !important; } .card.p-lg-5 { padding: 1rem !important; }</style>')

        h.body(r"""
<div class="page container-fluid">
<div class="card p-4 p-lg-5">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-3">
  <div>
    <h1 class="h3 mb-1">Digimat MBIO Processor Monitor</h1>
    <p class="text-secondary mb-0"><b>Assistant IA</b></p>
  </div>
  <div class="text-muted small">{mbio.version} /
    {main.nav}{bacnet.nav}
  </div>
</div>

<ul class="nav nav-tabs mt-3 mb-0 d-flex align-items-center" role="tablist">
  <li class="nav-item" role="presentation">
    <button class="nav-link active" id="tab-chat-btn" data-bs-toggle="tab" data-bs-target="#tab-chat" type="button">💬 Chat</button>
  </li>
  <li class="nav-item" role="presentation">
    <button class="nav-link" id="tab-audit-btn" data-bs-toggle="tab" data-bs-target="#tab-audit" type="button">📋 Audit</button>
  </li>
  <li class="nav-item" role="presentation">
    <button class="nav-link" id="tab-tools-btn" data-bs-toggle="tab" data-bs-target="#tab-tools" type="button">🔧 Outils</button>
  </li>
  <li class="nav-item ms-auto pb-1 d-flex gap-2 align-items-center">
    <span id="llm-status" class="small">● Prêt</span>
    <span id="llm-usage" class="text-muted small"></span>
    <span id="llm-rl-battery" style="display:none;vertical-align:middle" title="Tokens consommés / limite par minute (fenêtre glissante Anthropic — se recharge automatiquement)">
      <span style="display:inline-flex;align-items:center;gap:0">
        <span style="display:inline-block;width:24px;height:10px;border:1.5px solid #adb5bd;border-radius:2px;padding:1px;box-sizing:border-box">
          <span id="llm-rl-fill" style="display:block;height:100%;border-radius:1px;width:100%;background:#198754;transition:width 0.5s,background-color 0.5s"></span>
        </span>
        <span style="display:inline-block;width:2px;height:5px;background:#adb5bd;border-radius:0 1px 1px 0;margin-left:-1px;flex-shrink:0"></span>
      </span>
    </span>
    <span id="llm-ratelimit" class="text-muted small" style="display:none" title="Tokens consommés / limite par minute (fenêtre glissante Anthropic — se recharge automatiquement)"></span>
    <span class="text-muted small" id="llm-model-badge">{llm.model}</span>
    <button class="btn btn-sm btn-warning d-none" id="btn-auth" title="Configure API key">🔑 Auth</button>
    <button class="btn btn-sm btn-outline-danger" id="btn-clear" title="Clear conversation">🗑 Clear</button>
  </li>
</ul>

<div class="tab-content border border-top-0 rounded-bottom p-0 mb-0" style="overflow:hidden;">

  <!-- TAB CHAT -->
  <div class="tab-pane fade show active p-3" id="tab-chat" style="height:100%;box-sizing:border-box;">
  <div id="chat-inner" style="display:flex;flex-direction:column;height:100%;">
    <div id="chat-box" style="flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:8px;padding-bottom:8px;">
      <div id="chat-spacer" style="flex:1;min-height:0;"></div>
    </div>

    <!-- Suggestions -->
    <div id="suggestions-bar" class="d-flex flex-wrap gap-2 my-2" style="flex-shrink:0;">
    </div>

    <!-- Zone de saisie style ChatGPT -->
    <div id="input-box" class="mt-2" style="flex-shrink:0;position:relative;">
      <div id="img-preview-bar" style="display:none;margin-bottom:6px;display:flex;flex-wrap:wrap;gap:6px;"></div>
      <textarea id="chat-input" rows="1"
        placeholder="Posez une question... (glissez ou collez une image)"
        style="max-height:200px;"></textarea>
      <div id="input-box-footer">
        <span class="text-muted small" style="font-size:0.75rem;opacity:0.6;">Ctrl+Entrée · ↑↓ historique</span>
        <button id="btn-history" title="Historique des prompts">⏱</button>
        <button id="btn-send" title="Envoyer (Ctrl+Entrée)">
          <span id="btn-send-icon-send">
            <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 24 24">
              <path d="M12 20V4m0 0l-6 6m6-6l6 6" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
            </svg>
          </span>
          <span id="btn-send-icon-stop" style="display:none">
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
              <rect x="4" y="4" width="16" height="16" rx="2" ry="2"/>
            </svg>
          </span>
        </button>
      </div>
    </div>
  </div><!-- chat-inner -->
  </div>

  <!-- TAB AUDIT -->
  <div class="tab-pane fade p-3" id="tab-audit" style="height:100%;box-sizing:border-box;overflow-y:auto;">
    <div id="audit-list"></div>
  </div>

  <!-- TAB OUTILS -->
  <div class="tab-pane fade p-3" id="tab-tools" style="height:100%;box-sizing:border-box;overflow-y:auto;">
    <div id="tools-list"></div>
  </div>

</div><!-- tab-content -->
</div><!-- card -->
</div><!-- page -->

<!-- Modal Auth API Key -->
<div class="modal fade" id="authModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header py-2">
  <h6 class="modal-title fw-semibold">🔑 API Key</h6>
  <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
  <p class="small text-muted mb-2">Enter your Anthropic API key (starts with <code>sk-ant-</code>).</p>
  <input type="password" id="auth-apikey-input" class="form-control form-control-sm" placeholder="sk-ant-...">
  <div id="auth-error" class="text-danger small mt-1 d-none"></div>
</div>
<div class="modal-footer py-2 gap-2">
  <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
  <button type="button" class="btn btn-sm btn-primary" id="btn-auth-confirm">Save</button>
</div>
</div>
</div>
</div>

<style>
.bubble-user, .bubble-assistant, .bubble-confirm, .bubble-error, .chart-container {
  flex-shrink: 0;  /* empêche la compression flex quand de nouveaux éléments sont ajoutés */
}
.bubble-user {
  align-self: flex-end;
  background: #0d6efd;
  color: white;
  border-radius: 16px 16px 4px 16px;
  padding: 8px 14px;
  max-width: 80%;
  margin-right: 8px;
  white-space: pre-wrap;
  word-break: break-word;
}
.bubble-assistant {
  align-self: flex-start;
  background: #f1f3f5;
  border-radius: 4px 16px 16px 16px;
  padding: 10px 14px;
  max-width: 85%;
  word-break: break-word;
}
.bubble-assistant p { margin-bottom: 0.4rem; }
.bubble-assistant p:last-child { margin-bottom: 0; }
.bubble-assistant h1 { font-size: 1.15rem; font-weight: 700; margin: 0.6rem 0 0.3rem; }
.bubble-assistant h2 { font-size: 1.05rem; font-weight: 700; margin: 0.5rem 0 0.25rem; }
.bubble-assistant h3 { font-size: 0.97rem; font-weight: 600; margin: 0.4rem 0 0.2rem; }
.bubble-assistant h4, .bubble-assistant h5, .bubble-assistant h6 { font-size: 0.92rem; font-weight: 600; margin: 0.3rem 0 0.15rem; }
.bubble-assistant table {
  font-size: 0.85rem;
  border-collapse: collapse;
  margin: 0.4rem 0;
  width: auto;
}
.bubble-assistant table th,
.bubble-assistant table td {
  border: 1px solid #dee2e6;
  padding: 4px 10px;
  text-align: left;
  white-space: nowrap;
}
.bubble-assistant table th {
  background: #e9ecef;
  font-weight: 600;
}
.bubble-assistant table tr:nth-child(even) td {
  background: #f8f9fa;
}
.bubble-assistant pre {
  background: #f1f3f5;
  border-radius: 6px;
  padding: 8px 12px;
  font-size: 0.82rem;
  overflow-x: auto;
}
.bubble-assistant code {
  background: #e9ecef;
  border-radius: 3px;
  padding: 1px 5px;
  font-size: 0.85em;
}
.bubble-assistant pre code {
  background: none;
  padding: 0;
}
.bubble-confirm {
  align-self: flex-start;
  background: #fff3cd;
  border: 1px solid #ffc107;
  border-radius: 8px;
  padding: 10px 14px;
  max-width: 85%;
}
.bubble-error {
  align-self: center;
  background: #f8d7da;
  border: 1px solid #f5c2c7;
  border-radius: 8px;
  padding: 8px 14px;
  color: #842029;
  max-width: 85%;
}
.chart-container {
  width: 100%;
  max-width: 680px;
  background: white;
  border: 1px solid #dee2e6;
  border-radius: 8px;
  padding: 12px;
  align-self: flex-start;
}
#llm-status           { color: #198754; }  /* vert — Prêt */
#llm-status.streaming { color: #fd7e14; }  /* orange — En cours */
#llm-status.warning   { color: #ffc107; }  /* jaune — Interrompu */
#llm-status.error     { color: #dc3545; }  /* rouge — Erreur / Auth */

/* Zone de saisie style ChatGPT */
#input-box {
  display: block;
  width: 100%;
  background: #fff;
  border: 1.5px solid #d1d5db;
  border-radius: 16px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.07);
  padding: 10px 14px 8px 14px;
  box-sizing: border-box;
  transition: border-color 0.15s, box-shadow 0.15s;
}
#input-box:focus-within {
  border-color: #0d6efd;
  box-shadow: 0 2px 12px rgba(13,110,253,0.13);
}
#chat-input {
  width: 100%;
  border: none;
  outline: none;
  resize: none;
  background: transparent;
  font-size: 0.95rem;
  line-height: 1.5;
  min-height: 28px;
  max-height: 200px;
  overflow-y: auto;
  padding: 0;
  display: block;
  box-shadow: none !important;
}
#input-box-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 6px;
}
#btn-send {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  background: #111;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: background 0.15s, transform 0.1s;
  padding: 0;
}
#btn-send:hover { background: #333; transform: scale(1.07); }
#btn-send.btn-danger { background: #dc3545; }
#btn-send.btn-danger:hover { background: #b02a37; }
#btn-send:disabled { background: #adb5bd; cursor: not-allowed; transform: none; }
#input-box.drag-over {
  border-color: #0d6efd;
  box-shadow: 0 2px 12px rgba(13,110,253,0.25);
  background: rgba(13,110,253,0.04);
}
#img-preview-bar { display: none; margin-bottom: 6px; flex-wrap: wrap; gap: 6px; }
.img-thumb-wrap {
  position: relative;
  display: inline-block;
}
.img-thumb-wrap img {
  height: 64px;
  border-radius: 6px;
  border: 1px solid #dee2e6;
  object-fit: cover;
  cursor: default;
}
.img-thumb-remove {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #dc3545;
  color: #fff;
  border: none;
  font-size: 10px;
  line-height: 18px;
  text-align: center;
  cursor: pointer;
  padding: 0;
}
.bubble-user-img { height: 80px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.3); margin-top: 4px; display: block; }
#history-dropdown {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0; right: 0;
  background: #fff;
  border: 1px solid #dee2e6;
  border-radius: 10px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.12);
  max-height: 260px;
  overflow-y: auto;
  z-index: 1000;
}
.history-item {
  padding: 7px 14px;
  font-size: 0.875rem;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  border-bottom: 1px solid #f0f0f0;
}
.history-item:last-child { border-bottom: none; }
.history-item:hover { background: #f0f4ff; }
#btn-history {
  background: none;
  border: none;
  padding: 0 2px;
  cursor: pointer;
  color: #adb5bd;
  font-size: 1rem;
  line-height: 1;
  transition: color 0.15s;
}
#btn-history:hover { color: #495057; }
</style>

<script>
const SUGGESTIONS = {llm.suggestions};
const WELCOME_MESSAGE = {llm.welcome};
var SESSION_ID = localStorage.getItem('mbio_llm_session') || generateUUID();
localStorage.setItem('mbio_llm_session', SESSION_ID);

var currentBubble = null;
var streamSource = null;
var pendingChartId = 0;
var streamTimeoutHandle = null;
var pendingImages = [];  // [{media_type, data, preview}]

// ---- Images ----
function addImageFile(file) {
  if (!file || !file.type.startsWith('image/')) return;
  var reader = new FileReader();
  reader.onload = function(e) {
    var dataUrl = e.target.result;
    var comma = dataUrl.indexOf(',');
    var meta = dataUrl.substring(5, comma);   // e.g. "image/jpeg;base64"
    var mediaType = meta.split(';')[0];
    var base64 = dataUrl.substring(comma + 1);
    pendingImages.push({ media_type: mediaType, data: base64, preview: dataUrl });
    renderImagePreview();
  };
  reader.readAsDataURL(file);
}

function removeImage(idx) {
  pendingImages.splice(idx, 1);
  renderImagePreview();
}

function renderImagePreview() {
  var bar = $('#img-preview-bar');
  bar.empty();
  if (!pendingImages.length) { bar.hide(); return; }
  pendingImages.forEach(function(img, i) {
    var wrap = $('<div class="img-thumb-wrap"></div>');
    var im = $('<img>').attr('src', img.preview).attr('alt', 'image ' + (i + 1));
    var btn = $('<button class="img-thumb-remove" title="Supprimer">✕</button>');
    btn.on('click', function() { removeImage(i); });
    wrap.append(im).append(btn);
    bar.append(wrap);
  });
  bar.css('display', 'flex');
}
// ---- Historique des prompts (navigation ↑/↓ + dropdown) ----
var promptHistory = [];      // prompts envoyés, du plus récent [0] au plus ancien
var historyIndex  = -1;      // -1 = pas en navigation
var historyDraft  = '';      // sauvegarde du texte en cours avant navigation

function addToPromptHistory(text) {
  if (!text) return;
  // dédoublonnage : retirer si déjà présent
  var idx = promptHistory.indexOf(text);
  if (idx !== -1) promptHistory.splice(idx, 1);
  promptHistory.unshift(text);
  if (promptHistory.length > 50) promptHistory.pop();
}

function historyUp() {
  var inp = document.getElementById('chat-input');
  if (historyIndex === -1) historyDraft = inp.value;
  if (historyIndex < promptHistory.length - 1) {
    historyIndex++;
    inp.value = promptHistory[historyIndex];
    inp.style.height = 'auto';
    inp.style.height = Math.min(inp.scrollHeight, 200) + 'px';
    inp.setSelectionRange(0, 0);
  }
}

function historyDown() {
  var inp = document.getElementById('chat-input');
  if (historyIndex <= 0) {
    historyIndex = -1;
    inp.value = historyDraft;
  } else {
    historyIndex--;
    inp.value = promptHistory[historyIndex];
  }
  inp.style.height = 'auto';
  inp.style.height = Math.min(inp.scrollHeight, 200) + 'px';
  inp.setSelectionRange(inp.value.length, inp.value.length);
}

function showHistoryDropdown() {
  $('#history-dropdown').remove();
  if (!promptHistory.length) return;
  var dd = $('<div id="history-dropdown"></div>');
  promptHistory.slice(0, 15).forEach(function(txt, i) {
    var item = $('<div class="history-item"></div>').text(txt.length > 80 ? txt.slice(0, 80) + '…' : txt);
    item.on('click', function() {
      $('#chat-input').val(txt).trigger('input').focus();
      historyIndex = i;
      $('#history-dropdown').remove();
    });
    dd.append(item);
  });
  $('#input-box').append(dd);
  $(document).one('click', function(e) {
    if (!$(e.target).closest('#history-dropdown, #btn-history').length) $('#history-dropdown').remove();
  });
}

var STREAM_TIMEOUT_MS = 30000;  // 30s sans ping/événement → reset UI (serveur envoie un ping toutes les 10s)

function resetStreamTimeout() {
  if (streamTimeoutHandle) clearTimeout(streamTimeoutHandle);
  streamTimeoutHandle = setTimeout(function() {
    if (streamSource) {
      streamSource.close();
      streamSource = null;
    }
    renderError({ message: 'Timeout : aucune réponse reçue après 90 secondes.' });
  }, STREAM_TIMEOUT_MS);
}

function clearStreamTimeout() {
  if (streamTimeoutHandle) { clearTimeout(streamTimeoutHandle); streamTimeoutHandle = null; }
}

function generateUUID() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
    return v.toString(16);
  });
}

// ---- Rendu suggestions ----
function renderSuggestions() {
  var bar = $('#suggestions-bar');
  bar.empty();
  if (!SUGGESTIONS.length) return;
  SUGGESTIONS.forEach(function(s) {
    var btn = $('<button class="btn btn-sm btn-outline-secondary">' + s.label + '</button>');
    btn.on('click', function() {
      $('#chat-input').val(s.prompt);
      bar.hide();
      sendMessage();
    });
    bar.append(btn);
  });
}

// ---- Welcome ----
function showWelcome() {
  if (!WELCOME_MESSAGE) return;
  var div = $('<div class="bubble-assistant" style="opacity:0.8;font-style:italic;white-space:pre-wrap;"></div>').text(WELCOME_MESSAGE);
  $('#chat-box').append(div);
}

// ---- Bulles ----
function appendUserBubble(text, images) {
  var div = $('<div class="bubble-user"></div>');
  if (images && images.length) {
    images.forEach(function(img) {
      var src = img.preview || ('data:' + img.media_type + ';base64,' + img.data);
      div.append($('<img class="bubble-user-img">').attr('src', src));
    });
  }
  div.append($('<span></span>').text(text));
  $('#chat-box').append(div);
  scrollToBottom();
}

function createAssistantBubble() {
  var div = $('<div class="bubble-assistant"></div>');
  $('#chat-box').append(div);
  scrollToBottom();
  return div;
}

function appendToken(data) {
  if (!currentBubble) {
    currentBubble = createAssistantBubble();
    currentBubble.data('raw', '');
  }
  var raw = currentBubble.data('raw') + data.content;
  currentBubble.data('raw', raw);
  currentBubble.html(marked.parse(raw));
  scrollToBottom();
}

function renderToolStart(data) {
  // Outil en cours : indicateur discret dans la barre de statut uniquement
  setStatus('🔧 ' + data.name + '…', 'streaming');
  currentBubble = null;
}

function renderToolEnd(data) {
  // Remet le statut "En cours..." une fois l'outil terminé
  setStatus('En cours...', 'streaming');
}

function dispatchRender(data) {
  var rt = data.render_type;
  if (rt === 'chart') renderChart(data.data);
  else if (rt === 'chart_multi') renderChartMulti(data.data);
  else if (rt === 'datatable') renderDataTableInline(data.data);
  else if (rt === 'download') renderDownloadCard(data.data);
}

function renderDownloadCard(data) {
  if (!data || !data.url) return;
  var icon = '📄';
  var ext = (data.filename || '').split('.').pop().toLowerCase();
  if (ext === 'xlsx' || ext === 'xls') icon = '📊';
  else if (ext === 'csv') icon = '📋';
  else if (ext === 'pdf') icon = '📕';
  var desc = data.description ? '<div class="text-muted small mt-1">' + escHtml(data.description) + '</div>' : '';
  var card = $(
    '<div class="bubble-assistant d-inline-flex align-items-center gap-3 p-3" style="background:#f8f9fa;border:1px solid #dee2e6;">' +
    '<span style="font-size:1.8rem;">' + icon + '</span>' +
    '<div>' +
    '<div><a href="' + escHtml(data.url) + '" class="fw-semibold text-decoration-none">' +
    escHtml(data.filename || 'Télécharger') + '</a></div>' +
    desc +
    '</div>' +
    '<a href="' + escHtml(data.url) + '" class="btn btn-sm btn-outline-primary ms-2">⬇ Télécharger</a>' +
    '</div>'
  );
  $('#chat-box').append(card);
  currentBubble = null;
  scrollToBottom();
}

function renderChart(data) {
  if (!data || !data.data || !data.data.length) return;
  var cid = 'chart-' + (++pendingChartId);
  var container = $('<div class="chart-container">' +
    '<div class="text-muted small mb-1">' + escHtml(data.key || '') + ' — ' + escHtml(data.duration || '') +
    (data.unit ? ' (' + escHtml(data.unit) + ')' : '') + '</div>' +
    '<canvas id="' + cid + '" height="120"></canvas>' +
    '</div>');
  $('#chat-box').append(container);
  currentBubble = null;
  scrollToBottom();

  var pts = data.data;
  var labels = pts.map(function(p) { return new Date(p.ts * 1000).toLocaleTimeString(); });
  var values = pts.map(function(p) { return p.value; });
  new Chart(document.getElementById(cid), {
    type: 'line',
    data: {
      labels: labels,
      datasets: [{
        label: data.key || 'Valeur',
        data: values,
        borderColor: '#0d6efd',
        backgroundColor: 'rgba(13,110,253,0.08)',
        tension: 0.2,
        pointRadius: 2,
        fill: true,
      }]
    },
    options: { plugins: { legend: { display: false } }, scales: { x: { display: false } }, animation: false }
  });
}

var CHART_COLORS = ['#0d6efd','#d63384','#198754','#fd7e14','#6610f2','#20c997','#dc3545','#6c757d'];

function renderChartMulti(data) {
  if (!data || !data.series || !data.series.length) return;
  var cid = 'chart-' + (++pendingChartId);
  var title = data.series.map(function(s) { return s.key; }).join(', ') + ' — ' + escHtml(data.duration || '');
  var container = $('<div class="chart-container">' +
    '<div class="text-muted small mb-1">' + title + '</div>' +
    '<canvas id="' + cid + '" height="140"></canvas>' +
    '</div>');
  $('#chat-box').append(container);
  currentBubble = null;
  scrollToBottom();

  // Aligner sur les timestamps de la première série
  var labels = data.series[0].data.map(function(p) { return new Date(p.ts * 1000).toLocaleTimeString(); });
  var datasets = data.series.map(function(s, i) {
    return {
      label: s.key + (s.unit ? ' (' + s.unit + ')' : ''),
      data: s.data.map(function(p) { return p.value; }),
      borderColor: CHART_COLORS[i % CHART_COLORS.length],
      backgroundColor: 'transparent',
      tension: 0.2,
      pointRadius: 2,
      fill: false,
    };
  });
  new Chart(document.getElementById(cid), {
    type: 'line',
    data: { labels: labels, datasets: datasets },
    options: {
      plugins: { legend: { display: true, position: 'bottom', labels: { boxWidth: 12, font: { size: 11 } } } },
      scales: { x: { display: false } },
      animation: false,
    }
  });
}

function renderDataTableInline(data) {
  if (!data || !data.data || !data.data.length) return;
  var cols = Object.keys(data.data[0]);
  var thead = '<tr>' + cols.map(function(c) { return '<th>' + escHtml(c) + '</th>'; }).join('') + '</tr>';
  var tbody = data.data.map(function(row) {
    return '<tr>' + cols.map(function(c) {
      var v = row[c];
      if (typeof v === 'boolean') v = v ? '✓' : '✗';
      return '<td>' + escHtml(String(v === null || v === undefined ? '' : v)) + '</td>';
    }).join('') + '</tr>';
  }).join('');
  var tbl = $('<table class="table table-sm table-bordered table-hover w-100" style="font-size:0.85rem"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table>');
  var wrap = $('<div class="chart-container" style="overflow-x:auto;max-width:100%"></div>').append(tbl);
  $('#chat-box').append(wrap);
  currentBubble = null;
  scrollToBottom();
}

function renderConfirmCard(data) {
  var aid = data.action_id;
  var div = $('<div class="bubble-confirm" id="confirm-' + aid + '">' +
    '⚠ <b>Confirmation requise</b><br>' +
    'Écrire <b>' + escHtml(String(data.new_value)) + '</b>' +
    (data.unit ? ' ' + escHtml(data.unit) : '') +
    ' dans <code>' + escHtml(data.key) + '</code>' +
    ' (valeur actuelle : ' + escHtml(String(data.current_value)) + ')<br>' +
    '<div class="mt-2 d-flex gap-2">' +
    '<button class="btn btn-sm btn-success" onclick="sendConfirm(\'' + aid + '\',true)">✓ Confirmer</button>' +
    '<button class="btn btn-sm btn-secondary" onclick="sendConfirm(\'' + aid + '\',false)">✗ Annuler</button>' +
    '</div></div>');
  $('#chat-box').append(div);
  currentBubble = null;
  scrollToBottom();
}

function sendConfirm(actionId, confirmed) {
  $('#confirm-' + actionId + ' button').prop('disabled', true);
  $.ajax({
    url: '/api/v1/llm/confirm',
    method: 'POST',
    contentType: 'application/json',
    data: JSON.stringify({ session_id: SESSION_ID, action_id: actionId, confirmed: confirmed }),
    success: function(r) {
      var el = $('#confirm-' + actionId);
      if (confirmed) {
        el.append('<div class="text-success mt-1">✓ Exécuté</div>');
      } else {
        el.append('<div class="text-muted mt-1">Annulé</div>');
      }
    }
  });
}

function renderError(data) {
  clearStreamTimeout();
  var div = $('<div class="bubble-error">⚠ ' + escHtml(data.message || 'Erreur inconnue') + '</div>');
  $('#chat-box').append(div);
  currentBubble = null;
  scrollToBottom();
  setStatus('Erreur', 'error');
  setBtnSend();
  $('#chat-input').prop('disabled', false).focus();
}

function onStreamDone(data) {
  clearStreamTimeout();
  setStatus('Prêt');
  currentBubble = null;
  var usage = data.usage || {};
  if (usage.input_tokens || usage.output_tokens) {
    var inp = usage.input_tokens || 0;
    var out = usage.output_tokens || 0;
    $('#llm-usage').text('Tokens: ' + (inp + out) + ' (in:' + inp + ' out:' + out + ')');
  }
  if (data.ratelimit) updateRatelimit(data.ratelimit);
  setBtnSend();
  $('#chat-input').prop('disabled', false).focus();
}

// ---- SSE ----
var MESSAGE_RENDERERS = {
  'ping':         function() { /* réinitialise le timeout via onmessage, pas d'action UI */ },
  'token':        appendToken,
  'tool_start':   renderToolStart,
  'tool_end':     renderToolEnd,
  'render':       dispatchRender,
  'confirm':      renderConfirmCard,
  'confirm_result': function(d) { /* handled by sendConfirm callback */ },
  'done':         onStreamDone,
  'error':        renderError,
};

function startStream() {
  if (streamSource) {
    streamSource.close();
    streamSource = null;
  }
  var url = '/api/v1/llm/stream?session_id=' + encodeURIComponent(SESSION_ID);
  var es = new EventSource(url);
  streamSource = es;
  resetStreamTimeout();
  es.onmessage = function(e) {
    resetStreamTimeout();
    try {
      var data = JSON.parse(e.data);
      var fn = MESSAGE_RENDERERS[data.type];
      if (fn) fn(data);
    } catch(ex) { console.error('SSE parse error', ex); }
  };
  es.onerror = function() {
    // Déclenché à la fermeture du stream (après done/error) ou sur erreur réseau.
    // Vérifier que c'est bien NOTRE EventSource qui est concerné —
    // sinon un onerror tardif de l'ancien stream tuerait la nouvelle connexion.
    if (es !== streamSource) return;
    clearStreamTimeout();
    es.close();
    streamSource = null;
    // Remet l'UI en état si ce n'est pas déjà fait par done/error
    setBtnSend();
    $('#chat-input').prop('disabled', false);
    if ($('#llm-status').hasClass('streaming')) {
      setStatus('Prêt');
    }
  };
}

// ---- Envoi ----
function abortStream() {
  $.post('/api/v1/llm/abort', JSON.stringify({ session_id: SESSION_ID }),
    function() {}, 'json');
  if (streamSource) { streamSource.close(); streamSource = null; }
  clearStreamTimeout();
  setStatus('Interrompu', 'warning');
  setBtnSend();
  $('#chat-input').prop('disabled', false).focus();
  currentBubble = null;
}

function sendMessage() {
  // Si streaming en cours → abort
  if ($('#btn-send').hasClass('btn-danger')) {
    abortStream();
    return;
  }
  var msg = $('#chat-input').val().trim();
  if (!msg && !pendingImages.length) return;
  if (!msg) msg = '(image)';
  $('#suggestions-bar').hide();
  var inp = document.getElementById('chat-input');
  inp.value = '';
  inp.style.height = 'auto';
  setBtnStop();
  $('#chat-input').prop('disabled', true);
  currentBubble = null;
  historyIndex = -1;
  if (msg !== '(image)') addToPromptHistory(msg);
  var imgsToSend = pendingImages.slice();
  pendingImages = [];
  renderImagePreview();
  appendUserBubble(msg, imgsToSend);
  setStatus('En cours...', 'streaming');

  var postImages = imgsToSend.map(function(img) {
    return { media_type: img.media_type, data: img.data };
  });

  $.ajax({
    url: '/api/v1/llm/chat',
    method: 'POST',
    contentType: 'application/json',
    data: JSON.stringify({ session_id: SESSION_ID, message: msg, images: postImages }),
    success: function() {
      startStream();
    },
    error: function(xhr) {
      setStatus('Erreur', 'error');
      renderError({ message: 'Envoi échoué : ' + (xhr.responseText || xhr.status) });
      setBtnSend();
      $('#chat-input').prop('disabled', false);
    }
  });
}

// ---- Audit ----
function formatAuditParams(params) {
  if (!params || !Object.keys(params).length) return '';
  return Object.entries(params).map(function(kv) {
    return '<span class="badge bg-light text-dark border me-1">' +
      escHtml(kv[0]) + ' = ' + escHtml(String(kv[1])) + '</span>';
  }).join('');
}

function formatAuditResult(result, type) {
  if (!result) return '';
  var r = result;
  if (type === 'usage') {
    var utxt = '<span class="text-muted">in: <b>' + r.input_tokens + '</b> · out: <b>' + r.output_tokens + '</b> · total: <b>' + r.total + '</b> tokens';
    var rl = r.ratelimit;
    if (rl) {
      var rlRem = parseInt(rl.input_tokens_remaining !== undefined ? rl.input_tokens_remaining : rl.tokens_remaining, 10);
      var rlLim = parseInt(rl.input_tokens_limit || rl.tokens_limit || 0, 10);
      var rlRs  = parseInt(rl.input_tokens_reset_secs || rl.tokens_reset_secs || 0, 10);
      if (!isNaN(rlRem) && rlLim > 0) {
        var rlUsed = rlLim - rlRem;
        utxt += ' · RL: <b>' + fmtN(rlUsed) + '/' + fmtN(rlLim) + ' tkn/m</b>';
        if (rlRs > 0) utxt += ' ↺' + fmtResetSecs(rlRs);
      }
    }
    utxt += '</span>';
    return utxt;
  }
  if (r.error) return '<span class="text-danger">✗ ' + escHtml(r.error) + '</span>';
  if (r.status === 'done') {
    var parts = ['<span class="text-success">✓ ok</span>'];
    if (r.previous !== undefined && r.value !== undefined)
      parts.push('<span class="text-muted ms-2">' + escHtml(String(r.previous)) + ' → ' + escHtml(String(r.value)) + '</span>');
    return parts.join('');
  }
  if (r.status === 'skipped') return '<span class="text-muted">— ' + escHtml(r.message || 'skipped') + '</span>';
  if (r.count !== undefined) return '<span class="text-success">✓ ' + r.count + ' résultat(s)</span>';
  if (r.points !== undefined) return '<span class="text-success">✓ ' + r.points + ' points</span>';
  if (r.marked !== undefined) return '<span class="text-success">✓ marqué=' + r.marked + '</span>';
  // fallback : affichage compact
  var s = JSON.stringify(r);
  return '<span class="text-muted" style="font-size:0.8em;overflow-wrap:break-word;word-break:break-word;">' + escHtml(s.length > 120 ? s.substring(0, 120) + '…' : s) + '</span>';
}

function loadAudit() {
  $.getJSON('/api/v1/llm/history?session_id=' + encodeURIComponent(SESSION_ID), function(r) {
    var list = $('#audit-list').empty();
    var entries = r.audit || [];
    if (entries.length === 0) {
      list.append('<div class="text-center text-muted py-3">Aucune action enregistrée pour cette session.</div>');
      return;
    }
    var auditIdx = 0;
    entries.forEach(function(a) {
      var ts = new Date(a.ts * 1000).toLocaleTimeString();
      var isUsage = a.type === 'usage';
      var bgCls = isUsage ? 'bg-light' : '';
      var icon = a.is_write ? '✏️' : (isUsage ? '📊' : '🔧');
      var extraStyle = a.is_write ? 'border-left: 3px solid #fd7e14;' : '';
      var colId = 'audit-detail-' + (auditIdx++);

      // Détail complet JSON pour le volet dépliable
      var fullDetail = JSON.stringify({params: a.params, result: a.result}, null, 2);

      var card = $('<div class="card mb-2 ' + bgCls + '" style="' + extraStyle + 'min-width:0;overflow:hidden;"></div>');
      card.append(
        '<div class="card-body py-2 px-3" style="min-width:0;overflow:hidden;">' +
          // En-tête cliquable
          '<div class="d-flex align-items-center gap-2" style="min-width:0;cursor:pointer;" ' +
               'data-bs-toggle="collapse" data-bs-target="#' + colId + '" aria-expanded="false">' +
            '<span class="text-muted small flex-shrink-0">' + ts + '</span>' +
            '<span class="fw-semibold text-truncate flex-grow-1">' + icon + ' ' +
              escHtml(isUsage ? 'Tokens consommés' : (a.tool || a.type || '')) +
            '</span>' +
            // Résumé compact (toujours visible)
            '<span class="text-muted small flex-shrink-0 me-1">' + formatAuditResult(a.result, a.type) + '</span>' +
            '<span class="text-muted small flex-shrink-0" style="font-size:0.7rem;">▾</span>' +
          '</div>' +
          // Volet dépliable
          '<div class="collapse" id="' + colId + '">' +
            '<div class="mt-2 pt-2 border-top">' +
            (formatAuditParams(a.params) ? '<div class="mb-2">' + formatAuditParams(a.params) + '</div>' : '') +
            '<pre class="mb-0" style="font-size:0.78rem;max-height:300px;overflow:auto;background:#f8f9fa;border-radius:4px;padding:8px;">' +
              escHtml(fullDetail) +
            '</pre>' +
            '</div>' +
          '</div>' +
        '</div>'
      );
      list.append(card);
    });
  });
}

// ---- Outils ----
function loadTools() {
  $.getJSON('/api/v1/llm/tools', function(r) {
    var el = $('#tools-list').empty();
    (r.data || []).forEach(function(t) {
      var badge = t.confirm ? '<span class="badge bg-warning text-dark ms-1">confirm</span>' : '';
      var params = Object.entries(t.params || {}).map(function(kv) {
        return '<li><code>' + escHtml(kv[0]) + '</code> — ' + escHtml((kv[1].description || '')) + '</li>';
      }).join('');
      el.append('<div class="card mb-2 p-3">' +
        '<b>' + escHtml(t.name) + '</b>' + badge +
        '<p class="text-muted small mb-1 mt-1">' + escHtml(t.description) + '</p>' +
        (params ? '<ul class="mb-0" style="font-size:0.85rem">' + params + '</ul>' : '') +
        '</div>');
    });
  });
}

// ---- Utilitaires ----
function scrollToBottom() {
  var box = document.getElementById('chat-box');
  box.scrollTop = box.scrollHeight;
}

function setStatus(msg, cls) {
  var el = $('#llm-status');
  el.text('● ' + msg);
  el.removeClass('streaming warning error').addClass(cls || '');
}

function setBtnStop() {
  $('#btn-send').addClass('btn-danger').attr('title', 'Interrompre');
  $('#btn-send-icon-send').hide();
  $('#btn-send-icon-stop').show();
}

function setBtnSend() {
  $('#btn-send').removeClass('btn-danger').attr('title', 'Envoyer (Ctrl+Entrée)');
  $('#btn-send-icon-stop').hide();
  $('#btn-send-icon-send').show();
}

function escHtml(s) {
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

function fmtN(n) {
  n = parseInt(n, 10);
  if (isNaN(n)) return '?';
  if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
  if (n >= 1000)    return (n / 1000).toFixed(0) + 'k';
  return String(n);
}

// Stockage pour countdown périodique
var _rlResetAt = 0;    // epoch ms — quand la fenêtre /min se recharge
var _rlLim     = 0;    // limite (tokens ou input_tokens selon dispo)
var _rlRem     = 0;    // remaining au moment du dernier poll

function fmtResetSecs(secs) {
  if (secs <= 0) return null;
  return secs >= 3600 ? Math.ceil(secs / 3600) + 'h'
       : secs >= 60   ? Math.ceil(secs / 60)   + 'm'
                      : secs + 's';
}

function _renderRlBar(rem, lim, secs) {
  // Texte : consommés / limite, countdown
  var used = lim > 0 ? (lim - rem) : 0;
  var txt  = lim > 0 ? (fmtN(used) + '/' + fmtN(lim) + ' tkn/m') : (fmtN(rem) + ' tkn/m');
  var rs   = fmtResetSecs(secs);
  if (rs) txt += ' ↺' + rs;
  $('#llm-ratelimit').text(txt).show();
  // Batterie : vide = consommé, plein = disponible
  if (lim > 0) {
    var pct   = Math.max(0, Math.min(100, Math.round(rem / lim * 100)));
    var color = pct > 60 ? '#198754' : pct > 30 ? '#fd7e14' : '#dc3545';
    $('#llm-rl-fill').css({ width: pct + '%', backgroundColor: color });
    $('#llm-rl-battery').show();
  } else {
    $('#llm-rl-battery').hide();
  }
}

function updateRatelimit(rl) {
  if (!rl) {
    $('#llm-ratelimit').hide();
    $('#llm-rl-battery').hide();
    return;
  }
  // Choisir la limite la plus contraignante : input_tokens si dispo, sinon tokens
  var inRem  = parseInt(rl.input_tokens_remaining, 10);
  var inLim  = parseInt(rl.input_tokens_limit || 0, 10);
  var inSecs = parseInt(rl.input_tokens_reset_secs || 0, 10);
  var tokRem  = parseInt(rl.tokens_remaining, 10);
  var tokLim  = parseInt(rl.tokens_limit || 0, 10);
  var tokSecs = parseInt(rl.tokens_reset_secs || 0, 10);

  var rem, lim, secs;
  if (!isNaN(inRem) && inLim > 0) {
    rem = inRem; lim = inLim; secs = inSecs;
  } else if (!isNaN(tokRem) && tokLim > 0) {
    rem = tokRem; lim = tokLim; secs = tokSecs;
  } else {
    $('#llm-ratelimit').hide();
    $('#llm-rl-battery').hide();
    return;
  }

  _rlLim    = lim;
  _rlRem    = rem;
  _rlResetAt = secs > 0 ? Date.now() + secs * 1000 : 0;
  _renderRlBar(rem, lim, secs);
}

// Countdown secondes + recharge automatique à l'expiration du timer
setInterval(function() {
  if (!_rlLim) return;
  var now  = Date.now();
  var secs = _rlResetAt > 0 ? Math.max(0, Math.round((_rlResetAt - now) / 1000)) : 0;
  // Quand le timer expire : afficher recharge complète (rem = lim)
  var rem = (secs === 0 && _rlResetAt > 0) ? _rlLim : _rlRem;
  _renderRlBar(rem, _rlLim, secs);
}, 1000);

// ---- Hauteur dynamique ----
function resizeTabContent() {
  var tc = document.querySelector('.tab-content');
  if (!tc) return;
  var rect = tc.getBoundingClientRect();
  var available = Math.max(300, window.innerHeight - rect.top - 12);
  tc.style.height = available + 'px';
  // Ajuster chat-inner pour qu'il remplisse le tab-chat (soustraire suggestions + input + status)
  var inner = document.getElementById('chat-inner');
  if (inner) inner.style.height = '100%';
}
$(window).on('resize', resizeTabContent);
$('button[data-bs-toggle="tab"]').on('shown.bs.tab', resizeTabContent);

// ---- Init ----
$(function() {
  renderSuggestions();
  loadTools();
  resizeTabContent();

  // Auto-resize textarea
  $('#chat-input').on('input', function() {
    this.style.height = 'auto';
    this.style.height = Math.min(this.scrollHeight, 200) + 'px';
    resizeTabContent();
  });

  // Restore historique depuis serveur
  $.getJSON('/api/v1/llm/history?session_id=' + encodeURIComponent(SESSION_ID), function(r) {
    var msgs = r.messages || [];
    if (msgs.length === 0) {
      showWelcome();
    } else {
      msgs.forEach(function(m) {
        if (m.role === 'user') {
          var text = '', imgs = [];
          if (Array.isArray(m.content)) {
            m.content.forEach(function(b) {
              if (b.type === 'text') text = b.text;
              else if (b.type === 'image') imgs.push({ media_type: b.source.media_type, data: b.source.data });
            });
          } else {
            text = m.content || '';
          }
          if (text && text !== '(image)') addToPromptHistory(text);
          appendUserBubble(text, imgs);
          $('#suggestions-bar').hide();
        } else if (m.role === 'assistant') {
          var b = createAssistantBubble();
          b.html(marked.parse(m.content || ''));
        }
      });
    }
  });

  // Drag & drop images sur #input-box
  var dragCounter = 0;
  $('#input-box').on('dragenter', function(e) {
    e.preventDefault();
    dragCounter++;
    $(this).addClass('drag-over');
  }).on('dragleave', function(e) {
    dragCounter--;
    if (dragCounter <= 0) { dragCounter = 0; $(this).removeClass('drag-over'); }
  }).on('dragover', function(e) {
    e.preventDefault();
  }).on('drop', function(e) {
    e.preventDefault();
    dragCounter = 0;
    $(this).removeClass('drag-over');
    var files = e.originalEvent.dataTransfer.files;
    for (var i = 0; i < files.length; i++) addImageFile(files[i]);
  });

  // Coller une image depuis le presse-papier
  $('#chat-input').on('paste', function(e) {
    var items = (e.originalEvent.clipboardData || {}).items || [];
    for (var i = 0; i < items.length; i++) {
      if (items[i].type.startsWith('image/')) {
        e.preventDefault();
        addImageFile(items[i].getAsFile());
      }
    }
  });

  // Bouton envoyer
  $('#btn-send').on('click', sendMessage);

  // Ctrl+Enter + navigation historique ↑/↓
  $('#chat-input').on('keydown', function(e) {
    if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); sendMessage(); return; }
    if (e.key === 'ArrowUp' && !e.ctrlKey && !e.shiftKey) {
      // Remonter l'historique uniquement si curseur en position 0 ou champ vide
      var inp = this;
      if (inp.selectionStart === 0 && inp.selectionEnd === 0 || inp.value === '') {
        e.preventDefault();
        historyUp();
      }
    } else if (e.key === 'ArrowDown' && historyIndex !== -1) {
      e.preventDefault();
      historyDown();
    } else if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') {
      // Toute autre frappe sort du mode navigation
      historyIndex = -1;
    }
  });

  // Bouton historique → dropdown
  $('#btn-history').on('click', function(e) {
    e.stopPropagation();
    if ($('#history-dropdown').length) { $('#history-dropdown').remove(); return; }
    showHistoryDropdown();
  });

  // Auth state management
  var authModal = new bootstrap.Modal(document.getElementById('authModal'));

  function updateAuthState(apikey_ok) {
    if (!apikey_ok) {
      $('#btn-auth').removeClass('d-none');
      $('#chat-input').prop('disabled', true).attr('placeholder', 'API key required — click Auth to configure.');
      if (!$('#llm-status').hasClass('streaming')) {
        setStatus('API key required', 'error');
      }
    } else {
      $('#btn-auth').addClass('d-none');
      $('#chat-input').prop('disabled', false).attr('placeholder', 'Posez une question... (glissez ou collez une image)');
      if ($('#llm-status').hasClass('error')) {
        setStatus('Prêt');
      }
    }
  }

  $('#btn-auth').on('click', function() {
    $('#auth-apikey-input').val('');
    $('#auth-error').addClass('d-none').text('');
    $('#btn-auth-confirm').prop('disabled', false).text('Save');
    authModal.show();
    setTimeout(function() { $('#auth-apikey-input').focus(); }, 300);
  });

  $('#auth-apikey-input').on('keydown', function(e) {
    if (e.key === 'Enter') { e.preventDefault(); $('#btn-auth-confirm').trigger('click'); }
  });

  $('#btn-auth-confirm').on('click', function() {
    var key = $('#auth-apikey-input').val().trim();
    if (!key) { $('#auth-error').text('Please enter a key.').removeClass('d-none'); return; }
    $('#btn-auth-confirm').prop('disabled', true).text('Saving…');
    $('#auth-error').addClass('d-none');
    $.ajax({
      url: '/api/v1/llm/setapikey',
      method: 'POST',
      contentType: 'application/json',
      data: JSON.stringify({ apikey: key }),
      success: function() { authModal.hide(); },
      error: function(xhr) {
        $('#btn-auth-confirm').prop('disabled', false).text('Save');
        $('#auth-error').text('Error: ' + (xhr.responseText || xhr.status)).removeClass('d-none');
      }
    });
  });

  // Clear
  $('#btn-clear').on('click', function() {
    if (!confirm('Clear the entire conversation?')) return;
    $.ajax({
      url: '/api/v1/llm/clear',
      method: 'POST',
      contentType: 'application/json',
      data: JSON.stringify({ session_id: SESSION_ID }),
      success: function() {
        $('#chat-box').empty().prepend('<div id="chat-spacer" style="flex:1;min-height:0;"></div>');
        $('#llm-usage').text('');
        pendingImages = [];
        renderImagePreview();
        showWelcome();
        renderSuggestions();
        $('#suggestions-bar').show();
      }
    });
  });

  // Tabs
  $('#tab-audit-btn').on('shown.bs.tab', loadAudit);
  $('#tab-tools-btn').on('shown.bs.tab', loadTools);

  // Polling génération — rechargement automatique si la tâche LLM est resetée
  // + mise à jour de l'état auth (bouton Auth, readonly textarea)
  var LLM_GENERATION = null;
  function pollGeneration() {
    $.getJSON('/api/v1/llm/ping', function(r) {
      if (LLM_GENERATION === null) {
        LLM_GENERATION = r.generation;
      } else if (r.generation !== LLM_GENERATION) {
        window.location.reload();
      }
      updateAuthState(r.apikey_ok !== false);
      updateRatelimit(r.ratelimit || {});
    }).always(function() {
      setTimeout(pollGeneration, 5000);
    });
  }
  setTimeout(pollGeneration, 1000);  // premier ping rapide pour état initial auth
});
</script>
""")
        h.flush()
        return True
