//
//  SystemBillOfMaterialsView.swift
//  Cable
//
//  Created by Stefan Lange-Hegermann on 09.10.25.
//


import SwiftUI
import SwiftData

struct SystemBillOfMaterialsView: View {
    let systemName: String
    let loads: [SavedLoad]
    let batteries: [SavedBattery]
    let chargers: [SavedCharger]
    let unitSystem: UnitSystem

    @Environment(\.dismiss) private var dismiss
    @Environment(\.openURL) private var openURL
    @State private var completedItemIDs: Set<String>
    @State private var suppressRowTapForID: String?
    @State private var isExportingPDF = false
    @State private var activeShareItem: ExportedPDFShareItem?
    @State private var exportError: ExportError?

    private enum Component {
        case load(SavedLoad)
        case battery(SavedBattery)
        case charger(SavedCharger)
    }

    private enum Category: String, CaseIterable {
        case components
        case batteries
        case cables
        case fuses
        case accessories

        var title: String {
            switch self {
            case .components:
                return String(localized: "bom.category.components.title", defaultValue: "Components & Chargers")
            case .batteries:
                return String(localized: "bom.category.batteries.title", defaultValue: "Batteries")
            case .cables:
                return String(localized: "bom.category.cables.title", defaultValue: "Cables")
            case .fuses:
                return String(localized: "bom.category.fuses.title", defaultValue: "Fuses")
            case .accessories:
                return String(localized: "bom.category.accessories.title", defaultValue: "Accessories")
            }
        }

        var subtitle: String {
            switch self {
            case .components:
                return String(localized: "bom.category.components.subtitle", defaultValue: "Primary devices, controllers, and charging gear.")
            case .batteries:
                return String(localized: "bom.category.batteries.subtitle", defaultValue: "House banks and storage.")
            case .cables:
                return String(localized: "bom.category.cables.subtitle", defaultValue: "Sized power runs for every circuit.")
            case .fuses:
                return String(localized: "bom.category.fuses.subtitle", defaultValue: "Circuit protection and holders.")
            case .accessories:
                return String(localized: "bom.category.accessories.subtitle", defaultValue: "Fuses, lugs, and supporting hardware.")
            }
        }

        static var ordered: [Category] {
            [.components, .batteries, .cables, .fuses, .accessories]
        }
    }

    private struct CategorySection: Identifiable {
        let category: Category
        let items: [Item]

        var id: String { category.rawValue }
        var title: String { category.title }
        var subtitle: String { category.subtitle }

        var snapshot: BillOfMaterialsSectionSnapshot {
            BillOfMaterialsSectionSnapshot(
                id: id,
                title: title,
                subtitle: subtitle,
                items: items.map { item in
                    BillOfMaterialsItemSnapshot(
                        id: item.id,
                        title: item.title,
                        detail: item.detail,
                        iconSystemName: item.iconSystemName,
                        isPrimaryComponent: item.isPrimaryComponent,
                        metric: item.primaryMetricText
                    )
                }
            )
        }
    }

    private struct ExportedPDFShareItem: Identifiable {
        let url: URL
        var id: URL { url }
    }

    private struct ExportError: Identifiable {
        let id = UUID()
        let message: String
    }

    private struct Item: Identifiable {
        enum Destination {
            case component(String)
            case search(String)
        }

        let id: String
        let storageKeys: [String]
        let logicalID: String
        let title: String
        let detail: String
        let iconSystemName: String
        let destination: Destination
        let isPrimaryComponent: Bool
        let components: [Component]
        let category: Category
        let quantity: Int
        let mergeKey: String?
        let quantifiedDetailContext: String?
        let quantifiedDetailSecondaryContext: String?
        let quantityScale: Double?

