// Backup page controller — restic-engine UI.
// Reads JSON snapshots written by scripts/webui/data/generators/backup/* and
// dispatches actions back into the task system (which calls bash CLI).


// Module-level schema/retention data moved to backup-schema.js (loaded first).

class BackupPage {
    constructor(opts = {}) {
        // Embedded in the fleet Overview's Backups tab: the same controller, but
        // it must not couple to the /backup URL (the fleet owns /overview/backups).
        this.embedded = !!opts.embedded;
        this.currentTab = 'dashboard';
        this.dashboard = null;
        this.locations = null;
        this.snapshotsByLoc = {};
        this.expandedLocs = new Set();
        this.engines = [];            // [{id,name,supported_types}, ...] — fetched once
        this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
        this.eventBound = false;
        this._taskRefreshTimer = null;
        this._ac = new AbortController();   // dispose() aborts this to remove the document listeners on unmount
    }

    async init() {
        this.currentTab = this.parseTabFromUrl() || this.currentTab;
        this.applyActiveTabUi(this.currentTab);
        this.bindEvents();
        await this.refreshAll();
        await window.Dismissible?.load();
        this.render();
        this.updatePageHeader();
        this.updatePrimaryAction();
    }

    /* Read the active tab slug from window.location, supporting both
       /backup?=dashboard (the legacy libreportal ?= form used on /config)
       and /backup?backup=dashboard (standard query string) so links from
       either source resolve correctly. */
    parseTabFromUrl() {
        if (this.embedded) return null;   // embedded: always open on Dashboard; sub-tabs are in-page only
        const allowed = new Set(['dashboard', 'backups', 'locations', 'configuration']);
        // Path-based: /backup/<tab> (bare /backup → default tab).
        const seg = window.location.pathname.replace(/^\/backup\/?/, '').split('/')[0];
        if (seg && allowed.has(seg)) return seg;
        // Legacy ?=<tab> / ?backup=<tab> / ?tab=<tab> for old links.
        const search = window.location.search || '';
        const legacy = search.match(/\?=([^&]+)/);
        if (legacy && allowed.has(legacy[1])) return legacy[1];
        const params = new URLSearchParams(search);
        const q = params.get('backup') || params.get('tab');
        if (q && allowed.has(q)) return q;
        return null;
    }

    /* Toggle the sidebar .active class + panel visibility without going
       through switchTab's URL-update path (used on initial render and
       browser back/forward). */
    applyActiveTabUi(tab) {
        document.querySelectorAll('.backup-layout .sidebar .category[data-backup-tab]').forEach(b => {
            b.classList.toggle('active', b.dataset.backupTab === tab);
        });
        document.querySelectorAll('.backup-tabpanel').forEach(p => {
            p.classList.toggle('active', p.id === `backup-panel-${tab}`);
        });
    }

