/*
 * app.js — 分弦（ES module，录音工作台风格）：
 * 分轨方案选择 → POST /api/analyze（file + profile）→ 轮询 /api/jobs/{job}
 * → 多轨调音台（每轨独立 Audio + WaveSurfer Region 块，统一时钟，
 *   Solo/Mute/推子+dB，全局播放头，波形点选 seek）
 */
import WaveSurfer from './vendor/wavesurfer.esm.js';

const $ = (id) => document.getElementById(id);
const els = {
  viewUpload: $('view-upload'),
  viewProcessing: $('view-processing'),
  viewResult: $('view-result'),
  dropzone: $('dropzone'),
  fileInput: $('file-input'),
  btnDemo: $('btn-demo'),
  profilePicker: $('profile-picker'),
  outputFormat: $('output-format'),
  setupWave: $('setup-wave'),
  setupLcd: $('setup-lcd'),
  processingFile: $('processing-file'),
  btnProcessingRestart: $('btn-processing-restart'),
  progressBar: $('progress-bar'),
  progressPct: $('progress-pct'),
  stageList: $('stage-list'),
  subStatus: $('sub-status'),
  songName: $('song-name'),
  songMeta: $('song-meta'),
  btnRestart: $('btn-restart'),
  btnRewind: $('btn-rewind'),
  btnMasterPlay: $('btn-master-play'),
  masterTime: $('master-time'),
  masterDuration: $('master-duration'),
  masterCount: $('master-count'),
  btnExportSel: $('btn-export-sel'),
  exportSelLabel: $('export-sel-label'),
  tracksWrap: $('tracks-wrap'),
  rulerCanvas: $('ruler-canvas'),
  playhead: $('playhead'),
  trackListEl: $('track-list'),
  statusBar: $('status-bar'),
  toast: $('toast')
};

const STAGES = ['decode', 'separate', 'export'];
// Logic 轨道色板：饱和但暗一档
const COLOR_ORIGINAL = '#8E8E93';
const COLOR_MELODY = '#E8A33D';   // 人声
const COLOR_ACCOMP = '#3D9BE8';   // 伴奏
const COLOR_BY_NAME = {
  '鼓': '#E85C5C', '贝斯': '#9B6BE8', '吉他': '#4FC27E', '钢琴': '#54C8D8'
};
const STEM_FALLBACK = ['#D8C84F', '#9B6BE8', '#4FC27E', '#54C8D8'];
const KIND_LABEL = { original: '原曲', melody: '人声', accompaniment: '伴奏', stem: '乐器' };
const PLAY_SVG = '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>';
const PAUSE_SVG = '<svg viewBox="0 0 24 24"><path d="M6 5h4v14H6zM14 5h4v14h-4z"/></svg>';
const DL_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12m0 0l-4.5-4.5M12 15l4.5-4.5M4 20h16"/></svg>';

let currentJob = null;
let result = null;
let uploadedFile = null;
let objectUrl = null;
let fileName = '';
let pollTimer = null;
let selectedProfile = 'vocal';
let tracks = [];          // [{key,name,kind,url,color,audio,ws,solo,mute,volume,el,waveEl,dbEl}]
let playing = false;
let clockRaf = null;
let driftCheckAt = 0;

// ------------------------------------------------------------ 工具 ------
function toast(msg) {
  els.toast.textContent = msg;
  els.toast.classList.add('show');
  clearTimeout(toast._t);
  toast._t = setTimeout(() => els.toast.classList.remove('show'), 3200);
}

async function errText(resp) {
  try {
    const d = await resp.json();
    return d.detail || resp.statusText || ('HTTP ' + resp.status);
  } catch (e) {
    return resp.statusText || ('HTTP ' + resp.status);
  }
}

// mm:ss.cs
function fmtCs(sec) {
  if (!isFinite(sec) || sec < 0) sec = 0;
  const m = Math.floor(sec / 60);
  const s = Math.floor(sec % 60);
  const cs = Math.floor((sec % 1) * 100);
  return m + ':' + String(s).padStart(2, '0') + '.' + String(cs).padStart(2, '0');
}

