// Auto-Sync: schedule board + mirrored-playlist pipeline runs
// ─────────────────────────────────────────────────────────────────────
// Extracted from stats-automations.js (Cin review feedback). All
// references rely on globals available at runtime — `_esc`, `_escAttr`,
// `_autoParseUTC`, `_autoFormatTrigger`, `showToast`, `showConfirmDialog`,
// `loadMirroredPlaylists`, `updateMirroredCardPhase`,
// `openMirroredPlaylistModal`, `closeMirroredModal`, `youtubePlaylistStates`
// all live in stats-automations.js (or earlier helpers). This file
// declares the auto-sync-specific state + render/event functions on top.

const mirroredPipelinePollers = {};
const AUTO_SYNC_BUCKETS = [1, 2, 4, 8, 12, 16, 24, 48, 72, 168];
let _autoSyncStatusPoller = null;
let _autoSyncIsDragging = false;
let _autoSyncScheduleState = {
    playlists: [],
    automations: [],
    playlistSchedules: {},
    weeklySchedules: {},
    automationPipelines: [],
    runHistory: [],
    runHistoryTotal: 0,
};
let _autoSyncActiveTab = 'schedule';
let _autoSyncSidebarFilter = '';
let _autoSyncHistoryFilter = 'all';  // 'all' | 'error' | 'completed' | 'skipped'
let _autoSyncHistoryLimit = 50;
// Open weekly-editor popover state. ``null`` when no popover is open.
// Tracks playlist id + the current draft (time / days / tz) so the
// editor is a controlled component — clicking outside without saving
// discards the draft.
let _autoSyncWeeklyEditor = null;

function getMirroredSourceRef(p) {
    if (p && p.source_ref) return String(p.source_ref);
    const desc = (p && p.description) ? String(p.description).trim() : '';
    if ((p.source === 'spotify_public' || p.source === 'youtube') && /^https?:\/\//i.test(desc)) {
        return desc;
    }
    return (p && p.source_playlist_id) ? String(p.source_playlist_id) : '';
}

function autoSyncTriggerForHours(hours) {
    const h = parseInt(hours, 10) || 24;
    if (h >= 24 && h % 24 === 0) {
        return { interval: h / 24, unit: 'days' };
    }
    return { interval: h, unit: 'hours' };
}

function autoSyncHoursFromTrigger(config) {
    const interval = parseInt(config?.interval, 10) || 0;
    const unit = config?.unit || 'hours';
    if (!interval) return null;
    if (unit === 'minutes') return Math.max(1, Math.round(interval / 60));
    if (unit === 'days') return interval * 24;
    if (unit === 'weeks') return interval * 168;
    return interval;
}

function autoSyncBucketLabel(hours) {
    if (hours === 168) return 'Weekly';
    if (hours >= 24) return `${hours / 24}d`;
    return `${hours}h`;
}

function autoSyncIntervalLabel(hours) {
    if (hours === 168) return 'Every week';
    if (hours >= 24) {
        const days = hours / 24;
        return `Every ${days} day${days === 1 ? '' : 's'}`;
    }
    return `Every ${hours} hour${hours === 1 ? '' : 's'}`;
}

// Browser-detected default tz for new schedules. Used when the user
// creates a weekly schedule and hasn't picked an explicit tz — falls
// back to UTC on browsers where Intl is unavailable (very old ones).
function detectBrowserTimezone() {
    try {
        const tz = typeof Intl !== 'undefined'
            && Intl.DateTimeFormat
            && Intl.DateTimeFormat().resolvedOptions().timeZone;
        return tz || 'UTC';
    } catch (_) {
        return 'UTC';
    }
}

// Canonical weekday order Mon-Sun. Matches both the backend
// ``next_run_at`` weekday_map and the column ordering in the UI.
// Keeping the abbreviations short-lowercase ('mon' not 'MON' / 'Mon')
// matches the engine's existing config payload convention.
const AUTO_SYNC_WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
const AUTO_SYNC_WEEKDAY_LABELS = {
    mon: 'Mon', tue: 'Tue', wed: 'Wed', thu: 'Thu',
    fri: 'Fri', sat: 'Sat', sun: 'Sun',
};

// Build a ``weekly_time`` trigger_config payload from picker input.
// Defensive — caller may pass garbage; we clamp / drop / default so
// the resulting payload always passes ``next_run_at`` validation.
function autoSyncWeeklyTrigger({ time, days, tz } = {}) {
    const safeTime = typeof time === 'string' && /^\d{1,2}:\d{2}$/.test(time)
        ? time : '09:00';
    const safeDays = Array.isArray(days)
        ? days.filter(d => AUTO_SYNC_WEEKDAYS.includes(d))
        : [];
    const safeTz = (typeof tz === 'string' && tz) ? tz : detectBrowserTimezone();
    return { time: safeTime, days: safeDays, tz: safeTz };
}

// Parse the days/time/tz back out of a ``weekly_time`` trigger_config,
// with defensive fallbacks so a hand-edited row doesn't crash render.
// Returns null when the config isn't recognisable as a weekly trigger.
function autoSyncWeeklyFromTrigger(config) {
    if (!config || typeof config !== 'object') return null;
    const rawTime = typeof config.time === 'string' && /^\d{1,2}:\d{2}$/.test(config.time)
        ? config.time : '09:00';
    let days = Array.isArray(config.days)
        ? config.days.map(d => String(d).toLowerCase()).filter(d => AUTO_SYNC_WEEKDAYS.includes(d))
        : [];
    // Empty / all-invalid days = "every day" per next_run_at convention.
    // Surface that so the UI can render the schedule under all 7 day
    // columns instead of treating it as unscheduled.
    if (days.length === 0) days = [...AUTO_SYNC_WEEKDAYS];
    const tz = (typeof config.tz === 'string' && config.tz) ? config.tz : 'UTC';
    return { time: rawTime, days, tz };
}

// Human-readable label for a weekly schedule. Used on card metadata
// and column tooltips. Multi-day schedules collapse to "Mon, Wed, Fri
// @09:00"; full-week schedules collapse to "Daily @ 09:00".
function autoSyncWeeklyLabel(parsed) {
    if (!parsed) return 'Unscheduled';
    const { time, days } = parsed;
    if (!Array.isArray(days) || days.length === 0) return `Daily @ ${time}`;
    if (days.length === 7) return `Daily @ ${time}`;
    // Sort to canonical Mon-Sun order so card text doesn't shuffle
    // when the user toggles days on/off in arbitrary order.
    const ordered = AUTO_SYNC_WEEKDAYS.filter(d => days.includes(d));
    const dayList = ordered.map(d => AUTO_SYNC_WEEKDAY_LABELS[d]).join(', ');
    return `${dayList} @ ${time}`;
}

function autoSyncSourceLabel(source) {
    const labels = {
        spotify: 'Spotify',
        spotify_public: 'Spotify Link',
        tidal: 'Tidal',
        youtube: 'YouTube',
        deezer: 'Deezer',
        qobuz: 'Qobuz',
        beatport: 'Beatport',
        file: 'File Imports',
        itunes_link: 'iTunes Link',
        listenbrainz: 'ListenBrainz',
        lastfm: 'Last.fm Radio',
        soulsync_discovery: 'SoulSync Discovery',
    };
    return labels[source] || source || 'Other';
}

// Per-source logo URLs for the sidebar source-group headers and
// anywhere else a small branded chip helps disambiguate the source.
// Same URLs the dashboard equalizer / header-action orbs reference
// so the visual language stays consistent. Sources without a
// readily available logo (``beatport``, ``file``) fall through to
// no-image; the source-icon element drops to display:none via
// the ``<img onerror>`` swap so the header renders cleanly without
// a broken-image placeholder.
const _AUTO_SYNC_SOURCE_LOGOS = {
    spotify:            'https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png',
    spotify_public:     'https://storage.googleapis.com/pr-newsroom-wp/1/2023/05/Spotify_Primary_Logo_RGB_Green.png',
    tidal:              'https://www.svgrepo.com/show/519734/tidal.svg',
    youtube:            'https://www.svgrepo.com/show/13671/youtube.svg',
    deezer:             'https://cdn.brandfetch.io/idEUKgCNtu/theme/dark/symbol.svg?c=1bxid64Mup7aczewSAYMX&t=1758260798610',
    qobuz:              'https://www.svgrepo.com/show/504778/qobuz.svg',
    itunes_link:        'https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/ITunes_logo.svg/960px-ITunes_logo.svg.png',
    lastfm:             'https://www.last.fm/static/images/lastfm_avatar_twitter.52a5d69a85ac.png',
    listenbrainz:       'https://listenbrainz.org/static/img/listenbrainz-logo-no-text.svg',
    soulsync_discovery: '/static/favicon.png',
};

function autoSyncSourceIconHtml(source) {
    const src = _AUTO_SYNC_SOURCE_LOGOS[source];
    if (!src) return '';
    return `<img class="auto-sync-source-icon" data-svc="${_escAttr(source)}"
                 src="${src}" alt="" aria-hidden="true"
                 onerror="this.style.display='none'">`;
}

function autoSyncCanSchedulePlaylist(playlist) {
    if (!playlist) return false;
    const src = playlist.source || '';
    // ``file`` + ``beatport`` have no external refresh hook.
    // ``lastfm`` is excluded because each Last.fm Radio playlist is a
    // seed-track-specific similar-tracks snapshot that doesn't update
    // on the Last.fm side — auto-syncing it would just re-discover the
    // same 25 tracks every interval. Users mirror Last.fm radios once
    // to grab the downloads, then move on; they belong in the
    // Mirrored / Sync tab but not the Auto-Sync schedule board.
    return !['file', 'beatport', 'lastfm'].includes(src);
}

function autoSyncIsPipelineAutomation(auto) {
    return auto && auto.action_type === 'playlist_pipeline';
}

function autoSyncPlaylistIdFromAutomation(auto) {
    if (!autoSyncIsPipelineAutomation(auto)) return null;
    const cfg = auto.action_config || {};
    if (cfg.all === true || cfg.all === 'true') return null;
    const raw = cfg.playlist_id;
    if (raw === undefined || raw === null || raw === '') return null;
    const id = parseInt(raw, 10);
    return Number.isFinite(id) ? id : null;
}

function autoSyncIsScheduleOwned(auto) {
    // Primary signal: the explicit owned_by flag the board writes on every
    // schedule it creates. Falls back to the legacy name/group convention
    // so rows created before the column existed (or hand-edited from the
    // Automations page) still get recognized after backfill.
    if (auto?.owned_by === 'auto_sync') return true;
    const group = auto?.group_name || '';
    const name = auto?.name || '';
    return group === 'Playlist Auto-Sync' || name.startsWith('Auto-Sync:');
}

function buildAutoSyncScheduleState(playlists, automations, historyData = {}) {
    const playlistSchedules = {};
    const weeklySchedules = {};
    const automationPipelines = [];
    const pipelineAutomations = automations.filter(autoSyncIsPipelineAutomation);
    pipelineAutomations.forEach(auto => {
        const playlistId = autoSyncPlaylistIdFromAutomation(auto);
        const isOwned = autoSyncIsScheduleOwned(auto);

        if (playlistId && isOwned && auto.trigger_type === 'schedule') {
            const hours = autoSyncHoursFromTrigger(auto.trigger_config || {});
            if (hours) {
                playlistSchedules[playlistId] = {
                    automation_id: auto.id,
                    automation_name: auto.name,
                    hours,
                    enabled: auto.enabled !== false && auto.enabled !== 0,
                    owned: true,
                    next_run: auto.next_run,
                    trigger_config: auto.trigger_config || {},
                };
                return;
            }
        }
        if (playlistId && isOwned && auto.trigger_type === 'weekly_time') {
            // No ``|| {}`` coercion here on purpose — null / non-object
            // trigger_config from a hand-edited row should fall through
            // to automationPipelines as a "broken row" rather than be
            // silently bucketed as an every-day schedule. The helper
            // returns null for those cases; truthy config flows through
            // the helper's defensive defaults.
            const parsed = autoSyncWeeklyFromTrigger(auto.trigger_config);
            if (parsed) {
                weeklySchedules[playlistId] = {
                    automation_id: auto.id,
                    automation_name: auto.name,
                    time: parsed.time,
                    days: parsed.days,
                    tz: parsed.tz,
                    enabled: auto.enabled !== false && auto.enabled !== 0,
                    owned: true,
                    next_run: auto.next_run,
                    trigger_config: auto.trigger_config || {},
                };
                return;
            }
        }
        automationPipelines.push(auto);
    });
    return {
        playlists,
        automations,
        playlistSchedules,
        weeklySchedules,
        automationPipelines,
        runHistory: historyData.history || [],
        runHistoryTotal: historyData.total || 0,
    };
}

