// Admin → System — orchestrator + index view.
//
// One AdminSystem instance per page mount. Reads the URL path on init and
// dispatches to one of four sub-views:
//
//   /admin/config/system                  → index (gauges + trends + per-app table)
//   /admin/config/system/metric/<key>     → single-metric deep-dive page
//   /admin/config/system/app/<name>       → per-container app deep-dive
//   /admin/config/system/storage          → Docker disk breakdown
//
// Sub-views are separate page renderers (system-metric-page.js etc.) that
// each own their own DOM + lifecycle inside #config-section. We mount one
// at a time; switching means tearing down the active renderer and starting
// a fresh one. The SPA re-runs handleAdmin() on every navigation, which
// re-runs ConfigManager.renderConfig('system') → AdminSystem.init, so the
// dispatch happens organically with the router.
//
// Live (1 Hz, via LiveSystem SSE) data flows directly to whichever sub-view
// is mounted. The index view ticks gauges in place; the metric page splices
// the live value into its chart's tail.

class AdminSystem {
    constructor(rootId = 'config-section') {
        this.rootId = rootId;
        this.range = 60;            // minutes of history to chart
        this._timer = null;
        this._unsubLive = null;
        this.d = {};
        // Active sub-view renderer. Disposed on each init().
        this._subview = null;
    }

    root() { return document.getElementById(this.rootId); }

    // Path → view dispatch. AdminPath base is /admin/config/system; sub-paths
    // add segments after that. Falls through to 'index' for an unknown shape
    // so a typo'd URL doesn't blank the page.
    _parsePath() {
        const segs = String(window.location.pathname || '').split('/').filter(Boolean);
        // segs = ['admin','config','system', ...]
        const sub = segs[3];
        if (sub === 'metric'  && segs[4]) return { view: 'metric', key: decodeURIComponent(segs[4]) };
        if (sub === 'app'     && segs[4]) return { view: 'app',    name: decodeURIComponent(segs[4]) };
        if (sub === 'storage')            return { view: 'storage' };
        return { view: 'index' };
    }

    async init() {
        // Tear down any previous sub-view first (re-mount across nav).
        this._stopLive();
        if (this._timer) { clearInterval(this._timer); this._timer = null; }
        if (this._subview && typeof this._subview.dispose === 'function') {
            try { this._subview.dispose(); } catch (_) {}
        }
        this._subview = null;

        const parsed = this._parsePath();
        const r = this.root();
        if (!r) return;

        // Sub-pages live in their own classes; index lives here.
        if (parsed.view === 'metric' && window.SystemMetricPage) {
            this._subview = new window.SystemMetricPage(this.rootId);
            await this._subview.mount(parsed.key);
            return;
        }
        if (parsed.view === 'app') {
            // Per-app container deep-dive lives on the app page's Services
            // tab, not under /admin/system. Redirect any direct hit so old
            // bookmarks still resolve.
            if (window.navigateToRoute) {
                window.navigateToRoute(`/app/${encodeURIComponent(parsed.name)}/services`);
            }
            return;
        }
        if (parsed.view === 'storage' && window.SystemStoragePage) {
            this._subview = new window.SystemStoragePage(this.rootId);
            await this._subview.mount();
            return;
        }

        // Default: index view.
        r.innerHTML = '<div class="admin-page"><div class="backup-empty-state">Loading system stats…</div></div>';
        await this.refresh();
        this.bind();
        if (window.LiveSystem) {
            this._unsubLive = window.LiveSystem.subscribe((s) => this._applyLive(s));
        }
        this._timer = setInterval(() => {
            if (!document.querySelector('.sys-page')) {
                clearInterval(this._timer); this._timer = null;
                this._stopLive();
                return;
            }
            this.refresh();
        }, 30000);
    }

    _stopLive() {
        if (this._unsubLive) { try { this._unsubLive(); } catch (_) {} this._unsubLive = null; }
    }

    async refresh() {
        const [metrics, history, apps, appsHist, info] = await Promise.all([
            this.fetchJson('/data/system/metrics.json'),
            this.fetchJson(`/api/system/history?range=${this.range}`),
            this.fetchJson('/data/system/metrics_apps.json'),
            this.fetchJson('/data/system/metrics_apps_history.json'),
            this.fetchJson('/data/system/system_info.json')
        ]);
        this.d = { metrics, history, apps, appsHist, info };
        this.render();
    }

