import {brand} from './brand.js';
import {siteNavigation} from './site-nav.js';
import {languageSelector,nativeText,currentLanguage} from './i18n.js';
import { TransferClient } from './transfer.js';
import { createSink, MEMORY_LIMIT } from './sinks.js';

const $ = (selector) => document.querySelector(selector);
const node = (tag, className, content) => {
  const el = document.createElement(tag);
  if (className) el.className = className;
  if (content !== undefined) el.textContent = content;
  return el;
};
const bytes = (n) => {
  if (!n) return '0 octet';
  const units = ['o', 'Kio', 'Mio', 'Gio', 'Tio'];
  const i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), 4);
  return `${new Intl.NumberFormat(currentLanguage(), { maximumFractionDigits: i > 0 ? 1 : 0 }).format(n / 1024 ** i)} ${units[i]}`;
};
const date = (n) => new Intl.DateTimeFormat(currentLanguage(), { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }).format(n);
const icons = {
  transfer: '<path d="M4 7h14l-4-4M20 17H6l4 4"/>',
  plus: '<path d="M12 5v14M5 12h14"/>',
  upload: '<path d="M12 16V3m-5 5 5-5 5 5M4 15v5h16v-5"/>',
  file: '<path d="M14 3H6v18h12V7zM14 3v5h4M9 12h6m-6 4h6"/>',
  link: '<path d="m10 14 4-4m-7 6-1 1a4 4 0 0 1-5-5l5-5a4 4 0 0 1 6 0m0 1 1-1a4 4 0 0 1 6 5l-5 5a4 4 0 0 1-6 0"/>',
  shield: '<path d="m12 2 8 4v6c0 5-8 10-8 10S4 17 4 12V6zM8 12l3 3 5-6"/>',
  arrow: '<path d="M5 12h14m-5-5 5 5-5 5"/>',
  clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
  download: '<path d="M12 3v13m-5-5 5 5 5-5M4 17v4h16v-4"/>',
  check: '<path d="m5 12 4 4L19 6"/>',
};
const icon = (name) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons[name] || icons.file}</svg>`;
const shareId = new URLSearchParams(location.search).get('share') || location.pathname.match(/^\/s\/([^/]+)$/)?.[1];
const secret = location.hash.slice(1);
if (secret) history.replaceState(null, '', location.pathname + location.search);
const receiver = Boolean(shareId);
const LEGACY_ORIGIN = 'https://t.mailtingpot.com';
const PUBLIC_ORIGINS = [LEGACY_ORIGIN, 'https://transferwing.com', 'https://relyo.online'];
const REMOTE_ORIGIN = PUBLIC_ORIGINS.includes(location.origin) ? location.origin : brand.origin;
const ACCOUNT_URL = window.desktop ? `${REMOTE_ORIGIN}/account` : '/account';
const requestedBroker = new URLSearchParams(location.search).get('broker');
let brokerOrigin = PUBLIC_ORIGINS.includes(requestedBroker) ? requestedBroker : window.desktop && !receiver ? REMOTE_ORIGIN : location.origin;
const state = { files: [], shares: [], sessions: new Map(), logs: [], ready: false, busy: false, manifest: [], currentFile: null, paused: false };
let licenseState = null, licenseVerified = false, licenseRequested = false;
let client;

$('#app').classList.add('simple-layout');
$('#app').innerHTML = `
  ${window.desktop ? `<aside class="sidebar">
    <a class="brand" href="/" aria-label="TransferWing, accueil"><span class="brand-mark">${icon('transfer')}</span>TransferWing<span class="brand-dot">.</span></a>
    <div class="workspace-label">VOS FICHIERS</div>
    <nav aria-label="Navigation principale">
      <a class="nav-item ${receiver ? '' : 'active'}" href="/app">${icon('link')} Mes partages <span id="nav-count">0</span></a>
      <a class="nav-item" href="#activity">${icon('clock')} Activité</a>
      <a class="nav-item get-license" href="${ACCOUNT_URL}">${icon('file')} Mon compte</a>
      ${!receiver && window.desktop?.licenseStatus ? `<a class="nav-item" id="open-license" href="#license-title">${icon('shield')} <span class="license-label">Clé de licence</span><span id="license-check" class="license-check" hidden aria-label="Activée">✓</span></a>` : ''}
      <a class="nav-item" href="/securite">${icon('shield')} Sécurité</a>
    </nav>
    ${languageSelector()}
    <div class="sidebar-bottom"><span class="local-orb"></span><div><strong>Vos fichiers restent ici</strong><p>Aucune copie sur le serveur.</p></div></div>
  </aside>` : siteNavigation()}
  <main>
    <header class="topbar"><div class="breadcrumb">Espace de travail <span>/</span> <strong>${receiver ? 'Recevoir des fichiers' : 'Mes partages'}</strong></div><span id="server-status" class="status"><i></i> Connexion…</span></header>
    <div class="content">
      <div class="page-title"><div><div class="eyebrow">SIMPLE. DIRECT. CONFIDENTIEL.</div><h1>${receiver ? 'Recevoir des fichiers' : 'Envoyez vos fichiers.'}</h1><p>${receiver ? 'Connectez-vous directement à la personne qui vous partage ces fichiers.' : 'Choisissez vos fichiers. Créez un lien. Envoyez-le.'}</p></div><span class="version">TRANSFERWING 0.8.0</span></div>
      <div class="notice">${icon('shield')}<span id="connection-notice"><strong>Trois transferts gratuits.</strong> Choisissez vos fichiers, créez le lien et gardez cet ordinateur connecté.</span></div>
      ${!receiver && window.desktop?.licenseStatus ? licenseMarkup() : ''}
      <div id="message" class="message" role="status" aria-live="polite" hidden></div>
      <button id="retry-connection" class="button secondary" type="button" hidden>Réessayer la connexion</button>
      ${receiver ? receiverMarkup() : senderMarkup()}
      ${window.desktop?.diskSpace ? '<section class="card disk-card"><div class="section-title"><h2>Stockage de cet ordinateur</h2><button id="refresh-disk" class="button text-button">Actualiser</button></div><div id="disk-summary" aria-live="polite">Lecture de l’espace disque…</div><p class="field-help">L’espace utilisé comprend le système, les applications et vos fichiers. Ces informations restent sur cet ordinateur.</p><p id="disk-updated" class="field-help"></p></section>' : ''}
      ${!receiver ? connectionMarkup() : ''}
      <section class="activity card" id="activity"><div class="section-title"><div><div class="eyebrow">EN TEMPS RÉEL</div><h2>Activité des transferts</h2></div><button id="export-log" class="button text-button">${icon('download')} Exporter</button></div><div id="session-list"></div><div id="event-list" class="event-list"><p class="empty-note">Les connexions et transferts apparaîtront ici.</p></div></section>
      <footer><span>TransferWing · Version bêta</span><span>${icon('shield')} Chiffrement de bout en bout</span></footer>
    </div>
  </main>`;

function connectionMarkup() {
  return `<section class="card connection-card"><details id="connection-settings" class="share-options"><summary>Réglages avancés</summary><form id="connection-form"><div class="section-title"><h2>Connexion</h2></div><div class="field"><label for="broker-origin">Serveur de partage</label><select id="broker-origin"><option value="local">${location.protocol === 'https:' ? 'Ce serveur' : 'Essai sur cet ordinateur'}</option><option value="remote">Internet · TransferWing</option></select></div><details id="invitation-options" class="invitation-options" hidden><summary>Activer ma licence</summary><div class="field"><label for="publisher-token">Clé de licence</label><input id="publisher-token" type="password" autocomplete="off" maxlength="64" placeholder="Collez votre clé de licence" /><p class="field-help">Aucune clé nécessaire pour vos trois premiers transferts. Votre clé reste uniquement dans cette session.</p><p><a class="get-license" href="${ACCOUNT_URL}" target="_blank" rel="noopener noreferrer">Obtenez votre licence</a> · 1, 10 ou 100 postes</p></div></details><button class="button secondary" id="connect-server">Connecter</button></form></details>${window.desktop ? '<details class="receive-link-options"><summary>Recevoir un fichier avec un lien</summary><form id="open-link-form"><div class="field"><label for="incoming-link">Recevoir un lien dans cette application</label><input id="incoming-link" type="password" autocomplete="off" placeholder="Collez le lien complet reçu" maxlength="4096" /></div><button class="button secondary">Ouvrir le partage reçu</button><p class="field-help">L’application enregistre directement sur votre disque. Terminez vos envois avant d’ouvrir un lien reçu.</p></form></details>' : ''}</section>`;
}
function licenseMarkup() {
  return `<section class="card license-card" hidden aria-labelledby="license-title"><button id="license-back" type="button" class="button text-button">← <span>Mes partages</span></button><div class="section-title"><h2 id="license-title">Clé de licence</h2><button id="license-account" type="button" class="button secondary">Mon compte ↗</button></div><p id="license-machine" class="license-machine"></p><p id="license-status" role="status" aria-live="polite">Vérification du stockage sécurisé…</p><form id="license-form"><div class="field"><label for="activation-link">Clé de licence</label><input id="activation-link" type="password" autocomplete="off" spellcheck="false" maxlength="4096" placeholder="Collez votre lien d’activation" required /><p class="field-help">Collez le lien d’activation fourni dans votre compte. Il sert de clé de licence pour cet ordinateur.</p><p><a class="get-license" href="${ACCOUNT_URL}" target="_blank" rel="noopener noreferrer">Obtenez votre licence</a> · 1, 10 ou 100 postes</p></div><button id="license-activate" class="button primary">Activer cet ordinateur</button></form><div id="license-active-actions" hidden><button id="license-reconnect" type="button" class="button secondary">Vérifier et connecter sur Internet</button><button id="license-deactivate" type="button" class="button text-button danger">Désactiver cet ordinateur</button></div><p class="field-help">La clé de cet appareil reste dans le stockage sécurisé du système. Son nom peut être modifié depuis votre compte ; il ne constitue pas une preuve d’identité.</p></section>`;
}
function renderLicense() {
  if (!$('#license-status') || !licenseState) return;
  $('#license-machine').textContent = `Cet ordinateur : ${licenseState.machineName || 'Nom indisponible'}`;
  $('#license-status').textContent = licenseState.error || (licenseState.activated ? licenseVerified ? 'Licence vérifiée en ligne pour cette connexion.' : 'Compte activé sur cet ordinateur. Connexion en cours…' : 'Vous pouvez essayer gratuitement. Collez votre clé ci-dessous si vous avez déjà un compte.');
  $('#license-status').classList.toggle('license-verified', licenseVerified);
  $('#license-form').hidden = Boolean(licenseState.activated);
  $('#license-activate').disabled = !licenseState.supported || !licenseState.secureStorage;
  $('#license-active-actions').hidden = !licenseState.activated;
  $('.license-card').hidden = !licenseRequested;
  $('#license-check').hidden = !licenseState.activated;
  $('#invitation-options').hidden = true;
}
function requestLicense() {
  licenseRequested = true;
  if(window.desktop?.licenseStatus){$('.content').classList.add('license-view');$('.breadcrumb strong').textContent='Clé de licence';$('#open-license').classList.add('active');$('.nav-item[href="/app"]')?.classList.remove('active');renderLicense();if(!licenseState?.activated)$('#activation-link').focus();else $('#license-back').focus();window.scrollTo({top:0});}
  else {$('#connection-settings').open=true;const options=$('#invitation-options');options.hidden=false;options.open=true;$('#publisher-token').focus();options.scrollIntoView({block:'center'});}
}
function renderFileLimit(policy = client?.publisherPolicy) {
  if (!$('#file-limit')) return;
  const limit = policy?.maxFileBytes || 1e9;
  const label = limit >= 1e12 ? '1 To' : `${limit / 1e9} Go`;
  const offer = policy?.complimentary ? 'Administrateur · accès offert' : ({free:'Gratuit',solo:'Individuel',team:'Entreprise 10',business:'Entreprise 100'}[policy?.plan] || 'Essai gratuit');
  $('#file-limit').textContent = `${offer} · Jusqu’à ${label} par fichier. ` + (limit >= 1e11 ? 'Très gros fichiers : fonction en qualification. ' : '') + 'Une coupure nécessite de recommencer le téléchargement.';
}
function activeWork() { return state.busy || state.currentFile || state.shares.some(s => !s.revoked && s.expiresAt > Date.now()) || [...state.sessions.values()].some(s => s.state !== 'closed'); }
async function licenseProvider() {
  try { const result = await window.desktop.licensePublisherToken(); return result; }
  catch (error) { licenseVerified = false; renderLicense(); throw error; }
}
function senderMarkup() {
  return `<div class="work-grid"><section class="card create-card"><div class="section-title"><div><div class="eyebrow">ÉTAPE 1</div><h2>Choisissez vos fichiers</h2></div><span class="square-icon">${icon('plus')}</span></div>
    <form id="share-form">
      <label id="drop-zone" class="drop-zone" for="files"><span class="upload-icon">${icon('upload')}</span><strong>Glissez vos fichiers ici</strong><span>ou cliquez pour les sélectionner</span><input type="file" id="files" multiple class="visually-hidden" /></label>
      <div class="selection-bar"><span id="selection-summary">Aucun fichier sélectionné</span><button type="button" id="clear-files" class="text-button" hidden>Tout retirer</button></div><div id="selected-files" class="file-list"></div>
      <p id="file-limit" class="field-help">Vérification de votre offre…</p>
      <details id="share-options" class="share-options"><summary>Mot de passe et options <span>facultatif</span></summary>
      <div class="field"><label for="password">Mot de passe <span>facultatif</span></label><input id="password" type="password" minlength="8" maxlength="128" placeholder="Au moins 8 caractères" autocomplete="new-password" /><p class="field-help">Transmettez-le séparément du lien.</p></div>
      <div class="field"><label for="share-title">Nom du partage <span>facultatif</span></label><input id="share-title" maxlength="80" placeholder="Ex. Photos du projet" autocomplete="off" /></div>
      <div class="field-row"><div class="field"><label for="expires">Expiration</label><select id="expires"><option value="1">Dans 1 heure</option><option value="24" selected>Dans 24 heures</option><option value="72">Dans 3 jours</option><option value="168">Dans 7 jours</option></select></div><div class="field"><label for="max-peers">Connexions simultanées</label><select id="max-peers"><option value="1">1 personne</option><option value="3" selected>3 personnes</option><option value="5">5 personnes</option><option value="10">10 personnes</option></select></div></div>
      <label class="check-row"><input id="approval" type="checkbox" /><span><strong>Valider chaque accès</strong><small>Vous acceptez la demande avant d’afficher les fichiers.</small></span></label>
      ${window.desktop ? '<label class="check-row"><input id="awake" type="checkbox" checked /><span><strong>Éviter la veille automatique</strong><small>Pendant que vos partages sont actifs.</small></span></label>' : ''}
      </details>
      <p class="availability-help"><strong>Gardez cet ordinateur allumé et connecté.</strong> Gardez cette application ouverte jusqu’à la fin du téléchargement. Aucun fichier n’est stocké sur notre serveur.</p>
      <button type="submit" class="button primary full" id="create-share" disabled>Créer mon lien ${icon('arrow')}</button>
    </form></section>
    <section class="shares-panel"><div class="section-title"><div><div class="eyebrow">ÉTAPE 2</div><h2>Copiez votre lien <span id="share-count" class="count">0</span></h2></div></div><div id="share-list"><div class="empty-share"><span class="empty-symbol">${icon('link')}</span><h3>Votre prochain partage commence ici.</h3><p>Ajoutez des fichiers et créez votre premier lien. Vous pourrez le copier, suivre les accès et le désactiver à tout moment.</p></div></div><div class="tip"><strong>Un lien n’est pas une identité.</strong><p>Le nom du destinataire est déclaré, non vérifié. Le nom affiché ne certifie pas l’identité de la personne qui reçoit les fichiers.</p></div></section></div>`;
}
function renderTransferBranding(branding) {
  const banner = $('#transfer-branding');
  if (!banner || !branding || typeof branding.companyName !== 'string' || typeof branding.message !== 'string' || !/^#[a-fA-F0-9]{6}$/.test(branding.accent)) return;
  banner.replaceChildren();
  banner.style.borderTopColor = branding.accent;
  if (branding.companyName) banner.append(node('h2', '', branding.companyName.slice(0, 80)));
  if (branding.message) banner.append(node('p', 'transfer-brand-message', branding.message.slice(0, 300)));
  banner.append(node('p', 'muted', 'Présentation fournie par l’émetteur · Transfert sécurisé par TransferWing'));
  banner.hidden = false;
}
function receiverMarkup() {
  return `<section id="transfer-branding" class="card transfer-branding" hidden aria-label="Présentation de l’émetteur"></section><div class="receive-grid"><section class="card access-card" id="access-card"><div class="eyebrow">01 / SE CONNECTER</div><h2>Accéder au partage</h2><p id="share-availability" class="muted">Vérification de la disponibilité…</p><p class="availability-help">L’ordinateur de l’émetteur doit rester allumé, connecté et TransferWing ouvert pendant votre téléchargement. S’il est hors ligne, contactez-le pour qu’il rouvre le partage.</p><form id="access-form"><div class="field"><label for="display-name">Votre nom <span>déclaré, non vérifié</span></label><input id="display-name" maxlength="100" autocomplete="name" placeholder="Comment vous présenter ?" required /></div><div class="field"><label for="access-password">Mot de passe <span>si communiqué</span></label><input id="access-password" type="password" maxlength="128" autocomplete="off" /></div><button class="button primary full" id="join-share" disabled>Accéder aux fichiers ${icon('arrow')}</button></form></section><section class="card receive-card"><div class="section-title"><div><div class="eyebrow">02 / ENREGISTRER</div><h2>Fichiers partagés</h2></div><span id="route" class="badge">En attente</span></div><div id="receive-files"><div class="receive-empty">${icon('shield')}<p>Les noms des fichiers seront visibles après autorisation.</p></div></div><div id="download-controls" class="download-controls" hidden><button id="pause" class="button secondary">Mettre en pause</button><button id="cancel" class="button text-button danger">Annuler</button></div><p class="field-help">Un fichier à la fois. La pause conserve la session ouverte ; après fermeture ou coupure, relancez le téléchargement.</p><p class="field-help" id="storage-info"></p></section></div>`;
}
function message(content, error = false) {
  const el = $('#message'); el.textContent = content; el.hidden = !content; el.classList.toggle('error', error);
}
function addLog(label, detail = '', metadata = {}) {
  state.logs.unshift({ time: new Date().toISOString(), label, detail, ...metadata }); state.logs = state.logs.slice(0, 200);
  const list = $('#event-list'); list.replaceChildren();
  for (const event of state.logs.slice(0, 8)) {
    const row = node('div', 'event');
    row.append(node('span', 'event-dot'), node('strong', '', event.label), node('span', 'event-detail', event.detail), node('time', '', new Date(event.time).toLocaleTimeString(currentLanguage(), { hour: '2-digit', minute: '2-digit' })));
    list.append(row);
  }
}
function updateReady(ready) {
  state.ready = ready; $('#retry-connection').hidden = ready; const status = $('#server-status');
  status.replaceChildren(node('i'), document.createTextNode(ready ? brokerOrigin === location.origin && location.protocol === 'http:' ? 'Essai local' : 'Prêt à partager' : 'Connexion indisponible'));
  status.classList.toggle('online', ready);
  if ($('#create-share')) $('#create-share').disabled = !ready || !state.files.length || state.busy || state.files.some(f => f.size > (client?.publisherPolicy?.maxFileBytes || 1e9));
  if ($('#join-share')) $('#join-share').disabled = !ready || !secret || state.busy;
}
async function initializeClient(publisherToken = '') {
  if (client) { client.onEvent = () => {}; client.close(); }
  updateReady(false);
  $('#retry-connection').hidden = true;
  const useLicense = !receiver && licenseState?.activated && brokerOrigin === REMOTE_ORIGIN;
  licenseVerified = false; renderLicense();
  client = new TransferClient({ url: brokerOrigin, publisherToken: useLicense ? '' : publisherToken, tokenProvider: useLicense ? licenseProvider : null, onEvent: onEvent });
  try { await client.connect(); licenseVerified = Boolean(useLicense); renderLicense(); updateReady(true); renderFileLimit(); $('#connection-notice').textContent = brokerOrigin === location.origin && location.protocol === 'http:' ? 'Essai local : les liens fonctionnent sur cet ordinateur. Choisissez Internet pour partager à distance.' : (client.trialEnabled && !publisherToken && !useLicense ? 'Essai gratuit : 3 liens, sans carte bancaire. Votre destinataire ouvre simplement le lien pour télécharger.' : 'Prêt à partager. Gardez cette application ouverte pendant le téléchargement.'); }
  catch (error) { licenseVerified = false; renderLicense(); updateReady(false); message(error.message, true); }
}
function onEvent(event) {
  if(event.type==='publisher-policy'){renderFileLimit(event.policy);if(!receiver)renderSelection();}
  if(event.type==='free-quota'){const text=`Compte Free : ${event.remaining} lien(s) restant(s) ce mois, valables 24 h. Gérez votre offre dans Mon compte.`;message(text);$('#connection-notice').textContent=text;}
  if(event.type==='trial'){const text=`Essai gratuit : ${event.remaining} transfert${event.remaining>1?'s':''} restant${event.remaining>1?'s':''}.`;message(text);$('#connection-notice').textContent=text;}
  if (event.type === 'error') { message(event.message, true); addLog('Erreur', event.message); }
  if (event.type === 'connection') {
    if (event.state === 'disconnected' && !event.sessionId) {
      licenseVerified = false; renderLicense();
      updateReady(false); for (const share of state.shares) share.revoked = true; renderShares();
      message('Connexion interrompue. Cliquez sur Réessayer la connexion, puis recréez votre lien. Un téléchargement interrompu doit être recommencé.', true);
    }
    if (event.sessionId) {
      const session = state.sessions.get(event.sessionId) || { id: event.sessionId, name: receiver ? 'Émetteur' : 'Visiteur', progress: 0 };
      session.state = event.state; if (event.route && event.route !== 'unknown') session.route = event.route;
      state.sessions.set(event.sessionId, session); renderSessions();
      if (receiver && $('#route') && session.route) $('#route').textContent = session.route === 'websocket' ? 'Relais HTTPS chiffré' : session.route === 'relay' ? 'Relais chiffré' : 'Connexion directe';
      if (event.state === 'closed') {
        addLog('Connexion fermée', event.reason || '');
        if (receiver) {
          state.manifest = []; renderReceivedFiles();
          if (!state.currentFile) { state.busy = false; updateReady(state.ready); }
          message(event.reason || 'La connexion a été fermée. Reconnectez-vous au partage.', true);
        }
      }
    }
  }
  if (event.type === 'peer' || event.type === 'approval-needed') {
    const session = state.sessions.get(event.sessionId) || { id: event.sessionId, progress: 0 };
    session.name = event.displayName || 'Visiteur'; session.waiting = event.type === 'approval-needed';
    state.sessions.set(event.sessionId, session); renderSessions();
    addLog(session.waiting ? 'Autorisation demandée' : 'Accès avec le lien', `${session.name} · nom non vérifié`, { sessionId: event.sessionId, declaredName: session.name, identityVerified: false });
  }
  if (event.type === 'waiting-approval') message('La demande a été envoyée. Attendez l’accord de l’émetteur.');
  if (event.type === 'manifest') {
    state.manifest = event.files; state.busy = false; $('#access-password').value = '';
    $('#access-card').classList.add('connected-card'); $('#share-availability').textContent = 'Accès autorisé. Vous pouvez enregistrer les fichiers.';
    $('#access-form').hidden = true; message('Connexion établie. Choisissez un fichier à enregistrer.');
    renderReceivedFiles(); addLog('Accès autorisé', `${event.files.length} fichier(s)`);
  }
  if (event.type === 'progress') {
    const session = state.sessions.get(event.sessionId) || { id: event.sessionId, name: receiver ? 'Émetteur' : 'Visiteur' };
    if (session.fileId !== event.fileId) {
      const file = state.manifest.find(f => f.id === event.fileId) || state.shares.flatMap(s => s.files).find(f => f.id === event.fileId);
      addLog('Transfert en cours', `${session.name} · ${file?.name || 'Fichier'}`, { sessionId: event.sessionId, fileId: event.fileId, declaredName: session.name, identityVerified: false, total: event.total, role: event.role });
    }
    session.bytes = event.bytes; session.total = event.total; session.fileId = event.fileId; session.progress = event.total ? event.bytes / event.total : 1;
    session.state = 'transferring'; state.sessions.set(event.sessionId, session);
    // Keep rendering bounded even when hundreds of blocks arrive per second.
    const now = performance.now();
    if (!session.lastRender || now - session.lastRender > 150 || event.bytes === event.total) {
      session.lastRender = now; renderSessions();
      const row = document.querySelector(`[data-file-id="${event.fileId}"]`);
      if (row && receiver) { row.querySelector('progress').value = session.progress; row.querySelector('.file-status').textContent = `${bytes(event.bytes)} / ${bytes(event.total)}`; }
    }
  }
  if (event.type === 'complete') {
    const session = state.sessions.get(event.sessionId); if (session) { session.state = 'complete'; session.progress = 1; renderSessions(); }
    const file = state.manifest.find(f => f.id === event.fileId) || state.shares.flatMap(s => s.files).find(f => f.id === event.fileId);
    addLog(event.role === 'sender' ? 'Réception confirmée par le client' : 'Fichier enregistré', `${session?.name || 'Visiteur'} · ${file?.name || bytes(event.bytes)}`, { sessionId: event.sessionId, fileId: event.fileId, declaredName: session?.name || '', identityVerified: false, bytes: event.bytes, role: event.role });
    if (receiver) message('Fichier reçu et vérifié. L’enregistrement a été demandé à votre appareil.');
  }
  if (event.type === 'revoked') {
    const share = state.shares.find(s => s.id === event.shareId); if (share) share.revoked = true;
    renderShares(); setAwake();
  }
  if (event.type === 'share-locked') {
    const share = state.shares.find(s => s.id === event.shareId); if (share) share.lockUntil = event.until;
    renderShares(); message('Plusieurs mots de passe incorrects ont été saisis. Les nouveaux accès à ce partage sont suspendus pendant une minute.', true);
    addLog('Protection contre les essais répétés', share?.title || '', { shareId: event.shareId, until: event.until });
  }
}
function renderSelection() {
  $('#selection-summary').textContent = state.files.length ? `${state.files.length} fichier(s) · ${bytes(state.files.reduce((n, f) => n + f.size, 0))}` : 'Aucun fichier sélectionné';
  $('#clear-files').hidden = !state.files.length;
  const list = $('#selected-files'); list.replaceChildren();
  state.files.forEach((file, index) => {
    const row = node('div', 'selected-file'); row.append(node('span', '', file.webkitRelativePath || file.name), node('small', '', bytes(file.size)));
    const remove = node('button', 'remove-button', '×'); remove.type = 'button'; remove.setAttribute('aria-label', `Retirer ${file.name}`);
    remove.onclick = () => { state.files.splice(index, 1); renderSelection(); }; row.append(remove); list.append(row);
  }); updateReady(state.ready);
}
function selectFiles(files) {
  for (const file of files) if (!state.files.some(f => f.name === file.name && f.size === file.size && f.lastModified === file.lastModified)) state.files.push(file);
  renderSelection();
  const limit = client?.publisherPolicy?.maxFileBytes || 1e9;
  if(state.files.some(f=>f.size>limit))message(`Votre offre permet ${limit >= 1e12 ? '1 To' : limit/1e9+' Go'} par fichier. Retirez le fichier trop volumineux ou consultez Mon compte pour changer d’offre.`,true);
}
function renderShares() {
  $('#app').classList.toggle('has-shares', state.shares.length > 0);
  if (receiver) return;
  const active = state.shares.filter(s => !s.revoked && s.expiresAt > Date.now()).length;
  $('#share-count').textContent = active; if ($('#nav-count')) $('#nav-count').textContent = active;
  if (!state.shares.length) return;
  const list = $('#share-list'); list.replaceChildren();
  for (const share of [...state.shares].reverse()) {
    const expired = Date.now() >= share.expiresAt, disabled = share.revoked || expired;
    const card = node('article', `share-card ${disabled ? 'inactive' : ''}`);
    const top = node('div', 'share-top'); top.append(node('span', 'share-initial', share.title.charAt(0).toUpperCase()), node('span', `badge ${disabled ? '' : 'live'}`, share.revoked ? 'Désactivé' : expired ? 'Expiré' : share.lockUntil > Date.now() ? 'Accès suspendus 1 min' : 'Disponible'));
    card.append(top, node('h3', '', share.title), node('p', 'share-summary', `${share.files.length} fichier(s) · ${bytes(share.total)}`));
    const tags = node('div', 'share-tags'); tags.append(node('span', '', share.protected ? 'Mot de passe' : 'Lien secret'), node('span', '', share.approval ? 'Validation manuelle' : 'Accès automatique')); card.append(tags);
    const linkRow = node('div', 'link-row'); const input = node('input'); input.readOnly = true; input.value = share.url; input.setAttribute('aria-label', `Lien de ${share.title}`);
    const copy = node('button', 'button copy-button', 'Copier'); copy.disabled = disabled;
    copy.onclick = async () => { try { if (window.desktop?.copyLink) await window.desktop.copyLink(share.url); else await navigator.clipboard.writeText(share.url); copy.textContent = 'Copié !'; setTimeout(() => copy.textContent = 'Copier', 1800); } catch { input.select(); message('Sélectionnez et copiez le lien affiché.'); } };
    linkRow.append(input, copy); card.append(linkRow);
    const bottom = node('div', 'share-bottom'); bottom.append(node('span', '', `Expire le ${date(share.expiresAt)}`));
    const revoke = node('button', 'text-button danger', 'Désactiver'); revoke.disabled = disabled;
    revoke.onclick = async () => { try { await client.revokeShare(share.id); share.revoked = true; renderShares(); setAwake(); addLog('Partage désactivé', share.title); } catch (error) { message(error.message, true); } };
    bottom.append(revoke); card.append(bottom); list.append(card);
  }
}
function renderSessions() {
  const list = $('#session-list'); list.replaceChildren();
  for (const session of [...state.sessions.values()].slice(-10).reverse()) {
    const row = node('div', 'session');
    const identity = node('div', 'session-identity'); identity.append(node('strong', '', session.name || (receiver ? 'Émetteur' : 'Visiteur')), node('small', '', receiver ? 'Session de réception' : 'Nom déclaré · non vérifié'));
    const status = node('div', 'session-status'); const labels = { connected: 'Connecté', connecting: 'Connexion…', closed: 'Déconnecté', complete: 'Réception confirmée', transferring: 'Transfert en cours', new: 'Connexion…' };
    status.append(node('span', '', session.waiting ? 'Attend votre accord' : labels[session.state] || 'Connexion…'));
    if (session.total !== undefined) { const progress = node('progress'); progress.max = 1; progress.value = session.progress; progress.setAttribute('aria-label', 'Progression du transfert'); status.append(progress, node('small', '', `${bytes(session.bytes)} / ${bytes(session.total)}`)); }
    row.append(identity, status);
    if (session.waiting && session.state !== 'closed') {
      const actions = node('div', 'session-actions');
      for (const [label, method] of [['Accepter', 'approve'], ['Refuser', 'reject']]) {
        const button = node('button', `button ${method === 'approve' ? 'primary' : 'secondary'}`, label);
        button.onclick = async () => { try { await client[method](session.id); session.waiting = false; renderSessions(); addLog(label === 'Accepter' ? 'Accès accepté' : 'Accès refusé', session.name); } catch (error) { message(error.message, true); } };
        actions.append(button);
      } row.append(actions);
    } else if (session.route) row.append(node('span', 'badge', session.route === 'websocket' ? 'Relais HTTPS' : session.route === 'relay' ? 'Relais' : 'Direct'));
    list.append(row);
  }
}
function renderReceivedFiles() {
  const list = $('#receive-files'); list.replaceChildren();
  if (!state.manifest.length) { list.append(node('p', 'empty-note', 'Aucun fichier accessible. Reconnectez-vous avec le lien complet si nécessaire.')); $('#access-form').hidden = false; return; }
  for (const file of state.manifest) {
    const row = node('div', 'receive-file'); row.dataset.fileId = file.id;
    const badge = node('div', 'file-icon'); badge.innerHTML = icon('file');
    const info = node('div', 'receive-file-info'); const progress = node('progress'); progress.max = 1; progress.value = 0; progress.setAttribute('aria-label', `Progression de ${file.name}`);
    info.append(node('strong', '', file.name), node('small', 'file-status', bytes(file.size)), progress);
    const button = node('button', 'button secondary download-button', 'Enregistrer'); button.disabled = Boolean(state.currentFile);
    button.onclick = () => saveFile(file);
    row.append(badge, info, button); list.append(row);
  }
}
async function saveFile(file) {
  if (state.currentFile) return;
  state.currentFile = file.id; state.paused = false;
  document.querySelectorAll('.download-button').forEach(b => b.disabled = true);
  let sink;
  try {
    sink = await createSink(file);
    $('#download-controls').hidden = false; $('#pause').textContent = 'Mettre en pause';
    await window.desktop?.setKeepAwake(true);
    addLog('Téléchargement commencé', file.name); message(`Enregistrement de « ${file.name} »…`);
    await client.download(file.id, { sink });
  } catch (error) {
    if (sink) await sink.abort().catch(() => {});
    message(error.name === 'AbortError' ? 'Enregistrement annulé.' : error.message, error.name !== 'AbortError');
  } finally {
    state.currentFile = null; state.busy = false; $('#download-controls').hidden = true;
    document.querySelectorAll('.download-button').forEach(b => b.disabled = false);
    await window.desktop?.setKeepAwake(false); updateReady(state.ready);
  }
}
async function setAwake() {
  if (window.desktop) await window.desktop.setKeepAwake(Boolean($('#awake')?.checked && state.shares.some(s => !s.revoked && s.expiresAt > Date.now()))).catch(() => {});
}
$('#export-log').onclick = () => {
  const url = URL.createObjectURL(new Blob([JSON.stringify({ exportedAt: new Date().toISOString(), identity: 'Noms déclarés, non vérifiés', events: state.logs }, null, 2)], { type: 'application/json' }));
  const a = node('a'); a.href = url; a.download = 'transferwing-activite.json'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000);
};
if (!receiver) {
  $('#broker-origin').value = brokerOrigin === REMOTE_ORIGIN ? 'remote' : 'local';
  $('#connection-form').onsubmit = async event => {
    event.preventDefault();
    if (state.busy || state.shares.some(s => !s.revoked && s.expiresAt > Date.now()) || [...state.sessions.values()].some(s => s.state !== 'closed')) { message('Désactivez vos partages et terminez les transferts avant de changer de serveur.', true); return; }
    brokerOrigin = $('#broker-origin').value === 'remote' ? REMOTE_ORIGIN : location.origin;
    const token = $('#publisher-token').value.trim(); $('#publisher-token').value = '';
    await initializeClient(token);
  };
  $('#open-link-form')?.addEventListener('submit', event => {
    event.preventDefault();
    if (state.shares.some(s => !s.revoked && s.expiresAt > Date.now()) || state.currentFile) { message('Désactivez vos partages avant d’ouvrir un lien reçu.', true); return; }
    try {
      const incoming = new URL($('#incoming-link').value.trim());
      if (![location.origin, ...PUBLIC_ORIGINS].includes(incoming.origin) || !['/', '/app', '/app/'].includes(incoming.pathname) || incoming.username || incoming.password || !/^[0-9a-f-]{36}$/i.test(incoming.searchParams.get('share') || '') || !/^#[A-Za-z0-9_-]{43}$/.test(incoming.hash) || [...incoming.searchParams.keys()].some(key => key !== 'share')) throw new Error('Utilisez un lien TransferWing complet de ce serveur, de TransferWing ou de Relyo.');
      const target = new URL('/app', location.origin); target.searchParams.set('share', incoming.searchParams.get('share')); target.searchParams.set('broker', incoming.origin); target.hash = incoming.hash;
      $('#incoming-link').value = ''; location.assign(target.href);
    } catch (error) { message(error.message, true); }
  });
  $('#files').onchange = (event) => { selectFiles(event.target.files); event.target.value = ''; };
  $('#clear-files').onclick = () => { state.files = []; renderSelection(); };
  const drop = $('#drop-zone');
  drop.ondragover = (event) => { event.preventDefault(); drop.classList.add('dragover'); };
  drop.ondragleave = () => drop.classList.remove('dragover');
  drop.ondrop = (event) => { event.preventDefault(); drop.classList.remove('dragover'); selectFiles(event.dataTransfer.files); };
  $('#awake')?.addEventListener('change', setAwake);
  $('#share-form').onsubmit = async (event) => {
    event.preventDefault(); if (state.busy || !state.ready || !state.files.length) return;
    state.busy = true; updateReady(true); $('#create-share').textContent = 'Création du lien…';
    try {
      const files = [...state.files]; const title = $('#share-title').value.trim() || files[0].name;
      const password = $('#password').value, approval = $('#approval').checked;
      const share = await client.createShare({ files, password, expiresAt: Date.now() + Number($('#expires').value) * 3600_000, maxPeers: Number($('#max-peers').value), manualApproval: approval });
      const metadata = [...client.shares.get(share.id).files.values()].map(entry => entry.meta);
      state.shares.push({ ...share, title, files: metadata, total: files.reduce((n, f) => n + f.size, 0), protected: Boolean(password), approval });
      state.files = []; $('#password').value = ''; $('#share-title').value = ''; renderSelection(); renderShares(); setAwake();
      message('Le lien est prêt. Envoyez-le au destinataire et gardez cet ordinateur allumé, connecté et TransferWing ouvert jusqu’à la fin du téléchargement.'); addLog('Partage créé', title);
    } catch (error) { message(error.message, true); if(error.message.includes('trois transferts gratuits'))requestLicense(); }
    finally { state.busy = false; $('#create-share').innerHTML = `Créer mon lien ${icon('arrow')}`; updateReady(state.ready); }
  };
  setInterval(() => { renderShares(); setAwake(); }, 30_000);
} else {
  $('#storage-info').textContent = window.desktop ? 'Écriture directe sur disque par l’application.' : 'showSaveFilePicker' in window ? 'Écriture directe sur disque disponible. La fermeture du navigateur interrompt le transfert.' : `Mode compatible : fichiers jusqu’à ${bytes(MEMORY_LIMIT)}. Pour les gros fichiers, utilisez Chrome/Edge sur ordinateur ou l’application.`;
  if (!secret) message('La clé manque dans ce lien. Ouvrez à nouveau le lien complet fourni par l’émetteur, y compris la partie après #.', true);
  fetch(new URL(`/api/shares/${encodeURIComponent(shareId)}`, brokerOrigin)).then(async response => { if (!response.ok) throw new Error('Ce partage est indisponible, expiré ou désactivé.'); const info = await response.json(); renderTransferBranding(info.branding); $('#share-availability').textContent = info.online ? `Émetteur en ligne · expire le ${date(info.expiresAt)}` : 'L’émetteur est hors ligne.'; }).catch(error => { $('#share-availability').textContent = error.message; });
  $('#access-form').onsubmit = async event => {
    event.preventDefault(); if (state.busy || !secret) return;
    state.busy = true; updateReady(state.ready); message('Connexion chiffrée en cours…');
    try {
      await client.joinShare({ shareId, secret, password: $('#access-password').value, displayName: $('#display-name').value.trim() });
    } catch (error) { state.busy = false; updateReady(state.ready); message(error.message, true); }
  };
  $('#pause').onclick = () => { state.paused = !state.paused; state.paused ? client.pause() : client.resume(); $('#pause').textContent = state.paused ? 'Reprendre' : 'Mettre en pause'; message(state.paused ? 'Transfert en pause. Gardez les deux applications ouvertes.' : 'Transfert repris.'); };
  $('#cancel').onclick = () => client.cancelDownload();
}
window.addEventListener('beforeunload', event => { if (state.currentFile || state.shares.some(s => !s.revoked && s.expiresAt > Date.now())) { event.preventDefault(); event.returnValue = ''; } });
$('#retry-connection').onclick = () => initializeClient();
for(const link of document.querySelectorAll('.get-license'))if(window.desktop?.openLicenseAccount)link.addEventListener('click',event=>{event.preventDefault();window.desktop.openLicenseAccount().catch(()=>message('Impossible d’ouvrir la page des licences.',true));});
if (!receiver && window.desktop?.licenseStatus) {
  $('#open-license').onclick = event => { event.preventDefault(); requestLicense(); };
  const closeLicense=()=>{licenseRequested=false;$('.breadcrumb strong').textContent='Mes partages';$('.content').classList.remove('license-view');$('.license-card').hidden=true;$('#open-license').classList.remove('active');$('.nav-item[href="/app"]')?.classList.add('active');};
  $('#license-back').onclick=closeLicense;
  $('.nav-item[href="/app"]').onclick=event=>{event.preventDefault();closeLicense();};
  $('.nav-item[href="#activity"]').addEventListener('click',closeLicense);
  $('#license-account').onclick = async () => { try { await window.desktop.openLicenseAccount(); } catch { message('Impossible d’ouvrir votre compte dans le navigateur.', true); } };
  $('#license-form').onsubmit = async event => {
    event.preventDefault();
    if (activeWork()) { message('Terminez vos transferts et désactivez vos partages avant d’activer une licence.', true); return; }
    const link = $('#activation-link').value; $('#activation-link').value = ''; $('#license-activate').disabled = true;
    try {
      licenseState = await window.desktop.activateLicense(link); licenseVerified = false; renderLicense();
      brokerOrigin = REMOTE_ORIGIN; $('#broker-origin').value = 'remote'; $('#publisher-token').value = '';
      await initializeClient();
      if (state.ready) message('Cet ordinateur est activé et connecté au service Internet.');
    } catch (error) { message(error.message, true); }
    finally { renderLicense(); }
  };
  $('#license-reconnect').onclick = async () => {
    if (activeWork()) { message('Terminez vos transferts et désactivez vos partages avant de reconnecter le service.', true); return; }
    brokerOrigin = REMOTE_ORIGIN; $('#broker-origin').value = 'remote'; await initializeClient();
  };
  $('#license-deactivate').onclick = async () => {
    const confirmed = window.confirm(nativeText('deactivate',{name:licenseState?.machineName || brand.name}));
    if (!confirmed) return;
    $('#license-deactivate').disabled = true;
    try {
      await window.desktop.deactivateLicense();
      client.onEvent = () => {}; client.close();
      for (const share of state.shares) share.revoked = true;
      for (const session of state.sessions.values()) session.state = 'closed';
      renderShares(); renderSessions(); await setAwake();
      licenseVerified = false; licenseState = await window.desktop.licenseStatus(); renderLicense();
      brokerOrigin = REMOTE_ORIGIN; $('#broker-origin').value = 'remote'; await initializeClient();
      message('Cet ordinateur est désactivé. Vous pouvez continuer avec les essais gratuits disponibles.');
    } catch (error) { message(error.message, true); }
    finally { $('#license-deactivate').disabled = false; }
  };
  try {
    licenseState = await window.desktop.licenseStatus(); renderLicense();
    if (licenseState.activated) { brokerOrigin = REMOTE_ORIGIN; $('#broker-origin').value = 'remote'; }
  } catch { licenseState = { supported: true, secureStorage: false, activated: false, error: 'Impossible de lire l’état de la licence.' }; renderLicense(); }
}
await initializeClient();

// Only the native process chooses the measured directories; no paths cross this bridge.
if (window.desktop?.diskSpace) {
  let readingDisk = false;
  async function refreshDisk() {
    if (readingDisk) return;
    readingDisk = true;
    const button = $('#refresh-disk'); button.disabled = true;
    try {
      const disks = await window.desktop.diskSpace();
      const summary = $('#disk-summary'); summary.replaceChildren();
      for (const disk of disks) {
        const row = node('div', 'disk-volume');
        row.append(node('h3', '', disk.label));
        if (disk.unavailable) { row.append(node('p', '', 'Disque indisponible. Rebranchez-le puis actualisez.')); summary.append(row); continue; }
        const values = node('div', 'disk-values');
        for (const [label, value] of [['Disponible', disk.available], ['Utilisé', disk.used], ['Capacité totale', disk.total]]) {
          const item = node('div'); item.append(node('span', 'muted', label), node('strong', '', bytes(value))); values.append(item);
        }
        row.append(values);
        const meter = node('progress'); meter.max = disk.total; meter.value = disk.used + disk.reserved;
        meter.setAttribute('aria-label', 'Espace occupé ou réservé sur le disque'); row.append(meter);
        if (disk.reserved) row.append(node('p', 'field-help', `${bytes(disk.reserved)} supplémentaires sont réservés par le système de fichiers.`));
        if (disk.available / disk.total < .1 || disk.available < 1024 ** 3) row.append(node('p', 'disk-warning', 'Espace disponible faible. Vérifiez la place nécessaire avant de télécharger.'));
        summary.append(row);
      }
      $('#disk-updated').textContent = `Actualisé à ${new Date().toLocaleTimeString(currentLanguage())} · mise à jour toutes les 10 secondes.`;
    } catch { $('#disk-summary').textContent = 'Impossible de lire l’espace disque. Réessayez avec Actualiser.'; }
    finally { readingDisk = false; button.disabled = false; }
  }
  $('#refresh-disk').onclick = refreshDisk;
  refreshDisk();
  setInterval(() => { if (!document.hidden) refreshDisk(); }, 10000);
  document.addEventListener('visibilitychange', () => { if (!document.hidden) refreshDisk(); });
}