async function openAutoSyncScheduleModal() {
    let overlay = document.getElementById('auto-sync-schedule-modal');
    if (!overlay) {
        overlay = document.createElement('div');
        overlay.id = 'auto-sync-schedule-modal';
        overlay.className = 'auto-sync-overlay';
        document.body.appendChild(overlay);
    }
    overlay.innerHTML = `
        <div class="auto-sync-modal">
            <div class="auto-sync-header">
                <div>
                    <h3>Auto-Sync Schedule</h3>
                    <p>Drop mirrored playlists onto an interval to schedule refresh, discovery, sync, and wishlist processing.</p>
                </div>
                <button class="auto-sync-close" onclick="closeAutoSyncScheduleModal()">&times;</button>
            </div>
            <div class="auto-sync-loading">Loading schedule...</div>
        </div>
    `;
    overlay.style.display = 'flex';
    overlay.onclick = e => { if (e.target === overlay) closeAutoSyncScheduleModal(); };
    await refreshAutoSyncScheduleModal();
}

function closeAutoSyncScheduleModal() {
    const overlay = document.getElementById('auto-sync-schedule-modal');
    stopAutoSyncStatusPolling();
    if (overlay) overlay.remove();
}

async function refreshAutoSyncScheduleModal() {
    const overlay = document.getElementById('auto-sync-schedule-modal');
    if (!overlay) return;
    try {
        const [playlistRes, automationRes, historyRes] = await Promise.all([
            fetch('/api/mirrored-playlists'),
            fetch('/api/automations'),
            fetch(`/api/playlist-pipeline/history?limit=${_autoSyncHistoryLimit}`),
        ]);
        const playlists = await playlistRes.json();
        const automations = await automationRes.json();
        const historyData = await historyRes.json();
        if (!playlistRes.ok || playlists.error) throw new Error(playlists.error || 'Failed to load mirrored playlists');
        if (!automationRes.ok || automations.error) throw new Error(automations.error || 'Failed to load automations');
        if (!historyRes.ok || historyData.error) throw new Error(historyData.error || 'Failed to load pipeline run history');
        _autoSyncScheduleState = buildAutoSyncScheduleState(playlists, automations, historyData);
        renderAutoSyncScheduleModal();
        manageAutoSyncStatusPolling();
    } catch (err) {
        overlay.innerHTML = `
            <div class="auto-sync-modal">
                <div class="auto-sync-header">
                    <div><h3>Auto-Sync Schedule</h3><p>Could not load schedule data.</p></div>
                    <button class="auto-sync-close" onclick="closeAutoSyncScheduleModal()">&times;</button>
                </div>
                <div class="auto-sync-error">${_esc((err && err.message) || String(err))}</div>
            </div>
        `;
    }
}

function renderAutoSyncScheduleModal() {
    const overlay = document.getElementById('auto-sync-schedule-modal');
    if (!overlay) return;

    const { playlists, playlistSchedules, weeklySchedules, automationPipelines, runHistory, runHistoryTotal } = _autoSyncScheduleState;
    const scheduledCount = Object.keys(playlistSchedules).length + Object.keys(weeklySchedules || {}).length;
    const enabledCount = Object.values(playlistSchedules).filter(s => s.enabled).length
        + Object.values(weeklySchedules || {}).filter(s => s.enabled).length;
    const pipelineCount = automationPipelines.length;
    const totalTracks = playlists.reduce((sum, p) => sum + (parseInt(p.track_count, 10) || 0), 0);
    const scheduleActive = _autoSyncActiveTab === 'schedule';
    const weeklyActive = _autoSyncActiveTab === 'weekly';
    const automationsActive = _autoSyncActiveTab === 'automations';
    const historyActive = _autoSyncActiveTab === 'history';

    const schedulePanel = renderAutoSyncSchedulePanel(playlists, playlistSchedules);
    const weeklyPanel = renderAutoSyncWeeklyPanel(playlists, playlistSchedules);
    const automationPanel = renderAutoSyncAutomationPanel(automationPipelines, playlists);
    const historyPanel = renderAutoSyncHistoryPanel(runHistory, runHistoryTotal);
    const monitor = renderAutoSyncPipelineMonitor(playlists);

    overlay.innerHTML = `
        <div class="auto-sync-modal">
            <div class="auto-sync-header">
                <div>
                    <div class="auto-sync-eyebrow">Playlist automation</div>
                    <h3>Auto-Sync Manager</h3>
                    <p>Schedule mirrored playlists through the same playlist-pipeline engine used by Automations.</p>
                </div>
                <button class="auto-sync-close" onclick="closeAutoSyncScheduleModal()">&times;</button>
            </div>
            <div class="auto-sync-summary">
                <div><span>${scheduledCount}</span><small>scheduled playlists</small></div>
                <div><span>${enabledCount}</span><small>active schedules</small></div>
                <div><span>${pipelineCount}</span><small>automation pipelines</small></div>
                <div><span>${totalTracks}</span><small>mirrored tracks</small></div>
            </div>
            ${monitor}
            <div class="auto-sync-tabs">
                <button class="${scheduleActive ? 'active' : ''}" onclick="setAutoSyncTab('schedule')">Hourly Board</button>
                <button class="${weeklyActive ? 'active' : ''}" onclick="setAutoSyncTab('weekly')">Weekly Board</button>
                <button class="${automationsActive ? 'active' : ''}" onclick="setAutoSyncTab('automations')">Automation Pipelines</button>
                <button class="${historyActive ? 'active' : ''}" onclick="setAutoSyncTab('history')">
                    Run History
                    ${(() => {
                        const errs = (runHistory || []).filter(h => h.status === 'error' || h.status === 'skipped').length;
                        return errs ? `<span class="auto-sync-tab-badge error">${errs}</span>` : '';
                    })()}
                </button>
            </div>
            <div class="auto-sync-tab-panel ${scheduleActive ? 'active' : ''}" id="auto-sync-schedule-panel">${schedulePanel}</div>
            <div class="auto-sync-tab-panel ${weeklyActive ? 'active' : ''}" id="auto-sync-weekly-panel">${weeklyPanel}</div>
            <div class="auto-sync-tab-panel ${automationsActive ? 'active' : ''}" id="auto-sync-automation-panel">${automationPanel}</div>
            <div class="auto-sync-tab-panel ${historyActive ? 'active' : ''}" id="auto-sync-history-panel">${historyPanel}</div>
        </div>
    `;
    populateAutoSyncHistoryList(overlay);
    bindAutoSyncHistoryCardInteractions(overlay);
}

function setAutoSyncTab(tab) {
    const allowed = ['schedule', 'weekly', 'automations', 'history'];
    _autoSyncActiveTab = allowed.includes(tab) ? tab : 'schedule';
    // Switching tabs closes any open weekly editor so the popover
    // doesn't ghost-render over the wrong panel.
    if (_autoSyncActiveTab !== 'weekly') _autoSyncWeeklyEditor = null;
    renderAutoSyncScheduleModal();
}

function renderAutoSyncSchedulePanel(playlists, playlistSchedules) {
    const filter = (_autoSyncSidebarFilter || '').trim().toLowerCase();
    const matchesFilter = (p) => !filter || (p.name || '').toLowerCase().includes(filter)
        || autoSyncSourceLabel(p.source || '').toLowerCase().includes(filter);
    const schedulablePlaylists = playlists.filter(p => autoSyncCanSchedulePlaylist(p) && matchesFilter(p));
    const unavailablePlaylists = playlists.filter(p => !autoSyncCanSchedulePlaylist(p) && matchesFilter(p));
    const grouped = schedulablePlaylists.reduce((acc, p) => {
        const key = p.source || 'other';
        if (!acc[key]) acc[key] = [];
        acc[key].push(p);
        return acc;
    }, {});
    const sourceKeys = Object.keys(grouped).sort((a, b) => autoSyncSourceLabel(a).localeCompare(autoSyncSourceLabel(b)));

    const sidebarHtml = sourceKeys.length ? sourceKeys.map(source => `
        <div class="auto-sync-source-group">
            <div class="auto-sync-source-group-head">
                <span class="auto-sync-source-title">
                    ${autoSyncSourceIconHtml(source)}
                    <span class="auto-sync-source-title-label">${_esc(autoSyncSourceLabel(source))}</span>
                </span>
                <button type="button" class="auto-sync-source-bulk-btn"
                        onclick="event.stopPropagation(); openAutoSyncBulkMenu(event, '${_escAttr(source)}')"
                        title="Schedule all ${_escAttr(autoSyncSourceLabel(source))} playlists at the same interval">
                    Bulk
                </button>
            </div>
            ${grouped[source].map(p => {
                const schedule = playlistSchedules[p.id];
                const assigned = schedule ? autoSyncIntervalLabel(schedule.hours) : 'Unscheduled';
                return `
                    <div class="auto-sync-playlist ${schedule ? 'scheduled' : ''}" draggable="true" data-playlist-id="${p.id}" ondragstart="autoSyncDragStart(event)" ondragend="autoSyncDragEnd()">
                        <div class="auto-sync-playlist-name">${_esc(p.name)}</div>
                        <div class="auto-sync-playlist-meta">${p.track_count || 0} tracks &middot; ${_esc(assigned)}</div>
                    </div>
                `;
            }).join('')}
        </div>
    `).join('') : '<div class="auto-sync-empty">No refreshable mirrored playlists yet.</div>';

    const unavailableHtml = unavailablePlaylists.length ? `
        <div class="auto-sync-source-group auto-sync-source-group-disabled">
            <div class="auto-sync-source-title">Not schedulable</div>
            ${unavailablePlaylists.map(p => `
                <div class="auto-sync-playlist unavailable">
                    <div class="auto-sync-playlist-name">${_esc(p.name)}</div>
                    <div class="auto-sync-playlist-meta">${_esc(autoSyncSourceLabel(p.source))} &middot; refresh not supported</div>
                </div>
            `).join('')}
        </div>
    ` : '';

    // Merge standard buckets with any custom intervals that are already in
    // use, so a 6h or 36h schedule (created via Automations page or the
    // custom-interval prompt) still renders as its own column instead of
    // disappearing from the board.
    const customHours = Object.values(playlistSchedules)
        .map(s => parseInt(s?.hours, 10))
        .filter(h => Number.isFinite(h) && h > 0 && !AUTO_SYNC_BUCKETS.includes(h));
    const allBuckets = [...new Set([...AUTO_SYNC_BUCKETS, ...customHours])].sort((a, b) => a - b);
    const bucketHtml = allBuckets.map(hours => {
        const assigned = schedulablePlaylists.filter(p => playlistSchedules[p.id]?.hours === hours);
        const isCustom = !AUTO_SYNC_BUCKETS.includes(hours);
        return `
            <div class="auto-sync-column ${isCustom ? 'custom' : ''}" data-hours="${hours}" ondragover="autoSyncDragOver(event)" ondragleave="autoSyncDragLeave(event)" ondrop="autoSyncDrop(event, ${hours})">
                <div class="auto-sync-column-head">
                    <span>${autoSyncBucketLabel(hours)}${isCustom ? ' <em>custom</em>' : ''}</span>
                    <small>${assigned.length} playlist${assigned.length === 1 ? '' : 's'}</small>
                </div>
                <div class="auto-sync-column-list">
                    ${assigned.length ? assigned.map(p => autoSyncScheduledCardHtml(p, playlistSchedules[p.id])).join('') : '<div class="auto-sync-drop-hint"><strong>Drop here</strong><span>Schedule playlists at this interval</span></div>'}
                </div>
            </div>
        `;
    }).join('');

    const filterValue = _esc(_autoSyncSidebarFilter || '');
    return `
        <div class="auto-sync-board-intro">
            <div>
                <strong>Drag playlists into an interval</strong>
                <span>Each placement creates or updates an Auto-Sync-owned playlist-pipeline automation.</span>
            </div>
            <button onclick="refreshAutoSyncScheduleModal()">Refresh</button>
        </div>
            <div class="auto-sync-body">
                <aside class="auto-sync-sidebar">
                    <div class="auto-sync-sidebar-title">Mirrored playlists</div>
                    <div class="auto-sync-sidebar-filter">
                        <input type="search" class="auto-sync-sidebar-search" placeholder="Filter playlists…"
                               value="${filterValue}" oninput="setAutoSyncSidebarFilter(this.value)" />
                        ${_autoSyncSidebarFilter ? `<button type="button" class="auto-sync-sidebar-filter-clear" onclick="setAutoSyncSidebarFilter('')" aria-label="Clear filter">&times;</button>` : ''}
                    </div>
                    <div class="auto-sync-source-list">${sidebarHtml}${unavailableHtml}</div>
                </aside>
                <main class="auto-sync-board">${bucketHtml}</main>
            </div>
    `;
}