        var primaryMetricText: String? {
            if let scale = quantityScale {
                guard let unit = quantifiedDetailContext else { return nil }
                let value = Double(quantity) / scale
                let format = String(localized: "bom.quantity.cable.badge", defaultValue: "%1$.1f %2$@ · %3$@")
                return String(format: format, locale: Locale.current, value, unit, quantifiedDetailSecondaryContext ?? "")
            } else if quantity > 1 {
                if let fuseRating = quantifiedDetailSecondaryContext, let amps = Int(fuseRating) {
                    let format = String(localized: "bom.quantity.fuse.badge", defaultValue: "%1$d× · %2$d A")
                    return String(format: format, quantity, amps)
                }
                if let gauge = quantifiedDetailContext {
                    let format = String(localized: "bom.quantity.terminal.badge", defaultValue: "%1$d× · %2$@")
                    return String(format: format, quantity, gauge)
                }
                let format = String(localized: "bom.quantity.count.badge", defaultValue: "%d×")
                return String(format: format, quantity)
            }
            if let fuseRating = quantifiedDetailSecondaryContext, let amps = Int(fuseRating) {
                let format = String(localized: "bom.quantity.fuse.badge", defaultValue: "%1$d× · %2$d A")
                return String(format: format, 1, amps)
            }
            if let gauge = quantifiedDetailContext, !gauge.isEmpty {
                let format = String(localized: "bom.quantity.single.badge", defaultValue: "1× • %@")
                return String(format: format, gauge)
            }
            return nil
        }

        var primaryComponent: Component {
            components.first ?? components.last!
        }

        func updatingQuantity(_ quantity: Int, components newComponents: [Component], storageKeys newStorageKeys: [String]) -> Item {
            Item(
                id: id,
                storageKeys: newStorageKeys,
                logicalID: logicalID,
                title: title,
                detail: detail,
                iconSystemName: iconSystemName,
                destination: destination,
                isPrimaryComponent: isPrimaryComponent,
                components: newComponents,
                category: category,
                quantity: quantity,
                mergeKey: mergeKey,
                quantifiedDetailContext: quantifiedDetailContext,
                quantifiedDetailSecondaryContext: quantifiedDetailSecondaryContext,
                quantityScale: quantityScale
            )
        }

        func replacingID(_ newID: String) -> Item {
            Item(
                id: newID,
                storageKeys: storageKeys,
                logicalID: logicalID,
                title: title,
                detail: detail,
                iconSystemName: iconSystemName,
                destination: destination,
                isPrimaryComponent: isPrimaryComponent,
                components: components,
                category: category,
                quantity: quantity,
                mergeKey: mergeKey,
                quantifiedDetailContext: quantifiedDetailContext,
                quantifiedDetailSecondaryContext: quantifiedDetailSecondaryContext,
                quantityScale: quantityScale
            )
        }
    }

    init(
        systemName: String,
        loads: [SavedLoad],
        batteries: [SavedBattery],
        chargers: [SavedCharger],
        unitSystem: UnitSystem
    ) {
        self.systemName = systemName
        self.loads = loads
        self.batteries = batteries
        self.chargers = chargers
        self.unitSystem = unitSystem
        let loadKeys = loads.flatMap { load in
            load.bomCompletedItemIDs.map { SystemBillOfMaterialsView.storageKey(for: .load(load), itemID: $0) }
        }
        let batteryKeys = batteries.flatMap { battery in
            battery.bomCompletedItemIDs.map { SystemBillOfMaterialsView.storageKey(for: .battery(battery), itemID: $0) }
        }
        let chargerKeys = chargers.flatMap { charger in
            charger.bomCompletedItemIDs.map { SystemBillOfMaterialsView.storageKey(for: .charger(charger), itemID: $0) }
        }
        let initialKeys = loadKeys + batteryKeys + chargerKeys
        _completedItemIDs = State(initialValue: Set(initialKeys))
        _suppressRowTapForID = State(initialValue: nil)
    }

