import { api } from '../api.js';
import { showToast } from '../components/toast.js';
import { esc, hydrateAuthImages } from '../utils.js';
import { t, tn } from '../i18n.js';
import { frameDeviceOutput, displayAspectRatio } from '../lib/device-frame.js';

function formatDate(ts) {
  if (!ts) return '--';
  return new Date(ts * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
}

function getTypeIcon(item) {
  if (item.widget_id) return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/><rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/></svg>';
  if (item.mime_type && item.mime_type.startsWith('video/')) return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>';
  return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>';
}

// #74/#75 per-item schedule editor helpers. Client validation MIRRORS the server
// (server/routes/playlists.js validateBlocks): same time/date regexes, non-empty days.
const SCHED_TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
const SCHED_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;

function daysSummary(days) {
  const labels = t('itemsched.dow_short').split(',');
  const s = [...days].sort((a, b) => a - b);
  if (s.length === 7) return t('itemsched.every_day');
  if (s.length === 5 && [1, 2, 3, 4, 5].every(d => s.includes(d))) return t('itemsched.mon_fri');
  if (s.length === 2 && s.includes(0) && s.includes(6)) return t('itemsched.sat_sun');
  return s.map(d => labels[d]).join(' ');
}
function blockSummary(b) {
  let s = `${daysSummary(b.days)} ${b.start}-${b.end}`;
  if (b.start_date || b.end_date) s += ` · ${b.start_date || '…'}→${b.end_date || '…'}`;
  return s;
}
function scheduleSummary(schedules) {
  if (!schedules || !schedules.length) return '';
  return schedules.length === 1 ? blockSummary(schedules[0]) : `${blockSummary(schedules[0])} +${schedules.length - 1}`;
}
function validateScheduleBlocks(blocks) {
  for (const b of blocks) {
    if (!b.days || !b.days.length) return t('itemsched.err.days');
    if (!SCHED_TIME_RE.test(b.start)) return t('itemsched.err.start');
    if (!(SCHED_TIME_RE.test(b.end) || b.end === '24:00')) return t('itemsched.err.end');
    if (b.start_date && !SCHED_DATE_RE.test(b.start_date)) return t('itemsched.err.start_date');
    if (b.end_date && !SCHED_DATE_RE.test(b.end_date)) return t('itemsched.err.end_date');
  }
  return null;
}

let currentPlaylistId = null;

export function render(container) {
  const hash = window.location.hash;
  const match = hash.match(/#\/playlists\/(.+)/);
  if (match) {
    currentPlaylistId = match[1];
    renderDetail(container, match[1]);
  } else {
    currentPlaylistId = null;
    renderList(container);
  }
}

export function cleanup() {
  currentPlaylistId = null;
}

let showAutoGenerated = true;

async function renderList(container) {
  container.innerHTML = `
    <div class="page-header">
      <div>
        <h1>${t('playlist.title')} <span class="help-tip" data-tip="${t('playlist.help_tip')}">?</span></h1>
        <div class="subtitle">${t('playlist.subtitle')}</div>
      </div>
      <div style="display:flex;gap:8px;align-items:center">
        <label style="display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text-secondary);cursor:pointer">
          <input type="checkbox" id="showAutoToggle" ${showAutoGenerated ? 'checked' : ''}>
          ${t('playlist.show_auto_generated')}
        </label>
        <button class="btn btn-primary" id="createPlaylistBtn">${t('playlist.new_playlist_btn')}</button>
      </div>
    </div>
    <div id="playlistGrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px">
      <div style="color:var(--text-muted);padding:40px;text-align:center">${t('common.loading')}</div>
    </div>
  `;

  document.getElementById('createPlaylistBtn').addEventListener('click', showCreateModal);
  document.getElementById('showAutoToggle').addEventListener('change', (e) => {
    showAutoGenerated = e.target.checked;
    loadPlaylists();
  });
  loadPlaylists();
}

async function loadPlaylists() {
  const grid = document.getElementById('playlistGrid');
  if (!grid) return;

  try {
    const playlists = await api.getPlaylists();
    if (!playlists.length) {
      grid.innerHTML = `
        <div style="grid-column:1/-1;text-align:center;padding:60px 20px;color:var(--text-muted)">
          <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="margin:0 auto 16px;display:block;opacity:0.4">
            <line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/>
            <line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
          </svg>
          <h3 style="margin-bottom:8px;color:var(--text-primary)">${t('playlist.empty_title')}</h3>
          <p>${t('playlist.empty_desc')}</p>
        </div>
      `;
      return;
    }

    const filtered = showAutoGenerated ? playlists : playlists.filter(p => !p.is_auto_generated);
    if (!filtered.length) {
      grid.innerHTML = `
        <div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--text-muted)">
          ${playlists.length ? t('playlist.all_auto_generated') : ''}
        </div>
      `;
      return;
    }

    grid.innerHTML = filtered.map(p => `
      <a href="#/playlists/${esc(p.id)}" class="playlist-card" style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px;text-decoration:none;color:inherit;display:block;transition:border-color 0.15s;cursor:pointer">
        <div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:12px">
          <div style="display:flex;align-items:center;gap:8px">
            <div style="font-size:16px;font-weight:600;color:var(--text-primary)">${esc(p.name)}</div>
            ${p.is_auto_generated ? `<span style="font-size:10px;padding:2px 6px;border-radius:4px;background:var(--bg-input);color:var(--text-muted)">${t('playlist.tag_auto')}</span>` : ''}
            ${p.status === 'draft' ? `<span style="font-size:10px;padding:2px 6px;border-radius:4px;background:#78350f;color:#fbbf24">${t('playlist.tag_draft')}</span>` : ''}
          </div>
          <div style="font-size:12px;color:var(--text-muted);white-space:nowrap;margin-left:12px">${tn('playlist.item_count', p.item_count)}</div>
        </div>
        ${p.description ? `<div style="font-size:13px;color:var(--text-secondary);margin-bottom:12px;line-height:1.4">${esc(p.description)}</div>` : ''}
        <div style="display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted)">
          <span>${t('playlist.created_at', { date: formatDate(p.created_at) })}</span>
          ${p.display_count ? `<span>${tn('playlist.display_count', p.display_count)}</span>` : ''}
        </div>
      </a>
    `).join('');
  } catch (err) {
    grid.innerHTML = `<div style="grid-column:1/-1;color:var(--text-muted);padding:40px;text-align:center">${t('playlist.load_failed', { error: esc(err.message) })}</div>`;
  }
}

function showCreateModal() {
  const modal = document.createElement('div');
  modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000';
  modal.innerHTML = `
    <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px;width:400px;max-width:90vw">
      <h3 style="margin-bottom:16px;color:var(--text-primary)">${t('playlist.new_playlist')}</h3>
      <input type="text" id="newPlaylistName" class="input" placeholder="${t('playlist.name_placeholder')}" style="width:100%;margin-bottom:12px" autofocus>
      <textarea id="newPlaylistDesc" class="input" placeholder="${t('playlist.desc_placeholder')}" style="width:100%;height:60px;resize:vertical;margin-bottom:16px"></textarea>
      <div style="display:flex;gap:8px;justify-content:flex-end">
        <button class="btn btn-secondary" id="cancelCreateBtn">${t('common.cancel')}</button>
        <button class="btn btn-primary" id="confirmCreateBtn">${t('playlist.create_btn')}</button>
      </div>
    </div>
  `;
  document.body.appendChild(modal);

  const nameInput = document.getElementById('newPlaylistName');
  nameInput.focus();

  document.getElementById('cancelCreateBtn').addEventListener('click', () => modal.remove());
  modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });

  async function doCreate() {
    const name = nameInput.value.trim();
    if (!name) { nameInput.focus(); return; }
    const desc = document.getElementById('newPlaylistDesc').value.trim();
    try {
      const pl = await api.createPlaylist(name, desc);
      modal.remove();
      showToast(t('playlist.toast.created'));
      window.location.hash = `#/playlists/${pl.id}`;
    } catch (err) {
      showToast(err.message, 'error');
    }
  }

  document.getElementById('confirmCreateBtn').addEventListener('click', doCreate);
  nameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') doCreate(); });
}