// ──────────────────────────────────────────────────────────────────────
// Weekly schedule board — PR 3 of the schedule-types feature.
// Renders 7 day columns Mon-Sun. Each column lists the playlists
// scheduled to run on that weekday (multi-day schedules render
// under EACH matching column, matching how the user thinks about
// "this playlist runs on Mon AND Wed AND Fri"). Drag a playlist onto
// a column → create a single-day weekly schedule at the default time.
// Click a scheduled card → opens an editor popover for time + days
// + tz adjustments.
// ──────────────────────────────────────────────────────────────────────


function renderAutoSyncWeeklyPanel(playlists, playlistSchedules) {
    const weeklySchedules = _autoSyncScheduleState.weeklySchedules || {};
    const filter = (_autoSyncSidebarFilter || '').trim().toLowerCase();
    const matchesFilter = (p) => !filter || (p.name || '').toLowerCase().includes(filter)
        || autoSyncSourceLabel(p.source || '').toLowerCase().includes(filter);
    const schedulablePlaylists = playlists.filter(p => autoSyncCanSchedulePlaylist(p) && matchesFilter(p));
    const unavailablePlaylists = playlists.filter(p => !autoSyncCanSchedulePlaylist(p) && matchesFilter(p));
    const grouped = schedulablePlaylists.reduce((acc, p) => {
        const key = p.source || 'other';
        if (!acc[key]) acc[key] = [];
        acc[key].push(p);
        return acc;
    }, {});
    const sourceKeys = Object.keys(grouped).sort((a, b) => autoSyncSourceLabel(a).localeCompare(autoSyncSourceLabel(b)));

    const sidebarHtml = sourceKeys.length ? sourceKeys.map(source => `
        <div class="auto-sync-source-group">
            <div class="auto-sync-source-group-head">
                <span class="auto-sync-source-title">
                    ${autoSyncSourceIconHtml(source)}
                    <span class="auto-sync-source-title-label">${_esc(autoSyncSourceLabel(source))}</span>
                </span>
            </div>
            ${grouped[source].map(p => {
                const weekly = weeklySchedules[p.id];
                const hourly = playlistSchedules[p.id];
                let assigned = 'Unscheduled';
                if (weekly) assigned = autoSyncWeeklyLabel(weekly);
                else if (hourly) assigned = `Hourly (${autoSyncIntervalLabel(hourly.hours).toLowerCase()})`;
                return `
                    <div class="auto-sync-playlist ${weekly ? 'scheduled' : (hourly ? 'scheduled-elsewhere' : '')}"
                         draggable="true" data-playlist-id="${p.id}"
                         ondragstart="autoSyncWeeklyDragStart(event)" ondragend="autoSyncWeeklyDragEnd()">
                        <div class="auto-sync-playlist-name">${_esc(p.name)}</div>
                        <div class="auto-sync-playlist-meta">${p.track_count || 0} tracks &middot; ${_esc(assigned)}</div>
                    </div>
                `;
            }).join('')}
        </div>
    `).join('') : '<div class="auto-sync-empty">No refreshable mirrored playlists yet.</div>';

    const unavailableHtml = unavailablePlaylists.length ? `
        <div class="auto-sync-source-group auto-sync-source-group-disabled">
            <div class="auto-sync-source-title">Not schedulable</div>
            ${unavailablePlaylists.map(p => `
                <div class="auto-sync-playlist unavailable">
                    <div class="auto-sync-playlist-name">${_esc(p.name)}</div>
                    <div class="auto-sync-playlist-meta">${_esc(autoSyncSourceLabel(p.source))} &middot; refresh not supported</div>
                </div>
            `).join('')}
        </div>
    ` : '';

    // Build per-day column lists. Iterate the weeklySchedules dict
    // once instead of per-day scanning so multi-day schedules render
    // under each matching column without a double-loop.
    const playlistsById = new Map(schedulablePlaylists.map(p => [parseInt(p.id, 10), p]));
    const cardsByDay = {};
    AUTO_SYNC_WEEKDAYS.forEach(d => { cardsByDay[d] = []; });
    Object.entries(weeklySchedules).forEach(([pid, sched]) => {
        const playlist = playlistsById.get(parseInt(pid, 10));
        if (!playlist) return;
        (sched.days || []).forEach(day => {
            if (cardsByDay[day]) cardsByDay[day].push({ playlist, schedule: sched });
        });
    });

    const dayColumnsHtml = AUTO_SYNC_WEEKDAYS.map(day => {
        const cards = cardsByDay[day];
        const cardHtml = cards.length
            ? cards.map(({ playlist, schedule }) => autoSyncWeeklyCardHtml(playlist, schedule)).join('')
            : '<div class="auto-sync-drop-hint"><strong>Drop here</strong><span>Schedule playlists on this day</span></div>';
        return `
            <div class="auto-sync-column auto-sync-weekly-column" data-day="${day}"
                 ondragover="autoSyncWeeklyDragOver(event)"
                 ondragleave="autoSyncWeeklyDragLeave(event)"
                 ondrop="autoSyncWeeklyDrop(event, '${day}')">
                <div class="auto-sync-column-head">
                    <span>${AUTO_SYNC_WEEKDAY_LABELS[day]}</span>
                    <small>${cards.length} playlist${cards.length === 1 ? '' : 's'}</small>
                </div>
                <div class="auto-sync-column-list">${cardHtml}</div>
            </div>
        `;
    }).join('');

    const filterValue = _esc(_autoSyncSidebarFilter || '');
    const editorHtml = _autoSyncWeeklyEditor ? renderAutoSyncWeeklyEditor() : '';
    return `
        <div class="auto-sync-board-intro">
            <div>
                <strong>Drag playlists onto a day</strong>
                <span>Each placement creates a weekly-time schedule. Click a card to edit time, additional days, or timezone.</span>
            </div>
            <button onclick="refreshAutoSyncScheduleModal()">Refresh</button>
        </div>
        <div class="auto-sync-body">
            <aside class="auto-sync-sidebar">
                <div class="auto-sync-sidebar-title">Mirrored playlists</div>
                <div class="auto-sync-sidebar-filter">
                    <input type="search" class="auto-sync-sidebar-search" placeholder="Filter playlists…"
                           value="${filterValue}" oninput="setAutoSyncSidebarFilter(this.value)" />
                    ${_autoSyncSidebarFilter ? `<button type="button" class="auto-sync-sidebar-filter-clear" onclick="setAutoSyncSidebarFilter('')" aria-label="Clear filter">&times;</button>` : ''}
                </div>
                <div class="auto-sync-source-list">${sidebarHtml}${unavailableHtml}</div>
            </aside>
            <main class="auto-sync-board auto-sync-weekly-board">${dayColumnsHtml}</main>
        </div>
        ${editorHtml}
    `;
}


function autoSyncWeeklyCardHtml(playlist, schedule) {
    // Mirror the hourly board's ``autoSyncScheduledCardHtml`` shape so
    // the two boards stay visually consistent — same name + meta +
    // timing + actions row regardless of whether the schedule is
    // hourly or weekday-based. The only per-board differences are:
    //   - timing line: weekly label vs interval label
    //   - click opens the weekly editor (hourly board has no editor)
    //   - drag fns use the weekly-specific ondragstart / ondragend
    //   - unschedule calls the weekly-specific helper
    const enabled = schedule?.enabled !== false;
    const nextLabel = schedule?.next_run ? autoSyncNextRunLabel(schedule.next_run) : '';
    const isRunning = playlist.pipeline_state?.status === 'running';
    const health = autoSyncPlaylistHealth(playlist.id);
    const healthClass = health.level === 'failing' ? 'failing'
        : health.level === 'warning' ? 'warning'
        : '';
    const label = autoSyncWeeklyLabel(schedule);
    const tz = schedule?.tz || 'UTC';
    return `
        <div class="auto-sync-scheduled-card auto-sync-weekly-card ${enabled ? '' : 'disabled'} ${healthClass}"
             draggable="true"
             data-playlist-id="${playlist.id}"
             ondragstart="autoSyncWeeklyDragStart(event)"
             ondragend="autoSyncWeeklyDragEnd()"
             onclick="openAutoSyncWeeklyEditor(${playlist.id})">
            <div class="auto-sync-scheduled-main">
                <div class="auto-sync-scheduled-name">
                    ${health.level !== 'ok' ? `<span class="auto-sync-scheduled-health ${healthClass}" title="${_escAttr(health.tooltip)}">${health.level === 'failing' ? '!' : '⚠'}</span>` : ''}
                    ${_esc(playlist.name)}
                </div>
                <div class="auto-sync-scheduled-meta">${_esc(autoSyncSourceLabel(playlist.source))} &middot; ${playlist.track_count || 0} tracks</div>
                ${autoSyncOrganizeToggleHtml(playlist)}
                <div class="auto-sync-scheduled-timing">
                    <span>${_esc(label)}</span>
                    <small>${_esc(tz)}</small>
                    ${nextLabel ? `<small>${_esc(nextLabel)}</small>` : ''}
                </div>
            </div>
            <div class="auto-sync-scheduled-actions">
                <button class="run" onclick="event.stopPropagation(); runAutoSyncScheduledPlaylist(${playlist.id})" title="Run the playlist pipeline now" ${isRunning ? 'disabled' : ''}>${isRunning ? 'Running' : 'Run now'}</button>
                <button onclick="event.stopPropagation(); unscheduleAutoSyncWeekly(${playlist.id})" title="Remove this weekly schedule">&times;</button>
            </div>
        </div>
    `;
}