    var body: some View {
        NavigationStack {
            List {
                if categorySections.isEmpty {
                    Section("Components") {
                        Text(
                            String(
                                localized: "bom.empty.message",
                                comment: "Placeholder shown when there are no components to list in the BOM"
                            )
                        )
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    }
                }

                ForEach(categorySections) { section in
                    Section(header: sectionHeader(title: section.title, subtitle: section.subtitle)) {
                        ForEach(section.items) { item in
                            itemRow(for: item)
                        }
                    }
                }

                Section {
                    Text(footerMessage)
                        .font(.footnote)
                        .foregroundColor(.secondary)
                        .padding(.vertical, 4)
                }
            }
            .listStyle(.insetGrouped)
            .navigationTitle(
                String(
                    format: String(localized: "bom.navigation.title.system", defaultValue: "BOM – %@"),
                    locale: Locale.current,
                    systemName
                )
            )
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Close") {
                        dismiss()
                    }
                    .accessibilityIdentifier("system-bom-close-button")
                    
                }
                ToolbarItem(placement: .primaryAction) {
                    Button(action: exportBillOfMaterialsPDF) {
                        if isExportingPDF {
                            ProgressView()
                                .progressViewStyle(.circular)
                        } else {
                            Label(
                                String(localized: "bom.export.pdf.button", defaultValue: "Export PDF"),
                                systemImage: "square.and.arrow.up"
                            )
                        }
                    }
                    .disabled(categorySections.isEmpty || isExportingPDF)
                    .accessibilityIdentifier("system-bom-export-button")
                }
            }
            .onAppear {
                refreshCompletedItems()
                suppressRowTapForID = nil
            }
        }
        .accessibilityIdentifier("system-bom-view")
        .sheet(item: $activeShareItem, onDismiss: cleanupShareItem) { item in
            ShareSheet(items: [item.url])
        }
        .alert(item: $exportError) { error in
            Alert(
                title: Text(
                    String(localized: "bom.export.pdf.error.title", defaultValue: "Export Failed")
                ),
                message: Text(error.message),
                dismissButton: .default(
                    Text(
                        String(localized: "generic.ok", defaultValue: "OK")
                    )
                )
            )
        }
    }

    private func exportBillOfMaterialsPDF() {
        if categorySections.isEmpty {
            exportError = ExportError(
                message: String(localized: "bom.export.pdf.error.empty", defaultValue: "Add at least one component before exporting.")
            )
            return
        }

        AnalyticsTracker.log("BOM PDF Exported", properties: [
            "system": systemName,
            "item_count": categorySections.reduce(0) { $0 + $1.items.count },
        ])
        isExportingPDF = true
        let exporter = SystemBillOfMaterialsPDFExporter()
        let sections = sectionSnapshots
        let currentSystemName = systemName
        let currentUnitSystem = unitSystem

        Task {
            do {
                let url = try exporter.export(
                    systemName: currentSystemName,
                    unitSystem: currentUnitSystem,
                    sections: sections
                )
                await MainActor.run {
                    activeShareItem = ExportedPDFShareItem(url: url)
                }
            } catch {
                await MainActor.run {
                    exportError = ExportError(message: error.localizedDescription)
                }
            }

            await MainActor.run {
                isExportingPDF = false
            }
        }
    }

    private func cleanupShareItem() {
        if let url = activeShareItem?.url {
            try? FileManager.default.removeItem(at: url)
        }
        activeShareItem = nil
    }

    @ViewBuilder
    private func itemRow(for item: Item) -> some View {
        let isCompleted = item.storageKeys.allSatisfy { completedItemIDs.contains($0) }
        let destinationURL = destinationURL(for: item.destination, component: item.primaryComponent)

        HStack(spacing: 12) {
            let accessibilityLabel: String = {
                let format = isCompleted
                    ? String(localized: "bom.accessibility.mark.incomplete", defaultValue: "Mark %@ incomplete")
                    : String(localized: "bom.accessibility.mark.complete", defaultValue: "Mark %@ complete")
                return String.localizedStringWithFormat(format, item.title)
            }()

            Image(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
                .foregroundColor(isCompleted ? .accentColor : .secondary)
                .imageScale(.large)
                .onTapGesture {
                    setCompletion(!isCompleted, for: item)
                    suppressRowTapForID = item.id
                }
                .accessibilityLabel(accessibilityLabel)

            VStack(alignment: .leading, spacing: 4) {
                Text(item.title)
                    .font(.headline)
                    .foregroundStyle(isCompleted ? Color.primary.opacity(0.7) : Color.primary)
                    .strikethrough(isCompleted, color: .accentColor.opacity(0.6))

                if let metric = item.primaryMetricText {
                    Text(metric)
                        .font(.title3.weight(.semibold))
                        .foregroundColor(.accentColor)
                }

                if shouldShowDetail(for: item) {
                    Text(item.detail)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }

            Spacer(minLength: 8)

            if destinationURL != nil {
                Image(systemName: "arrow.up.right")
                    .font(.footnote.weight(.semibold))
                    .foregroundColor(.secondary)
            }
        }
        .padding(.vertical, 10)
        .contentShape(Rectangle())
        .listRowInsets(EdgeInsets(top: 6, leading: 20, bottom: 6, trailing: 16))
        .listRowBackground(
            Color(.secondarySystemGroupedBackground)
        )
        .onTapGesture {
            if suppressRowTapForID == item.id {
                suppressRowTapForID = nil
                return
            }
            if let destinationURL {
                trackAffiliateTap(item: item, url: destinationURL)
                openURL(destinationURL)
            }
            setCompletion(true, for: item)
            suppressRowTapForID = nil
        }
    }

    private func trackAffiliateTap(item: Item, url: URL) {
        let isComponent: Bool
        if case .component = item.destination { isComponent = true } else { isComponent = false }
        AnalyticsTracker.log("BOM Item Tapped", properties: [
            "item": item.title,
            "category": item.category.rawValue,
            "is_component": isComponent,
            "domain": url.host ?? "unknown",
            "system": systemName,
        ])
    }

    private func shouldShowDetail(for item: Item) -> Bool {
        !item.detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    private var categorySections: [CategorySection] {
        var allItems: [Item] = []
        sortedLoads.forEach { allItems.append(contentsOf: loadItems(for: $0)) }
        sortedBatteries.forEach { allItems.append(contentsOf: batteryItems(for: $0)) }
        sortedChargers.forEach { allItems.append(contentsOf: chargerItems(for: $0)) }

        let consolidated = consolidateSimilarItems(allItems)
        let grouped = Dictionary(grouping: consolidated, by: \Item.category)

        return Category.ordered.compactMap { category in
            guard let items = grouped[category], !items.isEmpty else { return nil }
            return CategorySection(category: category, items: items)
        }
    }

    private var sectionSnapshots: [BillOfMaterialsSectionSnapshot] {
        categorySections.map { $0.snapshot }
    }

    private func consolidateSimilarItems(_ items: [Item]) -> [Item] {
        var results: [Item] = []
        var indexByKey: [String: Int] = [:]

        for item in items {
            guard let key = item.mergeKey else {
                results.append(item)
                continue
            }

            if let index = indexByKey[key] {
                var existing = results[index]
                let combinedComponents = existing.components + item.components
                let combinedStorageKeys = existing.storageKeys + item.storageKeys
                let newQuantity = existing.quantity + item.quantity
                var updated = existing.updatingQuantity(newQuantity, components: combinedComponents, storageKeys: combinedStorageKeys)
                if existing.id != key {
                    updated = updated.replacingID(key)
                }
                results[index] = updated
            } else {
                indexByKey[key] = results.count
                let base = item.updatingQuantity(item.quantity, components: item.components, storageKeys: item.storageKeys)
                results.append(base.replacingID(key))
            }
        }

        return results
    }

    private var sortedLoads: [SavedLoad] {
        loads.sorted { lhs, rhs in
            lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
        }
    }

    private var sortedBatteries: [SavedBattery] {
        batteries.sorted { lhs, rhs in
            lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
        }
    }

    private var sortedChargers: [SavedCharger] {
        chargers.sorted { lhs, rhs in
            lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
        }
    }

    private func sectionHeader(title: String, subtitle: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(title)
                .font(.headline)
            Text(subtitle)
                .font(.caption)
                .foregroundColor(.secondary)
        }
    }

    private func items(for component: Component) -> [Item] {
        switch component {
        case .load(let load):
            return loadItems(for: load)
        case .battery(let battery):
            return batteryItems(for: battery)
        case .charger(let charger):
            return chargerItems(for: charger)
        }
    }

    private func loadItems(for load: SavedLoad) -> [Item] {
        let component: Component = .load(load)
        let lengthValue: Double
        if unitSystem == .imperial {
            lengthValue = load.length * 3.28084
        } else {
            lengthValue = load.length
        }
        let lengthLabel = String(format: "%.1f %@", lengthValue, unitSystem.lengthUnit)

        let crossSectionLabel: String
        let gaugeQuery: String
        let unknownSizeLabel = String(localized: "bom.size.unknown", comment: "Fallback label when the cable size is not yet determined")

        if unitSystem == .imperial {
            let awg = awgFromCrossSection(load.crossSection)
            if load.crossSection > 0 {
                crossSectionLabel = "AWG \(ElectricalCalculations.formatAWG(awg))"
                gaugeQuery = "AWG \(ElectricalCalculations.formatAWG(awg))"
            } else {
                crossSectionLabel = unknownSizeLabel
                gaugeQuery = "battery cable"
            }
        } else {
            if load.crossSection > 0 {
                crossSectionLabel = String(format: "%.1f mm²", load.crossSection)
                gaugeQuery = String(format: "%.1f mm2", load.crossSection)
            } else {
                crossSectionLabel = unknownSizeLabel
                gaugeQuery = "battery cable"
            }
        }

        let calculatedPower = load.power > 0 ? load.power : load.voltage * load.current
        let powerDetail = String(format: "%.0f W @ %.1f V", calculatedPower, load.voltage)

        let fuseRating = recommendedFuse(for: load)
        let fuseDetailFormat = String(localized: "bom.fuse.detail", defaultValue: "Inline holder and %dA fuse")
        let fuseDetail = String.localizedStringWithFormat(fuseDetailFormat, fuseRating)

        let terminalCount = 4
        let cableShoesDetailFormat = String(localized: "bom.terminals.detail", defaultValue: "Ring or spade terminals sized for %@ wiring")
        let cableShoesDetail = String.localizedStringWithFormat(
            cableShoesDetailFormat,
            crossSectionLabel.lowercased()
        )
        let deviceFallbackFormat = String(localized: "bom.search.device.fallback", defaultValue: "DC device %.0fW %.0fV")
        let deviceQuery = load.name.isEmpty
            ? String(format: deviceFallbackFormat, calculatedPower, load.voltage)
            : load.name

        let redCableSearchFormat = String(localized: "bom.search.cable.red", defaultValue: "%@ red battery cable")
        let redCableQuery = String(format: redCableSearchFormat, gaugeQuery)
        let blackCableSearchFormat = String(localized: "bom.search.cable.black", defaultValue: "%@ black battery cable")
        let blackCableQuery = String(format: blackCableSearchFormat, gaugeQuery)
        let fuseSearchFormat = String(localized: "bom.search.fuse", defaultValue: "inline fuse holder %dA")
        let fuseQuery = String(format: fuseSearchFormat, fuseRating)
        let terminalSearchFormat = String(localized: "bom.search.terminals", defaultValue: "%@ cable shoes")
        let terminalQuery = String(format: terminalSearchFormat, gaugeQuery)

        let componentStorageKey = Self.storageKey(for: component, itemID: "component")
        let redCableStorageKey = Self.storageKey(for: component, itemID: "cable-red")
        let blackCableStorageKey = Self.storageKey(for: component, itemID: "cable-black")
        let fuseStorageKey = Self.storageKey(for: component, itemID: "fuse")
        let terminalsStorageKey = Self.storageKey(for: component, itemID: "terminals")

        let lengthQuantity = Int((max(lengthValue, 0) * 100).rounded())
        let redCableMergeKey = "cable.red::\(crossSectionLabel)::\(unitSystem.lengthUnit)"
        let blackCableMergeKey = "cable.black::\(crossSectionLabel)::\(unitSystem.lengthUnit)"
        let fuseMergeKey = "fuse::\(fuseRating)"

        let items: [Item] = [
            Item(
                id: componentStorageKey,
                storageKeys: [componentStorageKey],
                logicalID: "component",
                title: load.name.isEmpty ? String(localized: "component.fallback.name", comment: "Fallback name for a component when no name is provided") : load.name,
                detail: powerDetail,
                iconSystemName: "bolt.fill",
                destination: load.componentID.map { .component($0) } ?? .search(deviceQuery),
                isPrimaryComponent: true,
                components: [component],
                category: .components,
                quantity: 1,
                mergeKey: nil,
                quantifiedDetailContext: nil,
                quantifiedDetailSecondaryContext: nil,
                quantityScale: nil
            ),
            Item(
                id: redCableStorageKey,
                storageKeys: [redCableStorageKey],
                logicalID: "cable-red",
                title: String(localized: "bom.item.cable.red", comment: "Title for the red power cable item"),
                detail: "",
                iconSystemName: "bolt.horizontal.circle",
                destination: .search(redCableQuery),
                isPrimaryComponent: false,
                components: [component],
                category: .cables,
                quantity: lengthQuantity,
                mergeKey: redCableMergeKey,
                quantifiedDetailContext: unitSystem.lengthUnit,
                quantifiedDetailSecondaryContext: crossSectionLabel,
                quantityScale: 100.0
            ),
            Item(
                id: blackCableStorageKey,
                storageKeys: [blackCableStorageKey],
                logicalID: "cable-black",
                title: String(localized: "bom.item.cable.black", comment: "Title for the black power cable item"),
                detail: "",
                iconSystemName: "bolt.horizontal.circle",
                destination: .search(blackCableQuery),
                isPrimaryComponent: false,
                components: [component],
                category: .cables,
                quantity: lengthQuantity,
                mergeKey: blackCableMergeKey,
                quantifiedDetailContext: unitSystem.lengthUnit,
                quantifiedDetailSecondaryContext: crossSectionLabel,
                quantityScale: 100.0
            ),
            Item(
                id: fuseStorageKey,
                storageKeys: [fuseStorageKey],
                logicalID: "fuse",
                title: String(localized: "bom.item.fuse", comment: "Title for the fuse and holder item"),
                detail: fuseDetail,
                iconSystemName: "bolt.shield",
                destination: .search(fuseQuery),
                isPrimaryComponent: false,
                components: [component],
                category: .fuses,
                quantity: 1,
                mergeKey: fuseMergeKey,
                quantifiedDetailContext: nil,
                quantifiedDetailSecondaryContext: String(fuseRating),
                quantityScale: nil
            ),
            Item(
                id: terminalsStorageKey,
                storageKeys: [terminalsStorageKey],
                logicalID: "terminals",
                title: String(localized: "bom.item.terminals", comment: "Title for the cable terminals item"),
                detail: cableShoesDetail,
                iconSystemName: "wrench.and.screwdriver",
                destination: .search(terminalQuery),
                isPrimaryComponent: false,
                components: [component],
                category: .accessories,
                quantity: terminalCount,
                mergeKey: "terminals::\(crossSectionLabel.lowercased())",
                quantifiedDetailContext: crossSectionLabel,
                quantifiedDetailSecondaryContext: nil,
                quantityScale: nil
            )
        ]

        return items
    }

    private func batteryItems(for battery: SavedBattery) -> [Item] {
        let component: Component = .battery(battery)
        let usableCapacityLabel = String(localized: "battery.bank.metric.usable_capacity", defaultValue: "Usable Capacity")
        let capacityLabel = String(format: "%.0f Ah @ %.1f V", battery.capacityAmpHours, battery.nominalVoltage)
        let usableLabel = String(format: "%@: %.0f Ah", usableCapacityLabel, battery.usableCapacityAmpHours)
        let detail = [capacityLabel, battery.chemistry.displayName, usableLabel].joined(separator: " • ")
        let capacityQuery = max(1, Int(round(battery.capacityAmpHours)))
        let voltageQuery = max(1, Int(round(battery.nominalVoltage)))
        let batterySearchFormat = String(localized: "bom.search.battery", defaultValue: "%dAh %dV %@ battery")
        let query = String(format: batterySearchFormat, capacityQuery, voltageQuery, battery.chemistry.displayName)
        let componentID = battery.componentID
        let storageKey = Self.storageKey(for: component, itemID: "battery")

        return [
            Item(
                id: storageKey,
                storageKeys: [storageKey],
                logicalID: "battery",
                title: battery.name.isEmpty ? String(localized: "component.fallback.name", comment: "Fallback name for a component when no name is provided") : battery.name,
                detail: detail,
                iconSystemName: battery.iconName,
                destination: componentID.map { .component($0) } ?? .search(query),
                isPrimaryComponent: true,
                components: [component],
                category: .batteries,
                quantity: 1,
                mergeKey: nil,
                quantifiedDetailContext: nil,
                quantifiedDetailSecondaryContext: nil,
                quantityScale: nil
            )
        ]
    }

    private func chargerItems(for charger: SavedCharger) -> [Item] {
        let component: Component = .charger(charger)
        let effectivePower = charger.effectivePowerWatts
        let powerString = String(format: "%.0f W", effectivePower)
        let outputString = String(format: "%.0f A @ %.1f V", charger.maxCurrentAmps, charger.outputVoltage)
        let inputString = String(format: "%.0f V AC", charger.inputVoltage)
        let detail = [powerString, outputString, inputString].joined(separator: " • ")
        let voltageQuery = max(1, Int(round(charger.outputVoltage)))
        let currentQuery = max(1, Int(round(charger.maxCurrentAmps)))
        let query = "\(voltageQuery)V \(currentQuery)A battery charger"
        let componentID = charger.componentID
        let storageKey = Self.storageKey(for: component, itemID: "charger")

        return [
            Item(
                id: storageKey,
                storageKeys: [storageKey],
                logicalID: "charger",
                title: charger.name.isEmpty ? String(localized: "component.fallback.name", comment: "Fallback name for a component when no name is provided") : charger.name,
                detail: detail,
                iconSystemName: charger.iconName,
                destination: componentID.map { .component($0) } ?? .search(query),
                isPrimaryComponent: true,
                components: [component],
                category: .components,
                quantity: 1,
                mergeKey: nil,
                quantifiedDetailContext: nil,
                quantifiedDetailSecondaryContext: nil,
                quantityScale: nil
            )
        ]
    }

    private func destinationURL(for destination: Item.Destination, component: Component) -> URL? {
        switch destination {
        case .component(let id):
            return VoltPlanRedirect.componentURL(id: id)
        case .search(let query):
            return VoltPlanRedirect.searchURL(query: query)
        }
    }

    private static func storageKey(for component: Component, itemID: String) -> String {
        switch component {
        case .load(let load):
            if load.identifier.isEmpty {
                load.identifier = UUID().uuidString
            }
            return "\(load.identifier)::\(itemID)"
        case .battery(let battery):
            return "\(battery.id.uuidString)::\(itemID)"
        case .charger(let charger):
            if charger.identifier.isEmpty {
                charger.identifier = UUID().uuidString
            }
            return "\(charger.identifier)::\(itemID)"
        }
    }

    private func setCompletion(_ isCompleted: Bool, for item: Item) {
        if isCompleted {
            item.storageKeys.forEach { completedItemIDs.insert($0) }
        } else {
            item.storageKeys.forEach { completedItemIDs.remove($0) }
        }

        for component in item.components {
            switch component {
            case .load(let load):
                if load.identifier.isEmpty {
                    load.identifier = UUID().uuidString
                }
                updateCompletionList(isCompleted, logicalID: item.logicalID, list: &load.bomCompletedItemIDs)
            case .battery(let battery):
                updateCompletionList(isCompleted, logicalID: item.logicalID, list: &battery.bomCompletedItemIDs)
            case .charger(let charger):
                if charger.identifier.isEmpty {
                    charger.identifier = UUID().uuidString
                }
                updateCompletionList(isCompleted, logicalID: item.logicalID, list: &charger.bomCompletedItemIDs)
            }
        }
    }

    private func updateCompletionList(_ isCompleted: Bool, logicalID: String, list: inout [String]) {
        var stored = Set(list)
        if isCompleted {
            stored.insert(logicalID)
        } else {
            stored.remove(logicalID)
        }
        list = Array(stored).sorted()
    }

    private func refreshCompletedItems() {
        let loadKeys = loads.flatMap { load in
            load.bomCompletedItemIDs.map { Self.storageKey(for: .load(load), itemID: $0) }
        }
        let batteryKeys = batteries.flatMap { battery in
            battery.bomCompletedItemIDs.map { Self.storageKey(for: .battery(battery), itemID: $0) }
        }
        let chargerKeys = chargers.flatMap { charger in
            charger.bomCompletedItemIDs.map { Self.storageKey(for: .charger(charger), itemID: $0) }
        }
        completedItemIDs = Set(loadKeys + batteryKeys + chargerKeys)
    }

    private func recommendedFuse(for load: SavedLoad) -> String {
        let fuse = ElectricalCalculations.recommendedFuse(forCurrent: load.current)
        return fuse == fuse.rounded() ? String(format: "%.0f", fuse) : String(format: "%.1f", fuse)
    }

    private func awgFromCrossSection(_ crossSectionMM2: Double) -> Double {
        let mapping: [(awg: Double, area: Double)] = [
            (20, 0.519), (18, 0.823), (16, 1.31), (14, 2.08), (12, 3.31), (10, 5.26),
            (8, 8.37), (6, 13.3), (4, 21.2), (2, 33.6), (1, 42.4), (-1, 53.5),
            (-2, 67.4), (-3, 85.0), (-4, 107.0)
        ]

        guard crossSectionMM2 > 0 else { return 0 }

        let closest = mapping.min { lhs, rhs in
            abs(lhs.area - crossSectionMM2) < abs(rhs.area - crossSectionMM2)
        }

        return closest?.awg ?? 0
    }

    private var footerMessage: String {
        String(localized: "affiliate.disclaimer", defaultValue: "Purchases through affiliate links may support VoltPlan.")
    }

    private var dateFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        return formatter
    }
}