    bindEvents() {
        if (this.eventBound) return;
        this.eventBound = true;

        // Backups/restores/deletes complete asynchronously with no live feed, so
        // repaint when one finishes (snapshot lists, last-backup times, sizes).
        // Registered with the task-refresh coordinator (single source of truth);
        // debounced because picking several apps queues a task each, finishing
        // in a burst. The Refresh button stays as a manual pull.
        window.taskRefresh?.register({
            id: 'backups',
            match: (d) => ['backup', 'restore', 'delete', 'delete_all'].includes(d.action)
                || /^libreportal\s+(backup|restore)\b/.test((d.task && d.task.command) || ''),
            run: () => {
                // Works whether this is the standalone page (window.backupPage) or
                // the instance embedded in the Overview Backups tab
                // (window.overviewBackupPage) — the hard-coded global alone would
                // make the embedded center's auto-refresh dead.
                if ((window.backupPage === this || window.overviewBackupPage === this) && document.getElementById('backup-page')) {
                    return this.refreshAll().then(() => this.render());
                }
            },
            debounceMs: 600,
        });

        // Browser back/forward is handled by the SPA's popstate listener —
        // pushTabToUrl includes a `route` field in state so the SPA's
        // handler picks it up and re-runs handleBackup, which re-parses
        // the URL via parseTabFromUrl() at init time.

        document.addEventListener('click', (e) => {
            // Clicking outside the export dropdown (and not on its trigger) closes it.
            const exportMenu = document.getElementById('backup-export-menu');
            if (exportMenu && !exportMenu.hidden
                && !e.target.closest('#backup-export-menu')
                && !e.target.closest('#backup-primary-action')) {
                this.toggleExportMenu(false);
            }

            const tabBtn = e.target.closest('.backup-layout .sidebar .category[data-backup-tab]');
            if (tabBtn) {
                this.switchTab(tabBtn.dataset.backupTab);
                return;
            }

            if (e.target.closest('#backup-refresh-btn')) {
                this.refreshAll().then(() => this.render());
                return;
            }

            if (e.target.closest('#backup-primary-action')) {
                this.handlePrimaryAction();
                return;
            }

            if (e.target.closest('[data-action="backup-system"]')) {
                this.runBackupSystem();
                return;
            }

            if (e.target.closest('[data-action="restore-system"]')) {
                this.confirmRestoreSystem();
                return;
            }

            // Deep-link from the Snapshots table → /app/<name>/backups?snapshot=<id>.
            // Routed via the SPA so the app page mounts in-place rather than a
            // full reload.
            const deepLink = e.target.closest('[data-deep-link]');
            if (deepLink) {
                e.preventDefault();
                if (window.navigateToRoute) window.navigateToRoute(deepLink.dataset.deepLink);
                else window.location.href = deepLink.dataset.deepLink;
                return;
            }

            // Tile actions, in priority order:
            //   1. "Backup now" pill on the tile → opens the pick modal
            //      preticked with that tile (explicit affordance, replaces
            //      the old implicit whole-tile click).
            //   2. Whole-tile click → navigates to the per-app Backups tab
            //      (or the system page for the System tile). This is the
            //      cohesion fix: each tile leads to the page that owns
            //      that subject's full detail, not a modal asking "do
            //      you want to back up?".
            const backupNowBtn = e.target.closest('[data-action="backup-now"]');
            if (backupNowBtn) {
                e.stopPropagation();
                if (backupNowBtn.dataset.system) {
                    this.openBackupPickModal({ preTickSystem: true });
                } else if (backupNowBtn.dataset.app) {
                    this.openBackupPickModal({ preTickApps: [backupNowBtn.dataset.app] });
                }
                return;
            }
            const tile = e.target.closest('.backup-app-tile');
            if (tile) {
                if (tile.dataset.system) {
                    // System has no dedicated page yet — keep the pick modal.
                    this.openBackupPickModal({ preTickSystem: true });
                } else if (tile.dataset.app && window.navigateToRoute) {
                    window.navigateToRoute(`/app/${encodeURIComponent(tile.dataset.app)}/backups`);
                }
                return;
            }

            if (e.target.closest('#backup-pick-confirm')) {
                this.confirmBackupPick();
                return;
            }

            const restoreBtn = e.target.closest('[data-action="restore-snapshot"]');
            if (restoreBtn) {
                e.stopPropagation();   // don't also toggle the row's details panel
                this.openRestoreModal(restoreBtn.dataset.app, restoreBtn.dataset.loc, restoreBtn.dataset.snapshot);
                return;
            }

            const deleteBtn = e.target.closest('[data-action="delete-snapshot"]');
            if (deleteBtn) {
                e.stopPropagation();
                this.openDeleteModal(deleteBtn.dataset.app, deleteBtn.dataset.loc, deleteBtn.dataset.snapshot);
                return;
            }

            // Row header / Details button → toggle the .task-details panel.
            // Matches the per-app Backups tab interaction.
            const snapToggle = e.target.closest('[data-action="toggle-snapshot-row"]');
            if (snapToggle) {
                const item = snapToggle.closest('.backup-snapshot-item');
                const details = item && item.querySelector('.task-details');
                if (details) details.classList.toggle('task-details-open');
                return;
            }

            const locEnable = e.target.closest('[data-action="toggle-location-enabled"]');
            if (locEnable) {
                const cb = locEnable.querySelector('input[type="checkbox"]');
                this.setLocationEnabled(parseInt(locEnable.dataset.loc, 10), cb ? cb.checked : true);
                return;
            }

            const locHeader = e.target.closest('[data-action="toggle-location"]');
            if (locHeader) {
                this.toggleLocationExpand(parseInt(locHeader.dataset.loc, 10));
                return;
            }

            const locSave = e.target.closest('[data-action="save-location"]');
            if (locSave) {
                this.saveInlineLocation(parseInt(locSave.dataset.loc, 10));
                return;
            }

            const locDelete = e.target.closest('[data-action="delete-location"]');
            if (locDelete) {
                this.deleteInlineLocation(parseInt(locDelete.dataset.loc, 10));
                return;
            }

            const sshSave = e.target.closest('[data-action="ssh-key-save"]');
            if (sshSave) { this.saveBackupSshKey(parseInt(sshSave.dataset.loc, 10)); return; }
            const sshGen = e.target.closest('[data-action="ssh-key-generate"]');
            if (sshGen) { this.generateBackupSshKey(parseInt(sshGen.dataset.loc, 10)); return; }
            const sshDel = e.target.closest('[data-action="ssh-key-delete"]');
            if (sshDel) { this.deleteBackupSshKey(parseInt(sshDel.dataset.loc, 10)); return; }
            const sshCopy = e.target.closest('[data-action="ssh-key-copy"]');
            if (sshCopy) { this.copyBackupSshKey(parseInt(sshCopy.dataset.loc, 10)); return; }

            const locTab = e.target.closest('[data-action="loc-tab"]');
            if (locTab) {
                const tabIdx = locTab.dataset.loc;
                const tabName = locTab.dataset.tab;
                const root = locTab.closest('.backup-location-config') || document;
                root.querySelectorAll(`[data-action="loc-tab"][data-loc="${tabIdx}"]`).forEach(b => {
                    const on = b === locTab;
                    b.classList.toggle('active', on);
                    b.setAttribute('aria-selected', on ? 'true' : 'false');
                });
                root.querySelectorAll(`[data-tab-panel][data-loc="${tabIdx}"]`).forEach(p => {
                    p.classList.toggle('active', p.dataset.tabPanel === tabName);
                });
                return;
            }

            if (e.target.closest('[data-close-modal]') || e.target.matches('.backup-modal')) {
                this.closeAllModals();
                return;
            }

            if (e.target.closest('#backup-restore-confirm')) { this.confirmRestore(); return; }
            if (e.target.closest('#backup-delete-confirm'))  { this.confirmDelete();  return; }
            if (e.target.closest('#backup-delete-location-confirm')) { this.confirmDeleteLocation(); return; }
            if (e.target.closest('#backup-add-location-confirm')) { this.confirmAddLocation(); return; }
            const engineBtn = e.target.closest('[data-action="open-engine-details"]');
            if (engineBtn) { this.openEngineDetailsModal(engineBtn); return; }

            const exportBtn = e.target.closest('[data-action="export-passwords"]');
            if (exportBtn) { this.toggleExportMenu(false); this.exportRepositoryPasswords(exportBtn); return; }

            const goToLocations = e.target.closest('[data-action="go-to-locations"]');
            if (goToLocations) { this.switchTab('locations'); return; }

            const dismissWarn = e.target.closest('[data-action="dismiss-config-warning"]');
            if (dismissWarn) {
                window.Dismissible?.dismiss('backup-config-warning');
                const banner = dismissWarn.closest('.backup-warning-banner');
                const divider = banner?.nextElementSibling;
                if (divider && divider.classList.contains('config-divider')) divider.remove();
                banner?.remove();
                return;
            }

            const saveBtn = e.target.closest('[data-backup-save]');
            if (saveBtn) {
                this.saveSection(saveBtn.dataset.backupSave);
                return;
            }
        }, { signal: this._ac.signal });

        document.addEventListener('input', (e) => {
            if (e.target.id === 'backup-snapshot-filter' || e.target.id === 'backup-snapshot-repo') {
                this.renderSnapshots();
            }
        }, { signal: this._ac.signal });

        // Type select changes refresh the visible connection fields inline.
        // Retention preset changes are handled by applyRetentionPreset, which
        // already updates CUSTOM_RETENTION too — no extra toggle wiring needed.
        document.addEventListener('change', (e) => {
            const detailsScope = e.target.closest('.backup-location-row .task-details');
            if (detailsScope) {
                const locIdx = parseInt(detailsScope.dataset.loc, 10);
                if (e.target.matches('[name$="_TYPE"]')) {
                    this.refreshInlineTypeFields(locIdx, e.target.value);
                }
                if (e.target.matches('[name$="_SSH_AUTH"]')) {
                    this.applySshAuthVisibility(detailsScope);
                }
                if (e.target.matches('[name$="_PATH_MODE"]')) {
                    this.applyPathModeVisibility(detailsScope);
                }
            }
            const presetSel = e.target.closest('[data-retention-preset]');
            if (presetSel) {
                this.applyRetentionPreset(presetSel);
            }
        }, { signal: this._ac.signal });
    }