async function renderDetail(container, playlistId) {
  container.innerHTML = `
    <div style="color:var(--text-muted);padding:40px;text-align:center">${t('common.loading')}</div>
  `;

  try {
    const playlist = await api.getPlaylist(playlistId);
    renderDetailContent(container, playlist);
  } catch (err) {
    container.innerHTML = `
      <div style="padding:40px;text-align:center;color:var(--text-muted)">
        <p>${t('playlist.load_failed', { error: esc(err.message) })}</p>
        <a href="#/playlists" class="btn btn-secondary" style="margin-top:16px">${t('playlist.back_to_playlists')}</a>
      </div>
    `;
  }
}

// #104: draft preview by REUSING the player. Iframes /player in device-free preview
// mode (same-origin -> dashboard CSP frame-src 'self' allows it). The player fetches
// /api/playlists/:id/preview-payload and renders with its unmodified renderer, so the
// preview is byte-identical to what a device shows. Orientation toggle just reloads
// the iframe with &orientation; the server passes it through.
// #238: Portrait here had the same fault as the device preview — the iframe was given the
// as-displayed 9/16 shape AND the player rotated inside it, so the portrait toggle showed sideways
// content. The stage is the panel's face; the iframe is its landscape framebuffer, turned back by
// the stand-in for the wall mount.
function showPlaylistPreview(playlist) {
  let orientation = 'landscape';
  const aspect = () => displayAspectRatio(orientation);
  const frameSrc = () => `/player?preview=1&playlist=${encodeURIComponent(playlist.id)}&orientation=${orientation}&t=${Date.now()}`;
  const overlay = document.createElement('div');
  overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;z-index:10000;padding:16px';
  overlay.innerHTML = `
    <div style="background:var(--bg-card);border-radius:8px;display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border);max-width:95vw;max-height:92vh">
      <div style="display:flex;justify-content:space-between;align-items:center;padding:10px 16px;border-bottom:1px solid var(--border);gap:12px">
        <strong style="color:var(--text-primary)">${t('widget.preview')} — ${esc(playlist.name)}</strong>
        <div style="display:flex;gap:8px;align-items:center">
          <button class="btn btn-primary btn-sm" id="pvpLandscape">${t('device.form.orientation.landscape')}</button>
          <button class="btn btn-secondary btn-sm" id="pvpPortrait">${t('device.form.orientation.portrait')}</button>
          <button class="btn btn-secondary btn-sm" id="pvpClose">${t('widget.close')}</button>
        </div>
      </div>
      <div style="padding:16px;display:flex;align-items:center;justify-content:center;background:#000">
          <!-- The stage is the sized box; the frame inside it is rotated to the panel's own shape
               (#238), so a portrait preview is no longer the double rotation it used to be.
               72vh rather than 78: the modal caps at 92vh with overflow:hidden, and the header plus
               the transport row cost ~12vh — at 78 the skip buttons fell off the bottom. -->
          <div id="pvpStage" style="height:72vh;max-width:92vw;aspect-ratio:${aspect()};background:#000">
            <iframe id="pvpFrame" style="border:0;background:#000" src="${frameSrc()}"></iframe>
          </div>
        </div>
        <div style="display:flex;justify-content:center;align-items:center;gap:12px;padding:10px 16px;border-top:1px solid var(--border)">
          <button class="btn btn-secondary btn-sm" id="pvpPrev" disabled>&#8249; ${t('playlist.preview_prev')}</button>
          <span id="pvpPosition" style="color:var(--text-muted);font-size:13px;min-width:110px;text-align:center">&nbsp;</span>
          <button class="btn btn-secondary btn-sm" id="pvpNext" disabled>${t('playlist.preview_next')} &#8250;</button>
      </div>
    </div>`;
  document.body.appendChild(overlay);
  const stage = overlay.querySelector('#pvpStage');
  const frame = overlay.querySelector('#pvpFrame');
  frameDeviceOutput(stage, frame, orientation);
  const btnL = overlay.querySelector('#pvpLandscape');
  const btnP = overlay.querySelector('#pvpPortrait');
  const btnPrev = overlay.querySelector('#pvpPrev');
  const btnNext = overlay.querySelector('#pvpNext');
  const position = overlay.querySelector('#pvpPosition');

  // #239: skip/next. The preview already IS the real player in device-free preview mode, so the
  // control is a message to that one iframe rather than a second copy of the playback logic.
  // Addressing frame.contentWindow (not a broadcast) and pinning targetOrigin to our own origin is
  // what keeps this off any real screen: a live display holds a socket to the server and is not
  // reachable from this page at all, and the preview player itself ignores the message unless it
  // booted with ?preview=1.
  const send = (action) => {
    try { frame.contentWindow?.postMessage({ source: 'screentinker-preview', action }, window.location.origin); } catch (e) {}
  };
  const onPlayerMessage = (ev) => {
    if (ev.origin !== window.location.origin) return;
    if (ev.source !== frame.contentWindow) return;   // ignore any other frame on the page
    const d = ev.data;
    if (!d || d.source !== 'screentinker-player' || d.type !== 'preview:state') return;
    // A multi-zone playlist plays all zones at once, so there is no single item to step through —
    // showing a counter there would be a lie and the buttons would appear dead.
    if (d.zoned || !d.total) {
      btnPrev.disabled = btnNext.disabled = true;
      position.textContent = d.zoned ? t('playlist.preview_zoned') : '';
      return;
    }
    btnPrev.disabled = btnNext.disabled = false;
    position.textContent = t('playlist.preview_position', { current: (d.index >= 0 ? d.index : 0) + 1, total: d.total });
  };
  window.addEventListener('message', onPlayerMessage);
  // The player posts its state as soon as it has content, but an orientation reload restarts it —
  // ask again on every load so the counter can never be left stale from the previous run.
  frame.addEventListener('load', () => send('sync'));

  const setOrientation = (o) => {
    orientation = o;
      // The stage carries the aspect; the frame is rotated inside it (#238).
      stage.style.aspectRatio = aspect();
      frameDeviceOutput(stage, frame, orientation);
      btnPrev.disabled = btnNext.disabled = true;   // reloading: no item until the player says so
      position.textContent = '';
    frame.src = frameSrc();
    btnL.className = 'btn btn-sm ' + (o === 'landscape' ? 'btn-primary' : 'btn-secondary');
    btnP.className = 'btn btn-sm ' + (o.startsWith('portrait') ? 'btn-primary' : 'btn-secondary');
  };
  btnL.onclick = () => setOrientation('landscape');
  btnP.onclick = () => setOrientation('portrait');
  btnPrev.onclick = () => send('prev');
  btnNext.onclick = () => send('next');
  // Listeners are on window/document, so they outlive the overlay unless close() takes them with
  // it — a leaked keydown handler would keep firing at a closed preview.
  const close = () => {
    overlay.remove();
    window.removeEventListener('message', onPlayerMessage);
    document.removeEventListener('keydown', onKey);
  };
  function onKey(ev) {
    if (ev.key === 'Escape') close();
    else if (ev.key === 'ArrowRight') send('next');
    else if (ev.key === 'ArrowLeft') send('prev');
  }
  overlay.querySelector('#pvpClose').onclick = close;
  overlay.onclick = (e) => { if (e.target === overlay) close(); };
  document.addEventListener('keydown', onKey);
}

