import { ZonePlayer } from '../shared/zoneplayer.js';

const $ = (s) => document.querySelector(s);
const $$ = (s) => document.querySelectorAll(s);
function authHeaders(extra) {
  const t = localStorage.getItem('sig-auth-token');
  return { ...(extra || {}), ...(t ? { 'x-auth-token': t } : {}) };
}
async function apiFetch(u, opts) {
  const r = await fetch(u, opts);
  if (r.status === 401) { try { const j = await r.clone().json(); if (j.authRequired) showLogin(); } catch {} }
  return r;
}
const JH = { 'Content-Type': 'application/json' };
const api = {
  get: (u) => apiFetch(u, { headers: authHeaders() }).then((r) => r.json()),
  post: (u, b) => apiFetch(u, { method: 'POST', headers: authHeaders(JH), body: JSON.stringify(b) }).then((r) => r.json()),
  put: (u, b) => apiFetch(u, { method: 'PUT', headers: authHeaders(JH), body: JSON.stringify(b) }).then((r) => r.json()),
  patch: (u, b) => apiFetch(u, { method: 'PATCH', headers: authHeaders(JH), body: JSON.stringify(b) }).then((r) => r.json()),
  del: (u) => apiFetch(u, { method: 'DELETE', headers: authHeaders() }).then((r) => r.json()),
};

const DAY_NAMES = ['일', '월', '화', '수', '목', '금', '토'];
const RES_PRESETS = [
  { label: 'FHD 1920×1080', width: 1920, height: 1080 },
  { label: '4K 3840×2160', width: 3840, height: 2160 },
  { label: 'HD 1280×720', width: 1280, height: 720 },
  { label: '세로 1080×1920', width: 1080, height: 1920 },
  { label: '전광판 와이드 1920×540', width: 1920, height: 540 },
  { label: '정사각 1080×1080', width: 1080, height: 1080 },
];

let state = { media: [], schedules: [], posters: [], playlists: [], settings: null, emergency: null };

// ==================== 탭 전환 ====================
$$('.tab').forEach((t) => t.addEventListener('click', () => {
  $$('.tab').forEach((x) => x.classList.remove('active'));
  $$('.tab-panel').forEach((x) => x.classList.remove('active'));
  t.classList.add('active');
  $('#panel-' + t.dataset.tab).classList.add('active');
}));

// ==================== 데이터 로드 ====================
async function loadAll() {
  const [media, schedules, posters, playlists, settings] = await Promise.all([
    api.get('/api/media'), api.get('/api/schedules'), api.get('/api/posters'), api.get('/api/playlists'), api.get('/api/settings'),
  ]);
  const st = await api.get('/api/state');
  state = { media, schedules, posters, playlists, settings, emergency: st.emergency };
  renderMedia();
  renderSchedules();
  renderPosters();
  renderSettings();
  renderEmergency();
}

// ==================== 미디어 ====================
function renderMedia() {
  $('#media-count').textContent = state.media.length;
  const grid = $('#media-grid');
  if (!state.media.length) { grid.innerHTML = '<div class="empty">아직 등록된 미디어가 없습니다. 위에서 추가하세요.</div>'; }
  else {
    grid.innerHTML = state.media.map((m) => {
      const srcUrl = m.type === 'upload' ? '/media/' + m.src : '/api/stream/' + m.id;
      let thumb;
      if (m.type === 'youtube') thumb = `<img src="${m.thumbnail}" alt="">`;
      else if (m.kind === 'image') thumb = `<img src="${srcUrl}" alt="">`;
      else thumb = `<video src="${srcUrl}#t=1" muted preload="metadata"></video>`;
      const typeLabel = m.kind === 'image' ? '🖼 이미지' : { upload: '업로드', folder: '폴더', youtube: '유튜브' }[m.type];
      const pillCls = m.kind === 'image' ? 'type-image' : 'type-' + m.type;
      return `<div class="media-item">
        <div class="media-thumb"><span class="type-pill ${pillCls}">${typeLabel}</span>${thumb}</div>
        <div class="media-body">
          <div class="media-name" title="${esc(m.name)}">${esc(m.name)}</div>
          <div class="media-actions">
            <button class="mini" data-em="${m.id}">🚨 긴급송출</button>
            <button class="mini danger" data-del="${m.id}">삭제</button>
          </div>
        </div>
      </div>`;
    }).join('');
  }
  // 긴급송출 셀렉트도 갱신
  const sel = $('#em-select');
  sel.innerHTML = state.media.map((m) => `<option value="${m.id}">${esc(m.name)}</option>`).join('') || '<option>미디어 없음</option>';
}

$('#media-grid').addEventListener('click', async (e) => {
  const del = e.target.dataset.del;
  const em = e.target.dataset.em;
  if (del) { if (confirm('이 미디어를 삭제할까요?')) { await api.del('/api/media/' + del); await loadAll(); } }
  if (em) { if (confirm('선택한 영상을 긴급 송출할까요?')) { await api.post('/api/emergency', { mediaId: em }); await refreshEmergency(); } }
});

$('#add-folder').addEventListener('click', async () => {
  const folderPath = $('#folder-path').value.trim();
  if (!folderPath) return;
  const r = await api.post('/api/media/folder', { folderPath });
  if (r.error) alert(r.error); else { $('#folder-path').value = ''; alert(`${r.length}개 영상을 등록했습니다.`); await loadAll(); }
});

$('#pick-file').addEventListener('click', () => $('#file-input').click());
$('#file-input').addEventListener('change', async (e) => {
  const files = e.target.files; if (!files.length) return;
  const fd = new FormData();
  for (const f of files) fd.append('files', f);
  const prog = $('#upload-progress'); prog.textContent = '업로드 중…';
  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/api/media/upload');
  xhr.upload.onprogress = (ev) => { if (ev.lengthComputable) prog.textContent = `업로드 중… ${Math.round(ev.loaded / ev.total * 100)}%`; };
  xhr.onload = async () => { prog.textContent = '완료!'; setTimeout(() => prog.textContent = '', 1500); $('#file-input').value = ''; await loadAll(); };
  xhr.onerror = () => { prog.textContent = '업로드 실패'; };
  xhr.send(fd);
});

$('#add-yt').addEventListener('click', async () => {
  const url = $('#yt-url').value.trim(); if (!url) return;
  const r = await api.post('/api/media/youtube', { url });
  if (r.error) alert(r.error); else { $('#yt-url').value = ''; await loadAll(); }
});

// ==================== 스케줄 ====================
function renderSchedules() {
  const list = $('#schedule-list');
  if (!state.schedules.length) { list.innerHTML = '<div class="empty">편성된 스케줄이 없습니다. “새 스케줄 만들기”를 눌러 추가하세요.</div>'; return; }
  list.innerHTML = state.schedules.map((s) => {
    const days = s.days.slice().sort().map((d) => DAY_NAMES[d]).join('·');
    const names = s.mediaIds.map((id) => state.media.find((m) => m.id === id)?.name).filter(Boolean);
    return `<div class="sched-item ${s.enabled ? '' : 'off'}">
      <div class="sched-main">
        <div class="sched-name">${esc(s.name)} ${s.enabled ? '' : '(사용 안 함)'}</div>
        <div class="sched-meta">
          <span class="sched-badge">${days}</span>
          <span class="sched-badge">${s.start} ~ ${s.end}</span>
          <span>영상 ${names.length}개${names.length ? ' · ' + esc(names.slice(0, 2).join(', ')) + (names.length > 2 ? ' 외' : '') : ''}</span>
        </div>
      </div>
      <button class="btn" data-edit="${s.id}">편집</button>
      <button class="btn btn-outline" data-delsc="${s.id}">삭제</button>
    </div>`;
  }).join('');
}

$('#schedule-list').addEventListener('click', async (e) => {
  const edit = e.target.dataset.edit, del = e.target.dataset.delsc;
  if (edit) openScheduleModal(state.schedules.find((s) => s.id === edit));
  if (del) { if (confirm('이 스케줄을 삭제할까요?')) { await api.del('/api/schedules/' + del); await loadAll(); } }
});
$('#new-schedule').addEventListener('click', () => openScheduleModal(null));

let editingId = null;
let pickState = []; // 선택된 mediaId 순서

function openScheduleModal(sc) {
  editingId = sc?.id || null;
  $('#sched-modal-title').textContent = sc ? '스케줄 편집' : '새 스케줄';
  $('#sc-name').value = sc?.name || '';
  $('#sc-start').value = sc?.start || '09:00';
  $('#sc-end').value = sc?.end || '18:00';
  $('#sc-enabled').checked = sc ? sc.enabled !== false : true;
  // 요일
  const daysWrap = $('#sc-days'); daysWrap.innerHTML = '';
  const selectedDays = new Set(sc?.days || [0, 1, 2, 3, 4, 5, 6]);
  DAY_NAMES.forEach((n, i) => {
    const b = document.createElement('button');
    b.className = 'day-btn' + (selectedDays.has(i) ? ' on' : '');
    b.textContent = n; b.dataset.day = i;
    b.onclick = () => b.classList.toggle('on');
    daysWrap.appendChild(b);
  });
  // 재생 순서 + 라이브러리
  pickState = sc ? [...sc.mediaIds] : [];
  $('#sc-folder').value = '';
  renderPlaylist();
  renderLibrary();
  setupSchedSource(sc);
  $('#sched-modal').style.display = 'flex';
}

