// Tempo — Connecter un courtier et importer un fichier (section de Réglages)
// ---------------------------------------------------------------------------
// Composant 100% FRONTEND. Aucune dépendance, aucun secret côté client.
//
// Architecture de sécurité (cf. SECURITY.md) :
//  - Le frontend NE détient AUCUN secret : ni service_role Supabase, ni token
//    MetaApi. Le mot de passe du courtier n'est JAMAIS conservé après l'envoi :
//    il transite (HTTPS) vers le Worker → MetaApi pour ouvrir l'accès en
//    LECTURE SEULE, puis on ne garde que l'accountId MetaApi côté Worker.
//  - Le mot de passe demandé est le MOT DE PASSE INVESTISSEUR (lecture seule) :
//    il ne permet pas de passer d'ordres. On le dit explicitement dans l'UI.
//  - Chaque appel au Worker porte le JWT Supabase de l'utilisateur
//    (Authorization: Bearer <access_token>). Le Worker le vérifie auprès de
//    /auth/v1/user AVANT toute action et n'agit que sur ce user_id.
//  - Le Worker écrit dans Supabase via service_role (secret Worker uniquement)
//    et n'appelle QUE des endpoints MetaApi de LECTURE (historique des deals).
//
// Endpoints du Worker (cloudflare-broker-sync.js, tous JSON, tous protégés par le JWT) :
//   GET  {WORKER}/status      → { connections: [ { id, label, login, server,
//                                  status: 'ok'|'syncing'|'error'|'pending'|'disconnected',
//                                  last_sync_at, last_error } ] }
//   POST {WORKER}/connect     body { login, password, server, label }
//                              → { connection, firstSync: { ok, imported, error } }
//   POST {WORKER}/sync        body { connectionId }
//                              → { results: [ { connectionId, ok, imported, error } ] }
//   POST {WORKER}/disconnect  body { connectionId }  → { metaapiDeleted }
//
// Textes : aucun message brut du Worker ou du prestataire n'est affiché (il
// peut citer MetaApi, une URL ou un code HTTP) ; il part dans
// console.warn('[Tempo] courtier : …') et l'écran montre une phrase française.
//
// Exposé : window.BrokerPanel (complet) + window.BrokerPanelCompact (réduit).

const BROKER_WORKER_DEFAULT = 'https://tempo-broker-sync.guillaume-briere92.workers.dev';

// Même phrase que le repli de sbErrorMessage (supabase.jsx) : un concept, un texte.
const BROKER_GENERIC_ERROR = 'Une erreur est survenue. Réessaie dans un instant.';
// Service de synchronisation injoignable ou en panne (appel sans réponse alors
// que le navigateur est en ligne, ou erreur 5xx) : ce n'est pas la connexion
// de l'utilisateur, on ne lui demande donc pas de la vérifier.
const BROKER_UNAVAILABLE = 'Le service de synchronisation est momentanément indisponible. Réessaie dans quelques minutes.';

// ─── Helpers réseau (JWT-authentifiés) ────────────────────────────────────
// Récupère le JWT de session Supabase. Sans session → on refuse l'appel.
async function _brokerAccessToken() {
  if (!window.sb) return null;
  try {
    const { data } = await window.sb.auth.getSession();
    return data?.session?.access_token || null;
  } catch (e) { return null; }
}

// Appel générique au Worker. Renvoie { data } ou { error: { message } } (FR).
// `worker` = base URL (sans slash final), `path` = '/status' etc.
async function _brokerCall(worker, path, { method = 'GET', body } = {}) {
  const token = await _brokerAccessToken();
  if (!token) return { error: { message: 'Tu dois être connecté pour gérer un courtier.' } };
  const base = String(worker || BROKER_WORKER_DEFAULT).replace(/\/+$/, '');
  let res;
  try {
    res = await fetch(base + path, {
      method,
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + token,
      },
      body: body ? JSON.stringify(body) : undefined,
    });
  } catch (e) {
    // Hors ligne : phrase commune de l'app (sbErrorMessage journalise le
    // message brut dans console.warn). En ligne, c'est le service qui ne
    // répond pas : on le dit, sans accuser la connexion de l'utilisateur.
    const offline = typeof navigator !== 'undefined' && navigator.onLine === false;
    const message = sbErrorMessage(e);
    return { error: { message: offline ? message : BROKER_UNAVAILABLE } };
  }
  // Essaie de lire un corps JSON ; tolère une réponse vide.
  let payload = null;
  try { payload = await res.json(); } catch (e) { payload = null; }
  if (!res.ok) {
    const raw = (payload && (payload.error || payload.message)) || '';
    return { error: { message: _brokerFrError(raw, res.status) } };
  }
  return { data: payload || {} };
}