    async fetchJson(url) {
        try { const r = await fetch(`${url}?t=${Date.now()}`); if (!r.ok) return null; return await r.json(); }
        catch { return null; }
    }

    bind() {
        if (this._bound) return;
        this._bound = true;
        document.addEventListener('click', (e) => {
            if (!document.querySelector('.sys-page')) return;
            const rb = e.target.closest('[data-sys-range]');
            if (rb) {
                this.range = parseInt(rb.dataset.sysRange) || 60;
                this.refresh();
                return;
            }
            // Expand a metric → navigate to its detail page. The SPA picks up
            // the new URL, ConfigManager re-renders, AdminSystem.init mounts
            // the metric page.
            const ex = e.target.closest('[data-sys-expand]');
            if (ex) {
                const k = ex.dataset.sysExpand;
                if (window.navigateToRoute) window.navigateToRoute(`/admin/config/system/metric/${encodeURIComponent(k)}`);
                return;
            }
            const ap = e.target.closest('[data-sys-app]');
            if (ap) {
                const name = ap.dataset.sysApp;
                // App rows jump to the app page's Services tab — the existing
                // home for per-container detail (logs, restart, IP, ports)
                // which the new docker-info endpoints now enrich with live
                // stats, limits, mounts, networks and healthcheck.
                if (window.navigateToRoute) window.navigateToRoute(`/app/${encodeURIComponent(name)}/services`);
                return;
            }
            const st = e.target.closest('[data-sys-storage]');
            if (st) {
                if (window.navigateToRoute) window.navigateToRoute('/admin/config/system/storage');
                return;
            }
        });
    }

    /* ---- formatting helpers (used by sub-pages via window.SystemFmt) ---- */
    escape(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }
    bytes(n) {
        n = Number(n) || 0;
        const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0;
        while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
        return `${n.toFixed(i ? 1 : 0)} ${u[i]}`;
    }
    rate(n) { return `${this.bytes(n)}/s`; }

    series(key) {
        const pts = (this.d.history && Array.isArray(this.d.history.points)) ? this.d.history.points : [];
        return pts.slice(-this.range).map(p => Number(p[key]) || 0);
    }

    rangeBtns() {
        const opts = [[60, '1h'], [360, '6h'], [1440, '24h'], [10080, '7d']];
        return `<div class="sys-range">${opts.map(([m, l]) =>
            `<button type="button" class="sys-range-btn ${this.range === m ? 'active' : ''}" data-sys-range="${m}">${l}</button>`
        ).join('')}</div>`;
    }

    chartCard(title, bodyHtml, meta, expandKey) {
        const exBtn = expandKey
            ? `<button type="button" class="sys-expand" data-sys-expand="${expandKey}" aria-label="Expand ${title}">
                  <svg viewBox="0 0 24 24" width="14" height="14"><path d="M3 9V3h6M21 9V3h-6M3 15v6h6M21 15v6h-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
               </button>` : '';
        return `<div class="sys-chart-card">
            <div class="sys-chart-head"><span>${title}</span><span class="sys-chart-head-right">${meta ? `<span class="sys-chart-meta">${meta}</span>` : ''}${exBtn}</span></div>
            <div class="sys-chart-body">${bodyHtml}</div>
        </div>`;
    }

    _gaugesHtml() {
        const C = window.LPCharts;
        const m = this.d.metrics || {};
        const cpu = m.cpu || {}, mem = m.memory || {};
        const disks = Array.isArray(m.disks) ? m.disks : [];
        const rootDisk = disks.find(d => d.mount === '/') || disks[0] || {};
        const wrap = (key, inner) =>
            `<button type="button" class="sys-gauge-wrap" data-sys-expand="${key}" aria-label="Expand ${key}">
                ${inner}
                <span class="sys-gauge-expand">
                    <svg viewBox="0 0 24 24" width="14" height="14"><path d="M3 9V3h6M21 9V3h-6M3 15v6h6M21 15v6h-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
                </span>
             </button>`;
        return `
            ${wrap('cpu',   C.gauge(cpu.percent || 0, { label: 'CPU', sublabel: `${cpu.cores || '?'} cores` }))}
            ${wrap('mem',   C.gauge(mem.percent || 0, { label: 'Memory', sublabel: `${this.bytes(mem.used)} / ${this.bytes(mem.total)}` }))}
            ${wrap('disk',  C.gauge(rootDisk.percent || 0, { label: 'Disk', sublabel: rootDisk.mount || '/' }))}
            ${wrap('load1', C.gauge(cpu.load1_percent || 0, { label: 'Load', display: (cpu.load1 ?? 0), suffix: '', sublabel: `1m · ${cpu.load5 ?? '–'}/${cpu.load15 ?? '–'}` }))}`;
    }

