USB-Meter / USB Meter / Views / Meter / Tabs / ChargeRecord / MeterChargeRecordTabView.swift
1 contributor
1300 lines | 52.426kb
//
//  MeterChargeRecordTabView.swift
//  USB Meter
//

import SwiftUI

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

struct MeterChargeRecordContentView: View {
    private struct SessionMetricRow {
        let label: String
        let value: String
    }

    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"
            }
        }
    }

    private enum ActiveMode: Hashable {
        case chargeSession
        case standbyPower
    }

    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."
            }
        }
    }

    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
    @EnvironmentObject private var usbMeter: Meter

    @State private var chargedDeviceLibraryVisibility = false
    @State private var chargerLibraryVisibility = false
    @State private var showingInlineTargetEditor = false
    @State private var draftTargetText = ""
    @State private var showingStopConfirm = false
    @State private var finalCheckpointMode: FinalCheckpoint = .full
    @State private var finalCheckpointText = ""
    @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 activeMode: ActiveMode = .chargeSession

    var body: some View {
        ScrollView {
            VStack(spacing: 14) {
                statusHeader

                if let openChargeSession {
                    chargingMonitorCard(openChargeSession)

                    if showsMeterTotalsCard {
                        meterTotalsCard
                    }

                    if let range = sessionChartTimeRange {
                        sessionChartCard(timeRange: range, session: openChargeSession)
                    }
                } else {
                    modePicker

                    switch activeMode {
                    case .chargeSession:
                        chargeSessionSetupCard
                    case .standbyPower:
                        standbyPowerCard
                    }

                    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)
        }
        .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()
            showingInlineTargetEditor = false
            draftTargetText = ""
        }
    }

    // MARK: - Computed Properties

    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 showsWirelessChargerSection: Bool {
        let transportMode = selectedDraftTransportMode ?? selectedChargedDevice?.supportedChargingModes.first
        return transportMode == .wireless
    }

    // MARK: - Status Header

    private var statusHeader: some View {
        HStack {
            Image(systemName: "bolt.fill")
                .foregroundColor(.pink)
            Text("Charging Session")
                .font(.system(.title3, design: .rounded).weight(.bold))
            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)
        }
        .padding(.horizontal, 18)
        .padding(.vertical, 12)
        .meterCard(tint: .pink, fillOpacity: 0.18, strokeOpacity: 0.24)
    }

    // MARK: - Mode Picker

    private var modePicker: some View {
        Picker("", selection: $activeMode) {
            Label("Charge Session", systemImage: "bolt.fill").tag(ActiveMode.chargeSession)
            Label("Standby Power", systemImage: "powersleep").tag(ActiveMode.standbyPower)
        }
        .pickerStyle(.segmented)
        .labelsHidden()
    }

    // MARK: - Charge Session Setup

    private var chargeSessionSetupCard: some View {
        VStack(alignment: .leading, spacing: 0) {
            // Device
            setupRow(icon: "iphone", iconColor: .blue) {
                if let device = selectedChargedDevice {
                    ChargedDeviceIdentityLabelView(chargedDevice: device, iconPointSize: 15)
                        .font(.subheadline.weight(.semibold))
                } else {
                    Text("No device selected")
                        .foregroundColor(.secondary)
                        .font(.subheadline)
                }
                Spacer(minLength: 8)
                Button(selectedChargedDevice == nil ? "Select" : "Change") {
                    chargedDeviceLibraryVisibility = true
                }
                .font(.caption.weight(.semibold))
                .buttonStyle(.bordered)
                .controlSize(.small)
            }

            // Charging type — only when device supports multiple
            if requiresExplicitTransportSelection, let device = selectedChargedDevice {
                Divider().padding(.leading, 46)
                setupRow(icon: draftChargingTransportMode?.symbolName ?? "bolt.slash", iconColor: .orange) {
                    Text("Type")
                        .foregroundColor(.secondary)
                        .font(.subheadline)
                    Spacer()
                    compactSelectionMenu(
                        title: draftChargingTransportMode?.title ?? "Choose",
                        options: device.supportedChargingModes.map { mode in
                            CompactSelectionOption(
                                id: mode.id, title: mode.title,
                                isSelected: draftChargingTransportMode == mode,
                                action: { draftChargingTransportMode = mode }
                            )
                        }
                    )
                }
            }

            // Charging state — only when device supports multiple
            if requiresExplicitChargingStateSelection, let device = selectedChargedDevice {
                Divider().padding(.leading, 46)
                setupRow(icon: draftChargingStateMode == .off ? "power.circle" : "power", iconColor: .purple) {
                    Text("Mode")
                        .foregroundColor(.secondary)
                        .font(.subheadline)
                    Spacer()
                    compactSelectionMenu(
                        title: draftChargingStateMode?.title ?? "Choose",
                        options: device.supportedChargingStateModes.map { mode in
                            CompactSelectionOption(
                                id: mode.id, title: mode.title,
                                isSelected: draftChargingStateMode == mode,
                                action: { draftChargingStateMode = mode }
                            )
                        }
                    )
                }
            }

            // Wireless charger — only when wireless transport
            if showsWirelessChargerSection {
                Divider().padding(.leading, 46)
                setupRow(icon: "antenna.radiowaves.left.and.right", iconColor: .teal) {
                    if let charger = selectedCharger {
                        ChargedDeviceIdentityLabelView(chargedDevice: charger, iconPointSize: 15)
                            .font(.subheadline.weight(.semibold))
                        if charger.chargerIdleCurrentAmps == nil {
                            Image(systemName: "exclamationmark.triangle.fill")
                                .foregroundColor(.orange)
                                .font(.caption)
                        }
                    } else {
                        Text("No charger selected")
                            .foregroundColor(.secondary)
                            .font(.subheadline)
                    }
                    Spacer(minLength: 8)
                    Button(selectedCharger == nil ? "Select" : "Change") {
                        chargerLibraryVisibility = true
                    }
                    .font(.caption.weight(.semibold))
                    .buttonStyle(.bordered)
                    .controlSize(.small)
                }
            }

            // Battery checkpoint
            Divider().padding(.leading, 46)
            setupRow(icon: "battery.75percent", iconColor: .green) {
                if initialCheckpointMode == .known {
                    Button { adjustInitialCheckpoint(by: -1) } label: {
                        Image(systemName: "minus.circle").font(.title3)
                    }
                    .buttonStyle(.plain)

                    TextField("—", text: $initialCheckpoint)
                        .keyboardType(.decimalPad)
                        .textFieldStyle(.roundedBorder)
                        .frame(width: 52)
                        .multilineTextAlignment(.center)

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

                    Button { adjustInitialCheckpoint(by: 1) } label: {
                        Image(systemName: "plus.circle").font(.title3)
                    }
                    .buttonStyle(.plain)
                } else {
                    Text(initialCheckpointMode == .flat
                         ? "Flat (device off / discharged)"
                         : "Unknown")
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
                Spacer()
                compactSelectionMenu(
                    title: initialCheckpointMode.title,
                    options: InitialCheckpointMode.allCases.map { mode in
                        CompactSelectionOption(
                            id: mode.id, title: mode.title,
                            isSelected: initialCheckpointMode == mode,
                            action: { initialCheckpointMode = mode }
                        )
                    }
                )
            }

            // Requirement errors
            if startRequirements.isEmpty == false {
                Divider()
                VStack(alignment: .leading, spacing: 6) {
                    ForEach(startRequirements) { requirement in
                        Label(requirement.message, systemImage: "exclamationmark.circle")
                            .font(.caption)
                            .foregroundColor(.orange)
                    }
                }
                .padding(.horizontal, 14)
                .padding(.vertical, 10)
            }

            // Start button
            Divider()
            Button("Start Session") {
                startSession()
            }
            .frame(maxWidth: .infinity)
            .padding(.vertical, 11)
            .font(.subheadline.weight(.semibold))
            .foregroundColor(canStartSession ? .green : .secondary)
            .buttonStyle(.plain)
            .disabled(!canStartSession)
        }
        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
    }

    // MARK: - Standby Power Card

    private var standbyPowerCard: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(spacing: 10) {
                Image(systemName: "powersleep")
                    .foregroundColor(.orange)
                    .font(.title3)
                VStack(alignment: .leading, spacing: 2) {
                    Text("Charger Standby Power")
                        .font(.subheadline.weight(.semibold))
                    Text("Measure idle draw with no device connected.")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }

            NavigationLink(
                destination: ChargerStandbyPowerWizardView(
                    preferredMeterMACAddress: meterMACAddress
                )
            ) {
                HStack {
                    Image(systemName: "plus.circle.fill")
                        .foregroundColor(.orange)
                    Text("New Measurement")
                        .font(.subheadline.weight(.semibold))
                    Spacer()
                    Image(systemName: "chevron.right")
                        .font(.caption.weight(.semibold))
                        .foregroundColor(.secondary)
                }
                .padding(.vertical, 10)
                .padding(.horizontal, 14)
                .meterCard(tint: .orange, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 14)
            }
            .buttonStyle(.plain)
        }
        .padding(18)
        .meterCard(tint: .orange, fillOpacity: 0.10, strokeOpacity: 0.16)
    }

    // MARK: - Charging Monitor Card

    private func chargingMonitorCard(_ openChargeSession: ChargeSessionSummary) -> some View {
        let displayedEnergyWh = displayedSessionEnergyWh(for: openChargeSession)
        let displayedChargeAh = displayedSessionChargeAh(for: openChargeSession)
        let canAddCheckpoint = appData.canAddBatteryCheckpoint(to: openChargeSession.id)
        let metricRows = sessionMetricRows(for: openChargeSession, displayedEnergyWh: displayedEnergyWh)

        return VStack(alignment: .leading, spacing: 12) {
            // Header
            HStack {
                if let device = selectedChargedDevice {
                    ChargedDeviceIdentityLabelView(chargedDevice: device, iconPointSize: 16)
                        .font(.headline)
                } else {
                    Text("Charging Monitor").font(.headline)
                }
                Spacer()
                Text(openChargeSession.status.title)
                    .font(.caption.weight(.bold))
                    .foregroundColor(headerStatusColor)
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .meterCard(tint: headerStatusColor, fillOpacity: 0.18, strokeOpacity: 0.24, cornerRadius: 999)
            }

            ChargeRecordMetricsTableView(
                labels: metricRows.map(\.label),
                values: metricRows.map(\.value)
            )

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

            if let batteryPrediction = selectedChargedDevice?.batteryLevelPrediction(
                for: openChargeSession,
                effectiveEnergyWhOverride: displayedEnergyWh
            ) {
                HStack(spacing: 6) {
                    Image(systemName: "battery.75percent")
                        .foregroundColor(.green)
                    Text("Predicted: \(batteryPrediction.predictedPercent.format(decimalDigits: 0))%")
                        .font(.caption.weight(.semibold))
                    Text("· \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh est.")
                        .font(.caption2)
                        .foregroundColor(.secondary)
                }
            }

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

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

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

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

            targetSectionView(
                for: openChargeSession,
                predictedPercent: selectedChargedDevice?.batteryLevelPrediction(
                    for: openChargeSession,
                    effectiveEnergyWhOverride: displayedEnergyWh
                )?.predictedPercent
            )

            if showingStopConfirm {
                stopConfirmPanel(for: openChargeSession)
            } else {
                // Session controls
                HStack(spacing: 10) {
                    if openChargeSession.status == .active {
                        Button("Pause") {
                            _ = 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") {
                            _ = 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") {
                        finalCheckpointMode = .full
                        finalCheckpointText = ""
                        showingStopConfirm = true
                    }
                    .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) {
            Label("Charging may have stopped", systemImage: "questionmark.circle.fill")
                .font(.subheadline.weight(.semibold))

            if let contradictionPercent = openChargeSession.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") {
                    finalCheckpointMode = .full
                    finalCheckpointText = ""
                    showingStopConfirm = true
                }
                .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: openChargeSession.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)
    }

    // MARK: - Target Section

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

        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 {
                Button {
                    let current = parsedDraftTarget ?? session.targetBatteryPercent ?? 80
                    let next = max(current - 1, 1)
                    draftTargetText = next.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 {
                    Button {} label: {
                        Image(systemName: "exclamationmark.triangle.fill")
                            .font(.body.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.")
                }

                Button {
                    let current = parsedDraftTarget ?? session.targetBatteryPercent ?? 80
                    let next = min(current + 1, 100)
                    draftTargetText = next.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)

            } else {
                if let targetPercent = session.targetBatteryPercent {
                    Text("\(targetPercent.format(decimalDigits: 0))%")
                        .font(.subheadline.weight(.semibold))
                        .foregroundColor(savedBelowPrediction ? .orange : .indigo)

                    if savedBelowPrediction {
                        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.")
                    }

                    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 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 func stopConfirmPanel(for session: ChargeSessionSummary) -> some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Final Checkpoint (optional)")
                .font(.subheadline.weight(.semibold))

            // Three compact option tiles
            HStack(spacing: 8) {
                ForEach([FinalCheckpoint.full, .skip, .custom], id: \.self) { mode in
                    Button {
                        finalCheckpointMode = mode
                        if mode != .custom {
                            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)
                }
            }

            // Custom % input
            if finalCheckpointMode == .custom {
                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)

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

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

                    Spacer()
                }
            }

            // Action row
            HStack(spacing: 10) {
                Button("Cancel") {
                    showingStopConfirm = false
                    finalCheckpointText = ""
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 9)
                .meterCard(tint: .secondary, fillOpacity: 0.10, strokeOpacity: 0.14, cornerRadius: 14)
                .buttonStyle(.plain)

                let stopDisabled = finalCheckpointMode == .custom
                    && finalCheckpointText.isEmpty == false
                    && parsedFinalCheckpoint == nil

                Button("Stop Session") {
                    _ = appData.stopChargeSession(
                        sessionID: session.id,
                        finalBatteryPercent: resolvedFinalCheckpoint
                    )
                    showingStopConfirm = false
                    finalCheckpointText = ""
                    finalCheckpointMode = .full
                }
                .frame(maxWidth: .infinity)
                .padding(.vertical, 9)
                .meterCard(tint: .red, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
                .buttonStyle(.plain)
                .disabled(stopDisabled)
            }
        }
        .padding(14)
        .meterCard(tint: .red, fillOpacity: 0.06, strokeOpacity: 0.14, cornerRadius: 16)
    }

    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.0
        case .skip:   return nil
        case .custom: return parsedFinalCheckpoint
        }
    }

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

    private func sessionChartCard(timeRange: ClosedRange<Date>, session: ChargeSessionSummary) -> 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: "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)
    }

    // MARK: - Helpers

    private func setupRow<Content: View>(
        icon: String,
        iconColor: Color = .secondary,
        @ViewBuilder content: () -> Content
    ) -> some View {
        HStack(spacing: 10) {
            Image(systemName: icon)
                .foregroundColor(iconColor)
                .font(.body.weight(.medium))
                .frame(width: 22, alignment: .center)
            content()
        }
        .padding(.horizontal, 14)
        .padding(.vertical, 11)
    }

    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 sessionMetricRows(
        for session: ChargeSessionSummary,
        displayedEnergyWh: Double
    ) -> [SessionMetricRow] {
        var rows: [SessionMetricRow] = []

        if shouldShowChargingTransport(for: session) {
            rows.append(SessionMetricRow(label: "Type", value: session.chargingTransportMode.title))
        }

        if shouldShowChargingState(for: session) {
            rows.append(SessionMetricRow(label: "Mode", value: session.chargingStateMode.title))
        }

        rows.append(SessionMetricRow(label: "Energy", value: "\(displayedEnergyWh.format(decimalDigits: 3)) Wh"))
        rows.append(SessionMetricRow(label: "Duration", value: formatDuration(max(session.effectiveDuration, 0))))
        rows.append(SessionMetricRow(label: "Auto Stop", value: autoStopLabel(for: session)))
        return rows
    }

    private func shouldShowChargingTransport(for session: ChargeSessionSummary) -> Bool {
        guard let selectedChargedDevice else { return true }
        return selectedChargedDevice.supportedChargingModes.count > 1
            || selectedChargedDevice.supportedChargingModes.contains(session.chargingTransportMode) == false
    }

    private func shouldShowChargingState(for session: ChargeSessionSummary) -> Bool {
        guard let selectedChargedDevice else { return true }
        return selectedChargedDevice.supportedChargingStateModes.count > 1
            || selectedChargedDevice.supportedChargingStateModes.contains(session.chargingStateMode) == false
    }

    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 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: 160, alignment: .leading)
            .meterCard(tint: .secondary, fillOpacity: 0.08, strokeOpacity: 0.14, cornerRadius: 12)
        }
        .buttonStyle(.plain)
    }
}