// 재생 순서 목록 (드래그앤드롭 정렬)
function renderPlaylist() {
  const wrap = $('#sc-playlist');
  if (!pickState.length) { wrap.innerHTML = '<div class="empty-drop">아래 라이브러리에서 영상을 추가하거나 폴더를 스캔하세요.</div>'; return; }
  wrap.innerHTML = pickState.map((id, i) => {
    const m = state.media.find((x) => x.id === id);
    if (!m) return '';
    const typeLabel = { upload: '업로드', folder: '폴더', youtube: '유튜브' }[m.type] || '';
    return `<div class="pl-item" draggable="true" data-id="${id}" data-idx="${i}">
      <span class="pl-handle">⠿</span>
      <span class="pl-num">${i + 1}</span>
      <span class="pl-name">${esc(m.name)} <span class="pl-type">· ${typeLabel}</span></span>
      <button class="pl-remove" data-remove="${id}" title="제거">✕</button>
    </div>`;
  }).join('');
}

// 라이브러리 (클릭 시 재생목록 맨 뒤에 추가)
function renderLibrary() {
  const wrap = $('#sc-library');
  if (!state.media.length) { wrap.innerHTML = '<div class="empty">먼저 미디어를 등록하세요.</div>'; return; }
  wrap.innerHTML = state.media.map((m) => {
    const count = pickState.filter((x) => x === m.id).length;
    return `<div class="pick-item ${count ? 'on' : ''}" data-add="${m.id}">
      <span class="order">${count || ''}</span>
      <span class="pick-name">${esc(m.name)}</span>
    </div>`;
  }).join('');
}

$('#sc-library').addEventListener('click', (e) => {
  const el = e.target.closest('[data-add]'); if (!el) return;
  pickState.push(el.dataset.add);
  renderPlaylist(); renderLibrary();
});

$('#sc-playlist').addEventListener('click', (e) => {
  const id = e.target.dataset.remove; if (!id) return;
  const idx = pickState.indexOf(id);
  if (idx >= 0) pickState.splice(idx, 1);
  renderPlaylist(); renderLibrary();
});

// 폴더 스캔 → 라이브러리 등록 + 재생목록 자동 배치(이름순)
$('#sc-scan').addEventListener('click', async () => {
  const folderPath = $('#sc-folder').value.trim(); if (!folderPath) return;
  $('#sc-scan').textContent = '스캔 중…'; $('#sc-scan').disabled = true;
  const added = await api.post('/api/media/folder', { folderPath });
  $('#sc-scan').textContent = '스캔 추가'; $('#sc-scan').disabled = false;
  if (added.error) { alert(added.error); return; }
  if (!added.length) { alert('폴더에서 영상 파일을 찾지 못했습니다.'); return; }
  state.media = await api.get('/api/media');          // 라이브러리 갱신
  added.forEach((m) => pickState.push(m.id));          // 순서대로 재생목록에 자동 배치
  $('#sc-folder').value = '';
  renderPlaylist(); renderLibrary();
});

// 드래그앤드롭 순서 변경
let dragIdx = null;
$('#sc-playlist').addEventListener('dragstart', (e) => {
  const item = e.target.closest('.pl-item'); if (!item) return;
  dragIdx = +item.dataset.idx; item.classList.add('dragging');
});
$('#sc-playlist').addEventListener('dragend', (e) => {
  const item = e.target.closest('.pl-item'); if (item) item.classList.remove('dragging');
  $$('#sc-playlist .pl-item').forEach((el) => el.classList.remove('drag-over'));
});
$('#sc-playlist').addEventListener('dragover', (e) => {
  e.preventDefault();
  const item = e.target.closest('.pl-item');
  $$('#sc-playlist .pl-item').forEach((el) => el.classList.remove('drag-over'));
  if (item) item.classList.add('drag-over');
});
$('#sc-playlist').addEventListener('drop', (e) => {
  e.preventDefault();
  const item = e.target.closest('.pl-item'); if (item == null || dragIdx == null) return;
  const dropIdx = +item.dataset.idx;
  if (dropIdx === dragIdx) return;
  const [moved] = pickState.splice(dragIdx, 1);
  pickState.splice(dropIdx, 0, moved);
  dragIdx = null;
  renderPlaylist();
});

$('#sc-cancel').addEventListener('click', () => $('#sched-modal').style.display = 'none');
$('#sc-save').addEventListener('click', async () => {
  const days = [...$('#sc-days').querySelectorAll('.day-btn.on')].map((b) => Number(b.dataset.day));
  const src = [...$$('input[name=schedsrc]')].find((r) => r.checked)?.value || 'direct';
  const body = {
    name: $('#sc-name').value.trim() || '새 스케줄',
    days: days.length ? days : [0, 1, 2, 3, 4, 5, 6],
    start: $('#sc-start').value || '00:00',
    end: $('#sc-end').value || '23:59',
    mediaIds: pickState,
    playlistId: src === 'playlist' ? ($('#sc-playlist-select').value || null) : null,
    enabled: $('#sc-enabled').checked,
  };
  if (editingId) await api.put('/api/schedules/' + editingId, body);
  else await api.post('/api/schedules', body);
  $('#sched-modal').style.display = 'none';
  await loadAll();
  openTimetable(); // 확정 후 송출 타임테이블 자동 표시
});

// 재생 순서 무작위 섞기 (폴더에서 불러온 뒤 랜덤 배정 → 사용자가 순서 조정)
$('#sc-shuffle').addEventListener('click', () => {
  for (let i = pickState.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [pickState[i], pickState[j]] = [pickState[j], pickState[i]];
  }
  renderPlaylist();
});

// ==================== 포스터 / 현수막 ====================
function mediaThumbUrl(m) {
  if (!m) return '';
  if (m.type === 'youtube') return m.thumbnail;
  return m.type === 'upload' ? '/media/' + m.src : '/api/stream/' + m.id;
}
function renderPosters() {
  const list = $('#poster-list');
  if (!state.posters.length) { list.innerHTML = '<div class="empty">등록된 포스터가 없습니다. “＋ 새 포스터/현수막”으로 추가하세요.</div>'; return; }
  list.innerHTML = state.posters.map((p) => {
    const m = state.media.find((x) => x.id === p.mediaId);
    const days = [...(p.days || [])].sort().map((d) => DAY_NAMES[d]).join('·');
    const slots = (p.slots || []).map((s) => `${s.start}~${s.end}`).join(', ');
    const range = `${p.startDate || '오늘'} ~ ${p.endDate || '무기한'}`;
    return `<div class="poster-item ${p.enabled === false ? 'off' : ''}">
      <img class="poster-thumb" src="${mediaThumbUrl(m)}" alt="">
      <div class="poster-main">
        <div class="poster-name">${esc(p.name)} ${p.enabled === false ? '(사용 안 함)' : ''}</div>
        <div class="poster-meta"><span class="badge">📅 ${esc(range)}</span><span class="badge">${days}</span><span class="badge">⏰ ${esc(slots || '시간대 없음')}</span></div>
      </div>
      <button class="btn" data-pedit="${p.id}">편집</button>
      <button class="btn btn-outline" data-pdel="${p.id}">삭제</button>
    </div>`;
  }).join('');
}
$('#poster-list').addEventListener('click', async (e) => {
  const ed = e.target.dataset.pedit, del = e.target.dataset.pdel;
  if (ed) openPosterModal(state.posters.find((p) => p.id === ed));
  if (del) { if (confirm('이 포스터를 삭제할까요?')) { await api.del('/api/posters/' + del); await loadAll(); } }
});
$('#new-poster').addEventListener('click', () => openPosterModal(null));

let editingPosterId = null;
let posterPick = null;
let posterSlots = [];