    _applyLive(s) {
        if (!s || !document.querySelector('.sys-page')) return;
        const m = this.d.metrics || {};
        this.d.metrics = {
            ...m,
            cpu: s.cpu || m.cpu,
            memory: s.memory || m.memory,
            disks: Array.isArray(s.disks) && s.disks.length ? s.disks : m.disks,
            network: s.network || m.network,
            docker: s.docker || m.docker,
            updated: new Date(s.t || Date.now()).toISOString()
        };
        const gaugesEl = document.querySelector('.sys-page .sys-gauges');
        if (gaugesEl) gaugesEl.innerHTML = this._gaugesHtml();
        const subEl = document.querySelector('.sys-page .page-header-title p');
        if (subEl) subEl.textContent = `Live host and per-app statistics. Updated ${new Date(s.t || Date.now()).toLocaleTimeString()}.`;
    }

    render() {
        const root = this.root();
        if (!root) return;
        const C = window.LPCharts;
        const m = this.d.metrics || {};
        const cpu = m.cpu || {}, mem = m.memory || {}, dk = m.docker || {};
        const info = this.d.info || {};
        const disks = Array.isArray(m.disks) ? m.disks : [];
        const rootDisk = disks.find(d => d.mount === '/') || disks[0] || {};

        const gauges = `<div class="sys-gauges">${this._gaugesHtml()}</div>`;

        const rx = this.series('net_rx'), tx = this.series('net_tx');
        const lastRx = rx[rx.length - 1] || 0, lastTx = tx[tx.length - 1] || 0;
        const hasSwap = (mem.swap_total || 0) > 0;
        const charts = `
            <div class="sys-section-head">
                <h2>Trends</h2>
                ${this.rangeBtns()}
            </div>
            <div class="sys-charts">
                ${this.chartCard('CPU usage',    C.areaChart(this.series('cpu'),  { color: 'accent',         max: 100, fmt: v => `${Math.round(v)}%` }), 'last %', 'cpu')}
                ${this.chartCard('Memory usage', C.areaChart(this.series('mem'),  { color: 'status-info',    max: 100, fmt: v => `${Math.round(v)}%` }), 'last %', 'mem')}
                ${this.chartCard('Disk usage',   C.areaChart(this.series('disk'), { color: 'status-warning', max: 100, fmt: v => `${Math.round(v)}%` }), rootDisk.mount || '/', 'disk')}
                ${this.chartCard('Network',
                    C.multiLine([{ values: rx, color: 'status-success' }, { values: tx, color: 'accent' }]) +
                    `<div class="sys-net-legend"><span><i class="dot ok"></i>↓ ${this.rate(lastRx)}</span><span><i class="dot accent"></i>↑ ${this.rate(lastTx)}</span></div>`,
                    'rx / tx', 'net_rx')}
                ${this.chartCard('Load (1m)',    C.areaChart(this.series('load1'), { color: 'accent', fmt: v => v.toFixed(2) }), `${cpu.cores || '?'} cores`, 'load1')}
                ${hasSwap ? this.chartCard('Swap usage', C.areaChart(this.series('swap'), { color: 'status-warning', max: 100, fmt: v => `${Math.round(v)}%` }), 'last %', 'swap') : ''}
            </div>`;

        const infoStrip = `
            <div class="sys-strip">
                ${this.stat('OS', info.os || '—')}
                ${this.stat('Kernel', info.kernel || '—')}
                ${this.stat('Uptime', (info.uptime || '—').replace(/^up /, ''))}
                ${this.stat('CPU', info.cpu || '—')}
                ${this.stat('Swap', mem.swap_total ? `${this.bytes(mem.swap_used)} / ${this.bytes(mem.swap_total)}` : 'none')}
            </div>`;

        // Docker strip — now a navigational tile too. "Storage" leads to the
        // dedicated breakdown page; the rest are display-only.
        const dockerStrip = `
            <div class="sys-section-head"><h2>Docker</h2></div>
            <div class="sys-strip">
                ${this.stat('Containers running', `${dk.containers_running ?? 0} / ${dk.containers_total ?? 0}`)}
                ${this.stat('Images', String(dk.images ?? 0))}
                ${this.stat('Volumes', String(dk.volumes ?? 0))}
                <button type="button" class="sys-stat sys-stat-link" data-sys-storage aria-label="Open storage breakdown">
                    <span class="sys-stat-label">Storage</span>
                    <strong class="sys-stat-value">Open breakdown →</strong>
                </button>
            </div>`;

        const apps = (this.d.apps && Array.isArray(this.d.apps.apps)) ? this.d.apps.apps : [];
        const appsHist = (this.d.appsHist && this.d.appsHist.apps) ? this.d.appsHist.apps : {};
        const appsBody = apps.length ? apps.map(a => {
            const spark = (appsHist[a.app] || []).map(p => Number(p.cpu) || 0);
            const statusCls = a.status === 'running' ? 'ok' : 'none';
            return `<tr class="sys-app-row" data-sys-app="${this.escape(a.app)}" tabindex="0" role="button" aria-label="Open ${this.escape(a.app)} details">
                <td class="sys-app-name"><span class="admin-status-dot ${statusCls}"></span>${this.escape(a.app)}
                    <span class="sys-app-sub">${a.running}/${a.containers} up</span></td>
                <td>${C.bar(a.cpu_percent)}<span class="sys-cell-val">${(a.cpu_percent || 0).toFixed(1)}%</span></td>
                <td>${C.bar(a.mem_percent)}<span class="sys-cell-val">${this.bytes(a.mem_bytes)}</span></td>
                <td class="sys-net-cell">↓${this.bytes(a.net_rx)} ↑${this.bytes(a.net_tx)}</td>
                <td class="sys-spark-cell">${C.sparkline(spark, { color: 'accent' })}</td>
                <td class="sys-app-arrow">›</td>
            </tr>`;
        }).join('') : `<tr><td colspan="6" class="sys-apps-empty">No running containers — install an app to see per-app stats.</td></tr>`;

        const appsTable = `
            <div class="sys-section-head"><h2>Per-app usage</h2><span class="sys-chart-meta">click a row to open the app's Services tab · sorted by CPU</span></div>
            <div class="sys-apps-wrap">
                <table class="sys-apps">
                    <thead><tr><th>App</th><th>CPU</th><th>Memory</th><th>Network</th><th>CPU trend</th><th></th></tr></thead>
                    <tbody>${appsBody}</tbody>
                </table>
            </div>`;

        const updated = m.updated ? new Date(m.updated).toLocaleTimeString() : '—';

        root.innerHTML = `
            <div class="admin-page sys-page">
                <div class="page-header config-page-header">
                    <div class="page-header-title">
                        <div class="admin-breadcrumb">Admin</div>
                        <h1>System</h1>
                        <p>Live host and per-app statistics. Updated ${updated}.</p>
                    </div>
                </div>
                ${gauges}
                ${charts}
                ${infoStrip}
                ${dockerStrip}
                ${appsTable}
            </div>`;
    }

