// qbt-components.jsx — utils, icons, smooth chart, shared flat UI
const { useState, useEffect, useRef } = React;

// ─── UTILS ───────────────────────────────────────────────────────────────────
const fmtB = b => {
  if (!b) return '0 B';
  const k = 1024, u = ['B','KiB','MiB','GiB','TiB'];
  const i = Math.min(4, Math.floor(Math.log(Math.abs(b)) / Math.log(k)));
  return (b / Math.pow(k, i)).toFixed(i > 1 ? 2 : 0) + '\u00a0' + u[i];
};
const fmtSp = b => b ? fmtB(b) + '/s' : '0\u00a0B/s';
const fmtSpShort = b => {
  if (!b) return '—';
  const k = 1024, u = ['B','K','M','G','T'];
  const i = Math.min(4, Math.floor(Math.log(b) / Math.log(k)));
  return (b / Math.pow(k, i)).toFixed(1) + u[i] + '/s';
};
const fmtETA = s => {
  if (!s || s >= 8640000) return '∞';
  if (s < 60) return s + 's';
  if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
  return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm';
};
const fmtDate = ts => (ts && ts > 0) ? new Date(ts * 1000).toLocaleString() : '—';
const fmtRatio = r => (r === undefined || r === null) ? '0.00' : (r < 0 ? '∞' : r.toFixed(2));
const fmtDur = s => {
  if (s === undefined || s === null || s < 0) return '—';
  if (s < 60) return s + 's';
  const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
  if (d > 0) return `${d}j ${h}h`;
  if (h > 0) return `${h}h ${m}m`;
  return `${m}m`;
};
// 2-letter ISO country code → emoji flag (regional indicator symbols)
const fmtFlag = cc => (cc && /^[A-Za-z]{2}$/.test(cc))
  ? cc.toUpperCase().replace(/./g, c => String.fromCodePoint(127397 + c.charCodeAt(0)))
  : '';

const STATE_MAP = {
  downloading:{cls:'chip-dl',lbl:'Téléchargement',col:'#818cf8',ic:'dn'},
  uploading:{cls:'chip-seed',lbl:'Partage',col:'#34d399',ic:'up'},
  seeding:{cls:'chip-seed',lbl:'Partage',col:'#34d399',ic:'up'},
  forcedUP:{cls:'chip-seed',lbl:'Partage',col:'#34d399',ic:'up'},
  forcedDL:{cls:'chip-dl',lbl:'Téléchargement',col:'#818cf8',ic:'dn'},
  pausedDL:{cls:'chip-pause',lbl:'Pausé',col:'#5b5f70',ic:'pa'},
  pausedUP:{cls:'chip-pause',lbl:'Pausé',col:'#5b5f70',ic:'pa'},
  paused:{cls:'chip-pause',lbl:'Pausé',col:'#5b5f70',ic:'pa'},
  stalledDL:{cls:'chip-stall',lbl:'Bloqué',col:'#fbbf24',ic:'al'},
  stalledUP:{cls:'chip-stall',lbl:'Bloqué',col:'#fbbf24',ic:'al'},
  stalled:{cls:'chip-stall',lbl:'Bloqué',col:'#fbbf24',ic:'al'},
  error:{cls:'chip-err',lbl:'Erreur',col:'#f87171',ic:'al'},
  errord:{cls:'chip-err',lbl:'Erreur',col:'#f87171',ic:'al'},
  missingFiles:{cls:'chip-err',lbl:'Manquant',col:'#f87171',ic:'al'},
  completed:{cls:'chip-done',lbl:'Terminé',col:'#34d399',ic:'ck'},
  checkingDL:{cls:'chip-chk',lbl:'Vérif.',col:'#a78bfa',ic:'rf'},
  checkingUP:{cls:'chip-chk',lbl:'Vérif.',col:'#a78bfa',ic:'rf'},
  checkingResumeData:{cls:'chip-chk',lbl:'Vérif.',col:'#a78bfa',ic:'rf'},
  moving:{cls:'chip-chk',lbl:'Déplac.',col:'#06b6d4',ic:'fo'},
  metaDL:{cls:'chip-dl',lbl:'Metadata',col:'#818cf8',ic:'dn'},
  allocating:{cls:'chip-chk',lbl:'Alloc.',col:'#a78bfa',ic:'rf'},
  queuedDL:{cls:'chip-pause',lbl:'File',col:'#5b5f70',ic:'pa'},
  queuedUP:{cls:'chip-pause',lbl:'File',col:'#5b5f70',ic:'pa'},
};
const stOf = s => STATE_MAP[s] || { cls:'chip-pause', lbl:s||'?', col:'#5b5f70', ic:'dn' };
const normState = s => {
  if (!s) return 'other';
  const l = s.toLowerCase();
  if (l.includes('download') || l==='metadl' || l==='allocating') return 'downloading';
  if (l.includes('upload') || l.includes('seeding')) return 'seeding';
  if (l.includes('paused')) return 'paused';
  if (l.includes('stalled')) return 'stalled';
  if (l.includes('error') || l.includes('missing')) return 'error';
  if (l==='completed') return 'completed';
  return 'other';
};