function hexRgb(hex) {
  return [
    parseInt(hex.slice(1, 3), 16),
    parseInt(hex.slice(3, 5), 16),
    parseInt(hex.slice(5, 7), 16)
  ];
}
function hexA(hex, a) {
  const [r, g, b] = hexRgb(hex);
  return `rgba(${r},${g},${b},${a})`;
}
// Region 波形配色：波形线 = 轨道色深一档，进度 = 浅一档
function shade(hex, f) {
  const [r, g, b] = hexRgb(hex);
  const m = (c) => Math.max(0, Math.min(255, Math.round(c * f)));
  return `rgb(${m(r)},${m(g)},${m(b)})`;
}
function tint(hex, f) {
  const [r, g, b] = hexRgb(hex);
  const m = (c) => Math.round(c + (255 - c) * f);
  return `rgb(${m(r)},${m(g)},${m(b)})`;
}
function fmtDb(v) {
  if (v <= 0) return '-inf';
  const db = 20 * Math.log10(v);
  return (db > 0 ? '+' : '') + db.toFixed(1);
}

function showView(name) {
  const views = { upload: els.viewUpload, processing: els.viewProcessing, result: els.viewResult };
  for (const k in views) views[k].classList.toggle('hidden', k !== name);
  window.scrollTo({ top: 0 });
}

// ------------------------------------------------------------ 进度 ------
function updateProgress(j) {
  const idx = j.stageIndex || 0;
  const done = j.status === 'done';
  els.stageList.querySelectorAll('li').forEach((li) => {
    const i = STAGES.indexOf(li.dataset.stage);
    li.classList.toggle('done', done || i < idx);
    li.classList.toggle('active', !done && i === idx);
    li.querySelector('.stage-badge').textContent = (done || i < idx) ? '✓' : '';
  });
  const p = Math.max(0, Math.min(1, j.progress || 0));
  const indeterminate = j.stage === 'separate' && /逐段/.test(j.message || '');
  els.progressBar.classList.toggle('indeterminate', indeterminate);
  els.progressBar.style.width = Math.round(p * 100) + '%';
  els.progressPct.textContent = indeterminate ? '处理中' : Math.round(p * 100) + '%';
  els.subStatus.textContent = j.message || '';
}

function resetProgress() {
  els.progressPct.textContent = '0%';
  els.progressBar.style.width = '0%';
  els.progressBar.classList.remove('indeterminate');
  els.subStatus.textContent = '';
  els.stageList.querySelectorAll('li').forEach((li) => {
    li.classList.remove('done', 'active');
    li.querySelector('.stage-badge').textContent = '';
  });
}

function friendlyJobError(message) {
  if (/MPS backend out of memory|out of memory/i.test(message || '')) {
    return '电脑当时可用内存不足，分离已经停止。请关闭一些占用较大的应用后重新选择音频；原文件没有损坏。';
  }
  return '这次分离没有完成：' + (message || '本机处理发生错误');
}

// ------------------------------------------------------------ 档位 ------
els.profilePicker.addEventListener('click', (e) => {
  const card = e.target.closest('.profile-card');
  if (!card) return;
  selectedProfile = card.dataset.profile;
  els.profilePicker.querySelectorAll('.profile-card').forEach((c) =>
    c.classList.toggle('active', c === card));
});

// ------------------------------------------------------------ 上传 ------
els.fileInput.addEventListener('change', (e) => {
  if (e.target.files[0]) handleFile(e.target.files[0]);
  e.target.value = '';
});
['dragover', 'dragenter'].forEach((ev) =>
  els.dropzone.addEventListener(ev, (e) => {
    e.preventDefault();
    e.stopPropagation();
    if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
    els.dropzone.classList.add('dragover');
  }));
['dragleave', 'drop'].forEach((ev) =>
  els.dropzone.addEventListener(ev, (e) => {
    e.preventDefault();
    e.stopPropagation();
    els.dropzone.classList.remove('dragover');
  }));