function renderAutoSyncWeeklyEditor() {
    const draft = _autoSyncWeeklyEditor;
    if (!draft) return '';
    const playlist = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === parseInt(draft.playlistId, 10));
    if (!playlist) return '';
    const dayToggles = AUTO_SYNC_WEEKDAYS.map(day => {
        const on = draft.days.includes(day);
        return `
            <button type="button"
                    class="auto-sync-weekly-day-toggle ${on ? 'active' : ''}"
                    onclick="toggleAutoSyncWeeklyEditorDay('${day}')">
                ${AUTO_SYNC_WEEKDAY_LABELS[day]}
            </button>
        `;
    }).join('');
    const existing = _autoSyncScheduleState.weeklySchedules?.[draft.playlistId];
    return `
        <div class="auto-sync-weekly-editor-backdrop" onclick="closeAutoSyncWeeklyEditor()">
            <div class="auto-sync-weekly-editor" onclick="event.stopPropagation()">
                <div class="auto-sync-weekly-editor-head">
                    <h4>Weekly schedule</h4>
                    <button type="button" class="auto-sync-close" onclick="closeAutoSyncWeeklyEditor()">&times;</button>
                </div>
                <div class="auto-sync-weekly-editor-playlist">${_esc(playlist.name)}</div>
                <div class="auto-sync-weekly-editor-section">
                    <label>Days</label>
                    <div class="auto-sync-weekly-editor-days">${dayToggles}</div>
                </div>
                <div class="auto-sync-weekly-editor-section">
                    <label for="auto-sync-weekly-time">Time</label>
                    <input type="time" id="auto-sync-weekly-time"
                           value="${_escAttr(draft.time)}"
                           oninput="setAutoSyncWeeklyEditorTime(this.value)" />
                </div>
                <div class="auto-sync-weekly-editor-section">
                    <label for="auto-sync-weekly-tz">Timezone (IANA)</label>
                    <input type="text" id="auto-sync-weekly-tz"
                           value="${_escAttr(draft.tz)}"
                           oninput="setAutoSyncWeeklyEditorTz(this.value)" />
                    <small class="auto-sync-weekly-editor-hint">e.g. America/Los_Angeles, Europe/London, Asia/Tokyo</small>
                </div>
                <div class="auto-sync-weekly-editor-actions">
                    ${existing ? `<button class="auto-sync-weekly-editor-delete" onclick="unscheduleAutoSyncWeeklyFromEditor()">Unschedule</button>` : ''}
                    <div class="auto-sync-weekly-editor-actions-right">
                        <button class="auto-sync-weekly-editor-cancel" onclick="closeAutoSyncWeeklyEditor()">Cancel</button>
                        <button class="auto-sync-weekly-editor-save" onclick="saveAutoSyncWeeklyFromEditor()">Save</button>
                    </div>
                </div>
            </div>
        </div>
    `;
}


function setAutoSyncSidebarFilter(value) {
    _autoSyncSidebarFilter = String(value || '');
    // Only re-render the sidebar/board portion to keep input focus.
    const panel = document.getElementById('auto-sync-schedule-panel');
    if (!panel) return;
    const { playlists, playlistSchedules } = _autoSyncScheduleState;
    panel.innerHTML = renderAutoSyncSchedulePanel(playlists, playlistSchedules);
    // Restore focus to the search input + caret position at end.
    const input = panel.querySelector('.auto-sync-sidebar-search');
    if (input) {
        input.focus();
        try { input.setSelectionRange(value.length, value.length); } catch (_) {}
    }
}

function getAutoSyncPipelinePlaylists(playlists) {
    return playlists
        .map(p => ({ playlist: p, state: p.pipeline_state || null }))
        .filter(item => item.state && item.state.status && item.state.status !== 'idle')
        .sort((a, b) => {
            const aRunning = a.state.status === 'running' ? 1 : 0;
            const bRunning = b.state.status === 'running' ? 1 : 0;
            if (aRunning !== bRunning) return bRunning - aRunning;
            return (b.state.finished_at || b.state.started_at || 0) - (a.state.finished_at || a.state.started_at || 0);
        });
}

function autoSyncPipelineStatusLabel(status) {
    if (status === 'running') return 'Running';
    if (status === 'finished') return 'Completed';
    if (status === 'skipped') return 'Skipped';
    if (status === 'error') return 'Needs attention';
    return 'Idle';
}

function autoSyncPipelineStatusClass(status) {
    if (status === 'running') return 'running';
    if (status === 'finished') return 'finished';
    if (status === 'error' || status === 'skipped') return 'error';
    return 'idle';
}

function renderAutoSyncPipelineMonitor(playlists) {
    const pipelineItems = getAutoSyncPipelinePlaylists(playlists);
    const running = pipelineItems.filter(item => item.state.status === 'running');
    const recent = pipelineItems.filter(item => item.state.status !== 'running').slice(0, 2);
    const visible = [...running, ...recent].slice(0, 4);
    const title = running.length
        ? `${running.length} pipeline${running.length === 1 ? '' : 's'} running`
        : 'No pipelines running';
    const detail = running.length
        ? 'Live status refreshes while this modal is open.'
        : 'Use Run now on a scheduled playlist when you want the pipeline immediately.';

    return `
        <section class="auto-sync-monitor">
            <div class="auto-sync-monitor-head">
                <div>
                    <span class="auto-sync-monitor-kicker">Live pipeline monitor</span>
                    <strong>${_esc(title)}</strong>
                    <small>${_esc(detail)}</small>
                </div>
                <button onclick="refreshAutoSyncScheduleModal()">Refresh</button>
            </div>
            ${visible.length ? `
                <div class="auto-sync-monitor-list">
                    ${visible.map(({ playlist, state }) => autoSyncPipelineMonitorCardHtml(playlist, state)).join('')}
                </div>
            ` : '<div class="auto-sync-monitor-empty"><span>Ready</span><small>Scheduled playlists appear here while the all-in-one pipeline runs.</small></div>'}
        </section>
    `;
}

function autoSyncPipelineMonitorCardHtml(playlist, state) {
    const status = state.status || 'idle';
    const progress = Math.max(0, Math.min(100, parseInt(state.progress, 10) || 0));
    const latest = Array.isArray(state.log) && state.log.length ? state.log[state.log.length - 1].message : '';
    const phase = state.phase || autoSyncPipelineStatusLabel(status);
    return `
        <article class="auto-sync-monitor-card ${autoSyncPipelineStatusClass(status)}">
            <div class="auto-sync-monitor-card-main">
                <div class="auto-sync-monitor-title-row">
                    <strong>${_esc(playlist.name || `Playlist #${playlist.id}`)}</strong>
                    <span>${_esc(autoSyncPipelineStatusLabel(status))}</span>
                </div>
                <div class="auto-sync-monitor-phase">${_esc(phase)}</div>
                <div class="auto-sync-monitor-progress" aria-label="${progress}% complete">
                    <div style="width: ${progress}%"></div>
                </div>
                ${latest ? `<small>${_esc(latest)}</small>` : ''}
            </div>
            <button onclick="event.stopPropagation(); openMirroredPlaylistModal(${playlist.id})">Details</button>
        </article>
    `;
}

function renderAutoSyncAutomationPanel(automationPipelines, playlists) {
    if (!automationPipelines.length) {
        return '<div class="auto-sync-automation-empty">No Automations-page playlist pipelines found.</div>';
    }
    return `
        <div class="auto-sync-automation-intro">
            <strong>Read-only Automations-page pipelines</strong>
            <span>These use the playlist pipeline but are managed from the Automations page, so this modal only displays them.</span>
        </div>
        <div class="auto-sync-automation-list">
            ${automationPipelines.map(auto => autoSyncAutomationCardHtml(auto, playlists)).join('')}
        </div>
    `;
}

function renderAutoSyncHistoryPanel(history, total) {
    if (!history.length) {
        return `
            <div class="auto-sync-history-empty">
                <strong>No playlist pipeline runs yet</strong>
                <span>Future Auto-Sync and playlist pipeline runs will record before/after playlist snapshots here.</span>
            </div>
        `;
    }
    const filter = _autoSyncHistoryFilter || 'all';
    const tabs = [
        ['all', 'All', history.length],
        ['error', 'Errors', history.filter(h => h.status === 'error' || h.status === 'skipped').length],
        ['completed', 'Completed', history.filter(h => h.status === 'completed' || h.status === 'finished').length],
    ];
    const filterTabsHtml = tabs.map(([key, label, count]) => `
        <button class="auto-sync-history-filter-btn ${filter === key ? 'active' : ''} ${key === 'error' && count > 0 ? 'has-errors' : ''}"
                onclick="setAutoSyncHistoryFilter('${key}')" type="button">
            ${_esc(label)} <em>${count}</em>
        </button>
    `).join('');
    const canLoadMore = total > history.length;
    return `
        <div class="auto-sync-history-intro">
            <div>
                <strong>Playlist pipeline run history</strong>
                <span>Each run records what changed on the mirrored playlist before and after refresh, discovery, sync, and wishlist processing.</span>
            </div>
            <div class="auto-sync-history-intro-controls">
                <div class="auto-sync-history-filters">${filterTabsHtml}</div>
                <button onclick="refreshAutoSyncScheduleModal()">Refresh</button>
            </div>
        </div>
        <div class="auto-sync-history-list" data-renderer="pending">
            <div class="auto-sync-history-loading">Preparing run history...</div>
        </div>
        ${canLoadMore ? `
            <div class="auto-sync-history-load-more-row">
                <button type="button" class="auto-sync-history-load-more" onclick="loadMoreAutoSyncHistory()">
                    Load more (${history.length} of ${total})
                </button>
            </div>
        ` : ''}
    `;
}

function setAutoSyncHistoryFilter(key) {
    _autoSyncHistoryFilter = ['error', 'completed'].includes(key) ? key : 'all';
    renderAutoSyncScheduleModal();
}

function loadMoreAutoSyncHistory() {
    _autoSyncHistoryLimit = Math.min(500, _autoSyncHistoryLimit + 50);
    refreshAutoSyncScheduleModal();
}

function openAutoSyncBulkMenu(event, source) {
    // Build a transient popover with all the standard buckets + a "Custom…"
    // entry. Position relative to the button that triggered it.
    closeAutoSyncBulkMenu();
    const anchor = event.currentTarget;
    if (!anchor) return;
    const menu = document.createElement('div');
    menu.className = 'auto-sync-bulk-menu';
    menu.id = 'auto-sync-bulk-menu';
    const buckets = [...AUTO_SYNC_BUCKETS];
    const buttons = buckets.map(h => `
        <button type="button" onclick="bulkScheduleAutoSyncSource('${_escAttr(source)}', ${h})">
            ${_esc(autoSyncIntervalLabel(h))}
        </button>
    `).join('');
    menu.innerHTML = `
        <div class="auto-sync-bulk-menu-title">Schedule all ${_esc(autoSyncSourceLabel(source))}</div>
        <div class="auto-sync-bulk-menu-buckets">${buttons}</div>
        <button type="button" class="auto-sync-bulk-menu-custom"
                onclick="promptAutoSyncBulkCustom('${_escAttr(source)}')">
            Custom interval…
        </button>
        <button type="button" class="auto-sync-bulk-menu-unschedule"
                onclick="bulkUnscheduleAutoSyncSource('${_escAttr(source)}')">
            Unschedule all
        </button>
    `;
    document.body.appendChild(menu);
    const rect = anchor.getBoundingClientRect();
    menu.style.top = `${rect.bottom + 4}px`;
    menu.style.left = `${Math.max(8, rect.right - menu.offsetWidth)}px`;
    // Close on outside click
    setTimeout(() => {
        document.addEventListener('click', _autoSyncBulkMenuOutsideClick, { once: true });
    }, 0);
}

function _autoSyncBulkMenuOutsideClick(event) {
    const menu = document.getElementById('auto-sync-bulk-menu');
    if (menu && !menu.contains(event.target)) closeAutoSyncBulkMenu();
}

function closeAutoSyncBulkMenu() {
    const existing = document.getElementById('auto-sync-bulk-menu');
    if (existing) existing.remove();
}

function promptAutoSyncBulkCustom(source) {
    closeAutoSyncBulkMenu();
    const raw = window.prompt('Custom interval in hours (e.g. 6, 36, 96):', '6');
    if (raw === null) return;
    const hours = parseInt(raw, 10);
    if (!Number.isFinite(hours) || hours < 1) {
        showToast('Interval must be a whole number of hours, 1 or greater', 'error');
        return;
    }
    bulkScheduleAutoSyncSource(source, hours);
}

async function bulkScheduleAutoSyncSource(source, hours) {
    closeAutoSyncBulkMenu();
    const { playlists } = _autoSyncScheduleState;
    const targets = (playlists || []).filter(p => p.source === source && autoSyncCanSchedulePlaylist(p));
    if (!targets.length) {
        showToast(`No schedulable ${autoSyncSourceLabel(source)} playlists`, 'info');
        return;
    }
    if (!await showConfirmDialog({
        title: `Schedule ${targets.length} ${autoSyncSourceLabel(source)} playlist${targets.length === 1 ? '' : 's'}`,
        message: `Every ${autoSyncIntervalLabel(hours).toLowerCase().replace(/^every /, '')}. Existing schedules in this source will be updated.`,
    })) return;
    let ok = 0, fail = 0;
    for (const playlist of targets) {
        try {
            await saveAutoSyncPlaylistScheduleSilent(playlist.id, hours);
            ok++;
        } catch (_err) {
            fail++;
        }
    }
    showToast(`Scheduled ${ok} ${autoSyncSourceLabel(source)} playlist${ok === 1 ? '' : 's'} at ${autoSyncBucketLabel(hours)}${fail ? ` (${fail} failed)` : ''}`, fail ? 'warning' : 'success');
    await refreshAutoSyncScheduleModal();
}

async function bulkUnscheduleAutoSyncSource(source) {
    closeAutoSyncBulkMenu();
    const { playlists, playlistSchedules } = _autoSyncScheduleState;
    const targets = (playlists || []).filter(p => p.source === source && playlistSchedules[p.id]);
    if (!targets.length) {
        showToast(`No scheduled ${autoSyncSourceLabel(source)} playlists to unschedule`, 'info');
        return;
    }
    if (!await showConfirmDialog({
        title: `Unschedule ${targets.length} ${autoSyncSourceLabel(source)} playlist${targets.length === 1 ? '' : 's'}`,
        message: 'Removes the Auto-Sync schedules. Mirrored playlists themselves stay.',
    })) return;
    let ok = 0, fail = 0;
    for (const playlist of targets) {
        const schedule = playlistSchedules[playlist.id];
        if (!schedule) continue;
        try {
            const res = await fetch(`/api/automations/${schedule.automation_id}`, { method: 'DELETE' });
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            ok++;
        } catch (_err) {
            fail++;
        }
    }
    showToast(`Removed ${ok} schedule${ok === 1 ? '' : 's'}${fail ? ` (${fail} failed)` : ''}`, fail ? 'warning' : 'success');
    await refreshAutoSyncScheduleModal();
}

async function saveAutoSyncPlaylistScheduleSilent(playlistId, hours) {
    // Like saveAutoSyncPlaylistSchedule but without toasts/refresh — caller
    // batches feedback. Re-uses the existing automation row when one already
    // exists for the playlist.
    const playlist = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === parseInt(playlistId, 10));
    if (!playlist) throw new Error('playlist not found');
    const existing = _autoSyncScheduleState.playlistSchedules[playlistId];
    const payload = {
        name: `Auto-Sync: ${playlist.name}`,
        trigger_type: 'schedule',
        trigger_config: autoSyncTriggerForHours(hours),
        action_type: 'playlist_pipeline',
        action_config: { playlist_id: String(playlistId), all: false },
        then_actions: [],
        group_name: 'Playlist Auto-Sync',
        owned_by: 'auto_sync',
    };
    const res = await fetch(existing ? `/api/automations/${existing.automation_id}` : '/api/automations', {
        method: existing ? 'PUT' : 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
    });
    const data = await res.json();
    if (!res.ok || data.error) throw new Error(data.error || 'Failed');
    return data;
}

function populateAutoSyncHistoryList(root = document) {
    const list = root.querySelector('.auto-sync-history-list');
    if (!list) return;
    const allHistory = Array.isArray(_autoSyncScheduleState.runHistory) ? _autoSyncScheduleState.runHistory : [];
    const total = _autoSyncScheduleState.runHistoryTotal || 0;
    const filter = _autoSyncHistoryFilter || 'all';
    const history = allHistory.filter(h => {
        if (filter === 'error') return h.status === 'error' || h.status === 'skipped';
        if (filter === 'completed') return h.status === 'completed' || h.status === 'finished';
        return true;
    });
    list.innerHTML = '';
    list.dataset.renderer = 'dom-cards';
    list.dataset.renderedCount = '0';
    if (filter !== 'all' && !history.length) {
        const note = document.createElement('div');
        note.className = 'auto-sync-history-empty';
        const strong = document.createElement('strong');
        strong.textContent = filter === 'error' ? 'No failed runs in the loaded window' : 'No completed runs in the loaded window';
        const small = document.createElement('span');
        small.textContent = 'Switch filters or load more history.';
        note.append(strong, small);
        list.appendChild(note);
        return;
    }
    history.forEach((entry, index) => {
        try {
            list.appendChild(createAutoSyncHistoryEntryElement(entry, index));
        } catch (err) {
            list.appendChild(createAutoSyncHistoryErrorElement(entry, index, err));
        }
    });
    const renderedCount = list.querySelectorAll('.auto-sync-history-entry').length;
    list.dataset.renderedCount = String(renderedCount);
    if (history.length && !renderedCount) {
        list.appendChild(createAutoSyncHistoryListFallback(history.length));
    }
    if (total > history.length) {
        const totalEl = document.createElement('div');
        totalEl.className = 'auto-sync-history-total';
        totalEl.textContent = `Showing ${history.length} of ${total} runs`;
        list.appendChild(totalEl);
    }
}

function createAutoSyncHistoryListFallback(count) {
    const fallback = document.createElement('div');
    fallback.className = 'auto-sync-history-empty';
    const title = document.createElement('strong');
    title.textContent = 'Run history could not render';
    const detail = document.createElement('span');
    detail.textContent = `${count} playlist pipeline run${count === 1 ? '' : 's'} loaded, but the card renderer did not complete. Refresh the page to reload the latest Auto-Sync assets.`;
    fallback.append(title, detail);
    return fallback;
}

function createAutoSyncHistoryErrorElement(entry, index, err) {
    const card = document.createElement('article');
    card.className = 'auto-sync-history-entry auto-sync-history-entry-error';
    const row = document.createElement('div');
    row.className = 'auto-sync-history-row';
    const head = document.createElement('div');
    head.className = 'auto-sync-history-card-head';
    const titleBlock = document.createElement('div');
    titleBlock.className = 'auto-sync-history-title-block';
    const title = document.createElement('div');
    title.className = 'auto-sync-history-title-row';
    const dot = document.createElement('span');
    dot.className = 'auto-sync-card-status-dot disabled';
    const name = document.createElement('strong');
    name.textContent = entry?.playlist_name || `Run #${entry?.id || index + 1}`;
    const badge = document.createElement('span');
    badge.className = 'auto-sync-history-status error';
    badge.textContent = 'Render error';
    title.append(dot, name, badge);
    const summary = document.createElement('small');
    summary.textContent = err?.message || 'This run history row could not be rendered.';
    titleBlock.append(title, summary);
    head.appendChild(titleBlock);
    row.appendChild(head);
    card.appendChild(row);
    return card;
}

