/* ============================================================================
   Vocal · Voice Studio — studio.vocal.ch
   Console dédiée à la création, l'enregistrement, la génération et le clonage
   de voix synthétiques pour les lignes téléphoniques et les bots vocaux Vocal.
   Backend partagé : api.vocal.ch (worker ch-vocal-api).
   ============================================================================ */

const { useState, useEffect, useRef, useCallback, useMemo, createContext, useContext, Fragment } = React;

const CONFIG = {
    API_URL: 'https://api.vocal.ch',
    MY_URL: 'https://my.vocal.ch',
    BOTS_URL: 'https://bots.vocal.ch',
    DEV_URL: 'https://developers.vocal.ch',
};

// ==================== CROSS-SUBDOMAIN SSO ====================
// Si on arrive depuis my.vocal.ch ou bots.vocal.ch avec un token, on l'adopte.
(() => {
    try {
        const params = new URLSearchParams(window.location.search);
        const tok = params.get('impersonate_token') || params.get('token');
        if (tok) {
            localStorage.setItem('vocal_my_token', tok);
            const userB64 = params.get('impersonate_user') || params.get('user');
            if (userB64) {
                try {
                    const u = JSON.parse(decodeURIComponent(escape(atob(userB64))));
                    localStorage.setItem('vocal_my_user', JSON.stringify(u));
                } catch (e) {}
            }
            sessionStorage.setItem('vocal_my_impersonating', '1');
            const clean = window.location.pathname + window.location.hash;
            window.history.replaceState(null, '', clean);
        }
    } catch (e) {}
})();

// ==================== CONSTANTS ====================
const LANGS = [
    { id: 'fr', label: 'Français' },
    { id: 'de', label: 'Deutsch' },
    { id: 'en', label: 'English' },
    { id: 'it', label: 'Italiano' },
];

const DEFAULT_OPENAI_VOICES = [
    { id: 'alloy',   label: 'Alloy',   gender: 'neutral', tone: 'neutre & polyvalent' },
    { id: 'echo',    label: 'Echo',    gender: 'male',    tone: 'masculin chaleureux' },
    { id: 'fable',   label: 'Fable',   gender: 'neutral', tone: 'narratif' },
    { id: 'onyx',    label: 'Onyx',    gender: 'male',    tone: 'masculin grave' },
    { id: 'nova',    label: 'Nova',    gender: 'female',  tone: 'féminin énergique' },
    { id: 'shimmer', label: 'Shimmer', gender: 'female',  tone: 'féminin doux' },
];

const PRESETS = [
    { id: 'accueil',    iconKey: 'Hand',  label: 'Accueil',    text: "Bonjour et bienvenue chez {ENTREPRISE}. Pour rejoindre les ventes, dites « ventes ». Pour le support, dites « support ». Pour parler à un conseiller, dites « conseiller »." },
    { id: 'vacances',   iconKey: 'Sun',   label: 'Vacances',  text: "Notre bureau est fermé du {DATE_DEBUT} au {DATE_FIN}. Laissez votre nom, numéro et message après le bip, nous vous rappellerons dès notre retour." },
    { id: 'horaires',   iconKey: 'Clock', label: 'Hors horaires', text: "Bonjour, nos bureaux sont actuellement fermés. Nos horaires sont du lundi au vendredi de 8h à 18h. Vous pouvez laisser un message ou rappeler ultérieurement." },
    { id: 'transfert',  iconKey: 'ChevronRight', label: 'Transfert', text: "Merci de patienter, nous vous mettons en relation avec un conseiller." },
    { id: 'voicemail',  iconKey: 'Mail',         label: 'Voicemail', text: "Vous êtes bien sur le répondeur de {ENTREPRISE}. Laissez votre message après le bip et nous vous rappellerons dans les meilleurs délais." },
    { id: 'attente',    iconKey: 'Clock',        label: 'Attente',   text: "Tous nos conseillers sont actuellement occupés. Votre appel est important, merci de patienter quelques instants." },
    { id: 'remerciement', iconKey: 'Heart',      label: 'Merci',     text: "Merci pour votre appel. À très bientôt chez {ENTREPRISE}." },
    { id: 'evenement',  iconKey: 'Confetti',     label: 'Événement', text: "Pour notre soirée portes ouvertes du {DATE}, n'hésitez pas à venir nous rendre visite, nous vous accueillerons avec plaisir." },
];

// Démo marketplace voix premium (en attendant /my/voice-marketplace).
const MARKETPLACE_VOICES = [
    { id: 'mkt_so', name: 'Sophie · Romande',  creator: 'Studio Léman',     desc: 'Voix féminine douce, accent romand, médical/beauté.', tags: ['fr', 'féminin', 'doux'],     gender: 'female', price_chf: 19, samples: 2400 },
    { id: 'mkt_ma', name: 'Markus · Bern',     creator: 'Voiceworks GmbH',  desc: 'Voix masculine grave, Schwizerdütsch berniñois.',     tags: ['de', 'masculin', 'grave'],   gender: 'male',   price_chf: 24, samples: 1700 },
    { id: 'mkt_lu', name: 'Luca · Ticino',     creator: 'Studio Tre',       desc: 'Voix masculine chaleureuse, italien tessinois.',       tags: ['it', 'masculin', 'chaud'],   gender: 'male',   price_chf: 22, samples: 980 },
    { id: 'mkt_em', name: 'Emma · Geneva',     creator: 'Lac Voice',        desc: 'Voix féminine premium, accent genevois pro.',          tags: ['fr', 'féminin', 'premium'], gender: 'female', price_chf: 29, samples: 3200 },
    { id: 'mkt_ha', name: 'Hans · Zürich',     creator: 'Voiceworks GmbH',  desc: 'Voix masculine corporate, Schwizerdütsch zurichois.',  tags: ['de', 'masculin', 'corporate'], gender: 'male', price_chf: 26, samples: 2110 },
    { id: 'mkt_le', name: 'Léa · Vaud',        creator: 'Vocal Originals',  desc: 'Voix féminine jeune dynamique, e-commerce romand.',    tags: ['fr', 'féminin', 'jeune'],   gender: 'female', price_chf: 21, samples: 1450 },
    { id: 'mkt_an', name: 'Antonio · Ticino',  creator: 'Studio Tre',       desc: 'Voix masculine narrative, italien standard.',          tags: ['it', 'masculin', 'narratif'], gender: 'male', price_chf: 23, samples: 720 },
    { id: 'mkt_ka', name: 'Karin · Basel',     creator: 'Voiceworks GmbH',  desc: 'Voix féminine pro, Hochdeutsch CH.',                   tags: ['de', 'féminin', 'pro'],     gender: 'female', price_chf: 27, samples: 1890 },
];

// Scripts de référence pour le recorder studio (entraînement clone).
const RECORDER_SCRIPTS = {
    fr: [
        "Bonjour, vous êtes bien chez Vocal. Comment puis-je vous aider aujourd'hui ?",
        "Notre cabinet est ouvert du lundi au vendredi, de huit heures à dix-huit heures.",
        "Pour prendre rendez-vous, je vous propose le mardi quinze octobre à quatorze heures trente.",
        "Pouvez-vous me donner votre nom, votre numéro de téléphone et la raison de votre appel s'il vous plaît ?",
        "Merci beaucoup pour votre appel. Je vous souhaite une excellente journée.",
        "Un, deux, trois, quatre, cinq, six, sept, huit, neuf, dix.",
        "Le sept janvier deux mille vingt-six à quinze heures quarante-cinq précises.",
        "L'adresse est : avenue de Mont-Blanc, vingt-quatre, à Genève, mille deux cent un.",
    ],
    de: [
        "Guten Tag, hier ist Vocal. Wie kann ich Ihnen heute helfen?",
        "Unser Büro ist von Montag bis Freitag, von acht bis achtzehn Uhr geöffnet.",
        "Ich schlage Ihnen einen Termin am Dienstag, dem fünfzehnten Oktober um vierzehn Uhr dreißig vor.",
        "Eins, zwei, drei, vier, fünf, sechs, sieben, acht, neun, zehn.",
        "Vielen Dank für Ihren Anruf. Ich wünsche Ihnen einen schönen Tag.",
    ],
    en: [
        "Hello, you've reached Vocal. How may I help you today?",
        "Our office is open Monday to Friday, from eight a.m. to six p.m.",
        "I'd like to suggest Tuesday October fifteenth at two thirty p.m. for your appointment.",
        "One, two, three, four, five, six, seven, eight, nine, ten.",
        "Thank you very much for your call. Have a great day.",
    ],
    it: [
        "Buongiorno, qui Vocal. Come posso aiutarla oggi?",
        "Il nostro ufficio è aperto dal lunedì al venerdì, dalle otto alle diciotto.",
        "Le propongo un appuntamento martedì quindici ottobre alle quattordici e trenta.",
        "Uno, due, tre, quattro, cinque, sei, sette, otto, nove, dieci.",
        "Grazie mille per la sua chiamata. Le auguro una buona giornata.",
    ],
};

// Démo dictionnaire de prononciation (en attendant l'endpoint persistant).
const DEFAULT_PRONUNCIATIONS = [
    { term: 'Vocal',    repl: 'vauKal',     lang: 'fr', note: 'Marque' },
    { term: 'Swisscom', repl: 'swisskom',   lang: 'fr', note: 'Opérateur' },
    { term: 'CHF',      repl: 'francs suisses', lang: 'fr', note: 'Devise' },
    { term: 'iOS',      repl: 'aïoss',      lang: 'fr', note: 'Plateforme' },
    { term: 'API',      repl: 'a-pi-i',     lang: 'fr', note: 'Sigle technique' },
];