els.dropzone.addEventListener('drop', (e) => {
  const f = e.dataTransfer.files && e.dataTransfer.files[0];
  if (f) handleFile(f);
});

async function handleFile(file) {
  if (!file.type.startsWith('audio') && !/\.(mp3|wav|m4a|flac|ogg|aac)$/i.test(file.name)) {
    toast('请上传音频文件（MP3 / WAV / M4A / FLAC / OGG）');
    return;
  }
  if (file.size > 500 * 1024 * 1024) {
    toast('音频超过 500MB，请先压缩或截取需要处理的部分');
    return;
  }
  teardownMixer();
  clearInterval(pollTimer);
  uploadedFile = file;
  fileName = file.name.replace(/\.[^.]+$/, '');
  if (objectUrl) URL.revokeObjectURL(objectUrl);
  objectUrl = URL.createObjectURL(file);

  resetProgress();
  document.querySelector('.processing-panel h2').textContent = '正在分离';
  showView('processing');
  els.processingFile.textContent = file.name;

  try {
    const fd = new FormData();
    fd.append('file', file);
    fd.append('profile', selectedProfile);
    fd.append('output', els.outputFormat.value);
    const resp = await fetch('/api/analyze', { method: 'POST', body: fd });
    if (!resp.ok) throw new Error(await errText(resp));
    const data = await resp.json();
    currentJob = data.job;
    pollJob(data.job);
  } catch (err) {
    console.error(err);
    toast('上传失败：' + (err && err.message ? err.message : '网络错误'));
    showView('upload');
  }
}

function pollJob(job) {
  clearInterval(pollTimer);
  let missed = 0;
  let checking = false;
  pollTimer = setInterval(async () => {
    if (checking) return;
    checking = true;
    try {
      const resp = await fetch('/api/jobs/' + job);
      if (resp.status === 404) {
        clearInterval(pollTimer);
        document.querySelector('.processing-panel h2').textContent = '没有完成';
        els.progressPct.textContent = '已停止';
        els.subStatus.textContent = '本机服务重新启动过，原任务记录已经中断。请重新选择音频；原文件没有损坏。';
        return;
      }
      if (!resp.ok) throw new Error(await errText(resp));
      const j = await resp.json();
      missed = 0;
      if (j.status === 'done') {
        clearInterval(pollTimer);
        updateProgress({ ...j, progress: 1 });
        result = j.result;
        setTimeout(() => {
          renderResults();
          showView('result');
        }, 350);
      } else if (j.status === 'error') {
        clearInterval(pollTimer);
        document.querySelector('.processing-panel h2').textContent = '没有完成';
        els.progressPct.textContent = '已停止';
        els.subStatus.textContent = friendlyJobError(j.message);
        toast('这次分离没有完成');
      } else {
        updateProgress(j);
      }
    } catch (err) {
      console.error(err);
      missed += 1;
      els.subStatus.textContent = '本机服务暂时没有回应，正在自动重连（第 ' + missed + ' 次）';
      if (missed === 1) toast('连接短暂中断，正在自动重连');
    } finally {
      checking = false;
    }
  }, 800);
}

// ------------------------------------------------------------ 演示 ------
els.btnDemo.addEventListener('click', async () => {
  els.btnDemo.disabled = true;
  try {
    const resp = await fetch('/test-song.wav');
    if (!resp.ok) throw new Error('HTTP ' + resp.status);
    const blob = await resp.blob();
    handleFile(new File([blob], 'test-song.wav', { type: 'audio/wav' }));
  } catch (err) {
    console.error(err);
    toast('演示曲载入失败：' + (err && err.message ? err.message : ''));
  } finally {
    els.btnDemo.disabled = false;
  }
});

// ------------------------------------------------------------ 重开 ------
els.btnRestart.addEventListener('click', () => {
  teardownMixer();
  clearInterval(pollTimer);
  result = null;
  currentJob = null;
  resetProgress();
  showView('upload');
});
els.btnProcessingRestart.addEventListener('click', () => els.btnRestart.click());