function createAutoSyncHistoryEntryElement(entry, index = 0) {
    entry = autoSyncNormalizeHistoryEntry(entry, index);
    const status = entry.status || 'completed';
    const before = entry.before_json || {};
    const after = entry.after_json || {};
    const result = entry.result_json || {};
    const started = entry.started_at ? _autoTimeAgo(entry.started_at) : '';
    const duration = entry.duration_seconds ? autoSyncDurationLabel(entry.duration_seconds) : '';
    const trackDelta = autoSyncDelta(after.track_count, before.track_count);
    const discoveredDelta = autoSyncDelta(after.discovered_count, before.discovered_count);
    const wishlistDelta = autoSyncDelta(after.wishlisted_count, before.wishlisted_count);
    const libraryDelta = autoSyncDelta(after.in_library_count, before.in_library_count);
    const entryId = `auto-sync-history-${entry.id}`;
    const playlistName = entry.playlist_name || after.name || before.name || `Playlist #${entry.playlist_id || 'unknown'}`;
    const triggerSource = entry.trigger_source || 'pipeline';

    const card = document.createElement('article');
    card.className = `auto-sync-history-entry ${status === 'completed' || status === 'finished' ? '' : 'auto-sync-history-entry-' + status}`.trim();
    card.id = `${entryId}-card`;
    card.dataset.historyEntry = entryId;

    const row = document.createElement('div');
    row.className = 'auto-sync-history-row';
    row.setAttribute('role', 'button');
    row.tabIndex = 0;
    row.setAttribute('aria-expanded', 'false');
    row.setAttribute('aria-controls', entryId);
    row.dataset.historyToggle = entryId;

    const dot = document.createElement('span');
    dot.className = `auto-sync-card-status-dot ${autoSyncHistoryStatusClass(status)}`;

    const info = document.createElement('div');
    info.className = 'auto-sync-history-info';

    const name = document.createElement('div');
    name.className = 'auto-sync-history-name';
    name.textContent = playlistName;

    const flow = document.createElement('div');
    flow.className = 'auto-sync-history-flow';
    autoSyncAppendFlowChip(flow, triggerSource, 'flow-trigger');
    autoSyncAppendFlowArrow(flow);
    autoSyncAppendFlowChip(flow, 'Refresh', 'flow-action');
    autoSyncAppendFlowArrow(flow);
    autoSyncAppendFlowChip(flow, 'Discover', 'flow-action');
    autoSyncAppendFlowArrow(flow);
    autoSyncAppendFlowChip(flow, 'Sync + wishlist', 'flow-notify');

    const metaRow = document.createElement('div');
    metaRow.className = 'auto-sync-history-meta-inline';
    const statusBadge = document.createElement('span');
    statusBadge.className = `auto-sync-history-status ${status}`;
    statusBadge.textContent = autoSyncHistoryStatusLabel(status);
    metaRow.appendChild(statusBadge);
    if (started) {
        const t = document.createElement('span');
        t.className = 'auto-sync-history-time';
        t.textContent = started;
        metaRow.appendChild(t);
    }
    if (duration) {
        const d = document.createElement('span');
        d.className = 'auto-sync-history-duration';
        d.textContent = duration;
        metaRow.appendChild(d);
    }
    const trackChip = document.createElement('span');
    const trackClass = trackDelta > 0 ? 'pos' : trackDelta < 0 ? 'neg' : 'zero';
    trackChip.className = `auto-sync-history-delta ${trackClass}`;
    trackChip.textContent = autoSyncDeltaLabel(after.track_count, trackDelta, 'tracks');
    metaRow.appendChild(trackChip);

    info.append(name, flow, metaRow);

    const actions = document.createElement('div');
    actions.className = 'auto-sync-history-actions';
    const expand = document.createElement('button');
    expand.type = 'button';
    expand.className = 'auto-sync-history-expand-btn';
    expand.dataset.historyToggleButton = entryId;
    expand.setAttribute('aria-label', 'Toggle details');
    expand.innerHTML = '<span class="auto-sync-history-expand-icon">&#9662;</span>';
    actions.appendChild(expand);

    row.append(dot, info, actions);

    const detail = document.createElement('div');
    detail.id = entryId;
    detail.className = 'auto-sync-history-detail';
    detail.innerHTML = autoSyncHistoryDetailHtml(entry, before, after, result, { trackDelta, discoveredDelta, wishlistDelta, libraryDelta });

    card.append(row, detail);
    return card;
}

function autoSyncDeltaLabel(after, delta, unit) {
    const a = parseInt(after, 10) || 0;
    if (!delta) return `${a} ${unit}`;
    const sign = delta > 0 ? '+' : '';
    return `${a} ${unit} (${sign}${delta})`;
}

function autoSyncAppendFlowChip(parent, text, className) {
    const span = document.createElement('span');
    span.className = className;
    span.textContent = text;
    parent.appendChild(span);
}

function autoSyncAppendFlowArrow(parent) {
    const span = document.createElement('span');
    span.className = 'flow-arrow';
    span.textContent = '->';
    parent.appendChild(span);
}