/*
 * A small picture of where this playlist's content actually lands.
 *
 * A playlist has no intrinsic layout — the server derives one from the items' own zone bindings
 * (#104) — so the page could previously show an item tagged "Bottom Ticker" with no indication
 * that the ticker is a thin strip along the bottom. People assigned content to zones by name and
 * found out where it went by looking at a screen.
 *
 * Drawn from the zone percentages, so it is correct for any layout including portrait ones without
 * a stored thumbnail. Zones with no items are dimmed: an empty zone on a real panel shows its
 * background colour, and that is worth seeing BEFORE publishing rather than after.
 */
function layoutMockup(playlist) {
  const layout = playlist && playlist.layout;
  const items = (playlist && playlist.items) || [];

  // No layout means fullscreen — every item shares one frame. Drawing a single empty box would
  // imply a choice was made; say it in words instead.
  if (!layout || !Array.isArray(layout.zones) || layout.zones.length === 0) {
    return `<div style="font-size:12px;color:var(--text-muted);margin-bottom:12px">${t('playlist.layout_fullscreen')}</div>`;
  }

  const counts = {};
  for (const it of items) if (it.zone_id) counts[it.zone_id] = (counts[it.zone_id] || 0) + 1;

  const w = Number(layout.width) || 1920;
  const h = Number(layout.height) || 1080;
  const portrait = h > w;
  // Fixed short edge, long edge derived — a portrait mockup must not be as wide as a landscape one
  // or it dominates the page.
  const boxW = portrait ? 90 : 200;
  const boxH = Math.round(boxW * (h / w));

  const zones = layout.zones.map((z) => {
    const n = counts[z.id] || 0;
    const filled = n > 0;
    return `<div title="${esc(z.name)}${filled ? ` — ${n}` : ''}" style="
      position:absolute;
      left:${z.x_percent}%; top:${z.y_percent}%;
      width:${z.width_percent}%; height:${z.height_percent}%;
      box-sizing:border-box;
      border:1px solid ${filled ? 'var(--accent)' : 'var(--border)'};
      background:${filled ? 'color-mix(in srgb, var(--accent) 18%, transparent)' : 'transparent'};
      display:flex;align-items:center;justify-content:center;
      font-size:9px;line-height:1;color:var(--text-muted);overflow:hidden;
    ">${filled ? n : ''}</div>`;
  }).join('');

  return `
    <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
      <div style="position:relative;width:${boxW}px;height:${boxH}px;background:var(--bg-primary);border:1px solid var(--border);border-radius:4px;flex:none">
        ${zones}
      </div>
      <div style="font-size:12px;color:var(--text-muted)">
        <div>${esc(layout.name || '')} &middot; ${w}&times;${h}${portrait ? ' (portrait)' : ''}</div>
        <div>${tn('playlist.zones_count', layout.zones.length)}</div>
        ${layout._preview_ambiguous ? `<div style="color:var(--warning)">${t('playlist.layout_ambiguous')}</div>` : ''}
      </div>
    </div>`;
}