// ------------------------------------------------------------ 结果 ------
function renderResults() {
  const r = result;
  const formatLabel = { mp3: 'MP3', wav: 'WAV', both: 'MP3 + WAV' }[r.output] || 'MP3';
  const filtered = (r.filteredTracks || []).length;
  els.songName.textContent = fileName || '未命名';
  els.songMeta.textContent =
    (r.profileLabel || r.profile || '') + ' · ' + formatLabel + ' · ' +
    fmtCs(r.duration) + (filtered ? ' · ' + filtered + ' 条弱轨未交付' : '');
  els.masterCount.textContent = (r.tracks ? r.tracks.length : 0) + ' 条分轨 + 原曲';
  els.masterDuration.textContent = fmtCs(r.duration);
  els.masterTime.textContent = fmtCs(0);
  els.statusBar.textContent =
    '时长 ' + fmtCs(r.duration) + ' · ' + (r.tracks ? r.tracks.length : 0) +
    ' 条清晰分轨 · 空格 播放/暂停 · 点击波形移动播放头 · S 只听这一轨 · M 关闭这一轨';
  buildMixer();
  drawRuler();
  updatePlayhead();
}

// ------------------------------------------------------------ 时间标尺 --
function drawRuler() {
  const c = els.rulerCanvas;
  if (!c || !result) return;
  const dpr = Math.min(2, window.devicePixelRatio || 1);
  const w = c.clientWidth * dpr, h = c.clientHeight * dpr;
  if (w <= 0) return;
  if (c.width !== w || c.height !== h) { c.width = w; c.height = h; }
  const ctx = c.getContext('2d');
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  const W = c.clientWidth, H = c.clientHeight;
  ctx.clearRect(0, 0, W, H);
  const dur = result.duration || 1;
  ctx.strokeStyle = '#4A4A4A';
  ctx.fillStyle = '#7A7A7A';
  ctx.font = '9px ' + '"SF Mono", ui-monospace, Menlo, monospace';
  ctx.textAlign = 'left';
  ctx.lineWidth = 1;
  for (let s = 0; s <= Math.ceil(dur); s++) {
    const x = Math.round((s / dur) * W) + 0.5;
    const major = s % 5 === 0;
    ctx.beginPath();
    ctx.moveTo(x, major ? H - 12 : H - 6);
    ctx.lineTo(x, H);
    ctx.stroke();
    if (major) {
      const label = Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
      ctx.fillText(label, x + 3, H - 12);
    }
  }
}
window.addEventListener('resize', () => {
  drawRuler();
  updatePlayhead();
});

// ------------------------------------------------------------ 播放头 ----
function updatePlayhead() {
  if (!tracks.length || !result) return;
  const m = master();
  const dur = result.duration || (m && m.duration) || 1;
  const frac = Math.min(1, Math.max(0, (m ? m.currentTime : 0) / dur));
  const wave = tracks[0].waveEl;
  if (!wave) return;
  const wrapRect = els.tracksWrap.getBoundingClientRect();
  const waveRect = wave.getBoundingClientRect();
  const x = waveRect.left - wrapRect.left + frac * waveRect.width;
  els.playhead.style.display = 'block';
  els.playhead.style.transform = 'translateX(' + x + 'px)';
}

// ------------------------------------------------------------ 调音台 ----
function trackColor(kind, name, stemIdx) {
  if (kind === 'original') return COLOR_ORIGINAL;
  if (kind === 'melody') return COLOR_MELODY;
  if (kind === 'accompaniment') return COLOR_ACCOMP;
  return COLOR_BY_NAME[name] || STEM_FALLBACK[stemIdx % STEM_FALLBACK.length];
}