    stat(label, value) {
        return `<div class="sys-stat"><span class="sys-stat-label">${this.escape(label)}</span><strong class="sys-stat-value">${this.escape(value)}</strong></div>`;
    }
}

// Lightweight formatter helpers shared with sub-pages so they don't each
// reimplement bytes()/rate(). Attached as a global so a sub-page that mounts
// before AdminSystem still has them.
window.SystemFmt = window.SystemFmt || {
    escape(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); },
    bytes(n) {
        n = Number(n) || 0;
        const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0;
        while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
        return `${n.toFixed(i ? 1 : 0)} ${u[i]}`;
    },
    rate(n) { return `${this.bytes(n)}/s`; },
    timeAgo(unixSec) {
        if (!unixSec) return '';
        const diff = Math.floor(Date.now() / 1000) - unixSec;
        if (diff < 60) return `${diff}s ago`;
        if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
        if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
        return `${Math.floor(diff / 86400)}d ago`;
    },
    timeAgoIso(iso) {
        if (!iso) return '';
        const t = Math.floor(new Date(iso).getTime() / 1000);
        return Number.isFinite(t) && t > 0 ? this.timeAgo(t) : '';
    },
    rgbVar(name) {
        return (getComputedStyle(document.documentElement).getPropertyValue(`--${name}-rgb`) || '').trim() || '0, 212, 255';
    },
};

window.AdminSystem = AdminSystem;
