function GeneratorModal({ onClose, onGenerate, partName }) {
  const [effort,  setEffort]  = React.useState(40);
  const [rest,    setRest]    = React.useState(20);
  const [rounds,  setRounds]  = React.useState(10);
  const [eZone,   setEZone]   = React.useState(5);
  const [rZone,   setRZone]   = React.useState(1);
  const [eBpm,    setEBpm]    = React.useState(168);
  const [rBpm,    setRBpm]    = React.useState(108);
  const [eSS,     setESS]     = React.useState('stand');
  const [rSS,     setRSS]     = React.useState('sit');
  const [name,    setName]    = React.useState(partName||'Intervalldel');

  const fi = { width:'100%', padding:'7px 9px', background:PC.bg, border:`1px solid ${PC.line}`, borderRadius:4, fontFamily:"'Space Mono',monospace", fontSize:13, color:PC.ink, outline:'none', boxSizing:'border-box' };
  const lbl = { fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:9, letterSpacing:'0.16em', textTransform:'uppercase', color:PC.muted, display:'block', marginBottom:4 };
  const miniBtn = (label, active, col, onClick) => (
    <button onClick={onClick} style={{ flex:1, padding:'5px 2px', borderRadius:3, fontFamily:"'Archivo',sans-serif", fontWeight:800, fontSize:10, background:`${col}18`, color:col, border:`1px solid ${active?col:'transparent'}`, cursor:'pointer', opacity:active?1:0.55 }}>{label}</button>
  );

  return (
    <div onClick={e=>{if(e.target===e.currentTarget)onClose();}}
      style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:200, backdropFilter:'blur(3px)' }}>
      <div style={{ background:PC.paper, border:`1px solid ${PC.line2}`, borderRadius:8, padding:24, width:400, display:'flex', flexDirection:'column', gap:13, maxHeight:'90vh', overflowY:'auto' }}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:20, letterSpacing:'-0.02em', textTransform:'uppercase', color:PC.ink }}>⚡ Intervallgenerator</div>

        <div><label style={lbl}>Navn på delen</label><input style={{...fi, fontFamily:"'Archivo',sans-serif"}} value={name} onChange={e=>setName(e.target.value)}/></div>

        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:8 }}>
          <div><label style={lbl}>Innsats (s)</label><input style={fi} type="number" min="5" value={effort} onChange={e=>setEffort(parseInt(e.target.value)||10)}/></div>
          <div><label style={lbl}>Pause (s)</label><input style={fi} type="number" min="5" value={rest} onChange={e=>setRest(parseInt(e.target.value)||10)}/></div>
          <div><label style={lbl}>Runder</label><input style={fi} type="number" min="1" max="30" value={rounds} onChange={e=>setRounds(parseInt(e.target.value)||1)}/></div>
        </div>

        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8 }}>
          <div>
            <label style={lbl}>Innsats sone</label>
            <div style={{ display:'flex', gap:3 }}>{ZONES.map(z=>miniBtn(`S${z.id}`, eZone===z.id, z.color, ()=>setEZone(z.id)))}</div>
          </div>
          <div>
            <label style={lbl}>Pause sone</label>
            <div style={{ display:'flex', gap:3 }}>{ZONES.map(z=>miniBtn(`S${z.id}`, rZone===z.id, z.color, ()=>setRZone(z.id)))}</div>
          </div>
        </div>

        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8 }}>
          <div><label style={lbl}>Innsats BPM</label><input style={fi} type="number" min="60" max="220" value={eBpm} onChange={e=>setEBpm(parseInt(e.target.value)||120)}/></div>
          <div><label style={lbl}>Pause BPM</label><input style={fi} type="number" min="60" max="220" value={rBpm} onChange={e=>setRBpm(parseInt(e.target.value)||100)}/></div>
        </div>

        <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:8 }}>
          <div>
            <label style={lbl}>Innsats posisjon</label>
            <div style={{ display:'flex', gap:3 }}>{SS_OPTS.map(s=>miniBtn(s.label, eSS===s.id, s.color, ()=>setESS(s.id)))}</div>
          </div>
          <div>
            <label style={lbl}>Pause posisjon</label>
            <div style={{ display:'flex', gap:3 }}>{SS_OPTS.map(s=>miniBtn(s.label, rSS===s.id, s.color, ()=>setRSS(s.id)))}</div>
          </div>
        </div>

        <div style={{ background:PC.bg, border:`1px solid ${PC.line}`, borderRadius:4, padding:'9px 12px', fontFamily:"'Space Mono',monospace", fontSize:12, color:PC.accent }}>
          {rounds} × {effort}s + {rest}s = {fmt((effort+rest)*rounds)} total
        </div>

        <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
          <button onClick={onClose} style={{ padding:'8px 16px', borderRadius:4, border:`1px solid ${PC.line2}`, background:'transparent', color:PC.ink2, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:'0.08em', cursor:'pointer' }}>Avbryt</button>
          <button onClick={()=>{
            const ivs=[];
            for(let i=0;i<rounds;i++){
              ivs.push({id:uid(),name:`Drag ${i+1}`, duration:effort,zone:eZone,bpm:eBpm,sitStand:eSS,notes:''});
              ivs.push({id:uid(),name:`Hvile ${i+1}`,duration:rest,  zone:rZone,bpm:rBpm,sitStand:rSS,notes:''});
            }
            onGenerate({name,type:'interval',intervals:ivs}); onClose();
          }} style={{ padding:'8px 16px', borderRadius:4, border:'none', background:PC.accent, color:'#fff', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:'0.08em', cursor:'pointer' }}>
            Generer {rounds*2} intervaller
          </button>
        </div>
      </div>
    </div>
  );
}