function teardownMixer() {
  pauseAll();
  for (const t of tracks) {
    try { t.ws && t.ws.destroy(); } catch (e) { /* 忽略 */ }
    t.audio.pause();
    t.audio.removeAttribute('src');
    t.audio.load();
  }
  tracks = [];
  playing = false;
  els.trackListEl.innerHTML = '';
  els.playhead.style.display = 'none';
  updateMasterBtn();
}

function buildMixer() {
  teardownMixer();
  const defs = [{
    key: 'original', name: '原曲', kind: 'original',
    url: objectUrl, download: uploadedFile ? uploadedFile.name : 'original'
  }];
  (result.tracks || []).forEach((t, i) => {
    defs.push({
      key: 't' + i, name: t.name, kind: t.kind, url: t.url,
      downloads: Object.entries(t.files || {}).map(([format, item]) => ({
        url: item.url, name: item.name, label: format.toUpperCase()
      }))
    });
  });

  let stemIdx = 0;
  defs.forEach((d) => {
    d.color = trackColor(d.kind, d.name, d.kind === 'stem' ? stemIdx++ : 0);
    addMixerTrack(d);
  });
  applyGains();
}

// 建一条混音台通道行
function addMixerTrack(d) {
  const color = d.color;
  const audio = new Audio();
  audio.preload = 'auto';
  audio.src = d.url;

  const t = {
    ...d, color, audio, ws: null,
    solo: false, mute: d.key === 'original', volume: 1,
    exportSelected: d.key !== 'original',
    el: null, waveEl: null, dbEl: null
  };
  tracks.push(t);

  // ---- 行 DOM：左侧控制条 + 右侧 Region 波形 ----
  const ch = document.createElement('div');
  ch.className = 'channel';
  ch.classList.toggle('selected', t.exportSelected);
  ch.style.setProperty('--ch', color);
  ch.style.setProperty('--ch-fill', hexA(color, 0.85));

  const controls = document.createElement('div');
  controls.className = 'ch-controls';

  let exportToggle = null;
  if (d.key !== 'original') {
    exportToggle = document.createElement('input');
    exportToggle.type = 'checkbox';
    exportToggle.className = 'ch-export';
    exportToggle.checked = true;
    exportToggle.title = '加入下载包';
    exportToggle.setAttribute('aria-label', '选择下载 ' + d.name);
    exportToggle.addEventListener('change', () => {
      t.exportSelected = exportToggle.checked;
      ch.classList.toggle('selected', t.exportSelected);
      updateExportLabel();
    });
  }

  const nameEl = document.createElement('span');
  nameEl.className = 'ch-name';
  nameEl.textContent = d.name;
  nameEl.title = d.name;
  const kindEl = document.createElement('span');
  kindEl.className = 'ch-kind';
  kindEl.textContent = KIND_LABEL[d.kind] || 'STEM';

  const spacer = document.createElement('span');
  spacer.className = 'spacer';

  const soloBtn = document.createElement('button');
  soloBtn.className = 'ch-flag ch-solo';
  soloBtn.textContent = 'S';
  soloBtn.title = 'Solo';
  soloBtn.setAttribute('aria-pressed', 'false');
  soloBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    t.solo = !t.solo;
    soloBtn.classList.toggle('active', t.solo);
    soloBtn.setAttribute('aria-pressed', String(t.solo));
    applyGains();
  });

  const muteBtn = document.createElement('button');
  muteBtn.className = 'ch-flag ch-mute';
  muteBtn.textContent = 'M';
  muteBtn.title = 'Mute';
  muteBtn.classList.toggle('active', t.mute);
  muteBtn.setAttribute('aria-pressed', String(t.mute));
  muteBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    t.mute = !t.mute;
    muteBtn.classList.toggle('active', t.mute);
    muteBtn.setAttribute('aria-pressed', String(t.mute));
    applyGains();
  });

  const vol = document.createElement('input');
  vol.type = 'range';
  vol.className = 'ch-vol';
  vol.min = '0'; vol.max = '100'; vol.value = '100';
  vol.setAttribute('aria-label', d.name + ' 音量');
  const dbEl = document.createElement('span');
  dbEl.className = 'ch-db';
  dbEl.textContent = fmtDb(1);
  vol.addEventListener('input', () => {
    t.volume = vol.value / 100;
    dbEl.textContent = fmtDb(t.volume);
    applyGains();
  });

  const downloadDefs = d.downloads && d.downloads.length
    ? d.downloads
    : [{ url: d.url, name: d.download, label: '' }];
  const downloadLinks = downloadDefs.map((item) => {
    const link = document.createElement('a');
    link.className = 'ch-dl' + (downloadDefs.length > 1 ? ' format' : '');
    link.href = item.url;
    link.download = item.name;
    link.title = '下载 ' + d.name + (item.label ? ' ' + item.label : '');
    link.setAttribute('aria-label', link.title);
    if (downloadDefs.length > 1) link.textContent = item.label;
    else link.innerHTML = DL_SVG;
    link.addEventListener('click', (e) => e.stopPropagation());
    return link;
  });

  if (exportToggle) controls.appendChild(exportToggle);
  controls.appendChild(nameEl);
  controls.appendChild(kindEl);
  controls.appendChild(spacer);
  controls.appendChild(soloBtn);
  controls.appendChild(muteBtn);
  controls.appendChild(vol);
  controls.appendChild(dbEl);
  downloadLinks.forEach((link) => controls.appendChild(link));

  const waveWrap = document.createElement('div');
  waveWrap.className = 'ch-wave';
  const region = document.createElement('div');
  region.className = 'ch-region';
  waveWrap.appendChild(region);

  ch.appendChild(controls);
  ch.appendChild(waveWrap);
  els.trackListEl.appendChild(ch);
  t.el = ch;
  t.waveEl = waveWrap;
  t.dbEl = dbEl;

  // 点轨道左侧空白处，也可以切换是否加入下载包
  ch.addEventListener('click', (e) => {
    if (!exportToggle || e.target.closest('.ch-export, .ch-flag, .ch-vol, .ch-dl, .ch-region')) return;
    t.exportSelected = !t.exportSelected;
    exportToggle.checked = t.exportSelected;
    ch.classList.toggle('selected', t.exportSelected);
    updateExportLabel();
  });

  // Region 波形：底色 = 轨道色 85%，波形线 = 深一档，进度 = 浅一档
  const ws = WaveSurfer.create({
    container: region,
    media: audio,
    height: 52,
    barWidth: 2,
    barGap: 1,
    waveColor: shade(color, 0.55),
    progressColor: tint(color, 0.35),
    cursorWidth: 0,
    normalize: true
  });
  t.ws = ws;
  ws.on('interaction', (newTime) => seekAll(newTime));
  ws.on('error', (e) => console.warn('波形加载失败:', d.name, e));

  if (d.key === 'original') {
    audio.addEventListener('ended', () => {
      pauseAll();
      seekAll(0);
    });
  }
  return t;
}