// Erreurs connues (messages anglais du prestataire ou français du Worker)
// → phrase française destinée à l'utilisateur. '' si l'erreur est inconnue.
function _brokerKnownError(raw, status) {
  const m = String(raw || '').toLowerCase();
  if (status === 401 || status === 403 || /unauthor|forbidden|invalid token|jwt|session invalide|expirée/.test(m))
    return 'Ta session a expiré. Reconnecte-toi.';
  if (/num[ée]ro de compte invalide/.test(m))
    return 'Numéro de compte invalide : chiffres uniquement.';
  if (/nom de serveur invalide/.test(m))
    return 'Nom de serveur invalide : 64 caractères maximum, sans caractères spéciaux.';
  if (/mot de passe trop long/.test(m))
    return 'Mot de passe trop long : 128 caractères maximum.';
  if (/champs requis manquants/.test(m))
    return 'Renseigne le numéro de compte, le mot de passe investisseur et le serveur.';
  if (/identifiants.*refus|invalid (account|login|credential)|auth.*fail|wrong password|password/.test(m))
    return 'Identifiants refusés par le courtier. Vérifie le numéro de compte, le mot de passe investisseur et le serveur.';
  if (/server.*not.*found|unknown server|invalid server|resolve/.test(m))
    return 'Serveur du courtier introuvable. Copie le nom exact depuis MetaTrader 5 (menu Outils → Options → Serveur).';
  if (/already.*(connect|exist)|duplicate|déjà connecté/.test(m))
    return 'Ce compte est déjà connecté.';
  if (/connexion introuvable/.test(m))
    return 'Ce compte n’est plus connecté à Tempo. Actualise la page.';
  if (/timeout|timed out|deadline/.test(m))
    return 'Le courtier met trop de temps à répondre. Réessaie dans un instant.';
  if (status === 429 || /rate.?limit|too many/.test(m))
    return 'Trop de tentatives — réessaie dans quelques minutes.';
  return '';
}

// Réponse en erreur du Worker → message FR. Le brut part dans la console.
function _brokerFrError(raw, status) {
  console.warn('[Tempo] courtier :', status, raw || '(réponse sans message)');
  const known = _brokerKnownError(raw, status);
  if (known) return known;
  if (status >= 500) return BROKER_UNAVAILABLE;
  return BROKER_GENERIC_ERROR;
}

// Dernière erreur d'une connexion (last_error, écrite par le Worker : texte
// technique qui peut citer le prestataire ou une URL) → phrase française.
// Le brut n'est jamais affiché : la carte le journalise.
function _brokerConnErrorText(raw) {
  const known = _brokerKnownError(raw, 0);
  if (known) return known;
  if (/aucun compte metaapi|provisioning|non pris en charge/i.test(String(raw || '')))
    return 'La connexion à ce compte n’a pas abouti. Connecte-le à nouveau avec le formulaire ci-dessous.';
  return 'La dernière synchronisation a échoué. Clique sur « Synchroniser maintenant » pour réessayer.';
}

// Date de dernière synchronisation → libellé court, fuseau du profil (fmtDateTime).
function _brokerSyncLabel(iso) {
  const s = iso ? fmtDateTime(iso) : '—';
  return s === '—' ? 'Pas encore synchronisé' : 'Dernière synchronisation : ' + s;
}

// Nombre de trades importés → phrase avec le bon pluriel.
function _brokerImportedText(n) {
  return fmtNum(n, 0) + (n > 1 ? ' nouveaux trades importés.' : ' nouveau trade importé.');
}

// Confirmation après /connect, selon la première synchronisation du Worker.
function _brokerConnectedText(firstSync) {
  if (!firstSync) return 'Compte connecté.';
  if (firstSync.ok) {
    const n = Number(firstSync.imported) || 0;
    return n > 0
      ? 'Compte connecté : ' + fmtNum(n, 0) + (n > 1 ? ' trades importés.' : ' trade importé.')
      : 'Compte connecté. Aucun trade clôturé à importer pour l’instant.';
  }
  return 'Compte connecté. Tes trades arriveront dans quelques minutes.';
}