    /* Release everything this page attached to document/window so navigating
       away (kernel unmount) can't leave stale listeners firing on the live DOM —
       the cause of the backup sidebar "content stacks on revisit" bug. Called by
       the feature module's unmount(). */
    dispose() {
        try { this._ac && this._ac.abort(); } catch (_) {}
        try { window.taskRefresh && window.taskRefresh.unregister('backups'); } catch (_) {}
        if (this._taskRefreshTimer) { clearTimeout(this._taskRefreshTimer); this._taskRefreshTimer = null; }
    }



    /* Load the unified config file once for the Locations editor: configData
       carries field metadata (titles/descriptions/options/advanced) the editor
       renders from; systemConfigs is the flat key->value map used for default
       lookups (e.g. CFG_BACKUP_ENGINE) and save-time change detection. */




    switchTab(tab, opts = {}) {
        if (!tab || tab === this.currentTab) return;
        this.currentTab = tab;
        this.applyActiveTabUi(tab);
        this.updatePageHeader();
        this.updatePrimaryAction();
        if (!opts.fromPopstate) this.pushTabToUrl(tab);
    }

    pushTabToUrl(tab) {
        if (this.embedded) return;        // embedded: keep the URL at /overview/backups (no sub-tab coupling)
        const url = `/backup/${tab}`;
        // Use replaceState for the *first* push (initial tab inferred from
        // URL); otherwise pushState so back/forward navigates between tabs.
        if (!this._pushedAnyTab) {
            window.history.replaceState({ backupTab: tab, route: url }, '', url);
            this._pushedAnyTab = true;
        } else {
            window.history.pushState({ backupTab: tab, route: url }, '', url);
        }
    }

