USB-Meter / USB Meter / Views / Meter / Tabs / ChargeRecord / MeterChargeRecordTabView.swift
1 contributor
1329 lines | 52.562kb
//
//  MeterChargeRecordTabView.swift
//  USB Meter
//

import SwiftUI

struct MeterChargeRecordTabView: View {
    var body: some View {
        MeterChargeRecordContentView()
    }
}

struct MeterChargeRecordContentView: View {
    private enum InitialCheckpointMode: String, CaseIterable, Identifiable {
        case known
        case unknown
        case flat

        var id: String { rawValue }

        var title: String {
            switch self {
            case .known:
                return "Known"
            case .unknown:
                return "Unknown"
            case .flat:
                return "Flat"
            }
        }
    }

    @EnvironmentObject private var appData: AppData
    @EnvironmentObject private var usbMeter: Meter

    @State private var chargedDeviceLibraryVisibility = false
    @State private var chargerLibraryVisibility = false
    @State private var editingChargedDevice: ChargedDeviceSummary?
    @State private var targetNotificationEditorVisibility = false
    @State private var pendingStopRequest: ChargeSessionStopRequest?
    @State private var pendingCheckpointDeletion: ChargeCheckpointSummary?
    @State private var draftChargingTransportMode: ChargingTransportMode?
    @State private var draftChargingStateMode: ChargingStateMode?
    @State private var initialCheckpointMode: InitialCheckpointMode = .known
    @State private var initialCheckpoint = ""
    @State private var showsMeterTotalsInfo = false
    @State private var showsInlineCheckpointEditor = false

    private enum SessionStartRequirement: Identifiable {
        case existingSession
        case device
        case chargingType
        case chargingMode
        case charger
        case initialCheckpointEmpty
        case initialCheckpointInvalid

        var id: String {
            switch self {
            case .existingSession:
                return "existing-session"
            case .device:
                return "device"
            case .chargingType:
                return "charging-type"
            case .chargingMode:
                return "charging-mode"
            case .charger:
                return "charger"
            case .initialCheckpointEmpty:
                return "initial-checkpoint-empty"
            case .initialCheckpointInvalid:
                return "initial-checkpoint-invalid"
            }
        }

        var message: String {
            switch self {
            case .existingSession:
                return "Stop or pause the current session before starting another one."
            case .device:
                return "Select the device that is charging."
            case .chargingType:
                return "Choose the charging type for this session."
            case .chargingMode:
                return "Choose whether the device is on or off for this session."
            case .charger:
                return "Select the wireless charger used in this session."
            case .initialCheckpointEmpty:
                return "Enter the initial battery percentage."
            case .initialCheckpointInvalid:
                return "Initial battery percentage must be between 0 and 100."
            }
        }
    }

    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                headerCard
                sessionSetupCard