function openPosterModal(p) {
  editingPosterId = p?.id || null;
  $('#poster-modal-title').textContent = p ? '포스터/현수막 편집' : '새 포스터/현수막';
  $('#po-name').value = p?.name || '';
  $('#po-start').value = p?.startDate || '';
  $('#po-end').value = p?.endDate || '';
  $('#po-secs').value = p?.displaySeconds || 15;
  $('#po-enabled').checked = p ? p.enabled !== false : true;
  posterPick = p?.mediaId || null;
  posterSlots = p ? (p.slots || []).map((s) => ({ ...s })) : [{ start: '09:00', end: '18:00' }];
  if (!posterSlots.length) posterSlots = [{ start: '09:00', end: '18:00' }];
  const daysWrap = $('#po-days'); daysWrap.innerHTML = '';
  const sel = new Set(p?.days || [0, 1, 2, 3, 4, 5, 6]);
  DAY_NAMES.forEach((n, i) => {
    const b = document.createElement('button');
    b.className = 'day-btn' + (sel.has(i) ? ' on' : '');
    b.textContent = n; b.dataset.day = i; b.onclick = () => b.classList.toggle('on');
    daysWrap.appendChild(b);
  });
  renderPosterImages();
  renderPosterSlots();
  $('#poster-modal').style.display = 'flex';
}
function renderPosterImages() {
  const imgs = state.media.filter((m) => m.kind === 'image');
  const wrap = $('#po-images');
  if (!imgs.length) { wrap.innerHTML = '<div class="empty">등록된 이미지가 없습니다. 🎬 미디어 탭에서 포스터 이미지를 먼저 업로드하세요.</div>'; return; }
  wrap.innerHTML = imgs.map((m) => `<div class="pi ${posterPick === m.id ? 'on' : ''}" data-img="${m.id}"><img src="${mediaThumbUrl(m)}" alt=""><div class="cap">${esc(m.name)}</div></div>`).join('');
}
$('#po-images').addEventListener('click', (e) => { const el = e.target.closest('[data-img]'); if (!el) return; posterPick = el.dataset.img; renderPosterImages(); });
function renderPosterSlots() {
  $('#po-slots').innerHTML = posterSlots.map((s, i) => `<div class="slot-row">
    <input type="time" data-slot-start="${i}" value="${s.start}">
    <span class="tilde">~</span>
    <input type="time" data-slot-end="${i}" value="${s.end}">
    <button class="slot-del" data-slot-del="${i}" type="button">✕</button></div>`).join('');
}
$('#po-add-slot').addEventListener('click', () => { posterSlots.push({ start: '12:00', end: '13:00' }); renderPosterSlots(); });
$('#po-slots').addEventListener('input', (e) => {
  const si = e.target.dataset.slotStart, ei = e.target.dataset.slotEnd;
  if (si !== undefined) posterSlots[+si].start = e.target.value;
  if (ei !== undefined) posterSlots[+ei].end = e.target.value;
});
$('#po-slots').addEventListener('click', (e) => {
  const di = e.target.dataset.slotDel; if (di === undefined) return;
  posterSlots.splice(+di, 1);
  if (!posterSlots.length) posterSlots.push({ start: '09:00', end: '18:00' });
  renderPosterSlots();
});
$('#po-cancel').addEventListener('click', () => $('#poster-modal').style.display = 'none');
$('#po-save').addEventListener('click', async () => {
  if (!posterPick) return alert('포스터 이미지를 선택하세요.');
  const days = [...$('#po-days').querySelectorAll('.day-btn.on')].map((b) => +b.dataset.day);
  const body = {
    name: $('#po-name').value.trim() || '새 포스터',
    mediaId: posterPick,
    startDate: $('#po-start').value || '',
    endDate: $('#po-end').value || '',
    days: days.length ? days : [0, 1, 2, 3, 4, 5, 6],
    slots: posterSlots,
    displaySeconds: +$('#po-secs').value || 15,
    enabled: $('#po-enabled').checked,
  };
  if (editingPosterId) await api.put('/api/posters/' + editingPosterId, body);
  else await api.post('/api/posters', body);
  $('#poster-modal').style.display = 'none';
  await loadAll();
});

// ==================== 설정 ====================
function renderSettings() {
  const s = state.settings;
  // 해상도 프리셋
  const wrap = $('#res-presets');
  wrap.innerHTML = RES_PRESETS.map((p) => {
    const active = p.width === s.resolution.width && p.height === s.resolution.height;
    return `<div class="chip ${active ? 'active' : ''}" data-w="${p.width}" data-h="${p.height}">${p.label}</div>`;
  }).join('');
  $('#res-w').value = s.resolution.width;
  $('#res-h').value = s.resolution.height;

  // 밝기
  const b = s.brightness;
  $$('input[name=bmode]').forEach((r) => r.checked = r.value === b.mode);
  toggleBrightBoxes(b.mode);
  $('#manual-range').value = b.manual; $('#manual-val').textContent = b.manual + '%';
  $('#day-start').value = b.day.start; $('#day-val').value = b.day.value; $('#day-val-t').textContent = b.day.value + '%';
  $('#night-start').value = b.night.start; $('#night-val').value = b.night.value; $('#night-val-t').textContent = b.night.value + '%';

  // 스케일 방식 / 대기 화면
  $$('input[name=scale]').forEach((r) => r.checked = r.value === (s.scaleMode || 'contain'));
  $$('input[name=idlemode]').forEach((r) => r.checked = r.value === (s.idleMode || 'black'));
  renderOverlay();
  renderLayout();
  renderPower();
  renderAuth();
}

// ---------- 화면 레이아웃(멀티존) ----------
const LAYOUT_PRESETS = [
  { key: 'single', label: '전체화면' },
  { key: 'split-lr', label: '좌우 분할\n(메인70+우30)' },
  { key: 'split-tb', label: '상하 분할\n(메인75+하25)' },
  { key: 'main-bottom', label: '메인 + 하단바' },
];
function renderLayout() {
  const cfg = state.settings.layout || { preset: 'single', subMediaIds: [] };
  $('#layout-presets').innerHTML = LAYOUT_PRESETS.map((p) =>
    `<div class="lp lp-${p.key} ${cfg.preset === p.key ? 'on' : ''}" data-lp="${p.key}"><div class="lp-vis"></div><span>${esc(p.label)}</span></div>`).join('');
  $('#sub-media-box').style.display = cfg.preset === 'single' ? 'none' : 'block';
  const sel = cfg.subMediaIds || [];
  $('#sub-media').innerHTML = state.media.length
    ? state.media.map((m) => { const i = sel.indexOf(m.id); return `<div class="pick-item ${i >= 0 ? 'on' : ''}" data-subpick="${m.id}"><span class="order">${i >= 0 ? i + 1 : ''}</span><span class="pick-name">${esc(m.name)}</span></div>`; }).join('')
    : '<div class="empty">먼저 미디어를 등록하세요.</div>';
}
$('#layout-presets').addEventListener('click', (e) => {
  const el = e.target.closest('[data-lp]'); if (!el) return;
  const cfg = state.settings.layout || { preset: 'single', subMediaIds: [] };
  saveLayout({ preset: el.dataset.lp, subMediaIds: cfg.subMediaIds || [] });
});
$('#sub-media').addEventListener('click', (e) => {
  const el = e.target.closest('[data-subpick]'); if (!el) return;
  const cfg = state.settings.layout || { preset: 'single', subMediaIds: [] };
  const ids = [...(cfg.subMediaIds || [])];
  const i = ids.indexOf(el.dataset.subpick);
  if (i >= 0) ids.splice(i, 1); else ids.push(el.dataset.subpick);
  saveLayout({ preset: cfg.preset, subMediaIds: ids });
});
async function saveLayout(layout) { state.settings = await api.put('/api/settings', { layout }); renderLayout(); }