function autoSyncNormalizeHistoryEntry(entry, index) {
    if (!entry || typeof entry !== 'object') {
        return {
            id: `unknown-${index}`,
            status: 'completed',
            playlist_name: 'Playlist pipeline run',
            trigger_source: 'pipeline',
            summary: 'Run history entry did not include detailed metadata.',
            before_json: {},
            after_json: {},
            result_json: {},
        };
    }
    return {
        ...entry,
        id: entry.id ?? `history-${index}`,
        before_json: autoSyncParseHistoryObject(entry.before_json),
        after_json: autoSyncParseHistoryObject(entry.after_json),
        result_json: autoSyncParseHistoryObject(entry.result_json),
    };
}

function bindAutoSyncHistoryCardInteractions(root = document) {
    root.querySelectorAll('[data-history-toggle]').forEach(row => {
        const entryId = row.dataset.historyToggle;
        row.addEventListener('click', () => autoSyncToggleHistoryEntry(entryId));
        row.addEventListener('keydown', event => autoSyncHistoryEntryKeydown(event, entryId));
    });
    root.querySelectorAll('[data-history-toggle-button]').forEach(button => {
        const entryId = button.dataset.historyToggleButton;
        button.addEventListener('click', event => {
            event.stopPropagation();
            autoSyncToggleHistoryEntry(entryId);
        });
    });
}

function autoSyncParseHistoryObject(value) {
    if (!value) return {};
    if (typeof value === 'object') return value;
    if (typeof value !== 'string') return {};
    try {
        const parsed = JSON.parse(value);
        return parsed && typeof parsed === 'object' ? parsed : {};
    } catch (_err) {
        return {};
    }
}

function autoSyncHistoryFallbackSummary(before, after, status) {
    const beforeTracks = parseInt(before.track_count, 10) || 0;
    const afterTracks = parseInt(after.track_count, 10) || 0;
    return `${autoSyncHistoryStatusLabel(status)} | ${beforeTracks} -> ${afterTracks} tracks`;
}

function autoSyncToggleHistoryEntry(entryId) {
    const el = document.getElementById(entryId);
    const card = document.getElementById(`${entryId}-card`);
    const row = card?.querySelector('.auto-sync-history-row');
    if (!el) return;
    const expanded = el.classList.toggle('expanded');
    if (card) card.classList.toggle('expanded', expanded);
    if (row) row.setAttribute('aria-expanded', expanded ? 'true' : 'false');
}

function autoSyncHistoryEntryKeydown(event, entryId) {
    if (event.key !== 'Enter' && event.key !== ' ') return;
    event.preventDefault();
    autoSyncToggleHistoryEntry(entryId);
}

function autoSyncHistoryStatusLabel(status) {
    if (status === 'completed' || status === 'finished') return 'Completed';
    if (status === 'error') return 'Error';
    if (status === 'skipped') return 'Skipped';
    return status || 'Run';
}

function autoSyncHistoryStatusClass(status) {
    if (status === 'completed' || status === 'finished') return 'enabled';
    if (status === 'error' || status === 'skipped') return 'disabled';
    return 'enabled';
}

function autoSyncDurationLabel(seconds) {
    const total = Math.max(0, Math.round(parseFloat(seconds) || 0));
    if (total < 60) return `${total}s`;
    const mins = Math.floor(total / 60);
    const secs = total % 60;
    return `${mins}m ${secs}s`;
}

function autoSyncDelta(after, before) {
    const a = parseInt(after, 10) || 0;
    const b = parseInt(before, 10) || 0;
    return a - b;
}

function autoSyncHistoryStatHtml(label, before, after, delta) {
    const beforeValue = parseInt(before, 10) || 0;
    const afterValue = parseInt(after, 10) || 0;
    const deltaText = delta ? ` (${delta > 0 ? '+' : ''}${delta})` : '';
    return `
        <div>
            <span>${_esc(label)}</span>
            <strong>${beforeValue} -> ${afterValue}${_esc(deltaText)}</strong>
        </div>
    `;
}

function autoSyncHistoryPreviewPill(label, before, after, delta) {
    return `<span>${_esc(autoSyncHistoryPreviewText(label, before, after, delta))}</span>`;
}

function autoSyncHistoryPreviewText(label, before, after, delta) {
    const beforeValue = parseInt(before, 10) || 0;
    const afterValue = parseInt(after, 10) || 0;
    const deltaText = delta ? ` ${delta > 0 ? '+' : ''}${delta}` : '';
    return `${label} ${beforeValue}->${afterValue}${deltaText}`;
}

function autoSyncHistoryResultPill(label, value) {
    if (value === undefined || value === null || value === '') return '';
    return `<span>${_esc(label)}: ${_esc(String(value))}</span>`;
}

function autoSyncHistoryDetailHtml(entry, before, after, result, deltas) {
    const stats = [
        ['Tracks', before.track_count, after.track_count, deltas.trackDelta],
        ['Discovered', before.discovered_count, after.discovered_count, deltas.discoveredDelta],
        ['Wishlisted', before.wishlisted_count, after.wishlisted_count, deltas.wishlistDelta],
        ['In library', before.in_library_count, after.in_library_count, deltas.libraryDelta],
    ];
    const statsHtml = stats.map(([label, b, a, d]) => autoSyncHistoryStatCardHtml(label, b, a, d)).join('');
    const factsHtml = [
        ['Started', autoSyncFormatDateTime(entry.started_at)],
        ['Finished', autoSyncFormatDateTime(entry.finished_at)],
        ['Duration', entry.duration_seconds ? autoSyncDurationLabel(entry.duration_seconds) : '—'],
        ['Source', entry.source || after.source || before.source || '—'],
    ].map(([k, v]) => `<div class="auto-sync-history-fact"><span>${_esc(k)}</span><strong>${_esc(autoSyncValueLabel(v))}</strong></div>`).join('');
    // `tracks_discovered` was a status STRING (e.g. "completed"), not a
    // count — kept it out of the pills so the panel doesn't show a
    // confusing "Discovered: completed" chip. Same data is already
    // surfaced as a before/after stat card above.
    const resultPills = [
        ['Refreshed', result.playlists_refreshed],
        ['Synced', result.tracks_synced],
        ['Skipped', result.sync_skipped],
        ['Wishlisted', result.wishlist_queued],
    ].filter(([, v]) => v !== undefined && v !== null && v !== '')
     .map(([k, v]) => `<span class="auto-sync-history-result-pill"><em>${_esc(k)}</em>${_esc(String(v))}</span>`).join('');
    const errorBlock = result.error ? `<div class="auto-sync-history-error">${_esc(result.error)}</div>` : '';
    const logsHtml = autoSyncHistoryLogsCompactHtml(entry.log_lines);
    const playlistId = entry.playlist_id || after.playlist_id || before.playlist_id || '';
    const playlistName = entry.playlist_name || after.name || before.name || '';
    const runAgainHtml = playlistId
        ? `<div class="auto-sync-history-detail-actions">
              <button type="button" class="auto-sync-history-run-again"
                  onclick="event.stopPropagation(); runMirroredPlaylistPipeline(${parseInt(playlistId, 10)}, '${_escAttr(playlistName)}')">
                  Run pipeline again
              </button>
           </div>`
        : '';
    return `
        <div class="auto-sync-history-stats-grid">${statsHtml}</div>
        <div class="auto-sync-history-facts-row">${factsHtml}</div>
        ${resultPills ? `<div class="auto-sync-history-result-row">${resultPills}</div>` : ''}
        ${runAgainHtml}
        ${errorBlock}
        ${logsHtml}
    `;
}

function autoSyncHistoryStatCardHtml(label, before, after, delta) {
    const a = parseInt(after, 10) || 0;
    const b = parseInt(before, 10) || 0;
    const deltaClass = delta > 0 ? 'pos' : delta < 0 ? 'neg' : 'zero';
    const deltaText = delta ? `${delta > 0 ? '+' : ''}${delta}` : '';
    return `
        <div class="auto-sync-history-stat">
            <div class="auto-sync-history-stat-label">${_esc(label)}</div>
            <div class="auto-sync-history-stat-value">
                <span class="stat-before">${b}</span>
                <span class="stat-arrow">&rarr;</span>
                <span class="stat-after">${a}</span>
                ${deltaText ? `<span class="stat-delta ${deltaClass}">${deltaText}</span>` : ''}
            </div>
        </div>
    `;
}

function autoSyncHistoryLogsCompactHtml(logLines) {
    if (!Array.isArray(logLines) || !logLines.length) return '';
    const lines = logLines.slice(-20).map(line => {
        const text = typeof line === 'string' ? line : (line.message || line.log_line || JSON.stringify(line));
        const type = typeof line === 'object' ? (line.type || line.log_type || 'info') : 'info';
        return `<div class="auto-sync-history-log-line auto-sync-history-log-${_escAttr(type)}">${_esc(text)}</div>`;
    }).join('');
    return `<div class="auto-sync-history-log-section">${lines}</div>`;
}

function autoSyncHistoryFactHtml(label, value) {
    return `
        <div>
            <span>${_esc(label)}</span>
            <strong>${_esc(autoSyncValueLabel(value))}</strong>
        </div>
    `;
}

function autoSyncHistorySnapshotHtml(title, snapshot) {
    const fields = [
        ['Name', snapshot.name],
        ['Source', snapshot.source],
        ['Tracks', snapshot.track_count],
        ['Discovered', snapshot.discovered_count],
        ['Wishlisted', snapshot.wishlisted_count],
        ['In library', snapshot.in_library_count],
    ];
    return `
        <section class="auto-sync-history-section auto-sync-history-snapshot">
            <div class="auto-sync-history-section-title">${_esc(title)}</div>
            <div class="auto-sync-history-facts compact">
                ${fields.map(([label, value]) => autoSyncHistoryFactHtml(label, value)).join('')}
            </div>
        </section>
    `;
}

function autoSyncHistoryObjectHtml(title, obj, options = {}) {
    if (!obj || typeof obj !== 'object') return '';
    const entries = Object.entries(obj)
        .filter(([key, value]) => !(options.skipPrivate && key.startsWith('_')) && value !== undefined && value !== null && value !== '')
        .slice(0, 24);
    if (!entries.length) return '';
    return `
        <section class="auto-sync-history-section">
            <div class="auto-sync-history-section-title">${_esc(title)}</div>
            <div class="auto-sync-history-payload">
                ${entries.map(([key, value]) => `
                    <div>
                        <span>${_esc(autoSyncHumanizeKey(key))}</span>
                        <strong>${_esc(autoSyncValueLabel(value))}</strong>
                    </div>
                `).join('')}
            </div>
        </section>
    `;
}

function autoSyncHistoryLogsHtml(logLines) {
    if (!Array.isArray(logLines) || !logLines.length) return '';
    return `
        <section class="auto-sync-history-section">
            <div class="auto-sync-history-section-title">Run Log</div>
            <div class="auto-sync-history-logs">
                ${logLines.slice(-12).map(line => {
                    const text = typeof line === 'string' ? line : (line.message || line.log_line || JSON.stringify(line));
                    const type = typeof line === 'object' ? (line.type || line.log_type || 'info') : 'info';
                    return `<div class="${_escAttr(type)}">${_esc(text)}</div>`;
                }).join('')}
            </div>
        </section>
    `;
}

function autoSyncFormatDateTime(value) {
    if (!value) return '';
    const ts = _autoParseUTC(value);
    if (!Number.isFinite(ts)) return value;
    return new Date(ts).toLocaleString();
}

function autoSyncHumanizeKey(key) {
    return String(key || '')
        .replace(/^_+/, '')
        .replace(/_/g, ' ')
        .replace(/\b\w/g, ch => ch.toUpperCase());
}

function autoSyncValueLabel(value) {
    if (value === undefined || value === null || value === '') return 'Not recorded';
    if (typeof value === 'boolean') return value ? 'Yes' : 'No';
    if (Array.isArray(value)) return value.length ? value.map(autoSyncValueLabel).join(', ') : 'None';
    if (typeof value === 'object') return JSON.stringify(value);
    return String(value);
}