    updatePageHeader() {
        const titleEl = document.getElementById('backup-section-title');
        const subEl = document.getElementById('backup-section-subtitle');
        const iconEl = document.getElementById('backup-page-header-icon');
        if (titleEl) titleEl.textContent = this.titleFor(this.currentTab);
        if (subEl)   subEl.textContent = this.subtitleFor(this.currentTab);
        if (iconEl)  iconEl.innerHTML = this.iconFor(this.currentTab);
    }

    titleFor(tab) {
        return {
            dashboard:     'Dashboard',
            backups:       'Backups',
            locations:     'Locations',
            configuration: 'Configuration'
        }[tab] || 'Backups';
    }

    subtitleFor(tab) {
        return {
            dashboard:     "Check what's protected — and when it last ran.",
            backups:       'Every backup across every enabled location.',
            locations:     'Where backups are stored. Add, edit, or remove destinations.',
            configuration: 'Schedule, retention, and engine settings.'
        }[tab] || '';
    }

    iconFor(tab) {
        const icons = {
            dashboard:
                '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
                '<rect x="3" y="3" width="7" height="9"></rect>' +
                '<rect x="14" y="3" width="7" height="5"></rect>' +
                '<rect x="14" y="12" width="7" height="9"></rect>' +
                '<rect x="3" y="16" width="7" height="5"></rect></svg>',
            backups:
                '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
                '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>' +
                '<polyline points="17 8 12 3 7 8"></polyline>' +
                '<line x1="12" y1="3" x2="12" y2="15"></line></svg>',
            locations:
                '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
                '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>' +
                '<circle cx="12" cy="10" r="3"></circle></svg>',
            configuration:
                '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">' +
                '<circle cx="12" cy="12" r="3"></circle>' +
                '<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>'
        };
        return icons[tab] || icons.backups;
    }

    updatePrimaryAction() {
        const btn = document.getElementById('backup-primary-action');
        if (!btn) return;
        // Switching tabs always closes the export dropdown.
        this.toggleExportMenu(false);
        if (this.currentTab === 'locations') {
            btn.style.display = '';
            btn.innerHTML = `
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <line x1="12" y1="5" x2="12" y2="19"></line>
                    <line x1="5" y1="12" x2="19" y2="12"></line>
                </svg>
                Add location
            `;
            btn.dataset.intent = 'add-location';
            btn.removeAttribute('aria-haspopup');
        } else if (this.currentTab === 'configuration') {
            btn.style.display = '';
            btn.innerHTML = `
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                </svg>
                Export
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <polyline points="6 9 12 15 18 9"></polyline>
                </svg>
            `;
            btn.dataset.intent = 'export-menu';
            btn.setAttribute('aria-haspopup', 'menu');
        } else if (this.currentTab === 'backups') {
            btn.style.display = '';
            btn.innerHTML = `
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="17 8 12 3 7 8"></polyline>
                    <line x1="12" y1="3" x2="12" y2="15"></line>
                </svg>
                Backup all apps
            `;
            btn.dataset.intent = 'backup-all';
            btn.removeAttribute('aria-haspopup');
        } else {
            // Dashboard and Migrate have no header primary action.
            btn.style.display = 'none';
            btn.removeAttribute('aria-haspopup');
        }
    }