                if let openChargeSession {
                    chargingMonitorCard(openChargeSession)

                    if showsMeterTotalsCard {
                        meterTotalsCard
                    }

                    if let sessionChartTimeRange {
                        sessionChartCard(timeRange: sessionChartTimeRange, session: openChargeSession)
                    }
                } else if showsMeterTotalsCard {
                    meterTotalsCard
                }
            }
            .padding()
        }
        .background(
            LinearGradient(
                colors: [.pink.opacity(0.14), Color.clear],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()
        )
        .sheet(isPresented: $chargedDeviceLibraryVisibility) {
            ChargedDeviceLibrarySheetView(
                visibility: $chargedDeviceLibraryVisibility,
                meterMACAddress: meterMACAddress,
                meterTint: usbMeter.color,
                mode: .device
            )
            .environmentObject(appData)
        }
        .sheet(isPresented: $chargerLibraryVisibility) {
            ChargedDeviceLibrarySheetView(
                visibility: $chargerLibraryVisibility,
                meterMACAddress: meterMACAddress,
                meterTint: usbMeter.color,
                mode: .charger
            )
            .environmentObject(appData)
        }
        .sheet(item: $editingChargedDevice) { chargedDevice in
            ChargedDeviceEditorSheetView(
                meterMACAddress: nil,
                kind: chargedDevice.kind,
                chargedDevice: chargedDevice
            )
            .environmentObject(appData)
        }
        .sheet(isPresented: $targetNotificationEditorVisibility) {
            if let openChargeSession {
                BatteryTargetNotificationEditorSheetView(
                    sessionID: openChargeSession.id,
                    initialTargetPercent: openChargeSession.targetBatteryPercent
                )
                .environmentObject(appData)
            }
        }
        .sheet(item: $pendingStopRequest) { request in
            ChargeSessionCompletionSheetView(
                sessionID: request.sessionID,
                title: request.title,
                confirmTitle: request.confirmTitle,
                explanation: request.explanation
            )
            .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")) {
                    if let openChargeSession {
                        _ = appData.deleteBatteryCheckpoint(
                            checkpointID: checkpoint.id,
                            for: openChargeSession.id
                        )
                    }
                },
                secondaryButton: .cancel()
            )
        }
        .onAppear {
            syncDraftSelections()
        }
        .onChange(of: selectedChargedDevice?.id) { _ in
            syncDraftSelections()
        }
        .onChange(of: openChargeSession?.id) { _ in
            syncDraftSelections()
            showsInlineCheckpointEditor = false
        }
    }

    private var meterMACAddress: String {
        usbMeter.btSerial.macAddress.description
    }

    private var selectedChargedDevice: ChargedDeviceSummary? {
        appData.currentChargedDeviceSummary(for: meterMACAddress)
    }

    private var selectedCharger: ChargedDeviceSummary? {
        appData.currentChargerSummary(for: meterMACAddress)
    }

    private var openChargeSession: ChargeSessionSummary? {
        appData.activeChargeSessionSummary(for: meterMACAddress)
    }

    private var showsMeterTotalsCard: Bool {
        usbMeter.supportsRecordingView
            || usbMeter.supportsDataGroupCommands
            || usbMeter.recordedAH > 0
            || usbMeter.recordedWH > 0
            || usbMeter.recordingDuration > 0
    }

    private var selectedDraftTransportMode: ChargingTransportMode? {
        openChargeSession?.chargingTransportMode ?? draftChargingTransportMode
    }

    private var selectedDraftChargingStateMode: ChargingStateMode? {
        openChargeSession?.chargingStateMode ?? draftChargingStateMode
    }

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

    private var hasInitialCheckpointInput: Bool {
        initialCheckpoint.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
    }

    private var shouldRequireInitialCheckpoint: Bool {
        initialCheckpointMode == .known
    }

    private var requiresExplicitTransportSelection: Bool {
        (selectedChargedDevice?.supportedChargingModes.count ?? 0) > 1
    }

    private var requiresExplicitChargingStateSelection: Bool {
        (selectedChargedDevice?.supportedChargingStateModes.count ?? 0) > 1
    }

    private var startRequirements: [SessionStartRequirement] {
        var requirements: [SessionStartRequirement] = []

        if openChargeSession != nil {
            requirements.append(.existingSession)
        }

        guard let selectedChargedDevice else {
            requirements.append(.device)
            return requirements
        }

        guard let chargingTransportMode = selectedDraftTransportMode else {
            requirements.append(.chargingType)
            return requirements
        }

        if selectedChargedDevice.supportedChargingModes.contains(chargingTransportMode) == false {
            requirements.append(.chargingType)
        }

        guard let chargingStateMode = selectedDraftChargingStateMode else {
            requirements.append(.chargingMode)
            return requirements
        }

        if selectedChargedDevice.supportedChargingStateModes.contains(chargingStateMode) == false {
            requirements.append(.chargingMode)
        }

        if chargingTransportMode == .wireless, selectedCharger == nil {
            requirements.append(.charger)
        }

        if shouldRequireInitialCheckpoint {
            if hasInitialCheckpointInput == false {
                requirements.append(.initialCheckpointEmpty)
            } else if initialCheckpointValue == nil {
                requirements.append(.initialCheckpointInvalid)
            }
        }

        return requirements
    }

    private var canStartSession: Bool {
        startRequirements.isEmpty
    }

    private var headerStatusTitle: String {
        guard let openChargeSession else {
            return "Idle"
        }
        return openChargeSession.status.title
    }

    private var headerStatusColor: Color {
        guard let openChargeSession else {
            return .secondary
        }

        switch openChargeSession.status {
        case .active:
            return .red
        case .paused:
            return .orange
        case .completed:
            return .green
        case .abandoned:
            return .secondary
        }
    }

    private var sessionChartTimeRange: ClosedRange<Date>? {
        guard let openChargeSession else {
            return nil
        }

        let end = openChargeSession.pausedAt ?? openChargeSession.lastObservedAt
        return openChargeSession.startedAt...max(end, openChargeSession.startedAt)
    }

    private var headerCard: some View {
        VStack(alignment: .leading, spacing: 8) {
            HStack {
                Text("Charging Session")
                    .font(.system(.title3, design: .rounded).weight(.bold))
                ContextInfoButton(
                    title: "Charging Session",
                    message: "Recording starts only after the device, charging type, charging mode, and initial checkpoint are explicit."
                )
                Spacer()
                Text(headerStatusTitle)
                    .font(.caption.weight(.bold))
                    .foregroundColor(headerStatusColor)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .meterCard(
                        tint: headerStatusColor,
                        fillOpacity: 0.18,
                        strokeOpacity: 0.24,
                        cornerRadius: 999
                    )
            }

        }
        .frame(maxWidth: .infinity)
        .padding(18)
        .meterCard(tint: .pink, fillOpacity: 0.18, strokeOpacity: 0.24)
    }

    private var sessionSetupCard: some View {
        VStack(alignment: .leading, spacing: 14) {
            HStack {
                Text(openChargeSession == nil ? "Session Setup" : "Session Context")
                    .font(.headline)
                ContextInfoButton(
                    title: openChargeSession == nil ? "Session Setup" : "Session Context",
                    message: "Select or create the device you are charging. Sessions, checkpoints, curve learning, and wireless charger warnings all depend on that explicit selection."
                )
                Spacer()
                Button("Library") {
                    chargedDeviceLibraryVisibility = true
                }
                .disabled(openChargeSession != nil)
            }

            if let selectedChargedDevice {
                deviceSummary(selectedChargedDevice)

                if openChargeSession == nil {
                    setupControls(for: selectedChargedDevice)
                }

                Button("Edit Device") {
                    editingChargedDevice = selectedChargedDevice
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)
            }

            if selectedChargedDevice != nil {
                Divider()
            }
            standbyMeasurementSection
        }
        .padding(18)
        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
    }

    private func deviceSummary(_ chargedDevice: ChargedDeviceSummary) -> some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(alignment: .top, spacing: 14) {
                ChargedDeviceQRCodeView(
                    qrIdentifier: chargedDevice.qrIdentifier,
                    side: 88
                )

                VStack(alignment: .leading, spacing: 8) {
                    Label(chargedDevice.name, systemImage: chargedDevice.identitySymbolName)
                        .font(.headline)

                    Text(chargedDevice.identityTitle)
                        .font(.caption.weight(.semibold))
                        .foregroundColor(.secondary)

                    Text(chargedDevice.chargingStateAvailability.description)
                        .font(.caption2)
                        .foregroundColor(.secondary)

                    Text(chargedDevice.supportedChargingModes.map(\.title).joined(separator: " + "))
                        .font(.caption2)
                        .foregroundColor(.secondary)

                    if let estimatedCapacity = chargedDevice.estimatedBatteryCapacityWh {
                        Text("Estimated capacity: \(estimatedCapacity.format(decimalDigits: 2)) Wh")
                            .font(.caption2)
                            .foregroundColor(.secondary)
                    }
                }

                Spacer(minLength: 0)
            }

            if showsWirelessChargerSection {
                Divider()
                wirelessChargerSection
            }
        }
    }

    private func setupControls(for chargedDevice: ChargedDeviceSummary) -> some View {
        VStack(alignment: .leading, spacing: 12) {
            if requiresExplicitTransportSelection {
                VStack(alignment: .leading, spacing: 8) {
                    Text("Charging Type")
                        .font(.subheadline.weight(.semibold))

                    compactSelectionMenu(
                        title: draftChargingTransportMode?.title ?? "Choose",
                        options: chargedDevice.supportedChargingModes.map { chargingTransportMode in
                            CompactSelectionOption(
                                id: chargingTransportMode.id,
                                title: chargingTransportMode.title,
                                isSelected: draftChargingTransportMode == chargingTransportMode,
                                action: { draftChargingTransportMode = chargingTransportMode }
                            )
                        }
                    )

                    if draftChargingTransportMode == nil {
                        Text("Pick the charging type explicitly before starting.")
                            .font(.caption2)
                            .foregroundColor(.orange)
                    }
                }
            } else if let chargingTransportMode = chargedDevice.supportedChargingModes.first {
                Label(
                    "Charging type: \(chargingTransportMode.title)",
                    systemImage: chargingTransportMode.symbolName
                )
                .font(.subheadline.weight(.semibold))
            }

            if requiresExplicitChargingStateSelection {
                VStack(alignment: .leading, spacing: 8) {
                    Text("Charging Mode")
                        .font(.subheadline.weight(.semibold))

                    compactSelectionMenu(
                        title: draftChargingStateMode?.title ?? "Choose",
                        options: chargedDevice.supportedChargingStateModes.map { chargingStateMode in
                            CompactSelectionOption(
                                id: chargingStateMode.id,
                                title: chargingStateMode.title,
                                isSelected: draftChargingStateMode == chargingStateMode,
                                action: { draftChargingStateMode = chargingStateMode }
                            )
                        }
                    )

                    if draftChargingStateMode == nil {
                        Text("Pick whether the device is on or off for this session.")
                            .font(.caption2)
                            .foregroundColor(.orange)
                    }
                }
            } else if let chargingStateMode = chargedDevice.supportedChargingStateModes.first {
                Label(
                    "Charging mode: \(chargingStateMode.title)",
                    systemImage: chargingStateMode == .off ? "power.circle" : "power"
                )
                .font(.subheadline.weight(.semibold))
            }

            VStack(alignment: .leading, spacing: 8) {
                HStack(spacing: 8) {
                    Text("Initial Checkpoint")
                        .font(.subheadline.weight(.semibold))
                    ContextInfoButton(
                        title: "Initial Checkpoint",
                        message: "Use the battery level shown by the device right now when it is known. A known checkpoint improves battery prediction and capacity learning, but the session can also start without one when the level is unavailable."
                    )
                }

                compactSelectionMenu(
                    title: initialCheckpointMode.title,
                    options: InitialCheckpointMode.allCases.map { mode in
                        CompactSelectionOption(
                            id: mode.id,
                            title: mode.title,
                            isSelected: initialCheckpointMode == mode,
                            action: { initialCheckpointMode = mode }
                        )
                    }
                )

                if initialCheckpointMode == .known {
                    HStack(spacing: 10) {
                        Button {
                            adjustInitialCheckpoint(by: -1)
                        } label: {
                            Image(systemName: "minus.circle")
                                .font(.title3)
                        }
                        .buttonStyle(.plain)

                        TextField("Battery %", text: $initialCheckpoint)
                            .keyboardType(.decimalPad)
                            .textFieldStyle(.roundedBorder)
                            .frame(width: 92)

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

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

                        Spacer()
                    }
                } else {
                    Text(
                        initialCheckpointMode == .flat
                        ? "Use Flat when the device does not turn on yet. Predictions and capacity estimates stay off until you record a positive battery level."
                        : "Start without an initial battery checkpoint only when the level cannot be read reliably, for example on a device without display."
                    )
                        .font(.caption2)
                        .foregroundColor(.orange)
                }

            }

            VStack(alignment: .leading, spacing: 8) {
                Text("Start Requirements")
                    .font(.subheadline.weight(.semibold))

                if startRequirements.isEmpty {
                    Label("Everything needed to start is ready.", systemImage: "checkmark.circle.fill")
                        .font(.caption)
                        .foregroundColor(.green)
                } else {
                    ForEach(startRequirements) { requirement in
                        Label(requirement.message, systemImage: "exclamationmark.circle")
                            .font(.caption)
                            .foregroundColor(.orange)
                    }
                }
            }

            Button("Start Session") {
                startSession()
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .meterCard(tint: .green, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
            .buttonStyle(.plain)
            .disabled(!canStartSession)
        }
    }

    private var wirelessChargerSection: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                Text("Wireless Charger")
                    .font(.subheadline.weight(.semibold))
                Spacer()
                Button(selectedCharger == nil ? "Select" : "Change") {
                    chargerLibraryVisibility = true
                }
                .disabled(openChargeSession != nil)
            }

            if let selectedCharger {
                HStack(alignment: .top, spacing: 12) {
                    ChargedDeviceQRCodeView(
                        qrIdentifier: selectedCharger.qrIdentifier,
                        side: 62
                    )

                    VStack(alignment: .leading, spacing: 6) {
                        Label(selectedCharger.name, systemImage: selectedCharger.identitySymbolName)
                            .font(.subheadline.weight(.semibold))

                        if let chargerMaximumPowerWatts = selectedCharger.chargerMaximumPowerWatts {
                            Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }

                        if let chargerIdleCurrentAmps = selectedCharger.chargerIdleCurrentAmps {
                            Text("Idle current: \(chargerIdleCurrentAmps.format(decimalDigits: 2)) A")
                                .font(.caption2)
                                .foregroundColor(.secondary)
                        } else {
                            Text("Idle current is missing, so wireless stop-threshold learning and auto-stop stay unavailable.")
                                .font(.caption2)
                                .foregroundColor(.orange)
                        }
                    }
                }

            } else {
                Text("Wireless sessions need a selected charger in addition to the charged device.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
    }

    private var standbyMeasurementSection: some View {
        VStack(alignment: .leading, spacing: 10) {
            HStack {
                Text("Charger Standby Power")
                    .font(.subheadline.weight(.semibold))
                Spacer()
                Button(selectedCharger == nil ? "Select Charger" : "Change Charger") {
                    chargerLibraryVisibility = true
                }
                .disabled(openChargeSession != nil)
            }

            if let selectedCharger {
                HStack(alignment: .top, spacing: 12) {
                    ChargedDeviceQRCodeView(
                        qrIdentifier: selectedCharger.qrIdentifier,
                        side: 62
                    )

                    VStack(alignment: .leading, spacing: 6) {
                        Label(selectedCharger.name, systemImage: selectedCharger.identitySymbolName)
                            .font(.subheadline.weight(.semibold))

                        Text(
                            selectedCharger.latestStandbyPowerMeasurement.map {
                                "Latest standby: \($0.averagePowerWatts.format(decimalDigits: 3)) W"
                            } ?? "No standby baseline saved yet."
                        )
                        .font(.caption)
                        .foregroundColor(.secondary)
                    }
                }

                NavigationLink(
                    destination: ChargerStandbyPowerWizardView(
                        preferredMeterMACAddress: meterMACAddress,
                        preferredChargerID: selectedCharger.id
                    )
                ) {
                    Label("New Measurement", systemImage: "plus.circle.fill")
                        .font(.subheadline.weight(.semibold))
                        .foregroundColor(.orange)
                }
                .buttonStyle(.plain)
                .disabled(openChargeSession != nil)

                if openChargeSession != nil {
                    Text("Stop or pause the active charge session before starting a standby-power run on this meter.")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            } else {
                NavigationLink(
                    destination: ChargerStandbyPowerWizardView(
                        preferredMeterMACAddress: meterMACAddress
                    )
                ) {
                    Label("New Measurement", systemImage: "plus.circle.fill")
                        .font(.subheadline.weight(.semibold))
                        .foregroundColor(.orange)
                }
                .buttonStyle(.plain)

                Text("Open the wizard and choose the charger there, or preselect one from Charge Record first.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
    }

    private func chargingMonitorCard(_ openChargeSession: ChargeSessionSummary) -> some View {
        let displayedEnergyWh = displayedSessionEnergyWh(for: openChargeSession)
        let displayedChargeAh = displayedSessionChargeAh(for: openChargeSession)
        let canAddCheckpoint = appData.canAddBatteryCheckpoint(to: openChargeSession.id)
        return VStack(alignment: .leading, spacing: 12) {
            HStack(spacing: 8) {
                Text("Charging Monitor")
                    .font(.headline)
                ContextInfoButton(
                    title: "Charging Monitor",
                    message: "The monitor keeps the session explicit: it never auto-selects another device or charger, and it never starts a new session on its own."
                )
            }

            ChargeRecordMetricsTableView(
                labels: ["Type", "Mode", "Energy", "Duration", "Auto Stop"],
                values: [
                    openChargeSession.chargingTransportMode.title,
                    openChargeSession.chargingStateMode.title,
                    "\(displayedEnergyWh.format(decimalDigits: 3)) Wh",
                    formatDuration(max(openChargeSession.effectiveDuration, 0)),
                    autoStopLabel(for: openChargeSession)
                ]
            )

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

            if let batteryPrediction = selectedChargedDevice?.batteryLevelPrediction(
                for: openChargeSession,
                effectiveEnergyWhOverride: displayedEnergyWh
            ) {
                VStack(alignment: .leading, spacing: 4) {
                    Text("Predicted battery level: \(batteryPrediction.predictedPercent.format(decimalDigits: 0))%")
                        .font(.caption.weight(.semibold))
                    Text(
                        "Anchored to \(batteryPrediction.anchorDescription) at \(batteryPrediction.anchorPercent.format(decimalDigits: 0))% using \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh estimated capacity."
                    )
                    .font(.caption2)
                    .foregroundColor(.secondary)
                }
            }

            if let sessionWarning = sessionWarning(for: openChargeSession) {
                Text(sessionWarning)
                    .font(.caption)
                    .foregroundColor(.orange)
            }

            if openChargeSession.isPaused {
                Text("Paused at \(openChargeSession.pausedAt?.format() ?? openChargeSession.lastObservedAt.format()). If it stays paused for more than 10 minutes, it stops automatically.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            if openChargeSession.requiresCompletionConfirmation {
                completionConfirmationCard(openChargeSession)
            }

            if let targetBatteryPercent = openChargeSession.targetBatteryPercent {
                Text("Target notification: \(targetBatteryPercent.format(decimalDigits: 0))%")
                    .font(.caption.weight(.semibold))
            } else {
                Text("No target battery notification configured.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            if !openChargeSession.checkpoints.isEmpty {
                checkpointList(
                    checkpoints: Array(openChargeSession.checkpoints.suffix(6).reversed())
                )
            }

            if canAddCheckpoint {
                Button(showsInlineCheckpointEditor ? "Hide Checkpoint Editor" : "Add Battery Checkpoint") {
                    showsInlineCheckpointEditor.toggle()
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .meterCard(tint: .green, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)

                if showsInlineCheckpointEditor {
                    BatteryCheckpointEditorContentView(
                        sessionID: openChargeSession.id,
                        message: "The checkpoint is stored on the active charge session and later used for capacity estimation and the typical charge curve.",
                        effectiveEnergyWhOverride: displayedEnergyWh,
                        measuredChargeAhOverride: displayedChargeAh,
                        onCancel: { showsInlineCheckpointEditor = false },
                        onSaved: { showsInlineCheckpointEditor = false }
                    )
                }
            } else if let checkpointEditingMessage = appData.batteryCheckpointCaptureRequirementMessage(for: openChargeSession.id) {
                Text(checkpointEditingMessage)
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            Button(openChargeSession.targetBatteryPercent == nil ? "Set Target Notification" : "Change Target Notification") {
                targetNotificationEditorVisibility = true
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .meterCard(tint: .indigo, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
            .buttonStyle(.plain)

            if openChargeSession.targetBatteryPercent != nil {
                Button("Clear Target Notification") {
                    _ = appData.setTargetBatteryPercent(nil, for: openChargeSession.id)
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 14)
                .buttonStyle(.plain)
            }

            if openChargeSession.status == .active {
                Button("Pause Session") {
                    _ = appData.pauseChargeSession(sessionID: openChargeSession.id, from: usbMeter)
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)
            } else if openChargeSession.status == .paused {
                Button("Resume Session") {
                    _ = appData.resumeChargeSession(sessionID: openChargeSession.id, from: usbMeter)
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 10)
                .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)
            }

            Button("Stop Session") {
                pendingStopRequest = ChargeSessionStopRequest(
                    sessionID: openChargeSession.id,
                    title: "Stop Session",
                    confirmTitle: "Stop",
                    explanation: "Record the final battery checkpoint before closing this session."
                )
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .meterCard(tint: .red, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
            .buttonStyle(.plain)

        }
        .padding(18)
        .meterCard(tint: .green, fillOpacity: 0.14, strokeOpacity: 0.20)
    }

    private func completionConfirmationCard(_ openChargeSession: ChargeSessionSummary) -> some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Completion Needs Confirmation")
                .font(.subheadline.weight(.semibold))

            if let contradictionPercent = openChargeSession.completionContradictionPercent {
                Text("Current says charging may have stopped, but the estimated battery level is only \(contradictionPercent.format(decimalDigits: 0))%.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            } else {
                Text("Current dropped to the learned stop threshold, but the battery prediction does not look like a normal finish yet.")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            Button("Finish Session With Final Checkpoint") {
                pendingStopRequest = ChargeSessionStopRequest(
                    sessionID: openChargeSession.id,
                    title: "Finish Session",
                    confirmTitle: "Finish",
                    explanation: "Add the final checkpoint before confirming the stop."
                )
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
            .buttonStyle(.plain)

            Button("Keep Monitoring") {
                _ = appData.continueChargeSessionMonitoring(sessionID: openChargeSession.id)
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 10)
            .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 sessionChartCard(
        timeRange: ClosedRange<Date>,
        session: ChargeSessionSummary
    ) -> some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(spacing: 8) {
                Text("Session Chart")
                    .font(.headline)
                ContextInfoButton(
                    title: "Session Chart",
                    message: "The chart is scoped to the explicit session window for \(session.sessionKind.shortTitle.lowercased()) charging."
                )
            }

            GeometryReader { geometry in
                let chartWidth = max(geometry.size.width, 1)
                let compactChartLayout = chartWidth < 760
                let chartHeight = compactChartLayout ? 290.0 : 350.0

                MeasurementChartView(
                    compactLayout: compactChartLayout,
                    availableSize: CGSize(width: chartWidth, height: chartHeight),
                    timeRange: timeRange,
                    showsRangeSelector: false,
                    rebasesEnergyToVisibleRangeStart: true
                )
                .environmentObject(usbMeter.measurements)
                .frame(maxWidth: .infinity, alignment: .topLeading)
            }
            .frame(height: 350)
        }
        .padding(18)
        .meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20)
    }

    private var meterTotalsCard: some View {
        return VStack(alignment: .leading, spacing: 12) {
            HStack(spacing: 8) {
                Text("Meter Recorder")
                    .font(.headline)

                Spacer(minLength: 0)

                Button {
                    showsMeterTotalsInfo.toggle()
                } label: {
                    Image(systemName: "info.circle")
                        .font(.body.weight(.semibold))
                        .foregroundColor(.secondary)
                }
                .buttonStyle(.plain)
                .accessibilityLabel("Meter recorder info")
                .popover(isPresented: $showsMeterTotalsInfo, arrowEdge: .top) {
                    VStack(alignment: .leading, spacing: 10) {
                        Text("Meter Recorder")
                            .font(.headline)
                        Text("These values come directly from the meter's built-in recorder. Keep them visible while comparing the app session against what the meter captured on its own.")
                            .font(.body)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                    .padding(16)
                    .frame(width: 280, alignment: .leading)
                }
            }

            ChargeRecordMetricsTableView(
                labels: ["Energy", "Duration", "Meter Threshold"],
                values: [
                    "\(usbMeter.recordedWH.format(decimalDigits: 3)) Wh",
                    usbMeter.recordingDurationDescription,
                    usbMeter.supportsRecordingThreshold ? "\(usbMeter.recordingTreshold.format(decimalDigits: 2)) A" : "Read-only"
                ]
            )

            if let recordingBootedAt = usbMeter.recordingBootedAt {
                Text("Recorder uptime suggests the meter booted at \(recordingBootedAt.format()).")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
        .padding(18)
        .meterCard(tint: .teal, fillOpacity: 0.14, strokeOpacity: 0.20)
    }

    private var showsWirelessChargerSection: Bool {
        let transportMode = selectedDraftTransportMode ?? selectedChargedDevice?.supportedChargingModes.first
        return transportMode == .wireless
    }

    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 displayedSessionEnergyWh(for session: ChargeSessionSummary) -> Double {
        let storedEnergyWh = session.effectiveOrMeasuredEnergyWh
        guard session.status.isOpen else {
            return storedEnergyWh
        }

        guard session.meterMACAddress == meterMACAddress else {
            return storedEnergyWh
        }

        if let baselineEnergyWh = session.meterEnergyBaselineWh {
            return max(storedEnergyWh, max(usbMeter.recordedWH - baselineEnergyWh, 0))
        }

        return storedEnergyWh
    }

    private func displayedSessionChargeAh(for session: ChargeSessionSummary) -> Double {
        let storedChargeAh = session.measuredChargeAh
        guard session.status.isOpen else {
            return storedChargeAh
        }

        guard session.meterMACAddress == meterMACAddress else {
            return storedChargeAh
        }

        if let baselineChargeAh = session.meterChargeBaselineAh {
            return max(storedChargeAh, max(usbMeter.recordedAH - baselineChargeAh, 0))
        }

        return storedChargeAh
    }

    private func checkpointList(checkpoints: [ChargeCheckpointSummary]) -> some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Battery Checkpoints")
                .font(.subheadline.weight(.semibold))

            ForEach(checkpoints, id: \.id) { checkpoint in
                HStack {
                    Text(checkpoint.timestamp.format())
                        .font(.caption2)
                        .foregroundColor(.secondary)
                    Text(checkpoint.flag.title)
                        .font(.caption2.weight(.semibold))
                        .foregroundColor(.secondary)
                    Spacer()
                    Text("\(checkpoint.batteryPercent.format(decimalDigits: 0))%")
                        .font(.caption.weight(.semibold))
                    Text("•")
                        .foregroundColor(.secondary)
                    Text("\(checkpoint.measuredEnergyWh.format(decimalDigits: 2)) Wh")
                        .font(.caption2)
                        .foregroundColor(.secondary)
                    Button {
                        pendingCheckpointDeletion = checkpoint
                    } label: {
                        Image(systemName: "trash")
                            .font(.caption.weight(.semibold))
                            .foregroundColor(.red)
                    }
                    .buttonStyle(.plain)
                    .help("Delete checkpoint")
                }
            }
        }
    }

    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 sessionWarning(for session: ChargeSessionSummary) -> String? {
        guard session.chargingTransportMode == .wireless,
              let chargerID = session.chargerID,
              let charger = appData.chargedDeviceSummary(id: chargerID) else {
            return nil
        }

        guard charger.chargerIdleCurrentAmps == nil else {
            return nil
        }

        return "The selected charger has no idle-current measurement. Wireless stop-threshold learning and precise auto-stop are unavailable for this session."
    }

    private func startSession() {
        guard let selectedChargedDevice,
              let chargingTransportMode = selectedDraftTransportMode,
              let chargingStateMode = selectedDraftChargingStateMode else {
            return
        }

        let chargerID = chargingTransportMode == .wireless ? selectedCharger?.id : nil
        let didStart = appData.startChargeSession(
            for: usbMeter,
            chargedDeviceID: selectedChargedDevice.id,
            chargerID: chargerID,
            chargingTransportMode: chargingTransportMode,
            chargingStateMode: chargingStateMode,
            autoStopEnabled: false,
            initialBatteryPercent: initialCheckpointMode == .known ? initialCheckpointValue : nil,
            startsFromFlatBattery: initialCheckpointMode == .flat
        )

        if didStart {
            initialCheckpoint = ""
            initialCheckpointMode = .known
        }
    }

    private func adjustInitialCheckpoint(by delta: Double) {
        guard initialCheckpointMode == .known else {
            return
        }

        let currentValue = initialCheckpointValue ?? 0
        let nextValue = min(max(currentValue + delta, 0), 100)
        initialCheckpoint = nextValue.format(decimalDigits: 0)
    }

    private func syncDraftSelections() {
        guard let selectedChargedDevice else {
            draftChargingTransportMode = nil
            draftChargingStateMode = nil
            return
        }

        if let openChargeSession {
            draftChargingTransportMode = openChargeSession.chargingTransportMode
            draftChargingStateMode = openChargeSession.chargingStateMode
            return
        }

        if let draftChargingTransportMode,
           selectedChargedDevice.supportedChargingModes.contains(draftChargingTransportMode) == false {
            self.draftChargingTransportMode = nil
        }

        if let draftChargingStateMode,
           selectedChargedDevice.supportedChargingStateModes.contains(draftChargingStateMode) == false {
            self.draftChargingStateMode = nil
        }

        if selectedChargedDevice.supportedChargingModes.count == 1 {
            draftChargingTransportMode = selectedChargedDevice.supportedChargingModes.first
        }

        if let draftChargingTransportMode {
            draftChargingStateMode = draftChargingStateMode
                ?? selectedChargedDevice.defaultChargingStateMode(for: draftChargingTransportMode)
        } else if selectedChargedDevice.supportedChargingStateModes.count == 1 {
            draftChargingStateMode = selectedChargedDevice.supportedChargingStateModes.first
        }
    }

    private struct CompactSelectionOption: Identifiable {
        let id: String
        let title: String
        let isSelected: Bool
        let action: () -> Void
    }

    private func compactSelectionMenu(
        title: String,
        options: [CompactSelectionOption]
    ) -> some View {
        Menu {
            ForEach(options) { option in
                Button {
                    option.action()
                } label: {
                    if option.isSelected {
                        Label(option.title, systemImage: "checkmark")
                    } else {
                        Text(option.title)
                    }
                }
            }
        } label: {
            HStack(spacing: 8) {
                Text(title)
                    .foregroundColor(.primary)
                Spacer()
                Image(systemName: "chevron.up.chevron.down")
                    .font(.caption.weight(.semibold))
                    .foregroundColor(.secondary)
            }
            .padding(.horizontal, 12)
            .padding(.vertical, 9)
            .frame(width: 180, alignment: .leading)
            .meterCard(tint: .secondary, fillOpacity: 0.08, strokeOpacity: 0.14, cornerRadius: 12)
        }
        .buttonStyle(.plain)
    }
}

struct ChargeSessionCompletionSheetView: View {
    @EnvironmentObject private var appData: AppData
    @Environment(\.dismiss) private var dismiss

    let sessionID: UUID
    let title: String
    let confirmTitle: String
    let explanation: String

    @State private var batteryPercent = ""

    var body: some View {
        NavigationView {
            Form {
                Section(
                    header: ContextInfoHeader(
                        title: "Final Checkpoint",
                        message: explanation
                    )
                ) {
                    TextField("Battery %", text: $batteryPercent)
                        .keyboardType(.decimalPad)
                }

                Section {
                    if let sessionWarning {
                        Text(sessionWarning)
                            .font(.footnote)
                            .foregroundColor(.orange)
                    } else if (parsedBatteryPercent ?? 0) >= 99.5 {
                        Text("A final checkpoint at 100% lets the app learn the stop current for this exact charging type when the session data is reliable.")
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    }
                }
            }
            .navigationTitle(title)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button(confirmTitle) {
                        guard let batteryPercent = parsedBatteryPercent else {
                            return
                        }

                        if appData.stopChargeSession(
                            sessionID: sessionID,
                            finalBatteryPercent: batteryPercent
                        ) {
                            dismiss()
                        }
                    }
                    .disabled(parsedBatteryPercent == nil)
                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }

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

    private var sessionWarning: String? {
        guard let session = appData.chargedDevices
            .flatMap(\.sessions)
            .first(where: { $0.id == sessionID }),
              session.chargingTransportMode == .wireless,
              let chargerID = session.chargerID,
              let charger = appData.chargedDeviceSummary(id: chargerID),
              charger.chargerIdleCurrentAmps == nil else {
            return nil
        }

        return "This charger has no idle-current measurement, so the final checkpoint will stop the session but will not learn a wireless stop threshold yet."
    }
}

private struct ChargeSessionStopRequest: Identifiable {
    let sessionID: UUID
    let title: String
    let confirmTitle: String
    let explanation: String

    var id: UUID {
        sessionID
    }
}