function renderDetailContent(container, playlist) {
  const isDraft = playlist.status === 'draft';
  const hasPublished = !!playlist.published_snapshot;

  container.innerHTML = `
    ${isDraft ? `
    <div id="draftBanner" style="background:#78350f;border:1px solid #92400e;border-radius:var(--radius-lg);padding:14px 20px;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;gap:16px">
      <div style="display:flex;align-items:center;gap:10px;color:#fbbf24">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
        <div>
          <div style="font-weight:600;font-size:14px">${t('playlist.draft.banner_title')}</div>
          <div style="font-size:12px;color:#fcd34d;opacity:0.85">${hasPublished ? t('playlist.draft.devices_showing_published') : t('playlist.draft.never_published')}</div>
        </div>
      </div>
      <div style="display:flex;gap:8px;flex-shrink:0">
        ${hasPublished ? `<button class="btn btn-secondary btn-sm" id="discardDraftBtn" style="color:#fbbf24;border-color:#92400e">${t('playlist.draft.discard_changes')}</button>` : ''}
        <button class="btn btn-sm" id="publishBtn" style="background:#f59e0b;color:#000;font-weight:600;border:none">${t('playlist.draft.publish')}</button>
      </div>
    </div>
    ` : ''}

    <div class="page-header">
      <div style="display:flex;align-items:center;gap:12px">
        <a href="#/playlists" style="color:var(--text-muted);text-decoration:none;font-size:20px" title="${t('playlist.back')}">&larr;</a>
        <div>
          <h1 id="playlistTitle" style="cursor:pointer" title="${t('playlist.click_to_rename')}">${esc(playlist.name)}</h1>
          <div class="subtitle" id="playlistDesc" style="cursor:pointer" title="${t('playlist.click_to_edit_desc')}">${playlist.description ? esc(playlist.description) : `<span style="opacity:0.5">${t('playlist.add_desc_placeholder')}</span>`}</div>
          ${playlist.display_count ? `<div style="font-size:12px;color:var(--text-muted);margin-top:4px">${tn('playlist.assigned_to', playlist.display_count)}</div>` : ''}
        </div>
      </div>
      <div style="display:flex;gap:8px">
        <button class="btn btn-secondary" id="previewPlaylistBtn">${t('widget.preview')}</button>
        <button class="btn btn-primary" id="addItemBtn">${t('playlist.add_content')}</button>
        <button class="btn btn-secondary" id="deletePlaylistBtn" style="color:var(--danger)">${t('playlist.delete_playlist')}</button>
      </div>
    </div>

    ${layoutMockup(playlist)}
    
    <div id="playlistItems" style="display:flex;flex-direction:column;gap:8px">
    </div>
  `;

  renderItems(playlist.items || []);

  const publishBtn = document.getElementById('publishBtn');
  if (publishBtn) {
    publishBtn.addEventListener('click', async () => {
      try {
        publishBtn.disabled = true;
        publishBtn.textContent = t('playlist.draft.publishing');
        const updated = await api.publishPlaylist(playlist.id);
        showToast(t('playlist.toast.published'));
        renderDetailContent(container, updated);
      } catch (err) {
        publishBtn.disabled = false;
        publishBtn.textContent = t('playlist.draft.publish');
        showToast(err.message, 'error');
      }
    });
  }
  const previewBtn = document.getElementById('previewPlaylistBtn');
  if (previewBtn) previewBtn.addEventListener('click', () => showPlaylistPreview(playlist));

  const discardBtn = document.getElementById('discardDraftBtn');
  if (discardBtn) {
    discardBtn.addEventListener('click', async () => {
      if (!confirm(t('playlist.confirm_discard_draft'))) return;
      try {
        const updated = await api.discardPlaylistDraft(playlist.id);
        showToast(t('playlist.toast.draft_discarded'));
        renderDetailContent(container, updated);
      } catch (err) {
        showToast(err.message, 'error');
      }
    });
  }

  document.getElementById('playlistTitle').addEventListener('click', () => inlineEdit(playlist, 'name'));
  document.getElementById('playlistDesc').addEventListener('click', () => inlineEdit(playlist, 'description'));

  document.getElementById('addItemBtn').addEventListener('click', () => showAddItemModal(playlist.id));

  document.getElementById('deletePlaylistBtn').addEventListener('click', async () => {
    if (!confirm(t('playlist.confirm_delete', { name: playlist.name }))) return;
    try {
      await api.deletePlaylist(playlist.id);
      showToast(t('playlist.toast.deleted'));
      window.location.hash = '#/playlists';
    } catch (err) {
      showToast(err.message, 'error');
    }
  });
}

async function refreshAfterMutation() {
  if (!currentPlaylistId) return;
  const mainContainer = document.getElementById('draftBanner')?.parentElement || document.querySelector('.page-header')?.parentElement;
  if (!mainContainer) return;
  try {
    const playlist = await api.getPlaylist(currentPlaylistId);
    renderDetailContent(mainContainer, playlist);
  } catch (e) { /* silent */ }
}