function SensorModal({ session, dispatch, onClose }) {
  const sensors = session.sensors || DEFAULT_SENSORS;
  const order = ['sound', 'temp', 'humidity', 'co2'];

  const updSensor = (key, patch) => dispatch(s => {
    if (!s.sensors) s.sensors = deepCopy(DEFAULT_SENSORS);
    s.sensors[key] = { ...s.sensors[key], ...patch };
  });

  const fi = { width:64, padding:'5px 7px', background:PC.bg, border:`1px solid ${PC.line}`, borderRadius:4, fontFamily:"'Space Mono',monospace", fontSize:12, color:PC.ink, outline:'none', textAlign:'center', boxSizing:'border-box' };

  return (
    <div onClick={e=>{if(e.target===e.currentTarget)onClose();}}
      style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:200, backdropFilter:'blur(3px)' }}>
      <div style={{ background:PC.paper, border:`1px solid ${PC.line2}`, borderRadius:8, padding:24, width:480, display:'flex', flexDirection:'column', gap:13, maxHeight:'90vh', overflowY:'auto' }}>
        <div>
          <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:20, letterSpacing:'-0.02em', textTransform:'uppercase', color:PC.ink }}>⚙ Miljøsensorer</div>
          <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:12, color:PC.muted, marginTop:4, lineHeight:1.5 }}>
            Velg hvilke sensorverdier som vises under timen, og sett ønsket målområde. Verdier utenfor området varsles på instruktør-skjermen.
          </div>
        </div>

        <div style={{ display:'flex', flexDirection:'column', gap:8, marginTop:4 }}>
          {order.map(key => {
            const s = sensors[key];
            if (!s) return null;
            return (
              <div key={key} style={{
                display:'grid',
                gridTemplateColumns:'34px 1fr auto',
                alignItems:'center', gap:14,
                padding:'12px 14px', borderRadius:4,
                background: s.enabled ? `${s.color}10` : PC.bg,
                border: `1px solid ${s.enabled ? `${s.color}45` : PC.line}`,
              }}>
                {/* Toggle */}
                <button onClick={()=>updSensor(key, { enabled: !s.enabled })}
                  aria-pressed={s.enabled}
                  style={{
                    width:34, height:20, padding:0, borderRadius:10,
                    border:'none', background: s.enabled ? s.color : PC.line2,
                    cursor:'pointer', position:'relative', transition:'background 0.15s',
                  }}>
                  <span style={{
                    position:'absolute', top:2, left: s.enabled ? 16 : 2,
                    width:16, height:16, borderRadius:'50%', background:'#fff',
                    transition:'left 0.15s', boxShadow:'0 1px 2px rgba(0,0,0,0.2)',
                  }} />
                </button>

                {/* Label + status */}
                <div>
                  <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:14, color: s.enabled ? PC.ink : PC.muted }}>
                    {s.label}
                  </div>
                  <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:11, color:PC.muted, marginTop:2 }}>
                    {s.enabled ? `Ønsket: ${s.min}–${s.max} ${s.unit}` : 'Slått av'}
                  </div>
                </div>

                {/* Range inputs */}
                <div style={{ display:'flex', alignItems:'center', gap:6, opacity: s.enabled ? 1 : 0.4 }}>
                  <input type="number" value={s.min} disabled={!s.enabled}
                    onChange={e=>updSensor(key, { min: parseInt(e.target.value)||0 })} style={fi} />
                  <span style={{ color:PC.dim, fontSize:11 }}>–</span>
                  <input type="number" value={s.max} disabled={!s.enabled}
                    onChange={e=>updSensor(key, { max: parseInt(e.target.value)||0 })} style={fi} />
                  <span style={{ fontFamily:"'Archivo',sans-serif", fontSize:11, color:PC.muted, minWidth:32, fontWeight:600 }}>{s.unit}</span>
                </div>
              </div>
            );
          })}
        </div>

        <div style={{ fontFamily:"'Archivo',sans-serif", fontSize:11, color:PC.dim, lineHeight:1.5, padding:'8px 12px', background:PC.bg, borderRadius:4, marginTop:4 }}>
          Innstillingene lagres i øktfilen. Lydnivå måles relativt og kalibreres første minutt av timen.
        </div>

        <div style={{ display:'flex', justifyContent:'flex-end' }}>
          <button onClick={onClose} style={{ padding:'8px 18px', borderRadius:4, border:'none', background:PC.accent, color:'#fff', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:'0.08em', cursor:'pointer' }}>Ferdig</button>
        </div>
      </div>
    </div>
  );
}