// ---------- 오버레이(자막 티커 / 시계 / 날씨) ----------
const WX_CITIES = [
  { name: '서울', lat: 37.5665, lon: 126.9780 }, { name: '부산', lat: 35.1796, lon: 129.0756 },
  { name: '인천', lat: 37.4563, lon: 126.7052 }, { name: '대구', lat: 35.8714, lon: 128.6014 },
  { name: '대전', lat: 36.3504, lon: 127.3845 }, { name: '광주', lat: 35.1595, lon: 126.8526 },
  { name: '울산', lat: 35.5384, lon: 129.3114 }, { name: '수원', lat: 37.2636, lon: 127.0286 },
  { name: '제주', lat: 33.4996, lon: 126.5312 },
];
(function initWxCities() {
  $('#ov-wx-city').innerHTML = WX_CITIES.map((c, i) => `<option value="${i}">${c.name}</option>`).join('');
})();
function renderOverlay() {
  const o = state.settings.overlay || { ticker: {}, clock: {}, weather: {} };
  const t = o.ticker || {}, c = o.clock || {}, w = o.weather || {};
  $('#ov-ticker-on').checked = !!t.enabled;
  $$('input[name=tickersrc]').forEach((r) => r.checked = r.value === (t.source || 'text'));
  $('#ov-ticker-text').value = t.text || '';
  $('#ov-ticker-rss').value = t.rssUrl || '';
  $('#ov-ticker-text-row').style.display = (t.source === 'rss') ? 'none' : 'block';
  $('#ov-ticker-rss-row').style.display = (t.source === 'rss') ? 'block' : 'none';
  $('#ov-ticker-pos').value = t.position || 'bottom';
  $('#ov-ticker-speed').value = t.speedSec || 20; $('#ov-ticker-speed-t').textContent = (t.speedSec || 20) + 's';
  $('#ov-ticker-color').value = t.color || '#ffffff';
  $('#ov-ticker-bg').value = t.bg || '#000000';
  $('#ov-ticker-op').value = t.opacity ?? 65; $('#ov-ticker-op-t').textContent = (t.opacity ?? 65) + '%';
  $('#ov-ticker-box').style.opacity = t.enabled ? '1' : '.5';
  $('#ov-clock-on').checked = !!c.enabled;
  $('#ov-clock-pos').value = c.position || 'top-right';
  $('#ov-clock-date').checked = c.showDate !== false;
  $('#ov-clock-box').style.opacity = c.enabled ? '1' : '.5';
  // 날씨
  $('#ov-wx-on').checked = !!w.enabled;
  const ci = WX_CITIES.findIndex((x) => Math.abs(x.lat - (w.lat ?? 37.5665)) < 0.01 && Math.abs(x.lon - (w.lon ?? 126.978)) < 0.01);
  $('#ov-wx-city').value = ci >= 0 ? ci : 0;
  $('#ov-wx-label').value = w.label || '서울';
  $('#ov-wx-pos').value = w.position || 'top-left';
  $('#ov-wx-box').style.opacity = w.enabled ? '1' : '.5';
}
let ovTimer = null;
function saveOverlay() {
  clearTimeout(ovTimer);
  ovTimer = setTimeout(async () => {
    const src = [...$$('input[name=tickersrc]')].find((r) => r.checked)?.value || 'text';
    const city = WX_CITIES[+$('#ov-wx-city').value] || WX_CITIES[0];
    const overlay = {
      ticker: {
        enabled: $('#ov-ticker-on').checked, text: $('#ov-ticker-text').value, source: src, rssUrl: $('#ov-ticker-rss').value,
        position: $('#ov-ticker-pos').value, speedSec: +$('#ov-ticker-speed').value,
        color: $('#ov-ticker-color').value, bg: $('#ov-ticker-bg').value, opacity: +$('#ov-ticker-op').value,
      },
      clock: { enabled: $('#ov-clock-on').checked, position: $('#ov-clock-pos').value, showDate: $('#ov-clock-date').checked },
      weather: { enabled: $('#ov-wx-on').checked, lat: city.lat, lon: city.lon, label: $('#ov-wx-label').value, position: $('#ov-wx-pos').value },
    };
    state.settings = await api.put('/api/settings', { overlay });
  }, 250);
}
['ov-ticker-on', 'ov-ticker-text', 'ov-ticker-rss', 'ov-ticker-pos', 'ov-ticker-speed', 'ov-ticker-color', 'ov-ticker-bg', 'ov-ticker-op', 'ov-clock-on', 'ov-clock-pos', 'ov-clock-date', 'ov-wx-on', 'ov-wx-city', 'ov-wx-label', 'ov-wx-pos'].forEach((id) => {
  const el = $('#' + id);
  const ev = (el.type === 'range' || el.type === 'text' || el.type === 'color') ? 'input' : 'change';
  el.addEventListener(ev, () => {
    if (id === 'ov-ticker-speed') $('#ov-ticker-speed-t').textContent = el.value + 's';
    if (id === 'ov-ticker-op') $('#ov-ticker-op-t').textContent = el.value + '%';
    if (id === 'ov-ticker-on') $('#ov-ticker-box').style.opacity = el.checked ? '1' : '.5';
    if (id === 'ov-clock-on') $('#ov-clock-box').style.opacity = el.checked ? '1' : '.5';
    if (id === 'ov-wx-on') $('#ov-wx-box').style.opacity = el.checked ? '1' : '.5';
    if (id === 'ov-wx-city') $('#ov-wx-label').value = WX_CITIES[+el.value].name;
    saveOverlay();
  });
});
$$('input[name=tickersrc]').forEach((r) => r.addEventListener('change', () => {
  $('#ov-ticker-text-row').style.display = (r.value === 'rss') ? 'none' : 'block';
  $('#ov-ticker-rss-row').style.display = (r.value === 'rss') ? 'block' : 'none';
  saveOverlay();
}));

// ---------- 화면 전원(취침) 스케줄 ----------
function renderPower() {
  const p = state.settings.power || {};
  $('#pw-on').checked = !!p.enabled;
  $('#pw-start').value = p.offStart || '00:00';
  $('#pw-end').value = p.offEnd || '06:00';
  $('#pw-box').style.opacity = p.enabled ? '1' : '.5';
  const sel = new Set(p.days || [0, 1, 2, 3, 4, 5, 6]);
  $('#pw-days').innerHTML = DAY_NAMES.map((n, i) => `<button class="day-btn ${sel.has(i) ? 'on' : ''}" data-pwday="${i}" type="button">${n}</button>`).join('');
}
$('#pw-days').addEventListener('click', (e) => { const b = e.target.closest('[data-pwday]'); if (!b) return; b.classList.toggle('on'); savePower(); });
['pw-on', 'pw-start', 'pw-end'].forEach((id) => $('#' + id).addEventListener('change', () => { if (id === 'pw-on') $('#pw-box').style.opacity = $('#pw-on').checked ? '1' : '.5'; savePower(); }));
async function savePower() {
  const days = [...$('#pw-days').querySelectorAll('.day-btn.on')].map((b) => +b.dataset.pwday);
  state.settings = await api.put('/api/settings', { power: { enabled: $('#pw-on').checked, offStart: $('#pw-start').value, offEnd: $('#pw-end').value, days } });
}

// ---------- 관리자 잠금(사용자 권한) ----------
function renderAuth() { /* 계정 블록은 정적(변경/로그아웃). 별도 상태표시 없음 */ }
$('#acc-change').addEventListener('click', async () => {
  const cur = $('#acc-cur').value, next = $('#acc-new').value;
  if (!cur || !next) return alert('현재/새 비밀번호를 입력하세요.');
  const r = await api.post('/api/auth/change-password', { current: cur, next });
  if (r.error) return alert(r.error);
  if (r.token) localStorage.setItem('sig-auth-token', r.token);
  $('#acc-cur').value = ''; $('#acc-new').value = '';
  alert('비밀번호가 변경되었습니다.');
});
$('#acc-logout').addEventListener('click', async () => {
  try { await api.post('/api/auth/logout', {}); } catch {}
  localStorage.removeItem('sig-auth-token');
  location.reload();
});

// ---------- 인증 게이트 (무조건 로그인) — 인증 전엔 CMS 안 보임 ----------
let appStarted = false;
function showGate(which) {
  $('#auth-gate').style.display = 'flex';
  $('#gate-setup').style.display = which === 'setup' ? 'block' : 'none';
  $('#gate-login').style.display = which === 'login' ? 'block' : 'none';
  setTimeout(() => { const el = which === 'setup' ? $('#setup-pw') : $('#login-pw'); if (el) el.focus(); }, 50);
}
function hideGate() { $('#auth-gate').style.display = 'none'; }
function showLogin() { showGate('login'); } // apiFetch가 401 만나면 호출
async function doSetup() {
  const pw = $('#setup-pw').value, pw2 = $('#setup-pw2').value;
  if ((pw || '').length < 4) { $('#setup-err').textContent = '비밀번호는 4자 이상이어야 합니다'; return; }
  if (pw !== pw2) { $('#setup-err').textContent = '비밀번호가 일치하지 않습니다'; return; }
  const r = await fetch('/api/auth/setup', { method: 'POST', headers: JH, body: JSON.stringify({ password: pw }) }).then((x) => x.json()).catch(() => ({}));
  if (r.token) { localStorage.setItem('sig-auth-token', r.token); $('#setup-err').textContent = ''; hideGate(); startApp(); }
  else $('#setup-err').textContent = r.error || '설정 실패';
}
async function doLogin() {
  const pw = $('#login-pw').value;
  const r = await fetch('/api/auth/login', { method: 'POST', headers: JH, body: JSON.stringify({ password: pw }) }).then((x) => x.json()).catch(() => ({}));
  if (r.ok) { if (r.token) localStorage.setItem('sig-auth-token', r.token); $('#login-pw').value = ''; $('#login-err').textContent = ''; hideGate(); startApp(); }
  else if (r.needsSetup) showGate('setup');
  else $('#login-err').textContent = r.error || '로그인 실패';
}
$('#setup-btn').addEventListener('click', doSetup);
$('#setup-pw2').addEventListener('keydown', (e) => { if (e.key === 'Enter') doSetup(); });
$('#login-btn').addEventListener('click', doLogin);
$('#login-pw').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); });
async function checkGate() {
  const st = await apiFetch('/api/auth/status', { headers: authHeaders() }).then((r) => r.json()).catch(() => ({}));
  if (!st.configured) { showGate('setup'); return false; }
  if (!st.authed) { showGate('login'); return false; }
  hideGate(); return true;
}
function startApp() {
  if (appStarted) { loadAll(); return; }
  appStarted = true;
  loadAll(); loadRemote(); loadCloud(); loadSnaps(); refreshUndo(); connect();
}