// ------------------------------------------------------------ 播放引擎 --
function master() { return tracks[0] ? tracks[0].audio : null; }

function audible(t) {
  const anySolo = tracks.some((x) => x.solo);
  const muted = anySolo ? (!t.solo || t.mute) : t.mute;
  return !muted;
}

function applyGains() {
  for (const t of tracks) {
    t.audio.volume = audible(t) ? t.volume : 0;
  }
  updateMasterBtn();
  updateExportLabel();
}

// 当前勾选下载的母分轨（不含原曲与子轨）→ result.tracks 序号
function selectedTrackIndices() {
  return tracks
    .filter((t) => t.key !== 'original' && t.exportSelected)
    .map((t) => parseInt(t.key.slice(1), 10))
    .filter((i) => !isNaN(i));
}

function updateExportLabel() {
  if (!result || !result.tracks) return;
  const idxs = selectedTrackIndices();
  els.btnExportSel.disabled = idxs.length === 0;
  if (!idxs.length) {
    els.exportSelLabel.textContent = '还没有选择音轨';
    return;
  }
  if (idxs.length === result.tracks.length) {
    els.exportSelLabel.textContent = '选中：全部 ' + idxs.length + ' 轨';
  } else {
    const names = idxs.map((i) => result.tracks[i] ? result.tracks[i].name : '').filter(Boolean);
    els.exportSelLabel.textContent = '选中：' + names.join('、');
  }
}