function autoSyncAutomationCardHtml(auto, playlists) {
    const cfg = auto.action_config || {};
    const playlistId = autoSyncPlaylistIdFromAutomation(auto);
    const playlist = playlistId ? playlists.find(p => parseInt(p.id, 10) === playlistId) : null;
    const target = cfg.all === true || cfg.all === 'true'
        ? 'All refreshable mirrored playlists'
        : playlist ? playlist.name : playlistId ? `Playlist #${playlistId}` : 'Custom pipeline target';
    const trigger = _autoFormatTrigger(auto.trigger_type, auto.trigger_config || {});
    const enabled = auto.enabled !== false && auto.enabled !== 0;
    const next = auto.next_run ? autoSyncNextRunLabel(auto.next_run) : 'not scheduled';
    const sourceLabel = playlist ? autoSyncSourceLabel(playlist.source) : (cfg.all === true || cfg.all === 'true' ? 'All sources' : 'Pipeline');
    return `
        <div class="auto-sync-automation-card">
            <span class="auto-sync-card-status-dot ${enabled ? 'enabled' : 'disabled'}"></span>
            <div class="auto-sync-automation-main">
                <div class="auto-sync-automation-title-row">
                    <strong>${_esc(auto.name || 'Playlist Pipeline')}</strong>
                </div>
                <div class="auto-sync-card-flow">
                    <span class="flow-trigger">${_esc(trigger)}</span>
                    <span class="flow-arrow">&rarr;</span>
                    <span class="flow-action">Playlist pipeline</span>
                    <span class="flow-arrow">&rarr;</span>
                    <span class="flow-notify">Refresh + sync</span>
                </div>
                <div class="auto-sync-automation-meta">
                    <span class="auto-sync-status ${enabled ? 'enabled' : 'disabled'}">${enabled ? 'Enabled' : 'Disabled'}</span>
                    <span>${_esc(sourceLabel)}</span>
                    <span>${_esc(target)}</span>
                    <span>${_esc(next)}</span>
                </div>
            </div>
            <div class="auto-sync-automation-lock">Read only</div>
        </div>
    `;
}

function autoSyncOrganizeToggleHtml(playlist) {
    const checked = playlist.organize_by_playlist ? 'checked' : '';
    return `
        <label class="auto-sync-organize-toggle" onclick="event.stopPropagation();" title="Download missing tracks into a playlist-named folder (artist - track)">
            <input type="checkbox" ${checked} onchange="setAutoSyncOrganizeByPlaylist(${playlist.id}, this.checked)">
            <span>Organize by playlist</span>
        </label>
    `;
}

async function setAutoSyncOrganizeByPlaylist(playlistId, enabled) {
    try {
        const res = await fetch(`/api/mirrored-playlists/${playlistId}/preferences`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ organize_by_playlist: !!enabled }),
        });
        const data = await res.json();
        if (!res.ok || data.error) throw new Error(data.error || 'Failed to update preference');
        const pl = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === parseInt(playlistId, 10));
        if (pl) pl.organize_by_playlist = !!enabled;
        showToast(enabled ? 'Auto-Sync will use playlist folders' : 'Auto-Sync will use standard download layout', 'success');
    } catch (err) {
        showToast(`Error: ${err.message}`, 'error');
        await refreshAutoSyncScheduleModal();
    }
}

function autoSyncScheduledCardHtml(playlist, schedule) {
    const enabled = schedule?.enabled !== false;
    const nextLabel = schedule?.next_run ? autoSyncNextRunLabel(schedule.next_run) : '';
    const isRunning = playlist.pipeline_state?.status === 'running';
    const health = autoSyncPlaylistHealth(playlist.id);
    const healthClass = health.level === 'failing' ? 'failing'
        : health.level === 'warning' ? 'warning'
        : '';
    return `
        <div class="auto-sync-scheduled-card ${enabled ? '' : 'disabled'} ${healthClass}" draggable="true" data-playlist-id="${playlist.id}" ondragstart="autoSyncDragStart(event)" ondragend="autoSyncDragEnd()">
            <div class="auto-sync-scheduled-main">
                <div class="auto-sync-scheduled-name">
                    ${health.level !== 'ok' ? `<span class="auto-sync-scheduled-health ${healthClass}" title="${_escAttr(health.tooltip)}">${health.level === 'failing' ? '!' : '⚠'}</span>` : ''}
                    ${_esc(playlist.name)}
                </div>
                <div class="auto-sync-scheduled-meta">${_esc(autoSyncSourceLabel(playlist.source))} &middot; ${playlist.track_count || 0} tracks</div>
                ${autoSyncOrganizeToggleHtml(playlist)}
                <div class="auto-sync-scheduled-timing">
                    <span>${_esc(autoSyncIntervalLabel(schedule?.hours || 24))}</span>
                    ${nextLabel ? `<small>${_esc(nextLabel)}</small>` : ''}
                </div>
            </div>
            <div class="auto-sync-scheduled-actions">
                <button class="run" onclick="event.stopPropagation(); runAutoSyncScheduledPlaylist(${playlist.id})" title="Run the playlist pipeline now" ${isRunning ? 'disabled' : ''}>${isRunning ? 'Running' : 'Run now'}</button>
                <button onclick="event.stopPropagation(); unscheduleAutoSyncPlaylist(${playlist.id})" title="Remove this Auto-Sync schedule">&times;</button>
            </div>
        </div>
    `;
}

function autoSyncPlaylistHealth(playlistId) {
    // Look at the last 3 runs for this playlist in the loaded history.
    // 3-in-a-row errors = failing (red dot). 1+ recent error = warning.
    const history = _autoSyncScheduleState.runHistory || [];
    const id = parseInt(playlistId, 10);
    const recent = history
        .filter(h => parseInt(h.playlist_id, 10) === id)
        .slice(0, 3);
    if (!recent.length) return { level: 'ok', tooltip: '' };
    const errored = recent.filter(h => h.status === 'error' || h.status === 'skipped');
    if (errored.length >= 3) {
        return { level: 'failing', tooltip: `Last ${recent.length} runs failed — check Run History tab` };
    }
    if (errored.length) {
        return { level: 'warning', tooltip: `${errored.length} of last ${recent.length} runs failed` };
    }
    return { level: 'ok', tooltip: '' };
}

function autoSyncNextRunLabel(nextRun) {
    if (!nextRun) return '';
    const ts = _autoParseUTC(nextRun);
    if (!Number.isFinite(ts)) return '';
    const diff = ts - Date.now();
    if (diff <= 0) return 'due now';
    const mins = Math.ceil(diff / 60000);
    if (mins < 60) return `next in ${mins}m`;
    const hours = Math.ceil(mins / 60);
    if (hours < 24) return `next in ${hours}h`;
    return `next in ${Math.ceil(hours / 24)}d`;
}

function autoSyncDragStart(event) {
    const playlistId = event.currentTarget?.dataset?.playlistId;
    if (!playlistId) return;
    _autoSyncIsDragging = true;
    event.dataTransfer.setData('text/plain', playlistId);
    event.dataTransfer.effectAllowed = 'move';
}

function autoSyncDragOver(event) {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'move';
    const col = event.currentTarget;
    if (col && !col.classList.contains('drag-over')) {
        col.classList.add('drag-over');
    }
}

function autoSyncDragLeave(event) {
    const col = event.currentTarget;
    if (!col) return;
    if (col.contains(event.relatedTarget)) return;
    col.classList.remove('drag-over');
}

async function autoSyncDrop(event, hours) {
    event.preventDefault();
    _autoSyncIsDragging = false;
    const col = event.currentTarget;
    if (col) col.classList.remove('drag-over');
    const playlistId = parseInt(event.dataTransfer.getData('text/plain'), 10);
    if (!playlistId) return;
    await saveAutoSyncPlaylistSchedule(playlistId, hours);
}

function autoSyncDragEnd() {
    _autoSyncIsDragging = false;
}

async function saveAutoSyncPlaylistSchedule(playlistId, hours) {
    const playlist = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === parseInt(playlistId, 10));
    if (!playlist) return;
    if (!autoSyncCanSchedulePlaylist(playlist)) {
        showToast('That playlist source cannot be refreshed by Auto-Sync.', 'info');
        return;
    }

    // Enforce one-schedule-per-playlist: if a weekly schedule exists,
    // drop it before installing the hourly one. Mirrors the same
    // mutual-exclusion the weekly save path enforces in reverse.
    const existingWeekly = _autoSyncScheduleState.weeklySchedules?.[playlistId];
    if (existingWeekly) {
        try {
            await fetch(`/api/automations/${existingWeekly.automation_id}`, { method: 'DELETE' });
        } catch (_) { /* best-effort cleanup */ }
    }

    const existing = _autoSyncScheduleState.playlistSchedules[playlistId];
    const payload = {
        name: `Auto-Sync: ${playlist.name}`,
        trigger_type: 'schedule',
        trigger_config: autoSyncTriggerForHours(hours),
        action_type: 'playlist_pipeline',
        action_config: { playlist_id: String(playlistId), all: false },
        then_actions: [],
        group_name: 'Playlist Auto-Sync',
        owned_by: 'auto_sync',
    };
    try {
        const res = await fetch(existing ? `/api/automations/${existing.automation_id}` : '/api/automations', {
            method: existing ? 'PUT' : 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload),
        });
        const data = await res.json();
        if (!res.ok || data.error) throw new Error(data.error || 'Failed to save Auto-Sync schedule');
        showToast(`${playlist.name} scheduled every ${autoSyncBucketLabel(hours)}`, 'success');
        await refreshAutoSyncScheduleModal();
    } catch (err) {
        showToast(`Error: ${err.message}`, 'error');
    }
}

async function unscheduleAutoSyncPlaylist(playlistId) {
    const schedule = _autoSyncScheduleState.playlistSchedules[playlistId];
    const playlist = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === parseInt(playlistId, 10));
    if (!schedule) return;
    if (!await showConfirmDialog({ title: 'Remove Auto-Sync', message: `Remove Auto-Sync schedule for "${playlist?.name || 'this playlist'}"?` })) return;
    try {
        const res = await fetch(`/api/automations/${schedule.automation_id}`, { method: 'DELETE' });
        const data = await res.json();
        if (!res.ok || data.error) throw new Error(data.error || 'Failed to remove Auto-Sync schedule');
        showToast('Auto-Sync schedule removed', 'success');
        await refreshAutoSyncScheduleModal();
    } catch (err) {
        showToast(`Error: ${err.message}`, 'error');
    }
}

// ──────────────────────────────────────────────────────────────────────
// Weekly-tab drag-drop + editor state mutators.
// ──────────────────────────────────────────────────────────────────────


function autoSyncWeeklyDragStart(event) {
    _autoSyncIsDragging = true;
    const id = event.currentTarget?.dataset?.playlistId || '';
    event.dataTransfer.setData('text/plain', id);
    event.dataTransfer.effectAllowed = 'move';
}


function autoSyncWeeklyDragEnd() {
    _autoSyncIsDragging = false;
}


function autoSyncWeeklyDragOver(event) {
    event.preventDefault();
    event.dataTransfer.dropEffect = 'move';
    const col = event.currentTarget;
    if (col && !col.classList.contains('drag-over')) {
        col.classList.add('drag-over');
    }
}


function autoSyncWeeklyDragLeave(event) {
    const col = event.currentTarget;
    if (!col) return;
    col.classList.remove('drag-over');
}