// Métadonnées d'affichage d'un statut de connexion.
function _brokerStatusMeta(status) {
  switch (String(status || '').toLowerCase()) {
    case 'ok':           return { label: 'Connecté',         tone: 'green' };
    case 'sync':
    case 'syncing':      return { label: 'Synchronisation…', tone: 'blue'  };
    case 'pending':      return { label: 'En attente…',      tone: 'blue'  };
    case 'error':        return { label: 'Erreur',           tone: 'red'   };
    case 'disconnected': return { label: 'Déconnecté',       tone: 'gray'  };
    default:             return { label: 'Inconnu',          tone: 'gray'  };
  }
}

// Petit point de statut coloré (pulsé quand la synchronisation est en cours).
function _BrokerDot({ tone }) {
  const color = tone === 'green' ? 'var(--green)'
    : tone === 'red' ? 'var(--red)'
    : tone === 'blue' ? 'var(--blue)'
    : 'var(--fg-4)';
  return (
    <span style={{
      width: 8, height: 8, borderRadius: 999, background: color, color,
      display: 'inline-block', flexShrink: 0,
      animation: tone === 'blue' ? 'pulse-dot 1.4s var(--ease) infinite' : 'none',
    }}/>
  );
}

// Pastille de statut (texte + point).
function _BrokerStatusPill({ status }) {
  const meta = _brokerStatusMeta(status);
  const bg = meta.tone === 'green' ? 'var(--green-soft)'
    : meta.tone === 'red' ? 'var(--red-soft)'
    : meta.tone === 'blue' ? 'var(--blue-soft)'
    : 'var(--bg-elev)';
  const fg = meta.tone === 'green' ? 'var(--green-text)'
    : meta.tone === 'red' ? 'var(--red-text)'
    : meta.tone === 'blue' ? 'var(--blue-600)'
    : 'var(--fg-2)';
  return (
    <span className="pill" style={{ background: bg, color: fg, border: '1px solid var(--line)', padding: '4px 10px', fontSize: 11.5, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0 }}>
      <_BrokerDot tone={meta.tone}/>
      {meta.label}
    </span>
  );
}