// ─── ICONS ────────────────────────────────────────────────────────────────────
const IP = {
  dn:'M12 3v13m0 0-4-4m4 4 4-4M4 20h16',
  up:'M12 21V8m0 0 4 4m-4-4-4 4M4 4h16',
  pa:'M6 4h4v16H6zm8 0h4v16h-4z',
  pl:'M5 3l14 9-14 9z',
  tr:'M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6m5 4v6m4-6v6',
  pl2:'M12 5v14M5 12h14',
  se:'M11 4a7 7 0 100 14 7 7 0 000-14zm10 10-4.3-4.3',
  xx:'M18 6 6 18M6 6l12 12',
  st:'M12 15a3 3 0 100-6 3 3 0 000 6zM12 2v2m0 18v2M4.22 4.22l1.42 1.42m12.72 12.72 1.42 1.42M2 12h2m18 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42',
  fi:'M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8zm0 0v6h6',
  us:'M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2M9 11a4 4 0 100-8 4 4 0 000 8zM23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75',
  li:'M13 2 3 14h9l-1 8 10-12h-9z',
  cD:'M6 9l6 6 6-6', cU:'M18 15l-6-6-6 6',
  tm:'M4 17l6-6-6-6M12 19h8',
  mg:'M6 3v7a6 6 0 0012 0V3M4 8h4M16 8h4',
  al:'M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0zM12 9v4m0 4h.01',
  fo:'M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z',
  ck:'M20 6 9 17l-5-5',
  rf:'M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15',
  gr:'M3 3h7v7H3zm11 0h7v7h-7zM3 14h7v7H3zm11 0h7v7h-7z',
  gl:'M12 22a10 10 0 100-20 10 10 0 000 20zM2 12h20M12 2a15.3 15.3 0 010 20M12 2a15.3 15.3 0 000 20',
  sh:'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
  db:'M12 8c4.42 0 8-1.34 8-3s-3.58-3-8-3-8 1.34-8 3 3.58 3 8 3zm8 1.5c0 1.66-3.58 3-8 3s-8-1.34-8-3M4 6v12c0 1.66 3.58 3 8 3s8-1.34 8-3V6',
  ac:'M22 12h-4l-3 9L9 3l-3 9H2',
  lo:'M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4m7 14l5-5-5-5m5 5H9',
  gauge:'M12 14a2 2 0 100-4 2 2 0 000 4zm0-10a8 8 0 00-8 8 8 8 0 001.5 4.6M20.5 16.6A8 8 0 0012 4',
};
const Ic = ({ n, s=16, style={} }) => (
  <svg viewBox="0 0 24 24" width={s} height={s} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={style}>
    <path d={IP[n]}/>
  </svg>
);

