// Peers page controller. List + add + remove peer records. The data behind
// it is generated by scripts/webui/data/generators/peers/webui_peers.sh and
// served at /data/peers/generated/peers.json — Phase 2 only knows about
// kind=backup-channel; the other kinds light up in Phase 3.

class PeersPage {
    constructor(rootId = 'config-section') {
        // rootId is the container the page renders into when embedded under
        // /admin/tools/peers. The peers-* element IDs are unique enough that
        // global document.getElementById queries still find them inside the
        // injected template — but keeping the ref for future scoped queries.
        this.rootId = rootId;
        this.peers = [];
        this.backupLocations = [];        // populated for the loc_idx dropdown
        this.taskManager = (typeof TaskManager !== 'undefined') ? new TaskManager() : null;
        this.eventBound = false;
        this._ac = new AbortController();
    }

    async init() {
        this.bindEvents();
        await this.refreshAll();
        this.render();
    }

    async refreshAll() {
        const ts = Date.now();
        const [peersData, backupLocs] = await Promise.all([
            this.fetchJson(`/data/peers/generated/peers.json?t=${ts}`),
            this.fetchJson(`/data/backup/generated/locations.json?t=${ts}`)
        ]);
        this.peers = peersData?.peers || [];
        this.backupLocations = (backupLocs?.locations || []).filter(l => l.enabled);
    }

    async fetchJson(url) {
        try { const r = await fetch(url); if (!r.ok) return null; return await r.json(); }
        catch { return null; }
    }

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