// ─── Carte d'une connexion existante ──────────────────────────────────────
// « Déconnecter » passe par une confirmation en ligne (modèle BtSessionRow du
// backtest) : question qui nomme le compte et la conséquence, [Déconnecter]
// rouge, [Annuler]. Une connexion déconnectée n'offre plus de synchronisation
// (le Worker l'ignore) ; seule une déconnexion inachevée chez le prestataire
// propose « Réessayer ».
// La réponse de /disconnect s'applique tout de suite à la carte (statut et
// déconnexion inachevée), même si le rechargement de /status échoue ensuite ;
// le prochain statut reçu du Worker reprend la main.
function _BrokerConnectionCard({ conn, worker, onChanged }) {
  const [busy, setBusy] = React.useState(null); // 'sync' | 'disconnect' | null
  const [err, setErr]   = React.useState('');
  const [note, setNote] = React.useState(null); // { tone: 'ok'|'neutral', text }
  const [confirming, setConfirming] = React.useState(false);
  const [localStatus, setLocalStatus] = React.useState(null);   // 'disconnected' après /disconnect
  const [localUnfinished, setLocalUnfinished] = React.useState(null); // metaapiDeleted === false
  const busyRef = React.useRef(false);          // garde anti double clic
  const cancelRef = React.useRef(null);
  const triggerRef = React.useRef(null);
  const focusBackRef = React.useRef(false);

  // Nouveau statut reçu du Worker → il fait foi.
  React.useEffect(() => { setLocalStatus(null); setLocalUnfinished(null); }, [conn.status, conn.last_error]);

  const status = localStatus || conn.status;
  const meta = _brokerStatusMeta(status);
  const disconnected = String(status || '').toLowerCase() === 'disconnected';
  // Déconnexion faite côté Tempo mais pas chez le prestataire (last_error posé par le Worker).
  const unfinished = disconnected && (localUnfinished != null ? localUnfinished : !!conn.last_error);

  // last_error n'est jamais affiché tel quel : on le journalise une fois par valeur.
  const serverDisconnected = String(conn.status || '').toLowerCase() === 'disconnected';
  React.useEffect(() => {
    if (!conn.last_error) return;
    if (serverDisconnected) console.warn('[Tempo] courtier : déconnexion non finalisée', conn.last_error);
    else console.warn('[Tempo] courtier : erreur de synchronisation', conn.last_error);
  }, [conn.last_error, serverDisconnected]);

  // Focus : sur « Annuler » quand la question apparaît, puis rendu au bouton
  // « Déconnecter » si l'utilisateur annule.
  React.useEffect(() => {
    if (confirming) { if (cancelRef.current) cancelRef.current.focus(); }
    else if (focusBackRef.current) {
      focusBackRef.current = false;
      if (triggerRef.current) triggerRef.current.focus();
    }
  }, [confirming]);

  const askDisconnect = () => { setErr(''); setNote(null); setConfirming(true); };
  const cancelDisconnect = () => {
    if (busyRef.current) return;
    focusBackRef.current = true;
    setErr('');
    setConfirming(false);
  };

  const doSync = async () => {
    if (busyRef.current) return;
    busyRef.current = true;
    setBusy('sync'); setErr(''); setNote(null);
    const r = await _brokerCall(worker, '/sync', { method: 'POST', body: { connectionId: conn.id } });
    busyRef.current = false;
    setBusy(null);
    if (r.error) { setErr(r.error.message); return; }
    // Un échec de la synchronisation est porté par le statut rechargé (pastille
    // « Erreur » + explication) ; un succès est confirmé ici.
    const results = r.data && Array.isArray(r.data.results) ? r.data.results : [];
    const mine = results.find(x => x && x.connectionId === conn.id);
    if (mine && mine.ok) {
      const n = Number(mine.imported) || 0;
      setNote({ tone: 'ok', text: 'Synchronisation terminée : ' + (n > 0 ? _brokerImportedText(n) : 'aucun nouveau trade.') });
    }
    onChanged && onChanged({ synced: true });
  };

  const doDisconnect = async () => {
    if (busyRef.current) return;
    busyRef.current = true;
    const wasDisconnected = disconnected; // relance d'une déconnexion inachevée
    setBusy('disconnect'); setErr(''); setNote(null);
    const r = await _brokerCall(worker, '/disconnect', { method: 'POST', body: { connectionId: conn.id } });
    busyRef.current = false;
    setBusy(null);
    if (r.error) { setErr(r.error.message); return; }
    setConfirming(false);
    const providerFailed = !!(r.data && r.data.metaapiDeleted === false);
    // Le Worker passe la connexion en 'disconnected' même si le prestataire
    // n'a pas suivi : la carte l'affiche sans attendre le rechargement.
    setLocalStatus('disconnected');
    setLocalUnfinished(providerFailed);
    if (wasDisconnected) {
      setNote(providerFailed
        ? { tone: 'neutral', text: 'La déconnexion chez notre prestataire de synchronisation n’a toujours pas abouti. Réessaie plus tard.' }
        : { tone: 'ok', text: 'Déconnexion finalisée.' });
    }
    onChanged && onChanged({ disconnected: true });
  };

  const login = conn.login || conn.account || '';
  const title = conn.label || (login ? 'Compte ' + login : 'Compte MetaTrader 5');
  const question = 'Déconnecter « ' + title + ' » ? La synchronisation automatique s’arrêtera ; '
    + 'tes trades déjà importés restent dans ton journal.';
  // Bouton désactivé : même rendu que .btn:disabled (charte §8).
  const offStyle = busy ? { opacity: 0.55, cursor: 'not-allowed' } : null;

  return (
    <div className="card" style={{ padding: '16px 18px', display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
        <div style={{ minWidth: 0 }}>
          <div title={title} style={{ fontSize: 14, fontWeight: 600, letterSpacing: '-.01em', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
            {title}
          </div>
          <div style={{ fontSize: 11.5, color: 'var(--fg-3)', marginTop: 3, overflowWrap: 'anywhere' }}>
            {login ? ('MetaTrader 5 · n° ' + login) : 'MetaTrader 5'}
            {conn.server ? ' · ' + conn.server : ''}
          </div>
        </div>
        <_BrokerStatusPill status={status}/>
      </div>

      <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11.5, color: 'var(--fg-3)' }}>
        <span>{_brokerSyncLabel(conn.last_sync_at)}</span>
      </div>

      {/* Statut « Erreur » : explication en français, même sans last_error. */}
      {meta.tone === 'red' && (
        <div style={{ fontSize: 11.5, color: 'var(--red-text)', background: 'var(--red-soft)', border: '1px solid var(--line)', borderRadius: 8, padding: '8px 10px', lineHeight: 1.45 }}>
          {_brokerConnErrorText(conn.last_error)}
        </div>
      )}

      {disconnected ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          <div style={{ fontSize: 11.5, color: 'var(--fg-3)', lineHeight: 1.45 }}>
            {conn.last_sync_at
              ? 'Synchronisation arrêtée. Tes trades déjà importés restent dans ton journal.'
              : 'Synchronisation arrêtée.'}
          </div>
          {unfinished && (
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 12, color: 'var(--fg-2)', lineHeight: 1.45 }}>
                La déconnexion chez notre prestataire de synchronisation n’est pas finalisée.
              </span>
              {/* La confirmation a déjà été donnée : on relance directement. */}
              <button type="button" onClick={doDisconnect} disabled={!!busy} className="btn tap"
                style={{ color: 'var(--fg)', ...offStyle }}>
                {Ico.refresh} {busy === 'disconnect' ? 'Déconnexion…' : 'Réessayer'}
              </button>
            </div>
          )}
        </div>
      ) : confirming ? (
        <div role="group" aria-label="Confirmer la déconnexion"
          onKeyDown={e => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); cancelDisconnect(); } }}
          style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <span style={{ flexBasis: '100%', fontSize: 12, color: 'var(--fg-2)', lineHeight: 1.45 }}>{question}</span>
          <button type="button" onClick={doDisconnect} disabled={!!busy} className="btn tap"
            style={{ background: 'var(--sell-bg, #dc2626)', borderColor: 'var(--sell-bg, #dc2626)', color: '#fff', ...offStyle }}>
            {busy === 'disconnect' ? 'Déconnexion…' : 'Déconnecter'}
          </button>
          <button type="button" ref={cancelRef} onClick={cancelDisconnect} disabled={!!busy} className="btn tap"
            style={{ color: 'var(--fg)', ...offStyle }}>
            Annuler
          </button>
        </div>
      ) : (
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <button type="button" onClick={doSync} disabled={!!busy} className="btn btn-outline-blue tap" style={{ ...offStyle }}>
            {Ico.sync} {busy === 'sync' ? 'Synchronisation…' : 'Synchroniser maintenant'}
          </button>
          <div style={{ flex: 1 }}/>
          <button type="button" ref={triggerRef} onClick={askDisconnect} disabled={!!busy} className="btn tap"
            style={{ color: 'var(--red-text)', borderColor: 'var(--line)', ...offStyle }}>
            Déconnecter
          </button>
        </div>
      )}

      {note && (
        <div role="status" style={{ fontSize: 12, lineHeight: 1.45, color: note.tone === 'ok' ? 'var(--green-text)' : 'var(--fg-2)' }}>
          {note.text}
        </div>
      )}
      {err && <div role="alert" style={{ fontSize: 12, lineHeight: 1.45, color: 'var(--red-text)' }}>{err}</div>}
    </div>
  );
}