// ─── SMOOTH AREA/SPLINE CHART (windowed) ──────────────────────────────────────
// Plots a pre-windowed/downsampled array of {dl,ul} evenly across the width,
// with an eased auto-scaling Y axis. Reads data via a ref so the RAF loop never
// goes stale and never re-inits.
const SpeedChart = ({ data }) => {
  const cvRef = useRef(null);
  const dataRef = useRef(data);
  dataRef.current = data;
  const maxRef = useRef(64 * 1024);

  useEffect(() => {
    const cv = cvRef.current;
    if (!cv) return;
    const ctx = cv.getContext('2d');
    let raf;

    const frame = () => {
      const d = dataRef.current || [];
      const W = cv.offsetWidth, H = cv.offsetHeight;
      if (W && H) {
        const dpr = window.devicePixelRatio || 1;
        if (cv.width !== W*dpr || cv.height !== H*dpr) { cv.width = W*dpr; cv.height = H*dpr; }
        ctx.setTransform(dpr,0,0,dpr,0,0);
        ctx.clearRect(0,0,W,H);

        const n = d.length;
        if (n >= 2) {
          const peak = Math.max(...d.map(p => Math.max(p.dl, p.ul)));
          const target = Math.max(peak * 1.15, 16 * 1024);  // adaptive scale, small floor
          maxRef.current += (target - maxRef.current) * 0.08; // eased
          const max = Math.max(maxRef.current, 1);
          const px = i => (i / (n - 1)) * W;
          const py = v => H - (v / max) * H * 0.78 - H * 0.13;

          const spline = (key, lc, c1, c2) => {
            const pts = d.map((p,i) => ({ x: px(i), y: py(p[key]) }));
            const trace = () => {
              ctx.moveTo(pts[0].x, pts[0].y);
              for (let i=0;i<pts.length-1;i++){
                const p0=pts[i-1]||pts[i], p1=pts[i], p2=pts[i+1], p3=pts[i+2]||p2;
                const c1x=p1.x+(p2.x-p0.x)/6, c1y=p1.y+(p2.y-p0.y)/6;
                const c2x=p2.x-(p3.x-p1.x)/6, c2y=p2.y-(p3.y-p1.y)/6;
                ctx.bezierCurveTo(c1x,c1y,c2x,c2y,p2.x,p2.y);
              }
            };
            ctx.beginPath(); trace();
            ctx.lineTo(pts[n-1].x, H); ctx.lineTo(pts[0].x, H); ctx.closePath();
            const g = ctx.createLinearGradient(0,0,0,H);
            g.addColorStop(0,c1); g.addColorStop(1,c2);
            ctx.fillStyle=g; ctx.fill();
            ctx.beginPath(); trace();
            ctx.shadowColor=lc; ctx.shadowBlur=12;
            ctx.strokeStyle=lc; ctx.lineWidth=2.2; ctx.lineJoin='round'; ctx.lineCap='round';
            ctx.stroke();
            ctx.shadowBlur=0;
            const last = pts[n-1];
            ctx.beginPath(); ctx.arc(last.x,last.y,3.5,0,Math.PI*2); ctx.fillStyle=lc; ctx.fill();
            ctx.beginPath(); ctx.arc(last.x,last.y,3.5,0,Math.PI*2);
            ctx.strokeStyle='rgba(10,11,15,0.6)'; ctx.lineWidth=2; ctx.stroke();
          };
          spline('dl', '#818cf8', 'rgba(99,102,241,0.30)', 'rgba(99,102,241,0)');
          spline('ul', '#34d399', 'rgba(52,211,153,0.22)', 'rgba(52,211,153,0)');
        }
      }
      raf = requestAnimationFrame(frame);
    };
    raf = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(raf);
  }, []);

  return <canvas ref={cvRef} style={{ width:'100%', height:'100%', display:'block' }}/>;
};