// ==================== FORMATTERS ====================
function fmtDate(d) { if (!d) return '-'; return new Date(d).toLocaleDateString('fr-CH', { day: '2-digit', month: '2-digit', year: 'numeric' }); }
function fmtDateTime(d) { if (!d) return '-'; const x = new Date(d); return `${String(x.getDate()).padStart(2,'0')}/${String(x.getMonth()+1).padStart(2,'0')}/${x.getFullYear()} ${String(x.getHours()).padStart(2,'0')}:${String(x.getMinutes()).padStart(2,'0')}`; }
function fmtCurrency(amount, currency = 'CHF') { if (amount === null || amount === undefined || Number.isNaN(amount)) return '-'; return `${Number(amount).toFixed(2)} ${currency}`; }
function fmtBytes(b) { if (!b) return '-'; if (b < 1024) return `${b} B`; if (b < 1024 * 1024) return `${(b/1024).toFixed(1)} KB`; return `${(b/1024/1024).toFixed(2)} MB`; }
function fmtMs(ms) {
    const s = Math.floor(ms / 1000); const mm = Math.floor(s / 60); const ss = s % 60;
    return `${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
}
function fmtRelative(d) {
    if (!d) return '-';
    const diff = (Date.now() - new Date(d).getTime()) / 1000;
    if (diff < 60) return 'à l\'instant';
    if (diff < 3600) return `il y a ${Math.floor(diff / 60)} min`;
    if (diff < 86400) return `il y a ${Math.floor(diff / 3600)} h`;
    if (diff < 604800) return `il y a ${Math.floor(diff / 86400)} j`;
    return fmtDate(d);
}

// ==================== ICONS ====================
const Icon = (path, viewBox = '0 0 24 24') => ({ className = 'w-5 h-5' }) => (
    <svg xmlns="http://www.w3.org/2000/svg" className={className} fill="none" viewBox={viewBox} stroke="currentColor" strokeWidth="2">
        {path}
    </svg>
);
const Icons = {
    Mic:        Icon(<><path strokeLinecap="round" strokeLinejoin="round" d="M19 11a7 7 0 01-14 0m7 7v3m-4 0h8M12 4a3 3 0 00-3 3v4a3 3 0 006 0V7a3 3 0 00-3-3z"/></>),
    MicOff:     Icon(<><path strokeLinecap="round" strokeLinejoin="round" d="M3 3l18 18M9 9v3a3 3 0 005.12 2.12M15 9.34V7a3 3 0 00-5.94-.6m6.94 6.06A7 7 0 0119 11M5 11a7 7 0 002.95 5.71M12 18v3m-4 0h8"/></>),
    Sparkles:   Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"/>),
    Settings:   Icon(<><path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></>),
    Library:    Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 10h16M4 14h10M4 18h6"/>),
    Beaker:     Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 9a4 4 0 01-8 0L8 4z"/>),
    Store:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M3 9l1-4h16l1 4M3 9v10a1 1 0 001 1h16a1 1 0 001-1V9M3 9h18M9 21V12m6 9V12"/>),
    Book:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>),
    Plug:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l-7 7 5 5 7-7m-5-5l5 5m-9 4l4 4M19 4l-2 2m4-1l-3 3"/>),
    Coin:       Icon(<><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor"/><path strokeLinecap="round" strokeLinejoin="round" d="M12 7v10m4-7H10a2 2 0 100 4h4a2 2 0 110 4H8"/></>),
    Play:       Icon(<><path strokeLinecap="round" strokeLinejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor"/></>),
    Stop:       Icon(<rect x="6" y="6" width="12" height="12" rx="2" fill="currentColor" stroke="none"/>),
    Pause:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M10 9v6m4-6v6"/>),
    Refresh:    Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>),
    Plus:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4"/>),
    Trash:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>),
    Copy:       Icon(<><rect x="9" y="9" width="11" height="11" rx="2" fill="none" stroke="currentColor"/><path strokeLinecap="round" strokeLinejoin="round" d="M5 15V5a2 2 0 012-2h10"/></>),
    Download:   Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 11l5 5m0 0l5-5m-5 5V4"/>),
    Upload:     Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>),
    Search:     Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>),
    Menu:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16"/>),
    LogOut:     Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>),
    Check:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7"/>),
    X:          Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12"/>),
    External:   Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>),
    Wave:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M3 12h2m4-7v14m4-10v6m4-9v12m4-7v2"/>),
    Lightning:  Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>),
    Robot:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M12 2v3m-6 3h12a2 2 0 012 2v8a2 2 0 01-2 2H6a2 2 0 01-2-2v-8a2 2 0 012-2zm3 4h6m-6 4h.01m5.99 0H15M9 18h6"/>),
    Phone:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>),
    ChevronLeft:  Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7"/>),
    ChevronRight: Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7"/>),
    Heart:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M4.318 6.318a4.5 4.5 0 016.364 0L12 7.636l1.318-1.318a4.5 4.5 0 116.364 6.364L12 20.364l-7.682-7.682a4.5 4.5 0 010-6.364z"/>),
    Star:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>),
    Eye:        Icon(<><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></>),
    Headset:    Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M3 14a9 9 0 0118 0v3a2 2 0 01-2 2h-1v-5h3M3 14v3a2 2 0 002 2h1v-5H3"/>),
    Bolt:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>),
    Edit:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>),
    Cart:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/>),
    Hand:       Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M7 11V6a2 2 0 014 0v5m0 0V4a2 2 0 014 0v7m0 0V6a2 2 0 014 0v9a7 7 0 01-7 7H10a7 7 0 01-7-7v-2a2 2 0 014 0v1"/>),
    Sun:        Icon(<><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor"/><path strokeLinecap="round" strokeLinejoin="round" d="M12 2v2M12 20v2M2 12h2M20 12h2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></>),
    Clock:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>),
    Mail:       Icon(<><path strokeLinecap="round" strokeLinejoin="round" d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><path strokeLinecap="round" strokeLinejoin="round" d="M22 6l-10 7L2 6"/></>),
    Confetti:   Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M11 4l1.5 3L16 8l-3 1.5L11 13l-1.5-3.5L6 8l3.5-1zm8 7l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2zM4 14l1 2 2 1-2 1-1 2-1-2-2-1 2-1 1-2z"/>),
    Sliders:    Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h10M14 6a3 3 0 106 0M20 6h0M4 12h2M6 12a3 3 0 106 0M12 12h8M4 18h14M18 18a3 3 0 106 0M20 18h0"/>),
    Users:      Icon(<path strokeLinecap="round" strokeLinejoin="round" d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zm14 10v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>),
};

// ==================== API ====================
const api = {
    token: null,
    async req(path, options = {}) {
        const headers = { 'Content-Type': 'application/json', ...options.headers };
        if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
        try {
            const resp = await fetch(`${CONFIG.API_URL}${path}`, { ...options, headers });
            const data = await resp.json().catch(() => ({}));
            if (!resp.ok && !data.error) data.error = `HTTP ${resp.status}`;
            data._status = resp.status;
            return data;
        } catch (e) {
            return { error: 'Erreur réseau', _status: 0 };
        }
    },
    get(path) { return this.req(path); },
    post(path, body) { return this.req(path, { method: 'POST', body: JSON.stringify(body || {}) }); },
    put(path, body) { return this.req(path, { method: 'PUT', body: JSON.stringify(body || {}) }); },
    del(path) { return this.req(path, { method: 'DELETE' }); },
    // Upload binaire (recorder → R2). Endpoint à brancher : POST /my/voice-clones/upload-sample
    async uploadBlob(path, blob, filename) {
        const headers = {};
        if (this.token) headers['Authorization'] = `Bearer ${this.token}`;
        const fd = new FormData();
        fd.append('file', blob, filename || 'recording.webm');
        try {
            const resp = await fetch(`${CONFIG.API_URL}${path}`, { method: 'POST', headers, body: fd });
            const data = await resp.json().catch(() => ({}));
            if (!resp.ok && !data.error) data.error = `HTTP ${resp.status}`;
            return data;
        } catch (e) {
            return { error: 'Erreur réseau' };
        }
    },
};

// ==================== NOTIFICATIONS ====================
const useNotifications = () => {
    const show = useCallback((message, type = 'info') => {
        const el = document.createElement('div');
        el.style.cssText = 'position:fixed;bottom:1.5rem;right:1.5rem;z-index:9999;padding:0.85rem 1.2rem;border-radius:12px;font-size:0.85rem;font-weight:600;color:#fff;box-shadow:0 12px 32px rgba(15,23,42,0.18);max-width:360px;font-family:Plus Jakarta Sans,sans-serif;animation:fadeIn 0.2s ease;';
        const colors = { success: '#16a34a', error: '#dc2626', info: '#6366f1', warning: '#ea580c' };
        el.style.background = colors[type] || colors.info;
        el.textContent = message;
        document.body.appendChild(el);
        setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(8px)'; el.style.transition = 'all 0.25s'; setTimeout(() => el.remove(), 280); }, 3500);
    }, []);
    return useMemo(() => ({
        success: (m) => show(m, 'success'),
        error:   (m) => show(m, 'error'),
        info:    (m) => show(m, 'info'),
        warning: (m) => show(m, 'warning'),
    }), [show]);
};

// ==================== ROUTING ====================
const VIEWS = ['composer', 'library', 'recorder', 'voices', 'abtest', 'marketplace', 'pronunciation', 'wiring', 'costs', 'settings'];
function parseHash() {
    const h = (window.location.hash || '').replace('#', '');
    return { view: VIEWS.includes(h) ? h : 'composer' };
}

// ==================== CONTEXT ====================
const AppContext = createContext();
const useApp = () => useContext(AppContext);

const AppProvider = ({ children }) => {
    const [user, setUser] = useState(() => {
        const saved = localStorage.getItem('vocal_my_user');
        const token = localStorage.getItem('vocal_my_token');
        if (saved && token) { api.token = token; try { return { ...JSON.parse(saved), token }; } catch { return null; } }
        return null;
    });
    const [route, setRoute] = useState(parseHash);
    const [sidebarOpen, setSidebarOpen] = useState(false);
    const notify = useNotifications();

    useEffect(() => {
        const onHash = () => setRoute(parseHash());
        window.addEventListener('hashchange', onHash);
        return () => window.removeEventListener('hashchange', onHash);
    }, []);

    const navigate = useCallback((view) => {
        setRoute({ view });
        setSidebarOpen(false);
        try { window.history.pushState(null, '', `#${view}`); } catch (e) {}
    }, []);

    const login = useCallback((userData, token) => {
        api.token = token;
        setUser({ ...userData, token });
        localStorage.setItem('vocal_my_user', JSON.stringify(userData));
        localStorage.setItem('vocal_my_token', token);
        notify.success('Connexion réussie');
    }, [notify]);

    const logout = useCallback(() => {
        api.post('/auth/logout').catch(() => {});
        api.token = null;
        setUser(null);
        localStorage.removeItem('vocal_my_user');
        localStorage.removeItem('vocal_my_token');
        notify.info('Déconnexion réussie');
    }, [notify]);

    const value = useMemo(() => ({
        user, setUser, route, navigate, login, logout, sidebarOpen, setSidebarOpen, notify,
    }), [user, route, navigate, login, logout, sidebarOpen, notify]);

    return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

// ==================== LOGIN ====================
const LoginScreen = () => {
    const { login } = useApp();
    const [email, setEmail] = useState('');
    const [code, setCode] = useState('');
    const [step, setStep] = useState('email');
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState('');

    useEffect(() => {
        const params = new URLSearchParams(window.location.search);
        const magicEmail = params.get('email');
        const magicCode = params.get('code');
        if (magicEmail && magicCode) {
            window.history.replaceState({}, '', window.location.pathname);
            setLoading(true);
            fetch(`${CONFIG.API_URL}/auth/verify-code`, {
                method: 'POST', headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ email: magicEmail, code: magicCode }),
            }).then(r => r.json()).then(data => {
                if (data.success && data.token) login(data.user || { email: magicEmail }, data.token);
                else { setEmail(magicEmail); setStep('code'); setError(data.error || 'Lien expiré'); }
            }).catch(() => { setEmail(magicEmail); setStep('code'); setError('Erreur réseau'); })
              .finally(() => setLoading(false));
        }
    }, []);

    const handleSendCode = async (e) => {
        e.preventDefault(); if (!email) return;
        setLoading(true); setError('');
        try {
            const r = await fetch(`${CONFIG.API_URL}/auth/request-code`, {
                method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }),
            }).then(r => r.json());
            if (r.success) setStep('code');
            else setError(r.error || 'Erreur');
        } catch (e) { setError('Erreur réseau'); }
        finally { setLoading(false); }
    };
    const handleVerify = async (e) => {
        e.preventDefault(); if (!code) return;
        setLoading(true); setError('');
        try {
            const r = await fetch(`${CONFIG.API_URL}/auth/verify-code`, {
                method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, code }),
            }).then(r => r.json());
            if (r.success && r.token) login(r.user || { email }, r.token);
            else setError(r.error || 'Code invalide');
        } catch (e) { setError('Erreur réseau'); }
        finally { setLoading(false); }
    };

    return (
        <div className="login-split">
            <div className="login-form-side">
                <div className="login-form-inner">
                    <div className="login-brand">
                        <img src="assets/img/vocal-logotype.svg" alt="Vocal" />
                        <span className="pill">Voice Studio</span>
                    </div>
                    <h1 className="login-title">{step === 'email' ? 'Créez vos voix synthétiques' : 'Vérifiez votre email'}</h1>
                    <p className="login-subtitle">
                        {step === 'email'
                            ? 'Recevez un code de connexion sécurisé à usage unique. Aucun mot de passe à gérer.'
                            : `Un code à 6 chiffres a été envoyé à ${email}.`}
                    </p>
                    {error && <div className="login-error">{error}</div>}
                    {step === 'email' ? (
                        <form onSubmit={handleSendCode} className="vstack gap-lg">
                            <div className="form-row">
                                <label>Email</label>
                                <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="vous@entreprise.ch" required autoFocus />
                            </div>
                            <button type="submit" className="btn btn-primary" disabled={loading || !email} style={{ width: '100%' }}>
                                {loading ? 'Envoi…' : 'Recevoir le code'}
                            </button>
                            <div className="text-xs text-muted" style={{ textAlign: 'center' }}>
                                Vous avez déjà un compte sur <a href={CONFIG.MY_URL}>my.vocal.ch</a> ? Utilisez la même adresse.
                            </div>
                        </form>
                    ) : (
                        <form onSubmit={handleVerify} className="vstack gap-lg">
                            <div className="form-row">
                                <label>Code reçu par email</label>
                                <input type="text" inputMode="numeric" maxLength={6} value={code} onChange={e => setCode(e.target.value.replace(/\D/g, ''))} placeholder="123456" required autoFocus style={{ letterSpacing: '0.6em', textAlign: 'center', fontSize: '1.25rem', fontWeight: 700 }} />
                            </div>
                            <button type="submit" className="btn btn-primary" disabled={loading || code.length < 4} style={{ width: '100%' }}>
                                {loading ? 'Connexion…' : 'Se connecter'}
                            </button>
                            <button type="button" className="btn btn-ghost btn-sm" onClick={() => { setStep('email'); setCode(''); setError(''); }}>← Changer d'email</button>
                        </form>
                    )}
                </div>
            </div>
            <aside className="login-aside">
                <div className="login-aside-inner">
                    <h2>Le studio dédié à vos voix synthétiques</h2>
                    <p>Générez, enregistrez, clonez et comparez vos voix pour vos lignes téléphoniques et vos bots vocaux Vocal.</p>
                    <div className="feature-mini-grid">
                        <div className="feature-mini"><div className="ico"><Icons.Mic className="w-5 h-5" /></div><div className="t">TTS on-demand</div><div className="d">Texte → MP3 prêt à diffuser</div></div>
                        <div className="feature-mini"><div className="ico"><Icons.Sliders className="w-5 h-5" /></div><div className="t">Recorder browser</div><div className="d">Enregistrez vos échantillons</div></div>
                        <div className="feature-mini"><div className="ico"><Icons.Users className="w-5 h-5" /></div><div className="t">Voice cloning</div><div className="d">ElevenLabs &amp; OpenAI</div></div>
                        <div className="feature-mini"><div className="ico"><Icons.Beaker className="w-5 h-5" /></div><div className="t">A/B compare</div><div className="d">Choisissez la voix la + naturelle</div></div>
                        <div className="feature-mini"><div className="ico"><Icons.Cart className="w-5 h-5" /></div><div className="t">Marketplace voix</div><div className="d">Voix premium curées Vocal</div></div>
                        <div className="feature-mini"><div className="ico"><Icons.Star className="w-5 h-5" /></div><div className="t">Hébergement Suisse</div><div className="d">Cloudflare CH, RGPD/nLPD</div></div>
                    </div>
                </div>
            </aside>
        </div>
    );
};