    handlePrimaryAction() {
        const intent = document.getElementById('backup-primary-action')?.dataset.intent;
        if (intent === 'add-location') {
            this.openAddLocationModal();
        } else if (intent === 'export-menu') {
            this.toggleExportMenu();
        } else {
            this.runBackupAllApps();
        }
    }

    toggleExportMenu(force) {
        const menu = document.getElementById('backup-export-menu');
        const btn = document.getElementById('backup-primary-action');
        if (!menu) return;
        const show = typeof force === 'boolean' ? force : menu.hidden;
        menu.hidden = !show;
        if (btn) btn.setAttribute('aria-expanded', show ? 'true' : 'false');
    }

    render() {
        this.renderDashboard();
        this.renderLocations();
        this.renderSnapshots();
        this.renderConfiguration();
    }




    /* Look up the icon + display name from window.apps the same way the
       dashboard and tasks page do. Falls back to the default app icon and
       a capitalised slug if the app isn't in the cached list. */
    appMeta(slug) {
        const apps = window.apps || [];
        const match = apps.find(a => {
            const command = a.command || '';
            return command.endsWith(` ${slug}`) || a.name?.toLowerCase() === slug.toLowerCase();
        });
        let icon = match?.icon || '/core/icons/apps/default.svg';
        if (!icon.startsWith('/')) icon = '/' + icon;
        const displayName = (typeof window.getAppDisplayName === 'function')
            ? window.getAppDisplayName(slug)
            : (slug.charAt(0).toUpperCase() + slug.slice(1));
        return { icon, displayName };
    }




    /* Inline-SVG icon for a location's backend type. Local gets the disk
       (stack of platters) glyph; everything else gets a cloud — that's
       the visual line between "lives on this box" and "lives somewhere else." */




    /* Hide the SSH password field when SSH auth = key, show it when = password.
       Applied at expand time and whenever the SSH_AUTH select changes. */

    /* Hide the custom PATH input when PATH_MODE=auto, show when =custom. */

    /* Trim the per-location ENGINE select to only engines whose
       supported_types include the location's current TYPE. If the currently
       saved engine isn't compatible, fall back to the first compatible one. */











    /* Post-render polish on the dynamic /config render: wrap the five raw
       retention number fields in a persona-preset dropdown. The five inputs
       stay in the DOM (so /config's save flow captures them unchanged) but
       are hidden under "Custom…" by default. */

    /* Build the <option> list for a "Backup style" dropdown. The scope's
       default preset is tagged "(default)" and floated to the top; the rest
       follow in declared order. inherit-global only makes sense per-location
       (there's nothing to inherit at the global level), so it's omitted when
       includeInherit is false. */

    /* Retention preset dropdown + hidden underlying fields.
       `prefix` is the CFG name prefix, e.g. 'CFG_BACKUP_' or 'CFG_BACKUP_LOC_3_'.
       When `includeInherit` is true (per-location scope), an "Inherit global"
       option is added at the top and an extra hidden CUSTOM_RETENTION field is
       written: false when inherit, true otherwise. The five raw KEEP_* inputs
       are always rendered (so the save flow captures them) but hidden until
       "Custom…" is selected. */





    /* Append a "Details" button next to every Engine field (global or
       per-location). The button reads its engine id from a sibling input
       at click time so per-location selects work even before save. */




    /* ----- Location modal (edit / add) ----- */




    /* Render a list of CFG_BACKUP_LOC_${idx}_${suffix} fields via the same
       ConfigShared.generateField machinery /config uses, so widgets and
       styling match pixel-for-pixel. Values are picked up from the location
       object (locations.json) using the camelCase mirrors of each suffix. */

    /* Resolve a location field's metadata. Source of truth is configs.json
       (window.configData) — titles/descriptions/options + a per-field "advanced"
       flag; BACKUP_LOC_FIELD_DEFS is the fallback for sparse location.configs.
       LOC_ADVANCED_SUFFIXES keeps the known overrides advanced even on legacy
       locations whose config predates the **ADVANCED** marker. */

    /* Ordered field list for a location type. Primary source is the generator-
       emitted schema.json (this.locSchema); BACKUP_LOC_FIELDS_BY_TYPE is the
       fallback if that file didn't load. */

