// Rootless podman-compose stacks, led by what needs attention.
//
// The grouping is the compose project, because that is the unit a person thinks
// in: not "thirteen containers" but "the Command Center database" and "the
// capacity planner's Supabase". State decides prominence within that grouping --
// what is running gets rows, what is stopped collapses to a line -- so neither
// axis has to be chosen over the other.
//
// The findings at the top are the same crossing the Firewall page reports, seen
// from the side that can close it: the firewall knows only that something is
// listening, while this page knows which container, which compose file, and
// which token is missing from it.

import Quickshell
import QtQuick
import qs.config
import qs.services

Item {
    id: root

    objectName: "containers"

    // "images", "volumes", or empty. Removal never happens on a first press.
    property string confirmingPrune: ""

    // Projects whose stopped containers have been expanded, by project name.
    property var expanded: []

    function isExpanded(name: string): bool { return root.expanded.indexOf(name) >= 0; }

    function toggleExpanded(name: string): void {
        root.expanded = root.isExpanded(name)
            ? root.expanded.filter(entry => entry !== name)
            : root.expanded.concat([name]);
    }

    function uptimeOf(container: var): string {
        const status = String(container.status ?? "");
        // podman already phrases this well ("Up 32 hours (healthy)"); the health
        // is shown separately, so only the duration is wanted here.
        const match = /^Up ([^(]+)/.exec(status);
        return match ? match[1].trim() : "";
    }

    function portSummary(container: var): string {
        const ports = container.ports ?? [];
        if (ports.length === 0)
            return "no published ports";
        return ports.map(port => {
            const host = String(port.hostIp ?? "");
            const where = host === "" ? "every interface" : host;
            return where + ":" + port.hostPort + " → " + port.containerPort;
        }).join(", ");
    }

    Component.onCompleted: Containers.refresh()

    // ── the list ────────────────────────────────────────────────────────────

    SettingsPage {
        anchors.fill: parent
        visible: Containers.logTarget === ""

        title: "Containers"
        lede: "Local services you run for development — what they expose, and what they cost."

        TextRow {
            visible: Containers.lastError !== ""
            label: "That did not work"
            detail: Containers.lastError
            value: ""
            divider: false
        }

        TextRow {
            visible: Containers.scanned && !Containers.available
            label: "podman is not available"
            detail: "Nothing here can be shown until podman is installed."
            value: ""
            divider: false
        }

        // ── finding: published to the network ───────────────────────────────

        SettingsCard {
            visible: Containers.reachable.length > 0
            title: Containers.reachable.length === 1
                ? "A container is published to your whole network"
                : "Containers are published to your whole network"
            subtitle: "These bind every interface, so any machine on your network can connect. "
                + "For a development database, loopback is almost always what you want. "
                + "Binding adds an address to the compose file and changes nothing else."

            Repeater {
                model: Containers.reachable

                delegate: SettingRow {
                    required property var modelData
                    width: parent.width
                    label: String(modelData.container ?? "")
                    detail: "Port " + modelData.hostPort + "/" + String(modelData.protocol ?? "tcp")
                        + " · " + String(modelData.image ?? "")
                        + (modelData.configFile
                            ? "\n" + Containers.shorten(String(modelData.configFile))
                            : "\nNo compose file is recorded for this container, so it cannot be bound from here.")
                    controlWidth: 250

                    Row {
                        anchors.right: parent.right
                        anchors.verticalCenter: parent.verticalCenter
                        spacing: 8

                        SettingsButton {
                            text: "Open compose file"
                            visible: String(modelData.configFile ?? "") !== ""
                            enabled: !Containers.busy
                            onClicked: Containers.openFile(String(modelData.configFile))
                        }

                        SettingsButton {
                            text: "Bind to localhost"
                            tone: "accent"
                            // Needs both halves of the label to find the entry in
                            // the compose file; the Supabase CLI records neither.
                            visible: String(modelData.configFile ?? "") !== ""
                                && String(modelData.service ?? "") !== ""
                            enabled: !Containers.busy
                            onClicked: Containers.bindLocal(
                                String(modelData.project), String(modelData.service))
                        }
                    }
                }
            }

            TextRow {
                visible: Containers.reachable.length > 0
                label: "After binding"
                detail: "The change takes effect the next time the stack comes up, because a "
                    + "published port is fixed when the container is created."
                value: ""
                divider: false
            }
        }

        // ── finding: disk nothing references ────────────────────────────────

        SettingsCard {
            visible: Containers.unusedImages.length > 0 || Containers.unusedVolumes.length > 0
            title: Containers.formatBytes(Containers.reclaimable) + " nothing is using"
            subtitle: "Images and volumes no container references. Removing them frees the space; "
                + "anything still needed downloads again on next use."

            Repeater {
                model: Containers.unusedImages.slice(0, 5)

                delegate: TextRow {
                    required property var modelData
                    width: parent.width
                    label: String(modelData.name ?? "")
                    detail: ""
                    value: Containers.formatBytes(Number(modelData.size ?? 0))
                }
            }

            TextRow {
                visible: Containers.unusedImages.length > 5
                label: "and " + (Containers.unusedImages.length - 5) + " more"
                detail: ""
                value: ""
            }

            SettingRow {
                width: parent.width
                label: root.confirmingPrune === "images"
                    ? "Remove " + Containers.unusedImages.length + " images?"
                    : "Unused images"
                detail: root.confirmingPrune === "images"
                    ? "Each is removed by name. Nothing that a container references is touched."
                    : Containers.unusedImages.length + " images, "
                        + Containers.formatBytes(Number(Containers.disk.imagesReclaimable ?? 0))
                visible: Containers.unusedImages.length > 0
                controlWidth: 230

                Row {
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                    spacing: 8

                    SettingsButton {
                        text: root.confirmingPrune === "images" ? "Keep them" : "Remove…"
                        enabled: !Containers.busy
                        onClicked: root.confirmingPrune =
                            root.confirmingPrune === "images" ? "" : "images"
                    }

                    SettingsButton {
                        visible: root.confirmingPrune === "images"
                        text: "Remove them"
                        tone: "danger"
                        enabled: !Containers.busy
                        onClicked: {
                            root.confirmingPrune = "";
                            Containers.pruneImages();
                        }
                    }
                }
            }

            SettingRow {
                width: parent.width
                visible: Containers.unusedVolumes.length > 0
                label: root.confirmingPrune === "volumes"
                    ? "Remove " + Containers.unusedVolumes.length + " volumes?"
                    : "Unused volumes"
                detail: root.confirmingPrune === "volumes"
                    ? "A volume holds data. These are the ones podman reports as referenced by "
                      + "nothing, but removing them cannot be undone."
                    : Containers.unusedVolumes.length + " volumes, "
                        + Containers.formatBytes(Number(Containers.disk.volumesReclaimable ?? 0))
                controlWidth: 230
                divider: false

                Row {
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                    spacing: 8

                    SettingsButton {
                        text: root.confirmingPrune === "volumes" ? "Keep them" : "Remove…"
                        enabled: !Containers.busy
                        onClicked: root.confirmingPrune =
                            root.confirmingPrune === "volumes" ? "" : "volumes"
                    }

                    SettingsButton {
                        visible: root.confirmingPrune === "volumes"
                        text: "Remove them"
                        tone: "danger"
                        enabled: !Containers.busy
                        onClicked: {
                            root.confirmingPrune = "";
                            Containers.pruneVolumes();
                        }
                    }
                }
            }
        }

        // ── the stacks ──────────────────────────────────────────────────────

        Repeater {
            model: Containers.projects

            delegate: SettingsCard {
                id: projectCard

                required property var modelData

                readonly property var runningSet:
                    (projectCard.modelData.containers ?? []).filter(c => c.state === "running")
                readonly property var stoppedSet:
                    (projectCard.modelData.containers ?? []).filter(c => c.state !== "running")
                readonly property string projectName: String(projectCard.modelData.name ?? "")

                title: String(projectCard.modelData.title ?? "")
                subtitle: {
                    const total = Number(projectCard.modelData.total ?? 0);
                    const up = Number(projectCard.modelData.running ?? 0);
                    const where = String(projectCard.modelData.configFile ?? "");
                    const counts = up === 0
                        ? total + (total === 1 ? " container, stopped" : " containers, all stopped")
                        : up + " of " + total + " running";
                    return where === "" ? counts : counts + " · " + Containers.shorten(where);
                }

                SettingRow {
                    width: parent.width
                    label: "The whole stack"
                    detail: projectCard.runningSet.length === 0
                        ? "Starts every container this project defines."
                        : "Stopping leaves the containers in place; nothing is removed."
                    controlWidth: 250
                    divider: projectCard.runningSet.length > 0
                             || projectCard.stoppedSet.length > 0

                    Row {
                        anchors.right: parent.right
                        anchors.verticalCenter: parent.verticalCenter
                        spacing: 8

                        SettingsButton {
                            text: "Start all"
                            visible: projectCard.stoppedSet.length > 0
                            enabled: !Containers.busy
                            onClicked: Containers.startProject(projectCard.projectName)
                        }

                        SettingsButton {
                            text: "Restart all"
                            visible: projectCard.runningSet.length > 0
                            enabled: !Containers.busy
                            onClicked: Containers.restartProject(projectCard.projectName)
                        }

                        SettingsButton {
                            text: "Stop all"
                            visible: projectCard.runningSet.length > 0
                            enabled: !Containers.busy
                            onClicked: Containers.stopProject(projectCard.projectName)
                        }
                    }
                }

                Repeater {
                    model: projectCard.runningSet

                    delegate: SettingRow {
                        required property var modelData
                        width: parent.width
                        label: String(modelData.name ?? "")
                        detail: String(modelData.image ?? "") + " · " + root.portSummary(modelData)
                        value: {
                            const health = String(modelData.health ?? "");
                            const up = root.uptimeOf(modelData);
                            if (health !== "" && up !== "")
                                return health + " · " + up;
                            return health !== "" ? health : up;
                        }
                        controlWidth: 250

                        Row {
                            anchors.right: parent.right
                            anchors.verticalCenter: parent.verticalCenter
                            spacing: 8

                            SettingsButton {
                                text: "Logs"
                                onClicked: Containers.openLogs(String(modelData.name))
                            }

                            SettingsButton {
                                text: "Restart"
                                enabled: !Containers.busy
                                onClicked: Containers.restart(String(modelData.name))
                            }

                            SettingsButton {
                                text: "Stop"
                                enabled: !Containers.busy
                                onClicked: Containers.stop(String(modelData.name))
                            }
                        }
                    }
                }

                SettingRow {
                    width: parent.width
                    visible: projectCard.stoppedSet.length > 0
                    activatable: true
                    divider: root.isExpanded(projectCard.projectName)
                    label: (root.isExpanded(projectCard.projectName) ? "▾  " : "▸  ")
                        + projectCard.stoppedSet.length
                        + (projectCard.stoppedSet.length === 1
                            ? " stopped container" : " stopped containers")
                    detail: {
                        const bad = projectCard.stoppedSet.filter(c => Number(c.exitCode ?? 0) !== 0);
                        return bad.length === 0
                            ? ""
                            : bad.length + (bad.length === 1 ? " exited" : " exited") + " badly.";
                    }
                    onActivated: root.toggleExpanded(projectCard.projectName)
                }

                Repeater {
                    model: root.isExpanded(projectCard.projectName) ? projectCard.stoppedSet : []

                    delegate: SettingRow {
                        required property var modelData
                        required property int index
                        width: parent.width
                        label: String(modelData.name ?? "")
                        detail: String(modelData.image ?? "") + " · " + String(modelData.status ?? "")
                        controlWidth: 180
                        divider: index < projectCard.stoppedSet.length - 1

                        Row {
                            anchors.right: parent.right
                            anchors.verticalCenter: parent.verticalCenter
                            spacing: 8

                            SettingsButton {
                                text: "Logs"
                                onClicked: Containers.openLogs(String(modelData.name))
                            }

                            SettingsButton {
                                text: "Start"
                                enabled: !Containers.busy
                                onClicked: Containers.start(String(modelData.name))
                            }
                        }
                    }
                }
            }
        }

        // ── containers no compose project claims ────────────────────────────

        SettingsCard {
            visible: Containers.loose.length > 0
            title: "Not part of a project"
            subtitle: "Started directly rather than by a compose file."

            Repeater {
                model: Containers.loose

                delegate: SettingRow {
                    required property var modelData
                    required property int index
                    width: parent.width
                    label: String(modelData.name ?? "")
                    detail: String(modelData.image ?? "") + " · " + String(modelData.status ?? "")
                    controlWidth: 180
                    divider: index < Containers.loose.length - 1

                    Row {
                        anchors.right: parent.right
                        anchors.verticalCenter: parent.verticalCenter
                        spacing: 8

                        SettingsButton {
                            text: "Logs"
                            onClicked: Containers.openLogs(String(modelData.name))
                        }

                        SettingsButton {
                            text: modelData.state === "running" ? "Stop" : "Start"
                            enabled: !Containers.busy
                            onClicked: modelData.state === "running"
                                ? Containers.stop(String(modelData.name))
                                : Containers.start(String(modelData.name))
                        }
                    }
                }
            }
        }

        TextRow {
            visible: Containers.scanned && Containers.available && Containers.total === 0
            label: "No containers"
            detail: "Nothing has been created on this machine yet."
            value: ""
            divider: false
        }
    }

    // ── logs ────────────────────────────────────────────────────────────────
    //
    // A drill-in rather than a panel inside the scrolling page: logs need their
    // own scrollback, and nesting one scrolling view inside another makes the
    // wheel ambiguous over the region where you most want to use it.

    Item {
        anchors.fill: parent
        visible: Containers.logTarget !== ""

        Column {
            id: logHeader
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.top: parent.top
            anchors.leftMargin: 34
            anchors.rightMargin: 34
            anchors.topMargin: 30
            spacing: 6

            Row {
                width: parent.width
                spacing: 12

                SettingsButton {
                    text: "‹  Back"
                    anchors.verticalCenter: parent.verticalCenter
                    onClicked: Containers.closeLogs()
                }

                Text {
                    anchors.verticalCenter: parent.verticalCenter
                    text: Containers.logTarget
                    color: Theme.fg
                    font.family: Theme.fontFamily
                    font.pixelSize: 22
                    font.weight: Font.DemiBold
                }
            }

            Text {
                width: parent.width
                text: Containers.logError !== ""
                    ? Containers.logError
                    : (Containers.logFollowing
                        ? "Following. The last " + Containers.logLines.count + " lines are shown."
                        : "The stream has ended.")
                color: Containers.logError !== "" ? Theme.danger : Theme.fgDim
                font.family: Theme.fontFamily
                font.pixelSize: Theme.fontSizeSmall
                wrapMode: Text.WordWrap
            }
        }

        Rectangle {
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.top: logHeader.bottom
            anchors.bottom: parent.bottom
            anchors.leftMargin: 34
            anchors.rightMargin: 34
            anchors.topMargin: 14
            anchors.bottomMargin: 30
            radius: Theme.cardRadius + 2
            color: Theme.alpha(Theme.bgDark, 0.75)
            border.width: 1
            border.color: Theme.alpha(Theme.fg, 0.07)

            ListView {
                id: logList

                anchors.fill: parent
                anchors.margins: 12
                clip: true
                model: Containers.logLines
                spacing: 1
                boundsBehavior: Flickable.StopAtBounds
                cacheBuffer: 400

                // Stay pinned to the newest line while the reader is already at
                // the bottom, and leave the view alone the moment they scroll up
                // to read something.
                property bool pinned: true
                onContentYChanged: logList.pinned =
                    logList.contentY >= logList.contentHeight - logList.height - 24
                onCountChanged: if (logList.pinned) logList.positionViewAtEnd()

                delegate: Text {
                    required property string line
                    width: logList.width - 24
                    text: line
                    color: Theme.fgDim
                    font.family: Theme.fontMono
                    font.pixelSize: Theme.fontSizeSmall
                    wrapMode: Text.WrapAnywhere
                    textFormat: Text.PlainText
                }
            }
        }
    }
}