els.btnExportSel.addEventListener('click', async () => {
  if (!currentJob || !result) return;
  const idxs = selectedTrackIndices();
  if (!idxs.length) {
    toast('请先勾选至少一条音轨');
    return;
  }
  const btn = els.btnExportSel;
  const oldText = btn.textContent;
  btn.disabled = true;
  btn.textContent = '打包中…';
  try {
    const resp = await fetch('/api/export-zip', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ job: currentJob, tracks: idxs })
    });
    if (!resp.ok) throw new Error(await errText(resp));
    const blob = await resp.blob();
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = (fileName || 'tracks') + '_选中分轨.zip';
    a.click();
    setTimeout(() => URL.revokeObjectURL(url), 5000);
    toast('已下载 ' + idxs.length + ' 条音轨（ZIP）');
  } catch (err) {
    console.error(err);
    toast('导出失败：' + (err && err.message ? err.message : '网络错误'));
  } finally {
    btn.disabled = false;
    btn.textContent = oldText;
  }
});

function seekAll(time) {
  if (!isFinite(time)) return;
  for (const t of tracks) {
    try { t.audio.currentTime = time; } catch (e) { /* 元数据未就绪 */ }
  }
  if (!playing) els.masterTime.textContent = fmtCs(time);
  updatePlayhead();
}

async function playAll() {
  const m = master();
  if (!m || !tracks.length) return;
  const t0 = m.currentTime;
  for (const t of tracks) {
    if (Math.abs(t.audio.currentTime - t0) > 0.04) {
      try { t.audio.currentTime = t0; } catch (e) { /* 忽略 */ }
    }
  }
  const results = await Promise.all(tracks.map((t) => t.audio.play().catch((e) => e)));
  const failed = results.filter((r) => r instanceof Error);
  if (failed.length === tracks.length) {
    toast('播放失败，请再试一次');
    return;
  }
  playing = true;
  updateMasterBtn();
  startClock();
}

function pauseAll() {
  for (const t of tracks) t.audio.pause();
  playing = false;
  updateMasterBtn();
  stopClock();
}

function updateMasterBtn() {
  els.btnMasterPlay.innerHTML = playing ? PAUSE_SVG : PLAY_SVG;
  els.btnMasterPlay.classList.toggle('playing', playing);
  els.btnMasterPlay.setAttribute('aria-pressed', String(playing));
}

els.btnMasterPlay.addEventListener('click', () => {
  if (playing) pauseAll();
  else playAll();
});

document.addEventListener('keydown', (e) => {
  if (e.code !== 'Space' || e.repeat || els.viewResult.classList.contains('hidden')) return;
  if (e.target instanceof Element && e.target.closest('button, a, input, select, textarea, [contenteditable="true"]')) return;
  e.preventDefault();
  els.btnMasterPlay.click();
});

els.btnRewind.addEventListener('click', () => {
  seekAll(0);
});

function startClock() {
  stopClock();
  const tick = () => {
    if (!playing) return;
    const m = master();
    if (m) {
      els.masterTime.textContent = fmtCs(m.currentTime);
      updatePlayhead();
      // 漂移校正（每 ~0.5s 一次）
      const now = performance.now();
      if (now >= driftCheckAt) {
        driftCheckAt = now + 500;
        const ref = m.currentTime;
        for (const t of tracks) {
          if (t.audio !== m && !t.audio.paused && Math.abs(t.audio.currentTime - ref) > 0.12) {
            try { t.audio.currentTime = ref; } catch (e) { /* 忽略 */ }
          }
        }
      }
    }
    clockRaf = requestAnimationFrame(tick);
  };
  clockRaf = requestAnimationFrame(tick);
}