function renderItems(items) {
  const itemsEl = document.getElementById('playlistItems');
  if (!itemsEl) return;

  if (!items.length) {
    itemsEl.innerHTML = `
      <div style="text-align:center;padding:40px;color:var(--text-muted);border:2px dashed var(--border);border-radius:var(--radius-lg)">
        <p style="margin-bottom:8px">${t('playlist.items_empty')}</p>
        <p style="font-size:13px">${t('playlist.items_empty_hint')}</p>
      </div>
    `;
    return;
  }

  itemsEl.innerHTML = items.map((item, i) => `
    <div class="playlist-item" data-item-id="${item.id}" data-index="${i}" draggable="true" style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;display:flex;align-items:center;gap:12px;cursor:grab;transition:border-color 0.15s">
      <div style="color:var(--text-muted);font-size:12px;min-width:24px;text-align:center;user-select:none">${i + 1}</div>
      <div style="width:48px;height:36px;border-radius:4px;overflow:hidden;background:var(--bg-input);flex-shrink:0;display:flex;align-items:center;justify-content:center">
        ${item.thumbnail_path
          ? `<img data-auth-src="/api/content/${esc(item.content_id)}/thumbnail" style="width:100%;height:100%;object-fit:cover">`
          : `<div style="color:var(--text-muted);opacity:0.5">${getTypeIcon(item)}</div>`
        }
      </div>
      <div style="flex:1;min-width:0">
        <div style="font-size:14px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(item.filename || item.widget_name || t('common.unknown'))}</div>
        <div style="font-size:12px;color:var(--text-muted);display:flex;align-items:center;gap:8px;min-width:0">
          <span style="white-space:nowrap">${item.widget_id ? t('playlist.item_widget') : esc(item.mime_type || t('playlist.unknown_type'))}</span>
          ${item.schedules && item.schedules.length ? `<span style="font-size:11px;padding:1px 6px;border-radius:4px;background:#0c2a3f;color:#7dd3fc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${esc(scheduleSummary(item.schedules))}">🕐 ${esc(scheduleSummary(item.schedules))}</span>` : ''}
        </div>
      </div>
      <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
        <label style="font-size:12px;color:var(--text-muted)">${t('playlist.duration')}</label>
        <input type="number" class="input item-duration" data-item-id="${item.id}" value="${item.duration_sec}" min="1" style="width:60px;padding:4px 8px;font-size:13px;text-align:center">
        <span style="font-size:12px;color:var(--text-muted)">${t('playlist.sec')}</span>
      </div>
      <div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
        <button class="btn-icon item-schedule" data-item-id="${item.id}" title="${t('itemsched.title')}" aria-label="${t('itemsched.title')}" style="color:${item.schedules && item.schedules.length ? '#38bdf8' : 'var(--text-muted)'};background:none;border:none;cursor:pointer;padding:4px;border-radius:4px">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 15 14"/></svg>
        </button>
        <button class="btn-icon item-replace" data-item-id="${item.id}" title="${t('playlist.replace_item')}" aria-label="${t('playlist.replace_item')}" style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
        </button>
        <button class="btn-icon item-duplicate" data-item-id="${item.id}" title="${t('playlist.duplicate_item')}" aria-label="${t('playlist.duplicate_item')}" style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
        </button>
        <button class="btn-icon item-move" data-item-id="${item.id}" data-dir="up" title="${t('playlist.move_up')}" aria-label="${t('playlist.move_up')}" ${i === 0 ? 'disabled' : ''} style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px;${i === 0 ? 'opacity:0.3;cursor:not-allowed' : ''}">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>
        </button>
        <button class="btn-icon item-move" data-item-id="${item.id}" data-dir="down" title="${t('playlist.move_down')}" aria-label="${t('playlist.move_down')}" ${i === items.length - 1 ? 'disabled' : ''} style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px;${i === items.length - 1 ? 'opacity:0.3;cursor:not-allowed' : ''}">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
        </button>
        <button class="btn-icon item-remove" data-item-id="${item.id}" title="${t('common.delete')}" aria-label="${t('playlist.remove_item')}" style="color:var(--text-muted);background:none;border:none;cursor:pointer;padding:4px;border-radius:4px">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
        </button>
      </div>
    </div>
  `).join('');
  hydrateAuthImages(itemsEl);

  itemsEl.querySelectorAll('.item-duration').forEach(input => {
    input.addEventListener('change', async (e) => {
      const itemId = e.target.dataset.itemId;
      const val = parseInt(e.target.value, 10);
      if (!val || val < 1) { e.target.value = 10; return; }
      try {
        await api.updatePlaylistItem(currentPlaylistId, itemId, { duration_sec: val });
        refreshAfterMutation();
      } catch (err) {
        showToast(err.message, 'error');
      }
    });
  });

  itemsEl.querySelectorAll('.item-remove').forEach(btn => {
    btn.addEventListener('click', async (e) => {
      const itemId = e.currentTarget.dataset.itemId;
      try {
        await api.deletePlaylistItem(currentPlaylistId, itemId);
        const playlist = await api.getPlaylist(currentPlaylistId);
        renderItems(playlist.items || []);
        refreshAfterMutation();
        showToast(t('playlist.toast.item_removed'));
      } catch (err) {
        showToast(err.message, 'error');
      }
    });
  });

  itemsEl.querySelectorAll('.item-schedule').forEach(btn => {
    btn.addEventListener('click', (e) => {
      const itemId = e.currentTarget.dataset.itemId;
      const item = items.find(it => String(it.id) === String(itemId));
      if (item) showScheduleModal(item);
    });
  });

  // #105 duplicate: server copies the row + its schedule blocks, appended at the end.
  itemsEl.querySelectorAll('.item-duplicate').forEach(btn => {
    btn.addEventListener('click', async (e) => {
      const itemId = e.currentTarget.dataset.itemId;
      try {
        e.currentTarget.disabled = true;
        await api.duplicatePlaylistItem(currentPlaylistId, itemId);
        const playlist = await api.getPlaylist(currentPlaylistId);
        renderItems(playlist.items || []);
        refreshAfterMutation();
        showToast(t('playlist.toast.item_duplicated'));
      } catch (err) {
        showToast(err.message, 'error');
      }
    });
  });

  // #105 replace: reuse the add-item picker in "replace" mode — swaps content/widget
  // in place, preserving duration/schedule/zone (server-side).
  itemsEl.querySelectorAll('.item-replace').forEach(btn => {
    btn.addEventListener('click', (e) => {
      const itemId = e.currentTarget.dataset.itemId;
      showAddItemModal(currentPlaylistId, { replaceItemId: itemId });
    });
  });

  itemsEl.querySelectorAll('.item-move').forEach(btn => {
    btn.addEventListener('click', async (e) => {
      if (btn.disabled) return;
      const itemId = parseInt(e.currentTarget.dataset.itemId, 10);
      const dir = e.currentTarget.dataset.dir;
      const order = Array.from(itemsEl.querySelectorAll('.playlist-item'))
        .map(el => parseInt(el.dataset.itemId, 10));
      const idx = order.indexOf(itemId);
      const swap = dir === 'up' ? idx - 1 : idx + 1;
      if (swap < 0 || swap >= order.length) return;
      [order[idx], order[swap]] = [order[swap], order[idx]];
      try {
        const updated = await api.reorderPlaylistItems(currentPlaylistId, order);
        renderItems(updated);
        refreshAfterMutation();
      } catch (err) {
        showToast(err.message, 'error');
      }
    });
  });

  setupDragReorder(itemsEl);
}