// ==================== SHARED COMPONENTS ====================
const PageHeader = ({ title, subtitle, actions }) => {
    const { setSidebarOpen } = useApp();
    return (
        <div className="page-header">
            <div className="page-header-left">
                <button className="menu-toggle" onClick={() => setSidebarOpen(true)} aria-label="Menu">
                    <Icons.Menu />
                </button>
                <div>
                    <h1 className="page-title">{title}</h1>
                    {subtitle && <p className="page-subtitle">{subtitle}</p>}
                </div>
            </div>
            {actions && <div className="page-actions">{actions}</div>}
        </div>
    );
};

const SoonBanner = ({ endpoint, children }) => (
    <div className="soon-banner">
        <Icons.Lightning className="w-5 h-5" />
        <div>
            <b>Aperçu produit.</b> {children || 'Cette section affiche l\'UX cible. '}
            {endpoint && <>Pour activer le mode live, branchez l'endpoint <code>{endpoint}</code> côté <code>ch-vocal-api</code>.</>}
        </div>
    </div>
);

const EmptyState = ({ icon: I = Icons.Sparkles, title, desc, action }) => (
    <div className="empty-state">
        <div className="empty-state-icon"><I className="w-8 h-8" /></div>
        <h3 className="empty-state-title">{title}</h3>
        {desc && <p className="empty-state-desc">{desc}</p>}
        {action}
    </div>
);

const Spinner = ({ size = 24 }) => (
    <span style={{ display: 'inline-block', width: size, height: size, border: '3px solid var(--border)', borderTopColor: 'var(--primary)', borderRadius: '50%', animation: 'spin 0.75s linear infinite' }} />
);

const VoiceAvatar = ({ name, gender }) => {
    const initials = (name || '?').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
    const cls = gender === 'female' ? 'female' : gender === 'male' ? 'male' : 'neutral';
    return <div className={`voice-avatar ${cls}`}>{initials}</div>;
};

const copyToClipboard = (text, notify) => {
    if (navigator.clipboard) {
        navigator.clipboard.writeText(text).then(
            () => notify?.success?.('Copié dans le presse-papier'),
            () => notify?.error?.('Impossible de copier')
        );
    } else {
        notify?.error?.('Presse-papier non disponible');
    }
};

// ==================== SIDEBAR ====================
const NAV = [
    { id: 'composer',     label: 'Composer (TTS)',       icon: Icons.Sparkles, section: 'create' },
    { id: 'recorder',     label: 'Recorder studio',      icon: Icons.Mic,      section: 'create' },
    { id: 'library',      label: 'Bibliothèque audio',   icon: Icons.Library,  section: 'create' },

    { id: 'voices',       label: 'Mes voix (clones)',    icon: Icons.Heart,    section: 'voices' },
    { id: 'marketplace',  label: 'Marketplace voix',     icon: Icons.Store,    section: 'voices' },
    { id: 'abtest',       label: 'A/B compare',          icon: Icons.Beaker,   section: 'voices' },

    { id: 'pronunciation', label: 'Prononciation',       icon: Icons.Book,     section: 'fine'  },
    { id: 'wiring',        label: 'Branchements',        icon: Icons.Plug,     section: 'fine'  },
    { id: 'costs',         label: 'Coûts & quotas',      icon: Icons.Coin,     section: 'fine'  },

    { id: 'settings',     label: 'Réglages',             icon: Icons.Settings, section: 'compte' },
];
const NAV_SECTIONS = {
    create: 'CRÉATION',
    voices: 'VOIX',
    fine:   'AFFINAGE',
    compte: 'COMPTE',
};

const Sidebar = () => {
    const { user, route, navigate, logout, sidebarOpen, setSidebarOpen } = useApp();
    const grouped = Object.entries(NAV_SECTIONS).map(([key, label]) => ({
        label, items: NAV.filter(i => i.section === key),
    }));

    return (
        <>
            {sidebarOpen && <div className="sidebar-overlay" onClick={() => setSidebarOpen(false)} />}
            <aside className={`sidebar ${sidebarOpen ? 'open' : ''}`}>
                <div className="sidebar-header">
                    <img src="assets/img/vocal-logotype.svg" alt="Vocal" className="logo" />
                    <span className="studio-pill">Voice</span>
                </div>

                <div className="sidebar-context">
                    <div className="sidebar-context-name">
                        <Icons.Mic className="w-4 h-4" /> Voice Studio
                    </div>
                    <div className="sidebar-context-status">
                        <span className="dot live" /> Connecté à api.vocal.ch
                    </div>
                </div>

                <nav className="sidebar-nav">
                    {grouped.map(group => (
                        <div className="nav-section" key={group.label}>
                            <div className="nav-section-title">{group.label}</div>
                            {group.items.map(item => (
                                <button key={item.id}
                                    className={`nav-item ${route.view === item.id ? 'active' : ''}`}
                                    onClick={() => navigate(item.id)}>
                                    <item.icon className="nav-icon" />
                                    {item.label}
                                </button>
                            ))}
                        </div>
                    ))}

                    <div className="nav-section">
                        <div className="nav-section-title">RACCOURCIS</div>
                        <a href={CONFIG.MY_URL} target="_blank" rel="noopener" className="nav-item">
                            <Icons.External className="nav-icon" /> my.vocal.ch
                        </a>
                        <a href={CONFIG.BOTS_URL} target="_blank" rel="noopener" className="nav-item">
                            <Icons.Robot className="nav-icon" /> Bot Studio
                        </a>
                    </div>
                </nav>

                <div className="sidebar-footer">
                    <div className="user-info">
                        <div className="user-avatar">{(user?.name || user?.email || 'U')[0].toUpperCase()}</div>
                        <div className="user-details">
                            <div className="user-name">{user?.name || user?.client?.name || 'Mon compte'}</div>
                            <div className="user-role">{user?.email}</div>
                        </div>
                        <button onClick={logout} title="Déconnexion" className="btn-icon" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)' }}>
                            <Icons.LogOut className="w-4 h-4" />
                        </button>
                    </div>
                </div>
            </aside>
        </>
    );
};

/* ============================================================================
   VIEW : COMPOSER (TTS hero workflow)
   ============================================================================ */