// ---------- 재생목록(캠페인) 관리 ----------
$('#open-playlists').addEventListener('click', () => { renderPlaylistList(); $('#pl-form').style.display = 'none'; $('#playlist-modal').style.display = 'flex'; });
$('#pl-close').addEventListener('click', () => $('#playlist-modal').style.display = 'none');
function renderPlaylistList() {
  $('#pl-list').innerHTML = state.playlists.length
    ? state.playlists.map((p) => `<div class="sched-item"><div class="sched-main"><div class="sched-name">${esc(p.name)}</div><div class="sched-meta">영상 ${(p.mediaIds || []).length}개</div></div><button class="btn" data-pledit="${p.id}">편집</button><button class="btn btn-outline" data-pldel="${p.id}">삭제</button></div>`).join('')
    : '<div class="empty">재생목록이 없습니다. “＋ 새 재생목록”으로 만드세요.</div>';
}
$('#pl-list').addEventListener('click', async (e) => {
  const ed = e.target.dataset.pledit, del = e.target.dataset.pldel;
  if (ed) openPlaylistForm(state.playlists.find((p) => p.id === ed));
  if (del) { if (confirm('이 재생목록을 삭제할까요?')) { await api.del('/api/playlists/' + del); await loadAll(); renderPlaylistList(); } }
});
$('#pl-new').addEventListener('click', () => openPlaylistForm(null));
let plEditingId = null, plPick = [];
function openPlaylistForm(p) { plEditingId = p?.id || null; $('#pl-name').value = p?.name || ''; plPick = p ? [...p.mediaIds] : []; renderPlOrder(); renderPlLibrary(); $('#pl-form').style.display = 'block'; }
function renderPlOrder() {
  const wrap = $('#pl-playlist');
  if (!plPick.length) { wrap.innerHTML = '<div class="empty-drop">아래에서 영상을 추가하세요.</div>'; return; }
  wrap.innerHTML = plPick.map((id, i) => { const m = state.media.find((x) => x.id === id); return `<div class="pl-item" draggable="true" data-id="${id}" data-idx="${i}"><span class="pl-handle">⠿</span><span class="pl-num">${i + 1}</span><span class="pl-name">${esc(m ? m.name : '(삭제됨)')}</span><button class="pl-remove" data-plrm="${id}">✕</button></div>`; }).join('');
}
function renderPlLibrary() {
  $('#pl-library').innerHTML = state.media.length
    ? state.media.map((m) => { const c = plPick.filter((x) => x === m.id).length; return `<div class="pick-item ${c ? 'on' : ''}" data-pladd="${m.id}"><span class="order">${c || ''}</span><span class="pick-name">${esc(m.name)}</span></div>`; }).join('')
    : '<div class="empty">미디어를 먼저 등록하세요.</div>';
}
$('#pl-library').addEventListener('click', (e) => { const el = e.target.closest('[data-pladd]'); if (!el) return; plPick.push(el.dataset.pladd); renderPlOrder(); renderPlLibrary(); });
$('#pl-playlist').addEventListener('click', (e) => { const id = e.target.dataset.plrm; if (!id) return; const i = plPick.indexOf(id); if (i >= 0) plPick.splice(i, 1); renderPlOrder(); renderPlLibrary(); });
$('#pl-shuffle').addEventListener('click', () => { for (let i = plPick.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1));[plPick[i], plPick[j]] = [plPick[j], plPick[i]]; } renderPlOrder(); });
let plDrag = null;
$('#pl-playlist').addEventListener('dragstart', (e) => { const it = e.target.closest('.pl-item'); if (!it) return; plDrag = +it.dataset.idx; it.classList.add('dragging'); });
$('#pl-playlist').addEventListener('dragend', (e) => { const it = e.target.closest('.pl-item'); if (it) it.classList.remove('dragging'); });
$('#pl-playlist').addEventListener('dragover', (e) => e.preventDefault());
$('#pl-playlist').addEventListener('drop', (e) => { e.preventDefault(); const it = e.target.closest('.pl-item'); if (!it || plDrag == null) return; const di = +it.dataset.idx; if (di === plDrag) return; const [m] = plPick.splice(plDrag, 1); plPick.splice(di, 0, m); plDrag = null; renderPlOrder(); });
$('#pl-cancel').addEventListener('click', () => { $('#pl-form').style.display = 'none'; });
$('#pl-save').addEventListener('click', async () => {
  const body = { name: $('#pl-name').value.trim() || '새 재생목록', mediaIds: plPick };
  if (plEditingId) await api.put('/api/playlists/' + plEditingId, body); else await api.post('/api/playlists', body);
  $('#pl-form').style.display = 'none'; await loadAll(); renderPlaylistList();
});

// ---------- 스케줄 재생 소스(직접 / 재생목록) ----------
function setupSchedSource(sc) {
  const usePlaylist = !!(sc && sc.playlistId);
  $$('input[name=schedsrc]').forEach((r) => r.checked = r.value === (usePlaylist ? 'playlist' : 'direct'));
  $('#sc-playlist-select').innerHTML = state.playlists.length
    ? state.playlists.map((p) => `<option value="${p.id}">${esc(p.name)} (영상 ${(p.mediaIds || []).length})</option>`).join('')
    : '<option value="">등록된 재생목록 없음</option>';
  if (usePlaylist) $('#sc-playlist-select').value = sc.playlistId;
  applySchedSourceUI(usePlaylist ? 'playlist' : 'direct');
}
function applySchedSourceUI(v) {
  $('#sc-direct-box').style.display = v === 'playlist' ? 'none' : 'block';
  $('#sc-playlist-select').style.display = v === 'playlist' ? 'inline-block' : 'none';
}
$$('input[name=schedsrc]').forEach((r) => r.addEventListener('change', () => applySchedSourceUI(r.value)));

function toggleBrightBoxes(mode) {
  $('#manual-box').classList.toggle('active', mode === 'manual');
  $('#auto-box').classList.toggle('active', mode === 'auto');
}

$('#res-presets').addEventListener('click', (e) => {
  const chip = e.target.closest('.chip'); if (!chip) return;
  saveSettings({ resolution: { width: +chip.dataset.w, height: +chip.dataset.h, preset: `${chip.dataset.w}x${chip.dataset.h}` } });
});
$('#apply-res').addEventListener('click', () => {
  const w = +$('#res-w').value, h = +$('#res-h').value;
  if (w > 0 && h > 0) saveSettings({ resolution: { width: w, height: h, preset: `${w}x${h}` } });
});

$$('input[name=bmode]').forEach((r) => r.addEventListener('change', () => {
  toggleBrightBoxes(r.value);
  saveSettings({ brightness: { ...state.settings.brightness, mode: r.value } });
}));

// 슬라이더 실시간 라벨 + 저장(디바운스)
let bTimer = null;
function debounceBrightness() {
  clearTimeout(bTimer);
  bTimer = setTimeout(() => {
    saveSettings({ brightness: {
      ...state.settings.brightness,
      manual: +$('#manual-range').value,
      day: { start: $('#day-start').value, value: +$('#day-val').value },
      night: { start: $('#night-start').value, value: +$('#night-val').value },
    }});
  }, 250);
}
$('#manual-range').addEventListener('input', (e) => { $('#manual-val').textContent = e.target.value + '%'; debounceBrightness(); });
$('#day-val').addEventListener('input', (e) => { $('#day-val-t').textContent = e.target.value + '%'; debounceBrightness(); });
$('#night-val').addEventListener('input', (e) => { $('#night-val-t').textContent = e.target.value + '%'; debounceBrightness(); });
$('#day-start').addEventListener('change', debounceBrightness);
$('#night-start').addEventListener('change', debounceBrightness);
$$('input[name=scale]').forEach((r) => r.addEventListener('change', () => saveSettings({ scaleMode: r.value })));
$$('input[name=idlemode]').forEach((r) => r.addEventListener('change', () => saveSettings({ idleMode: r.value })));

// ==================== 모니터 감지 ====================
let detectedMonitor = null; // 플레이어가 보고한 전광판 모니터
function onScreenInfo(info) {
  detectedMonitor = info;
  const el = $('#md-current');
  el.classList.add('live');
  el.textContent = `${info.width} × ${info.height}` + (info.dpr && info.dpr !== 1 ? ` (배율 ${info.dpr}×)` : '');
  $('#apply-detected').disabled = false;
}
$('#apply-detected').addEventListener('click', () => {
  if (!detectedMonitor) return;
  saveSettings({ resolution: { width: detectedMonitor.width, height: detectedMonitor.height, preset: `${detectedMonitor.width}x${detectedMonitor.height}` } });
});
// 이 PC에 연결된 모니터 목록 감지 (Window Management API, 지원 시)
$('#scan-monitors').addEventListener('click', async () => {
  const list = $('#monitor-list');
  try {
    if (window.getScreenDetails) {
      const details = await window.getScreenDetails();
      list.innerHTML = details.screens.map((sc, i) => {
        const w = Math.round(sc.width * (sc.devicePixelRatio || 1));
        const h = Math.round(sc.height * (sc.devicePixelRatio || 1));
        return `<div class="mon" data-w="${w}" data-h="${h}">모니터 ${i + 1}${sc.isPrimary ? ' (주)' : ''}<small>${w} × ${h}</small></div>`;
      }).join('');
    } else {
      const w = Math.round(window.screen.width * (window.devicePixelRatio || 1));
      const h = Math.round(window.screen.height * (window.devicePixelRatio || 1));
      list.innerHTML = `<div class="mon" data-w="${w}" data-h="${h}">이 모니터<small>${w} × ${h}</small></div>`;
    }
  } catch (e) {
    const w = Math.round(window.screen.width * (window.devicePixelRatio || 1));
    const h = Math.round(window.screen.height * (window.devicePixelRatio || 1));
    list.innerHTML = `<div class="mon" data-w="${w}" data-h="${h}">이 모니터<small>${w} × ${h}</small></div>`;
  }
});
$('#monitor-list').addEventListener('click', (e) => {
  const mon = e.target.closest('.mon'); if (!mon) return;
  saveSettings({ resolution: { width: +mon.dataset.w, height: +mon.dataset.h, preset: `${mon.dataset.w}x${mon.dataset.h}` } });
});