    /* Split a type's fields into the Connection tab vs the Advanced tab. */

    /* Connection-tab body: the generic fields plus, for sftp, the SSH key card.
       Used both on first render and when the Type select changes. */

    /* SSH key card for an sftp location. LibrePortal holds the private key in
       the location's ssh.key file; only the public key is shown — that's what
       you paste into the remote server's authorized_keys. Hidden by
       applySshAuthVisibility when SSH auth = password. */








    /* ----- Add location modal ----- */



    /* ----- Snapshot restore/delete modals ----- */



    locName(idx) {
        const l = (this.locations?.locations || []).find(x => String(x.idx) === String(idx));
        return l?.name || `Location ${idx}`;
    }

    closeAllModals() {
        document.querySelectorAll('.backup-modal.open').forEach(m => m.classList.remove('open'));
    }






    /* ----- Back-up checklist modal -----
       Triggered by clicking any tile in the Backup status grid. Lists System
       config first (special row, key="__system__"), then every installed app.
       Pre-ticks the tile that was clicked. Confirm queues one backup task
       per ticked item — except when EVERYTHING is ticked, in which case we
       collapse to `libreportal backup all` (which also runs `backup system`
       under the hood) so we only queue one task instead of N. */



    /* ----- Migrate (Phase 1: shared-backup) ----- */





    async runTask(command, type, app) {
        if (!this.taskManager) {
            this.notify('Task system unavailable', 'error');
            return;
        }
        try {
            await this.taskManager.createTask(command, type, app);
            setTimeout(() => this.refreshAll().then(() => this.render()), 1500);
        } catch (err) {
            this.notify(`Failed to queue task: ${err.message || err}`, 'error');
        }
    }

    /* ----- Generic save handler ----- */


    async saveSection(sectionId) {
        let scope;
        if (sectionId.startsWith('location-')) {
            const idx = sectionId.slice('location-'.length);
            scope = document.querySelector(`.backup-location-row[data-loc="${idx}"] .task-details`);
        } else {
            scope = document.querySelector(`#backup-panel-${sectionId}`);
        }
        if (!scope) return;

        const cfg = window.systemConfigs || {};
        const changes = [];
        scope.querySelectorAll('[data-backup-field]').forEach(el => {
            const name = el.name;
            if (!name || name.startsWith('__')) return;
            let value;
            if (el.hasAttribute('data-backup-bool')) {
                value = el.checked ? 'true' : 'false';
            } else {
                value = (el.value ?? '').toString();
            }
            const original = (cfg[name] ?? '').toString();
            if (value === original) return;
            changes.push(`${name}=${value.replace(/\|/g, '%7C')}`);
        });

        if (!changes.length) {
            this.notify('No changes to save.', 'info');
            return;
        }

        const encoded = changes.join('|');
        try {
            if (!window.tasksManager?.router) throw new Error('Task system not available');
            await window.tasksManager.router.routeAction('config_update', {
                changes: `'${encoded.replace(/'/g, "'\\''")}'`
            });
            this.notify(`Saving ${changes.length} change${changes.length === 1 ? '' : 's'}…`, 'success');
            setTimeout(() => this.reloadAfterSave(), 2500);
        } catch (err) {
            this.notify(`Save failed: ${err.message || err}`, 'error');
        }
    }


    notify(message, type) {
        if (window.notificationSystem) {
            window.notificationSystem.show(message, type || 'info');
        } else {
            console.log(`[backup ${type || 'info'}] ${message}`);
        }
    }

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

    formatBytes(b) {
        if (!b || b < 0) return '0 B';
        const units = ['B', 'KB', 'MB', 'GB', 'TB'];
        let i = 0;
        let v = b;
        while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
        return `${v.toFixed(v < 10 ? 2 : 1)} ${units[i]}`;
    }

    formatRelative(iso) {
        if (!iso) return '—';
        const t = new Date(iso).getTime();
        if (!t) return iso;
        const diff = Math.max(0, Date.now() - t);
        const s = Math.floor(diff / 1000);
        if (s < 60)  return 'just now';
        const m = Math.floor(s / 60);
        if (m < 60)  return `${m}m ago`;
        const h = Math.floor(m / 60);
        if (h < 48)  return `${h}h ago`;
        const d = Math.floor(h / 24);
        if (d < 30)  return `${d}d ago`;
        return new Date(iso).toLocaleDateString();
    }

}

window.BackupPage = BackupPage;