function SaveAsCloudModal({ defaultName, onClose, onSave }) {
  const [name, setName] = React.useState(defaultName || 'Mitt program');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  const handleSave = async () => {
    if (!name.trim()) { setError('Navn kan ikke være tomt.'); return; }
    setBusy(true); setError(null);
    try {
      await onSave(name.trim());
      onClose();
    } catch (err) {
      setError(err.message || 'Kunne ikke lagre.');
      setBusy(false);
    }
  };

  return (
    <div onClick={e=>{if(e.target===e.currentTarget && !busy)onClose();}}
      style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:200, backdropFilter:'blur(3px)' }}>
      <div style={{ background:PC.paper, border:`1px solid ${PC.line2}`, borderRadius:8, padding:24, width:400, display:'flex', flexDirection:'column', gap:14 }}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>Lagre som ny i sky</div>
        <div>
          <label style={{ fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:10, letterSpacing:'0.16em', textTransform:'uppercase', color:PC.muted, display:'block', marginBottom:6 }}>Navn</label>
          <input value={name} onChange={e=>setName(e.target.value)} autoFocus
            style={{ width:'100%', padding:'9px 12px', borderRadius:6, border:`1.5px solid ${PC.line2}`, background:PC.bg, fontFamily:"'Archivo',sans-serif", fontSize:14, color:PC.ink, outline:'none', boxSizing:'border-box' }} />
        </div>
        {error && <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>{error}</div>}
        <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
          <button onClick={onClose} disabled={busy} style={{ padding:'8px 16px', borderRadius:4, border:`1px solid ${PC.line2}`, background:'transparent', color:PC.ink2, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:'0.08em', cursor:busy?'default':'pointer' }}>Avbryt</button>
          <button onClick={handleSave} disabled={busy} style={{ padding:'8px 16px', borderRadius:4, border:'none', background:PC.accent, color:'#fff', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', letterSpacing:'0.08em', cursor:busy?'default':'pointer' }}>
            {busy ? 'Lagrer …' : 'Lagre'}
          </button>
        </div>
      </div>
    </div>
  );
}