async function saveSettings(patch) {
  state.settings = await api.put('/api/settings', patch);
  renderSettings();
}

// ==================== 긴급 송출 ====================
async function refreshEmergency() {
  const st = await api.get('/api/state');
  state.emergency = st.emergency;
  renderEmergency();
}
function renderEmergency() {
  const active = state.emergency?.active;
  $('#emergency-card').classList.toggle('active', !!active);
  $('#em-start').style.display = active ? 'none' : 'block';
  $('#em-stop').style.display = active ? 'block' : 'none';
  if (active) {
    const m = state.media.find((x) => x.id === state.emergency.mediaId);
    if (m) $('#em-select').value = m.id;
  }
}
$('#em-start').addEventListener('click', async () => {
  const id = $('#em-select').value;
  if (!id || id === '미디어 없음') return alert('먼저 미디어를 등록하세요.');
  await api.post('/api/emergency', { mediaId: id });
  await refreshEmergency();
});
$('#em-stop').addEventListener('click', async () => {
  await api.del('/api/emergency');
  await refreshEmergency();
});

// ==================== 전광판 열기 ====================
$('#open-player').addEventListener('click', () => window.open('/player/', 'signage-player', 'width=1280,height=720'));

// ==================== 재생 이력 ====================
function histMode(m) { return { emergency: '🚨 긴급', poster: '🖼 포스터', schedule: '정상', idle: '대기' }[m] || m; }
async function openHistory() {
  const h = await api.get('/api/history?limit=500');
  const rows = h.map((e) => `<tr><td>${esc(String(e.t).replace('T', ' ').slice(0, 19))}</td><td>${esc(e.name || '')}</td><td>${histMode(e.mode)}</td><td>${esc(e.schedule || '')}</td></tr>`).join('');
  $('#hist-table').innerHTML = `<thead><tr><th>시간</th><th>콘텐츠</th><th>모드</th><th>편성</th></tr></thead><tbody>${rows || '<tr><td colspan="4" class="empty">아직 재생 이력이 없습니다</td></tr>'}</tbody>`;
  $('#history-modal').style.display = 'flex';
}
$('#open-history').addEventListener('click', openHistory);
$('#hist-close').addEventListener('click', () => $('#history-modal').style.display = 'none');
$('#hist-csv').addEventListener('click', () => window.open('/api/history.csv', '_blank'));
$('#hist-clear').addEventListener('click', async () => { if (confirm('재생 이력을 모두 지울까요?')) { await api.del('/api/history'); openHistory(); } });

// ==================== 송출 타임테이블 ====================
const TT_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316'];
const HOUR_H = 26; // 시간당 높이(px)

function hm2min(hm) { const [h, m] = hm.split(':').map(Number); return h * 60 + m; }

function openTimetable() {
  renderTimetable();
  $('#timetable-modal').style.display = 'flex';
}
$('#open-timetable').addEventListener('click', openTimetable);
$('#tt-close').addEventListener('click', () => $('#timetable-modal').style.display = 'none');

function renderTimetable() {
  const bodyH = 24 * HOUR_H;
  const days = ['일', '월', '화', '수', '목', '금', '토'];

  // 각 스케줄이 차지하는 (요일→구간) 계산 (자정 넘김은 분할)
  const blocksByDay = Array.from({ length: 7 }, () => []);
  state.schedules.forEach((s, si) => {
    if (s.enabled === false) return;
    const color = TT_COLORS[si % TT_COLORS.length];
    const start = hm2min(s.start), end = hm2min(s.end);
    const nameCount = (s.mediaIds || []).length;
    (s.days || []).forEach((d) => {
      if (start < end || end === start) {
        blocksByDay[d].push({ s, color, from: start, to: end === start ? 1440 : end, nameCount });
      } else {
        // 자정 넘김: 당일 [start,1440) + 다음날 [0,end)
        blocksByDay[d].push({ s, color, from: start, to: 1440, nameCount });
        blocksByDay[(d + 1) % 7].push({ s, color, from: 0, to: end, nameCount });
      }
    });
  });

  // 시간 축
  let hourLines = '';
  for (let h = 0; h <= 24; h++) {
    hourLines += `<div class="tt-hourline" style="top:${h * HOUR_H}px"></div>`;
    if (h < 24) hourLines += `<div class="tt-hourlabel" style="top:${h * HOUR_H}px">${String(h).padStart(2, '0')}</div>`;
  }

  const now = new Date();
  const nowMin = now.getHours() * 60 + now.getMinutes();
  const nowTop = (nowMin / 1440) * bodyH;
  const todayIdx = now.getDay();

  let html = `<div class="tt-col tt-axis"><div class="tt-headcell">시간</div><div class="tt-body" style="height:${bodyH}px">${hourLines}</div></div>`;

  for (let d = 0; d < 7; d++) {
    const cls = d === 0 ? 'sun' : d === 6 ? 'sat' : '';
    let blocks = '';
    for (let h = 0; h <= 24; h++) blocks += `<div class="tt-hourline" style="top:${h * HOUR_H}px"></div>`;
    blocksByDay[d].sort((a, b) => a.from - b.from).forEach((b) => {
      const top = (b.from / 1440) * bodyH;
      const height = Math.max(14, ((b.to - b.from) / 1440) * bodyH);
      const t = `${String(Math.floor(b.from / 60)).padStart(2, '0')}:${String(b.from % 60).padStart(2, '0')}`;
      blocks += `<div class="tt-block" style="top:${top}px;height:${height}px;background:${b.color}"
        title="${esc(b.s.name)} · ${b.s.start}~${b.s.end} · 영상 ${b.nameCount}개">
        ${esc(b.s.name)}<small>${t} · 영상 ${b.nameCount}</small></div>`;
    });
    // 오늘 열에 현재 시각선
    const nowLine = d === todayIdx
      ? `<div class="tt-nowline" style="top:${nowTop}px"></div><div class="tt-nowdot" style="top:${nowTop}px"></div>`
      : '';
    html += `<div class="tt-col"><div class="tt-headcell ${cls}">${days[d]}</div><div class="tt-body" style="height:${bodyH}px">${blocks}${nowLine}</div></div>`;
  }
  $('#timetable').innerHTML = html;
}

// ==================== 저장 / 불러오기 ====================
$('#export-schedules').addEventListener('click', async () => {
  const data = await api.get('/api/export');
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  const stamp = new Date().toISOString().slice(0, 10);
  a.download = `사이니지-송출구성-${stamp}.json`;
  a.click();
  URL.revokeObjectURL(a.href);
});
$('#import-schedules').addEventListener('click', () => $('#import-file').click());
$('#import-file').addEventListener('change', async (e) => {
  const file = e.target.files[0]; if (!file) return;
  try {
    const data = JSON.parse(await file.text());
    const mode = confirm('불러오기 방식을 선택하세요.\n\n확인 = 현재 구성을 교체(덮어쓰기)\n취소 = 기존에 이어서 추가(병합)') ? 'replace' : 'merge';
    const r = await api.post('/api/import', { data, mode });
    if (r.error) alert(r.error);
    else { alert(`불러오기 완료 · 미디어 ${r.media}개, 스케줄 ${r.schedules}개`); await loadAll(); }
  } catch (err) { alert('올바른 구성 파일이 아닙니다: ' + err.message); }
  $('#import-file').value = '';
});

// ==================== 드래그앤드롭 파일 업로드 ====================
(function setupDropUpload() {
  const card = document.querySelectorAll('.add-card')[1]; // 업로드 카드
  if (!card) return;
  ['dragenter', 'dragover'].forEach((ev) => card.addEventListener(ev, (e) => { e.preventDefault(); card.classList.add('dragover'); }));
  ['dragleave', 'drop'].forEach((ev) => card.addEventListener(ev, (e) => { e.preventDefault(); card.classList.remove('dragover'); }));
  card.addEventListener('drop', async (e) => {
    const files = [...(e.dataTransfer?.files || [])].filter((f) => f.type.startsWith('video/') || f.type.startsWith('image/'));
    if (!files.length) return;
    const fd = new FormData(); files.forEach((f) => fd.append('files', f));
    $('#upload-progress').textContent = '업로드 중…';
    await fetch('/api/media/upload', { method: 'POST', body: fd });
    $('#upload-progress').textContent = '완료!'; setTimeout(() => $('#upload-progress').textContent = '', 1500);
    await loadAll();
  });
})();

// ==================== 미리보기 (동일 엔진, 무음) ====================
const previewStage = $('#preview-stage');
const previewEngine = new ZonePlayer(previewStage, { muted: true });
let previewRes = { width: 1920, height: 1080 };

