1 contributor
1796 lines | 70.546kb
//
//  ChargeSessionDetailView.swift
//  USB Meter
//
//  Created by Codex on 22/04/2026.
//

import SwiftUI

enum ChargeSessionDetailPresentation {
    case navigation
    case embedded
}

struct ChargeSessionDetailView: View {
    private enum FinalCheckpoint: Hashable {
        case full
        case skip
        case custom

        var label: String {
            switch self {
            case .full:   return "Full"
            case .skip:   return "Skip"
            case .custom: return "Other %"
            }
        }

        var icon: String {
            switch self {
            case .full:   return "battery.100percent"
            case .skip:   return "minus.circle"
            case .custom: return "pencil"
            }
        }
    }

    @EnvironmentObject private var appData: AppData

    let chargedDeviceID: UUID
    let sessionID: UUID
    let monitoringMeter: Meter?
    let presentation: ChargeSessionDetailPresentation

    @State private var pendingCheckpointDeletion: ChargeCheckpointSummary?
    @State private var pendingSessionDeletion: ChargeSessionSummary?
    @State private var pendingSessionStopRequest: ChargeSessionStopRequest?
    @State private var detectedTrimWindow: ChargingWindowDetector.DetectedWindow?
    @State private var trimBannerDismissedForSessionID: UUID?
    @State private var showingInlineTargetEditor = false
    @State private var draftTargetText = ""
    @State private var showingStopConfirm = false
    @State private var finalCheckpointMode: FinalCheckpoint = .skip
    @State private var finalCheckpointText = ""
    @State private var stopFailureMessage: String?

    init(
        chargedDeviceID: UUID,
        sessionID: UUID,
        monitoringMeter: Meter? = nil,
        presentation: ChargeSessionDetailPresentation = .navigation
    ) {
        self.chargedDeviceID = chargedDeviceID
        self.sessionID = sessionID
        self.monitoringMeter = monitoringMeter
        self.presentation = presentation
    }

    private var chargedDevice: ChargedDeviceSummary? {
        appData.chargedDeviceSummary(id: chargedDeviceID)
    }

    private var session: ChargeSessionSummary? {
        chargedDevice?.sessions.first(where: { $0.id == sessionID })
    }

    private var liveMonitoringMeter: Meter? {
        guard let session,
              session.status.isOpen,
              let meterMACAddress = session.meterMACAddress else {
            return nil
        }

        if let monitoringMeter,
           monitoringMeter.btSerial.macAddress.description == meterMACAddress {
            return monitoringMeter
        }

        return appData.meters.values.first {
            $0.btSerial.macAddress.description == meterMACAddress
        }
    }

    private var hasMonitoringControls: Bool {
        session?.status.isOpen == true && liveMonitoringMeter != nil
    }

    private var shouldShowTrimBanner: Bool {
        guard hasMonitoringControls,
              let session,
              session.isTrimmed == false,
              trimBannerDismissedForSessionID != session.id,
              let detectedTrimWindow else {
            return false
        }
        return detectedTrimWindow.trimRatio > ChargingWindowDetector.significantTrimThreshold
    }

    var body: some View {
        Group {
            if let chargedDevice, let session {
                content(chargedDevice: chargedDevice, session: session)
            } else {
                unavailableState
            }
        }
        .sheet(item: $pendingSessionStopRequest) { request in
            ChargeSessionCompletionSheetView(
                sessionID: request.sessionID,
                title: request.title,
                confirmTitle: request.confirmTitle,
                explanation: request.explanation,
                monitoringMeter: liveMonitoringMeter,
                appliesTrim: request.appliesTrim,
                trimStart: request.trimStart,
                trimEnd: request.trimEnd
            )
            .environmentObject(appData)
        }
        .alert(item: $pendingCheckpointDeletion) { checkpoint in
            Alert(
                title: Text("Delete Battery Checkpoint"),
                message: Text("Remove the checkpoint at \(checkpoint.timestamp.format()) with \(checkpoint.batteryPercent.format(decimalDigits: 0))% from this session?"),
                primaryButton: .destructive(Text("Delete")) {
                    _ = appData.deleteBatteryCheckpoint(
                        checkpointID: checkpoint.id,
                        for: checkpoint.sessionID
                    )
                },
                secondaryButton: .cancel()
            )
        }
        .alert(item: $pendingSessionDeletion) { session in
            Alert(
                title: Text("Delete Session?"),
                message: Text("Deleting this charging session also recalculates capacity and every derived metric that used it."),
                primaryButton: .destructive(Text("Delete")) {
                    _ = appData.deleteChargeSession(sessionID: session.id)
                },
                secondaryButton: .cancel()
            )
        }
        .onAppear {
            syncMonitoringRestore()
            runTrimDetection()
        }
        .onChange(of: session?.id) { _ in
            pendingSessionStopRequest = nil
            detectedTrimWindow = nil
            trimBannerDismissedForSessionID = nil
            showingInlineTargetEditor = false
            draftTargetText = ""
            showingStopConfirm = false
            finalCheckpointMode = .skip
            finalCheckpointText = ""
            stopFailureMessage = nil
            syncMonitoringRestore()
            runTrimDetection()
        }
        .onChange(of: session?.aggregatedSamples.count) { _ in
            syncMonitoringRestore()
            runTrimDetection()
        }
        .onChange(of: finalCheckpointMode) { _ in
            stopFailureMessage = nil
        }
        .onChange(of: finalCheckpointText) { _ in
            stopFailureMessage = nil
        }
    }