function setupDragReorder(container) {
  let dragEl = null;

  container.addEventListener('dragstart', (e) => {
    dragEl = e.target.closest('.playlist-item');
    if (!dragEl) return;
    dragEl.style.opacity = '0.4';
    e.dataTransfer.effectAllowed = 'move';
  });

  container.addEventListener('dragend', () => {
    if (dragEl) dragEl.style.opacity = '';
    dragEl = null;
    container.querySelectorAll('.playlist-item').forEach(el => el.style.borderTop = '');
  });

  container.addEventListener('dragover', (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
    const target = e.target.closest('.playlist-item');
    container.querySelectorAll('.playlist-item').forEach(el => el.style.borderTop = '');
    if (target && target !== dragEl) {
      target.style.borderTop = '2px solid var(--primary)';
    }
  });

  container.addEventListener('drop', async (e) => {
    e.preventDefault();
    const target = e.target.closest('.playlist-item');
    if (!target || !dragEl || target === dragEl) return;

    container.insertBefore(dragEl, target);

    const order = Array.from(container.querySelectorAll('.playlist-item'))
      .map(el => parseInt(el.dataset.itemId, 10));

    try {
      const items = await api.reorderPlaylistItems(currentPlaylistId, order);
      renderItems(items);
      refreshAfterMutation();
    } catch (err) {
      showToast(err.message, 'error');
      const playlist = await api.getPlaylist(currentPlaylistId);
      renderItems(playlist.items || []);
    }
  });
}

function inlineEdit(playlist, field) {
  const el = field === 'name' ? document.getElementById('playlistTitle') : document.getElementById('playlistDesc');
  if (!el) return;

  const current = playlist[field] || '';
  const isName = field === 'name';

  if (isName) {
    const input = document.createElement('input');
    input.type = 'text';
    input.className = 'input';
    input.value = current;
    input.style.cssText = 'font-size:24px;font-weight:700;padding:2px 8px;width:100%';
    el.replaceWith(input);
    input.focus();
    input.select();

    async function save() {
      const val = input.value.trim();
      if (!val) { input.value = current; return; }
      try {
        const updated = await api.updatePlaylist(playlist.id, { [field]: val });
        playlist[field] = updated[field];
      } catch (err) {
        showToast(err.message, 'error');
      }
      const newEl = document.createElement('h1');
      newEl.id = 'playlistTitle';
      newEl.style.cursor = 'pointer';
      newEl.title = t('playlist.click_to_rename');
      newEl.textContent = playlist.name;
      input.replaceWith(newEl);
      newEl.addEventListener('click', () => inlineEdit(playlist, 'name'));
    }

    input.addEventListener('blur', save);
    input.addEventListener('keydown', (e) => { if (e.key === 'Enter') input.blur(); if (e.key === 'Escape') { input.value = current; input.blur(); } });
  } else {
    const input = document.createElement('textarea');
    input.className = 'input';
    input.value = current;
    input.style.cssText = 'font-size:13px;padding:4px 8px;width:100%;height:40px;resize:vertical';
    el.replaceWith(input);
    input.focus();

    async function save() {
      const val = input.value.trim();
      try {
        const updated = await api.updatePlaylist(playlist.id, { description: val });
        playlist.description = updated.description;
      } catch (err) {
        showToast(err.message, 'error');
      }
      const newEl = document.createElement('div');
      newEl.className = 'subtitle';
      newEl.id = 'playlistDesc';
      newEl.style.cursor = 'pointer';
      newEl.title = t('playlist.click_to_edit_desc');
      if (playlist.description) {
        newEl.textContent = playlist.description;
      } else {
        newEl.innerHTML = `<span style="opacity:0.5">${t('playlist.add_desc_placeholder')}</span>`;
      }
      input.replaceWith(newEl);
      newEl.addEventListener('click', () => inlineEdit(playlist, 'description'));
    }

    input.addEventListener('blur', save);
    input.addEventListener('keydown', (e) => { if (e.key === 'Escape') { input.value = current; input.blur(); } });
  }
}