// ─── Formulaire « Connecter MetaTrader 5 » ────────────────────────────────
// Vrai <form> : Entrée valide depuis n'importe quel champ ; garde anti double
// envoi par ref (le bouton reste désactivé pendant l'appel).
function _BrokerConnectForm({ worker, onConnected, onDirtyChange }) {
  const uid = React.useId();
  const [login, setLogin]   = React.useState('');
  const [pwd, setPwd]       = React.useState('');
  const [server, setServer] = React.useState('');
  const [label, setLabel]   = React.useState('');
  const [showPwd, setShowPwd] = React.useState(false);
  const [busy, setBusy]     = React.useState(false);
  const [err, setErr]       = React.useState('');
  const [ok, setOk]         = React.useState(''); // message de confirmation
  const busyRef  = React.useRef(false);
  const okTimer  = React.useRef(null);

  React.useEffect(() => () => clearTimeout(okTimer.current), []);

  // Saisie en cours (identifiants non envoyés) : la fenêtre « Ajouter des
  // trades » demande alors « Abandonner ta saisie ? » avant de se fermer.
  // Après une connexion réussie, les champs sont vidés, donc dirty = false.
  const dirty = !!(login + server + pwd + label).trim();
  React.useEffect(() => { if (typeof onDirtyChange === 'function') onDirtyChange(dirty); }, [dirty]);
  React.useEffect(() => () => { if (typeof onDirtyChange === 'function') onDirtyChange(false); }, []);

  // Libellés en casse de phrase, même gabarit que la saisie manuelle (IM_LABEL, import-modal.jsx).
  const labelStyle = { fontSize: 11.5, fontWeight: 600, color: 'var(--fg-2)', marginBottom: 5, display: 'block' };

  const valid = !!(login.trim() && pwd.trim() && server.trim());

  const submit = async () => {
    if (busyRef.current || !valid) return;
    busyRef.current = true;
    setBusy(true); setErr(''); setOk('');
    clearTimeout(okTimer.current);
    const body = {
      login: login.replace(/\s+/g, ''), // « 5123 4567 » collé depuis un email → chiffres seuls
      password: pwd,                    // transite vers MetaApi ; jamais stocké côté client
      server: server.trim(),
      label: label.trim() || null,
    };
    const r = await _brokerCall(worker, '/connect', { method: 'POST', body });
    // Sécurité : on efface le mot de passe de la mémoire dès la réponse,
    // succès comme échec.
    setPwd('');
    busyRef.current = false;
    setBusy(false);
    if (r.error) { setErr(r.error.message); return; }
    setOk(_brokerConnectedText(r.data && r.data.firstSync));
    setLogin(''); setServer(''); setLabel(''); setShowPwd(false);
    onConnected && onConnected();
    okTimer.current = setTimeout(() => setOk(''), 6000);
  };

  const off = busy || !valid;

  return (
    <form className="card" noValidate aria-labelledby={uid + '-title'}
      onSubmit={e => { e.preventDefault(); submit(); }}
      style={{ padding: '20px 22px' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
        {Ico.connections}
        <div id={uid + '-title'} style={{ fontSize: 14, fontWeight: 600, letterSpacing: '-.01em' }}>Connecter MetaTrader 5</div>
      </div>
      <p style={{ fontSize: 12, color: 'var(--fg-3)', margin: '0 0 16px', lineHeight: 1.5 }}>
        {'Tes trades clôturés sont importés automatiquement, sans copier-coller. La synchronisation est en lecture seule : Tempo ne peut jamais passer d’ordre à ta place.'}
      </p>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: 12, marginBottom: 12 }}>
        <div>
          <label htmlFor={uid + '-login'} style={labelStyle}>Numéro de compte</label>
          <input id={uid + '-login'} className="input" value={login} onChange={e => setLogin(e.target.value)}
            inputMode="numeric" autoComplete="off" placeholder="Ex. 51234567" disabled={busy}/>
        </div>
        <div>
          <label htmlFor={uid + '-server'} style={labelStyle}>Serveur du courtier</label>
          <input id={uid + '-server'} className="input" value={server} onChange={e => setServer(e.target.value)}
            autoComplete="off" placeholder="Ex. ICMarketsSC-Demo" disabled={busy}/>
        </div>
      </div>

      <div style={{ marginBottom: 8 }}>
        <label htmlFor={uid + '-pwd'} style={labelStyle}>Mot de passe investisseur (lecture seule)</label>
        <div style={{ position: 'relative' }}>
          <input id={uid + '-pwd'} className="input" type={showPwd ? 'text' : 'password'} value={pwd}
            onChange={e => setPwd(e.target.value)} autoComplete="new-password"
            placeholder="Mot de passe investisseur" disabled={busy}
            style={{ paddingRight: 72 }}/>
          <button type="button" onClick={() => setShowPwd(s => !s)} disabled={busy} className="tap tap-44"
            aria-label={showPwd ? 'Masquer le mot de passe' : 'Afficher le mot de passe'} aria-pressed={showPwd}
            style={{ position: 'absolute', right: 6, top: '50%', transform: 'translateY(-50%)',
              background: 'transparent', border: 'none', cursor: 'pointer',
              color: 'var(--fg-3)', fontSize: 11.5, fontWeight: 600, padding: '4px 6px' }}>
            {showPwd ? 'Masquer' : 'Afficher'}
          </button>
        </div>
      </div>

      <div style={{ display: 'flex', gap: 8, alignItems: 'flex-start', fontSize: 11.5, color: 'var(--fg-3)', background: 'var(--bg-soft)', border: '1px solid var(--line)', borderRadius: 8, padding: '9px 11px', marginBottom: 14, lineHeight: 1.5 }}>
        {Ico.risk}
        <span>
          Utilise bien le mot de passe <b style={{ color: 'var(--fg-2)' }}>investisseur</b>
          {' : il ne permet pas de trader. Tempo ne le stocke jamais ; il sert seulement à ouvrir l’accès en lecture chez notre prestataire de synchronisation, en HTTPS.'}
        </span>
      </div>

      <div style={{ marginBottom: 16 }}>
        <label htmlFor={uid + '-label'} style={labelStyle}>Libellé (optionnel)</label>
        <input id={uid + '-label'} className="input" value={label} onChange={e => setLabel(e.target.value)}
          autoComplete="off" placeholder={'Ex. Compte FTMO 100 000'} disabled={busy}/>
      </div>

      <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
        <button type="submit" disabled={off} className="btn btn-blue tap"
          style={{ color: '#fff', opacity: off ? 0.55 : 1, cursor: off ? 'not-allowed' : 'pointer' }}>
          {busy ? 'Connexion…' : 'Connecter'}
        </button>
        {ok && <span role="status" style={{ fontSize: 12.5, color: 'var(--green-text)', fontWeight: 600, lineHeight: 1.45 }}>{ok}</span>}
        {err && <span role="alert" style={{ fontSize: 12.5, color: 'var(--red-text)', lineHeight: 1.45 }}>{err}</span>}
      </div>
    </form>
  );
}