    private func content(
        chargedDevice: ChargedDeviceSummary,
        session: ChargeSessionSummary
    ) -> some View {
        ScrollView {
            VStack(spacing: 16) {
                if hasMonitoringControls {
                    monitoringSessionCard(session, chargedDevice: chargedDevice)

                    if shouldShowTrimBanner {
                        trimDetectionBanner(session)
                    }

                    if shouldShowSessionChart(session) {
                        chartCard(session)
                    }
                } else {
                    overviewCard(session, chargedDevice: chargedDevice)
                    batteryCard(session, chargedDevice: chargedDevice)

                    if shouldShowSessionChart(session) {
                        chartCard(session)
                    }

                    if session.status.isOpen {
                        followerNoticeCard(session)
                    } else {
                        managementCard(session)
                    }
                }
            }
            .padding(presentation == .embedded ? 16 : 20)
        }
        .background(
            LinearGradient(
                colors: [statusTint(for: session).opacity(0.14), Color.clear],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()
        )
        .navigationTitle(session.status.isOpen ? "Current Session" : "Session Details")
    }

    private var unavailableState: some View {
        VStack(spacing: 12) {
            Image(systemName: "bolt.slash")
                .font(.title2)
                .foregroundColor(.secondary)
            Text("This session is no longer available.")
                .font(.headline)
            Text("It may have been deleted or synced from another device.")
                .font(.footnote)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .padding(24)
        .navigationTitle("Session")
    }

    private func monitoringSessionCard(
        _ session: ChargeSessionSummary,
        chargedDevice: ChargedDeviceSummary
    ) -> some View {
        let displayedEnergyWh = displayedSessionEnergyWh(for: session)
        let batteryPrediction = chargedDevice.batteryLevelPrediction(
            for: session,
            effectiveEnergyWhOverride: displayedEnergyWh
        )

        return VStack(alignment: .leading, spacing: 14) {
            HStack {
                ChargedDeviceIdentityLabelView(chargedDevice: chargedDevice, iconPointSize: 16)
                    .font(.headline)

                Spacer()

                Text(session.status.title)
                    .font(.caption.weight(.bold))
                    .foregroundColor(monitoringStatusColor(for: session))
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .meterCard(tint: monitoringStatusColor(for: session), fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999)
            }

            if let batteryPrediction {
                batteryGaugeSection(
                    prediction: batteryPrediction,
                    session: session,
                    displayedEnergyWh: displayedEnergyWh
                )
            }

            sessionMetricsGrid(
                session: session,
                chargedDevice: chargedDevice,
                displayedEnergyWh: displayedEnergyWh,
                hasPrediction: batteryPrediction != nil
            )

            if session.stopThresholdAmps > 0 {
                Text("Stop threshold: \(session.stopThresholdAmps.format(decimalDigits: 2)) A")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            if let sessionWarning = sessionWarning(for: session) {
                Label(sessionWarning, systemImage: "exclamationmark.triangle")
                    .font(.caption)
                    .foregroundColor(.orange)
            }

            if session.isPaused {
                Label(
                    "Paused \(session.pausedAt?.format() ?? session.lastObservedAt.format()). Auto-stops after 10 min.",
                    systemImage: "pause.circle"
                )
                .font(.caption)
                .foregroundColor(.secondary)
            }

            if session.requiresCompletionConfirmation && !showingStopConfirm {
                completionConfirmationCard(session)
            }

            BatteryCheckpointSectionView(
                sessionID: session.id,
                checkpoints: session.checkpoints,
                message: "Checkpoints are used for capacity estimation and the typical charge curve.",
                canAddCheckpoint: appData.canAddBatteryCheckpoint(to: session.id),
                canDeleteCheckpoint: true,
                requirementMessage: appData.batteryCheckpointCaptureRequirementMessage(for: session.id),
                effectiveEnergyWhOverride: displayedEnergyWh,
                onDelete: { checkpoint in
                    pendingCheckpointDeletion = checkpoint
                }
            )

            targetSectionView(
                session: session,
                predictedPercent: batteryPrediction?.predictedPercent
            )

            if showingStopConfirm {
                stopConfirmPanel(
                    session: session,
                    displayedEnergyWh: displayedEnergyWh
                )
            } else {
                monitoringActionRow(session)
            }
        }
        .padding(18)
        .meterCard(tint: .green, fillOpacity: 0.14, strokeOpacity: 0.20)
    }

    private func overviewCard(
        _ session: ChargeSessionSummary,
        chargedDevice: ChargedDeviceSummary
    ) -> some View {
        MeterInfoCardView(
            title: session.status.isOpen ? "Open Session" : "Overview",
            tint: statusTint(for: session),
            isCollapsible: true
        ) {
            VStack(alignment: .leading, spacing: 10) {
                MeterInfoRowView(label: "Device", value: chargedDevice.name)

                Divider()

                HStack(alignment: .top, spacing: 12) {
                    overviewStatCell(label: "Started", value: session.startedAt.format())
                    if let endedAt = session.endedAt {
                        overviewStatCell(label: "Ended", value: endedAt.format())
                    }
                }

                HStack(alignment: .top, spacing: 12) {
                    overviewStatCell(label: "Duration", value: sessionDurationText(session))
                    overviewStatCell(label: "Status", value: session.status.title)
                }

                Divider()

                HStack(alignment: .top, spacing: 12) {
                    overviewStatCell(label: "Charging Type", value: session.chargingTransportMode.title)
                    overviewStatCell(label: "Charging Mode", value: session.chargingStateMode.title)
                }

                HStack(alignment: .top, spacing: 12) {
                    overviewStatCell(label: "Source", value: session.sourceMode.title)
                    overviewStatCell(label: "Auto Stop", value: autoStopDescription(for: session))
                }

                if session.isTrimmed {
                    Divider()
                    HStack(alignment: .top, spacing: 12) {
                        overviewStatCell(label: "Trim Start", value: session.effectiveTrimStart.format())
                        overviewStatCell(label: "Trim End", value: session.effectiveTrimEnd.format())
                    }
                }

                let meterLabel: String? = session.meterName ?? session.meterMACAddress
                if meterLabel != nil || session.meterModel != nil {
                    Divider()
                    HStack(alignment: .top, spacing: 12) {
                        if let label = meterLabel {
                            overviewStatCell(label: "Meter", value: label)
                        }
                        if let model = session.meterModel {
                            overviewStatCell(label: "Meter Model", value: model)
                        }
                    }
                }

                if session.minimumObservedCurrentAmps != nil
                    || session.maximumObservedCurrentAmps != nil
                    || session.maximumObservedPowerWatts != nil
                    || session.maximumObservedVoltageVolts != nil
                    || (chargedDevice.isCharger && session.selectedSourceVoltageVolts != nil)
                    || session.completionCurrentAmps != nil
                    || session.selectedDataGroup != nil {

                    Divider()

                    if session.minimumObservedCurrentAmps != nil || session.maximumObservedCurrentAmps != nil {
                        HStack(alignment: .top, spacing: 12) {
                            if let v = session.minimumObservedCurrentAmps {
                                overviewStatCell(label: "Min Current", value: "\(v.format(decimalDigits: 2)) A")
                            }
                            if let v = session.maximumObservedCurrentAmps {
                                overviewStatCell(label: "Max Current", value: "\(v.format(decimalDigits: 2)) A")
                            }
                        }
                    }

                    if session.maximumObservedPowerWatts != nil || session.maximumObservedVoltageVolts != nil {
                        HStack(alignment: .top, spacing: 12) {
                            if let v = session.maximumObservedPowerWatts {
                                overviewStatCell(label: "Max Power", value: "\(v.format(decimalDigits: 2)) W")
                            }
                            if let v = session.maximumObservedVoltageVolts {
                                overviewStatCell(label: "Max Voltage", value: "\(v.format(decimalDigits: 2)) V")
                            }
                        }
                    }

                    if session.completionCurrentAmps != nil
                        || (chargedDevice.isCharger && session.selectedSourceVoltageVolts != nil) {
                        HStack(alignment: .top, spacing: 12) {
                            if let v = session.completionCurrentAmps {
                                overviewStatCell(label: "Completion Current", value: "\(v.format(decimalDigits: 2)) A")
                            }
                            if chargedDevice.isCharger, let v = session.selectedSourceVoltageVolts {
                                overviewStatCell(label: "Selected Voltage", value: "\(v.format(decimalDigits: 2)) V")
                            }
                        }
                    }

                    if let dg = session.selectedDataGroup {
                        MeterInfoRowView(label: "Data Group", value: "\(dg)")
                    }
                }
            }
        }
    }

    private func overviewStatCell(label: String, value: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(label)
                .font(.caption2)
                .foregroundColor(.secondary)
            Text(value)
                .font(.footnote.weight(.medium))
                .monospacedDigit()
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

    private func batteryCard(
        _ session: ChargeSessionSummary,
        chargedDevice: ChargedDeviceSummary
    ) -> some View {
        let displayedEnergyWh = displayedSessionEnergyWh(for: session)
        let batteryPrediction = chargedDevice.batteryLevelPrediction(
            for: session,
            effectiveEnergyWhOverride: hasMonitoringControls ? displayedEnergyWh : nil
        )

        return MeterInfoCardView(title: "Battery", tint: .orange, isCollapsible: true) {
            VStack(alignment: .leading, spacing: 10) {

                // Energy
                HStack(alignment: .top, spacing: 12) {
                    overviewStatCell(label: "Battery Energy", value: "\(displayedEnergyWh.format(decimalDigits: 3)) Wh")
                    if let capacityEstimateWh = session.capacityEstimateWh {
                        overviewStatCell(label: "Capacity Estimate", value: "\(capacityEstimateWh.format(decimalDigits: 2)) Wh")
                    }
                }
                if abs(displayedEnergyWh - session.measuredEnergyWh) > 0.01 {
                    MeterInfoRowView(label: "Measured Energy", value: "\(session.measuredEnergyWh.format(decimalDigits: 3)) Wh")
                }
                if let effectiveBatteryEnergyWh = session.effectiveBatteryEnergyWh,
                   abs(effectiveBatteryEnergyWh - session.measuredEnergyWh) > 0.01 {
                    MeterInfoRowView(label: "Effective Battery Energy", value: "\(effectiveBatteryEnergyWh.format(decimalDigits: 3)) Wh")
                }
                if let chargerID = session.chargerID,
                   let charger = appData.chargedDeviceSummary(id: chargerID) {
                    MeterInfoRowView(label: chargedDevice.hasMultipleChargingTransports ? "Wireless Charger" : "Charger", value: charger.name)
                }
                if let wirelessSessionHint = wirelessSessionHint(for: session) {
                    Text(wirelessSessionHint)
                        .font(.caption2)
                        .foregroundColor(session.shouldWarnAboutLowWirelessEfficiency ? .orange : .secondary)
                }
                if let sessionWarning = sessionWarning(for: session) {
                    Label(sessionWarning, systemImage: "exclamationmark.triangle")
                        .font(.caption2)
                        .foregroundColor(.orange)
                }

                // Battery percentages
                if session.startBatteryPercent != nil || session.endBatteryPercent != nil {
                    Divider()
                    HStack(alignment: .top, spacing: 12) {
                        if let v = session.startBatteryPercent {
                            overviewStatCell(label: "Start Battery", value: "\(v.format(decimalDigits: 0))%")
                        }
                        if let v = session.endBatteryPercent {
                            overviewStatCell(label: "End Battery", value: "\(v.format(decimalDigits: 0))%")
                        }
                    }
                    if session.batteryDeltaPercent != nil || session.targetBatteryPercent != nil {
                        HStack(alignment: .top, spacing: 12) {
                            if let v = session.batteryDeltaPercent {
                                overviewStatCell(label: "Battery Delta", value: "\(v.format(decimalDigits: 0))%")
                            }
                            if let v = session.targetBatteryPercent {
                                overviewStatCell(label: "Target", value: "\(v.format(decimalDigits: 0))%")
                            }
                        }
                    }
                    if let batteryPrediction {
                        HStack(alignment: .top, spacing: 12) {
                            overviewStatCell(label: "Predicted Battery", value: "\(batteryPrediction.predictedPercent.format(decimalDigits: 0))%")
                        }
                        Text(
                            "Anchored to \(batteryPrediction.anchorDescription) at \(batteryPrediction.anchorPercent.format(decimalDigits: 0))% using \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh estimated capacity."
                        )
                        .font(.caption2)
                        .foregroundColor(.secondary)
                    }
                }

                // Checkpoints
                Divider()
                BatteryCheckpointSectionView(
                    sessionID: session.id,
                    checkpoints: session.checkpoints,
                    message: session.status.isOpen
                        ? "The checkpoint is stored on the active charge session and is used for capacity estimation and charge-level prediction."
                        : "These checkpoints were saved with this closed session and feed capacity estimation and charge-level prediction.",
                    canAddCheckpoint: hasMonitoringControls && appData.canAddBatteryCheckpoint(to: session.id),
                    canDeleteCheckpoint: hasMonitoringControls || session.status.isOpen == false,
                    requirementMessage: session.status.isOpen ? appData.batteryCheckpointCaptureRequirementMessage(for: session.id) : nil,
                    effectiveEnergyWhOverride: hasMonitoringControls ? displayedEnergyWh : nil,
                    onDelete: { checkpoint in
                        pendingCheckpointDeletion = checkpoint
                    }
                )
            }
        }
    }

    private func batteryGaugeSection(
        prediction: BatteryLevelPrediction,
        session: ChargeSessionSummary,
        displayedEnergyWh: Double
    ) -> some View {
        let percent = prediction.predictedPercent
        let color = batteryColor(for: percent)
        let duration = displayedSessionDuration(for: session)
        let rateWhPerSec: Double? = duration > 300 && displayedEnergyWh > 0.01
            ? displayedEnergyWh / duration
            : nil
        let etaToFull = etaText(
            rateWhPerSec: rateWhPerSec,
            remainingWh: max(prediction.estimatedCapacityWh - displayedEnergyWh, 0),
            isRelevant: percent < 98
        )
        let etaToTarget = etaToTargetText(
            session: session,
            prediction: prediction,
            displayedEnergyWh: displayedEnergyWh,
            rateWhPerSec: rateWhPerSec
        )

        return VStack(spacing: 10) {
            HStack(alignment: .lastTextBaseline, spacing: 8) {
                HStack(alignment: .lastTextBaseline, spacing: 3) {
                    Text("\(Int(percent.rounded()))")
                        .font(.system(size: 52, weight: .bold, design: .rounded))
                        .foregroundColor(color)
                        .monospacedDigit()
                    Text("%")
                        .font(.title2.weight(.semibold))
                        .foregroundColor(color.opacity(0.8))
                }

                Spacer()

                VStack(alignment: .trailing, spacing: 2) {
                    Text("\(prediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh")
                        .font(.callout.weight(.bold))
                        .foregroundColor(.orange)
                        .monospacedDigit()
                    Text("est. capacity")
                        .font(.caption2)
                        .foregroundColor(.secondary)
                }
            }

            batteryProgressBar(
                percent: percent,
                startPercent: session.startBatteryPercent,
                targetPercent: session.targetBatteryPercent
            )

            HStack(spacing: 14) {
                if let etaToFull {
                    etaPill(icon: "clock.fill", tint: .green, value: etaToFull, label: "to full")
                }

                if let etaToTarget, let target = session.targetBatteryPercent {
                    etaPill(
                        icon: "bell.badge.fill",
                        tint: .indigo,
                        value: etaToTarget,
                        label: "to \(Int(target.rounded()))%"
                    )
                }

                Spacer()

                Text("anchored to \(prediction.anchorDescription) at \(prediction.anchorPercent.format(decimalDigits: 0))%")
                    .font(.caption2)
                    .foregroundColor(.secondary)
                    .multilineTextAlignment(.trailing)
            }
        }
        .padding(14)
        .meterCard(tint: color, fillOpacity: 0.10, strokeOpacity: 0.16, cornerRadius: 16)
    }

    private func etaPill(icon: String, tint: Color, value: String, label: String) -> some View {
        VStack(alignment: .leading, spacing: 1) {
            HStack(spacing: 4) {
                Image(systemName: icon)
                    .font(.caption)
                    .foregroundColor(tint)
                Text(value)
                    .font(.caption.weight(.bold))
            }
            Text(label)
                .font(.caption2)
                .foregroundColor(.secondary)
        }
    }

    private func batteryProgressBar(
        percent: Double,
        startPercent: Double?,
        targetPercent: Double?
    ) -> some View {
        let color = batteryColor(for: percent)
        return GeometryReader { geo in
            let width = geo.size.width
            ZStack(alignment: .leading) {
                Capsule()
                    .fill(Color.primary.opacity(0.10))
                Rectangle()
                    .fill(
                        LinearGradient(
                            colors: [color.opacity(0.6), color],
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )
                    .frame(width: max(width * CGFloat(percent / 100), 4))
                    .animation(.easeInOut(duration: 0.4), value: percent)
                if let start = startPercent, start > 2, start < 98 {
                    Rectangle()
                        .fill(Color.white.opacity(0.55))
                        .frame(width: 2, height: 20)
                        .offset(x: width * CGFloat(start / 100) - 1)
                }
                if let target = targetPercent {
                    Rectangle()
                        .fill(Color.indigo.opacity(0.9))
                        .frame(width: 2.5, height: 20)
                        .offset(x: width * CGFloat(target / 100) - 1.25)
                }
            }
            .clipShape(Capsule())
        }
        .frame(height: 20)
    }

    private func sessionMetricsGrid(
        session: ChargeSessionSummary,
        chargedDevice: ChargedDeviceSummary,
        displayedEnergyWh: Double,
        hasPrediction: Bool
    ) -> some View {
        let capacityFallback: Double? = hasPrediction ? nil : (
            session.capacityEstimateWh
                ?? chargedDevice.estimatedBatteryCapacityWh(for: session.chargingTransportMode)
                ?? chargedDevice.estimatedBatteryCapacityWh
        )
        let columns = [GridItem(.flexible()), GridItem(.flexible())]

        return LazyVGrid(columns: columns, spacing: 8) {
            metricCell(label: "Energy", value: "\(displayedEnergyWh.format(decimalDigits: 3)) Wh", tint: .blue)
            metricCell(label: "Duration", value: formatDuration(displayedSessionDuration(for: session)), tint: .teal)

            if shouldShowChargingTransport(for: session, chargedDevice: chargedDevice) {
                metricCell(label: "Type", value: session.chargingTransportMode.title, tint: .orange)
            }
            if shouldShowChargingState(for: session, chargedDevice: chargedDevice) {
                metricCell(label: "Mode", value: session.chargingStateMode.title, tint: .purple)
            }

            metricCell(label: "Auto Stop", value: autoStopLabel(for: session), tint: .secondary)

            if let capacityFallback {
                metricCell(label: "Est. Capacity", value: "\(capacityFallback.format(decimalDigits: 2)) Wh", tint: .orange)
            }
        }
    }

    private func metricCell(label: String, value: String, tint: Color) -> some View {
        VStack(alignment: .leading, spacing: 3) {
            Text(label)
                .font(.caption2)
                .foregroundColor(.secondary)
            Text(value)
                .font(.subheadline.weight(.semibold))
                .lineLimit(1)
                .minimumScaleFactor(0.7)
                .monospacedDigit()
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(.horizontal, 12)
        .padding(.vertical, 10)
        .meterCard(tint: tint, fillOpacity: 0.08, strokeOpacity: 0.12, cornerRadius: 12)
    }

    private func completionConfirmationCard(_ session: ChargeSessionSummary) -> some View {
        VStack(alignment: .leading, spacing: 10) {
            Label("Charging may have stopped", systemImage: "questionmark.circle.fill")
                .font(.subheadline.weight(.semibold))

            if let contradictionPercent = session.completionContradictionPercent {
                Text("Current dropped but estimated level is only \(contradictionPercent.format(decimalDigits: 0))%.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            } else {
                Text("Current dropped to the stop threshold but the prediction doesn't confirm a full charge yet.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            HStack(spacing: 10) {
                Button("Finish") {
                    beginStopConfirmation(for: session)
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 9)
                .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)

                Button("Keep Monitoring") {
                    _ = appData.continueChargeSessionMonitoring(sessionID: session.id)
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 9)
                .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)
            }
        }
        .padding(14)
        .meterCard(tint: .orange, fillOpacity: 0.10, strokeOpacity: 0.16, cornerRadius: 16)
    }

    private func targetSectionView(session: ChargeSessionSummary, predictedPercent: Double?) -> some View {
        let draftBelowPrediction: Bool = {
            guard let draft = parsedDraftTarget, let predictedPercent else { return false }
            return draft <= predictedPercent
        }()
        let savedBelowPrediction: Bool = {
            guard let saved = session.targetBatteryPercent, let predictedPercent else { return false }
            return saved <= predictedPercent
        }()

        return HStack(alignment: .center, spacing: 8) {
            Image(systemName: "bell.badge")
                .foregroundColor(.indigo)
                .font(.subheadline)

            Text("Notify at")
                .font(.subheadline.weight(.semibold))

            Spacer(minLength: 8)

            if showingInlineTargetEditor {
                targetEditorControls(
                    session: session,
                    draftBelowPrediction: draftBelowPrediction,
                    predictedPercent: predictedPercent
                )
            } else {
                savedTargetControls(
                    session: session,
                    savedBelowPrediction: savedBelowPrediction,
                    predictedPercent: predictedPercent
                )
            }
        }
    }

    private func targetEditorControls(
        session: ChargeSessionSummary,
        draftBelowPrediction: Bool,
        predictedPercent: Double?
    ) -> some View {
        Group {
            Button {
                let current = parsedDraftTarget ?? session.targetBatteryPercent ?? 80
                draftTargetText = max(current - 1, 1).format(decimalDigits: 0)
            } label: {
                Image(systemName: "minus.circle")
                    .font(.title3)
            }
            .buttonStyle(.plain)

            TextField("-", text: $draftTargetText)
                .keyboardType(.decimalPad)
                .textFieldStyle(.roundedBorder)
                .frame(width: 48)
                .multilineTextAlignment(.center)
                .foregroundColor(draftBelowPrediction ? .orange : .primary)

            Text("%")
                .font(.subheadline)
                .foregroundColor(.secondary)

            if draftBelowPrediction, let predictedPercent {
                predictionWarningButton(predictedPercent: predictedPercent)
            }

            Button {
                let current = parsedDraftTarget ?? session.targetBatteryPercent ?? 80
                draftTargetText = min(current + 1, 100).format(decimalDigits: 0)
            } label: {
                Image(systemName: "plus.circle")
                    .font(.title3)
            }
            .buttonStyle(.plain)

            Button {
                if let value = parsedDraftTarget {
                    _ = appData.setTargetBatteryPercent(value, for: session.id)
                }
                showingInlineTargetEditor = false
            } label: {
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(parsedDraftTarget != nil ? .indigo : .secondary)
                    .font(.title3)
            }
            .buttonStyle(.plain)
            .disabled(parsedDraftTarget == nil)

            Button {
                showingInlineTargetEditor = false
                draftTargetText = ""
            } label: {
                Image(systemName: "xmark.circle")
                    .foregroundColor(.secondary)
                    .font(.title3)
            }
            .buttonStyle(.plain)
        }
    }

    private func savedTargetControls(
        session: ChargeSessionSummary,
        savedBelowPrediction: Bool,
        predictedPercent: Double?
    ) -> some View {
        Group {
            if let targetPercent = session.targetBatteryPercent {
                Text("\(targetPercent.format(decimalDigits: 0))%")
                    .font(.subheadline.weight(.semibold))
                    .foregroundColor(savedBelowPrediction ? .orange : .indigo)

                if savedBelowPrediction, let predictedPercent {
                    predictionWarningButton(predictedPercent: predictedPercent)
                }

                Button {
                    _ = appData.setTargetBatteryPercent(nil, for: session.id)
                } label: {
                    Image(systemName: "xmark.circle.fill")
                        .foregroundColor(.secondary)
                        .font(.callout)
                }
                .buttonStyle(.plain)
                .help("Remove alert")
            }

            Button {
                draftTargetText = session.targetBatteryPercent.map {
                    $0.format(decimalDigits: 0)
                } ?? "80"
                showingInlineTargetEditor = true
            } label: {
                Image(systemName: session.targetBatteryPercent == nil ? "plus" : "pencil")
                    .font(.caption.weight(.semibold))
                    .frame(width: 30, height: 30)
                    .contentShape(Rectangle())
            }
            .meterCard(tint: .indigo, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 10)
            .buttonStyle(.plain)
            .help(session.targetBatteryPercent == nil ? "Set battery alert" : "Edit battery alert")
        }
    }

    private func predictionWarningButton(predictedPercent: Double) -> some View {
        Button {} label: {
            Image(systemName: "exclamationmark.triangle.fill")
                .font(.callout.weight(.semibold))
                .foregroundColor(.orange)
        }
        .buttonStyle(.plain)
        .help("Battery is already predicted at \(predictedPercent.format(decimalDigits: 0))% - this alert won't fire. Raise the value or add a checkpoint to correct the prediction.")
    }

    private func monitoringActionRow(_ session: ChargeSessionSummary) -> some View {
        HStack(spacing: 10) {
            if session.status == .active {
                Button("Pause") {
                    _ = appData.pauseChargeSession(sessionID: session.id, from: liveMonitoringMeter)
                }
                .monitoringActionStyle(tint: .orange)
            } else if session.status == .paused {
                Button("Resume") {
                    _ = appData.resumeChargeSession(sessionID: session.id, from: liveMonitoringMeter)
                }
                .monitoringActionStyle(tint: .blue)
            }

            Button("Terminate Session") {
                beginStopConfirmation(for: session)
            }
            .monitoringActionStyle(tint: .red)
        }
    }

    private func stopConfirmPanel(
        session: ChargeSessionSummary,
        displayedEnergyWh: Double
    ) -> some View {
        let canSave = hasSavableChargeData(
            session: session,
            displayedEnergyWh: displayedEnergyWh
        )
        let saveDisabledReason = saveDisabledReason(
            session: session,
            displayedEnergyWh: displayedEnergyWh
        )
        let isSaveEnabled = saveDisabledReason == nil

        return VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text("Final Checkpoint")
                    .font(.subheadline.weight(.semibold))
                Text("optional")
                    .font(.caption2.weight(.semibold))
                    .foregroundColor(.secondary)
            }

            finalCheckpointPicker(session)

            if finalCheckpointMode == .custom {
                customFinalCheckpointRow
            }

            if let saveDisabledReason {
                Label(saveDisabledReason, systemImage: "exclamationmark.triangle.fill")
                    .font(.caption)
                    .foregroundColor(.red)
                    .fixedSize(horizontal: false, vertical: true)
            } else if let stopFailureMessage {
                Label(stopFailureMessage, systemImage: "exclamationmark.triangle.fill")
                    .font(.caption)
                    .foregroundColor(.red)
                    .fixedSize(horizontal: false, vertical: true)
            } else if finalCheckpointMode == .custom, let parsedFinalCheckpoint {
                Label("Final checkpoint will be saved at \(parsedFinalCheckpoint.format(decimalDigits: 0))%.", systemImage: "checkmark.circle.fill")
                    .font(.caption)
                    .foregroundColor(.green)
                    .fixedSize(horizontal: false, vertical: true)
            }

            HStack(spacing: 8) {
                Button("Discard") {
                    discardSession(session)
                }
                .monitoringPanelActionStyle(tint: .secondary)

                Button {
                    stopSession(
                        session,
                        displayedEnergyWh: displayedEnergyWh
                    )
                } label: {
                    Label("Save Session", systemImage: "checkmark.circle.fill")
                        .frame(maxWidth: .infinity)
                }
                .monitoringPanelActionStyle(tint: isSaveEnabled ? .green : .secondary, isProminent: isSaveEnabled)
                .disabled(!isSaveEnabled)
                .help(saveDisabledReason ?? "Close and save this session")

                Button("Cancel") {
                    resetStopConfirmation()
                }
                .monitoringPanelActionStyle(tint: .secondary)
            }
        }
        .padding(14)
        .meterCard(tint: canSave ? .green : .red, fillOpacity: 0.06, strokeOpacity: 0.14, cornerRadius: 16)
    }

    private func finalCheckpointPicker(_ session: ChargeSessionSummary) -> some View {
        return HStack(spacing: 8) {
            ForEach([FinalCheckpoint.full, .skip, .custom], id: \.self) { mode in
                Button {
                    finalCheckpointMode = mode
                    if mode == .custom {
                        prefillFinalCheckpointIfNeeded(for: session)
                    } else {
                        finalCheckpointText = ""
                    }
                } label: {
                    VStack(spacing: 5) {
                        Image(systemName: mode.icon)
                            .font(.title3)
                            .foregroundColor(finalCheckpointMode == mode ? .primary : .secondary)
                        Text(mode.label)
                            .font(.caption.weight(.semibold))
                            .foregroundColor(finalCheckpointMode == mode ? .primary : .secondary)
                    }
                    .frame(maxWidth: .infinity)
                    .padding(.vertical, 10)
                    .background(finalCheckpointMode == mode ? Color.primary.opacity(0.10) : Color.clear)
                    .meterCard(
                        tint: finalCheckpointMode == mode ? .primary : .secondary,
                        fillOpacity: finalCheckpointMode == mode ? 0.08 : 0.04,
                        strokeOpacity: finalCheckpointMode == mode ? 0.20 : 0.10,
                        cornerRadius: 12
                    )
                }
                .buttonStyle(.plain)
            }
        }
    }

    private var customFinalCheckpointRow: some View {
        let isInvalid = finalCheckpointText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
            || parsedFinalCheckpoint == nil

        return HStack(spacing: 8) {
            Button {
                adjustFinalCheckpoint(by: -1)
            } label: {
                Image(systemName: "minus.circle").font(.title3)
            }
            .buttonStyle(.plain)

            TextField("-", text: $finalCheckpointText)
                .keyboardType(.decimalPad)
                .textFieldStyle(.roundedBorder)
                .frame(width: 56)
                .multilineTextAlignment(.center)
                .overlay(
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(isInvalid ? Color.red.opacity(0.75) : Color.clear, lineWidth: 1)
                )

            Text("%").foregroundColor(.secondary)

            Text("required")
                .font(.caption2.weight(.semibold))
                .foregroundColor(isInvalid ? .red : .secondary)

            Button {
                adjustFinalCheckpoint(by: 1)
            } label: {
                Image(systemName: "plus.circle").font(.title3)
            }
            .buttonStyle(.plain)

            Spacer()
        }
    }

    private func followerNoticeCard(_ session: ChargeSessionSummary) -> some View {
        MeterInfoCardView(title: "Monitoring Device", tint: .secondary) {
            if let meterName = session.meterName {
                MeterInfoRowView(label: "Controlled On", value: meterName)
            }
            Text("Pause, stop, checkpoint, and live trim controls are available on the device connected to the monitoring meter.")
                .font(.caption2)
                .foregroundColor(.secondary)
        }
    }

    private func managementCard(_ session: ChargeSessionSummary) -> some View {
        MeterInfoCardView(title: "Administration", tint: .red) {
            Button(role: .destructive) {
                pendingSessionDeletion = session
            } label: {
                Label("Delete Session", systemImage: "trash")
                    .font(.subheadline.weight(.semibold))
                    .frame(maxWidth: .infinity)
                    .padding(.vertical, 10)
                    .meterCard(tint: .red, fillOpacity: 0.12, strokeOpacity: 0.20, cornerRadius: 14)
            }
            .buttonStyle(.plain)
        }
    }

    @ViewBuilder
    private func trimDetectionBanner(_ session: ChargeSessionSummary) -> some View {
        if let window = detectedTrimWindow {
            HStack(spacing: 12) {
                Image(systemName: "scissors.circle.fill")
                    .font(.title3)
                    .foregroundColor(.blue)

                VStack(alignment: .leading, spacing: 2) {
                    Text("Charging ended early")
                        .font(.subheadline.weight(.semibold))
                    Text("Active charging was detected between \(window.start.format(as: "HH:mm")) and \(window.end.format(as: "HH:mm")).")
                        .font(.caption)
                        .foregroundColor(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                }

                Spacer(minLength: 0)

                VStack(spacing: 6) {
                    Button("Trim Start") {
                        setSessionTrim(sessionID: session.id, start: window.start, end: session.trimEnd)
                        trimBannerDismissedForSessionID = session.id
                    }
                    .font(.caption.weight(.semibold))
                    .buttonStyle(.borderedProminent)
                    .controlSize(.small)
                    .tint(.blue)

                    Button("End & Finish") {
                        requestStop(
                            session,
                            applyingTrimStart: session.trimStart ?? window.start,
                            trimEnd: window.end,
                            title: "Trim End & Finish",
                            confirmTitle: "Finish",
                            explanation: "The detected charging window will be saved before the session is closed."
                        )
                        trimBannerDismissedForSessionID = session.id
                    }
                    .font(.caption.weight(.semibold))
                    .buttonStyle(.bordered)
                    .controlSize(.small)
                    .tint(.red)
                }
            }
            .padding(14)
            .background(
                RoundedRectangle(cornerRadius: 14)
                    .fill(Color.blue.opacity(0.10))
                    .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.blue.opacity(0.22), lineWidth: 1))
            )
            .transition(.opacity.combined(with: .move(edge: .top)))
        }
    }

    private func shouldShowSessionChart(_ session: ChargeSessionSummary) -> Bool {
        !session.aggregatedSamples.isEmpty || liveMonitoringMeter != nil
    }

    private func chartCard(_ session: ChargeSessionSummary) -> some View {
        ChargeSessionChartCardView(
            session: session,
            monitoringMeter: liveMonitoringMeter,
            controlMode: chartControlMode(for: session),
            onSetTrim: { start, end in
                setSessionTrim(sessionID: session.id, start: start, end: end)
            },
            onStopWithTrim: { start, end in
                requestStop(
                    session,
                    applyingTrimStart: start,
                    trimEnd: end,
                    title: "Trim End & Finish",
                    confirmTitle: "Finish",
                    explanation: "The selected chart window will be saved as this session's active charging window before the session is closed."
                )
            }
        )
    }

    private func chartControlMode(for session: ChargeSessionSummary) -> ChargeSessionChartControlMode {
        if hasMonitoringControls {
            return .activeMonitoring
        }

        if session.status.isOpen == false {
            return .closed
        }

        return .none
    }

    private func setSessionTrim(sessionID: UUID, start: Date?, end: Date?) {
        _ = appData.setSessionTrim(sessionID: sessionID, start: start, end: end)
        trimBannerDismissedForSessionID = sessionID
    }

    private func requestStop(
        _ session: ChargeSessionSummary,
        applyingTrimStart trimStart: Date?,
        trimEnd: Date?,
        title: String,
        confirmTitle: String,
        explanation: String
    ) {
        pendingSessionStopRequest = ChargeSessionStopRequest(
            sessionID: session.id,
            title: title,
            confirmTitle: confirmTitle,
            explanation: explanation,
            appliesTrim: trimStart != nil || trimEnd != nil,
            trimStart: trimStart,
            trimEnd: trimEnd
        )
    }

    private var parsedDraftTarget: Double? {
        let normalized = draftTargetText
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .replacingOccurrences(of: ",", with: ".")
        guard let value = Double(normalized), value >= 1, value <= 100 else { return nil }
        return value
    }

    private var parsedFinalCheckpoint: Double? {
        let normalized = finalCheckpointText
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .replacingOccurrences(of: ",", with: ".")
        guard let value = Double(normalized), value >= 0, value <= 100 else { return nil }
        return value
    }

    private var resolvedFinalCheckpoint: Double? {
        switch finalCheckpointMode {
        case .full:   return 100
        case .skip:   return nil
        case .custom: return parsedFinalCheckpoint
        }
    }

    private func adjustFinalCheckpoint(by delta: Double) {
        let current = parsedFinalCheckpoint ?? suggestedFinalCheckpointPercent(for: session) ?? 0
        let next = min(max(current + delta, 0), 100)
        finalCheckpointText = next.format(decimalDigits: 0)
    }

    private func suggestedFinalCheckpointPercent(for session: ChargeSessionSummary?) -> Double? {
        guard let session else { return nil }
        if let endBatteryPercent = session.endBatteryPercent {
            return endBatteryPercent
        }
        if let latestCheckpoint = session.checkpoints.max(by: { $0.timestamp < $1.timestamp }) {
            return latestCheckpoint.batteryPercent
        }
        return session.targetBatteryPercent ?? session.completionContradictionPercent
    }

    private func prefillFinalCheckpointIfNeeded(for session: ChargeSessionSummary) {
        guard finalCheckpointText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
              let suggestedPercent = suggestedFinalCheckpointPercent(for: session) else {
            return
        }
        finalCheckpointText = suggestedPercent.format(decimalDigits: 0)
    }

    private func hasSavableChargeData(
        session: ChargeSessionSummary,
        displayedEnergyWh: Double
    ) -> Bool {
        session.hasSavableChargeData
            || displayedEnergyWh > 0
    }

    private func saveDisabledReason(
        session: ChargeSessionSummary,
        displayedEnergyWh: Double
    ) -> String? {
        if finalCheckpointMode == .custom {
            let trimmed = finalCheckpointText.trimmingCharacters(in: .whitespacesAndNewlines)
            if trimmed.isEmpty {
                return "Enter the final battery percentage or choose Skip."
            }
            if parsedFinalCheckpoint == nil {
                return "Final battery percentage must be between 0 and 100."
            }
        }

        guard hasSavableChargeData(
            session: session,
            displayedEnergyWh: displayedEnergyWh
        ) else {
            return "This session has no charging data to save. Discard it instead."
        }

        return nil
    }

    private func stopSession(
        _ session: ChargeSessionSummary,
        displayedEnergyWh: Double
    ) {
        stopFailureMessage = nil

        if let saveDisabledReason = saveDisabledReason(
            session: session,
            displayedEnergyWh: displayedEnergyWh
        ) {
            stopFailureMessage = saveDisabledReason
            return
        }

        let didSave = appData.stopChargeSession(
            sessionID: session.id,
            finalBatteryPercent: resolvedFinalCheckpoint,
            from: liveMonitoringMeter
        )
        if didSave {
            resetStopConfirmation()
        } else {
            stopFailureMessage = "The session could not be closed. Live readings were flushed, but the stored session did not accept the save yet. Try again in a moment."
        }
    }

    private func beginStopConfirmation(for session: ChargeSessionSummary) {
        finalCheckpointMode = .skip
        finalCheckpointText = ""
        stopFailureMessage = nil
        showingStopConfirm = true
    }

    private func discardSession(_ session: ChargeSessionSummary) {
        _ = appData.deleteChargeSession(sessionID: session.id)
        resetStopConfirmation()
    }

    private func resetStopConfirmation() {
        showingStopConfirm = false
        finalCheckpointText = ""
        finalCheckpointMode = .skip
        stopFailureMessage = nil
    }

    private func syncMonitoringRestore() {
        guard let session,
              session.status.isOpen,
              let liveMonitoringMeter,
              session.meterMACAddress == liveMonitoringMeter.btSerial.macAddress.description else {
            return
        }
        liveMonitoringMeter.restoreChargeMonitoringIfNeeded(from: session)
    }

    private func runTrimDetection() {
        guard hasMonitoringControls,
              let session,
              session.isTrimmed == false,
              !session.aggregatedSamples.isEmpty else {
            detectedTrimWindow = nil
            return
        }

        let sessionEnd = session.endedAt ?? session.lastObservedAt
        detectedTrimWindow = ChargingWindowDetector.detect(
            samples: session.aggregatedSamples,
            sessionStart: session.startedAt,
            sessionEnd: sessionEnd
        )
    }

    private func displayedSessionEnergyWh(for session: ChargeSessionSummary) -> Double {
        let storedEnergyWh = session.effectiveOrMeasuredEnergyWh
        guard session.isTrimmed == false else { return storedEnergyWh }
        guard session.status.isOpen else { return storedEnergyWh }
        guard let liveMonitoringMeter else { return storedEnergyWh }
        guard session.meterMACAddress == liveMonitoringMeter.btSerial.macAddress.description else { return storedEnergyWh }
        if let baselineEnergyWh = session.meterEnergyBaselineWh {
            return max(storedEnergyWh, max(liveMonitoringMeter.recordedWH - baselineEnergyWh, 0))
        }
        return storedEnergyWh
    }

    private func displayedSessionDuration(for session: ChargeSessionSummary) -> TimeInterval {
        let storedDuration = max(session.effectiveDuration, 0)
        guard session.isTrimmed == false else { return storedDuration }
        guard session.status.isOpen else { return storedDuration }
        guard let liveMonitoringMeter else { return storedDuration }
        guard session.meterMACAddress == liveMonitoringMeter.btSerial.macAddress.description else { return storedDuration }
        return max(storedDuration, max(liveMonitoringMeter.chargeRecordDuration, 0))
    }

    private func sessionDurationText(_ session: ChargeSessionSummary) -> String {
        let displayedDuration = displayedSessionDuration(for: session)
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = displayedDuration >= 3600 ? [.hour, .minute] : [.minute, .second]
        formatter.unitsStyle = .abbreviated
        formatter.zeroFormattingBehavior = .dropAll
        return formatter.string(from: displayedDuration) ?? "0m"
    }

    private func formatDuration(_ duration: TimeInterval) -> String {
        let totalSeconds = Int(duration.rounded(.down))
        let hours = totalSeconds / 3600
        let minutes = (totalSeconds % 3600) / 60
        let seconds = totalSeconds % 60
        if hours > 0 {
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
        }
        return String(format: "%02d:%02d", minutes, seconds)
    }

    private func autoStopDescription(for session: ChargeSessionSummary) -> String {
        if session.autoStopEnabled == false {
            return "Manual"
        }

        if let sessionWarning = sessionWarning(for: session),
           sessionWarning.contains("idle-current") {
            return "Blocked by charger setup"
        }

        if session.stopThresholdAmps > 0 {
            return "\(session.stopThresholdAmps.format(decimalDigits: 2)) A"
        }

        return "Learning"
    }

    private func autoStopLabel(for session: ChargeSessionSummary) -> String {
        if session.autoStopEnabled == false {
            return "Manual"
        }
        if let sessionWarning = sessionWarning(for: session), session.chargingTransportMode == .wireless {
            return sessionWarning.contains("idle-current") ? "Blocked" : "Manual"
        }
        if session.stopThresholdAmps > 0 {
            return "\(session.stopThresholdAmps.format(decimalDigits: 2)) A"
        }
        return "Learning"
    }

    private func shouldShowChargingTransport(
        for session: ChargeSessionSummary,
        chargedDevice: ChargedDeviceSummary
    ) -> Bool {
        chargedDevice.supportedChargingModes.count > 1
            || chargedDevice.supportedChargingModes.contains(session.chargingTransportMode) == false
    }

    private func shouldShowChargingState(
        for session: ChargeSessionSummary,
        chargedDevice: ChargedDeviceSummary
    ) -> Bool {
        chargedDevice.supportedChargingStateModes.count > 1
            || chargedDevice.supportedChargingStateModes.contains(session.chargingStateMode) == false
    }

    private func batteryColor(for percent: Double) -> Color {
        if percent >= 75 { return .green }
        if percent >= 35 { return .orange }
        return .red
    }

    private func etaText(
        rateWhPerSec: Double?,
        remainingWh: Double,
        isRelevant: Bool
    ) -> String? {
        guard isRelevant, let rateWhPerSec, rateWhPerSec > 0.0001 else { return nil }
        let seconds = remainingWh / rateWhPerSec
        return seconds > 120 ? formatETA(seconds) : nil
    }

    private func etaToTargetText(
        session: ChargeSessionSummary,
        prediction: BatteryLevelPrediction,
        displayedEnergyWh: Double,
        rateWhPerSec: Double?
    ) -> String? {
        guard let target = session.targetBatteryPercent, target > prediction.predictedPercent + 1 else {
            return nil
        }
        let targetEnergyWh = (target / 100) * prediction.estimatedCapacityWh
        return etaText(
            rateWhPerSec: rateWhPerSec,
            remainingWh: max(targetEnergyWh - displayedEnergyWh, 0),
            isRelevant: true
        )
    }

    private func formatETA(_ seconds: TimeInterval) -> String {
        let totalMinutes = Int(seconds / 60)
        if totalMinutes < 60 { return "\(totalMinutes)m" }
        let hours = totalMinutes / 60
        let minutes = totalMinutes % 60
        return minutes == 0 ? "\(hours)h" : "\(hours)h \(minutes)m"
    }

    private func monitoringStatusColor(for session: ChargeSessionSummary) -> Color {
        switch session.status {
        case .active:
            return .red
        case .paused:
            return .orange
        case .completed:
            return .green
        case .abandoned:
            return .secondary
        }
    }

    private func sessionWarning(for session: ChargeSessionSummary) -> String? {
        nil
    }

    private func wirelessSessionHint(for session: ChargeSessionSummary) -> String? {
        guard session.chargingTransportMode == .wireless else {
            return nil
        }

        var components: [String] = []
        if let wirelessEfficiencyFactor = session.wirelessEfficiencyFactor {
            components.append("Efficiency \(Int((wirelessEfficiencyFactor * 100).rounded()))%")
        }
        if session.usesEstimatedWirelessEfficiency {
            components.append("Estimated from wired baseline and checkpoints")
        }
        if session.shouldWarnAboutLowWirelessEfficiency {
            components.append("Low wireless efficiency, so capacity confidence is reduced")
        }

        return components.isEmpty ? nil : components.joined(separator: " - ")
    }

    private func statusTint(for session: ChargeSessionSummary) -> Color {
        switch session.status {
        case .active:
            return .green
        case .paused:
            return .orange
        case .completed:
            return .teal
        case .abandoned:
            return .secondary
        }
    }
}

enum ChargeSessionChartControlMode {
    case none
    case activeMonitoring
    case closed
}

struct ChargeSessionChartCardView: View {
    let session: ChargeSessionSummary
    let monitoringMeter: Meter?
    let controlMode: ChargeSessionChartControlMode
    let onSetTrim: (Date?, Date?) -> Void
    let onStopWithTrim: (Date?, Date?) -> Void

    @StateObject private var storedMeasurements = Measurements()

    private var chartMeasurements: Measurements {
        if let monitoringMeter,
           session.status.isOpen,
           session.meterMACAddress == monitoringMeter.btSerial.macAddress.description {
            return monitoringMeter.chargeRecordMeasurements
        }
        return storedMeasurements
    }

    private var fullTimeRange: ClosedRange<Date> {
        let start = session.startedAt
        let end = max(session.endedAt ?? session.lastObservedAt, start)
        return start...end
    }

    private var fixedTimeRange: ClosedRange<Date>? {
        if monitoringMeter != nil && session.status.isOpen {
            return nil
        }
        return session.effectiveTimeRange
    }

    private var liveTrimBounds: (lower: Date?, upper: Date?) {
        guard monitoringMeter != nil && session.status.isOpen else {
            return (nil, nil)
        }
        return (session.trimStart, session.trimEnd)
    }

    private var showsRangeSelector: Bool {
        controlMode != .none && !session.aggregatedSamples.isEmpty
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(spacing: 8) {
                Image(systemName: "chart.xyaxis.line")
                    .foregroundColor(.blue)
                Text("Session Chart")
                    .font(.headline)
                ContextInfoButton(
                    title: "Session Chart",
                    message: chartInfoMessage
                )
                Spacer(minLength: 0)
            }

            MeasurementChartView(
                timeRange: fixedTimeRange,
                timeRangeLowerBound: liveTrimBounds.lower,
                timeRangeUpperBound: liveTrimBounds.upper,
                showsRangeSelector: showsRangeSelector,
                rebasesEnergyToVisibleRangeStart: true,
                extendsTimelineToPresent: false,
                showsTemperatureSeries: false,
                rangeSelectorConfiguration: rangeSelectorConfiguration
            )
            .environmentObject(chartMeasurements)
            .frame(maxWidth: .infinity, alignment: .topLeading)
        }
        .padding(18)
        .meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20)
        .onAppear(perform: restoreStoredMeasurementsIfNeeded)
        .onChange(of: session.id) { _ in
            restoreStoredMeasurementsIfNeeded()
        }
        .onChange(of: session.aggregatedSamples.count) { _ in
            restoreStoredMeasurementsIfNeeded()
        }
    }

    private var chartInfoMessage: String {
        if monitoringMeter != nil && session.status.isOpen {
            return "This chart combines the persisted session curve with current live data from this meter."
        }

        return "This chart is scoped to the saved session window for \(session.sessionKind.shortTitle.lowercased()) charging."
    }

    private var rangeSelectorConfiguration: MeasurementChartRangeSelectorConfiguration? {
        switch controlMode {
        case .none:
            return nil
        case .activeMonitoring:
            return MeasurementChartRangeSelectorConfiguration(
                keepAction: MeasurementChartSelectionAction(
                    title: "Trim Start",
                    shortTitle: "Start",
                    systemName: "arrow.right.to.line",
                    tone: .destructive,
                    handler: applyActiveStartTrim
                ),
                removeAction: MeasurementChartSelectionAction(
                    title: "Trim End & Finish",
                    shortTitle: "End",
                    systemName: "arrow.left.to.line",
                    tone: .destructiveProminent,
                    handler: requestActiveEndTrim
                ),
                resetAction: MeasurementChartResetAction(
                    title: "Reset Trim",
                    shortTitle: "Reset",
                    systemName: "arrow.counterclockwise",
                    tone: .reversible,
                    confirmationTitle: "Reset session trim?",
                    confirmationButtonTitle: "Reset trim",
                    handler: {
                        onSetTrim(nil, nil)
                    }
                )
            )
        case .closed:
            return MeasurementChartRangeSelectorConfiguration(
                keepAction: MeasurementChartSelectionAction(
                    title: "Trim Window",
                    shortTitle: "Trim",
                    systemName: "scissors",
                    tone: .destructive,
                    handler: applyClosedTrim
                ),
                removeAction: nil,
                resetAction: MeasurementChartResetAction(
                    title: "Reset Trim",
                    shortTitle: "Reset",
                    systemName: "arrow.counterclockwise",
                    tone: .reversible,
                    confirmationTitle: "Reset session trim?",
                    confirmationButtonTitle: "Reset trim",
                    handler: {
                        onSetTrim(nil, nil)
                    }
                )
            )
        }
    }

    private func restoreStoredMeasurementsIfNeeded() {
        guard monitoringMeter == nil || session.status.isOpen == false else {
            return
        }
        storedMeasurements.resetSeries()
        _ = storedMeasurements.restorePersistedChargeSessionSamplesIfNeeded(
            from: session,
            replacingLiveBufferIfNeeded: true
        )
    }

    private func applyActiveStartTrim(_ range: ClosedRange<Date>) {
        onSetTrim(normalizedStart(range.lowerBound), session.trimEnd)
    }

    private func requestActiveEndTrim(_ range: ClosedRange<Date>) {
        let start = session.trimStart ?? normalizedStart(range.lowerBound)
        let end = normalizedEnd(range.upperBound)
        onStopWithTrim(start, end)
    }

    private func applyClosedTrim(_ range: ClosedRange<Date>) {
        onSetTrim(normalizedStart(range.lowerBound), normalizedEnd(range.upperBound))
    }

    private func normalizedStart(_ date: Date) -> Date? {
        date.timeIntervalSince(fullTimeRange.lowerBound) <= 1 ? nil : date
    }

    private func normalizedEnd(_ date: Date) -> Date? {
        fullTimeRange.upperBound.timeIntervalSince(date) <= 1 ? nil : date
    }
}

private struct ChargeSessionStopRequest: Identifiable {
    let sessionID: UUID
    let title: String
    let confirmTitle: String
    let explanation: String
    let appliesTrim: Bool
    let trimStart: Date?
    let trimEnd: Date?

    var id: String {
        [
            sessionID.uuidString,
            title,
            trimStart?.timeIntervalSince1970.description ?? "nil",
            trimEnd?.timeIntervalSince1970.description ?? "nil"
        ].joined(separator: "-")
    }
}

private extension View {
    func monitoringActionStyle(tint: Color) -> some View {
        frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .meterCard(tint: tint, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
            .buttonStyle(.plain)
    }

    func monitoringPanelActionStyle(tint: Color, isProminent: Bool = false) -> some View {
        frame(maxWidth: .infinity)
            .padding(.vertical, 9)
            .meterCard(
                tint: tint,
                fillOpacity: isProminent ? 0.22 : 0.10,
                strokeOpacity: isProminent ? 0.32 : 0.14,
                cornerRadius: 14
            )
            .buttonStyle(.plain)
    }
}