async function showAddItemModal(playlistId, opts = {}) {
  // #105: when opts.replaceItemId is set, picking an item REPLACES that item's
  // content/widget in place (preserving duration/schedule/zone) instead of adding.
  const replaceItemId = opts.replaceItemId || null;
  const modal = document.createElement('div');
  modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000';
  modal.innerHTML = `
    <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px;max-width:560px;width:95vw;max-height:80vh;display:flex;flex-direction:column">
      <h3 style="margin-bottom:16px;color:var(--text-primary)">${replaceItemId ? t('playlist.replace_modal_title') : t('playlist.add_modal_title')}</h3>
      <div style="display:flex;gap:8px;margin-bottom:12px">
        <button class="btn btn-primary btn-sm tab-btn active" data-tab="content">${t('playlist.tab_content')}</button>
        <button class="btn btn-secondary btn-sm tab-btn" data-tab="widgets">${t('playlist.tab_widgets')}</button>
      </div>
      <input type="text" id="addItemSearch" class="input" placeholder="${t('playlist.search_placeholder')}" style="width:100%;margin-bottom:12px">
      <div id="addItemList" style="flex:1;overflow-y:auto;min-height:200px;max-height:400px"></div>
      <div style="display:flex;justify-content:flex-end;margin-top:16px">
        <button class="btn btn-secondary" id="closeAddModal">${t('playlist.close')}</button>
      </div>
    </div>
  `;
  document.body.appendChild(modal);

  let activeTab = 'content';
  let allContent = [];
  let allWidgets = [];

  try {
    [allContent, allWidgets] = await Promise.all([
      api.getContent(),
      api.getWidgets ? api.getWidgets() : Promise.resolve([])
    ]);
  } catch (err) {
    document.getElementById('addItemList').innerHTML = `<div style="color:var(--text-muted);padding:20px;text-align:center">${t('playlist.load_failed', { error: esc(err.message) })}</div>`;
  }

  function renderTab() {
    const list = document.getElementById('addItemList');
    const search = (document.getElementById('addItemSearch')?.value || '').toLowerCase();
    const items = activeTab === 'content' ? allContent : allWidgets;
    const filtered = items.filter(item => {
      const name = (item.filename || item.name || '').toLowerCase();
      return name.includes(search);
    });

    if (!filtered.length) {
      list.innerHTML = `<div style="color:var(--text-muted);padding:20px;text-align:center">${activeTab === 'content' ? t('playlist.no_content_found') : t('playlist.no_widgets_found')}</div>`;
      return;
    }

    list.innerHTML = filtered.map(item => {
      const isWidget = activeTab === 'widgets';
      const name = item.filename || item.name || t('common.unknown');
      // #237: the server gives a video item the clip's own length instead of the 10s default.
      // Show that length here so the duration the item lands with is something the operator
      // saw coming, rather than a number that appears in the list after the fact.
      const clipSec = !isWidget && Number(item.duration_sec) > 0 ? Math.ceil(item.duration_sec) : 0;
      const clip = clipSec ? ` · ${Math.floor(clipSec / 60)}:${String(clipSec % 60).padStart(2, '0')}` : '';
      const sub = isWidget ? (item.widget_type || t('playlist.item_widget')) : ((item.mime_type || '') + clip);
      const thumb = item.thumbnail_path ? `/api/content/${esc(item.id)}/thumbnail` : null;
      return `
        <div class="add-item-row" data-id="${esc(item.id)}" data-type="${isWidget ? 'widget' : 'content'}" style="display:flex;align-items:center;gap:12px;padding:10px;border-radius:var(--radius);cursor:pointer;transition:background 0.1s">
          <div style="width:40px;height:30px;border-radius:4px;overflow:hidden;background:var(--bg-input);flex-shrink:0;display:flex;align-items:center;justify-content:center">
            ${thumb ? `<img data-auth-src="${thumb}" style="width:100%;height:100%;object-fit:cover">` : '<div style="color:var(--text-muted);opacity:0.4"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/></svg></div>'}
          </div>
          <div style="flex:1;min-width:0">
            <div style="font-size:13px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(name)}</div>
            <div style="font-size:11px;color:var(--text-muted)">${esc(sub)}</div>
          </div>
          <button class="btn btn-primary btn-sm add-item-btn" data-id="${esc(item.id)}" data-type="${isWidget ? 'widget' : 'content'}">${replaceItemId ? t('playlist.replace_btn') : t('playlist.add_btn')}</button>
        </div>
      `;
    }).join('');
    hydrateAuthImages(list, { eager: true });

    list.querySelectorAll('.add-item-btn').forEach(btn => {
      btn.addEventListener('click', async (e) => {
        e.stopPropagation();
        const id = btn.dataset.id;
        const type = btn.dataset.type;
        const data = type === 'widget' ? { widget_id: id } : { content_id: id };
        try {
          btn.disabled = true;
          if (replaceItemId) {
            btn.textContent = t('playlist.replacing');
            // PUT supports a content/widget swap; the server nulls the opposite FK and
            // preserves duration/schedule/zone. Close on success and re-render the list.
            await api.updatePlaylistItem(playlistId, replaceItemId, data);
            modal.remove();
            const playlist = await api.getPlaylist(playlistId);
            renderItems(playlist.items || []);
            refreshAfterMutation();
            showToast(t('playlist.toast.item_replaced'));
            return;
          }
          btn.textContent = t('playlist.adding');
          await api.addPlaylistItem(playlistId, data);
          btn.textContent = t('playlist.added');
          btn.classList.remove('btn-primary');
          btn.classList.add('btn-secondary');
          refreshAfterMutation();
        } catch (err) {
          btn.disabled = false;
          btn.textContent = replaceItemId ? t('playlist.replace_btn') : t('playlist.add_btn');
          showToast(err.message, 'error');
        }
      });
    });
  }

  modal.querySelectorAll('.tab-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      activeTab = btn.dataset.tab;
      modal.querySelectorAll('.tab-btn').forEach(b => {
        b.classList.toggle('btn-primary', b.dataset.tab === activeTab);
        b.classList.toggle('btn-secondary', b.dataset.tab !== activeTab);
        b.classList.toggle('active', b.dataset.tab === activeTab);
      });
      renderTab();
    });
  });

  document.getElementById('addItemSearch').addEventListener('input', renderTab);

  document.getElementById('closeAddModal').addEventListener('click', () => modal.remove());
  modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });

  renderTab();
}