function CloudProgramsModal({ onClose, onOpen, dirty }) {
  const user = useAuthUser();
  const [rows, setRows] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [busyId, setBusyId] = React.useState(null);

  const refresh = React.useCallback(() => {
    if (!user) return;
    setError(null);
    window.TaktCloud.listPrograms().then(setRows).catch(err => setError(err.message || 'Kunne ikke hente programmene dine.'));
  }, [user]);

  React.useEffect(() => { refresh(); }, [refresh]);

  const handleOpen = async (id) => {
    if (dirty && !window.confirm('Du har ulagrede endringer. Åpne et annet program likevel?')) return;
    setBusyId(id); setError(null);
    try {
      const { id: openedId, program } = await window.TaktCloud.getProgram(id);
      onOpen(openedId, program);
      onClose();
    } catch (err) {
      setError(err.message || 'Kunne ikke åpne programmet.');
      setBusyId(null);
    }
  };

  const handleDelete = async (id, name) => {
    if (!window.confirm(`Slette «${name}»? Dette kan ikke angres.`)) return;
    setBusyId(id); setError(null);
    try {
      await window.TaktCloud.deleteProgram(id);
      refresh();
    } catch (err) {
      setError(err.message || 'Kunne ikke slette programmet.');
    } finally {
      setBusyId(null);
    }
  };

  return (
    <div onClick={e=>{if(e.target===e.currentTarget)onClose();}}
      style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:200, backdropFilter:'blur(3px)' }}>
      <div style={{ background:PC.paper, border:`1px solid ${PC.line2}`, borderRadius:8, padding:24, width:480, maxHeight:'80vh', overflowY:'auto', display:'flex', flexDirection:'column', gap:14 }}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>Mine programmer</div>
        {!user ? (
          <AuthScreen compact />
        ) : error ? (
          <div style={{ fontSize:13, color:PC.z5, fontWeight:600 }}>{error}</div>
        ) : rows === null ? (
          <div style={{ fontSize:13, color:PC.muted }}>Laster …</div>
        ) : rows.length === 0 ? (
          <div style={{ fontSize:13, color:PC.muted }}>Du har ingen lagrede programmer i skyen ennå.</div>
        ) : (
          <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
            {rows.map(row => (
              <div key={row.id} style={{ display:'flex', alignItems:'center', gap:10, padding:'12px 14px', borderRadius:6, border:`1px solid ${PC.line2}` }}>
                <div style={{ flex:1, minWidth:0 }}>
                  <div style={{ fontWeight:700, fontSize:14, color:PC.ink, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{row.name}</div>
                  <div style={{ fontSize:11, color:PC.muted, marginTop:2 }}>{fmt(row.totalSec)} · {row.partCount} deler · sist endret {new Date(row.updatedAt).toLocaleDateString('no-NO')}</div>
                </div>
                <button onClick={()=>handleOpen(row.id)} disabled={busyId===row.id} style={{ padding:'6px 12px', borderRadius:4, border:'none', background:PC.accent, color:'#fff', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, textTransform:'uppercase', cursor:'pointer' }}>Åpne</button>
                <button onClick={()=>handleDelete(row.id, row.name)} disabled={busyId===row.id} style={{ padding:'6px 10px', borderRadius:4, border:`1px solid ${PC.z5}55`, background:'transparent', color:PC.z5, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, cursor:'pointer' }}>Slett</button>
              </div>
            ))}
          </div>
        )}
        <div style={{ display:'flex', justifyContent:'flex-end' }}>
          <button onClick={onClose} style={{ padding:'8px 16px', borderRadius:4, border:`1px solid ${PC.line2}`, background:'transparent', color:PC.ink2, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', cursor:'pointer' }}>Lukk</button>
        </div>
      </div>
    </div>
  );
}

function ShareModal({ cloudId, onClose }) {
  const [token, setToken] = React.useState(undefined); // undefined = laster, null = ikke delt
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [copied, setCopied] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    window.TaktCloud.getProgram(cloudId)
      .then(({ shareToken }) => { if (!cancelled) setToken(shareToken || null); })
      .catch(err => { if (!cancelled) setError(err.message || 'Kunne ikke hente delingsstatus.'); });
    return () => { cancelled = true; };
  }, [cloudId]);

  const link = token ? `${window.location.origin}/instruktor?share=${token}` : null;

  const handleShare = async () => {
    setBusy(true); setError(null);
    try {
      const newToken = await window.TaktCloud.shareProgram(cloudId);
      setToken(newToken);
    } catch (err) {
      setError(err.message || 'Kunne ikke opprette delingslenke.');
    } finally {
      setBusy(false);
    }
  };

  const handleUnshare = async () => {
    setBusy(true); setError(null);
    try {
      await window.TaktCloud.unshareProgram(cloudId);
      setToken(null);
    } catch (err) {
      setError(err.message || 'Kunne ikke trekke tilbake delingen.');
    } finally {
      setBusy(false);
    }
  };

  const handleCopy = async () => {
    if (!link) return;
    try { await navigator.clipboard.writeText(link); setCopied(true); setTimeout(()=>setCopied(false), 2000); } catch {}
  };

  return (
    <div onClick={e=>{if(e.target===e.currentTarget && !busy)onClose();}}
      style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:200, backdropFilter:'blur(3px)' }}>
      <div style={{ background:PC.paper, border:`1px solid ${PC.line2}`, borderRadius:8, padding:24, width:440, display:'flex', flexDirection:'column', gap:16 }}>
        <div style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:18, color:PC.ink }}>Del program</div>
        {token === undefined ? (
          <div style={{ fontSize:13, color:PC.muted }}>Laster …</div>
        ) : token ? (
          <>
            <div style={{ fontSize:13, color:PC.ink2, lineHeight:1.5 }}>Alle med denne lenken kan åpne programmet på instruktørskjermen — uten å logge inn.</div>
            <div style={{ display:'flex', gap:8 }}>
              <input readOnly value={link} onFocus={e=>e.target.select()}
                style={{ flex:1, padding:'9px 12px', borderRadius:6, border:`1.5px solid ${PC.line2}`, background:PC.bg, fontFamily:"'Space Mono',monospace", fontSize:12, color:PC.ink, outline:'none' }} />
              <button onClick={handleCopy} style={{ padding:'9px 14px', borderRadius:6, border:'none', background:PC.accent, color:'#fff', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, cursor:'pointer', whiteSpace:'nowrap' }}>
                {copied ? '✓ Kopiert' : 'Kopier'}
              </button>
            </div>
            <button onClick={handleUnshare} disabled={busy} style={{ alignSelf:'flex-start', background:'none', border:'none', padding:0, color:PC.z5, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, cursor:busy?'default':'pointer' }}>
              Trekk tilbake deling
            </button>
          </>
        ) : (
          <>
            <div style={{ fontSize:13, color:PC.ink2, lineHeight:1.5 }}>Opprett en lenke som gir tilgang til dette programmet uten innlogging.</div>
            <button onClick={handleShare} disabled={busy} style={{ padding:'10px 16px', borderRadius:6, border:'none', background:PC.accent, color:'#fff', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:13, cursor:busy?'default':'pointer' }}>
              {busy ? 'Oppretter …' : 'Opprett delingslenke'}
            </button>
          </>
        )}
        {error && <div style={{ fontSize:12, color:PC.z5, fontWeight:600 }}>{error}</div>}
        <div style={{ display:'flex', justifyContent:'flex-end' }}>
          <button onClick={onClose} style={{ padding:'8px 16px', borderRadius:4, border:`1px solid ${PC.line2}`, background:'transparent', color:PC.ink2, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:12, textTransform:'uppercase', cursor:'pointer' }}>Lukk</button>
        </div>
      </div>
    </div>
  );
}