// ─── CHART CARD ──────────────────────────────────────────────────────────────
const CHART_RANGES = [['1h','1h'],['1j','1j'],['7j','7j'],['all','Tout']];
const ChartCard = ({ info, data, range, setRange }) => (
  <section className="card chart-card">
    <div className="chart-head">
      <div className="chart-now">
        <div className="chart-now-i">
          <span className="chart-now-l"><span className="leg-dot" style={{background:'#818cf8'}}/>Téléchargement</span>
          <span className="chart-now-v dl">{fmtSp(info?.dl_info_speed||0)}</span>
        </div>
        <div className="chart-now-i">
          <span className="chart-now-l"><span className="leg-dot" style={{background:'#34d399'}}/>Upload</span>
          <span className="chart-now-v ul">{fmtSp(info?.up_info_speed||0)}</span>
        </div>
      </div>
      <div className="chart-ranges">
        {CHART_RANGES.map(([id,lbl])=>(
          <button key={id} className={`crange${range===id?' on':''}`} onClick={()=>setRange(id)}>{lbl}</button>
        ))}
      </div>
    </div>
    <div className="chart-canvas-wrap"><SpeedChart data={data}/></div>
  </section>
);

// ─── LOGIN ───────────────────────────────────────────────────────────────────
const LoginScreen = ({ onConnect }) => {
  const [key, setKey] = useState(() => localStorage.getItem('qbt_key') || '');
  const [loading, setLoading] = useState(false);
  const [err, setErr] = useState('');
  const [showHelp, setShowHelp] = useState(false);

  const connect = async () => {
    setLoading(true); setErr('');
    try {
      const api = new ProxyQBitAPI(key.trim());
      await api.login();
      localStorage.setItem('qbt_key', key.trim());
      onConnect(api, 'qBittorrent');
    } catch (e) {
      setErr(e.message || 'Connexion impossible.');
      setLoading(false);
    }
  };
  const onKey = e => { if (e.key === 'Enter') connect(); };

  return (
    <div className="login-bg">
      <div className="login-card">
        <div className="l-logo">
          <div className="l-logo-ic">⚡</div>
          <div><div className="l-title">qBit Dashboard</div><div className="l-sub">Connectez-vous à votre instance qBittorrent</div></div>
        </div>
        <div className="fg">
          <label className="fg-label">Clé d'accès</label>
          <input className="finput" type="password" value={key} onChange={e=>setKey(e.target.value)} placeholder="••••••••" onKeyDown={onKey}/>
        </div>
        {err && <div className="l-err">{err}</div>}
        <div className="l-actions">
          <button className="btn btn-p btn-full" onClick={connect} disabled={loading}>
            <Ic n="li"/>{loading ? 'Connexion…' : 'Se connecter'}
          </button>
        </div>
        <div className="cors-tip">
          <div className="cors-hdr" onClick={()=>setShowHelp(!showHelp)}>
            <span style={{display:'flex',alignItems:'center',gap:6}}><Ic n="al" s={13}/>Comment ça se connecte à qBittorrent ?</span>
            <Ic n={showHelp?'cU':'cD'} s={13}/>
          </div>
          {showHelp && (
            <div className="cors-body">
              <div className="cors-step">
                Ce dashboard parle à un <b>proxy Vercel</b> (<code>/api/qbt</code>), même origine,
                qui relaie vers ton qBittorrent exposé en <b>HTTPS via Tailscale Funnel</b>.<br/><br/>
                La <b>clé d'accès</b> est la valeur <code>QBT_ACCESS_KEY</code> définie dans les
                variables d'environnement Vercel. Les identifiants qBittorrent restent côté serveur.<br/><br/>
                Voir <b>README.md</b> pour la procédure complète.
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

// ─── ADD TORRENT MODAL ────────────────────────────────────────────────────────
const AddModal = ({ onClose, onAdd }) => {
  const [tab, setTab] = useState('url');
  const [urls, setUrls] = useState('');
  const [file, setFile] = useState(null);
  const [drag, setDrag] = useState(false);
  const [savePath, setSavePath] = useState('');
  const [paused, setPaused] = useState(false);
  const fileRef = useRef(null);

  const onDrop = e => {
    e.preventDefault(); setDrag(false);
    const f = e.dataTransfer.files[0];
    if (f && f.name.endsWith('.torrent')) { setFile(f); setTab('file'); }
  };
  const submit = () => {
    const opts = {};
    if (savePath) opts.savepath = savePath;
    if (paused) opts.paused = 'true';
    onAdd(tab, tab === 'url' ? urls : file, opts);
    onClose();
  };

  return (
    <div className="overlay" onClick={e => e.target===e.currentTarget && onClose()}>
      <div className="modal">
        <div className="modal-h">
          <Ic n="mg"/><span className="modal-title">Ajouter un torrent</span>
          <button className="btn btn-g" style={{marginLeft:'auto',padding:'6px 9px'}} onClick={onClose}><Ic n="xx"/></button>
        </div>
        <div className="mtabs">
          <div className={`mtab${tab==='url'?' on':''}`} onClick={()=>setTab('url')}>Lien Magnet / URL</div>
          <div className={`mtab${tab==='file'?' on':''}`} onClick={()=>setTab('file')}>Fichier .torrent</div>
        </div>
        {tab==='url'
          ? <textarea className="finput" rows={4} style={{resize:'vertical',fontFamily:'var(--mono)',fontSize:12}} value={urls} onChange={e=>setUrls(e.target.value)} placeholder="magnet:?xt=urn:btih:…&#10;https://…"/>
          : <div className={`dropzone${drag?' drag':''}`} onDragOver={e=>{e.preventDefault();setDrag(true)}} onDragLeave={()=>setDrag(false)} onDrop={onDrop} onClick={()=>fileRef.current?.click()}>
              <input ref={fileRef} type="file" accept=".torrent" style={{display:'none'}} onChange={e=>setFile(e.target.files[0])}/>
              <Ic n="fo" s={28}/><p>{file ? file.name : 'Glissez un .torrent ici ou cliquez'}</p>
            </div>
        }
        <div className="fg" style={{marginTop:12}}>
          <label className="fg-label">Chemin de sauvegarde</label>
          <input className="finput" style={{fontSize:12}} value={savePath} onChange={e=>setSavePath(e.target.value)} placeholder="/downloads/"/>
        </div>
        <label className="chk-row">
          <input type="checkbox" checked={paused} onChange={e=>setPaused(e.target.checked)}/><span>Démarrer en pause</span>
        </label>
        <div className="modal-actions">
          <button className="btn btn-g" onClick={onClose}>Annuler</button>
          <button className="btn btn-p" onClick={submit} disabled={tab==='url'?!urls.trim():!file}><Ic n="pl2"/>Ajouter</button>
        </div>
      </div>
    </div>
  );
};

// ─── SPEED LIMIT MODAL ────────────────────────────────────────────────────────
const SpeedModal = ({ api, onClose }) => {
  const [dl, setDl] = useState('');
  const [ul, setUl] = useState('');
  const [maxDl, setMaxDl] = useState('');
  useEffect(() => {
    Promise.all([api.getDlLimit(), api.getUlLimit()]).then(([d, u]) => {
      setDl(d > 0 ? Math.round(d / 1024) : '');
      setUl(u > 0 ? Math.round(u / 1024) : '');
    }).catch(()=>{});
    api.getPreferences().then(p => {
      setMaxDl(p && p.max_active_downloads > 0 ? p.max_active_downloads : '');
    }).catch(()=>{});
  }, []);
  const save = async () => {
    await api.setDlLimit(dl ? parseInt(dl)*1024 : 0).catch(()=>{});
    await api.setUlLimit(ul ? parseInt(ul)*1024 : 0).catch(()=>{});
    const n = maxDl ? parseInt(maxDl) : -1;
    await api.setPreferences({ queueing_enabled: n > 0, max_active_downloads: n }).catch(()=>{});
    onClose();
  };
  const Row = ({icon,col,label,val,set,unit,ph}) => (
    <div className="spd-row">
      <span className="spd-label"><Ic n={icon} s={15} style={{color:col}}/>{label}</span>
      <input className="finput" style={{width:120,textAlign:'right'}} type="number" min="0" value={val} onChange={e=>set(e.target.value)} placeholder={ph}/>
      <span className="spd-unit">{unit}</span>
    </div>
  );
  return (
    <div className="overlay" onClick={e => e.target===e.currentTarget && onClose()}>
      <div className="modal">
        <div className="modal-h">
          <Ic n="li"/><span className="modal-title">Limites globales</span>
          <button className="btn btn-g" style={{marginLeft:'auto',padding:'6px 9px'}} onClick={onClose}><Ic n="xx"/></button>
        </div>
        <Row icon="dn" col="#818cf8" label="Téléchargement" val={dl} set={setDl} unit="KiB/s" ph="∞"/>
        <Row icon="up" col="#34d399" label="Upload" val={ul} set={setUl} unit="KiB/s" ph="∞"/>
        <p style={{fontSize:11.5,color:'var(--t3)',marginTop:4,marginBottom:14}}>Laissez vide ou 0 pour une vitesse illimitée.</p>
        <div style={{borderTop:'1px solid var(--border)',paddingTop:14}}>
          <Row icon="dn" col="#fbbf24" label="Téléchargements simultanés max" val={maxDl} set={setMaxDl} unit="actifs" ph="∞"/>
          <p style={{fontSize:11.5,color:'var(--t3)',marginTop:4}}>Nombre max de torrents téléchargés en même temps (file d'attente qBittorrent). Vide = illimité.</p>
        </div>
        <div className="modal-actions">
          <button className="btn btn-g" onClick={onClose}>Annuler</button>
          <button className="btn btn-p" onClick={save}><Ic n="st"/>Appliquer</button>
        </div>
      </div>
    </div>
  );
};

// ─── DELETE MODAL ─────────────────────────────────────────────────────────────
const DeleteModal = ({ torrent, onClose, onConfirm }) => {
  const [wf, setWf] = useState(false);
  return (
    <div className="overlay" onClick={e => e.target===e.currentTarget && onClose()}>
      <div className="modal">
        <div className="modal-h">
          <Ic n="al" style={{color:'var(--red)'}}/><span className="modal-title">Supprimer le torrent</span>
          <button className="btn btn-g" style={{marginLeft:'auto',padding:'6px 9px'}} onClick={onClose}><Ic n="xx"/></button>
        </div>
        <p className="del-msg">Supprimer <span className="del-name">« {torrent?.name} »</span> ?</p>
        <label className="chk-row">
          <input type="checkbox" checked={wf} onChange={e=>setWf(e.target.checked)}/><span>Supprimer aussi les fichiers du disque</span>
        </label>
        <div className="modal-actions">
          <button className="btn btn-g" onClick={onClose}>Annuler</button>
          <button className="btn btn-d" onClick={()=>{onConfirm(torrent.hash,wf);onClose();}}><Ic n="tr"/>Supprimer</button>
        </div>
      </div>
    </div>
  );
};

// ─── EXPORT ───────────────────────────────────────────────────────────────────
Object.assign(window, {
  fmtB, fmtSp, fmtSpShort, fmtETA, fmtDate, fmtDur, fmtFlag, fmtRatio, stOf, normState, Ic, IP,
  SpeedChart, ChartCard, LoginScreen, AddModal, SpeedModal, DeleteModal,
});
