aggregate method

Future<List<BudgetItem>> aggregate(
  1. ElectricalNode root
)

Implementation

Future<List<BudgetItem>> aggregate(ElectricalNode root) async {
  final Map<String, double> cableQuantities = {};
  final Map<String, int> protectionQuantities = {};
  final Map<String, String> protectionNames = {};
  final Map<String, double> protectionRatings = {};
  int enclosureCount = 0;
  int enclosureModules = 0;

  // Helper to traverse tree
  void traverse(ElectricalNode node) {
    node.map(
      source: (_) {},
      panel: (n) {
        enclosureCount++;
        for (var child in n.children) {
          child.maybeMap(
            protection: (p) => enclosureModules += p.poles,
            orElse: () {},
          );
        }
        _processCable(n.inputCable, true, cableQuantities);
      },
      protection: (n) {
        String key;
        String name;

        if (n.catalogData != null) {
          key = n.catalogData!.componentId;
          name = "Ref: $key";
        } else {
          // Generic
          String typeShort = _getShortType(n.protectionType);
          String curve = n.curve;
          int amps = n.ratingAmps.toInt();
          key = 'GEN-$typeShort-$amps$curve';
          name = _buildGenericName(n);
        }

        protectionQuantities.update(key, (val) => val + 1, ifAbsent: () => 1);
        protectionRatings[key] = n.ratingAmps;

        if (!protectionNames.containsKey(key)) {
          protectionNames[key] = name;
        }
      },
      load: (n) {
        _processCable(n.inputCable, n.isThreePhase, cableQuantities);
      },
    );

    // Recursive
    node.maybeMap(
      source: (n) => n.children.forEach(traverse),
      panel: (n) => n.children.forEach(traverse),
      protection: (n) => n.children.forEach(traverse),
      orElse: () {},
    );
  }

  traverse(root);

  List<BudgetItem> items = [];

  // 0. Pre-fetch library cables for smart matching
  List<ComponentTemplate> libraryCables = [];
  if (componentRepository != null) {
    final result = await componentRepository!.getByType(ComponentType.cable);
    result.fold((l) => null, (r) => libraryCables = r);
  }

  // 1. Enclosures (First)
  if (enclosureCount > 0) {
    String sizeDesc =
        "Est. ${enclosureModules + (enclosureModules * 0.2).toInt()} módulos";

    items.add(BudgetItem(
      id: 'ENCLOSURE-STD',
      name: 'Cuadro Eléctrico / Envolvente',
      description: '$enclosureCount unidades ($sizeDesc)',
      quantity: enclosureCount.toDouble(),
      unitPrice: 150.0,
      category: BudgetCategory.enclosure,
    ));
  }

  // 2. Protections (Second, Sorted by Amperage)
  var protKeys = protectionQuantities.keys.toList();
  protKeys.sort((a, b) {
    double ratingA = protectionRatings[a] ?? 0;
    double ratingB = protectionRatings[b] ?? 0;
    return ratingA.compareTo(ratingB);
  });

  // Fetch all protection details in parallel for better performance
  final catalogKeys = protKeys.where((k) => !k.startsWith('GEN-')).toList();
  final repository = componentRepository; // Capture repository reference for async callbacks

  final protectionDetailsFutures = catalogKeys.map((key) async {
    if (repository == null) return null;
    try {
      final result = await repository.getById(key);
      return result.fold(
        (l) => null,
        (comp) => MapEntry(key, comp),
      );
    } catch (_) {
      return null;
    }
  });

  final protectionDetailsResults = await Future.wait(protectionDetailsFutures);
  final protectionDetailsMap = Map.fromEntries(
    protectionDetailsResults.whereType<MapEntry<String, ComponentTemplate>>(),
  );

  for (var key in protKeys) {
    final count = protectionQuantities[key]!;
    var name = protectionNames[key] ?? key;
    double price = 0.0;

    // Lookup real name if it's a catalog ID
    if (!key.startsWith('GEN-') && protectionDetailsMap.containsKey(key)) {
      final comp = protectionDetailsMap[key];
      if (comp != null) {
        final manu = comp.manufacturer ?? "";
        final model = comp.name;
        name = "$manu $model".trim();

        // Capture price from library
        comp.maybeMap(
          protection: (p) => price = p.price ?? 0.0,
          orElse: () {},
        );
      }
    }

    items.add(BudgetItem(
      id: key,
      name: name,
      description: '$count unidades',
      quantity: count.toDouble(),
      unitPrice: price,
      category: BudgetCategory.protection,
    ));
  }

  // 3. Cables (Third, Sorted by Section)
  var cableKeys = cableQuantities.keys.toList();
  cableKeys.sort((a, b) {
    try {
      double secA = double.parse(a.split('-')[2]);
      double secB = double.parse(b.split('-')[2]);
      return secA.compareTo(secB);
    } catch (e) {
      return 0;
    }
  });

  for (var key in cableKeys) {
    final length = cableQuantities[key]!;

    final parts = key.split('-');

    String materialCode = parts[1];
    String sectionStr = parts[2];
    String polesNum = parts[3];

    String wiresStr = "${polesNum}G";
    // 3 -> 3G (F+N+T), 5 -> 5G (3F+N+T)

    String matName = materialCode == 'CU' ? 'Manguera' : 'Manguera Al';
    String name = "$matName $wiresStr$sectionStr mm²";
    double price = 0.0;

    try {
      final targetMat = materialCode == 'CU'
          ? CableMaterial.copper
          : CableMaterial.aluminum;
      final targetSec = double.tryParse(sectionStr) ?? 0.0;

      final match = libraryCables.firstWhere(
        (c) => c.maybeMap(
          cable: (cab) =>
              cab.material == targetMat && cab.section == targetSec,
          orElse: () => false,
        ),
        orElse: () => const ComponentTemplate.cable(
            id: '',
            name: '',
            section: 0,
            material: CableMaterial.copper,
            insulationType: '',
            maxOperatingTemp: 0),
      );

      if (match.id.isNotEmpty) {
        match.mapOrNull(cable: (c) {
          name = "${c.name} ($wiresStr)";
          price = c.price ?? 0.0;
        });
      }
    } catch (e) {
      // Ignore errors during sorting or lookup
    }

    name = _cleanName(name);

    items.add(BudgetItem(
      id: key,
      name: name,
      description: 'Total: ${length.toStringAsFixed(1)} m',
      quantity: length,
      unitPrice: price,
      category: BudgetCategory.cable,
    ));
  }

  return items;
}