// #74/#75: per-item schedule editor. Multiple blocks (days + time window + optional
// date range) OR together; an item with no blocks always plays. Client validation
// mirrors the server; saving marks the playlist DRAFT (must re-publish to reach devices).
function showScheduleModal(item) {
  let blocks = (item.schedules || []).map(b => ({
    days: Array.isArray(b.days) ? [...b.days] : [],
    start: b.start || '00:00',
    end: b.end || '24:00',
    start_date: b.start_date || '',
    end_date: b.end_date || ''
  }));

  const modal = document.createElement('div');
  modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:1000';
  document.body.appendChild(modal);

  function blockRow(b, idx) {
    const eod = b.end === '24:00';
    const dayLabels = t('itemsched.dow_short').split(',');
    return `
      <div style="border:1px solid var(--border);border-radius:var(--radius);padding:12px;margin-bottom:10px">
        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
          <strong style="font-size:13px">${t('itemsched.block', { n: idx + 1 })}</strong>
          <button class="sched-remove" data-idx="${idx}" title="${t('itemsched.remove_block')}" style="color:var(--text-muted);background:none;border:none;cursor:pointer;font-size:14px">✕</button>
        </div>
        <div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px">
          ${dayLabels.map((lbl, d) => `<button class="sched-day" data-idx="${idx}" data-day="${d}" style="padding:4px 9px;border-radius:6px;font-size:12px;cursor:pointer;border:1px solid var(--border);background:${b.days.includes(d) ? 'var(--accent)' : 'var(--bg-input)'};color:${b.days.includes(d) ? '#000' : 'var(--text-muted)'}">${lbl}</button>`).join('')}
        </div>
        <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center">
          <label style="font-size:12px;color:var(--text-muted)">${t('itemsched.from')} <input type="time" class="input sched-start" data-idx="${idx}" value="${esc(b.start)}" style="width:118px"></label>
          <label style="font-size:12px;color:var(--text-muted)">${t('itemsched.to')} <input type="time" class="input sched-end" data-idx="${idx}" value="${esc(eod ? '00:00' : b.end)}" ${eod ? 'disabled' : ''} style="width:118px"></label>
          <label style="font-size:12px;color:var(--text-muted)"><input type="checkbox" class="sched-eod" data-idx="${idx}" ${eod ? 'checked' : ''}> ${t('itemsched.end_of_day')}</label>
        </div>
        <div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;margin-top:10px">
          <label style="font-size:12px;color:var(--text-muted)">${t('itemsched.starts')} <input type="date" class="input sched-sd" data-idx="${idx}" value="${esc(b.start_date)}" style="width:150px"></label>
          <label style="font-size:12px;color:var(--text-muted)">${t('itemsched.ends')} <input type="date" class="input sched-ed" data-idx="${idx}" value="${esc(b.end_date)}" style="width:150px"></label>
          <span style="font-size:11px;color:var(--text-muted)">${t('itemsched.dates_hint')}</span>
        </div>
      </div>`;
  }

  function render() {
    modal.innerHTML = `
      <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px;width:580px;max-width:94vw;max-height:88vh;overflow:auto">
        <h3 style="margin:0 0 4px">${t('itemsched.title')}</h3>
        <p style="font-size:12px;color:var(--text-muted);margin:0 0 10px">${esc(item.filename || item.widget_name || 'item')}</p>
        <p style="font-size:12px;color:#7dd3fc;background:#0c2a3f;border-radius:6px;padding:8px 10px;margin:0 0 16px">${t('itemsched.hint')}</p>
        <div>${blocks.length ? blocks.map(blockRow).join('') : `<p style="font-size:13px;color:var(--text-muted);margin:0 0 10px">${t('itemsched.none')}</p>`}</div>
        <button class="btn btn-secondary btn-sm" id="schedAddBlock" style="margin-bottom:4px">${t('itemsched.add_block')}</button>
        <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px">
          <button class="btn btn-secondary" id="schedCancel">${t('itemsched.cancel')}</button>
          <button class="btn" id="schedSave" style="background:#f59e0b;color:#000;font-weight:600;border:none">${t('itemsched.save')}</button>
        </div>
      </div>`;
    wire();
  }

  function wire() {
    modal.querySelectorAll('.sched-day').forEach(btn => btn.addEventListener('click', () => {
      const i = +btn.dataset.idx, d = +btn.dataset.day;
      const set = new Set(blocks[i].days);
      if (set.has(d)) set.delete(d); else set.add(d);
      blocks[i].days = [...set];
      render();
    }));
    modal.querySelectorAll('.sched-start').forEach(el => el.addEventListener('change', () => { blocks[+el.dataset.idx].start = el.value; }));
    modal.querySelectorAll('.sched-end').forEach(el => el.addEventListener('change', () => { blocks[+el.dataset.idx].end = el.value; }));
    modal.querySelectorAll('.sched-eod').forEach(el => el.addEventListener('change', () => {
      blocks[+el.dataset.idx].end = el.checked ? '24:00' : '17:00';
      render();
    }));
    modal.querySelectorAll('.sched-sd').forEach(el => el.addEventListener('change', () => { blocks[+el.dataset.idx].start_date = el.value; }));
    modal.querySelectorAll('.sched-ed').forEach(el => el.addEventListener('change', () => { blocks[+el.dataset.idx].end_date = el.value; }));
    modal.querySelectorAll('.sched-remove').forEach(btn => btn.addEventListener('click', () => { blocks.splice(+btn.dataset.idx, 1); render(); }));
    document.getElementById('schedAddBlock').addEventListener('click', () => {
      blocks.push({ days: [0, 1, 2, 3, 4, 5, 6], start: '09:00', end: '17:00', start_date: '', end_date: '' });
      render();
    });
    document.getElementById('schedCancel').addEventListener('click', () => modal.remove());
    document.getElementById('schedSave').addEventListener('click', doSave);
  }

  async function doSave() {
    const payload = blocks.map(b => ({
      days: b.days, start: b.start, end: b.end,
      start_date: b.start_date || null, end_date: b.end_date || null
    }));
    const err = validateScheduleBlocks(payload);
    if (err) { showToast(err, 'error'); return; }
    try {
      const saved = await api.setItemSchedules(currentPlaylistId, item.id, payload);
      item.schedules = saved;
      modal.remove();
      // Saving makes the playlist a DRAFT — surface the re-publish step explicitly.
      showToast(payload.length ? t('itemsched.toast.saved') : t('itemsched.toast.cleared'));
      const playlist = await api.getPlaylist(currentPlaylistId);
      renderItems(playlist.items || []);
      refreshAfterMutation();
    } catch (e) {
      showToast(e.message, 'error');
    }
  }

  modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
  render();
}