function layoutPreview() {
  const frame = $('#preview-frame');
  previewStage.style.width = previewRes.width + 'px';
  previewStage.style.height = previewRes.height + 'px';
  const scale = Math.min(frame.clientWidth / previewRes.width, frame.clientHeight / previewRes.height);
  previewStage.style.transform = `scale(${scale})`;
}
window.addEventListener('resize', layoutPreview);

function applyPreviewDirective(d) {
  if (d.resolution) {
    previewRes = d.resolution;
    layoutPreview();
    $('#pi-res').textContent = `${d.resolution.width} × ${d.resolution.height}`;
  }
  const modeText = { emergency: '🚨 긴급 송출', poster: '🖼 포스터/현수막', schedule: '정상 송출', idle: '대기(편성 없음)', off: '🌙 화면 꺼짐(취침)' }[d.mode] || d.mode;
  $('#pi-mode').textContent = modeText;
  $('#pi-bright').textContent = (d.brightness ?? 100) + '%';
  if (d.mode === 'idle') $('#pi-now').textContent = '-';
  else if (d.scheduleName) $('#pi-now').textContent = d.scheduleName;
  previewEngine.apply(d);
}

// ==================== WebSocket ====================
let ws = null;
function connect() {
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
  ws = new WebSocket(`${proto}://${location.host}/ws?role=preview`);
  ws.onopen = () => { $('#conn').classList.add('ok'); $('#conn-text').textContent = '연결됨'; };
  ws.onclose = () => { $('#conn').classList.remove('ok'); $('#conn-text').textContent = '재연결 중…'; setTimeout(connect, 1500); };
  ws.onmessage = (ev) => {
    const msg = JSON.parse(ev.data);
    if (msg.type === 'directive') applyPreviewDirective(msg.directive);
    else if (msg.type === 'nowplaying') { if (msg.media) $('#pi-now').textContent = msg.media.name; }
    else if (msg.type === 'screeninfo') onScreenInfo(msg);
    else if (msg.type === 'remote-state') setRemoteState(msg.state);
    else if (msg.type === 'changed') { loadAll(); refreshUndo(); loadCloud(); }
  };
}

// ==================== 원격 지원 ====================
const RM_STATE_TEXT = { disabled: '비활성화', connecting: '연결 중…', connected: '연결됨 (원격 제어 가능)', disconnected: '연결 끊김 — 재시도 중', denied: '인증 실패 — 토큰 확인' };
async function loadRemote() {
  const r = await api.get('/api/remote');
  $('#rm-name').value = r.clientName || '';
  $('#rm-url').value = r.managerUrl || '';
  $('#rm-token').placeholder = r.hasToken ? '저장된 토큰 있음 (변경 시에만 입력)' : '운영자에게 받은 토큰';
  $('#rm-allow-update').checked = r.allowUpdate !== false;
  setRemoteState(r.enabled ? r.connection : 'disabled');
  $('#rm-enable').style.display = r.enabled ? 'none' : 'block';
  $('#rm-disable').style.display = r.enabled ? 'block' : 'none';
}
function setRemoteState(s) {
  const box = $('#remote-status');
  box.className = 'remote-status ' + s;
  $('#remote-state-text').textContent = RM_STATE_TEXT[s] || s;
}
$('#rm-enable').addEventListener('click', async () => {
  const url = $('#rm-url').value.trim();
  const token = $('#rm-token').value.trim();
  if (!url) return alert('매니저 주소를 입력하세요.');
  const body = { enabled: true, managerUrl: url, clientName: $('#rm-name').value.trim(), allowUpdate: $('#rm-allow-update').checked };
  if (token) body.token = token;
  await api.put('/api/remote', body);
  $('#rm-token').value = '';
  await loadRemote();
});
$('#rm-disable').addEventListener('click', async () => {
  await api.put('/api/remote', { enabled: false });
  await loadRemote();
});
$('#rm-allow-update').addEventListener('change', async (e) => { await api.put('/api/remote', { allowUpdate: e.target.checked }); });

// ==================== 클라우드 백업 ====================
function fmtWhen(s) { return s ? new Date(s).toLocaleString('ko-KR') : '-'; }
async function loadCloud() {
  const c = await api.get('/api/cloud').catch(() => null);
  if (!c) return;
  const linked = !!c.linked;
  const box = $('#cloud-status'); box.className = 'remote-status ' + (linked ? 'connected' : '');
  $('#cloud-state-text').textContent = linked ? `연결됨 · ${c.email}${c.orgName ? ' (' + c.orgName + ')' : ''}` : '연결 안 됨';
  $('#cloud-linkform').style.display = linked ? 'none' : 'block';
  $('#cloud-linked').style.display = linked ? 'block' : 'none';
  if (linked) {
    $('#cl-auto').checked = c.autoBackup !== false;
    const status = c.lastBackupStatus === 'ok' ? '정상' : (c.lastBackupStatus || '-');
    $('#cloud-info').innerHTML = `기기: <b>${esc(c.deviceName || '-')}</b><br>마지막 백업: ${fmtWhen(c.lastBackupAt)} (${esc(status)})` +
      (c.lastRestoreAt ? `<br>마지막 복구 적용: ${fmtWhen(c.lastRestoreAt)}` : '');
  }
}
$('#cl-link').addEventListener('click', async () => {
  const email = $('#cl-email').value.trim(); const pw = $('#cl-pw').value; const device = $('#cl-device').value.trim();
  if (!email || !pw) return alert('이메일과 비밀번호를 입력하세요.');
  $('#cl-link').disabled = true; $('#cl-link').textContent = '연결 중…';
  const r = await api.post('/api/cloud/link', { email, password: pw, deviceName: device });
  $('#cl-link').disabled = false; $('#cl-link').textContent = '계정 연결';
  if (r.error) return alert('연결 실패: ' + r.error);
  $('#cl-pw').value = '';
  await loadCloud();
});
$('#cl-unlink').addEventListener('click', async () => {
  if (!confirm('클라우드 계정 연결을 해제할까요? (자동 백업이 중단됩니다)')) return;
  await api.post('/api/cloud/unlink', {}); await loadCloud();
});
$('#cl-auto').addEventListener('change', async (e) => { await api.put('/api/cloud', { autoBackup: e.target.checked }); });
$('#cl-backup-now').addEventListener('click', async () => {
  $('#cl-backup-now').disabled = true; $('#cl-backup-now').textContent = '백업 중…';
  const r = await api.post('/api/cloud/backup-now', {});
  $('#cl-backup-now').disabled = false; $('#cl-backup-now').textContent = '지금 서버에 백업';
  if (r.error) alert('백업 실패: ' + r.error); else { alert('서버에 백업되었습니다.'); await loadCloud(); }
});
$('#cl-view-backups').addEventListener('click', async () => {
  const box = $('#cl-backup-list');
  if (box.style.display === 'block') { box.style.display = 'none'; return; }
  const r = await api.get('/api/cloud/backups').catch(() => ({}));
  const list = (r && r.backups) || [];
  box.innerHTML = list.length
    ? list.map((b) => `<div class="snap-row"><span>${fmtWhen(b.createdAt)} · ${b.kind === 'auto' ? '자동' : '수동'}${b.label ? ' · ' + esc(b.label) : ''}</span><span class="snap-size">${(b.size / 1024).toFixed(1)} KB</span></div>`).join('')
    : '<div class="hint" style="padding:8px">서버에 저장된 백업이 없습니다.</div>';
  box.style.display = 'block';
});

// ==================== 되돌리기 · 로컬 저장 ====================
async function refreshUndo() {
  const r = await api.get('/api/undo').catch(() => ({ count: 0 }));
  const n = (r && r.count) || 0;
  const badge = $('#undo-count'); const btn = $('#undo-btn');
  if (!badge || !btn) return;
  if (n > 0) { badge.textContent = n; badge.style.display = 'inline-block'; btn.disabled = false; }
  else { badge.style.display = 'none'; btn.disabled = true; }
}
$('#undo-btn').addEventListener('click', async () => {
  const r = await api.post('/api/undo', {});
  if (r.error) return alert(r.error);
  await loadAll(); await refreshUndo();
});
async function loadSnaps() {
  const list = await api.get('/api/snapshots').catch(() => []);
  const box = $('#snap-list'); if (!box) return;
  box.innerHTML = (list || []).length
    ? list.map((s) => `<div class="snap-row"><span title="${esc(s.name)}">🗂 ${esc(s.name)} <small>· ${fmtWhen(s.at)}</small></span>
        <span class="snap-actions"><button class="btn btn-xs" data-snap-restore="${s.id}">불러오기</button><button class="btn btn-xs btn-outline" data-snap-del="${s.id}">삭제</button></span></div>`).join('')
    : '<div class="hint" style="padding:8px">저장된 편성이 없습니다.</div>';
}
$('#snap-save').addEventListener('click', async () => {
  const name = $('#snap-name').value.trim();
  await api.post('/api/snapshots', { name });
  $('#snap-name').value = '';
  await loadSnaps();
});
$('#snap-list').addEventListener('click', async (e) => {
  const rs = e.target.closest('[data-snap-restore]');
  if (rs) { if (!confirm('이 저장을 현재 편성에 불러올까요? (지금 편성은 되돌리기로 복구 가능)')) return; const r = await api.post(`/api/snapshots/${rs.dataset.snapRestore}/restore`, {}); if (r.error) return alert(r.error); await loadAll(); await refreshUndo(); return; }
  const dl = e.target.closest('[data-snap-del]');
  if (dl) { if (!confirm('이 저장을 삭제할까요?')) return; await api.del(`/api/snapshots/${dl.dataset.snapDel}`); await loadSnaps(); }
});