function App() {
  const { session, dispatch, silentUpdate, undo, redo, canUndo, canRedo, setSession, cloudId, dirty, markSaved } = usePlannerState();
  const [selection, setSelection] = React.useState(null);
  const [pxPerSec, setPxPerSec]   = React.useState(1.4);
  const [genModal, setGenModal]   = React.useState(null);
  const [sensorModal, setSensorModal] = React.useState(false);
  const [savedToast, setSavedToast] = React.useState(false);
  const [showSaveAsModal, setShowSaveAsModal] = React.useState(false);
  const [showCloudModal, setShowCloudModal]   = React.useState(false);
  const [showShareModal, setShowShareModal]   = React.useState(false);
  const [showAuthModal, setShowAuthModal]     = React.useState(false);
  const [cloudBusy, setCloudBusy]   = React.useState(false);
  const [cloudError, setCloudError] = React.useState(null);
  const scrollRef = React.useRef(null);
  const total     = totalDur(session);

  const handleSaveToCloud = async () => {
    setCloudBusy(true); setCloudError(null);
    try {
      const row = await window.TaktCloud.saveProgram({ id: cloudId, name: session.name, data: session });
      markSaved(row.id);
    } catch (err) {
      setCloudError(err.message || 'Kunne ikke lagre i sky.');
    } finally {
      setCloudBusy(false);
    }
  };

  const handleSaveAsNewToCloud = async (name) => {
    const dataToSave = { ...session, name };
    silentUpdate(s => { s.name = name; });
    const row = await window.TaktCloud.saveProgram({ id: null, name, data: dataToSave });
    markSaved(row.id);
  };

  // Dirty-sjekken skjer inni CloudProgramsModal før denne kalles.
  const handleOpenCloudProgram = (id, program) => {
    setSession(program, { cloudId: id, dirty: false });
    setSelection(null);
  };

  // ── Test-run state ─────────────────────────────────────────
  const [playheadSec, setPlayheadSec] = React.useState(0);
  const [testPlaying, setTestPlaying] = React.useState(false);
  const playStartRef     = React.useRef(null);
  const currentTrackIdRef = React.useRef(null);
  const draggingRef       = React.useRef(false);

  const trackAt = React.useCallback(sec => {
    return session.tracks
      .filter(t => sec >= t.startSec && sec < t.startSec + t.durationSec)
      .sort((a, b) => b.startSec - a.startSec)[0] || null;
  }, [session.tracks]);

  // Tick when playing
  React.useEffect(() => {
    if (!testPlaying) return;
    const id = setInterval(() => {
      if (draggingRef.current) return;
      const ps = playStartRef.current;
      if (!ps) return;
      const sec = ps.baseElapsed + (Date.now() - ps.startedAt) / 1000;
      if (sec >= total) {
        setTestPlaying(false);
        setPlayheadSec(total);
        if (window.TaktSpotify) TaktSpotify.pause();
        playStartRef.current = null;
        return;
      }
      setPlayheadSec(sec);
      const track = trackAt(sec);
      const newId = track?.id ?? null;
      if (newId !== currentTrackIdRef.current) {
        currentTrackIdRef.current = newId;
        if (window.TaktSpotify) {
          if (track) TaktSpotify.play(session.tracks, sec);
          else TaktSpotify.pause();
        }
      }
    }, 250);
    return () => clearInterval(id);
  }, [testPlaying, total, session.tracks, trackAt]);

  const handleTestPlay = async () => {
    if (!window.TaktSpotify) return;
    let status = TaktSpotify.getStatus();
    if (status === 'not-logged-in') { alert('Logg inn med Spotify i panelet til venstre først'); return; }
    if (status === 'no-premium')    { alert('Spotify Premium kreves'); return; }
    if (status === 'no-device') {
      await TaktSpotify.getDevices();        // auto-select an already-active device if present
      status = TaktSpotify.getStatus();
      if (status === 'no-device') { alert('Åpne Spotify og start en sang, velg så enhet i Spotify-panelet til venstre.'); return; }
    }
    playStartRef.current = { startedAt: Date.now(), baseElapsed: playheadSec };
    currentTrackIdRef.current = null; // force play call on next tick
    setTestPlaying(true);
  };

  const handleTestPause = () => {
    if (window.TaktSpotify) TaktSpotify.pause();
    const ps = playStartRef.current;
    if (ps) setPlayheadSec(ps.baseElapsed + (Date.now() - ps.startedAt) / 1000);
    playStartRef.current = null;
    setTestPlaying(false);
  };

  const handleTestStop = () => {
    if (window.TaktSpotify) TaktSpotify.pause();
    playStartRef.current = null;
    currentTrackIdRef.current = null;
    setTestPlaying(false);
    setPlayheadSec(0);
  };

  const handleSeek = React.useCallback((sec, opts = {}) => {
    const clamped = Math.max(0, Math.min(total, sec));
    const phase = opts.phase || 'click';
    setPlayheadSec(clamped);
    if (!testPlaying) return;

    playStartRef.current = { startedAt: Date.now(), baseElapsed: clamped };

    if (phase === 'drag') {
      if (!draggingRef.current) {
        draggingRef.current = true;
        if (window.TaktSpotify) TaktSpotify.pause();
        currentTrackIdRef.current = null;
      }
      return;
    }

    // 'click' or 'end' — resume playback at new position
    draggingRef.current = false;
    const track = trackAt(clamped);
    currentTrackIdRef.current = track?.id ?? null;
    if (window.TaktSpotify) {
      if (track) TaktSpotify.seek(session.tracks, clamped, true);
      else TaktSpotify.pause();
    }
  }, [total, testPlaying, session.tracks, trackAt]);

  const handleZoom = React.useCallback(newPps => {
    const el = scrollRef.current;
    const center = el ? (el.scrollTop + el.clientHeight/2) / pxPerSec : 0;
    setPxPerSec(newPps);
    requestAnimationFrame(()=>{ if(el) el.scrollTop = Math.max(0, center*newPps - el.clientHeight/2); });
  }, [pxPerSec]);

  const handleFit = React.useCallback(() => {
    const el = scrollRef.current;
    if (!el || total <= 0) { setPxPerSec(1.4); return; }
    const targetH = Math.max(200, el.clientHeight - 32);
    const newPps = Math.max(0.2, Math.min(6, targetH / (total + 60)));
    setPxPerSec(newPps);
    requestAnimationFrame(() => { if (el) el.scrollTop = 0; });
  }, [total]);

  React.useEffect(()=>{
    const h = e => {
      if((e.metaKey||e.ctrlKey) && e.key==='z'){ e.shiftKey?redo():undo(); e.preventDefault(); }
      if((e.metaKey||e.ctrlKey) && e.key==='y'){ redo(); e.preventDefault(); }
    };
    window.addEventListener('keydown', h);
    return ()=>window.removeEventListener('keydown', h);
  }, [undo, redo]);

  const saveFn = () => {
    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(session)); } catch {}
    setSavedToast(true);
    setTimeout(() => setSavedToast(false), 2000);
  };
  const exportFn = () => {
    const b = new Blob([JSON.stringify(session,null,2)],{type:'application/json'});
    const a = document.createElement('a'); a.href=URL.createObjectURL(b);
    a.download=`${session.name.replace(/\s+/g,'_')}.json`; a.click();
  };
  const importFn = () => {
    const inp=document.createElement('input'); inp.type='file'; inp.accept='.json';
    inp.onchange=e=>{
      const f=e.target.files[0]; if(!f) return;
      if (f.size > 512*1024) { alert('Filen er for stor (maks 512 KB).'); return; }
      const r=new FileReader();
      r.onload=ev=>{
        let raw;
        try { raw = JSON.parse(ev.target.result); }
        catch { alert('Ugyldig JSON-fil'); return; }
        const result = window.TaktProgram.validateProgram(raw);
        if (!result.ok) { alert('Programmet er ugyldig:\n' + result.errors.join('\n')); return; }
        setSession(window.TaktProgram.normalizeProgram(result.program));
        setSelection(null);
      };
      r.readAsText(f);
    }; inp.click();
  };
  const newSession = () => {
    if (!window.confirm('Start ny økt? Det aktive arbeidet ditt forsvinner med mindre du har lagret som JSON først.')) return;
    setSession({
      name: 'Ny økt',
      parts: [],
      tracks: [],
      sensors: deepCopy(DEFAULT_SENSORS),
    });
    setSelection(null);
  };
  const addPart = () => {
    const nid=uid();
    dispatch(s=>s.parts.push({id:nid,name:'Ny del',type:'song',intervals:[]}));
    setSelection({type:'part',id:nid});
  };
  const addTrack = () => {
    const nid=uid();
    dispatch(s=>s.tracks.push({id:nid,title:'Nytt spor',artist:'',bpm:128,startSec:0,durationSec:200,notes:'',spotifyUri:''}));
    setSelection({type:'track',id:nid});
  };
  const handleDropTrack = (data, startSec) => {
    const nid=uid();
    dispatch(s=>s.tracks.push({id:nid,...data,startSec}));
    setSelection({type:'track',id:nid});
  };
  const handleGenerate = (partId, data) => {
    if(partId && partId!=='new'){
      dispatch(s=>{ const p=s.parts.find(x=>x.id===partId); if(p){p.name=data.name;p.type=data.type;p.intervals=data.intervals;} });
    } else {
      const nid=uid();
      dispatch(s=>s.parts.push({id:nid,...data}));
      setSelection({type:'part',id:nid});
    }
  };

  const totalIvs = session.parts.reduce((a,p)=>a+p.intervals.length,0);
  const sensorsOn = Object.values(session.sensors || {}).filter(s => s?.enabled).length;
  const tb = (label, onClick, accent=false, disabled=false) => (
    <button onClick={onClick} disabled={disabled} style={{ padding:'6px 13px', borderRadius:4, border:accent?'none':`1px solid ${PC.line2}`, background:accent?PC.accent:'transparent', color:accent?'#fff':disabled?PC.dim:PC.ink2, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, textTransform:'uppercase', letterSpacing:'0.08em', cursor:disabled?'default':'pointer', whiteSpace:'nowrap', flexShrink:0 }}>{label}</button>
  );
  const ib = (label, onClick, disabled=false) => (
    <button onClick={onClick} disabled={disabled} style={{ width:26, height:26, borderRadius:4, border:`1px solid ${PC.line}`, background:'transparent', color:disabled?PC.dim:PC.ink2, fontSize:14, cursor:disabled?'default':'pointer', display:'flex', alignItems:'center', justifyContent:'center', flexShrink:0 }}>{label}</button>
  );

  return (
    <div style={{ height:'100vh', display:'flex', flexDirection:'column', overflow:'hidden', fontFamily:"'Archivo',sans-serif", background:PC.bg, color:PC.ink }}>

      {/* TOPBAR */}
      <div style={{ height:56, display:'flex', alignItems:'center', gap:12, padding:'0 18px', background:PC.paper, borderBottom:`1px solid ${PC.line}`, flexShrink:0 }}>
        <div style={{ display:'flex', alignItems:'baseline', gap:5, flexShrink:0 }}>
          <span style={{ fontFamily:"'Archivo',sans-serif", fontWeight:900, fontSize:22, letterSpacing:'-0.03em', color:PC.ink }}>TAKT</span>
          <span style={{ width:7, height:7, borderRadius:'50%', background:PC.accent, display:'inline-block', marginBottom:1 }} />
        </div>
        <div style={{ width:1, height:22, background:PC.line, flexShrink:0 }} />
        <span style={{ fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:10, letterSpacing:'0.16em', textTransform:'uppercase', color:PC.muted, flexShrink:0 }}>Planlegger</span>
        <span style={{ color:PC.dim, flexShrink:0 }}>/</span>
        <input value={session.name} onChange={e=>dispatch(s=>{s.name=e.target.value;})}
          style={{ background:'transparent', border:'none', outline:'none', fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:16, color:PC.ink, padding:'3px 6px', borderRadius:4, flex:1, minWidth:0 }} />
        <div style={{ marginLeft:'auto', display:'flex', alignItems:'center', gap:8, flexShrink:0 }}>
          <span style={{ fontFamily:"'Space Mono',monospace", fontSize:11, color:PC.accent, background:`${PC.accent}12`, padding:'3px 9px', borderRadius:4, whiteSpace:'nowrap', flexShrink:0 }}>{fmt(total)}</span>
          {cloudId && (
            <span style={{ padding:'4px 10px', borderRadius:4, background: dirty?`${PC.z3}18`:`${PC.z2}18`, color: dirty?PC.z3:PC.z2, border:`1px solid ${dirty?PC.z3:PC.z2}40`, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:10, letterSpacing:'0.08em', textTransform:'uppercase', whiteSpace:'nowrap', flexShrink:0 }}>
              {dirty ? 'Ulagrede endringer' : 'Lagret i sky ✓'}
            </span>
          )}
          <AuthPill onLoginClick={()=>setShowAuthModal(true)} />
          {savedToast && (
            <span style={{ padding:'4px 10px', borderRadius:4, background:`${PC.z2}18`, color:PC.z2, border:`1px solid ${PC.z2}40`, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, letterSpacing:'0.08em', textTransform:'uppercase', whiteSpace:'nowrap', flexShrink:0 }}>✓ Lagret</span>
          )}
          {cloudError && (
            <span style={{ padding:'4px 10px', borderRadius:4, background:`${PC.z5}18`, color:PC.z5, border:`1px solid ${PC.z5}40`, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, whiteSpace:'nowrap', flexShrink:0, maxWidth:220, overflow:'hidden', textOverflow:'ellipsis' }} title={cloudError}>{cloudError}</span>
          )}
          {tb('Ny økt',     newSession)}
          {tb('Åpne fil',   importFn)}
          {tb('Lagre',      saveFn)}
          {tb('Lagre som…', exportFn)}
          <div style={{ width:1, height:22, background:PC.line, flexShrink:0 }} />
          {tb('Mine programmer', ()=>setShowCloudModal(true))}
          {tb('Lagre i sky', handleSaveToCloud, false, cloudBusy)}
          {tb('Lagre som ny i sky', ()=>setShowSaveAsModal(true), false, cloudBusy)}
          {tb('Del', ()=>setShowShareModal(true), false, !cloudId)}
          {tb('▶ Kjør time', () => {
            if (cloudId) {
              window.open('/instruktor?program=' + cloudId, '_blank');
            } else {
              window.TaktProgram.writeLastUsed(session);
              window.open('/instruktor', '_blank');
            }
          }, true, false)}
        </div>
      </div>

      {/* TOOLBAR */}
      <div style={{ height:40, display:'flex', alignItems:'center', gap:6, padding:'0 14px', background:PC.paper, borderBottom:`1px solid ${PC.line}`, flexShrink:0 }}>
        <span style={{ fontFamily:"'Space Mono',monospace", fontSize:10, color:PC.muted, whiteSpace:'nowrap', flexShrink:0 }}>{Math.round(pxPerSec*600)/10} px/min</span>
        {ib('−', ()=>handleZoom(Math.max(0.2,+(pxPerSec/1.35).toFixed(3))))}
        {ib('+', ()=>handleZoom(Math.min(6,+(pxPerSec*1.35).toFixed(3))))}
        {tb('Tilpass', handleFit)}
        <div style={{ width:1, height:18, background:PC.line, flexShrink:0 }} />
        {[
          {dot:PC.accent, label:'◼ Del',       onClick:addPart},
          {dot:PC.z4,     label:'⚡ Generator', onClick:()=>setGenModal('new')},
          {dot:PC.ink2,   label:'♪ Spor',       onClick:addTrack},
        ].map(({dot,label,onClick})=>(
          <button key={label} onClick={onClick} style={{ padding:'5px 10px', borderRadius:4, border:'none', background:'transparent', color:PC.ink2, fontFamily:"'Archivo',sans-serif", fontWeight:600, fontSize:12, cursor:'pointer', display:'flex', alignItems:'center', gap:6, whiteSpace:'nowrap', flexShrink:0 }}>
            <span style={{ width:7, height:7, borderRadius:2, background:dot, flexShrink:0 }} />{label}
          </button>
        ))}
        <div style={{ width:1, height:18, background:PC.line, flexShrink:0 }} />
        <button onClick={()=>setSensorModal(true)} style={{ padding:'5px 10px', borderRadius:4, border:'none', background:'transparent', color:PC.ink2, fontFamily:"'Archivo',sans-serif", fontWeight:600, fontSize:12, cursor:'pointer', display:'flex', alignItems:'center', gap:6, whiteSpace:'nowrap', flexShrink:0 }}>
          ⚙ Sensorer
          {sensorsOn > 0 && (
            <span style={{ background:`${PC.accent}22`, color:PC.accent, padding:'1px 6px', borderRadius:10, fontFamily:"'Space Mono',monospace", fontSize:10, fontWeight:700 }}>{sensorsOn}</span>
          )}
        </button>
        <div style={{ width:1, height:18, background:PC.line, flexShrink:0 }} />

        {/* Test-run controls */}
        <button onClick={testPlaying ? handleTestPause : handleTestPlay}
          style={{ padding:'5px 12px', borderRadius:4, border:'none',
            background: testPlaying ? `${PC.z5}18` : `${PC.accent}18`,
            color: testPlaying ? PC.z5 : PC.accent,
            fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11,
            cursor:'pointer',
            display:'flex', alignItems:'center', gap:5, whiteSpace:'nowrap', flexShrink:0,
          }}>
          {testPlaying ? '❚❚ Pause' : '▶ Test økt'}
        </button>
        {(testPlaying || playheadSec > 0) && (
          <>
            <span style={{ fontFamily:"'Space Mono',monospace", fontSize:11, color:PC.accent, fontWeight:700, flexShrink:0 }}>
              {fmt(playheadSec)}
            </span>
            <button onClick={handleTestStop}
              style={{ padding:'4px 8px', borderRadius:4, border:'none', background:'transparent', color:PC.muted, fontFamily:"'Archivo',sans-serif", fontWeight:700, fontSize:11, cursor:'pointer', flexShrink:0 }}>
              ⏹ Stopp
            </button>
          </>
        )}

        <div style={{ width:1, height:18, background:PC.line, flexShrink:0 }} />
        <span style={{ fontFamily:"'Archivo',sans-serif", fontSize:11, color:PC.dim, whiteSpace:'nowrap', flexShrink:0 }}>{session.parts.length} deler · {totalIvs} iv · {session.tracks.length} spor</span>
        <div style={{ marginLeft:'auto', display:'flex', gap:6, flexShrink:0 }}>
          {tb('↶ Angre',   undo,      false, !canUndo)}
          {tb('↷ Gjør om', redo,      false, !canRedo)}
          {tb('Eksporter JSON', exportFn)}
        </div>
      </div>

      {/* WORKSPACE */}
      <div style={{ flex:1, display:'flex', overflow:'hidden' }}>
        <SpotifyDrawer />
        <Timeline session={session} pxPerSec={pxPerSec} selection={selection} setSelection={setSelection}
          dispatch={dispatch} silentUpdate={silentUpdate} scrollRef={scrollRef} onDropTrack={handleDropTrack}
          playheadSec={playheadSec} onSeek={handleSeek} testPlaying={testPlaying} />
        <Inspector selection={selection} session={session} dispatch={dispatch} setSelection={setSelection}
          onOpenGenerator={id=>setGenModal(id)} />
      </div>

      {genModal!==null && (
        <GeneratorModal
          partName={genModal==='new' ? 'Intervalldel' : (session.parts.find(p=>p.id===genModal)?.name||'Intervalldel')}
          onClose={()=>setGenModal(null)}
          onGenerate={data=>handleGenerate(genModal, data)} />
      )}

      {sensorModal && (
        <SensorModal session={session} dispatch={dispatch} onClose={()=>setSensorModal(false)} />
      )}

      {showSaveAsModal && (
        <SaveAsCloudModal
          defaultName={session.name}
          onClose={()=>setShowSaveAsModal(false)}
          onSave={handleSaveAsNewToCloud}
        />
      )}

      {showCloudModal && (
        <CloudProgramsModal
          dirty={dirty}
          onOpen={handleOpenCloudProgram}
          onClose={()=>setShowCloudModal(false)}
        />
      )}

      {showShareModal && cloudId && (
        <ShareModal cloudId={cloudId} onClose={()=>setShowShareModal(false)} />
      )}

      {showAuthModal && (
        <div onClick={e=>{if(e.target===e.currentTarget)setShowAuthModal(false);}}
          style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.4)', display:'flex', alignItems:'center', justifyContent:'center', zIndex:200, backdropFilter:'blur(3px)' }}>
          <div style={{ background:PC.paper, border:`1px solid ${PC.line2}`, borderRadius:8, padding:28 }}>
            <AuthScreen onSuccess={()=>setShowAuthModal(false)} />
          </div>
        </div>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
