# BundleService.java

    package com.university.bookstore.composite;

    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;

    import com.university.bookstore.model.Material;

    /**
     * Service for managing material bundles using the Composite pattern.
     * Provides operations for creating, managing, and analyzing material bundles.
     *
     * <p>This service demonstrates the Composite pattern in action by providing
     * a high-level interface for working with both individual materials and bundles.</p>
     *
     * @author Navid Mohaghegh
     * @version 3.0
     * @since 2024-09-15
     */
    public class BundleService {

        private final Map<String, MaterialBundle> bundles;

        /**
         * Creates a new bundle service.
         */
        public BundleService() {
            this.bundles = new HashMap<>();
        }

        /**
         * Creates a new material bundle with the specified name and discount.
         *
         * @param name the bundle name
         * @param discount the discount rate (0.0 to 1.0)
         * @return the created bundle
         * @throws IllegalArgumentException if name is null or empty
         */
        public MaterialBundle createBundle(String name, double discount) {
            if (name == null || name.trim().isEmpty()) {
                throw new IllegalArgumentException("Bundle name cannot be null or empty");
            }

            String bundleName = name.trim();
            if (bundles.containsKey(bundleName)) {
                throw new IllegalArgumentException("Bundle already exists: " + bundleName);
            }

            MaterialBundle bundle = new MaterialBundle(bundleName, discount);
            bundles.put(bundleName, bundle);
            return bundle;
        }

        /**
         * Adds a material to the specified bundle.
         *
         * @param bundleName the name of the bundle
         * @param material the material to add
         * @throws IllegalArgumentException if bundle not found or material is null
         */
        public void addToBundle(String bundleName, Material material) {
            if (material == null) {
                throw new IllegalArgumentException("Material cannot be null");
            }

            MaterialBundle bundle = bundles.get(bundleName);
            if (bundle == null) {
                throw new IllegalArgumentException("Bundle not found: " + bundleName);
            }

            bundle.addComponent(new MaterialLeaf(material));
        }

        /**
         * Adds a bundle to another bundle (nested bundles).
         *
         * @param parentBundleName the name of the parent bundle
         * @param childBundleName the name of the child bundle to add
         * @throws IllegalArgumentException if either bundle is not found
         */
        public void addBundleToBundle(String parentBundleName, String childBundleName) {
            MaterialBundle parent = bundles.get(parentBundleName);
            MaterialBundle child = bundles.get(childBundleName);

            if (parent == null) {
                throw new IllegalArgumentException("Parent bundle not found: " + parentBundleName);
            }
            if (child == null) {
                throw new IllegalArgumentException("Child bundle not found: " + childBundleName);
            }

            parent.addComponent(child);
        }

        /**
         * Adds a component to the specified bundle.
         *
         * @param bundleName the name of the bundle
         * @param component the component to add
         * @throws IllegalArgumentException if bundle not found or component is null
         */
        public void addComponentToBundle(String bundleName, MaterialComponent component) {
            if (component == null) {
                throw new IllegalArgumentException("Component cannot be null");
            }

            MaterialBundle bundle = bundles.get(bundleName);
            if (bundle == null) {
                throw new IllegalArgumentException("Bundle not found: " + bundleName);
            }

            bundle.addComponent(component);
        }

        /**
         * Removes a material from the specified bundle.
         *
         * @param bundleName the name of the bundle
         * @param material the material to remove
         * @return true if the material was removed, false if not found
         */
        public boolean removeFromBundle(String bundleName, Material material) {
            MaterialBundle bundle = bundles.get(bundleName);
            if (bundle == null) {
                return false;
            }

            return bundle.removeComponent(new MaterialLeaf(material));
        }

        /**
         * Gets a bundle by name.
         *
         * @param name the bundle name
         * @return Optional containing the bundle if found
         */
        public Optional<MaterialBundle> getBundle(String name) {
            return Optional.ofNullable(bundles.get(name));
        }

        /**
         * Gets all bundles.
         *
         * @return list of all bundles
         */
        public List<MaterialBundle> getAllBundles() {
            return new ArrayList<>(bundles.values());
        }

        /**
         * Gets all bundle names.
         *
         * @return list of bundle names
         */
        public List<String> getBundleNames() {
            return new ArrayList<>(bundles.keySet());
        }

        /**
         * Calculates the total savings for a bundle.
         *
         * @param bundleName the name of the bundle
         * @return the total savings amount
         * @throws IllegalArgumentException if bundle not found
         */
        public double calculateBundleSavings(String bundleName) {
            MaterialBundle bundle = bundles.get(bundleName);
            if (bundle == null) {
                throw new IllegalArgumentException("Bundle not found: " + bundleName);
            }

            return bundle.getTotalSavings();
        }

        /**
         * Calculates the total savings across all bundles.
         *
         * @return the total savings amount
         */
        public double calculateTotalSavings() {
            return bundles.values().stream()
                .mapToDouble(MaterialBundle::getTotalSavings)
                .sum();
        }

        /**
         * Gets the total value of all bundles.
         *
         * @return the total value
         */
        public double getTotalBundleValue() {
            return bundles.values().stream()
                .mapToDouble(MaterialBundle::getPrice)
                .sum();
        }

        /**
         * Gets the total discounted value of all bundles.
         *
         * @return the total discounted value
         */
        public double getTotalDiscountedBundleValue() {
            return bundles.values().stream()
                .mapToDouble(MaterialBundle::getDiscountedPrice)
                .sum();
        }

        /**
         * Gets bundles that contain materials of the specified type.
         *
         * @param type the material type to filter by
         * @return list of bundles containing the specified type
         */
        public List<MaterialBundle> getBundlesByMaterialType(Material.MaterialType type) {
            return bundles.values().stream()
                .filter(bundle -> bundle.containsType(type))
                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
        }

        /**
         * Gets bundles with discounts greater than or equal to the specified threshold.
         *
         * @param minDiscount the minimum discount rate
         * @return list of bundles meeting the criteria
         */
        public List<MaterialBundle> getBundlesByDiscount(double minDiscount) {
            return bundles.values().stream()
                .filter(bundle -> bundle.getDiscountRate() >= minDiscount)
                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
        }

        /**
         * Gets bundles with values within the specified range.
         *
         * @param minValue the minimum value
         * @param maxValue the maximum value
         * @return list of bundles within the value range
         */
        public List<MaterialBundle> getBundlesByValueRange(double minValue, double maxValue) {
            return bundles.values().stream()
                .filter(bundle -> {
                    double value = bundle.getPrice();
                    return value >= minValue && value <= maxValue;
                })
                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
        }

        /**
         * Removes a bundle by name.
         *
         * @param name the bundle name
         * @return true if the bundle was removed, false if not found
         */
        public boolean removeBundle(String name) {
            return bundles.remove(name) != null;
        }

        /**
         * Clears all bundles.
         */
        public void clearAllBundles() {
            bundles.clear();
        }

        /**
         * Gets the number of bundles.
         *
         * @return the bundle count
         */
        public int getBundleCount() {
            return bundles.size();
        }

        /**
         * Checks if any bundles exist.
         *
         * @return true if no bundles exist
         */
        public boolean isEmpty() {
            return bundles.isEmpty();
        }

        /**
         * Gets bundle statistics.
         *
         * @return bundle statistics
         */
        public BundleStats getBundleStats() {
            int totalBundles = bundles.size();
            int totalItems = bundles.values().stream()
                .mapToInt(MaterialBundle::getItemCount)
                .sum();
            double totalValue = getTotalBundleValue();
            double totalDiscountedValue = getTotalDiscountedBundleValue();
            double totalSavings = calculateTotalSavings();
            double averageDiscount = bundles.values().stream()
                .mapToDouble(MaterialBundle::getDiscountRate)
                .average()
                .orElse(0.0);

            return new BundleStats(
                totalBundles,
                totalItems,
                totalValue,
                totalDiscountedValue,
                totalSavings,
                averageDiscount
            );
        }

        /**
         * Statistics class for bundle analysis.
         */
        public static class BundleStats {
            private final int totalBundles;
            private final int totalItems;
            private final double totalValue;
            private final double totalDiscountedValue;
            private final double totalSavings;
            private final double averageDiscount;

            public BundleStats(int totalBundles, int totalItems, double totalValue,
                              double totalDiscountedValue, double totalSavings, double averageDiscount) {
                this.totalBundles = totalBundles;
                this.totalItems = totalItems;
                this.totalValue = totalValue;
                this.totalDiscountedValue = totalDiscountedValue;
                this.totalSavings = totalSavings;
                this.averageDiscount = averageDiscount;
            }

            public int getTotalBundles() { return totalBundles; }
            public int getTotalItems() { return totalItems; }
            public double getTotalValue() { return totalValue; }
            public double getTotalDiscountedValue() { return totalDiscountedValue; }
            public double getTotalSavings() { return totalSavings; }
            public double getAverageDiscount() { return averageDiscount; }

            @Override
            public String toString() {
                return String.format("BundleStats[Bundles=%d, Items=%d, Value=$%.2f, Discounted=$%.2f, Savings=$%.2f, AvgDiscount=%.1f%%]",
                    totalBundles, totalItems, totalValue, totalDiscountedValue, totalSavings, averageDiscount * 100);
            }
        }

        @Override
        public String toString() {
            return String.format("BundleService[Bundles=%d, TotalValue=$%.2f, TotalSavings=$%.2f]",
                getBundleCount(), getTotalBundleValue(), calculateTotalSavings());
        }
    }