        document.addEventListener('click', (e) => {
            if (e.target.closest('#peers-refresh-btn')) {
                this.runTask('libreportal regen webui --force', 'webui', null);
                setTimeout(() => this.refreshAll().then(() => this.render()), 2000);
                return;
            }
            if (e.target.closest('#peers-add-btn'))     { this.openAddModal();   return; }
            if (e.target.closest('#peers-add-confirm')) { this.confirmAdd();      return; }
            if (e.target.closest('#peers-token-btn'))   { this.openTokenModal();  return; }
            if (e.target.closest('#peers-pair-btn'))    { this.openPairModal();   return; }
            if (e.target.closest('#peers-pair-confirm')){ this.confirmPair();     return; }
            if (e.target.closest('#peers-pull-confirm')){ this.confirmPull();     return; }
            const removeBtn = e.target.closest('[data-action="peer-remove"]');
            if (removeBtn) { this.removePeer(removeBtn.dataset.name); return; }
            const checkBtn = e.target.closest('[data-action="peer-check"]');
            if (checkBtn) { this.checkPeer(checkBtn.dataset.name); return; }
            const pullBtn = e.target.closest('[data-action="peer-pull"]');
            if (pullBtn) {
                this.openPullModal(pullBtn.dataset.peer, pullBtn.dataset.app);
                return;
            }
            const appsBtn = e.target.closest('[data-action="peer-apps"]');
            if (appsBtn) {
                this.fetchAndShowPeerApps(appsBtn.dataset.name);
                return;
            }
            if (e.target.closest('[data-close-modal]') || e.target.matches('.backup-modal')) {
                this.closeAllModals();
                return;
            }
        }, { signal: this._ac.signal });
    }

    dispose() { try { this._ac && this._ac.abort(); } catch (_) {} }

    render() {
        const list  = document.getElementById('peers-list');
        const empty = document.getElementById('peers-empty');
        if (!list || !empty) return;

        if (!this.peers.length) {
            list.innerHTML = '';
            empty.hidden = false;
            return;
        }
        empty.hidden = true;

        list.innerHTML = this.peers.map(p => this.renderPeerCard(p)).join('');
    }

    renderPeerCard(peer) {
        const cfg = peer.config || {};
        const cfgSummary = this.summariseConfig(peer.kind, cfg);
        const statusClass = {
            ok: 'ok',
            'no-snapshots': 'warn',
            'config-error': 'fail',
            'not-yet-implemented': 'warn',
            'unknown-kind': 'fail',
            unknown: 'none'
        }[peer.status] || 'none';
        const statusLabel = {
            ok: 'Reachable',
            'no-snapshots': 'No recent snapshots',
            'config-error': 'Config error',
            'not-yet-implemented': 'Not yet supported',
            'unknown-kind': 'Unknown kind',
            unknown: 'Not checked'
        }[peer.status] || peer.status;

        return `
            <div class="backup-location-row" style="margin-bottom:10px; padding:14px 18px; background:var(--surface-2, #1a1a1a); border-radius:8px">
                <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:14px">
                    <div style="min-width:0; flex:1">
                        <div style="display:flex; align-items:center; gap:10px; margin-bottom:4px">
                            <strong style="font-size:1.1em">${this.escape(peer.name)}</strong>
                            <span class="backup-card-hint" style="opacity:.7">(${this.escape(peer.kind)})</span>
                            <span class="backup-status-dot ${statusClass}" title="${this.escape(statusLabel)}"></span>
                            <span class="backup-card-hint" style="font-size:.8em">${this.escape(statusLabel)}</span>
                        </div>
                        <div class="backup-card-hint" style="font-size:.9em">${cfgSummary}</div>
                        ${peer.last_seen ? `<div class="backup-card-hint" style="font-size:.78em; margin-top:4px">Checked ${this.escape(this.formatRelativeTime(peer.last_seen))}</div>` : ''}
                    </div>
                    <div style="display:flex; gap:8px; flex-shrink:0">
                        <button class="backup-secondary-btn" data-action="peer-check" data-name="${this.escape(peer.name)}">Check</button>
                        ${peer.kind === 'direct-ssh-direct' ? `
                            <button class="backup-secondary-btn" data-action="peer-apps" data-name="${this.escape(peer.name)}">List apps</button>
                        ` : ''}
                        <button class="backup-danger-btn" data-action="peer-remove" data-name="${this.escape(peer.name)}">Remove</button>
                    </div>
                </div>
                <div class="peers-apps-zone" data-peer="${this.escape(peer.name)}" hidden style="margin-top:12px"></div>
            </div>
        `;
    }

    /* --- pairing wizard --- */

    async openTokenModal() {
        const modal = document.getElementById('peers-token-modal');
        const body  = document.getElementById('peers-token-modal-body');
        if (!modal || !body) return;
        body.innerHTML = `<p class="backup-card-hint">Fetching token…</p>`;
        modal.classList.add('open');
        // Run via the task system so the keypair generation (first-time only)
        // surfaces in the task log if it fails.
        const id = await this.runTaskCapture('libreportal peer token', 'peer', null);
        const token = await this.readTaskOutput(id);
        if (!token || !token.trim().startsWith('lp-peer:')) {
            body.innerHTML = `<p style="color:var(--danger,#dc2626)">Could not generate token. Check the task log.</p>`;
            return;
        }
        const trimmed = token.trim();
        body.innerHTML = `
            <p>Copy this token and paste it into the other LibrePortal's <strong>Pair with token</strong> dialog. Symmetric — you'll need to do the same with theirs.</p>
            <textarea class="form-control" readonly style="width:100%; min-height:140px; font-family:monospace; font-size:.85em; word-break:break-all">${this.escape(trimmed)}</textarea>
            <p class="backup-card-hint" style="margin-top:8px">
                Anyone with this token can SSH in as the manager user with a forced-command (only the safe peer-shell verbs).
                Treat it like a strong password — share only over a channel you trust (encrypted chat, password manager).
            </p>
        `;
    }

    openPairModal() {
        const modal = document.getElementById('peers-pair-modal');
        const body  = document.getElementById('peers-pair-modal-body');
        if (!modal || !body) return;
        body.innerHTML = `
            <div class="backup-form-grid">
                <div>
                    <label for="peer-pair-token">Pairing token from the other LibrePortal</label>
                    <textarea class="form-control" id="peer-pair-token" placeholder="lp-peer:v1:…" style="min-height:120px; font-family:monospace; font-size:.85em"></textarea>
                </div>
                <div>
                    <label for="peer-pair-name">Local label (optional)</label>
                    <input type="text" class="form-control" id="peer-pair-name" placeholder="Override the name in the token">
                    <span class="backup-card-hint">Only set this if the token's name would collide with an existing peer here.</span>
                </div>
            </div>
            <p class="backup-card-hint" style="margin-top:8px">
                Accepting authorizes the originator to call peer-shell as the manager user (locked-down: no shell, no
                forwarding, only the whitelisted verbs). A peer record is also created so this host can pull from them.
            </p>
        `;
        modal.classList.add('open');
    }

    async confirmPair() {
        const modal = document.getElementById('peers-pair-modal');
        if (!modal) return;
        const token = modal.querySelector('#peer-pair-token')?.value?.trim();
        const override = modal.querySelector('#peer-pair-name')?.value?.trim();
        if (!token || !token.startsWith('lp-peer:')) {
            this.notify('That doesn\'t look like a pairing token (should start with lp-peer:).', 'error');
            return;
        }
        this.closeAllModals();
        const cmd = override
            ? `libreportal peer pair '${token.replace(/'/g, "'\\''")}' ${override}`
            : `libreportal peer pair '${token.replace(/'/g, "'\\''")}'`;
        await this.runTask(cmd, 'peer', null);
        setTimeout(() => this.refreshAll().then(() => this.render()), 2000);
    }

    /* --- direct-ssh peer: list-apps + pull --- */

    async fetchAndShowPeerApps(peerName) {
        const zone = document.querySelector(`.peers-apps-zone[data-peer="${CSS.escape(peerName)}"]`);
        if (!zone) return;
        zone.hidden = false;
        zone.innerHTML = `<p class="backup-card-hint">Fetching app list…</p>`;
        const id = await this.runTaskCapture(`libreportal peer apps ${peerName}`, 'peer', null);
        const out = await this.readTaskOutput(id);
        let parsed;
        try { parsed = JSON.parse(out); } catch { parsed = null; }
        if (!parsed || !Array.isArray(parsed.apps)) {
            zone.innerHTML = `<p style="color:var(--danger,#dc2626)">No app list — peer unreachable or returned malformed JSON. See task log.</p>`;
            return;
        }
        if (!parsed.apps.length) {
            zone.innerHTML = `<p class="backup-card-hint">${this.escape(peerName)} has no apps installed.</p>`;
            return;
        }
        zone.innerHTML = `
            <div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:6px">
                ${parsed.apps.map(a => `
                    <div style="display:flex; justify-content:space-between; align-items:center; padding:6px 10px; background:var(--surface-2, #1a1a1a); border-radius:6px">
                        <div>
                            <strong>${this.escape(a.slug)}</strong>
                            <span class="backup-card-hint" style="font-size:.78em; display:block">${this.formatBytes((a.size_kb || 0) * 1024)}</span>
                        </div>
                        <button class="backup-primary-btn" data-action="peer-pull" data-peer="${this.escape(peerName)}" data-app="${this.escape(a.slug)}">Pull</button>
                    </div>
                `).join('')}
            </div>
        `;
    }

    openPullModal(peer, app) {
        const modal = document.getElementById('peers-pull-modal');
        const body  = document.getElementById('peers-pull-modal-body');
        if (!modal || !body) return;
        body.innerHTML = `
            <p>Pull <strong>${this.escape(app)}</strong> from peer <strong>${this.escape(peer)}</strong>?</p>
            <p class="backup-card-hint">Streams the app's live data over SSH (via the peer-shell forced-command) and replaces this host's copy.</p>
            <div style="margin-top:14px; display:flex; flex-direction:column; gap:8px">
                <label style="display:flex; align-items:flex-start; gap:8px; cursor:pointer">
                    <input type="checkbox" id="peer-pull-opt-pre-backup" checked>
                    <span>Back up the destination's existing copy first
                        <span class="backup-card-hint" style="display:block; font-size:.85em">Safety net — snapshots ${this.escape(app)} into your first enabled backup location, tagged <code>pre-migrate</code>.</span>
                    </span>
                </label>
                <label style="display:flex; align-items:flex-start; gap:8px; cursor:pointer">
                    <input type="checkbox" id="peer-pull-opt-rewrite" checked>
                    <span>Rewrite host-bound URLs to this host
                        <span class="backup-card-hint" style="display:block; font-size:.85em">Replaces CFG_*_URL / *_DOMAIN / *_HOSTNAME with this host's values after the transfer.</span>
                    </span>
                </label>
            </div>
        `;
        modal.dataset.peer = peer;
        modal.dataset.app = app;
        modal.classList.add('open');
    }

    async confirmPull() {
        const modal = document.getElementById('peers-pull-modal');
        if (!modal) return;
        const { peer, app } = modal.dataset;
        const preBackup = document.getElementById('peer-pull-opt-pre-backup')?.checked;
        const rewrite   = document.getElementById('peer-pull-opt-rewrite')?.checked;
        const opts = [];
        if (preBackup === false) opts.push('--no-pre-backup');
        if (rewrite   === false) opts.push('--keep-urls');
        const optStr = opts.length ? ' ' + opts.join(' ') : '';
        this.closeAllModals();
        await this.runTask(`libreportal peer pull ${peer} ${app}${optStr}`, 'peer', app);
    }

    /* --- task-capture helpers (used by token/apps which need the output) --- */

    async runTaskCapture(command, type, app) {
        // Same as runTask but returns the task id so the caller can read its log.
        if (!this.taskManager) {
            this.notify('Task system unavailable', 'error');
            return null;
        }
        try {
            const t = await this.taskManager.createTask(command, type, app);
            return t?.id || null;
        } catch (err) {
            this.notify(`Failed to queue task: ${err.message || err}`, 'error');
            return null;
        }
    }

    async readTaskOutput(taskId, maxWaitMs = 8000) {
        // Poll the task's log until it completes or we hit a cap. Fast verbs
        // (token, apps) finish in well under a second; this is the cheapest
        // way to surface their stdout without subscribing to SSE.
        if (!taskId) return '';
        const start = Date.now();
        while (Date.now() - start < maxWaitMs) {
            try {
                const r = await fetch(`/api/tasks/${taskId}`);
                if (r.ok) {
                    const t = await r.json();
                    if (t.status === 'completed' || t.status === 'failed') {
                        const lr = await fetch(`/api/tasks/${taskId}/log`);
                        if (lr.ok) return await lr.text();
                        return '';
                    }
                }
            } catch { /* network blip — retry */ }
            await new Promise(res => setTimeout(res, 250));
        }
        return '';
    }

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

    summariseConfig(kind, cfg) {
        if (kind === 'backup-channel') {
            const host = cfg.hostname ? `hostname=<code>${this.escape(cfg.hostname)}</code>` : '<em>no hostname set</em>';
            const loc  = cfg.loc_idx != null ? `location <code>${this.escape(String(cfg.loc_idx))}</code>` : 'any enabled location';
            return `${host} · ${loc}`;
        }
        // direct-ssh kinds: minimal placeholder until Phase 3.
        return Object.keys(cfg).length
            ? Object.entries(cfg).map(([k, v]) => `${this.escape(k)}=<code>${this.escape(String(v))}</code>`).join(' · ')
            : '<em>(no config)</em>';
    }

    openAddModal() {
        const modal = document.getElementById('peers-add-modal');
        const body  = document.getElementById('peers-add-modal-body');
        if (!modal || !body) return;

        const locOptions = ['<option value="">Any enabled location</option>']
            .concat(this.backupLocations.map(l =>
                `<option value="${l.idx}">${this.escape(l.name || `Location ${l.idx}`)} (idx ${l.idx})</option>`))
            .join('');

        body.innerHTML = `
            <div class="backup-form-grid">
                <div>
                    <label for="peer-add-name">Name</label>
                    <input type="text" class="form-control" id="peer-add-name" placeholder="e.g. homelab">
                    <span class="backup-card-hint">Used as the label everywhere — letters, digits, ._- only.</span>
                </div>
                <div>
                    <label for="peer-add-kind">Kind</label>
                    <select class="form-control" id="peer-add-kind">
                        <option value="backup-channel" selected>Backup channel (shared backup location)</option>
                        <option value="direct-ssh-direct" disabled>Direct SSH (Phase 3)</option>
                        <option value="direct-ssh-via-relay" disabled>Via Connect relay (Phase 3b)</option>
                    </select>
                </div>
                <div>
                    <label for="peer-add-hostname">Source hostname</label>
                    <input type="text" class="form-control" id="peer-add-hostname" placeholder="Hostname as it appears in restic snapshots">
                    <span class="backup-card-hint">Match the source LibrePortal's CFG_INSTALL_NAME (or hostname if that's unset).</span>
                </div>
                <div>
                    <label for="peer-add-loc">Preferred location</label>
                    <select class="form-control" id="peer-add-loc">${locOptions}</select>
                    <span class="backup-card-hint">Pinning a location skips probing the others for snapshots.</span>
                </div>
            </div>
        `;
        modal.classList.add('open');
    }

    async confirmAdd() {
        const modal = document.getElementById('peers-add-modal');
        if (!modal) return;
        const name = modal.querySelector('#peer-add-name')?.value?.trim();
        const kind = modal.querySelector('#peer-add-kind')?.value || 'backup-channel';
        const host = modal.querySelector('#peer-add-hostname')?.value?.trim();
        const loc  = modal.querySelector('#peer-add-loc')?.value;

        if (!name) { this.notify('Name is required.', 'error'); return; }
        if (!host) { this.notify('Hostname is required for backup-channel peers.', 'error'); return; }
        if (!/^[A-Za-z0-9._-]+$/.test(name)) {
            this.notify('Name must use letters, digits, dot, underscore or dash.', 'error');
            return;
        }

        this.closeAllModals();
        const cfgPairs = [`hostname=${host}`];
        if (loc) cfgPairs.push(`loc_idx=${loc}`);
        const cmd = `libreportal peer add ${name} ${kind} ${cfgPairs.join(' ')}`;
        await this.runTask(cmd, 'peer', null);
        setTimeout(() => this.refreshAll().then(() => this.render()), 1500);
    }

    async removePeer(name) {
        if (!confirm(`Remove peer "${name}"?\n\nThis only removes the local label — backups and the other host are untouched.`)) return;
        await this.runTask(`libreportal peer remove ${name}`, 'peer', null);
        setTimeout(() => this.refreshAll().then(() => this.render()), 1500);
    }

    async checkPeer(name) {
        await this.runTask(`libreportal peer check ${name}`, 'peer', null);
        setTimeout(() => this.refreshAll().then(() => this.render()), 2000);
    }

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

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

    notify(message, kind) {
        if (window.notificationSystem && window.notificationSystem.show) window.notificationSystem.show(message, kind || 'info');
        else if (kind === 'error') console.error(message);
        else console.log(message);
    }

    formatRelativeTime(iso) {
        if (!iso) return 'never';
        const t = Date.parse(iso);
        if (!t) return iso;
        const diff = Date.now() - t;
        const minute = 60_000, hour = 60 * minute, day = 24 * hour;
        if (diff < hour)    return `${Math.max(1, Math.round(diff / minute))} min ago`;
        if (diff < day)     return `${Math.round(diff / hour)} h ago`;
        if (diff < 7 * day) return `${Math.round(diff / day)} d ago`;
        return new Date(t).toISOString().slice(0, 10);
    }

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

window.PeersPage = PeersPage;