#Preview("System BOM – Metric") {
    // Sample SavedLoad model stubs for preview
    let load1 = SavedLoad(
        name: "Fridge",
        voltage: 12.0,
        current: 6.0,
        power: 72.0,
        length: 6.5,
        crossSection: 0.0,
        iconName: "",
        colorName: "",
        isWattMode: true,
        dutyCyclePercent: 19,
        dailyUsageHours: 0,
        system: nil,
        remoteIconURLString: nil,
        componentID: nil,
        bomCompletedItemIDs: [],
        identifier: UUID().uuidString
    )

    let battery = SavedBattery(
        name: "House Bank",
        nominalVoltage: 12.8,
        capacityAmpHours: 200,
        chemistry: .lithiumIronPhosphate,
        system: nil
    )

    let charger = SavedCharger(
        name: "Shore Charger",
        inputVoltage: 230,
        outputVoltage: 14.6,
        maxCurrentAmps: 40,
        maxPowerWatts: 600,
        system: nil
    )

    SystemBillOfMaterialsView(
        systemName: "Camper Van",
        loads: [load1],
        batteries: [battery],
        chargers: [charger],
        unitSystem: .metric
    )
}

#Preview("System BOM – Imperial") {
    let load = SavedLoad(
        name: "Inverter",
        voltage: 12.0,
        current: 40.0,
        power: 480.0,
        length: 12.0,
        crossSection: 21.2,
        iconName: "",
        colorName: "",
        isWattMode: false,
        dutyCyclePercent: 100,
        dailyUsageHours: 0,
        system: nil,
        remoteIconURLString: nil,
        componentID: nil,
        bomCompletedItemIDs: ["component", "cable-red"],
        identifier: UUID().uuidString
    )

    SystemBillOfMaterialsView(
        systemName: "Boat",
        loads: [load],
        batteries: [],
        chargers: [],
        unitSystem: .imperial
    )
}