// ==================== 시계 ====================
setInterval(() => {
  $('#clock').textContent = new Date().toLocaleTimeString('ko-KR', { hour12: false });
}, 1000);

// ==================== 유틸 ====================
function esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }

// ==================== 디자인 템플릿 (PRO) ====================
(function initTemplates() {
  const modal = $('#tpl-modal');
  if (!modal || !window.TEMPLATES) return;
  let tplIdx = 0, vals = {};
  const cur = () => window.TEMPLATES[tplIdx];
  function resolveRes() {
    const sel = $('#tpl-res').value;
    if (sel === 'cur') { const r = (state.settings && state.settings.resolution) || {}; return { W: r.width || 1920, H: r.height || 1080 }; }
    const [W, H] = sel.split('x').map(Number); return { W: W || 1920, H: H || 1080 };
  }
  function renderGrid() {
    const grid = $('#tpl-grid'); grid.innerHTML = '';
    window.TEMPLATES.forEach((t, i) => {
      const wrap = document.createElement('div');
      wrap.style.cssText = `cursor:pointer;border:2px solid ${i === tplIdx ? '#7c3aed' : 'var(--line)'};border-radius:8px;overflow:hidden;background:#000`;
      const cv = document.createElement('canvas'); cv.style.cssText = 'width:100%;display:block';
      window.renderTemplate(cv, t, null, 320, 180);
      const lb = document.createElement('div'); lb.textContent = t.name;
      lb.style.cssText = 'font-size:11px;font-weight:700;padding:4px 6px;background:#fff;color:#111;text-align:center';
      wrap.appendChild(cv); wrap.appendChild(lb);
      wrap.onclick = () => { tplIdx = i; vals = {}; renderGrid(); renderFields(); renderPreview(); };
      grid.appendChild(wrap);
    });
  }
  function renderFields() {
    const box = $('#tpl-fields'); box.innerHTML = '';
    cur().fields.forEach((f) => {
      const lab = document.createElement('label'); lab.className = 'field'; lab.style.marginBottom = '8px';
      lab.appendChild(document.createTextNode(f.label + ' '));
      const multi = (f.def || '').includes('\n') || (f.def || '').length > 16;
      const inp = document.createElement(multi ? 'textarea' : 'input');
      if (multi) inp.rows = 2;
      inp.value = vals[f.key] != null ? vals[f.key] : f.def;
      inp.placeholder = f.def;
      inp.oninput = () => { vals[f.key] = inp.value; renderPreview(); };
      lab.appendChild(inp); box.appendChild(lab);
    });
  }
  function renderPreview() {
    const { W, H } = resolveRes();
    const scale = Math.min(760 / W, 500 / H, 1);
    window.renderTemplate($('#tpl-preview'), cur(), vals, Math.max(1, Math.round(W * scale)), Math.max(1, Math.round(H * scale)));
  }
  $('#tpl-res').addEventListener('change', renderPreview);
  $('#tpl-cancel').addEventListener('click', () => modal.style.display = 'none');
  modal.addEventListener('click', (e) => { if (e.target === modal) modal.style.display = 'none'; });

  $('#open-tpl').addEventListener('click', async () => {
    const note = $('#tpl-pro-note'), gen = $('#tpl-generate');
    let pro = false;
    try {
      const s = await api.get('/api/cloud/pro-status');
      pro = !!s.pro;
      if (!s.linked) note.innerHTML = '디자인 템플릿은 <b>프로 요금제 전용</b> 기능입니다. 먼저 <b>설정 → 클라우드 계정 연결</b> 후 이용하세요.';
      else if (s.expired) note.innerHTML = '구독 <b>사용기간이 만료</b>되었습니다. 관리자에게 문의해 프로를 연장하세요.';
      else if (!pro) note.innerHTML = '현재 <b>베이직</b> 요금제입니다. 디자인 템플릿은 <b>프로 전용</b>이며, 관리자에게 프로 전환을 문의하세요.';
    } catch { note.innerHTML = '클라우드 상태를 확인할 수 없습니다. 네트워크를 확인하세요.'; }
    note.style.display = pro ? 'none' : 'block';
    gen.disabled = !pro; gen.style.opacity = pro ? '1' : '.5'; gen.style.cursor = pro ? 'pointer' : 'not-allowed';
    tplIdx = 0; vals = {};
    renderGrid(); renderFields(); renderPreview();
    modal.style.display = 'flex';
  });

  $('#tpl-generate').addEventListener('click', () => {
    if ($('#tpl-generate').disabled) return;
    const { W, H } = resolveRes();
    const cv = document.createElement('canvas');
    window.renderTemplate(cv, cur(), vals, W, H);
    cv.toBlob((blob) => {
      if (!blob) return alert('이미지 생성에 실패했습니다.');
      const fd = new FormData();
      fd.append('files', blob, `template-${cur().id}-${Date.now()}.png`);
      const xhr = new XMLHttpRequest();
      xhr.open('POST', '/api/media/upload');
      const h = authHeaders(); Object.keys(h).forEach((k) => xhr.setRequestHeader(k, h[k]));
      $('#tpl-generate').textContent = '생성 중…'; $('#tpl-generate').disabled = true;
      xhr.onload = async () => { modal.style.display = 'none'; $('#tpl-generate').textContent = '이미지 생성 → 미디어에 추가'; $('#tpl-generate').disabled = false; await loadAll(); alert(`템플릿 이미지(${W}×${H})가 미디어에 추가되었습니다.\n🖼 포스터·현수막 탭에서 편성하세요.`); };
      xhr.onerror = () => { alert('업로드에 실패했습니다.'); $('#tpl-generate').textContent = '이미지 생성 → 미디어에 추가'; $('#tpl-generate').disabled = false; };
      xhr.send(fd);
    }, 'image/png');
  });
})();

// ==================== 클라우드 AI 이미지 가져오기 ====================
(function initAiImport() {
  const modal = $('#ai-import-modal'); if (!modal) return;
  $('#ai-import-close').addEventListener('click', () => modal.style.display = 'none');
  modal.addEventListener('click', (e) => { if (e.target === modal) modal.style.display = 'none'; });
  $('#open-ai-import').addEventListener('click', async () => {
    const grid = $('#ai-import-grid'), note = $('#ai-import-note');
    note.style.display = 'none'; grid.innerHTML = '<div style="color:#888">불러오는 중…</div>';
    modal.style.display = 'flex';
    const r = await apiFetch('/api/cloud/ai-images', { headers: authHeaders() }).then((x) => x.json()).catch(() => ({ error: '연결 실패' }));
    if (r.error) { grid.innerHTML = ''; note.textContent = r.error + ' — 설정에서 클라우드 계정(프로)을 연결했는지 확인하세요.'; note.style.display = 'block'; return; }
    const recs = r.records || [];
    if (!recs.length) { grid.innerHTML = '<div style="color:#888;grid-column:1/-1;text-align:center;padding:30px">웹에서 만든 AI 이미지가 없습니다. cms.cdmb.net에서 먼저 생성하세요.</div>'; return; }
    grid.innerHTML = recs.map((x) => `
      <div class="ai-imp-item" data-url="${encodeURIComponent(x.url)}" data-name="${(x.userText || 'AI 이미지').replace(/"/g, '')}" style="cursor:pointer;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;background:#fff">
        <img src="${x.url}" style="width:100%;aspect-ratio:1;object-fit:cover;display:block" loading="lazy" />
        <div style="padding:6px 8px;font-size:11.5px;color:#555">${x.userText ? x.userText.slice(0, 30) : 'AI 이미지'}<br><span style="color:#999">${x.provider}${x.withText ? '·글자' : ''} · 클릭해서 가져오기</span></div>
      </div>`).join('');
  });
  $('#ai-import-grid').addEventListener('click', async (e) => {
    const el = e.target.closest('[data-url]'); if (!el) return;
    el.style.opacity = '.5';
    const url = decodeURIComponent(el.dataset.url);
    const r = await apiFetch('/api/cloud/ai-import', { method: 'POST', headers: authHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ url, name: el.dataset.name }) }).then((x) => x.json()).catch(() => ({ error: '실패' }));
    if (r.error) { alert(r.error); el.style.opacity = '1'; return; }
    el.style.outline = '3px solid #22c55e'; el.querySelector('span').textContent = '✓ 미디어에 추가됨';
    await loadAll();
  });
})();

// 시작 — 무조건 로그인 게이트 통과 후에만 앱 로드
layoutPreview();
(async () => { if (await checkGate()) startApp(); })();