// ─── Zone « Importer un fichier (CSV, HTML ou TSV) » ────────────────────────────
// Réutilise TOUJOURS le flux d'import existant via onImportFile/openImport
// (onglet « Importer un fichier » de la fenêtre « Ajouter des trades »).
function _BrokerImportZone({ onImportFile, openImport }) {
  const open = onImportFile || openImport;
  return (
    <div className="card" style={{ padding: '20px 22px', display: 'flex', alignItems: 'flex-start', gap: 14, flexWrap: 'wrap' }}>
      <div style={{ flex: 1, minWidth: 200 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
          {Ico.upload}
          <div style={{ fontSize: 14, fontWeight: 600, letterSpacing: '-.01em' }}>Importer un fichier (CSV, HTML ou TSV)</div>
        </div>
        <p style={{ fontSize: 12, color: 'var(--fg-3)', margin: 0, lineHeight: 1.5 }}>
          {'Pas de synchronisation automatique ? Importe ton relevé MetaTrader 5 ou tout export CSV, HTML ou TSV. Disponible à tout moment, même avec un compte connecté.'}
        </p>
      </div>
      {/* Pas de bouton factice : sans flux d'import branché, on ne l'affiche pas. */}
      {typeof open === 'function' && (
        <button type="button" onClick={() => open(null, 'csv')} className="btn tap"
          style={{ alignSelf: 'center', color: 'var(--fg)' }}>
          {Ico.upload} Importer un fichier
        </button>
      )}
    </div>
  );
}

// ─── Squelette d'une carte de connexion (premier chargement uniquement) ───
function _BrokerCardSkeleton() {
  const bar = (w, h, extra) => <div style={{ width: w, height: h, borderRadius: 6, background: 'var(--bg-elev)', ...extra }}/>;
  return (
    <div className="card" role="status" aria-busy="true" aria-label="Chargement de tes comptes connectés…"
      style={{ padding: '16px 18px', display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 7 }}>{bar('40%', 13)}{bar('62%', 9)}</div>
        {bar(84, 22, { borderRadius: 999, flexShrink: 0 })}
      </div>
      {bar('34%', 9)}
      <div style={{ display: 'flex', gap: 8 }}>{bar(176, 32)}</div>
    </div>
  );
}

// ─── Panneau principal ────────────────────────────────────────────────────
// Props :
//   worker        : URL du Worker (défaut BROKER_WORKER_DEFAULT)
//   onSynced      : callback appelé après une synchronisation ou une connexion
//                   réussie (l'intégration y branche state.refreshTrades())
//   onImportFile / openImport : ouvre le flux d'import fichier existant
//   compact       : rendu réduit (cf. BrokerPanelCompact)
//   inModal       : rendu dans la fenêtre « Ajouter des trades », qui a déjà son
//                   onglet « Importer un fichier » : la zone d'import est masquée
function BrokerPanel({ worker, onSynced, onImportFile, openImport, compact = false, inModal = false, onDirtyChange }) {
  const WORKER = worker || BROKER_WORKER_DEFAULT;
  const [conns, setConns]     = React.useState([]);
  const [loaded, setLoaded]   = React.useState(false); // au moins un chargement réussi
  const [loading, setLoading] = React.useState(true);
  const [loadErr, setLoadErr] = React.useState('');
  const liveRef = React.useRef(true);

  // Un rechargement ne vide jamais la liste : une erreur de chargement n'est
  // pas « aucun compte connecté ». On garde les cartes déjà affichées et on
  // montre un bandeau avec « Réessayer ».
  const loadStatus = React.useCallback(async () => {
    setLoading(true);
    const r = await _brokerCall(WORKER, '/status', { method: 'GET' });
    if (!liveRef.current) return;
    setLoading(false);
    if (r.error) { setLoadErr(r.error.message); return; }
    const list = (r.data && (r.data.connections || r.data.items)) || [];
    setConns(Array.isArray(list) ? list : []);
    setLoadErr('');
    setLoaded(true);
  }, [WORKER]);

  React.useEffect(() => {
    liveRef.current = true;
    loadStatus();
    return () => { liveRef.current = false; };
  }, [loadStatus]);

  // Après une action réussie : on recharge le statut ET on prévient l'app
  // (rechargement des trades via onSynced).
  const handleChanged = async () => {
    await loadStatus();
    if (typeof onSynced === 'function') {
      try { await onSynced(); } catch (e) { console.warn('[Tempo] courtier : rechargement des trades', e); }
    }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: compact ? 12 : 14 }}>
      {/* Bandeau d'erreur de chargement (les données déjà chargées restent affichées) */}
      {loadErr && (
        <div className="card" role="alert" style={{ padding: '12px 16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', background: 'var(--red-soft)', borderColor: 'var(--red)' }}>
          <div style={{ flex: 1, minWidth: 200, fontSize: 12.5, fontWeight: 500, color: 'var(--red-text)', lineHeight: 1.5 }}>
            Impossible de charger tes comptes connectés. {loadErr}
          </div>
          <button type="button" onClick={loadStatus} disabled={loading} className="btn tap"
            style={{ color: 'var(--fg)', ...(loading ? { opacity: 0.55, cursor: 'not-allowed' } : null) }}>
            {Ico.refresh} {loading ? 'Chargement…' : 'Réessayer'}
          </button>
        </div>
      )}

      {/* État des connexions */}
      {!loaded ? (
        loading && !loadErr && <_BrokerCardSkeleton/>
      ) : conns.length > 0 ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          {conns.map((c, i) => (
            <_BrokerConnectionCard key={c.id || c.login || i} conn={c} worker={WORKER} onChanged={handleChanged}/>
          ))}
        </div>
      ) : (
        <div className="card" style={{ padding: '18px 20px', display: 'flex', alignItems: 'center', gap: 12 }}>
          {Ico.connections}
          <div style={{ fontSize: 12.5, color: 'var(--fg-2)', lineHeight: 1.5 }}>
            Aucun courtier connecté pour l’instant. Connecte ton compte ci-dessous pour importer
            tes trades automatiquement.
          </div>
        </div>
      )}

      {/* Formulaire de connexion MT5 */}
      <_BrokerConnectForm worker={WORKER} onConnected={handleChanged} onDirtyChange={onDirtyChange}/>

      {/* Import fichier — toujours disponible */}
      {!inModal && <_BrokerImportZone onImportFile={onImportFile} openImport={openImport}/>}

      {/* Note Futures / Tradovate */}
      <div style={{ fontSize: 11.5, color: 'var(--fg-3)', lineHeight: 1.5, padding: '2px 2px 0' }}>
        {'Futures (Tradovate) : synchronisation bientôt disponible. En attendant, importe ton relevé en CSV.'}
      </div>
    </div>
  );
}

// Version compacte : même contenu, simplement un wrapper sémantique pour les
// intégrations mobiles / encarts réduits. (Le rendu interne s'adapte déjà.)
function BrokerPanelCompact(props) {
  return <BrokerPanel {...props} compact={true}/>;
}

Object.assign(window, { BrokerPanel, BrokerPanelCompact, BROKER_WORKER_DEFAULT });
