<?php
// All Sites Overview — admin page with the multi-select bulk-management
// surface that was inlined onto the regular Sites page in 2026.05.196.
//
// Posture: all-users. Per-user users will only see THEIR sites in the table
// because the $sites SQL is owner-scoped for non-root. The "Filter by user"
// typeahead, the "Owner" column, and a few destructive actions remain
// root-only via the same $is_root gates the inline version used.
//
// Registered in $allowed_pages and routed via index.php. Authentication is
// handled by auto-prepend.php. The bulk-action endpoints
// (api/sites-bulk.php + api/sites-bulk-disable.php) key off action+job_id,
// not the originating page, so they continue to work unchanged.

if (!defined('AUTH_USER')) {
    header('Location: login.php');
    exit;
}

require_once('/docker/whp/web/libs/mysqlmgmt.php');
require_once('/docker/whp/web/libs/account_disable.php');

$message = '';
$message_type = '';
$is_root = (defined('AUTH_USER') && AUTH_USER === 'root');

// Build status_map (username -> ['autostart','reason']) from the
// accounts_disabled table — account-level is the unit of suspension.
// (Lifted verbatim from sites.php so the Status column renders identically.)
$status_map = [];
if ($is_root) {
    try {
        $_status_mgmt = new mysqlmgmt();
        $_status_db = $_status_mgmt->getMySQLConnection();
        if ($_status_db) {
            $rows = $_status_db->query("SELECT username, reason FROM whp.accounts_disabled")
                               ->fetchAll(PDO::FETCH_ASSOC);
            foreach ($rows as $r) {
                $status_map[$r['username']] = [
                    'autostart' => 'false',
                    'reason'    => $r['reason'] ?? '',
                ];
            }
        }
    } catch (Throwable $_e) {
        error_log("[sites-overview.php] status_map query failed: " . $_e->getMessage());
    }
}

// Initialize database connection
$MySQLMgmt = new mysqlmgmt();
$db = $MySQLMgmt->getMySQLConnection();

if (!$db) {
    $message = 'Database connection failed';
    $message_type = 'danger';
}