async function autoSyncWeeklyDrop(event, day) {
    event.preventDefault();
    _autoSyncIsDragging = false;
    const col = event.currentTarget;
    if (col) col.classList.remove('drag-over');
    const playlistId = parseInt(event.dataTransfer.getData('text/plain'), 10);
    if (!playlistId) return;
    if (!AUTO_SYNC_WEEKDAYS.includes(day)) return;

    const playlist = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === playlistId);
    if (!playlist || !autoSyncCanSchedulePlaylist(playlist)) {
        showToast('That playlist source cannot be refreshed by Auto-Sync.', 'info');
        return;
    }

    // Augment OR create: if a weekly schedule already exists for this
    // playlist, append the dropped day (no-op when already present);
    // otherwise create a single-day schedule with default time + browser tz.
    const existing = _autoSyncScheduleState.weeklySchedules?.[playlistId];
    const days = existing
        ? (existing.days.includes(day) ? existing.days : [...existing.days, day])
        : [day];
    const time = existing?.time || '09:00';
    const tz = existing?.tz || detectBrowserTimezone();
    await saveAutoSyncWeeklySchedule(playlistId, { time, days, tz });
}


function openAutoSyncWeeklyEditor(playlistId) {
    const pid = parseInt(playlistId, 10);
    if (!pid) return;
    const existing = _autoSyncScheduleState.weeklySchedules?.[pid];
    _autoSyncWeeklyEditor = {
        playlistId: pid,
        time: existing?.time || '09:00',
        days: existing ? [...existing.days] : [],
        tz: existing?.tz || detectBrowserTimezone(),
    };
    renderAutoSyncScheduleModal();
}


function closeAutoSyncWeeklyEditor() {
    _autoSyncWeeklyEditor = null;
    renderAutoSyncScheduleModal();
}


function toggleAutoSyncWeeklyEditorDay(day) {
    if (!_autoSyncWeeklyEditor) return;
    if (!AUTO_SYNC_WEEKDAYS.includes(day)) return;
    const idx = _autoSyncWeeklyEditor.days.indexOf(day);
    if (idx >= 0) {
        _autoSyncWeeklyEditor.days.splice(idx, 1);
    } else {
        _autoSyncWeeklyEditor.days.push(day);
    }
    renderAutoSyncScheduleModal();
}


function setAutoSyncWeeklyEditorTime(value) {
    if (!_autoSyncWeeklyEditor) return;
    _autoSyncWeeklyEditor.time = String(value || '09:00');
}


function setAutoSyncWeeklyEditorTz(value) {
    if (!_autoSyncWeeklyEditor) return;
    _autoSyncWeeklyEditor.tz = String(value || 'UTC');
}


async function saveAutoSyncWeeklyFromEditor() {
    if (!_autoSyncWeeklyEditor) return;
    const { playlistId, time, days, tz } = _autoSyncWeeklyEditor;
    if (!days.length) {
        showToast('Pick at least one day for the weekly schedule.', 'error');
        return;
    }
    await saveAutoSyncWeeklySchedule(playlistId, { time, days, tz });
    _autoSyncWeeklyEditor = null;
}


async function unscheduleAutoSyncWeeklyFromEditor() {
    if (!_autoSyncWeeklyEditor) return;
    const { playlistId } = _autoSyncWeeklyEditor;
    _autoSyncWeeklyEditor = null;
    await unscheduleAutoSyncWeekly(playlistId);
}


async function saveAutoSyncWeeklySchedule(playlistId, { time, days, tz }) {
    const playlist = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === parseInt(playlistId, 10));
    if (!playlist) return;
    if (!autoSyncCanSchedulePlaylist(playlist)) {
        showToast('That playlist source cannot be refreshed by Auto-Sync.', 'info');
        return;
    }
    const triggerConfig = autoSyncWeeklyTrigger({ time, days, tz });
    if (!triggerConfig.days.length) {
        showToast('Pick at least one day for the weekly schedule.', 'error');
        return;
    }

    // Enforce one-schedule-per-playlist: if the playlist currently has
    // an hourly schedule, drop it before installing the weekly one. The
    // engine can technically run both side-by-side as two separate
    // automations, but the UI assumes one schedule per playlist and
    // showing two cards under the same playlist row would surprise
    // users. Delete-then-create is safe — the worst case (POST fails)
    // leaves the playlist unscheduled, which is recoverable from the UI.
    const existingHourly = _autoSyncScheduleState.playlistSchedules?.[playlistId];
    if (existingHourly) {
        try {
            await fetch(`/api/automations/${existingHourly.automation_id}`, { method: 'DELETE' });
        } catch (_) { /* best-effort cleanup */ }
    }

    const existingWeekly = _autoSyncScheduleState.weeklySchedules?.[playlistId];
    const payload = {
        name: `Auto-Sync: ${playlist.name}`,
        trigger_type: 'weekly_time',
        trigger_config: triggerConfig,
        action_type: 'playlist_pipeline',
        action_config: { playlist_id: String(playlistId), all: false },
        then_actions: [],
        group_name: 'Playlist Auto-Sync',
        owned_by: 'auto_sync',
    };
    try {
        const res = await fetch(existingWeekly ? `/api/automations/${existingWeekly.automation_id}` : '/api/automations', {
            method: existingWeekly ? 'PUT' : 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload),
        });
        const data = await res.json();
        if (!res.ok || data.error) throw new Error(data.error || 'Failed to save weekly schedule');
        showToast(`${playlist.name} scheduled ${autoSyncWeeklyLabel(triggerConfig).toLowerCase()}`, 'success');
        await refreshAutoSyncScheduleModal();
    } catch (err) {
        showToast(`Error: ${err.message}`, 'error');
    }
}


async function unscheduleAutoSyncWeekly(playlistId) {
    const schedule = _autoSyncScheduleState.weeklySchedules?.[playlistId];
    const playlist = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === parseInt(playlistId, 10));
    if (!schedule) return;
    if (!await showConfirmDialog({ title: 'Remove Weekly Schedule', message: `Remove weekly schedule for "${playlist?.name || 'this playlist'}"?` })) return;
    try {
        const res = await fetch(`/api/automations/${schedule.automation_id}`, { method: 'DELETE' });
        const data = await res.json();
        if (!res.ok || data.error) throw new Error(data.error || 'Failed to remove weekly schedule');
        showToast('Weekly schedule removed', 'success');
        await refreshAutoSyncScheduleModal();
    } catch (err) {
        showToast(`Error: ${err.message}`, 'error');
    }
}


async function runAutoSyncScheduledPlaylist(playlistId) {
    const playlist = _autoSyncScheduleState.playlists.find(p => parseInt(p.id, 10) === parseInt(playlistId, 10));
    if (!playlist) return;
    await runMirroredPlaylistPipeline(playlistId, playlist.name || `Playlist #${playlistId}`);
    await refreshAutoSyncScheduleModal();
}

function manageAutoSyncStatusPolling() {
    const overlay = document.getElementById('auto-sync-schedule-modal');
    if (!overlay) {
        stopAutoSyncStatusPolling();
        return;
    }
    const hasRunning = _autoSyncScheduleState.playlists.some(p => p.pipeline_state?.status === 'running');
    if (!hasRunning) {
        stopAutoSyncStatusPolling();
        return;
    }
    if (_autoSyncStatusPoller) return;
    _autoSyncStatusPoller = setInterval(() => {
        if (_autoSyncIsDragging) return;
        refreshAutoSyncScheduleModal();
    }, 3000);
}

function stopAutoSyncStatusPolling() {
    if (!_autoSyncStatusPoller) return;
    clearInterval(_autoSyncStatusPoller);
    _autoSyncStatusPoller = null;
}

async function parseMirroredPipelineResponse(res, fallbackMessage) {
    const text = await res.text();
    let data = {};
    if (text) {
        try {
            data = JSON.parse(text);
        } catch (_err) {
            const detail = res.status === 404
                ? 'Auto-Sync endpoint not found. Restart the SoulSync server so the new backend routes load.'
                : fallbackMessage;
            throw new Error(detail);
        }
    }
    if (!res.ok || data.error) {
        throw new Error(data.error || fallbackMessage);
    }
    return data;
}

async function editMirroredSourceRef(playlistId, name, source, currentRef) {
    const label = (source === 'spotify_public' || source === 'youtube')
        ? 'original playlist URL'
        : 'original playlist ID or URL';
    const nextRef = window.prompt(`Update ${label} for "${name}"`, currentRef || '');
    if (nextRef === null) return;
    const trimmed = nextRef.trim();
    if (!trimmed) {
        showToast('Source link or ID is required', 'error');
        return;
    }

    try {
        const res = await fetch(`/api/mirrored-playlists/${playlistId}/source-ref`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ source_ref: trimmed })
        });
        const data = await res.json();
        if (!res.ok || data.error) {
            throw new Error(data.error || 'Failed to update source reference');
        }
        showToast(`Updated source for ${name}`, 'success');
        loadMirroredPlaylists();
        const openModal = document.getElementById('mirrored-track-modal');
        if (openModal) {
            closeMirroredModal();
            openMirroredPlaylistModal(playlistId);
        }
    } catch (err) {
        showToast(`Error: ${err.message}`, 'error');
    }
}

function applyMirroredPipelineState(playlistId, state) {
    const hash = `mirrored_${playlistId}`;
    const existing = youtubePlaylistStates[hash] || {};
    const status = state.status || 'idle';
    let phase = existing.phase;
    if (status === 'running') phase = 'pipeline_running';
    else if (status === 'finished') phase = 'pipeline_complete';
    else if (status === 'error' || status === 'skipped') phase = 'pipeline_error';

    youtubePlaylistStates[hash] = {
        ...existing,
        phase,
        pipeline_status: status,
        pipeline_progress: state.progress || 0,
        pipeline_phase: state.phase || '',
        pipeline_error: state.error || '',
        pipeline_log: state.log || [],
        pipeline_result: state.result || null,
    };

    updateMirroredCardPhase(hash, phase);
}

async function runMirroredPlaylistPipeline(playlistId, name) {
    try {
        const res = await fetch(`/api/mirrored-playlists/${playlistId}/pipeline/run`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({})
        });
        const data = await parseMirroredPipelineResponse(res, 'Failed to start Auto-Sync');
        applyMirroredPipelineState(playlistId, data.state || { status: 'running', progress: 0, phase: 'Starting pipeline...' });
        showToast(`Auto-Sync started for ${name}`, 'success');
        _autoSyncScheduleState.playlists = _autoSyncScheduleState.playlists.map(p => (
            parseInt(p.id, 10) === parseInt(playlistId, 10)
                ? { ...p, pipeline_state: data.state || { status: 'running', progress: 0, phase: 'Starting pipeline...' } }
                : p
        ));
        renderAutoSyncScheduleModal();
        manageAutoSyncStatusPolling();
        pollMirroredPipelineStatus(playlistId, name);
    } catch (err) {
        showToast(`Error: ${err.message}`, 'error');
    }
}

function pollMirroredPipelineStatus(playlistId, name) {
    const key = `mirrored_${playlistId}`;
    if (mirroredPipelinePollers[key]) clearInterval(mirroredPipelinePollers[key]);

    const tick = async () => {
        try {
            const res = await fetch(`/api/mirrored-playlists/${playlistId}/pipeline/status`);
            const state = await parseMirroredPipelineResponse(res, 'Failed to read Auto-Sync status');
            applyMirroredPipelineState(playlistId, state);

            if (state.status === 'finished') {
                clearInterval(mirroredPipelinePollers[key]);
                delete mirroredPipelinePollers[key];
                showToast(`Auto-Sync complete for ${name}`, 'success');
                loadMirroredPlaylists();
                refreshAutoSyncScheduleModal();
            } else if (state.status === 'error' || state.status === 'skipped') {
                clearInterval(mirroredPipelinePollers[key]);
                delete mirroredPipelinePollers[key];
                showToast(state.error || `Pipeline stopped for ${name}`, 'error');
                loadMirroredPlaylists();
                refreshAutoSyncScheduleModal();
            } else if (state.status === 'idle') {
                clearInterval(mirroredPipelinePollers[key]);
                delete mirroredPipelinePollers[key];
            }
        } catch (err) {
            clearInterval(mirroredPipelinePollers[key]);
            delete mirroredPipelinePollers[key];
            showToast(`Pipeline status error: ${err.message}`, 'error');
        }
    };

    tick();
    mirroredPipelinePollers[key] = setInterval(tick, 2500);
}