function stopClock() {
  if (clockRaf) { cancelAnimationFrame(clockRaf); clockRaf = null; }
}

// ------------------------------------------------------------ 上传态装饰 --
// 面板顶部：多轨彩色 Region 波形缓慢滚动/呼吸（低透明度，纯装饰）
const SETUP_LANES = ['#E8A33D', '#3D9BE8', '#E85C5C', '#9B6BE8', '#4FC27E', '#54C8D8', '#D8C84F'];
const setupWaveFx = {
  lanes: null,
  gen() {
    // 每条泳道一组伪随机柱高（滚动时循环使用）
    let seed = 7;
    const rand = () => {
      seed = (seed * 1103515245 + 12345) & 0x7fffffff;
      return seed / 0x7fffffff;
    };
    this.lanes = SETUP_LANES.map((color, li) => {
      const cols = 160;
      const bars = new Float32Array(cols);
      for (let i = 0; i < cols; i++) {
        const t = i / cols;
        const env = 0.4 + 0.6 * Math.abs(Math.sin(t * Math.PI * (2.1 + li * 0.7)));
        bars[i] = Math.min(1, env * (0.5 + rand() * 0.5));
      }
      return { color, bars, speed: 12 + li * 3.5, phase: li * 1.3 };
    });
  },
  draw(scrollT) {
    const c = els.setupWave;
    if (!c) return;
    const dpr = Math.min(2, window.devicePixelRatio || 1);
    const w = c.clientWidth * dpr, h = c.clientHeight * dpr;
    if (w <= 0) return;
    if (c.width !== w || c.height !== h) { c.width = w; c.height = h; }
    const ctx = c.getContext('2d');
    ctx.clearRect(0, 0, w, h);
    const n = this.lanes.length;
    const laneH = h / n;
    for (const lane of this.lanes) {
      const li = this.lanes.indexOf(lane);
      const cy = laneH * li + laneH / 2;
      const regionH = laneH * 0.66;
      // 呼吸：透明度随时间缓变（0.15 - 0.3）
      const breathe = 0.225 + 0.075 * Math.sin(scrollT * 0.6 + lane.phase);
      // Region 底
      const [r, g, b] = hexRgb(lane.color);
      ctx.fillStyle = `rgba(${r},${g},${b},${breathe * 0.55})`;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(0, cy - regionH / 2, w, regionH, 3 * dpr);
      else ctx.rect(0, cy - regionH / 2, w, regionH);
      ctx.fill();
      // 内部波形柱（深一档），按速度横向滚动
      const bars = lane.bars;
      const bw = w / 220; // 屏上约 220 柱
      const offset = (scrollT * lane.speed * dpr) % (bars.length * bw);
      ctx.fillStyle = `rgba(${Math.round(r * 0.55)},${Math.round(g * 0.55)},${Math.round(b * 0.55)},${breathe})`;
      const total = bars.length * bw;
      for (let x = -offset; x < w; x += bw * 4) {
        // 每 4 步取一柱，降低密度
        const bi = ((Math.round((x + offset) / bw) % bars.length) + bars.length) % bars.length;
        const bh = Math.max(2 * dpr, bars[bi] * (regionH - 4 * dpr));
        ctx.fillRect(x, cy - bh / 2, Math.max(1, bw * 1.6), bh);
      }
    }
  },
  start() {
    if (!this.lanes) this.gen();
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      this.draw(0); // 降级：静态一帧
      return;
    }
    if (this.running) return;
    this.running = true;
    const tick = () => {
      if (els.viewUpload.classList.contains('hidden')) {
        this.running = false;
        return;
      }
      this.draw(performance.now() / 1000);
      requestAnimationFrame(tick);
    };
    requestAnimationFrame(tick);
  }
};
setupWaveFx.start();

// 回到上传视图时重启波形动画
new MutationObserver(() => {
  if (!els.viewUpload.classList.contains('hidden')) setupWaveFx.start();
}).observe(els.viewUpload, { attributes: true, attributeFilter: ['class'] });
