import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import QtQuick.Window
import "../../shared" as Shared

Item {
    id: root

    implicitWidth: Shared.Theme.popoutWidth
    implicitHeight: Math.min(flickable.contentHeight + Shared.Theme.popoutPadding * 2, Screen.height - 32)

    PopoutBackground { anchors.fill: parent }

    // --- State ---
    readonly property int historySize: 30
    property var cpuHistory: []
    property var memHistory: []
    property var gpuHistory: []

    function pushHistory(arr, val) {
        let a = arr.slice();
        a.push(val);
        if (a.length > historySize) a.shift();
        return a;
    }

    property real cpuVal: 0
    property string memText: "--"
    property real memVal: 0
    property string tempText: "--"
    property int tempVal: 0
    property string gpuText: "--"
    property real gpuUsage: 0
    property int gpuTemp: 0
    property string nvmeTempText: "--"
    property int nvmeTempVal: 0
    property string diskRootText: "--"
    property real diskRootVal: 0
    property string diskDataText: "--"
    property real diskDataVal: 0
    property string updatesText: ""
    property string updatesClass: ""
    property bool updatesExpanded: false
    property var updatesPackages: []
    property string alhpText: ""
    property string alhpClass: ""
    property bool alhpExpanded: false
    property var alhpPackages: []
    property string networkIp: "--"
    property string networkIface: "--"
    property bool idleActive: false
    // Battery & power profile (laptop only)
    property int batteryPercent: -1
    property string batteryState: ""  // "Charging", "Discharging", "Full", "Not charging"
    property string powerProfile: ""  // "power-saver", "balanced", "performance"
    property var panelWindow: null
    property string audioDrawer: ""  // "" = closed, "sink" or "source"

    function thresholdColor(val, warn, crit) {
        if (val >= crit) return Shared.Theme.danger;
        if (val >= warn) return Shared.Theme.warning;
        return Shared.Theme.success;
    }

    function tempColor(t) {
        if (t >= 85) return Shared.Theme.danger;
        if (t >= 70) return Shared.Theme.warning;
        return Shared.Theme.success;
    }

    MouseArea { anchors.fill: parent }

    Flickable {
        id: flickable
        anchors.fill: parent
        anchors.margins: Shared.Theme.popoutPadding
        contentHeight: col.implicitHeight
        clip: true
        boundsBehavior: Flickable.StopAtBounds

    ColumnLayout {
        id: col
        width: flickable.width
        spacing: 6

        // ─── UPDATES ─────────────────────────

        // updates pending — clickable, expands package list
        Loader {
            Layout.fillWidth: true
            active: root.updatesClass === "pending" || root.updatesClass === "many"
            visible: active
            sourceComponent: Rectangle {
                implicitHeight: updRow.implicitHeight + 14
                radius: Shared.Theme.radiusSmall
                color: updMouse.containsMouse ? Shared.Theme.surface1 : Shared.Theme.surface0
                Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }

                RowLayout {
                    id: updRow
                    anchors.fill: parent
                    anchors.margins: 8
                    spacing: 10
                    Text { text: "\u{f0ab7}"; color: Shared.Theme.warning; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
                    Text { text: root.updatesText + " updates available"; color: Shared.Theme.text; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily; Layout.fillWidth: true }
                    Text {
                        text: "\u{f0140}"
                        color: Shared.Theme.overlay0
                        font.pixelSize: 12
                        font.family: Shared.Theme.iconFont
                        rotation: root.updatesExpanded ? 180 : 0
                        Behavior on rotation { NumberAnimation { duration: Shared.Theme.animFast } }
                    }
                }

                MouseArea {
                    id: updMouse
                    anchors.fill: parent
                    hoverEnabled: true
                    cursorShape: Qt.PointingHandCursor
                    onClicked: root.updatesExpanded = !root.updatesExpanded
                }
            }
        }

        // updates expanded package list
        Loader {
            Layout.fillWidth: true
            active: root.updatesExpanded && root.updatesPackages.length > 0
            visible: active
            sourceComponent: Rectangle {
                radius: Shared.Theme.radiusSmall
                color: Shared.Theme.surface0
                implicitHeight: pkgCol.implicitHeight + 12

                ColumnLayout {
                    id: pkgCol
                    anchors.fill: parent
                    anchors.margins: 6
                    spacing: 2

                    Repeater {
                        model: root.updatesPackages
                        Text {
                            required property string modelData
                            text: modelData
                            color: Shared.Theme.subtext0
                            font.pixelSize: Shared.Theme.fontSmall
                            font.family: Shared.Theme.fontFamily
                            Layout.fillWidth: true
                            elide: Text.ElideRight
                        }
                    }
                }
            }
        }

        // system up to date
        Loader {
            Layout.fillWidth: true
            active: root.updatesClass !== "pending" && root.updatesClass !== "many" && root.updatesClass !== ""
            visible: active
            sourceComponent: RowLayout {
                spacing: 10
                Text { text: "\u{f05e0}"; color: Shared.Theme.success; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
                Text { text: "System up to date"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
            }
        }

        // ALHP good
        Loader {
            Layout.fillWidth: true
            active: root.alhpClass === "good"
            visible: active
            sourceComponent: RowLayout {
                spacing: 10
                Text { text: "\u{f05e0}"; color: Shared.Theme.success; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
                Text { text: "ALHP \u2713"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
            }
        }

        // ALHP mirror stale
        Loader {
            Layout.fillWidth: true
            active: root.alhpClass === "stale"
            visible: active
            sourceComponent: RowLayout {
                spacing: 10
                Text { text: "\u{f0026}"; color: Shared.Theme.warning; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
                Text { text: "ALHP mirror outdated"; color: Shared.Theme.warning; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
            }
        }

        // ALHP packages building — clickable, expands package list
        Loader {
            Layout.fillWidth: true
            active: root.alhpClass === "bad"
            visible: active
            sourceComponent: RowLayout {
                spacing: 10

                Text { text: "\u{f0954}"; color: Shared.Theme.danger; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
                Text {
                    text: "ALHP " + root.alhpText + " building"
                    color: Shared.Theme.danger
                    font.pixelSize: Shared.Theme.fontSize
                    font.family: Shared.Theme.fontFamily
                    Layout.fillWidth: true
                }
                Text {
                    visible: root.alhpPackages.length > 0
                    text: "\u{f0140}"
                    color: Shared.Theme.overlay0
                    font.pixelSize: 12
                    font.family: Shared.Theme.iconFont
                    rotation: root.alhpExpanded ? 180 : 0
                    Behavior on rotation { NumberAnimation { duration: Shared.Theme.animFast } }
                }

                MouseArea {
                    anchors.fill: parent
                    hoverEnabled: root.alhpPackages.length > 0
                    cursorShape: root.alhpPackages.length > 0 ? Qt.PointingHandCursor : Qt.ArrowCursor
                    onClicked: if (root.alhpPackages.length > 0) root.alhpExpanded = !root.alhpExpanded
                }
            }
        }

        // ALHP expanded package list
        Loader {
            Layout.fillWidth: true
            active: root.alhpExpanded && root.alhpPackages.length > 0
            visible: active
            sourceComponent: Rectangle {
                radius: Shared.Theme.radiusSmall
                color: Shared.Theme.surface0
                implicitHeight: alhpPkgCol.implicitHeight + 12

                ColumnLayout {
                    id: alhpPkgCol
                    anchors.fill: parent
                    anchors.margins: 6
                    spacing: 2

                    Repeater {
                        model: root.alhpPackages
                        Text {
                            required property string modelData
                            text: modelData
                            color: Shared.Theme.subtext0
                            font.pixelSize: Shared.Theme.fontSmall
                            font.family: Shared.Theme.fontFamily
                            Layout.fillWidth: true
                            elide: Text.ElideRight
                        }
                    }
                }
            }
        }

        // ─── separator ───
        Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }

        // ─── STORAGE & PERFORMANCE ───────────

        MetricBar { label: Shared.Config.diskMount1Label; value: root.diskRootText; fill: root.diskRootVal; barColor: thresholdColor(root.diskRootVal * 100, 70, 90); valueColor: thresholdColor(root.diskRootVal * 100, 70, 90) }
        MetricBar { label: Shared.Config.diskMount2Label; value: root.diskDataText; fill: root.diskDataVal; barColor: thresholdColor(root.diskDataVal * 100, 70, 90); valueColor: thresholdColor(root.diskDataVal * 100, 70, 90) }

        Item { implicitHeight: 2 }

        MetricBar {
            label: "CPU"
            value: root.cpuVal.toFixed(0) + "%"
            fill: root.cpuVal / 100
            barColor: thresholdColor(root.cpuVal, 50, 80)
            valueColor: thresholdColor(root.cpuVal, 50, 80)
            suffix: root.tempText
            suffixColor: tempColor(root.tempVal)
            history: root.cpuHistory
        }

        MetricBar {
            label: "Mem"
            value: root.memText
            fill: root.memVal
            barColor: thresholdColor(root.memVal * 100, 60, 85)
            valueColor: thresholdColor(root.memVal * 100, 60, 85)
            history: root.memHistory
        }

        MetricBar {
            visible: Shared.Config.hasDiscreteGpu
            label: "GPU"
            value: root.gpuText
            fill: root.gpuUsage / 100
            barColor: thresholdColor(root.gpuUsage, 50, 80)
            valueColor: thresholdColor(root.gpuUsage, 50, 80)
            history: root.gpuHistory
        }

        // Battery (laptop only)
        MetricBar {
            visible: Shared.Config.hasBattery && root.batteryPercent >= 0
            label: root.batteryState === "Charging" ? "\u{f0084}" : "\u{f007a}"
            value: root.batteryPercent + "%"
            fill: root.batteryPercent / 100
            barColor: root.batteryPercent <= 15 ? Shared.Theme.danger : root.batteryPercent <= 30 ? Shared.Theme.warning : Shared.Theme.success
            valueColor: barColor
            suffix: root.batteryState === "Charging" ? "CHG" : (root.batteryState === "Full" ? "FULL" : "")
            suffixColor: Shared.Theme.subtext0
        }

        MetricBar {
            label: "NVMe"
            value: root.nvmeTempText
            fill: root.nvmeTempVal / 100
            barColor: tempColor(root.nvmeTempVal)
            valueColor: tempColor(root.nvmeTempVal)
        }

        // ─── separator ───
        Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }

        // ─── AUDIO ───────────────────────────

        RowLayout {
            Layout.fillWidth: true
            spacing: 8

            VolumeSlider {
                Layout.fillWidth: true
                audio: Pipewire.defaultAudioSink?.audio ?? null
                icon: (!audio || audio.muted) ? "\u{f057f}" : "\u{f057e}"
                label: "Out"
                accentColor: Shared.Theme.sky
                drawerActive: root.audioDrawer === "sink"
                onDrawerToggled: root.audioDrawer = root.audioDrawer === "sink" ? "" : "sink"
            }

            VolumeSlider {
                Layout.fillWidth: true
                audio: Pipewire.defaultAudioSource?.audio ?? null
                icon: (!audio || audio.muted) ? "\u{f036d}" : "\u{f036c}"
                label: "Mic"
                accentColor: Shared.Theme.sky
                drawerActive: root.audioDrawer === "source"
                onDrawerToggled: root.audioDrawer = root.audioDrawer === "source" ? "" : "source"
            }
        }

        // Device drawer
        Loader {
            Layout.fillWidth: true
            active: root.audioDrawer !== ""
            visible: active
            sourceComponent: ColumnLayout {
                spacing: 3

                Repeater {
                    model: Pipewire.nodes

                    Rectangle {
                        id: devItem
                        required property var modelData

                        readonly property bool isSinkDrawer: root.audioDrawer === "sink"
                        readonly property bool matchesFilter: modelData.audio && !modelData.isStream
                            && modelData.isSink === isSinkDrawer
                        readonly property bool isDefault: isSinkDrawer
                            ? Pipewire.defaultAudioSink === modelData
                            : Pipewire.defaultAudioSource === modelData

                        visible: matchesFilter
                        Layout.fillWidth: true
                        implicitHeight: matchesFilter ? 28 : 0
                        radius: Shared.Theme.radiusSmall
                        color: devMouse.containsMouse ? Shared.Theme.surface1 : (isDefault ? Shared.Theme.surface0 : "transparent")

                        Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }

                        RowLayout {
                            anchors.fill: parent
                            anchors.leftMargin: 8
                            anchors.rightMargin: 8
                            spacing: 6

                            Text {
                                text: devItem.isDefault ? "\u{f012c}" : "\u{f0765}"
                                color: devItem.isDefault ? Shared.Theme.sky : Shared.Theme.overlay0
                                font.pixelSize: 12
                                font.family: Shared.Theme.iconFont
                            }
                            Text {
                                text: devItem.modelData.description || devItem.modelData.name
                                color: devItem.isDefault ? Shared.Theme.text : Shared.Theme.subtext0
                                font.pixelSize: Shared.Theme.fontSmall
                                font.family: Shared.Theme.fontFamily
                                font.bold: devItem.isDefault
                                Layout.fillWidth: true
                                elide: Text.ElideRight
                            }
                        }

                        MouseArea {
                            id: devMouse
                            anchors.fill: parent
                            hoverEnabled: true
                            onClicked: {
                                if (devItem.isSinkDrawer)
                                    Pipewire.preferredDefaultAudioSink = devItem.modelData;
                                else
                                    Pipewire.preferredDefaultAudioSource = devItem.modelData;
                                root.audioDrawer = "";
                            }
                        }
                    }
                }
            }
        }

        // ─── separator ───
        Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }

        // ─── NETWORK ─────────────────────────

        RowLayout {
            Layout.fillWidth: true
            spacing: 10
            Text { text: "\u{f0bf0}"; color: Shared.Theme.overlay0; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
            Text { text: root.networkIface; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
            Item { Layout.fillWidth: true }
            Text { text: root.networkIp; color: Shared.Theme.text; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily; font.bold: true }
        }

        // ─── separator ───
        Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 4; Layout.bottomMargin: 4 }

        // ─── SYSTRAY ─────────────────────────

        Flow {
            Layout.fillWidth: true
            spacing: 6

            Repeater {
                model: SystemTray.items

                Rectangle {
                    id: trayItem
                    required property var modelData

                    visible: modelData.status !== Status.Passive
                    width: 32
                    height: 32
                    radius: Shared.Theme.radiusSmall
                    color: trayMouse.containsMouse ? Shared.Theme.surface1 : "transparent"

                    Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }

                    IconImage {
                        anchors.centerIn: parent
                        source: trayItem.modelData.icon
                        implicitSize: 22
                    }

                    // Tooltip — positioned above icon, clamped to popout width
                    Rectangle {
                        id: tooltip
                        visible: trayMouse.containsMouse && tipText.text !== ""
                        width: Math.min(tipText.implicitWidth + 12, Shared.Theme.popoutWidth - Shared.Theme.popoutPadding * 2)
                        height: tipText.implicitHeight + 8
                        radius: 6
                        color: Shared.Theme.surface0
                        z: 10

                        // Position above the icon, clamp horizontally within the popout
                        y: -height - 4
                        x: {
                            let centered = (trayItem.width - width) / 2;
                            let globalX = trayItem.mapToItem(col, centered, 0).x;
                            let maxX = col.width - width;
                            let clampedGlobalX = Math.max(0, Math.min(globalX, maxX));
                            return centered + (clampedGlobalX - globalX);
                        }

                        Text {
                            id: tipText
                            anchors.centerIn: parent
                            width: parent.width - 12
                            text: trayItem.modelData.tooltipTitle || trayItem.modelData.title || ""
                            color: Shared.Theme.text
                            font.pixelSize: Shared.Theme.fontSmall
                            font.family: Shared.Theme.fontFamily
                            elide: Text.ElideRight
                            maximumLineCount: 1
                        }
                    }

                    MouseArea {
                        id: trayMouse
                        anchors.fill: parent
                        hoverEnabled: true
                        acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton

                        onClicked: event => {
                            let item = trayItem.modelData;
                            if (event.button === Qt.RightButton || item.onlyMenu) {
                                if (item.hasMenu && root.panelWindow) {
                                    let pos = trayItem.mapToItem(null, trayItem.width / 2, trayItem.height / 2);
                                    item.display(root.panelWindow, pos.x, pos.y);
                                }
                            } else if (event.button === Qt.MiddleButton) {
                                item.secondaryActivate();
                            } else {
                                item.activate();
                            }
                        }

                        onWheel: event => {
                            trayItem.modelData.scroll(event.angleDelta.y, false);
                        }
                    }
                }
            }
        }

        // ─── POWER PROFILE (laptop only) ────
        RowLayout {
            Layout.fillWidth: true
            visible: Shared.Config.hasPowerProfiles
            spacing: 6

            Text { text: "\u{f0425}"; color: Shared.Theme.overlay0; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
            Text { text: "Profile"; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
            Item { Layout.fillWidth: true }

            Repeater {
                model: [
                    { name: "power-saver", icon: "\u{f19be}" },
                    { name: "balanced", icon: "\u{f0a01}" },
                    { name: "performance", icon: "\u{f1b4b}" }
                ]

                Rectangle {
                    required property var modelData
                    required property int index
                    width: 28; height: 28
                    radius: Shared.Theme.radiusSmall
                    color: root.powerProfile === modelData.name ? Shared.Theme.surface1 : (ppMouse.containsMouse ? Shared.Theme.surface0 : "transparent")

                    Text {
                        anchors.centerIn: parent
                        text: modelData.icon
                        color: root.powerProfile === modelData.name ? Shared.Theme.focus : Shared.Theme.overlay0
                        font.pixelSize: 14
                        font.family: Shared.Theme.iconFont
                    }

                    MouseArea {
                        id: ppMouse
                        anchors.fill: parent
                        hoverEnabled: true
                        onClicked: root.setPowerProfile(modelData.name)
                    }
                }
            }
        }

        // ─── separator ───
        Rectangle { Layout.fillWidth: true; height: 1; color: Shared.Theme.surface0; Layout.topMargin: 6; Layout.bottomMargin: 6 }

        // ─── ACTIONS ─────────────────────────

        RowLayout {
            Layout.fillWidth: true
            spacing: 6

            ActionIcon {
                icon: "\u{f1436}"
                label: "Idle"
                iconColor: root.idleActive ? Shared.Theme.green : Shared.Theme.overlay0
                onActivated: {
                    if (root.idleActive) idleKill.running = true;
                    else idleStart.running = true;
                    root.idleActive = !root.idleActive;
                }
            }

            ActionIcon {
                icon: "\u{f033e}"
                label: "Lock"
                iconColor: Shared.Theme.subtext0
                onActivated: { Shared.PopoutState.close(); lockProc.running = true; }
            }

            Item { Layout.fillWidth: true }

            HoldAction {
                icon: "\u{f0425}"
                label: "Logout"
                iconColor: Shared.Theme.subtext0
                holdColor: Shared.Theme.green
                onConfirmed: { Shared.PopoutState.close(); logoutProc.running = true; }
            }

            HoldAction {
                icon: "\u{f0709}"
                label: "Reboot"
                iconColor: Shared.Theme.yellow
                holdColor: Shared.Theme.yellow
                onConfirmed: { Shared.PopoutState.close(); rebootProc.running = true; }
            }

            HoldAction {
                icon: "\u{f0425}"
                label: "Off"
                iconColor: Shared.Theme.danger
                holdColor: Shared.Theme.danger
                onConfirmed: { Shared.PopoutState.close(); offProc.running = true; }
            }
        }
    } // ColumnLayout
    } // Flickable

    // ═══════════════════════════════════════
    // COMPONENTS
    // ═══════════════════════════════════════

    component Sparkline: Canvas {
        id: spark
        property var history: []
        property color lineColor: Shared.Theme.overlay0

        onHistoryChanged: requestPaint()
        onWidthChanged: requestPaint()
        onHeightChanged: requestPaint()

        onPaint: {
            let ctx = getContext("2d");
            ctx.clearRect(0, 0, width, height);
            if (history.length < 2) return;
            ctx.strokeStyle = Qt.alpha(lineColor, 0.5);
            ctx.lineWidth = 1;
            ctx.beginPath();
            let step = width / (root.historySize - 1);
            let offset = root.historySize - history.length;
            for (let i = 0; i < history.length; i++) {
                let x = (offset + i) * step;
                let y = height - history[i] * height;
                if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
            }
            ctx.stroke();
        }
    }

    component MetricBar: RowLayout {
        property string label
        property string value
        property real fill: 0
        property color barColor: Shared.Theme.green
        property color valueColor: Shared.Theme.text
        property string suffix: ""
        property color suffixColor: Shared.Theme.overlay0
        property var history: null

        Layout.fillWidth: true
        implicitHeight: 22
        spacing: 10

        Text {
            text: parent.label
            color: Shared.Theme.overlay0
            font.pixelSize: Shared.Theme.fontSize
            font.family: Shared.Theme.fontFamily
            Layout.preferredWidth: 40
        }

        Rectangle {
            Layout.fillWidth: true
            implicitHeight: 10
            radius: 5
            color: Shared.Theme.surface0

            Sparkline {
                anchors.fill: parent
                visible: history && history.length > 1
                history: parent.parent.history ?? []
                lineColor: barColor
            }

            Rectangle {
                anchors.left: parent.left
                anchors.top: parent.top
                anchors.bottom: parent.bottom
                width: parent.width * Math.min(1, Math.max(0, fill))
                radius: 5
                color: barColor
                opacity: Shared.Theme.opacityFill
                Behavior on width { NumberAnimation { duration: Shared.Theme.animFast; easing.type: Easing.OutCubic } }
                Behavior on color { ColorAnimation { duration: Shared.Theme.animNormal } }
            }
        }

        Text {
            text: parent.value
            color: parent.valueColor
            font.pixelSize: Shared.Theme.fontSize
            font.family: Shared.Theme.fontFamily
            font.bold: true
            horizontalAlignment: Text.AlignRight
            Layout.preferredWidth: 86
        }

        Loader {
            active: parent.suffix !== ""
            visible: active
            sourceComponent: Text {
                text: suffix
                color: suffixColor
                font.pixelSize: Shared.Theme.fontSize
                font.family: Shared.Theme.fontFamily
                font.bold: true
            }
        }
    }

    component VolumeSlider: Rectangle {
        id: vs
        property var audio: null
        property string icon
        property string label
        property color accentColor
        property bool drawerActive: false
        signal drawerToggled()

        property real vol: 0
        property bool muted: audio?.muted ?? false

        function updateVol() {
            if (audio && !isNaN(audio.volume) && audio.volume >= 0)
                vol = audio.volume;
        }
        onAudioChanged: { updateVol(); bindRetry.retries = 0; bindRetry.running = true; }
        Connections {
            target: vs.audio
            function onVolumeChanged() { vs.updateVol(); }
        }

        // Retry binding pickup after async PwObjectTracker re-bind
        Timer {
            id: bindRetry
            interval: 300
            running: false
            repeat: true
            property int retries: 0
            onTriggered: {
                vs.updateVol();
                retries++;
                if (vs.vol > 0 || retries >= 5) running = false;
            }
        }

        Layout.fillWidth: true
        implicitHeight: 36
        radius: Shared.Theme.radiusSmall
        color: drawerActive ? Qt.alpha(accentColor, Shared.Theme.opacityLight) : Shared.Theme.surface0
        border.width: drawerActive ? 1 : 0
        border.color: Qt.alpha(accentColor, Shared.Theme.opacityMedium)
        opacity: muted ? 0.45 : 1.0
        clip: true

        Behavior on opacity { NumberAnimation { duration: Shared.Theme.animFast } }
        Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }

        Rectangle {
            anchors.left: parent.left
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            width: vs.muted ? 0 : parent.width * Math.min(1, vs.vol)
            radius: Shared.Theme.radiusSmall
            color: Qt.alpha(vs.accentColor, Shared.Theme.opacityMedium)
            Behavior on width { NumberAnimation { duration: Shared.Theme.animFast; easing.type: Easing.OutCubic } }
        }

        RowLayout {
            anchors.fill: parent
            anchors.margins: 8
            spacing: 8
            Text { text: vs.icon; color: vs.accentColor; font.pixelSize: 14; font.family: Shared.Theme.iconFont }
            Text { text: vs.label; color: vs.accentColor; font.pixelSize: Shared.Theme.fontSize; font.family: Shared.Theme.fontFamily }
            Item { Layout.fillWidth: true }
            Text {
                text: vs.muted ? "Muted" : Math.round(vs.vol * 100) + "%"
                color: vs.muted ? Shared.Theme.overlay0 : Shared.Theme.text
                font.pixelSize: Shared.Theme.fontSize
                font.family: Shared.Theme.fontFamily
                font.bold: !vs.muted
            }
        }

        MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
            property bool dragging: false
            property real startX: 0
            property int pressedBtn: Qt.NoButton
            onClicked: event => {
                if (event.button === Qt.MiddleButton) { pavuProc.running = true; return; }
                if (event.button === Qt.RightButton) { if (vs.audio) vs.audio.muted = !vs.audio.muted; return; }
                if (!dragging) vs.drawerToggled();
            }
            onPressed: event => { dragging = false; startX = event.x; pressedBtn = event.button; }
            onPositionChanged: event => {
                if (pressed && pressedBtn === Qt.LeftButton) {
                    if (Math.abs(event.x - startX) > 5) dragging = true;
                    if (dragging && vs.audio) vs.audio.volume = Math.max(0, Math.min(1, event.x / vs.width));
                }
            }
            onWheel: event => {
                if (!vs.audio) return;
                let step = 0.05;
                if (event.angleDelta.y > 0) vs.audio.volume = Math.min(1.0, vs.vol + step);
                else vs.audio.volume = Math.max(0.0, vs.vol - step);
            }
        }
    }

    component ActionIcon: Rectangle {
        property string icon
        property string label
        property color iconColor
        signal activated()

        implicitWidth: actCol.implicitWidth + 16
        implicitHeight: actCol.implicitHeight + 14
        radius: Shared.Theme.radiusSmall
        color: actMouse.containsMouse ? Shared.Theme.surface1 : "transparent"
        border.width: actMouse.containsMouse ? 1 : 0
        border.color: Shared.Theme.surface2

        Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }

        Column {
            id: actCol
            anchors.centerIn: parent
            spacing: 3
            Text { anchors.horizontalCenter: parent.horizontalCenter; text: icon; color: iconColor; font.pixelSize: 18; font.family: Shared.Theme.iconFont }
            Text { anchors.horizontalCenter: parent.horizontalCenter; text: label; color: Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
        }

        MouseArea { id: actMouse; anchors.fill: parent; hoverEnabled: true; onClicked: parent.activated() }
    }

    component HoldAction: Rectangle {
        id: ha
        property string icon
        property string label
        property color iconColor
        property color holdColor: Shared.Theme.red
        signal confirmed()

        readonly property real holdDuration: 800
        property real holdProgress: 0
        property bool holding: false

        implicitWidth: haCol.implicitWidth + 16
        implicitHeight: haCol.implicitHeight + 14
        radius: Shared.Theme.radiusSmall
        color: haMouse.containsMouse ? Shared.Theme.surface1 : "transparent"
        border.width: haMouse.containsMouse || holding ? 1 : 0
        border.color: holding ? Qt.alpha(holdColor, Shared.Theme.opacityStrong) : Shared.Theme.surface2

        Behavior on color { ColorAnimation { duration: Shared.Theme.animFast } }

        // Fill overlay
        Rectangle {
            anchors.left: parent.left
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            width: parent.width * ha.holdProgress
            radius: Shared.Theme.radiusSmall
            color: Qt.alpha(ha.holdColor, Shared.Theme.opacityLight)
        }

        Column {
            id: haCol
            anchors.centerIn: parent
            spacing: 3
            Text { anchors.horizontalCenter: parent.horizontalCenter; text: ha.icon; color: ha.holding ? ha.holdColor : ha.iconColor; font.pixelSize: 18; font.family: Shared.Theme.iconFont }
            Text { anchors.horizontalCenter: parent.horizontalCenter; text: ha.holding ? "Hold…" : ha.label; color: ha.holding ? ha.holdColor : Shared.Theme.overlay0; font.pixelSize: Shared.Theme.fontSmall; font.family: Shared.Theme.fontFamily }
        }

        Timer {
            id: holdTimer
            interval: 16
            running: ha.holding
            repeat: true
            onTriggered: {
                ha.holdProgress += interval / ha.holdDuration;
                if (ha.holdProgress >= 1.0) {
                    ha.holding = false;
                    ha.holdProgress = 0;
                    ha.confirmed();
                }
            }
        }

        MouseArea {
            id: haMouse
            anchors.fill: parent
            hoverEnabled: true
            onPressed: { ha.holding = true; ha.holdProgress = 0; }
            onReleased: { ha.holding = false; ha.holdProgress = 0; }
            onCanceled: { ha.holding = false; ha.holdProgress = 0; }
        }
    }

    // ═══════════════════════════════════════
    // DATA FETCHING
    // ═══════════════════════════════════════

    // ─── Native sysfs/proc readers (no per-tick subprocess) ───────────────────
    // Fixed paths read directly; hwmon/battery paths resolved once by
    // pathDiscovery below. FileView.reload()+text() is synchronous (verified).

    FileView { id: statFile;      path: "/proc/stat";    blockLoading: true }
    FileView { id: meminfoFile;   path: "/proc/meminfo"; blockLoading: true }
    FileView { id: cpuTempFile;   blockLoading: true }
    FileView { id: nvmeTempFile;  blockLoading: true }
    FileView { id: batCapFile;    blockLoading: true }
    FileView { id: batStatusFile; blockLoading: true }

    property real prevCpuActive: -1
    property real prevCpuTotal: -1
    function readCpu() {
        statFile.reload();
        let p = statFile.text().split("\n")[0].trim().split(/\s+/);  // cpu user nice system idle …
        let active = parseFloat(p[1]) + parseFloat(p[3]);            // user + system
        let total  = active + parseFloat(p[4]);                      // + idle
        if (!isNaN(active) && root.prevCpuTotal > 0) {
            let da = active - root.prevCpuActive, dt = total - root.prevCpuTotal;
            if (dt > 0) { root.cpuVal = (da / dt) * 100; root.cpuHistory = root.pushHistory(root.cpuHistory, root.cpuVal / 100); }
        }
        root.prevCpuActive = active;
        root.prevCpuTotal = total;
    }

    function readMem() {
        meminfoFile.reload();
        let t = meminfoFile.text();
        let total = parseInt((t.match(/MemTotal:\s+(\d+)/) || [])[1]);      // kB
        let avail = parseInt((t.match(/MemAvailable:\s+(\d+)/) || [])[1]);  // kB
        if (!isNaN(total) && !isNaN(avail) && total > 0) {
            let used = total - avail;
            root.memText = (used / 1048576).toFixed(1) + "/" + (total / 1048576).toFixed(0) + "G";
            root.memVal = used / total;
            root.memHistory = root.pushHistory(root.memHistory, root.memVal);
        }
    }

    function readTemp() {
        if (!cpuTempFile.path) return;
        cpuTempFile.reload();
        let v = parseInt(cpuTempFile.text().trim());  // millidegrees
        if (!isNaN(v)) { root.tempVal = Math.round(v / 1000); root.tempText = root.tempVal + "°C"; }
    }

    function readNvme() {
        if (!nvmeTempFile.path) return;
        nvmeTempFile.reload();
        let v = parseInt(nvmeTempFile.text().trim());
        if (!isNaN(v) && v > 0) { root.nvmeTempVal = Math.round(v / 1000); root.nvmeTempText = root.nvmeTempVal + "°C"; }
    }

    function readBattery() {
        if (!batCapFile.path) return;
        batCapFile.reload(); batStatusFile.reload();
        let c = parseInt(batCapFile.text().trim());
        let s = batStatusFile.text().trim();
        if (!isNaN(c)) root.batteryPercent = c;
        if (s) root.batteryState = s;
    }

    // Resolve hwmon/battery sysfs paths once per popout open, then read.
    Process {
        id: pathDiscovery
        command: ["sh", "-c",
            "for h in /sys/class/hwmon/hwmon*; do n=$(cat \"$h/name\" 2>/dev/null); " +
            "case \"$n\" in k10temp|zenpower|coretemp) sel=''; for l in \"$h\"/temp*_label; do [ -r \"$l\" ] || continue; " +
            "case \"$(cat \"$l\")\" in Tctl|Tdie|'Package id 0') sel=\"${l%_label}_input\"; break;; esac; done; " +
            "[ -z \"$sel\" ] && sel=\"$h/temp1_input\"; echo \"cpuTemp=$sel\";; " +
            "nvme) echo \"nvmeTemp=$h/temp1_input\";; esac; done; " +
            "b=$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -1); [ -n \"$b\" ] && echo \"batDir=$b\" || true"
        ]
        stdout: StdioCollector { onStreamFinished: {
            for (let ln of this.text.trim().split("\n")) {
                let i = ln.indexOf("="); if (i < 0) continue;
                let k = ln.slice(0, i), v = ln.slice(i + 1);
                if (k === "cpuTemp") cpuTempFile.path = v;
                else if (k === "nvmeTemp") nvmeTempFile.path = v;
                else if (k === "batDir") { batCapFile.path = v + "/capacity"; batStatusFile.path = v + "/status"; }
            }
            root.readTemp(); root.readNvme();
            if (Shared.Config.hasBattery) root.readBattery();
        }}
    }

    Process { id: gpuProc; command: [Shared.Config.gpuScript]; stdout: StdioCollector {
        onStreamFinished: { try {
            let d = JSON.parse(this.text);
            let tt = d.tooltip || "";
            let lines = tt.split("\n");
            let usage = 0, temp = 0, power = 0;
            for (let i = 0; i < lines.length; i++) {
                let v = parseInt(lines[i].replace(/[^0-9]/g, ""));
                if (isNaN(v)) continue;
                if (lines[i].indexOf("Usage") >= 0) usage = v;
                else if (lines[i].indexOf("Temp") >= 0) temp = v;
                else if (lines[i].indexOf("Power") >= 0) power = v;
            }
            root.gpuUsage = usage; root.gpuTemp = temp;
            root.gpuText = usage + "% " + temp + "°C " + power + "W";
            root.gpuHistory = root.pushHistory(root.gpuHistory, usage / 100);
        } catch(e) {} }
    }}

    Process { id: diskProc; command: ["sh", "-c",
        "df -h " + Shared.Config.diskMount1 + " --output=pcent,size 2>/dev/null | tail -1 && " +
        "df -h " + Shared.Config.diskMount2 + " --output=pcent,size 2>/dev/null | tail -1 && " +
        "df " + Shared.Config.diskMount1 + " --output=pcent 2>/dev/null | tail -1 && " +
        "df " + Shared.Config.diskMount2 + " --output=pcent 2>/dev/null | tail -1"
    ]; stdout: StdioCollector {
        onStreamFinished: {
            let lines = this.text.trim().split("\n");
            if (lines.length >= 1) root.diskRootText = lines[0].trim().replace(/\s+/g, " of ");
            if (lines.length >= 2) root.diskDataText = lines[1].trim().replace(/\s+/g, " of ");
            if (lines.length >= 3) root.diskRootVal = parseInt(lines[2]) / 100 || 0;
            if (lines.length >= 4) root.diskDataVal = parseInt(lines[3]) / 100 || 0;
        }
    }}

    Process { id: updateProc; command: ["bash", "-c", "timeout 120 checkupdates 2>/dev/null; echo \":$?\""]; stdout: StdioCollector {
        onStreamFinished: {
            let lines = this.text.trim().split("\n");
            let exitMatch = lines[lines.length - 1].match(/:(\d+)$/);
            let exit = exitMatch ? parseInt(exitMatch[1]) : 1;
            // checkupdates: exit 0 = updates available, exit 2 = no updates, else error
            if (exit !== 0 && exit !== 2) return;  // error — keep previous state
            let pkgs = lines.filter(l => l.trim() !== "" && !l.match(/^:/));
            let n = pkgs.length;
            root.updatesPackages = pkgs;
            if (n === 0) { root.updatesText = ""; root.updatesClass = "uptodate"; root.updatesExpanded = false; }
            else if (n > 50) { root.updatesText = n.toString(); root.updatesClass = "many"; }
            else { root.updatesText = n.toString(); root.updatesClass = "pending"; }
        }
    }}

    Process { id: alhpProc; command: ["sh", "-c", "alhp.utils -j 2>/dev/null || echo '{}'"]; stdout: StdioCollector {
        onStreamFinished: {
            try {
                let d = JSON.parse(this.text);
                let total = d.total || 0;
                let stale = d.mirror_out_of_date || false;
                root.alhpPackages = Array.isArray(d.packages) ? d.packages : [];
                if (stale) { root.alhpText = "stale"; root.alhpClass = "stale"; }
                else if (total > 0) { root.alhpText = total.toString(); root.alhpClass = "bad"; }
                else { root.alhpText = ""; root.alhpClass = "good"; root.alhpExpanded = false; }
            } catch(e) { root.alhpText = "?"; root.alhpClass = "down"; }
        }
    }}

    Process { id: netProc; command: ["sh", "-c", "ip -4 -o addr show | grep -v '" + Shared.Config.netExcludePattern + "' | head -1 | awk '{split($4,a,\"/\"); print $2 \":\" a[1]}'"]; stdout: StdioCollector {
        onStreamFinished: { let l = this.text.trim(); if (l) { let p = l.split(":"); root.networkIface = p[0] || "--"; root.networkIp = p[1] || "--"; } else { root.networkIface = "Network"; root.networkIp = "Offline"; } }
    }}

    // Power profile (laptop) — battery is read natively via readBattery().
    Process { id: ppGetProc; command: ["powerprofilesctl", "get"]; stdout: StdioCollector {
        onStreamFinished: root.powerProfile = this.text.trim()
    }}
    Process { id: ppSetProc; command: ["true"]; stdout: StdioCollector {
        onStreamFinished: { rerun(ppGetProc); }
    }}
    function setPowerProfile(profile) { ppSetProc.command = ["powerprofilesctl", "set", profile]; ppSetProc.running = true; }

    Process { id: pavuProc; command: ["pavucontrol"] }
    Process { id: idleProc; command: ["pgrep", "-x", Shared.Config.idleProcess]; stdout: StdioCollector { onStreamFinished: root.idleActive = this.text.trim().length > 0 } }
    Process { id: idleKill; command: ["killall", Shared.Config.idleProcess] }
    Process { id: idleStart; command: ["sh", "-c", "setsid " + Shared.Config.idleProcess + " > /dev/null 2>&1 &"] }
    Process { id: lockProc; command: [Shared.Config.lockCommand] }
    Process { id: logoutProc; command: Shared.Config.powerActions[1].command }
    Process { id: rebootProc; command: Shared.Config.powerActions[2].command }
    Process { id: offProc; command: Shared.Config.powerActions[3].command }

    function rerun(proc) { proc.running = false; proc.running = true; }
    Timer { interval: 5000; running: true; repeat: true; onTriggered: {
        root.readCpu(); root.readMem(); root.readTemp(); root.readNvme(); rerun(idleProc);
        if (Shared.Config.hasDiscreteGpu) rerun(gpuProc);
        if (Shared.Config.hasBattery) root.readBattery();
    }}
    Timer { interval: 30000; running: true; repeat: true; onTriggered: { rerun(diskProc); rerun(netProc); if (Shared.Config.hasPowerProfiles) rerun(ppGetProc); } }
    // checkupdates can be slow/flaky; don't restart it while a run is still in flight.
    Timer { interval: 300000; running: true; repeat: true; onTriggered: {
        if (!updateProc.running) updateProc.running = true;
        if (!alhpProc.running)   alhpProc.running = true;
    }}

    // Native reads are synchronous; stagger only the remaining process spawns.
    Component.onCompleted: {
        pathDiscovery.running = true;
        root.readCpu(); root.readMem();
        staggerTimer.running = true;
    }
    Timer {
        id: staggerTimer
        interval: 200
        onTriggered: {
            if (Shared.Config.hasDiscreteGpu) rerun(gpuProc);
            rerun(idleProc); rerun(diskProc); rerun(netProc);
            updateProc.running = true; alhpProc.running = true;
            if (Shared.Config.hasPowerProfiles) rerun(ppGetProc);
        }
    }
}