// Get sites — root sees all, non-root sees only their own. Same query shape as
// sites.php so the data-* attributes on each <input> stay consistent with the
// bulk JS's expectations (data-container-type-id, data-cpu-per-container, …).
$sites = [];
if ($db) {
    try {
        if ($is_root) {
            $stmt = $db->query("
                SELECT
                    s.*,
                    COALESCE(sd.fqdn, d.domain_name) as primary_domain,
                    ct.name as container_type_name,
                    ct.base_image,
                    ct.min_cpu,
                    ct.min_memory
                FROM whp.sites s
                JOIN whp.domains d ON s.primary_domain_id = d.id
                LEFT JOIN whp.site_domains sd ON s.id = sd.site_id AND sd.is_primary = 1
                JOIN whp.container_types ct ON s.container_type_id = ct.id
                ORDER BY s.site_name ASC
            ");
        } else {
            $stmt = $db->prepare("
                SELECT
                    s.*,
                    COALESCE(sd.fqdn, d.domain_name) as primary_domain,
                    ct.name as container_type_name,
                    ct.base_image,
                    ct.min_cpu,
                    ct.min_memory
                FROM whp.sites s
                JOIN whp.domains d ON s.primary_domain_id = d.id
                LEFT JOIN whp.site_domains sd ON s.id = sd.site_id AND sd.is_primary = 1
                JOIN whp.container_types ct ON s.container_type_id = ct.id
                WHERE s.username = ?
                ORDER BY s.site_name ASC
            ");
            $stmt->execute([AUTH_USER]);
        }
        $sites = $stmt->fetchAll();
    } catch (PDOException $e) {
        $sites = [];
        $message = 'Error loading sites: ' . $e->getMessage();
        $message_type = 'danger';
    }
}

// Root-only: distinct list of users who actually own at least one site.
// Drives the "Filter by user" typeahead at the top of the table. Avoids the
// empty-result trap of listing every defunct username in whp.users.
$site_owners = [];
if ($is_root && $db && !empty($sites)) {
    try {
        $owners_rows = $db->query("
            SELECT u.username, COUNT(s.id) AS site_count
            FROM whp.users u
            JOIN whp.sites s ON s.username = u.username
            GROUP BY u.username
            ORDER BY u.username ASC
        ")->fetchAll(PDO::FETCH_ASSOC);
        foreach ($owners_rows as $r) {
            $site_owners[] = [
                'username'   => $r['username'],
                'site_count' => (int)$r['site_count'],
            ];
        }
    } catch (Throwable $_e) {
        // Fall back to deriving owners from the already-loaded $sites array.
        error_log("[sites-overview.php] site_owners query failed: " . $_e->getMessage());
        $counts = [];
        foreach ($sites as $s) {
            $u = $s['username'] ?? '';
            if ($u === '') continue;
            $counts[$u] = ($counts[$u] ?? 0) + 1;
        }
        ksort($counts);
        foreach ($counts as $u => $n) {
            $site_owners[] = ['username' => $u, 'site_count' => $n];
        }
    }
}

// Container types — needed by the bulk "Change PHP / Container Type" action
// modal. Same query as sites.php (excludes PHP-FPM types).
$container_types = [];
if ($db) {
    try {
        $stmt = $db->query("SELECT * FROM whp.container_types WHERE active = 1 AND name NOT LIKE 'PHP-FPM%' ORDER BY name ASC");
        $container_types = $stmt->fetchAll();
    } catch (PDOException $e) {
        $container_types = [];
    }
}

// Available domains (for "Add Additional Domain" bulk action). Same query
// as sites.php's $all_available_domains.
$all_available_domains = [];
if ($db) {
    try {
        if ($is_root) {
            $stmt = $db->query("
                SELECT d.*, u.username as owner
                FROM whp.domains d
                JOIN whp.client_allowances u ON d.username = u.username
                WHERE d.active = 1
                ORDER BY d.domain_name ASC
            ");
        } else {
            $stmt = $db->prepare("
                SELECT d.*, d.username as owner
                FROM whp.domains d
                WHERE d.username = ? AND d.active = 1
                ORDER BY d.domain_name ASC
            ");
            $stmt->execute([AUTH_USER]);
        }
        $all_available_domains = $stmt->fetchAll();
    } catch (PDOException $e) {
        $all_available_domains = [];
    }
}
?>

<?php if (!empty($message)): ?>
<div class="alert alert-<?php echo $message_type; ?> alert-dismissible fade show" role="alert">
    <?php echo htmlspecialchars($message); ?>
    <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>

<?php if (empty($sites)): ?>
<div class="alert alert-info">
    <i class="fa fa-info-circle me-1"></i>
    <strong>No sites to show.</strong>
    <a href="index.php?page=sites" class="alert-link">Go to the Sites page</a> to create one.
</div>
<?php else: ?>
<!-- Sites Overview table with multi-select bulk-management.
     - Root sees all sites + bulk actions across owners.
     - Non-root sees only their own sites (the $sites query already filters).
     - "Disable Selected" only renders for root since suspension is a root-only op. -->
<div class="row" id="sitesOverviewSection">
    <div class="col-12">
        <div class="card">
            <div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
                <h5 class="card-title mb-0"><i class="fa fa-list me-2"></i><?php echo $is_root ? 'All Sites' : 'Your Sites'; ?></h5>
                <div class="d-flex gap-2 align-items-center">
                    <span class="text-muted small" id="sitesSelectAllOnPageHint" style="display:none;">
                        <a href="#" id="selectAllOnPageLink">Select all <span id="sitesOverviewVisibleCount"><?php echo count($sites); ?></span> on this page</a>
                    </span>
                    <?php if ($is_root): ?>
                    <button type="button" class="btn btn-sm btn-outline-danger" id="bulkDisableOpenBtn" disabled>
                        <i class="fa fa-ban me-1"></i>Disable Selected&hellip;
                    </button>
                    <?php endif; ?>
                </div>
            </div>
            <?php if ($is_root && !empty($site_owners)): ?>
            <!-- Root-only: client-side "Filter by user" control. Hidden for
                 non-root users, who already only see their own sites. The
                 inline <select> is a graceful fallback if typeahead-multiselect.js
                 fails to load; the JS at the bottom of this page upgrades it
                 in-place. Data-driven from $site_owners (users WITH at least one
                 site — avoids the empty-result trap of listing all defunct users). -->
            <div class="whp-user-filter-bar px-3 py-2" id="sitesUserFilterBar">
                <div class="whp-user-filter-row">
                    <label class="whp-user-filter-label" for="sitesUserFilterSelect">
                        <i class="fa fa-filter me-1"></i>Filter by user:
                    </label>
                    <div class="whp-user-filter-host" id="sitesUserFilterHost">
                        <!-- Fallback <select>: replaced by TypeaheadMultiselect when JS is ready -->
                        <select id="sitesUserFilterSelect" class="form-select form-select-sm" aria-label="Filter sites by owning user">
                            <option value="">All users (<?php echo count($sites); ?> sites)</option>
                            <?php foreach ($site_owners as $owner): ?>
                                <option value="<?php echo htmlspecialchars($owner['username']); ?>">
                                    <?php echo htmlspecialchars($owner['username']); ?> (<?php echo (int)$owner['site_count']; ?>)
                                </option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                    <a href="#" class="whp-user-filter-clear" id="sitesUserFilterClear" style="display:none;">
                        <i class="fa fa-times me-1"></i>Clear filter
                    </a>
                    <span class="whp-user-filter-status text-muted small" id="sitesUserFilterStatus" aria-live="polite"></span>
                </div>
            </div>
            <?php endif; ?>
            <div class="card-body p-0">
                <div class="table-responsive">
                    <table class="table table-sm table-hover mb-0" id="sitesOverviewTable">
                        <thead>
                            <tr>
                                <th style="width:36px;">
                                    <input type="checkbox" id="selectAllSites" title="Select all" aria-label="Select all sites">
                                </th>
                                <th>Site Name</th>
                                <th>Primary Domain</th>
                                <?php if ($is_root): ?><th>Owner</th><?php endif; ?>
                                <th>Container Type</th>
                                <th>Status</th>
                            </tr>
                        </thead>
                        <tbody>
                        <?php foreach ($sites as $site):
                            $st = $status_map[$site['username']] ?? null;
                            if (!$st || $st['autostart'] === 'true' || $st['autostart'] === '') {
                                $status_badge = '<span class="badge bg-success">Active</span>';
                            } else {
                                $reason_label = ($st['reason'] !== '') ? htmlspecialchars($st['reason']) : 'disabled';
                                $status_badge = '<span class="badge bg-danger">Disabled: ' . $reason_label . '</span>';
                            }
                        ?>
                            <tr data-username="<?php echo htmlspecialchars($site['username']); ?>">
                                <td>
                                    <input type="checkbox" class="site-select-cb"
                                           value="<?php echo (int)$site['id']; ?>"
                                           data-site-name="<?php echo htmlspecialchars($site['site_name']); ?>"
                                           data-primary-domain="<?php echo htmlspecialchars($site['primary_domain']); ?>"
                                           data-owner="<?php echo htmlspecialchars($site['username']); ?>"
                                           data-container-type-id="<?php echo (int)$site['container_type_id']; ?>"
                                           data-container-type-name="<?php echo htmlspecialchars($site['container_type_name'] ?? ''); ?>"
                                           data-cpu-per-container="<?php echo htmlspecialchars($site['cpu_per_container']); ?>"
                                           data-memory-per-container="<?php echo (int)$site['memory_per_container']; ?>"
                                           data-auto-recreate="<?php echo (int)($site['auto_recreate_on_update'] ?? 0); ?>"
                                           aria-label="Select <?php echo htmlspecialchars($site['site_name']); ?>">
                                </td>
                                <td><?php echo htmlspecialchars($site['site_name']); ?></td>
                                <td><?php echo htmlspecialchars($site['primary_domain']); ?><?php echo !empty($site['is_wildcard']) ? ' <span class="badge bg-secondary ms-1">Wildcard</span>' : ''; ?></td>
                                <?php if ($is_root): ?><td><?php echo htmlspecialchars($site['username']); ?></td><?php endif; ?>
                                <td><span class="badge bg-secondary"><?php echo htmlspecialchars($site['container_type_name'] ?? '—'); ?></span></td>
                                <td><?php echo $status_badge; ?></td>
                            </tr>
                        <?php endforeach; ?>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- Sticky action bar: only visible when >=1 row is selected. Hidden empty-state per spec. -->
<style>
.whp-bulk-action-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1040;
    background: var(--whp-bg, #fff);
    border-top: 1px solid var(--whp-border, #dee2e6);
    box-shadow: 0 -2px 12px rgba(0,0,0,0.08);
    padding: 0.75rem 1.5rem;
    display: none;
}
.whp-bulk-action-bar.is-visible { display: flex; }
.whp-bulk-action-bar .whp-bulk-count {
    font-weight: 600;
    margin-right: 1rem;
    display: inline-flex;
    align-items: center;
}
.whp-bulk-action-bar .whp-bulk-count .badge {
    margin-right: 0.4rem;
}
.whp-bulk-action-bar .btn { white-space: nowrap; }
.whp-bulk-action-bar .whp-bulk-actions {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    flex: 1;
}
@media (max-width: 768px) {
    .whp-bulk-action-bar { flex-direction: column; align-items: stretch; }
}
/* Row hidden by the root-only "Filter by user" control. Important so it
   wins over Bootstrap's .table tr display:table-row. */
#sitesOverviewTable tbody tr.whp-row-hidden { display: none !important; }
</style>
<div class="whp-bulk-action-bar" id="bulkActionBar" role="toolbar" aria-label="Bulk site actions">
    <div class="whp-bulk-count">
        <span class="badge bg-primary" id="bulkSelectedCount">0</span> site(s) selected
    </div>
    <div class="whp-bulk-actions">
        <button type="button" class="btn btn-sm btn-info"     data-bulk-action="recreate_containers">
            <i class="fa fa-refresh me-1"></i>Recreate Containers
        </button>
        <button type="button" class="btn btn-sm btn-secondary" data-bulk-action="change_container_type">
            <i class="fa fa-exchange me-1"></i>Change PHP Version
        </button>
        <button type="button" class="btn btn-sm btn-secondary" data-bulk-action="change_resources">
            <i class="fa fa-sliders me-1"></i>CPU / Memory
        </button>
        <button type="button" class="btn btn-sm btn-secondary" data-bulk-action="toggle_auto_recreate">
            <i class="fa fa-cogs me-1"></i>Auto-recreate Flag
        </button>
        <button type="button" class="btn btn-sm btn-warning"   data-bulk-action="ssl_renew">
            <i class="fa fa-lock me-1"></i>Renew SSL
        </button>
        <button type="button" class="btn btn-sm btn-secondary" data-bulk-action="add_additional_domain">
            <i class="fa fa-plus me-1"></i>Add Domain
        </button>
        <button type="button" class="btn btn-sm btn-danger"    data-bulk-action="delete_sites">
            <i class="fa fa-trash me-1"></i>Delete
        </button>
    </div>
    <button type="button" class="btn btn-sm btn-link text-muted ms-auto" id="bulkClearSelectionBtn" aria-label="Clear selection">
        <i class="fa fa-times me-1"></i>Clear
    </button>
</div>

<!-- Bulk action confirm/config modal. Body is filled by JS per-action. -->
<div class="modal fade" id="bulkActionModal" tabindex="-1" aria-labelledby="bulkActionModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="bulkActionModalLabel">Bulk Action</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body" id="bulkActionModalBody">
                <!-- filled by JS -->
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                <button type="button" class="btn btn-primary"   id="bulkActionConfirmBtn">Confirm</button>
            </div>
        </div>
    </div>
</div>

<!-- Bulk results modal. Per-site result rows streamed in from the API.
     For async actions (recreate / ssl / change_type / delete) we poll the
     status endpoint and update this table as each site completes. -->
<div class="modal fade" id="bulkResultsModal" tabindex="-1" aria-labelledby="bulkResultsModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
    <div class="modal-dialog modal-lg modal-dialog-scrollable">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="bulkResultsModalLabel"><i class="fa fa-tasks me-2"></i>Bulk Action Progress</h5>
                <button type="button" class="btn-close" id="bulkResultsCloseBtn" data-bs-dismiss="modal" aria-label="Close" disabled></button>
            </div>
            <div class="modal-body">
                <div class="mb-2">
                    <div class="progress" role="progressbar" aria-label="Bulk progress">
                        <div class="progress-bar progress-bar-striped progress-bar-animated" id="bulkResultsProgressBar" style="width: 0%">0%</div>
                    </div>
                </div>
                <div class="mb-3 small">
                    <span class="badge bg-success me-2"><i class="fa fa-check me-1"></i><span id="bulkResultsOkCount">0</span> ok</span>
                    <span class="badge bg-danger  me-2"><i class="fa fa-times me-1"></i><span id="bulkResultsFailCount">0</span> failed</span>
                    <span class="badge bg-secondary me-2"><i class="fa fa-forward me-1"></i><span id="bulkResultsSkipCount">0</span> skipped</span>
                    <span class="text-muted">of <span id="bulkResultsTotalCount">0</span> total</span>
                </div>
                <div class="table-responsive" style="max-height: 50vh;">
                    <table class="table table-sm table-hover mb-0" id="bulkResultsTable">
                        <thead><tr><th>Site</th><th>Action</th><th>Status</th><th>Message</th></tr></thead>
                        <tbody id="bulkResultsTbody"></tbody>
                    </table>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-warning" id="bulkResultsCancelBtn" style="display: none;">
                    <i class="fa fa-stop me-1"></i>Cancel Job
                </button>
                <button type="button" class="btn btn-secondary" id="bulkResultsDoneBtn" data-bs-dismiss="modal" disabled>Close</button>
            </div>
        </div>
    </div>
</div>

<!-- Task 20: Bulk Disable Modal (root-only — the open button only renders for root) -->
<div class="modal fade" id="bulkDisableModal" tabindex="-1" aria-labelledby="bulkDisableModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="bulkDisableModalLabel"><i class="fa fa-ban me-2"></i>Bulk Disable Sites</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <p>You are about to disable <strong id="bulkDisableCount">0</strong> site(s):</p>
                <ul id="bulkDisableSiteList" class="mb-3"></ul>
                <div class="mb-3">
                    <label for="bulkDisableReason" class="form-label">Reason <span class="text-danger">*</span></label>
                    <select class="form-select" id="bulkDisableReason" required>
                        <option value="">-- Select reason --</option>
                        <option value="unpaid">Unpaid invoice</option>
                        <option value="tos_violation">ToS violation</option>
                        <option value="maintenance">Maintenance</option>
                        <option value="admin_other">Other (admin)</option>
                    </select>
                </div>
                <div class="mb-3">
                    <label for="bulkDisableNote" class="form-label">Note (optional)</label>
                    <textarea class="form-control" id="bulkDisableNote" rows="2" placeholder="Internal note&hellip;"></textarea>
                </div>
                <div id="bulkDisableResult" class="mt-2" style="display:none;"></div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                <button type="button" class="btn btn-danger" id="bulkDisableConfirmBtn">
                    <i class="fa fa-ban me-1"></i>Disable Selected Sites
                </button>
            </div>
        </div>
    </div>
</div>
<?php endif; ?>

<script>
// CSRF token for bulk-action AJAX. Same constant name as sites.php so the
// bulk JS body remained a verbatim move (no rename needed).
const nonce = '<?php echo get_csrf_token(); ?>';

// Shared helper: show message in a result div using safe DOM construction.
// Lifted from sites.php — the bulk-disable JS uses it. Kept page-local so
// sites-overview.php has no implicit dependency on sites.php being loaded.
function whpSetResultMsg(divId, isSuccess, msg) {
    const el = document.getElementById(divId);
    if (!el) return;
    el.style.display = '';
    const wrapper = document.createElement('div');
    wrapper.className = 'alert alert-' + (isSuccess ? 'success' : 'danger');
    wrapper.textContent = msg;
    while (el.firstChild) el.removeChild(el.firstChild);
    el.appendChild(wrapper);
}
function whpClearResultDiv(divId) {
    const el = document.getElementById(divId);
    if (!el) return;
    el.style.display = 'none';
    while (el.firstChild) el.removeChild(el.firstChild);
}

// ---- Task 18 / 20: Sites overview table checkbox + bulk-disable wiring ----
(function() {
    const selectAll = document.getElementById('selectAllSites');
    const bulkBtn   = document.getElementById('bulkDisableOpenBtn');
    if (!selectAll || !bulkBtn) return;

    // Helper — restrict to visible rows so a "Filter by user" selection
    // doesn't bleed hidden rows into the disable-bulk operation. Matches the
    // newer bulk-action wiring further down; .whp-row-hidden is applied by
    // the user-filter control.
    function isVisibleRow(cb) {
        const tr = cb.closest('tr');
        return !!(tr && !tr.classList.contains('whp-row-hidden'));
    }
    function checkedVisible() {
        return Array.from(document.querySelectorAll('.site-select-cb:checked')).filter(isVisibleRow);
    }
    function refreshBulkBtn() {
        bulkBtn.disabled = (checkedVisible().length === 0);
    }

    selectAll.addEventListener('change', function() {
        // Only toggle the rows the operator can see; hidden rows stay alone.
        Array.from(document.querySelectorAll('.site-select-cb')).filter(isVisibleRow).forEach(function(cb) { cb.checked = selectAll.checked; });
        refreshBulkBtn();
    });
    document.querySelectorAll('.site-select-cb').forEach(function(cb) {
        cb.addEventListener('change', refreshBulkBtn);
    });

    bulkBtn.addEventListener('click', function() {
        const checked = checkedVisible();
        if (checked.length === 0) return;
        document.getElementById('bulkDisableCount').textContent = checked.length;
        const ul = document.getElementById('bulkDisableSiteList');
        while (ul.firstChild) ul.removeChild(ul.firstChild);
        checked.forEach(function(cb) {
            const li = document.createElement('li');
            li.textContent = cb.dataset.siteName || cb.value;
            ul.appendChild(li);
        });
        document.getElementById('bulkDisableReason').value = '';
        document.getElementById('bulkDisableNote').value = '';
        whpClearResultDiv('bulkDisableResult');
        new bootstrap.Modal(document.getElementById('bulkDisableModal')).show();
    });

    const bulkConfirmBtn = document.getElementById('bulkDisableConfirmBtn');
    if (bulkConfirmBtn) {
        bulkConfirmBtn.addEventListener('click', function() {
            const reason = document.getElementById('bulkDisableReason').value;
            if (!reason) { alert('Please select a reason.'); return; }
            const note   = document.getElementById('bulkDisableNote').value;
            const ids    = checkedVisible().map(function(cb) { return cb.value; });
            bulkConfirmBtn.disabled = true;
            bulkConfirmBtn.textContent = 'Disabling…';
            const fd = new FormData();
            fd.append('csrf_token', nonce);
            fd.append('reason', reason);
            if (note) fd.append('note', note);
            ids.forEach(function(id) { fd.append('site_ids[]', id); });
            fetch('api/sites-bulk-disable.php', { method: 'POST', body: fd })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.results) {
                        let okCount = 0, failCount = 0;
                        Object.values(data.results).forEach(function(r) { r.ok ? okCount++ : failCount++; });
                        const msg = okCount + ' site(s) disabled' + (failCount ? ', ' + failCount + ' failed' : '') + '.';
                        whpSetResultMsg('bulkDisableResult', failCount === 0, msg);
                    } else {
                        whpSetResultMsg('bulkDisableResult', false, 'Unexpected response from server.');
                    }
                    bulkConfirmBtn.disabled = false;
                    bulkConfirmBtn.textContent = 'Disable Selected Sites';
                    setTimeout(function() { location.reload(); }, 1500);
                })
                .catch(function() {
                    whpSetResultMsg('bulkDisableResult', false, 'Request failed.');
                    bulkConfirmBtn.disabled = false;
                    bulkConfirmBtn.textContent = 'Disable Selected Sites';
                });
        });
    }
})();

// Server-emitted data sources for the bulk-management UI.
// These are kept as top-level page vars so the bulk JS block stays a
// pure JS file (clean separation; easier to extract later).
const WHP_BULK_CONTAINER_TYPES = <?php
    $ct_for_js = array_map(function($ct) {
        return ['id' => (int)$ct['id'], 'name' => $ct['name'], 'base_image' => $ct['base_image'] ?? ''];
    }, $container_types ?? []);
    echo json_encode($ct_for_js);
?>;
const WHP_BULK_AVAILABLE_DOMAINS = <?php
    $ad_for_js = array_map(function($d) {
        return ['id' => (int)$d['id'], 'domain_name' => $d['domain_name'], 'owner' => $d['owner'] ?? $d['username'] ?? ''];
    }, $all_available_domains ?? []);
    echo json_encode($ad_for_js);
?>;


// ---- Multi-site bulk management wiring ----
// Sticky action bar, action dispatch, per-action confirm modal, and async
// results modal with poll-based progress. All endpoints go through
// api/sites-bulk.php; per-row ownership enforcement happens server-side.
//
// Must be wrapped in DOMContentLoaded — the bulk action bar markup is
// rendered after the body, and bootstrap.Modal is loaded in the footer
// after this script. (See feedback_whp_inline_scripts_need_dom_ready.)
document.addEventListener('DOMContentLoaded', function() {
    const overviewSection  = document.getElementById('sitesOverviewSection');
    if (!overviewSection) return; // empty-sites state — nothing to wire

    const tableBody         = document.querySelector('#sitesOverviewTable tbody');
    const selectAllHeader   = document.getElementById('selectAllSites');
    const selectAllOnPage   = document.getElementById('selectAllOnPageLink');
    const selectAllHint     = document.getElementById('sitesSelectAllOnPageHint');
    const actionBar         = document.getElementById('bulkActionBar');
    const countBadge        = document.getElementById('bulkSelectedCount');
    const clearBtn          = document.getElementById('bulkClearSelectionBtn');
    const actionModalEl     = document.getElementById('bulkActionModal');
    const actionModalBody   = document.getElementById('bulkActionModalBody');
    const actionModalLabel  = document.getElementById('bulkActionModalLabel');
    const actionConfirmBtn  = document.getElementById('bulkActionConfirmBtn');
    const resultsModalEl    = document.getElementById('bulkResultsModal');
    const resultsTbody      = document.getElementById('bulkResultsTbody');
    const resultsOk         = document.getElementById('bulkResultsOkCount');
    const resultsFail       = document.getElementById('bulkResultsFailCount');
    const resultsSkip       = document.getElementById('bulkResultsSkipCount');
    const resultsTotal      = document.getElementById('bulkResultsTotalCount');
    const resultsBar        = document.getElementById('bulkResultsProgressBar');
    const resultsCloseBtn   = document.getElementById('bulkResultsCloseBtn');
    const resultsDoneBtn    = document.getElementById('bulkResultsDoneBtn');
    const resultsCancelBtn  = document.getElementById('bulkResultsCancelBtn');
    let   currentJobId      = null;

    // Visibility helpers — rows hidden by the "Filter by user" control wear the
    // .whp-row-hidden class on the <tr>. The bulk-action UI must never operate
    // on off-screen rows, so getCheckedBoxes() returns only checkboxes whose
    // row is currently visible. The filter logic below also unchecks any
    // checkbox it hides, so this is a defence-in-depth guard rather than the
    // primary mechanism.
    function isRowVisible(cb) {
        const tr = cb.closest('tr');
        return !!(tr && !tr.classList.contains('whp-row-hidden'));
    }
    function getAllVisibleCheckboxes() {
        return Array.from(document.querySelectorAll('.site-select-cb')).filter(isRowVisible);
    }
    function getCheckedBoxes() {
        return getAllVisibleCheckboxes().filter(function(cb) { return cb.checked; });
    }
    function refreshSelectionUI() {
        const visibleBoxes = getAllVisibleCheckboxes();
        const checked = visibleBoxes.filter(function(cb) { return cb.checked; });
        const n = checked.length;
        countBadge.textContent = n;
        actionBar.classList.toggle('is-visible', n > 0);
        const bulkDisableBtn = document.getElementById('bulkDisableOpenBtn');
        if (bulkDisableBtn) bulkDisableBtn.disabled = (n === 0);
        const totalBoxes = visibleBoxes.length;
        if (selectAllHint) {
            selectAllHint.style.display = (n < totalBoxes && totalBoxes > 1) ? '' : 'none';
        }
        // Update the "Select all N on this page" link's count to match the
        // currently-visible row count, so it doesn't lie when a filter is on.
        const visibleCountEl = document.getElementById('sitesOverviewVisibleCount');
        if (visibleCountEl) visibleCountEl.textContent = String(totalBoxes);
        if (selectAllHeader) {
            if (n === 0)               { selectAllHeader.checked = false; selectAllHeader.indeterminate = false; }
            else if (n === totalBoxes) { selectAllHeader.checked = true;  selectAllHeader.indeterminate = false; }
            else                        { selectAllHeader.indeterminate = true; }
        }
    }
    if (tableBody) {
        tableBody.addEventListener('change', function(e) {
            if (e.target && e.target.classList.contains('site-select-cb')) refreshSelectionUI();
        });
    }
    if (selectAllHeader) {
        selectAllHeader.addEventListener('change', function() {
            // Toggle only the rows the operator can actually see; hidden rows
            // stay in whatever state they were (they will be unchecked when
            // hidden anyway).
            getAllVisibleCheckboxes().forEach(function(cb) { cb.checked = selectAllHeader.checked; });
            refreshSelectionUI();
        });
    }
    if (selectAllOnPage) {
        selectAllOnPage.addEventListener('click', function(e) {
            e.preventDefault();
            getAllVisibleCheckboxes().forEach(function(cb) { cb.checked = true; });
            refreshSelectionUI();
        });
    }
    if (clearBtn) {
        clearBtn.addEventListener('click', function() {
            // Clear visible only; matches header select-all semantics.
            getAllVisibleCheckboxes().forEach(function(cb) { cb.checked = false; });
            refreshSelectionUI();
        });
    }
    refreshSelectionUI();

    // ---- Root-only "Filter by user" wiring ----
    // Hides table rows whose data-username does not match the selected user.
    // Composes with any future client-side row filter (e.g. a name search):
    // each filter dimension applies its own CSS class to <tr>, and the row
    // is visible iff it carries none of them. Currently we only have one
    // dimension (whp-row-hidden = "hidden by user filter"); the visibility
    // check above is class-based, so adding a second hidden-class is a
    // trivial extension.
    //
    // URL persistence via history.replaceState (?filter_user=<username>) so
    // F5 restores the filter without polluting browser history.
    (function setupUserFilter() {
        const bar       = document.getElementById('sitesUserFilterBar');
        if (!bar) return; // non-root or no owners — nothing to wire
        const host      = document.getElementById('sitesUserFilterHost');
        const fallback  = document.getElementById('sitesUserFilterSelect');
        const clearLink = document.getElementById('sitesUserFilterClear');
        const status    = document.getElementById('sitesUserFilterStatus');
        const allRows   = Array.from(document.querySelectorAll('#sitesOverviewTable tbody tr'));
        const owners    = Array.from(fallback.options)
            .map(function(o) { return { value: o.value, label: o.textContent.trim() }; })
            .filter(function(o) { return o.value !== ''; });

        function updateUrl(username) {
            try {
                const url = new URL(window.location.href);
                if (username) url.searchParams.set('filter_user', username);
                else          url.searchParams.delete('filter_user');
                history.replaceState(null, '', url.toString());
            } catch (_e) { /* IE-only, ignore */ }
        }

        function applyFilter(username) {
            let visibleCount = 0;
            allRows.forEach(function(tr) {
                const owner = tr.getAttribute('data-username') || '';
                const matches = !username || owner === username;
                tr.classList.toggle('whp-row-hidden', !matches);
                if (matches) visibleCount++;
                // Off-screen rows must not silently carry into bulk operations.
                if (!matches) {
                    const cb = tr.querySelector('.site-select-cb');
                    if (cb && cb.checked) cb.checked = false;
                }
            });
            if (clearLink) clearLink.style.display = username ? '' : 'none';
            if (status) {
                if (username) {
                    status.textContent = 'Showing ' + visibleCount + ' of ' + allRows.length + ' sites';
                } else {
                    status.textContent = '';
                }
            }
            updateUrl(username);
            refreshSelectionUI();
        }

        function selectFromControl(username) {
            // Drive the fallback <select> so a/t11y readers see the value too.
            if (fallback) fallback.value = username || '';
            applyFilter(username || '');
        }

        // Initial filter from ?filter_user=<username>, if it's a known owner.
        let initial = '';
        try {
            const params = new URLSearchParams(window.location.search);
            const v = params.get('filter_user');
            if (v && owners.some(function(o) { return o.value === v; })) initial = v;
        } catch (_e) { /* ignore */ }

        // Upgrade the fallback <select> to a TypeaheadMultiselect (single-select
        // mode: empty `selected: []`, onChange picks the last value and clears
        // the rest). If the JS isn't loaded for any reason, the <select>
        // continues to work as-is.
        let tams = null;
        if (window.TypeaheadMultiselect && host) {
            // Detach the fallback from layout so the typeahead host owns the box.
            fallback.style.display = 'none';
            fallback.setAttribute('aria-hidden', 'true');
            const mount = document.createElement('div');
            mount.className = 'whp-user-filter-tams';
            host.appendChild(mount);
            try {
                tams = new TypeaheadMultiselect(mount, {
                    items: owners,
                    selected: initial ? [initial] : [],
                    placeholder: 'Type a username to filter...',
                    emptyMessage: 'No users match.',
                    noItemsMessage: 'No users available.',
                    maxVisible: 200,
                    onChange: function(values) {
                        // Single-select semantics: most-recently-checked wins.
                        let pick = '';
                        if (values && values.length > 0) {
                            pick = values[values.length - 1];
                            if (values.length > 1) {
                                // Drop everything except the latest pick.
                                tams.setSelected([pick]);
                                return; // setSelected will recurse via onChange
                            }
                        }
                        selectFromControl(pick);
                    }
                });
            } catch (_e) {
                // Typeahead init failed — fall back to plain <select>.
                fallback.style.display = '';
                fallback.removeAttribute('aria-hidden');
                tams = null;
            }
        }

        if (fallback) {
            fallback.addEventListener('change', function() {
                const v = fallback.value || '';
                if (tams) tams.setSelected(v ? [v] : []);
                else      selectFromControl(v);
            });
        }
        if (clearLink) {
            clearLink.addEventListener('click', function(e) {
                e.preventDefault();
                if (tams) tams.setSelected([]);
                else      selectFromControl('');
            });
        }

        // Apply initial state.
        selectFromControl(initial);
    })();

    let pendingAction = null;

    const actionTitles = {
        recreate_containers:    'Recreate Containers',
        change_container_type:  'Change PHP / Container Type',
        change_resources:       'Change CPU / Memory Limits',
        toggle_auto_recreate:   'Toggle Auto-recreate Flag',
        ssl_renew:              'Renew SSL Certificates',
        add_additional_domain:  'Add Additional Domain',
        delete_sites:           'Delete Sites'
    };

    function escapeHtml(s) {
        return String(s)
            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    }

    // Build DOM nodes via DOM API rather than HTML strings: keeps every
    // user-controlled value going through textContent (textContent is the
    // XSS-safe path the rest of WHP's admin UIs use for site/domain names
    // pulled from the DB).
    function siteListNode(checkedBoxes) {
        const wrap = document.createElement('div');
        wrap.className = 'border rounded p-2 mb-3';
        wrap.style.maxHeight = '220px';
        wrap.style.overflowY = 'auto';
        const ul = document.createElement('ul');
        ul.className = 'mb-0';
        checkedBoxes.forEach(function(cb) {
            const li = document.createElement('li');
            const name   = cb.dataset.siteName || ('site#' + cb.value);
            const domain = cb.dataset.primaryDomain || '';
            li.textContent = name + (domain ? ' — ' + domain : '');
            if (cb.dataset.owner) {
                const owner = document.createElement('span');
                owner.className = 'text-muted small ms-1';
                owner.textContent = '(' + cb.dataset.owner + ')';
                li.appendChild(document.createTextNode(' '));
                li.appendChild(owner);
            }
            ul.appendChild(li);
        });
        wrap.appendChild(ul);
        return wrap;
    }

    const containerTypes = WHP_BULK_CONTAINER_TYPES;
    const availableDomains = WHP_BULK_AVAILABLE_DOMAINS;

    function appendIntro(parent, action, n) {
        const intro = document.createElement('p');
        const title = document.createElement('strong');
        title.textContent = actionTitles[action] || action;
        intro.appendChild(document.createTextNode('You are about to apply '));
        intro.appendChild(title);
        intro.appendChild(document.createTextNode(' to '));
        const c = document.createElement('strong');
        c.textContent = String(n);
        intro.appendChild(c);
        intro.appendChild(document.createTextNode(' site(s):'));
        parent.appendChild(intro);
    }
    function alertDiv(cls, html) {
        const d = document.createElement('div');
        d.className = 'alert ' + cls + ' small mb-0';
        d.innerHTML = html; // static literal strings only — no user data
        return d;
    }
    function selectFromList(id, opts, placeholder) {
        const sel = document.createElement('select');
        sel.id = id;
        sel.className = 'form-select';
        if (placeholder) {
            const o = document.createElement('option'); o.value = ''; o.textContent = placeholder;
            sel.appendChild(o);
        }
        opts.forEach(function(o) {
            const opt = document.createElement('option');
            opt.value = o.value;
            opt.textContent = o.label;
            sel.appendChild(opt);
        });
        return sel;
    }
    function labelWrap(text, child) {
        const div = document.createElement('div');
        div.className = 'mb-2';
        const lab = document.createElement('label');
        lab.className = 'form-label';
        lab.textContent = text;
        div.appendChild(lab);
        div.appendChild(child);
        return div;
    }
    function helpText(text) {
        const d = document.createElement('div');
        d.className = 'form-text';
        d.textContent = text;
        return d;
    }

    function buildModalBody(action, checked) {
        const root = document.createDocumentFragment();
        appendIntro(root, action, checked.length);
        root.appendChild(siteListNode(checked));

        if (action === 'recreate_containers') {
            root.appendChild(alertDiv('alert-warning',
                '<i class="fa fa-exclamation-triangle me-1"></i>' +
                'Each site\'s containers will be stopped, removed, and recreated with the latest image. Brief downtime per site is expected.'));
        } else if (action === 'delete_sites') {
            root.appendChild(alertDiv('alert-danger',
                '<i class="fa fa-exclamation-triangle me-1"></i>' +
                '<strong>Destructive:</strong> containers, HAProxy backends, wildcard DNS records, and the site row itself will be permanently removed. This cannot be undone.'));
            const labelDiv = document.createElement('div'); labelDiv.className = 'mt-2';
            const label = document.createElement('label'); label.className = 'form-label small';
            label.innerHTML = 'Type <code>DELETE</code> to confirm:';
            const input = document.createElement('input');
            input.type = 'text'; input.id = 'bulkDeleteConfirmInput';
            input.className = 'form-control form-control-sm'; input.autocomplete = 'off';
            labelDiv.appendChild(label); labelDiv.appendChild(input);
            root.appendChild(labelDiv);
        } else if (action === 'change_container_type') {
            const opts = containerTypes.map(function(ct) {
                return { value: ct.id, label: ct.name + (ct.base_image ? ' — ' + ct.base_image : '') };
            });
            root.appendChild(labelWrap('New container type', selectFromList('bulkNewContainerTypeId', opts, '-- Select target container type --')));
            root.appendChild(alertDiv('alert-warning',
                '<i class="fa fa-exclamation-triangle me-1"></i>' +
                'This will <strong>destroy and recreate</strong> each site\'s containers using the new image. The image will be pulled once if it isn\'t already present.'));
        } else if (action === 'change_resources') {
            const row = document.createElement('div'); row.className = 'row g-2';
            const c1 = document.createElement('div'); c1.className = 'col-md-6';
            const l1 = document.createElement('label'); l1.className = 'form-label'; l1.textContent = 'CPU per container (cores)';
            const i1 = document.createElement('input'); i1.type = 'number'; i1.step = '0.05'; i1.min = '0';
            i1.id = 'bulkCpuPerContainer'; i1.className = 'form-control'; i1.placeholder = 'leave blank for no change';
            c1.appendChild(l1); c1.appendChild(i1);
            const c2 = document.createElement('div'); c2.className = 'col-md-6';
            const l2 = document.createElement('label'); l2.className = 'form-label'; l2.textContent = 'Memory per container (MB)';
            const i2 = document.createElement('input'); i2.type = 'number'; i2.step = '1'; i2.min = '0';
            i2.id = 'bulkMemoryPerContainer'; i2.className = 'form-control'; i2.placeholder = 'leave blank for no change';
            c2.appendChild(l2); c2.appendChild(i2);
            row.appendChild(c1); row.appendChild(c2);
            root.appendChild(row);

            // PHP-FPM pm.max_children override. Three-way knob: leave blank = no
            // change; tick "Clear" = remove the override (revert to image formula);
            // type a number = set/replace. Only affects PHP / PHP-FPM container
            // types — non-PHP sites in the selection are no-op'd by the worker.
            const row2 = document.createElement('div'); row2.className = 'row g-2 mt-2';
            const c3 = document.createElement('div'); c3.className = 'col-md-6';
            const l3 = document.createElement('label'); l3.className = 'form-label';
            l3.appendChild(document.createTextNode('PHP-FPM '));
            const l3code = document.createElement('code'); l3code.textContent = 'pm.max_children';
            l3.appendChild(l3code);
            l3.appendChild(document.createTextNode(' override '));
            const l3hint = document.createElement('span'); l3hint.className = 'text-muted small';
            l3hint.textContent = '(PHP types only)';
            l3.appendChild(l3hint);
            const i3 = document.createElement('input'); i3.type = 'number'; i3.step = '1'; i3.min = '2'; i3.max = '50';
            i3.id = 'bulkFpmOverride'; i3.className = 'form-control'; i3.placeholder = 'leave blank for no change';
            c3.appendChild(l3); c3.appendChild(i3);
            const c4 = document.createElement('div'); c4.className = 'col-md-6 d-flex align-items-end';
            const fcWrap = document.createElement('div'); fcWrap.className = 'form-check';
            const i4 = document.createElement('input'); i4.type = 'checkbox';
            i4.id = 'bulkFpmOverrideClear'; i4.className = 'form-check-input';
            const l4 = document.createElement('label'); l4.className = 'form-check-label'; l4.htmlFor = 'bulkFpmOverrideClear';
            l4.textContent = 'Clear override (revert to image formula default)';
            fcWrap.appendChild(i4); fcWrap.appendChild(l4);
            // Mutual exclusion: ticking "Clear" disables the number; vice versa.
            i3.addEventListener('input', function() { if (i3.value) { i4.checked = false; i4.disabled = true; } else { i4.disabled = false; } });
            i4.addEventListener('change', function() { if (i4.checked) { i3.value = ''; i3.disabled = true; } else { i3.disabled = false; } });
            c4.appendChild(fcWrap);
            row2.appendChild(c3); row2.appendChild(c4);
            root.appendChild(row2);

            root.appendChild(helpText('Limits are written to the DB immediately. Recreate containers afterward (or chain into "Recreate Containers") for the change to take effect.'));
        } else if (action === 'toggle_auto_recreate') {
            const opts = [{ value: '1', label: 'Enabled (1)' }, { value: '0', label: 'Disabled (0)' }];
            root.appendChild(labelWrap('Set auto-recreate flag to:', selectFromList('bulkAutoRecreateValue', opts, null)));
        } else if (action === 'ssl_renew') {
            root.appendChild(alertDiv('alert-info',
                '<i class="fa fa-info-circle me-1"></i>' +
                'Cert renewal will be attempted per site. Already-valid certs may be re-issued depending on the configured renewal threshold; sites without SSL configured will be reported as failed.'));
        } else if (action === 'add_additional_domain') {
            const opts = availableDomains.map(function(d) {
                return { value: d.id, label: d.domain_name + (d.owner ? ' (' + d.owner + ')' : '') };
            });
            const wrap = labelWrap('Domain to link as an additional domain', selectFromList('bulkAddDomainId', opts, '-- Select domain to attach --'));
            wrap.appendChild(helpText('Coordinates with the per-site Additional Domains UX — uses the same source list. Re-running the site-edit save afterward will push HAProxy/SSL changes.'));
            root.appendChild(wrap);
        }
        return root;
    }

    function bindActionBarButtons() {
        document.querySelectorAll('.whp-bulk-action-bar [data-bulk-action]').forEach(function(btn) {
            btn.addEventListener('click', function() {
                const action  = btn.dataset.bulkAction;
                const checked = getCheckedBoxes();
                if (checked.length === 0) return;
                pendingAction = action;
                actionModalLabel.textContent = actionTitles[action] || 'Bulk Action';
                while (actionModalBody.firstChild) actionModalBody.removeChild(actionModalBody.firstChild);
                actionModalBody.appendChild(buildModalBody(action, checked));
                actionConfirmBtn.className = 'btn btn-primary';
                actionConfirmBtn.textContent = 'Confirm';
                if (action === 'delete_sites') {
                    actionConfirmBtn.className = 'btn btn-danger';
                    actionConfirmBtn.textContent = 'Permanently Delete';
                } else if (action === 'recreate_containers' || action === 'change_container_type') {
                    actionConfirmBtn.className = 'btn btn-warning';
                    actionConfirmBtn.textContent = 'Run';
                }
                bootstrap.Modal.getOrCreateInstance(actionModalEl).show();
            });
        });
    }
    bindActionBarButtons();

    actionConfirmBtn.addEventListener('click', function() {
        if (!pendingAction) return;
        const checked = getCheckedBoxes();
        if (checked.length === 0) return;

        const fd = new FormData();
        fd.append('csrf_token', nonce);
        fd.append('bulk_action', pendingAction);
        checked.forEach(function(cb) { fd.append('site_ids[]', cb.value); });

        if (pendingAction === 'delete_sites') {
            const conf = document.getElementById('bulkDeleteConfirmInput');
            if (!conf || conf.value !== 'DELETE') { alert('Type DELETE to confirm.'); return; }
        }
        if (pendingAction === 'change_container_type') {
            const sel = document.getElementById('bulkNewContainerTypeId');
            if (!sel || !sel.value) { alert('Please pick a container type.'); return; }
            fd.append('new_container_type_id', sel.value);
        }
        if (pendingAction === 'change_resources') {
            const cpu       = document.getElementById('bulkCpuPerContainer').value.trim();
            const mem       = document.getElementById('bulkMemoryPerContainer').value.trim();
            const fpmSetVal = document.getElementById('bulkFpmOverride').value.trim();
            const fpmClear  = document.getElementById('bulkFpmOverrideClear').checked;
            if (cpu === '' && mem === '' && fpmSetVal === '' && !fpmClear) {
                alert('Set at least one of CPU, memory, or the FPM override.');
                return;
            }
            if (cpu !== '')       fd.append('cpu_per_container', cpu);
            if (mem !== '')       fd.append('memory_per_container', mem);
            // Three-way override semantics:
            //   - clear ticked      → server sets column to NULL (revert to formula)
            //   - number filled     → server sets column to that value
            //   - both empty/false  → server doesn't touch the column
            if (fpmClear)         fd.append('fpm_max_children_override', 'clear');
            else if (fpmSetVal !== '') fd.append('fpm_max_children_override', fpmSetVal);
        }
        if (pendingAction === 'toggle_auto_recreate') {
            const sel = document.getElementById('bulkAutoRecreateValue');
            fd.append('auto_recreate_value', sel ? sel.value : '0');
        }
        if (pendingAction === 'add_additional_domain') {
            const sel = document.getElementById('bulkAddDomainId');
            if (!sel || !sel.value) { alert('Please pick a domain.'); return; }
            fd.append('domain_id', sel.value);
        }

        bootstrap.Modal.getOrCreateInstance(actionModalEl).hide();
        openResultsModal(checked.length);

        fetch('api/sites-bulk.php', { method: 'POST', body: fd })
            .then(function(r) { return r.json(); })
            .then(function(data) {
                if (data.status !== 'ok') {
                    while (resultsTbody.firstChild) resultsTbody.removeChild(resultsTbody.firstChild);
                    const tr = document.createElement('tr');
                    const td = document.createElement('td');
                    td.colSpan = 4; td.className = 'text-danger';
                    td.textContent = 'Error: ' + (data.message || 'unknown');
                    tr.appendChild(td); resultsTbody.appendChild(tr);
                    finalizeResults({ ok: 0, failed: 0, skipped: 0 }, checked.length);
                    return;
                }
                if (data.job_id) {
                    if (data.results) renderBulkResults(data.results);
                    pollBulkJob(data.job_id, checked.length);
                } else if (data.results) {
                    renderBulkResults(data.results);
                    finalizeResults(data.summary || computeSummary(data.results), data.results.length);
                }
            })
            .catch(function() {
                while (resultsTbody.firstChild) resultsTbody.removeChild(resultsTbody.firstChild);
                const tr = document.createElement('tr');
                const td = document.createElement('td');
                td.colSpan = 4; td.className = 'text-danger';
                td.textContent = 'Request failed.';
                tr.appendChild(td); resultsTbody.appendChild(tr);
                finalizeResults({ ok: 0, failed: 0, skipped: 0 }, checked.length);
            });
    });

    function computeSummary(rows) {
        const s = { ok: 0, failed: 0, skipped: 0 };
        rows.forEach(function(r) { if (r.status && s[r.status] !== undefined) s[r.status]++; });
        return s;
    }

    function openResultsModal(total) {
        while (resultsTbody.firstChild) resultsTbody.removeChild(resultsTbody.firstChild);
        resultsOk.textContent = '0';
        resultsFail.textContent = '0';
        resultsSkip.textContent = '0';
        resultsTotal.textContent = total;
        resultsBar.style.width = '0%';
        resultsBar.textContent = '0%';
        resultsBar.classList.add('progress-bar-animated', 'progress-bar-striped');
        resultsCloseBtn.disabled = true;
        resultsDoneBtn.disabled = true;
        // Cancel button is hidden until we know there's a job_id to cancel
        // (sync paths that return results inline don't have one).
        if (resultsCancelBtn) {
            resultsCancelBtn.style.display = 'none';
            resultsCancelBtn.disabled = false;
            resultsCancelBtn.innerHTML = '<i class="fa fa-stop me-1"></i>Cancel Job';
        }
        currentJobId = null;
        bootstrap.Modal.getOrCreateInstance(resultsModalEl).show();
    }

    function badgeFor(status) {
        const span = document.createElement('span');
        span.className = 'badge ' +
            (status === 'ok' ? 'bg-success' :
             status === 'failed' ? 'bg-danger' :
             status === 'skipped' ? 'bg-warning text-dark' : 'bg-secondary');
        span.textContent = status || '';
        return span;
    }

    function renderBulkResults(rows) {
        const sitesById = {};
        document.querySelectorAll('.site-select-cb').forEach(function(cb) {
            sitesById[cb.value] = cb.dataset.siteName || ('site#' + cb.value);
        });
        while (resultsTbody.firstChild) resultsTbody.removeChild(resultsTbody.firstChild);
        rows.forEach(function(r) {
            const tr = document.createElement('tr');
            const tdName   = document.createElement('td');
            tdName.textContent = sitesById[r.site_id] || ('site#' + r.site_id);
            const tdAction = document.createElement('td');
            const code = document.createElement('code'); code.className = 'small';
            code.textContent = r.action || '';
            tdAction.appendChild(code);
            const tdStatus = document.createElement('td');
            tdStatus.appendChild(badgeFor(r.status));
            const tdMsg = document.createElement('td');
            tdMsg.className = 'small';
            tdMsg.textContent = r.message || '';
            tr.appendChild(tdName); tr.appendChild(tdAction); tr.appendChild(tdStatus); tr.appendChild(tdMsg);
            resultsTbody.appendChild(tr);
        });
        const s = computeSummary(rows);
        resultsOk.textContent   = s.ok;
        resultsFail.textContent = s.failed;
        resultsSkip.textContent = s.skipped;
        const total = parseInt(resultsTotal.textContent, 10) || rows.length || 1;
        const done  = s.ok + s.failed + s.skipped;
        const pct   = Math.min(100, Math.round((done / total) * 100));
        resultsBar.style.width = pct + '%';
        resultsBar.textContent = pct + '%';
    }

    function finalizeResults(summary, total) {
        resultsBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
        resultsCloseBtn.disabled = false;
        resultsDoneBtn.disabled = false;
        // Job is terminal — hide the cancel button.
        if (resultsCancelBtn) resultsCancelBtn.style.display = 'none';
        currentJobId = null;
        if (summary) {
            resultsOk.textContent   = summary.ok || 0;
            resultsFail.textContent = summary.failed || 0;
            resultsSkip.textContent = summary.skipped || 0;
        }
        resultsBar.style.width = '100%';
        resultsBar.textContent = '100%';
    }

    function pollBulkJob(jobId, totalCount) {
        currentJobId = jobId;
        // Async job — surface the Cancel button now that we have a job_id.
        if (resultsCancelBtn) resultsCancelBtn.style.display = '';

        let stop = false;
        function tick() {
            if (stop) return;
            fetch('api/sites-bulk.php?bulk_action=status&job_id=' + encodeURIComponent(jobId))
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data && data.results) renderBulkResults(data.results);
                    if (data && data.done) {
                        stop = true;
                        finalizeResults(data.summary || null, totalCount);
                        return;
                    }
                    setTimeout(tick, 1500);
                })
                .catch(function() { setTimeout(tick, 3000); });
        }
        tick();
    }

    // Cancel button wires to api/sites-bulk.php?bulk_action=cancel_job. The
    // worker's pcntl_signal SIGTERM handler writes a cancellation row and
    // exits; the running pollBulkJob() picks up data.done = true on the next
    // tick and runs finalizeResults() — no need to stop the poll here.
    if (resultsCancelBtn) {
        resultsCancelBtn.addEventListener('click', function() {
            if (!currentJobId) return;
            if (!confirm('Stop this bulk job? Sites already processed will keep their changes; the rest will be marked cancelled.')) {
                return;
            }
            resultsCancelBtn.disabled = true;
            resultsCancelBtn.innerHTML = '<i class="fa fa-spinner fa-spin me-1"></i>Cancelling…';

            const fd = new FormData();
            fd.append('csrf_token', nonce);
            fd.append('bulk_action', 'cancel_job');
            fd.append('job_id', currentJobId);

            fetch('api/sites-bulk.php', { method: 'POST', body: fd })
                .then(function(r) { return r.json(); })
                .then(function(data) {
                    if (!data || data.status !== 'ok') {
                        alert('Cancel failed: ' + (data && data.message ? data.message : 'unknown'));
                        resultsCancelBtn.disabled = false;
                        resultsCancelBtn.innerHTML = '<i class="fa fa-stop me-1"></i>Cancel Job';
                    }
                    // On success we leave the button disabled+spinning; the
                    // polling tick will flip it back to hidden via finalize.
                })
                .catch(function() {
                    alert('Cancel request failed.');
                    resultsCancelBtn.disabled = false;
                    resultsCancelBtn.innerHTML = '<i class="fa fa-stop me-1"></i>Cancel Job';
                });
        });
    }
});
</script>