const ComposerView = () => {
    const { notify } = useApp();
    const [text, setText] = useState('');
    const [label, setLabel] = useState('');
    const [provider, setProvider] = useState('openai');
    const [model, setModel] = useState('tts-1');
    const [voice, setVoice] = useState('alloy');
    const [voiceCloneId, setVoiceCloneId] = useState('');
    const [language, setLanguage] = useState('fr');
    const [format, setFormat] = useState('mp3');
    const [speed, setSpeed] = useState(1.0);
    const [voicesData, setVoicesData] = useState({ openai: { voices: [], models: [] }, elevenlabs: { available: false }, custom: [] });
    const [generating, setGenerating] = useState(false);
    const [lastGenerated, setLastGenerated] = useState(null);

    useEffect(() => {
        api.get('/my/tts/voices').then(r => { if (r && !r.error) setVoicesData(r); });
    }, []);

    const charCount = text.length;
    const charClass = charCount > 3800 ? 'danger' : charCount > 3000 ? 'warn' : '';

    const generate = async () => {
        const txt = text.trim();
        if (!txt) return notify.error('Texte requis');
        if (txt.length > 4000) return notify.error('Texte trop long (max 4000 caractères)');
        setGenerating(true);
        try {
            const payload = {
                text: txt, label: label.trim() || txt.slice(0, 60),
                provider, model, language, format, speed: parseFloat(speed) || 1.0,
            };
            if (voiceCloneId) payload.voice_clone_id = parseInt(voiceCloneId);
            else payload.voice = voice;
            const r = await api.post('/my/tts', payload);
            if (r?.error) return notify.error(r.error + (r.detail ? ` — ${r.detail}` : ''));
            notify.success('Audio généré');
            setLastGenerated(r);
        } catch (e) {
            notify.error('Erreur : ' + e.message);
        } finally { setGenerating(false); }
    };

    const openaiVoices = (voicesData.openai?.voices || []).length ? voicesData.openai.voices : DEFAULT_OPENAI_VOICES.map(v => v.id);
    const openaiModels = (voicesData.openai?.models || []).length ? voicesData.openai.models : ['tts-1', 'tts-1-hd'];

    return (
        <>
            <PageHeader
                title="Composer"
                subtitle="Transformez du texte en MP3 prêt à diffuser. Réutilisable sur vos lignes (accueil, vacances, IVR, hold, voicemail) et vos bots."
            />
            <div className="page-content">
                <div className="composer-grid">
                    <div>
                        <div className="card">
                            <div className="card-body">
                                <label className="field-label">Texte à dire</label>
                                <textarea
                                    className="composer-textarea"
                                    value={text}
                                    onChange={e => setText(e.target.value)}
                                    rows={6}
                                    maxLength={4000}
                                    placeholder="Bonjour et bienvenue chez…"
                                />
                                <div className="composer-counter">
                                    <span>Variables : <code>{'{ENTREPRISE}'}</code>, <code>{'{DATE}'}</code>, <code>{'{NOM}'}</code></span>
                                    <span className={charClass}>{charCount}/4000</span>
                                </div>

                                <label className="field-label" style={{ marginTop: '1rem' }}>Modèles de message</label>
                                <div className="preset-row">
                                    {PRESETS.map(p => {
                                        const PIcon = Icons[p.iconKey] || Icons.Sparkles;
                                        return (
                                            <button key={p.id} className="preset-chip" onClick={() => {
                                                setText(p.text);
                                                if (!label) setLabel(p.label);
                                            }}>
                                                <PIcon className="w-4 h-4" /> {p.label}
                                            </button>
                                        );
                                    })}
                                </div>
                            </div>
                        </div>

                        {lastGenerated && lastGenerated.file_url && (
                            <div className="preview-card">
                                <div className="preview-card-title">
                                    <Icons.Check className="w-4 h-4" /> {lastGenerated.label || 'Audio généré'}
                                </div>
                                <audio controls src={lastGenerated.file_url} />
                                <div className="preview-card-actions">
                                    <code className="preview-card-url">{lastGenerated.file_url}</code>
                                    <button className="btn btn-secondary btn-sm" onClick={() => copyToClipboard(lastGenerated.file_url, notify)}>
                                        <Icons.Copy className="w-4 h-4" /> Copier
                                    </button>
                                    <a className="btn btn-secondary btn-sm" href={lastGenerated.file_url} download={`${lastGenerated.label || 'voice'}.${lastGenerated.format || format}`}>
                                        <Icons.Download className="w-4 h-4" /> Télécharger
                                    </a>
                                </div>
                                <div className="preview-card-meta">
                                    <span>{lastGenerated.char_count} caractères</span>
                                    <span>·</span>
                                    <span>{fmtCurrency(lastGenerated.cost_chf || 0)}</span>
                                    <span>·</span>
                                    <span>{fmtBytes(lastGenerated.file_size)}</span>
                                </div>
                            </div>
                        )}
                    </div>

                    <div className="composer-side">
                        <h3>Paramètres voix</h3>

                        <div>
                            <label className="field-label">Libellé</label>
                            <input className="field-control" value={label} onChange={e => setLabel(e.target.value)} placeholder="Ex : Accueil semaine" />
                        </div>

                        <div>
                            <label className="field-label">Langue</label>
                            <select className="field-control" value={language} onChange={e => setLanguage(e.target.value)}>
                                {LANGS.map(l => <option key={l.id} value={l.id}>{l.label}</option>)}
                            </select>
                        </div>

                        <div>
                            <label className="field-label">Voix custom (clone)</label>
                            <select className="field-control" value={voiceCloneId} onChange={e => {
                                setVoiceCloneId(e.target.value);
                                if (e.target.value) {
                                    const c = (voicesData.custom || []).find(v => String(v.id) === e.target.value);
                                    if (c) setProvider(c.provider);
                                }
                            }}>
                                <option value="">— Aucune (voix standard) —</option>
                                {(voicesData.custom || []).map(v => (
                                    <option key={v.id} value={v.id}>{v.label} ({v.provider})</option>
                                ))}
                            </select>
                        </div>

                        <div>
                            <label className="field-label">Fournisseur</label>
                            <select className="field-control" value={provider} disabled={!!voiceCloneId} onChange={e => {
                                setProvider(e.target.value);
                                if (e.target.value === 'elevenlabs') setModel('eleven_multilingual_v2');
                                else { setModel('tts-1'); setVoice('alloy'); }
                            }}>
                                <option value="openai">OpenAI (intégré)</option>
                                <option value="elevenlabs" disabled={!voicesData.elevenlabs?.available}>
                                    ElevenLabs {voicesData.elevenlabs?.available ? '' : '(non configuré)'}
                                </option>
                            </select>
                        </div>

                        {!voiceCloneId && provider === 'openai' && (
                            <>
                                <div>
                                    <label className="field-label">Voix</label>
                                    <select className="field-control" value={voice} onChange={e => setVoice(e.target.value)}>
                                        {openaiVoices.map(v => <option key={v} value={v}>{v}</option>)}
                                    </select>
                                </div>
                                <div>
                                    <label className="field-label">Modèle</label>
                                    <select className="field-control" value={model} onChange={e => setModel(e.target.value)}>
                                        {openaiModels.map(m => <option key={m} value={m}>{m}</option>)}
                                    </select>
                                </div>
                            </>
                        )}
                        {!voiceCloneId && provider === 'elevenlabs' && (
                            <div>
                                <label className="field-label">Voice ID ElevenLabs</label>
                                <input className="field-control" value={voice} onChange={e => setVoice(e.target.value)} placeholder="21m00Tcm4TlvDq8ikWAM" />
                                <div className="field-help">Voir la voice library ElevenLabs.</div>
                            </div>
                        )}

                        <div>
                            <label className="field-label">Format</label>
                            <select className="field-control" value={format} onChange={e => setFormat(e.target.value)}>
                                <option value="mp3">MP3</option>
                                <option value="wav">WAV</option>
                                <option value="ogg">OGG</option>
                            </select>
                        </div>

                        <div>
                            <div className="range-row">
                                <span className="field-label" style={{ marginBottom: 0 }}>Vitesse</span>
                                <span className="range-value">{Number(speed).toFixed(2)}x</span>
                            </div>
                            <input type="range" min="0.5" max="2" step="0.05" value={speed} onChange={e => setSpeed(e.target.value)} />
                        </div>

                        <button className="generate-btn-large" onClick={generate} disabled={generating || !text.trim()}>
                            <Icons.Sparkles className="w-5 h-5" />
                            {generating ? 'Génération…' : 'Générer le MP3'}
                        </button>
                        <div className="field-help" style={{ textAlign: 'center' }}>
                            Coût estimé : <b>{(charCount * 0.000015).toFixed(3)} CHF</b>
                        </div>
                    </div>
                </div>
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : LIBRARY (audio recordings)
   ============================================================================ */
const LibraryView = () => {
    const { notify, navigate } = useApp();
    const [items, setItems] = useState(null);
    const [search, setSearch] = useState('');
    const [filterLang, setFilterLang] = useState('');
    const [filterProvider, setFilterProvider] = useState('');

    const load = useCallback(async () => {
        const r = await api.get('/my/tts');
        if (r?.error) notify.error(r.error);
        else setItems(r.recordings || r.data || []);
    }, [notify]);

    useEffect(() => { load(); }, [load]);

    const remove = async (rec) => {
        if (!confirm(`Supprimer « ${rec.label || 'cet enregistrement'} » ?`)) return;
        const r = await api.del(`/my/tts/${rec.id}`);
        if (r?.error) notify.error(r.error);
        else { notify.success('Supprimé'); load(); }
    };

    const filtered = useMemo(() => {
        if (!items) return null;
        const q = search.trim().toLowerCase();
        return items.filter(i => {
            if (filterLang && i.language !== filterLang) return false;
            if (filterProvider && i.provider !== filterProvider) return false;
            if (q && !((i.label || '').toLowerCase().includes(q) || (i.text || '').toLowerCase().includes(q))) return false;
            return true;
        });
    }, [items, search, filterLang, filterProvider]);

    return (
        <>
            <PageHeader
                title="Bibliothèque audio"
                subtitle="Tous vos MP3 générés. Réutilisez-les sur vos lignes (accueil, vacances, IVR…) et vos bots."
                actions={<>
                    <button className="btn btn-secondary btn-sm" onClick={load}>
                        <Icons.Refresh className="w-4 h-4" /> Actualiser
                    </button>
                    <button className="btn btn-primary btn-sm" onClick={() => navigate('composer')}>
                        <Icons.Plus className="w-4 h-4" /> Nouveau MP3
                    </button>
                </>}
            />
            <div className="page-content">
                <div className="library-toolbar">
                    <input className="search-input" type="search" placeholder="Rechercher dans le libellé ou le texte…" value={search} onChange={e => setSearch(e.target.value)} />
                    <select className="search-input" style={{ flex: '0 0 130px' }} value={filterLang} onChange={e => setFilterLang(e.target.value)}>
                        <option value="">Toutes langues</option>
                        {LANGS.map(l => <option key={l.id} value={l.id}>{l.label}</option>)}
                    </select>
                    <select className="search-input" style={{ flex: '0 0 150px' }} value={filterProvider} onChange={e => setFilterProvider(e.target.value)}>
                        <option value="">Tous fournisseurs</option>
                        <option value="openai">OpenAI</option>
                        <option value="elevenlabs">ElevenLabs</option>
                    </select>
                </div>

                {filtered === null ? (
                    <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem 0' }}><Spinner size={32} /></div>
                ) : filtered.length === 0 ? (
                    <EmptyState
                        icon={Icons.Library}
                        title={items?.length ? 'Aucun résultat' : 'Bibliothèque vide'}
                        desc={items?.length ? 'Modifiez votre recherche ou vos filtres.' : 'Générez votre premier MP3 depuis le Composer.'}
                        action={<button className="btn btn-primary" onClick={() => navigate('composer')}>
                            <Icons.Sparkles className="w-4 h-4" /> Ouvrir le Composer
                        </button>}
                    />
                ) : (
                    <div className="library-list">
                        {filtered.map(rec => (
                            <div key={rec.id} className="library-item">
                                <div className="library-item-head">
                                    <div style={{ flex: 1, minWidth: 0 }}>
                                        <div className="library-item-title">{rec.label || '(sans libellé)'}</div>
                                        <div className="library-item-meta">
                                            <span className="voice-tag">{rec.provider}</span>
                                            {rec.voice && <span className="voice-tag">{rec.voice}</span>}
                                            {rec.language && <span className={`voice-tag ${rec.language}`}>{rec.language.toUpperCase()}</span>}
                                            <span className="meta-sep">·</span>
                                            <span>{rec.char_count || 0} car.</span>
                                            <span className="meta-sep">·</span>
                                            <span>{fmtCurrency(rec.cost_chf || 0)}</span>
                                            <span className="meta-sep">·</span>
                                            <span>{fmtRelative(rec.created_at)}</span>
                                        </div>
                                    </div>
                                    <div className="library-item-actions">
                                        <button className="btn btn-secondary btn-sm" onClick={() => copyToClipboard(rec.file_url, notify)} title="Copier l'URL"><Icons.Copy className="w-4 h-4" /></button>
                                        <a className="btn btn-secondary btn-sm" href={rec.file_url} download={`${rec.label || 'voice'}.${rec.format}`} title="Télécharger"><Icons.Download className="w-4 h-4" /></a>
                                        <button className="btn btn-secondary btn-sm" onClick={() => remove(rec)} title="Supprimer" style={{ color: 'var(--danger)' }}><Icons.Trash className="w-4 h-4" /></button>
                                    </div>
                                </div>
                                <audio controls src={rec.file_url} preload="none" />
                                {rec.text && (
                                    <details>
                                        <summary>Texte</summary>
                                        <p>{rec.text}</p>
                                    </details>
                                )}
                            </div>
                        ))}
                    </div>
                )}
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : VOICES — clones synthétiques (OpenAI / ElevenLabs)
   ============================================================================ */
const VoicesView = () => {
    const { notify, navigate } = useApp();
    const [items, setItems] = useState(null);
    const [showModal, setShowModal] = useState(false);
    const [draft, setDraft] = useState({ label: '', provider: 'elevenlabs', sample_url: '', language: 'fr', api_key: '' });
    const [saving, setSaving] = useState(false);

    const load = useCallback(async () => {
        const r = await api.get('/my/voice-clones');
        if (r?.error) notify.error(r.error);
        else setItems(r.voice_clones || r.clones || []);
    }, [notify]);

    useEffect(() => { load(); }, [load]);

    // Si on revient du Recorder avec un sample_url tout frais, pré-remplir le modal.
    useEffect(() => {
        try {
            const pending = localStorage.getItem('vocal_studio_pending_sample_url');
            if (pending) {
                setDraft(d => ({ ...d, sample_url: pending, label: d.label || `Clone ${new Date().toLocaleDateString('fr-CH')}` }));
                setShowModal(true);
                localStorage.removeItem('vocal_studio_pending_sample_url');
            }
        } catch (e) {}
    }, []);

    const create = async () => {
        if (!draft.label.trim() || !draft.sample_url.trim()) return notify.error('Libellé et sample_url requis');
        setSaving(true);
        try {
            const r = await api.post('/my/voice-clones', draft);
            if (r?.error) return notify.error(r.error);
            notify.success('Voix créée');
            setShowModal(false);
            setDraft({ label: '', provider: 'elevenlabs', sample_url: '', language: 'fr', api_key: '' });
            load();
        } finally { setSaving(false); }
    };

    const remove = async (v) => {
        if (!confirm(`Supprimer la voix « ${v.label} » ?`)) return;
        const r = await api.del(`/my/voice-clones/${v.id}`);
        if (r?.error) notify.error(r.error);
        else { notify.success('Supprimée'); load(); }
    };

    return (
        <>
            <PageHeader
                title="Mes voix"
                subtitle="Vos voix synthétiques personnelles. Clonez votre propre voix ou celle de votre marque via OpenAI ou ElevenLabs."
                actions={<>
                    <button className="btn btn-secondary btn-sm" onClick={() => navigate('recorder')}>
                        <Icons.Mic className="w-4 h-4" /> Enregistrer un échantillon
                    </button>
                    <button className="btn btn-secondary btn-sm" onClick={load}>
                        <Icons.Refresh className="w-4 h-4" /> Actualiser
                    </button>
                    <button className="btn btn-primary btn-sm" onClick={() => setShowModal(true)}>
                        <Icons.Plus className="w-4 h-4" /> Ajouter une voix
                    </button>
                </>}
            />
            <div className="page-content">
                {items === null ? (
                    <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem 0' }}><Spinner size={32} /></div>
                ) : items.length === 0 ? (
                    <EmptyState
                        icon={Icons.Heart}
                        title="Aucune voix custom"
                        desc="Créez votre première voix synthétique. Pointez vers un échantillon (mp3/wav) hébergé, ou enregistrez-en un avec le Recorder studio."
                        action={
                            <div className="hstack" style={{ justifyContent: 'center' }}>
                                <button className="btn btn-secondary" onClick={() => navigate('recorder')}>
                                    <Icons.Mic className="w-4 h-4" /> Enregistrer
                                </button>
                                <button className="btn btn-primary" onClick={() => setShowModal(true)}>
                                    <Icons.Plus className="w-4 h-4" /> Ajouter une voix
                                </button>
                            </div>
                        }
                    />
                ) : (
                    <div className="voice-grid">
                        {items.map(v => (
                            <div key={v.id} className="voice-card">
                                <div className="voice-card-head">
                                    <VoiceAvatar name={v.label} gender={v.gender} />
                                    <div style={{ flex: 1, minWidth: 0 }}>
                                        <div className="voice-card-name">{v.label}</div>
                                        <div className="voice-card-meta">
                                            {v.provider} · {v.language || '—'} · {fmtRelative(v.created_at)}
                                        </div>
                                    </div>
                                </div>
                                <div className="voice-tags">
                                    <span className={`voice-tag ${v.provider}`}>{v.provider}</span>
                                    {v.language && <span className={`voice-tag ${v.language}`}>{v.language.toUpperCase()}</span>}
                                    <span className={`voice-status ${v.status || 'pending'}`}>
                                        <span className="dot" />{v.status || 'pending'}
                                    </span>
                                </div>
                                {v.sample_url && (
                                    <div className="voice-card-player">
                                        <audio controls src={v.sample_url} style={{ width: '100%', height: 32 }} />
                                    </div>
                                )}
                                <div className="voice-card-actions">
                                    <button className="btn btn-secondary btn-sm" onClick={() => navigate('composer')} title="Utiliser dans Composer">
                                        <Icons.Sparkles className="w-4 h-4" /> Utiliser
                                    </button>
                                    <div className="spacer" />
                                    <button className="btn btn-secondary btn-sm" onClick={() => remove(v)} style={{ color: 'var(--danger)' }} title="Supprimer">
                                        <Icons.Trash className="w-4 h-4" />
                                    </button>
                                </div>
                            </div>
                        ))}
                    </div>
                )}

                {showModal && (
                    <div className="modal-backdrop" onClick={() => setShowModal(false)}>
                        <div className="modal" onClick={e => e.stopPropagation()}>
                            <div className="modal-header">
                                <h2>Nouvelle voix synthétique</h2>
                                <button className="btn-icon" onClick={() => setShowModal(false)}><Icons.X /></button>
                            </div>
                            <div className="modal-body">
                                <div className="field-stack">
                                    <div>
                                        <label className="field-label">Libellé *</label>
                                        <input className="field-control" value={draft.label} onChange={e => setDraft({ ...draft, label: e.target.value })} placeholder="Ex : Voix Marie (FR)" />
                                    </div>
                                    <div>
                                        <label className="field-label">Fournisseur</label>
                                        <select className="field-control" value={draft.provider} onChange={e => setDraft({ ...draft, provider: e.target.value })}>
                                            <option value="elevenlabs">ElevenLabs (instant cloning)</option>
                                            <option value="openai">OpenAI (custom voices)</option>
                                        </select>
                                    </div>
                                    <div>
                                        <label className="field-label">URL de l'échantillon (mp3 / wav, ≥ 30s recommandé)</label>
                                        <input className="field-control" value={draft.sample_url} onChange={e => setDraft({ ...draft, sample_url: e.target.value })} placeholder="https://r2.vocal.ch/voices/sample.mp3" />
                                        <div className="field-help">
                                            Vous pouvez générer cette URL en enregistrant via le <a href="#recorder" onClick={e => { e.preventDefault(); setShowModal(false); navigate('recorder'); }}>Recorder studio</a>.
                                        </div>
                                    </div>
                                    <div>
                                        <label className="field-label">Langue de l'échantillon</label>
                                        <select className="field-control" value={draft.language} onChange={e => setDraft({ ...draft, language: e.target.value })}>
                                            {LANGS.map(l => <option key={l.id} value={l.id}>{l.label}</option>)}
                                        </select>
                                    </div>
                                    <div>
                                        <label className="field-label">Clé API (optionnel — sinon clé du compte)</label>
                                        <input className="field-control" type="password" value={draft.api_key} onChange={e => setDraft({ ...draft, api_key: e.target.value })} placeholder="••••••••" />
                                    </div>
                                </div>
                            </div>
                            <div className="modal-footer">
                                <button className="btn btn-secondary" onClick={() => setShowModal(false)}>Annuler</button>
                                <button className="btn btn-primary" onClick={create} disabled={saving}>{saving ? 'Création…' : 'Créer la voix'}</button>
                            </div>
                        </div>
                    </div>
                )}
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : RECORDER — enregistrement micro browser, multi-take, upload pour clone
   ============================================================================ */
const RecorderView = () => {
    const { notify, navigate } = useApp();
    const [permission, setPermission] = useState('prompt');
    const [status, setStatus] = useState('idle');
    const [elapsed, setElapsed] = useState(0);
    const [scriptLang, setScriptLang] = useState('fr');
    const [scriptIdx, setScriptIdx] = useState(0);
    const [takes, setTakes] = useState([]);
    const [uploadingId, setUploadingId] = useState(null);

    const streamRef = useRef(null);
    const mediaRecRef = useRef(null);
    const chunksRef = useRef([]);
    const startTsRef = useRef(0);
    const tickRef = useRef(null);
    const audioCtxRef = useRef(null);
    const analyserRef = useRef(null);
    const rafRef = useRef(null);
    const canvasRef = useRef(null);

    const scripts = RECORDER_SCRIPTS[scriptLang] || RECORDER_SCRIPTS.fr;

    useEffect(() => () => {
        if (mediaRecRef.current && mediaRecRef.current.state !== 'inactive') mediaRecRef.current.stop();
        if (streamRef.current) streamRef.current.getTracks().forEach(t => t.stop());
        if (rafRef.current) cancelAnimationFrame(rafRef.current);
        if (tickRef.current) clearInterval(tickRef.current);
        if (audioCtxRef.current) audioCtxRef.current.close().catch(() => {});
    }, []);

    const drawWaveform = useCallback(() => {
        const canvas = canvasRef.current; const analyser = analyserRef.current;
        if (!canvas || !analyser) return;
        const ctx = canvas.getContext('2d');
        const bufferLen = analyser.fftSize;
        const data = new Uint8Array(bufferLen);
        analyser.getByteTimeDomainData(data);

        const w = canvas.width = canvas.clientWidth * (window.devicePixelRatio || 1);
        const h = canvas.height = canvas.clientHeight * (window.devicePixelRatio || 1);
        ctx.clearRect(0, 0, w, h);

        const grad = ctx.createLinearGradient(0, 0, w, 0);
        grad.addColorStop(0, '#818cf8');
        grad.addColorStop(1, '#6366f1');
        ctx.lineWidth = 2 * (window.devicePixelRatio || 1);
        ctx.strokeStyle = grad;
        ctx.beginPath();
        const slice = w / bufferLen;
        let x = 0;
        for (let i = 0; i < bufferLen; i++) {
            const v = data[i] / 128.0;
            const y = (v * h) / 2;
            if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
            x += slice;
        }
        ctx.lineTo(w, h / 2);
        ctx.stroke();

        rafRef.current = requestAnimationFrame(drawWaveform);
    }, []);

    const startRecording = async () => {
        try {
            const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } });
            streamRef.current = stream;
            setPermission('granted');

            const AC = window.AudioContext || window.webkitAudioContext;
            audioCtxRef.current = new AC();
            const src = audioCtxRef.current.createMediaStreamSource(stream);
            const analyser = audioCtxRef.current.createAnalyser();
            analyser.fftSize = 1024;
            src.connect(analyser);
            analyserRef.current = analyser;

            const mimeCandidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4'];
            const mime = mimeCandidates.find(m => MediaRecorder.isTypeSupported(m)) || '';
            const rec = new MediaRecorder(stream, mime ? { mimeType: mime } : {});
            chunksRef.current = [];
            rec.ondataavailable = (e) => { if (e.data && e.data.size > 0) chunksRef.current.push(e.data); };
            rec.onstop = () => {
                const blob = new Blob(chunksRef.current, { type: rec.mimeType || 'audio/webm' });
                const url = URL.createObjectURL(blob);
                const id = Date.now();
                setTakes(prev => [{
                    id, blob, url,
                    name: `Take ${prev.length + 1}`,
                    duration: Math.round((Date.now() - startTsRef.current) / 1000),
                    scriptText: scripts[scriptIdx] || '',
                    scriptIdx,
                    createdAt: new Date(),
                }, ...prev]);
                setStatus('idle');
                if (rafRef.current) cancelAnimationFrame(rafRef.current);
                rafRef.current = null;
                if (audioCtxRef.current) { audioCtxRef.current.close().catch(() => {}); audioCtxRef.current = null; }
                if (streamRef.current) { streamRef.current.getTracks().forEach(t => t.stop()); streamRef.current = null; }
            };
            mediaRecRef.current = rec;
            rec.start();
            startTsRef.current = Date.now();
            setStatus('recording');
            setElapsed(0);
            tickRef.current = setInterval(() => setElapsed(Date.now() - startTsRef.current), 200);
            rafRef.current = requestAnimationFrame(drawWaveform);
        } catch (e) {
            setPermission('denied');
            notify.error('Accès au micro refusé : ' + (e.message || 'autorisez le micro dans le navigateur'));
        }
    };

    const stopRecording = () => {
        if (mediaRecRef.current && mediaRecRef.current.state !== 'inactive') {
            mediaRecRef.current.stop();
            if (tickRef.current) { clearInterval(tickRef.current); tickRef.current = null; }
            setStatus('processing');
        }
    };

    const removeTake = (id) => {
        setTakes(prev => {
            const t = prev.find(x => x.id === id);
            if (t?.url) URL.revokeObjectURL(t.url);
            return prev.filter(x => x.id !== id);
        });
    };

    const downloadTake = (t) => {
        const a = document.createElement('a');
        a.href = t.url;
        const ext = (t.blob?.type || '').includes('webm') ? 'webm' : 'mp4';
        a.download = `${t.name.replace(/\s+/g, '-').toLowerCase()}-${t.id}.${ext}`;
        document.body.appendChild(a); a.click(); a.remove();
    };

    const useForClone = async (t) => {
        setUploadingId(t.id);
        const ext = (t.blob?.type || '').includes('webm') ? 'webm' : 'mp4';
        const filename = `recorder-${t.id}.${ext}`;
        const r = await api.uploadBlob('/my/voice-clones/upload-sample', t.blob, filename);
        setUploadingId(null);
        if (r?.error) {
            if (r._status === 404 || (r.error || '').includes('HTTP 404')) {
                notify.warning('Endpoint /my/voice-clones/upload-sample à brancher côté API. Téléchargez le fichier puis hébergez-le, ou utilisez un sample_url externe.');
            } else {
                notify.error(r.error);
            }
            return;
        }
        if (r.sample_url) {
            try { localStorage.setItem('vocal_studio_pending_sample_url', r.sample_url); } catch (e) {}
            notify.success('Échantillon uploadé. Création du clone…');
            navigate('voices');
        } else {
            notify.success('Échantillon uploadé');
        }
    };

    const totalDuration = takes.reduce((acc, t) => acc + (t.duration || 0), 0);

    return (
        <>
            <PageHeader
                title="Recorder studio"
                subtitle="Enregistrez votre voix dans le navigateur, multi-takes, puis utilisez le meilleur take pour entraîner un clone synthétique."
                actions={<>
                    <select className="field-control" style={{ width: 'auto' }} value={scriptLang} onChange={e => { setScriptLang(e.target.value); setScriptIdx(0); }}>
                        {LANGS.map(l => <option key={l.id} value={l.id}>{l.label}</option>)}
                    </select>
                </>}
            />
            <div className="page-content">
                {permission === 'denied' && (
                    <div className="soon-banner" style={{ background: 'rgba(220, 38, 38, 0.06)', borderColor: '#fecaca', color: '#b91c1c', marginBottom: '1rem' }}>
                        <Icons.MicOff className="w-5 h-5" />
                        <div>
                            <b>Accès au micro refusé.</b> Autorisez le micro pour <code>studio.vocal.ch</code> dans les paramètres de votre navigateur, puis rechargez la page.
                        </div>
                    </div>
                )}

                <div className="recorder-stage">
                    <div className="recorder-display">
                        <div className={`recorder-status ${status}`}>
                            <span className="recording-dot" />
                            {status === 'recording' ? 'Enregistrement en cours' : status === 'processing' ? 'Traitement…' : 'Prêt à enregistrer'}
                        </div>
                        <canvas ref={canvasRef} className="recorder-canvas" />
                        <div className="recorder-time">{fmtMs(elapsed)}</div>
                        <div className="recorder-script">
                            <div style={{ fontSize: '0.7rem', textTransform: 'uppercase', letterSpacing: '0.06em', color: '#94a3b8', fontWeight: 700, marginBottom: '0.4rem' }}>
                                Script {scriptIdx + 1} / {scripts.length}
                            </div>
                            “{scripts[scriptIdx]}”
                        </div>
                        <div className="recorder-controls">
                            <button className="rec-btn-secondary" onClick={() => setScriptIdx(i => Math.max(0, i - 1))} disabled={scriptIdx === 0 || status === 'recording'} title="Script précédent">
                                <Icons.ChevronLeft className="w-4 h-4" />
                            </button>
                            {status === 'recording' ? (
                                <button className="rec-btn stop" onClick={stopRecording} title="Arrêter">
                                    <Icons.Stop className="w-7 h-7" />
                                </button>
                            ) : (
                                <button className="rec-btn start" onClick={startRecording} disabled={status === 'processing'} title="Démarrer">
                                    <Icons.Mic className="w-7 h-7" />
                                </button>
                            )}
                            <button className="rec-btn-secondary" onClick={() => setScriptIdx(i => Math.min(scripts.length - 1, i + 1))} disabled={scriptIdx === scripts.length - 1 || status === 'recording'} title="Script suivant">
                                <Icons.ChevronRight className="w-4 h-4" />
                            </button>
                        </div>
                    </div>

                    <div className="recorder-script-list">
                        <div style={{ fontSize: '0.78rem', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: '0.2rem' }}>
                            Scripts ({scriptLang.toUpperCase()})
                        </div>
                        {scripts.map((s, i) => {
                            const done = takes.some(t => t.scriptIdx === i && t.scriptText === s);
                            return (
                                <div key={i} className={`script-row ${i === scriptIdx ? 'active' : ''} ${done ? 'done' : ''}`} onClick={() => status !== 'recording' && setScriptIdx(i)}>
                                    <div className="script-num">{done ? <Icons.Check className="w-4 h-4" /> : i + 1}</div>
                                    <div className="script-text">{s}</div>
                                    <div className="script-meta">{done ? 'Enregistré' : `${s.length} car.`}</div>
                                </div>
                            );
                        })}
                    </div>
                </div>

                <div style={{ marginTop: '1.25rem' }}>
                    <div className="row" style={{ marginBottom: '0.6rem' }}>
                        <div style={{ fontWeight: 700 }}>Mes takes ({takes.length})</div>
                        <div className="spacer" />
                        {takes.length > 0 && (
                            <span className="text-muted text-xs" style={{ display: 'inline-flex', alignItems: 'center', gap: '0.3rem' }}>
                                Total : {fmtMs(totalDuration * 1000)} ·
                                {takes.length >= 3
                                    ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', color: 'var(--success, #16a34a)', fontWeight: 600 }}><Icons.Check className="w-3.5 h-3.5" /> assez pour cloner</span>
                                    : ` ${3 - takes.length} take(s) recommandé(s) pour un meilleur clone`}
                            </span>
                        )}
                    </div>
                    {takes.length === 0 ? (
                        <EmptyState
                            icon={Icons.Wave}
                            title="Aucun take pour le moment"
                            desc="Cliquez sur le bouton micro pour enregistrer votre première prise. Pour un clone de qualité, visez 30 à 90 secondes au total."
                        />
                    ) : (
                        <div className="takes-list">
                            {takes.map((t, idx) => (
                                <div key={t.id} className="take-item">
                                    <div className="take-num">#{takes.length - idx}</div>
                                    <div className="take-name">{t.name}</div>
                                    <audio controls src={t.url} preload="metadata" />
                                    <div className="text-muted text-xs" style={{ flexShrink: 0, minWidth: 50, textAlign: 'right' }}>{t.duration}s</div>
                                    <div className="take-actions">
                                        <button className="btn btn-secondary btn-sm" onClick={() => downloadTake(t)} title="Télécharger"><Icons.Download className="w-4 h-4" /></button>
                                        <button className="btn btn-primary btn-sm" onClick={() => useForClone(t)} disabled={uploadingId === t.id} title="Utiliser pour cloner">
                                            {uploadingId === t.id ? '…' : <><Icons.Sparkles className="w-4 h-4" /> Cloner</>}
                                        </button>
                                        <button className="btn btn-secondary btn-sm" onClick={() => removeTake(t.id)} title="Supprimer" style={{ color: 'var(--danger)' }}><Icons.Trash className="w-4 h-4" /></button>
                                    </div>
                                </div>
                            ))}
                        </div>
                    )}
                </div>
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : A/B COMPARE — deux voix sur même texte
   ============================================================================ */
const VoiceSelector = ({ value, onChange, voicesData }) => {
    const openaiVoices = (voicesData?.openai?.voices || DEFAULT_OPENAI_VOICES.map(v => v.id));
    return (
        <div className="field-stack">
            <div>
                <label className="field-label">Voix custom</label>
                <select className="field-control" value={value.voice_clone_id} onChange={e => {
                    const id = e.target.value;
                    const c = (voicesData?.custom || []).find(v => String(v.id) === id);
                    onChange({ ...value, voice_clone_id: id, provider: c ? c.provider : value.provider });
                }}>
                    <option value="">— Aucune —</option>
                    {(voicesData?.custom || []).map(v => <option key={v.id} value={v.id}>{v.label}</option>)}
                </select>
            </div>
            {!value.voice_clone_id && (
                <>
                    <div>
                        <label className="field-label">Fournisseur</label>
                        <select className="field-control" value={value.provider} onChange={e => onChange({ ...value, provider: e.target.value })}>
                            <option value="openai">OpenAI</option>
                            <option value="elevenlabs" disabled={!voicesData?.elevenlabs?.available}>ElevenLabs</option>
                        </select>
                    </div>
                    <div>
                        <label className="field-label">Voix</label>
                        {value.provider === 'openai' ? (
                            <select className="field-control" value={value.voice} onChange={e => onChange({ ...value, voice: e.target.value })}>
                                {openaiVoices.map(v => <option key={v} value={v}>{v}</option>)}
                            </select>
                        ) : (
                            <input className="field-control" value={value.voice} onChange={e => onChange({ ...value, voice: e.target.value })} placeholder="Voice ID ElevenLabs" />
                        )}
                    </div>
                </>
            )}
        </div>
    );
};

const ABTestView = () => {
    const { notify } = useApp();
    const [text, setText] = useState("Bonjour, vous êtes bien chez Vocal. Comment puis-je vous aider aujourd'hui ?");
    const [voicesData, setVoicesData] = useState({ openai: { voices: [] }, custom: [] });
    const [a, setA] = useState({ provider: 'openai', voice: 'alloy', voice_clone_id: '' });
    const [b, setB] = useState({ provider: 'openai', voice: 'nova',  voice_clone_id: '' });
    const [aRes, setARes] = useState(null);
    const [bRes, setBRes] = useState(null);
    const [genA, setGenA] = useState(false);
    const [genB, setGenB] = useState(false);
    const [winner, setWinner] = useState(null);

    useEffect(() => {
        api.get('/my/tts/voices').then(r => { if (r && !r.error) setVoicesData(r); });
    }, []);

    const buildPayload = (cfg) => {
        const p = { text: text.trim(), label: 'A/B', language: 'fr', format: 'mp3', speed: 1.0, provider: cfg.provider, model: cfg.provider === 'elevenlabs' ? 'eleven_multilingual_v2' : 'tts-1' };
        if (cfg.voice_clone_id) p.voice_clone_id = parseInt(cfg.voice_clone_id);
        else p.voice = cfg.voice;
        return p;
    };

    const generate = async (which) => {
        if (!text.trim()) return notify.error('Texte requis');
        const setGen = which === 'a' ? setGenA : setGenB;
        const setRes = which === 'a' ? setARes : setBRes;
        const cfg = which === 'a' ? a : b;
        setGen(true);
        const r = await api.post('/my/tts', buildPayload(cfg));
        setGen(false);
        if (r?.error) return notify.error(r.error);
        setRes(r);
    };

    const generateBoth = async () => {
        setWinner(null);
        await Promise.all([generate('a'), generate('b')]);
    };

    return (
        <>
            <PageHeader
                title="A/B compare"
                subtitle="Comparez deux voix côte à côte sur le même texte. Choisissez celle qui sonne le plus naturel pour votre marque."
                actions={
                    <button className="btn btn-primary btn-sm" onClick={generateBoth} disabled={genA || genB || !text.trim()}>
                        <Icons.Sparkles className="w-4 h-4" /> Générer A &amp; B
                    </button>
                }
            />
            <div className="page-content">
                <div className="card" style={{ marginBottom: '1rem' }}>
                    <div className="card-body">
                        <label className="field-label">Texte de test (identique pour A et B)</label>
                        <textarea className="composer-textarea" value={text} onChange={e => setText(e.target.value)} rows={3} maxLength={1500} />
                        <div className="composer-counter">
                            <span>Recommandé : 60–200 caractères pour un bon comparatif.</span>
                            <span>{text.length}/1500</span>
                        </div>
                    </div>
                </div>

                <div className="ab-grid">
                    <div className={`ab-side ${winner === 'a' ? 'win' : ''}`}>
                        <div className="ab-side-label"><span className="ab-letter">A</span> Variante A {winner === 'a' && '— préférée'}</div>
                        <VoiceSelector value={a} onChange={setA} voicesData={voicesData} />
                        <button className="btn btn-secondary" onClick={() => generate('a')} disabled={genA || !text.trim()}>
                            {genA ? 'Génération…' : <><Icons.Sparkles className="w-4 h-4" /> Générer A</>}
                        </button>
                        {aRes?.file_url && (
                            <>
                                <audio controls src={aRes.file_url} style={{ width: '100%', height: 36 }} />
                                <div className="ab-vote-row">
                                    <button className={`btn btn-sm ${winner === 'a' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setWinner('a')}>
                                        <Icons.Heart className="w-4 h-4" /> Préférer A
                                    </button>
                                </div>
                            </>
                        )}
                    </div>

                    <div className={`ab-side ${winner === 'b' ? 'win' : ''}`}>
                        <div className="ab-side-label"><span className="ab-letter">B</span> Variante B {winner === 'b' && '— préférée'}</div>
                        <VoiceSelector value={b} onChange={setB} voicesData={voicesData} />
                        <button className="btn btn-secondary" onClick={() => generate('b')} disabled={genB || !text.trim()}>
                            {genB ? 'Génération…' : <><Icons.Sparkles className="w-4 h-4" /> Générer B</>}
                        </button>
                        {bRes?.file_url && (
                            <>
                                <audio controls src={bRes.file_url} style={{ width: '100%', height: 36 }} />
                                <div className="ab-vote-row">
                                    <button className={`btn btn-sm ${winner === 'b' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setWinner('b')}>
                                        <Icons.Heart className="w-4 h-4" /> Préférer B
                                    </button>
                                </div>
                            </>
                        )}
                    </div>
                </div>

                {winner && (
                    <div className="card" style={{ marginTop: '1rem' }}>
                        <div className="card-body">
                            <div className="row">
                                <Icons.Check className="w-5 h-5" />
                                <div style={{ flex: 1 }}>
                                    <b>Préférence enregistrée :</b> Variante {winner.toUpperCase()}.
                                    Vous pouvez maintenant utiliser cette config dans vos bots et lignes téléphoniques.
                                </div>
                                <a className="btn btn-primary btn-sm" href={`${CONFIG.BOTS_URL}/`} target="_blank" rel="noopener">
                                    <Icons.Robot className="w-4 h-4" /> Appliquer à un bot
                                </a>
                            </div>
                        </div>
                    </div>
                )}
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : MARKETPLACE — voix premium curées
   ============================================================================ */
const MarketplaceView = () => {
    const [search, setSearch] = useState('');
    const [filterLang, setFilterLang] = useState('');
    const [filterGender, setFilterGender] = useState('');

    const filtered = useMemo(() => {
        const q = search.trim().toLowerCase();
        return MARKETPLACE_VOICES.filter(v => {
            if (filterLang && !v.tags.includes(filterLang)) return false;
            if (filterGender && v.gender !== filterGender) return false;
            if (q && !(v.name.toLowerCase().includes(q) || v.desc.toLowerCase().includes(q) || v.creator.toLowerCase().includes(q))) return false;
            return true;
        });
    }, [search, filterLang, filterGender]);

    return (
        <>
            <PageHeader
                title="Marketplace voix"
                subtitle="Voix premium curées par Vocal — accents suisses (FR-CH, DE-CH, IT-CH), créateurs locaux, prêtes à l'emploi."
            />
            <div className="page-content">
                <SoonBanner endpoint="GET /my/voice-marketplace">
                    Catalogue de démonstration. Le marketplace live (souscription, paiement, attribution à un bot/ligne) sera connecté à <code>ch-vocal-api</code>.
                </SoonBanner>

                <div className="library-toolbar">
                    <input className="search-input" type="search" placeholder="Rechercher une voix, un créateur…" value={search} onChange={e => setSearch(e.target.value)} />
                    <select className="search-input" style={{ flex: '0 0 130px' }} value={filterLang} onChange={e => setFilterLang(e.target.value)}>
                        <option value="">Toutes langues</option>
                        <option value="fr">FR-CH</option>
                        <option value="de">DE-CH</option>
                        <option value="it">IT-CH</option>
                    </select>
                    <select className="search-input" style={{ flex: '0 0 130px' }} value={filterGender} onChange={e => setFilterGender(e.target.value)}>
                        <option value="">Tous genres</option>
                        <option value="female">Féminin</option>
                        <option value="male">Masculin</option>
                    </select>
                </div>

                <div className="voice-grid">
                    {filtered.map(v => (
                        <div key={v.id} className="voice-card">
                            <div className="voice-card-head">
                                <VoiceAvatar name={v.name} gender={v.gender} />
                                <div style={{ flex: 1, minWidth: 0 }}>
                                    <div className="voice-card-name">{v.name}</div>
                                    <div className="voice-card-meta">par {v.creator}</div>
                                </div>
                                <span className="voice-tag premium">{v.price_chf} CHF/mois</span>
                            </div>
                            <div className="text-muted text-xs">{v.desc}</div>
                            <div className="voice-tags">
                                {v.tags.map(tag => {
                                    const cls = ['fr', 'de', 'en', 'it'].includes(tag) ? tag : '';
                                    return <span key={tag} className={`voice-tag ${cls}`}>{tag}</span>;
                                })}
                            </div>
                            <div className="voice-card-meta">
                                <Icons.Eye className="w-3 h-3" /> {v.samples} appels chez d'autres clients
                            </div>
                            <div className="voice-card-actions">
                                <button className="btn btn-secondary btn-sm">
                                    <Icons.Play className="w-4 h-4" /> Écouter
                                </button>
                                <div className="spacer" />
                                <button className="btn btn-primary btn-sm">
                                    <Icons.Cart className="w-4 h-4" /> Souscrire
                                </button>
                            </div>
                        </div>
                    ))}
                </div>
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : PRONUNCIATION — dictionnaire perso
   ============================================================================ */
const PronunciationView = () => {
    const { notify } = useApp();
    const [items, setItems] = useState(DEFAULT_PRONUNCIATIONS);
    const [draft, setDraft] = useState({ term: '', repl: '', lang: 'fr', note: '' });

    const add = () => {
        if (!draft.term.trim() || !draft.repl.trim()) return notify.error('Terme et substitution requis');
        setItems(prev => [{ ...draft }, ...prev]);
        setDraft({ term: '', repl: '', lang: 'fr', note: '' });
        notify.success('Règle ajoutée (locale — branchez POST /my/pronunciations pour persister)');
    };

    const remove = (term) => {
        if (!confirm(`Supprimer la règle « ${term} » ?`)) return;
        setItems(prev => prev.filter(i => i.term !== term));
    };

    return (
        <>
            <PageHeader
                title="Dictionnaire de prononciation"
                subtitle="Forcez la prononciation de marques, noms propres, sigles. Appliqué automatiquement à toutes les générations TTS."
            />
            <div className="page-content">
                <SoonBanner endpoint="GET/POST/DELETE /my/pronunciations">
                    Les règles sont actuellement stockées en mémoire. Pour persister, branchez les endpoints côté <code>ch-vocal-api</code> + injection avant chaque appel TTS.
                </SoonBanner>

                <div className="card" style={{ marginBottom: '1rem' }}>
                    <div className="card-header"><h2>Nouvelle règle</h2></div>
                    <div className="card-body">
                        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 110px 1fr auto', gap: '0.6rem', alignItems: 'flex-end' }}>
                            <div>
                                <label className="field-label">Terme original</label>
                                <input className="field-control" value={draft.term} onChange={e => setDraft({ ...draft, term: e.target.value })} placeholder="Swisscom" />
                            </div>
                            <div>
                                <label className="field-label">Substitution phonétique</label>
                                <input className="field-control" value={draft.repl} onChange={e => setDraft({ ...draft, repl: e.target.value })} placeholder="swisskom" />
                            </div>
                            <div>
                                <label className="field-label">Langue</label>
                                <select className="field-control" value={draft.lang} onChange={e => setDraft({ ...draft, lang: e.target.value })}>
                                    {LANGS.map(l => <option key={l.id} value={l.id}>{l.id.toUpperCase()}</option>)}
                                </select>
                            </div>
                            <div>
                                <label className="field-label">Note (optionnel)</label>
                                <input className="field-control" value={draft.note} onChange={e => setDraft({ ...draft, note: e.target.value })} placeholder="Marque, sigle…" />
                            </div>
                            <button className="btn btn-primary" onClick={add}><Icons.Plus className="w-4 h-4" /> Ajouter</button>
                        </div>
                    </div>
                </div>

                {items.length === 0 ? (
                    <EmptyState icon={Icons.Book} title="Aucune règle" desc="Ajoutez votre première règle de prononciation." />
                ) : (
                    <table className="pronun-table">
                        <thead>
                            <tr><th>Terme</th><th>Devient</th><th>Langue</th><th>Note</th><th></th></tr>
                        </thead>
                        <tbody>
                            {items.map(i => (
                                <tr key={i.term}>
                                    <td className="term">{i.term}</td>
                                    <td className="repl">{i.repl}</td>
                                    <td><span className={`voice-tag ${i.lang}`}>{i.lang.toUpperCase()}</span></td>
                                    <td>{i.note || '—'}</td>
                                    <td style={{ textAlign: 'right' }}>
                                        <button className="btn btn-secondary btn-sm" onClick={() => remove(i.term)} style={{ color: 'var(--danger)' }}><Icons.Trash className="w-4 h-4" /></button>
                                    </td>
                                </tr>
                            ))}
                        </tbody>
                    </table>
                )}
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : WIRING — quels bots / lignes utilisent quelle voix
   ============================================================================ */
const WiringView = () => {
    const [bots, setBots] = useState(null);
    const [lines, setLines] = useState(null);
    const [voices, setVoices] = useState({ custom: [] });

    useEffect(() => {
        api.get('/my/bots').then(r => setBots(r?.bots || r?.data || []));
        api.get('/my/lines').then(r => setLines(r?.lines || r?.data || []));
        api.get('/my/tts/voices').then(r => { if (r && !r.error) setVoices(r); });
    }, []);

    const voiceLabel = (b) => {
        if (b.voice_clone_id) {
            const c = (voices.custom || []).find(v => v.id === b.voice_clone_id);
            return c ? `${c.label} (clone)` : `Clone #${b.voice_clone_id}`;
        }
        return b.voice || '—';
    };

    return (
        <>
            <PageHeader
                title="Branchements"
                subtitle="Vue inverse : quelle voix est utilisée par quel bot, quelle ligne. Pratique pour rebrander rapidement."
                actions={<>
                    <a className="btn btn-secondary btn-sm" href={CONFIG.BOTS_URL} target="_blank" rel="noopener">
                        <Icons.Robot className="w-4 h-4" /> Bot Studio
                    </a>
                    <a className="btn btn-secondary btn-sm" href={`${CONFIG.MY_URL}/#lines`} target="_blank" rel="noopener">
                        <Icons.Phone className="w-4 h-4" /> Mes lignes
                    </a>
                </>}
            />
            <div className="page-content">
                <div className="card" style={{ marginBottom: '1rem' }}>
                    <div className="card-header"><h2><Icons.Robot className="w-5 h-5" /> Bots</h2></div>
                    <div className="card-body">
                        {bots === null ? <Spinner /> : bots.length === 0 ? (
                            <p className="text-muted">Aucun bot. Créez votre premier bot dans le <a href={CONFIG.BOTS_URL} target="_blank" rel="noopener">Bot Studio</a>.</p>
                        ) : (
                            <table className="pronun-table">
                                <thead><tr><th>Bot</th><th>Voix</th><th>Langue</th><th>Statut</th><th></th></tr></thead>
                                <tbody>
                                    {bots.map(b => (
                                        <tr key={b.id}>
                                            <td className="term">{b.name}</td>
                                            <td className="repl">{voiceLabel(b)}</td>
                                            <td><span className={`voice-tag ${b.language || ''}`}>{(b.language || '?').toUpperCase()}</span></td>
                                            <td><span className={`voice-status ${b.is_active ? 'ready' : 'pending'}`}><span className="dot" />{b.is_active ? 'actif' : 'brouillon'}</span></td>
                                            <td style={{ textAlign: 'right' }}>
                                                <a className="btn btn-secondary btn-sm" href={`${CONFIG.BOTS_URL}/#bot-${b.id}/voice`} target="_blank" rel="noopener">
                                                    <Icons.Edit className="w-4 h-4" /> Modifier voix
                                                </a>
                                            </td>
                                        </tr>
                                    ))}
                                </tbody>
                            </table>
                        )}
                    </div>
                </div>

                <div className="card">
                    <div className="card-header"><h2><Icons.Phone className="w-5 h-5" /> Lignes téléphoniques</h2></div>
                    <div className="card-body">
                        {lines === null ? <Spinner /> : lines.length === 0 ? (
                            <p className="text-muted">Aucune ligne. Configurez vos numéros sur <a href={CONFIG.MY_URL} target="_blank" rel="noopener">my.vocal.ch</a>.</p>
                        ) : (
                            <table className="pronun-table">
                                <thead><tr><th>Numéro</th><th>Accueil (audio)</th><th>Voicemail (audio)</th><th></th></tr></thead>
                                <tbody>
                                    {lines.map(l => (
                                        <tr key={l.id}>
                                            <td className="term">{l.phone_number || l.number || `Ligne #${l.id}`}</td>
                                            <td className="text-muted text-xs">{l.greeting_audio_url ? <a href={l.greeting_audio_url} target="_blank" rel="noopener">Écouter</a> : <span className="text-muted">Synthèse temps réel</span>}</td>
                                            <td className="text-muted text-xs">{l.voicemail_audio_url ? <a href={l.voicemail_audio_url} target="_blank" rel="noopener">Écouter</a> : <span className="text-muted">—</span>}</td>
                                            <td style={{ textAlign: 'right' }}>
                                                <a className="btn btn-secondary btn-sm" href={`${CONFIG.MY_URL}/#lines`} target="_blank" rel="noopener">
                                                    <Icons.Edit className="w-4 h-4" /> Modifier
                                                </a>
                                            </td>
                                        </tr>
                                    ))}
                                </tbody>
                            </table>
                        )}
                    </div>
                </div>
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : COSTS — consommation TTS du mois
   ============================================================================ */
const CostsView = () => {
    const [items, setItems] = useState(null);
    useEffect(() => {
        api.get('/my/tts').then(r => setItems(r?.recordings || r?.data || []));
    }, []);

    const stats = useMemo(() => {
        if (!items) return null;
        const now = new Date();
        const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
        const inMonth = items.filter(i => new Date(i.created_at) >= monthStart);
        const totalChars = inMonth.reduce((a, i) => a + (i.char_count || 0), 0);
        const totalCost  = inMonth.reduce((a, i) => a + (i.cost_chf || 0), 0);
        const byProvider = {};
        const byVoice = {};
        for (const i of inMonth) {
            byProvider[i.provider] = (byProvider[i.provider] || 0) + (i.cost_chf || 0);
            const v = i.voice || (i.voice_clone_id ? `clone#${i.voice_clone_id}` : 'inconnu');
            byVoice[v] = (byVoice[v] || 0) + (i.cost_chf || 0);
        }
        return {
            count: inMonth.length, totalChars, totalCost,
            byProvider: Object.entries(byProvider).sort((a, b) => b[1] - a[1]),
            byVoice: Object.entries(byVoice).sort((a, b) => b[1] - a[1]).slice(0, 8),
            lifetimeCount: items.length,
            lifetimeCost: items.reduce((a, i) => a + (i.cost_chf || 0), 0),
        };
    }, [items]);

    return (
        <>
            <PageHeader
                title="Coûts &amp; quotas"
                subtitle="Consommation TTS du mois en cours, par voix et par fournisseur."
            />
            <div className="page-content">
                <SoonBanner endpoint="GET /my/tts/usage">
                    Affiche les statistiques calculées depuis votre historique TTS local. L'endpoint live (<code>/my/tts/usage</code>) fournira plus tard les quotas restants par module.
                </SoonBanner>

                {!stats ? <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem 0' }}><Spinner size={32} /></div> : (
                    <>
                        <div className="kpi-grid" style={{ marginBottom: '1rem' }}>
                            <div className="kpi-card">
                                <div className="kpi-card-label"><Icons.Sparkles className="w-3 h-3" /> Générations ce mois</div>
                                <div className="kpi-card-value">{stats.count}</div>
                                <div className="kpi-card-trend">{stats.lifetimeCount} au total depuis le début</div>
                            </div>
                            <div className="kpi-card">
                                <div className="kpi-card-label"><Icons.Coin className="w-3 h-3" /> Dépense ce mois</div>
                                <div className="kpi-card-value">{fmtCurrency(stats.totalCost)}</div>
                                <div className="kpi-card-trend">{fmtCurrency(stats.lifetimeCost)} depuis le début</div>
                            </div>
                            <div className="kpi-card">
                                <div className="kpi-card-label"><Icons.Edit className="w-3 h-3" /> Caractères ce mois</div>
                                <div className="kpi-card-value">{stats.totalChars.toLocaleString('fr-CH')}</div>
                                <div className="kpi-card-trend">≈ {Math.ceil(stats.totalChars / 150)} min d'audio</div>
                            </div>
                            <div className="kpi-card">
                                <div className="kpi-card-label"><Icons.Wave className="w-3 h-3" /> Coût moyen / piste</div>
                                <div className="kpi-card-value">{stats.count ? fmtCurrency(stats.totalCost / stats.count) : '—'}</div>
                                <div className="kpi-card-trend">Mois en cours</div>
                            </div>
                        </div>

                        <div className="card" style={{ marginBottom: '1rem' }}>
                            <div className="card-header"><h2>Répartition par fournisseur</h2></div>
                            <div className="card-body">
                                {stats.byProvider.length === 0 ? <p className="text-muted">Aucune donnée ce mois.</p> : (() => {
                                    const max = Math.max(...stats.byProvider.map(([_, c]) => c), 0.01);
                                    return stats.byProvider.map(([p, c]) => (
                                        <div key={p} className="cost-bar-row">
                                            <span className="cost-bar-label">{p}</span>
                                            <div className="cost-bar-track"><div className="cost-bar-fill" style={{ width: `${(c / max) * 100}%` }} /></div>
                                            <span className="cost-bar-value">{fmtCurrency(c)}</span>
                                        </div>
                                    ));
                                })()}
                            </div>
                        </div>

                        <div className="card">
                            <div className="card-header"><h2>Top voix utilisées (ce mois)</h2></div>
                            <div className="card-body">
                                {stats.byVoice.length === 0 ? <p className="text-muted">Aucune donnée ce mois.</p> : (() => {
                                    const max = Math.max(...stats.byVoice.map(([_, c]) => c), 0.01);
                                    return stats.byVoice.map(([v, c]) => (
                                        <div key={v} className="cost-bar-row">
                                            <span className="cost-bar-label">{v}</span>
                                            <div className="cost-bar-track"><div className="cost-bar-fill" style={{ width: `${(c / max) * 100}%` }} /></div>
                                            <span className="cost-bar-value">{fmtCurrency(c)}</span>
                                        </div>
                                    ));
                                })()}
                            </div>
                        </div>
                    </>
                )}
            </div>
        </>
    );
};

/* ============================================================================
   VIEW : SETTINGS — préférences locales + clés API providers
   ============================================================================ */
const SettingsView = () => {
    const { notify, user } = useApp();
    const [defaults, setDefaults] = useState(() => {
        try { return JSON.parse(localStorage.getItem('vocal_studio_defaults') || '{}'); }
        catch { return {}; }
    });
    const [draft, setDraft] = useState({
        language: defaults.language || 'fr',
        format: defaults.format || 'mp3',
        provider: defaults.provider || 'openai',
        voice: defaults.voice || 'alloy',
        speed: defaults.speed || 1.0,
    });

    const save = () => {
        localStorage.setItem('vocal_studio_defaults', JSON.stringify(draft));
        setDefaults(draft);
        notify.success('Préférences enregistrées (locales)');
    };

    return (
        <>
            <PageHeader title="Réglages Voice Studio" subtitle="Préférences par défaut du Composer et raccourcis vers les autres consoles." />
            <div className="page-content">
                <div className="card" style={{ marginBottom: '1rem' }}>
                    <div className="card-header"><h2>Compte connecté</h2></div>
                    <div className="card-body">
                        <div className="row">
                            <div className="user-avatar" style={{ width: 50, height: 50 }}>{(user?.name || user?.email || 'U')[0].toUpperCase()}</div>
                            <div style={{ flex: 1 }}>
                                <div style={{ fontWeight: 700 }}>{user?.name || user?.client?.name || 'Mon compte'}</div>
                                <div className="text-muted text-xs">{user?.email}</div>
                            </div>
                            <a className="btn btn-secondary btn-sm" href={`${CONFIG.MY_URL}/#account`} target="_blank" rel="noopener">
                                <Icons.External className="w-4 h-4" /> Gérer sur my.vocal.ch
                            </a>
                        </div>
                    </div>
                </div>

                <div className="card" style={{ marginBottom: '1rem' }}>
                    <div className="card-header"><h2>Préférences Composer</h2></div>
                    <div className="card-body">
                        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '0.85rem' }}>
                            <div>
                                <label className="field-label">Langue par défaut</label>
                                <select className="field-control" value={draft.language} onChange={e => setDraft({ ...draft, language: e.target.value })}>
                                    {LANGS.map(l => <option key={l.id} value={l.id}>{l.label}</option>)}
                                </select>
                            </div>
                            <div>
                                <label className="field-label">Format par défaut</label>
                                <select className="field-control" value={draft.format} onChange={e => setDraft({ ...draft, format: e.target.value })}>
                                    <option value="mp3">MP3</option><option value="wav">WAV</option><option value="ogg">OGG</option>
                                </select>
                            </div>
                            <div>
                                <label className="field-label">Fournisseur</label>
                                <select className="field-control" value={draft.provider} onChange={e => setDraft({ ...draft, provider: e.target.value })}>
                                    <option value="openai">OpenAI</option><option value="elevenlabs">ElevenLabs</option>
                                </select>
                            </div>
                            <div>
                                <label className="field-label">Voix défaut (OpenAI)</label>
                                <select className="field-control" value={draft.voice} onChange={e => setDraft({ ...draft, voice: e.target.value })}>
                                    {DEFAULT_OPENAI_VOICES.map(v => <option key={v.id} value={v.id}>{v.label} — {v.tone}</option>)}
                                </select>
                            </div>
                            <div>
                                <div className="range-row">
                                    <span className="field-label" style={{ marginBottom: 0 }}>Vitesse défaut</span>
                                    <span className="range-value">{Number(draft.speed).toFixed(2)}x</span>
                                </div>
                                <input type="range" min="0.5" max="2" step="0.05" value={draft.speed} onChange={e => setDraft({ ...draft, speed: parseFloat(e.target.value) })} />
                            </div>
                        </div>
                        <div style={{ marginTop: '1rem', display: 'flex', justifyContent: 'flex-end' }}>
                            <button className="btn btn-primary" onClick={save}>Enregistrer</button>
                        </div>
                    </div>
                </div>

                <div className="card" style={{ marginBottom: '1rem' }}>
                    <div className="card-header"><h2>Clés API providers</h2></div>
                    <div className="card-body">
                        <p className="text-muted text-sm">
                            Les clés OpenAI et ElevenLabs sont configurées au niveau du compte sur <a href={`${CONFIG.MY_URL}/#account`} target="_blank" rel="noopener">my.vocal.ch → Compte → Intégrations</a>.
                            Une clé par défaut est appliquée à tous les bots et générations TTS.
                        </p>
                        <a className="btn btn-secondary btn-sm" href={`${CONFIG.MY_URL}/#account`} target="_blank" rel="noopener">
                            <Icons.External className="w-4 h-4" /> Configurer les intégrations
                        </a>
                    </div>
                </div>

                <div className="card">
                    <div className="card-header"><h2>Raccourcis</h2></div>
                    <div className="card-body" style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
                        <a className="btn btn-secondary btn-sm" href={CONFIG.MY_URL} target="_blank" rel="noopener">
                            <Icons.External className="w-4 h-4" /> Tableau de bord (my.vocal.ch)
                        </a>
                        <a className="btn btn-secondary btn-sm" href={CONFIG.BOTS_URL} target="_blank" rel="noopener">
                            <Icons.Robot className="w-4 h-4" /> Bot Studio (bots.vocal.ch)
                        </a>
                        <a className="btn btn-secondary btn-sm" href={CONFIG.DEV_URL} target="_blank" rel="noopener">
                            <Icons.External className="w-4 h-4" /> API docs (developers.vocal.ch)
                        </a>
                    </div>
                </div>
            </div>
        </>
    );
};

/* ============================================================================
   APP SHELL
   ============================================================================ */
const Shell = () => {
    const { route } = useApp();
    return (
        <div className="app-layout">
            <Sidebar />
            <main className="main-content">
                {route.view === 'composer'      && <ComposerView />}
                {route.view === 'library'       && <LibraryView />}
                {route.view === 'recorder'      && <RecorderView />}
                {route.view === 'voices'        && <VoicesView />}
                {route.view === 'abtest'        && <ABTestView />}
                {route.view === 'marketplace'   && <MarketplaceView />}
                {route.view === 'pronunciation' && <PronunciationView />}
                {route.view === 'wiring'        && <WiringView />}
                {route.view === 'costs'         && <CostsView />}
                {route.view === 'settings'      && <SettingsView />}
            </main>
        </div>
    );
};

const App = () => {
    const { user } = useApp();
    return user ? <Shell /> : <LoginScreen />;
};

const Root = () => (
    <AppProvider>
        <App />
    </AppProvider>
);

ReactDOM.createRoot(document.getElementById('app')).render(<Root />);
