// opportunity_layer.js

class OpportunityLayer {
    constructor(map) {
        this.map = map;
        
        // Mapbox Source and Layer IDs
        this.sourceId = 'opportunity-source';
        this.layerId = 'opportunity-layer';
        this.lineLayerId = 'opportunity-line-layer';
        this.pointsSourceId = 'opp-clicked-points';
        this.pointsLayerId = 'opp-clicked-points-layer';
        this.heatmapLayerId = 'opp-clicked-heatmap-layer'; 
        
        this.isVisible = false;
        this.dataCache = {};
        this.geoJsonData = null;
        
        // Array to store points for multi-selection (Ctrl + Click)
        this.accumulatedPoints = [];
        this.selectedHexes = new Map();
        
        // Configuration for Points of Interest (POIs) and chain stores
        // Note: Negative weights represent repulsion (decreases area score)
        this.layerConfigs = [
            { id: 'supermarket',   name: 'سوپرمارکت',        type: 'csv',   url: 'supermarket_81080.csv',                 defaultWeight: 5,  color: '#1f77b4', defaultMinCount: 3, defaultMaxCount: 500, defaultActive: true },            
            { id: 'pharmacy',      name: 'داروخانه',         type: 'csv',   url: 'pharmacy_13201.csv',                    defaultWeight: 1.8,  color: '#d62728', defaultMinCount: 0, defaultMaxCount: 500, defaultActive: true },
            { id: 'fruits',        name: 'میوه و تره‌بار',   type: 'csv',   url: 'fruits-and-vegetables-sales_15366.csv', defaultWeight: 1.2,  color: '#2ca02c', defaultMinCount: 0, defaultMaxCount: 500, defaultActive: true },
            { id: 'bakery',        name: 'نانوایی',          type: 'csv',   url: 'iranian-bakery_26179.csv',              defaultWeight: 1.0,  color: '#ff7f0e', defaultMinCount: 1, defaultMaxCount: 500, defaultActive: true },
            { id: 'confectionery', name: 'قنادی',            type: 'csv',   url: 'confectionery_9614.csv',                defaultWeight: 0.5,  color: '#e377c2', defaultMinCount: 0, defaultMaxCount: 500, defaultActive: true },
            { id: 'coffee',        name: 'کافی‌شاپ',         type: 'csv',   url: 'coffee-shop_25422.csv',                 defaultWeight: 0.3,  color: '#8c564b', defaultMinCount: 0, defaultMaxCount: 500, defaultActive: true },
            { id: 'estate',        name: 'مشاور املاک',      type: 'csv',   url: 'estate-agency_54231.csv',               defaultWeight: 0.2,  color: '#7f7f7f', defaultMinCount: 0, defaultMaxCount: 500, defaultActive: false },
            
            { id: 'okcs',          name: 'افق کوروش',        type: 'chain', brandKey: 'okcs',                             defaultWeight: -2.0, color: '#C62828', defaultMinCount: 0, defaultMaxCount: 1,   defaultActive: true },
            { id: 'canbo',         name: 'جانبو',            type: 'chain', brandKey: 'canbo',                            defaultWeight: -2.0, color: '#000000', defaultMinCount: 0, defaultMaxCount: 1,   defaultActive: true },
            { id: 'seven',         name: 'هفت',              type: 'chain', brandKey: 'seven',                            defaultWeight: -2.0, color: '#0D47A1', defaultMinCount: 0, defaultMaxCount: 1,   defaultActive: true },
            { id: 'minoo',         name: 'مینو مارت',        type: 'chain', brandKey: 'minoo',                            defaultWeight: -1.8, color: '#FF9800', defaultMinCount: 0, defaultMaxCount: 1,   defaultActive: true },
            { id: 'amiran',        name: 'امیران',           type: 'chain', brandKey: 'amiran',                           defaultWeight: -1.8, color: '#B7D82B', defaultMinCount: 0, defaultMaxCount: 1,   defaultActive: true },
            { id: 'beheshti',      name: 'بهشتی',            type: 'chain', brandKey: 'beheshti',                         defaultWeight: -1.8, color: '#FF5900', defaultMinCount: 0, defaultMaxCount: 1,   defaultActive: true },
            { id: 'gandom',        name: 'گندم',             type: 'chain', brandKey: 'gandom',                           defaultWeight: -1.8, color: '#70247B', defaultMinCount: 0, defaultMaxCount: 1,   defaultActive: true },
            
            { id: 'dailymarket',   name: 'دیلی‌مارکت',       type: 'chain', brandKey: 'dailymarket',                      defaultWeight: -5.0, color: '#2E7D32', defaultMinCount: 0, defaultMaxCount: 200, defaultActive: true }
        ];

        // Exclusion rules to filter out specific hexagonal cells (e.g., preventing cannibalization)
        this.exclusionRulesConfig = [
            {
                id: 'exclude_dailymarket',
                type: 'layerExists',
                layerId: 'dailymarket',
                label: 'حذف سلول‌هایی که دیلی‌مارکت دارن',
                defaultActive: true
            },
            {
                id: 'exclude_high_competition',
                type: 'highCompetition',
                layers: ['dailymarket', 'okcs', 'canbo', 'seven', 'minoo', 'amiran', 'beheshti', 'gandom'],
                threshold: 3,
                label: 'حذف سلول‌های با رقابت بالا (>3 زنجیره‌ای)',
                defaultActive: false
            }
        ];

        // DOM elements references
        this.elements = {
            btnRun: document.getElementById('btn-run-opportunity'),
            gridSizeSelect: document.getElementById('opp-grid-size'),
            filterBox: document.getElementById('opp-filter-box'),
            container: document.getElementById('opportunity-controls') 
        };

        this.init();
    }

    init() {
        this.buildUI();

        if (this.elements.btnRun) {
            this.elements.btnRun.addEventListener('click', () => this.toggleAnalysis());
        }
        
        // Handle filter chip selections
        document.querySelectorAll('.filter-chip').forEach(btn => {
            if (btn.closest('#opp-filter-box')) {
                btn.addEventListener('click', (e) => {
                    document.querySelectorAll('#opp-filter-box .filter-chip')
                        .forEach(b => b.classList.remove('active'));
                    e.target.classList.add('active');
                    this.applyFilter(e.target.getAttribute('data-filter'));
                });
            }
        });
    }

    // Dynamically builds the UI panel for layer configurations and exclusion rules
    buildUI() {
        if (!this.elements.container || document.getElementById('multi-layer-settings-panel')) return;

        const uiWrapper = document.createElement('div');
        uiWrapper.id = 'multi-layer-settings-panel';
        uiWrapper.style.cssText = "font-family: 'YekanBakh', sans-serif; margin-bottom:15px; background:#f8f9fa; padding:12px; border-radius:8px; border:1px solid #ced4da; box-shadow:0 2px 4px rgba(0,0,0,0.05);";

        const title = document.createElement('h6');
        title.innerHTML = '⚙️ تنظیمات لایه‌ها، ضرایب و آستانه‌ها';
        title.style.cssText = 'font-size:13px; font-weight:bold; margin-top:0; margin-bottom:5px; color:#333;';
        uiWrapper.appendChild(title);
        
        const helpText = document.createElement('div');
        helpText.innerHTML = `
            <small style="color:#666;">
                * ضریب منفی = دافعه (کاهش امتیاز منطقه)<br>
                * حداقل/حداکثر = تعداد مد نظر در هر سلول شبکه<br>
            </small>`;
        helpText.style.cssText = 'margin-bottom:10px; border-bottom:1px solid #ddd; padding-bottom:5px;';
        uiWrapper.appendChild(helpText);

        const scrollArea = document.createElement('div');
        scrollArea.style.cssText = 'max-height:260px; overflow-y:auto; padding-left:5px;';

        // Generate controls for each layer configuration
        this.layerConfigs.forEach(config => {
            const row = document.createElement('div');
            row.style.cssText = `display:flex; flex-direction:column; gap:4px; margin-bottom:8px; font-size:12px; padding:6px; background:#fff; border:1px solid #eee; border-radius:4px; border-right:4px solid ${config.color};`;

            // Top row: Checkbox and Weight Input
            const topRow = document.createElement('div');
            topRow.style.cssText = 'display:flex; justify-content:space-between; align-items:center; gap:8px;';

            const label = document.createElement('label');
            label.style.cssText = 'display:flex; align-items:center; gap:5px; cursor:pointer; flex:1; margin:0;';
            
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.id = `chk_${config.id}`;
            checkbox.dataset.id = config.id;
            if (config.defaultActive) checkbox.checked = true;

            const nameSpan = document.createElement('span');
            nameSpan.textContent = config.name;

            label.appendChild(checkbox);
            label.appendChild(nameSpan);

            const weightDiv = document.createElement('div');
            weightDiv.style.cssText = 'display:flex; align-items:center; gap:5px;';

            const weightLabel = document.createElement('label');
            weightLabel.innerText = 'ضریب:';
            weightLabel.style.cssText = 'margin:0; color:#666; font-size:11px;';

            const weightInput = document.createElement('input');
            weightInput.type = 'number';
            weightInput.step = '0.5';
            weightInput.value = config.defaultWeight;
            weightInput.id = `weight_${config.id}`;
            weightInput.style.cssText = 'font-family: inherit; width:50px; padding:2px; border:1px solid #ccc; border-radius:4px; text-align:center; height:24px; direction:ltr;';

            weightDiv.appendChild(weightLabel);
            weightDiv.appendChild(weightInput);
            topRow.appendChild(label);
            topRow.appendChild(weightDiv);

            // Bottom row: Min and Max constraints
            const bottomRow = document.createElement('div');
            bottomRow.style.cssText = 'display:flex; align-items:center; gap:6px; flex-wrap:wrap; font-size:11px; color:#555;';

            const minLabel = document.createElement('label');
            minLabel.style.cssText = 'display:flex; align-items:center; gap:3px; margin:0;';
            minLabel.innerHTML = 'حداقل:';
            const minInput = document.createElement('input');
            minInput.type = 'number';
            minInput.id = `min_${config.id}`;
            minInput.value = (config.defaultMinCount ?? 0);
            minInput.style.cssText = 'font-family: inherit; width:50px; padding:1px 2px; border:1px solid #ccc; border-radius:4px; text-align:center; height:22px; direction:ltr;';

            minLabel.appendChild(minInput);

            const maxLabel = document.createElement('label');
            maxLabel.style.cssText = 'display:flex; align-items:center; gap:3px; margin:0;';
            maxLabel.innerHTML = 'حداکثر:';
            const maxInput = document.createElement('input');
            maxInput.type = 'number';
            maxInput.id = `max_${config.id}`;
            maxInput.value = (config.defaultMaxCount ?? 999999);
            maxInput.style.cssText = 'font-family: inherit; width:60px; padding:1px 2px; border:1px solid #ccc; border-radius:4px; text-align:center; height:22px; direction:ltr;';

            maxLabel.appendChild(maxInput);

            bottomRow.appendChild(minLabel);
            bottomRow.appendChild(maxLabel);

            row.appendChild(topRow);
            row.appendChild(bottomRow);
            scrollArea.appendChild(row);
        });

        uiWrapper.appendChild(scrollArea);

        // Generate controls for exclusion rules
        const exclusionPanel = document.createElement('div');
        exclusionPanel.className = 'exclusion-panel';
        exclusionPanel.style.cssText = 'margin-top:10px; padding-top:8px; border-top:1px solid #ddd; font-size:12px;';

        const exTitle = document.createElement('h6');
        exTitle.textContent = '🧹 قوانین حذف سلول‌ها';
        exTitle.style.cssText = 'font-size:12px; font-weight:bold; margin:0 0 6px 0; color:#333;';
        exclusionPanel.appendChild(exTitle);

        this.exclusionRulesConfig.forEach(rule => {
            const lbl = document.createElement('label');
            lbl.style.cssText = 'display:flex; align-items:center; gap:5px; margin-bottom:4px; cursor:pointer;';

            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.id = rule.id;
            if (rule.defaultActive) cb.checked = true;
            
            const span = document.createElement('span');
            span.textContent = rule.label;

            lbl.appendChild(cb);
            lbl.appendChild(span);
            exclusionPanel.appendChild(lbl);
        });

        uiWrapper.appendChild(exclusionPanel);

        // Inject UI into DOM
        if (this.elements.btnRun) {
            this.elements.container.insertBefore(uiWrapper, this.elements.btnRun);
        } else {
            this.elements.container.prepend(uiWrapper);
        }
    }

    // Retrieves configuration of checked layers from the UI
    getActiveConfigs() {
        const active = [];
        this.layerConfigs.forEach(config => {
            const checkbox = document.getElementById(`chk_${config.id}`);
            if (checkbox && checkbox.checked) {
                let currentWeight = parseFloat(document.getElementById(`weight_${config.id}`).value);
                if (isNaN(currentWeight)) currentWeight = config.defaultWeight;

                const minValue = parseInt(document.getElementById(`min_${config.id}`)?.value);
                const maxValue = parseInt(document.getElementById(`max_${config.id}`)?.value);

                const minCount = Number.isFinite(minValue) ? minValue : 0;
                const maxCount = Number.isFinite(maxValue) ? maxValue : Number.POSITIVE_INFINITY;

                active.push({ ...config, currentWeight, minCount, maxCount });
            }
        });
        return active;
    }

    // Retrieves checked exclusion rules from the UI
    getActiveExclusionRules() {
        const activeRules = [];
        this.exclusionRulesConfig.forEach(rule => {
            const cb = document.getElementById(rule.id);
            if (cb && cb.checked) {
                activeRules.push(rule);
            }
        });
        return activeRules;
    }

    // Toggles the analysis map layers and triggers data processing
    async toggleAnalysis() {
        const btn = this.elements.btnRun;
        
        // Hide analysis if already visible
        if (this.isVisible) {
            this.map.setLayoutProperty(this.layerId, 'visibility', 'none');
            this.map.setLayoutProperty(this.lineLayerId, 'visibility', 'none');
            if (this.map.getLayer(this.pointsLayerId)) {
                this.map.setLayoutProperty(this.pointsLayerId, 'visibility', 'none');
            }
            if (this.map.getLayer(this.heatmapLayerId)) {
                this.map.setLayoutProperty(this.heatmapLayerId, 'visibility', 'none');
            }
            
            this.isVisible = false;
            if (this.elements.filterBox) this.elements.filterBox.style.display = 'none';
            if (btn) {
                btn.textContent = "شروع پردازش و تحلیل مکانی";
                btn.className = 'state-idle';
            }
            return;
        }

        const activeConfigs = this.getActiveConfigs();
        if (activeConfigs.length === 0) {
            alert('لطفاً حداقل یک لایه را برای تحلیل انتخاب کنید.');
            return;
        }

        const activeExclusionRules = this.getActiveExclusionRules();

        // Ensure layers needed for exclusion rules are loaded, even if not selected for rendering
        const requiredForExclusionIds = new Set();
        activeExclusionRules.forEach(rule => {
            if (rule.layerId) requiredForExclusionIds.add(rule.layerId);
            if (rule.layers) rule.layers.forEach(id => requiredForExclusionIds.add(id));
        });

        const allNeededConfigs = [...activeConfigs];
        requiredForExclusionIds.forEach(id => {
            if (!allNeededConfigs.some(c => c.id === id)) {
                const cfg = this.layerConfigs.find(c => c.id === id);
                // Assign weight 0 to exclusion-only layers so they don't affect standard scoring
                if (cfg) allNeededConfigs.push({ ...cfg, isExclusionOnly: true, currentWeight: 0 }); 
            }
        });

        const gridSize = this.elements.gridSizeSelect ? parseInt(this.elements.gridSizeSelect.value) : 500;
        
        if (btn) {
            btn.disabled = true;
            btn.textContent = "در حال محاسبه و ترکیب داده‌ها...";
            btn.className = 'state-loading';
        }
        try {
            const dataLoaded = await this.fetchMissingData(allNeededConfigs);
            if (!dataLoaded) {
                alert('خطا در بارگذاری داده‌ها. لطفاً دوباره تلاش کنید.');
                return;
            }

            // Generate spatial grid and evaluate cells
            this.geoJsonData = this.generateCombinedGrid(activeConfigs, allNeededConfigs, gridSize, activeExclusionRules);
            this.addOrUpdateLayers();
            
            // Make layers visible
            this.map.setLayoutProperty(this.layerId, 'visibility', 'visible');
            this.map.setLayoutProperty(this.lineLayerId, 'visibility', 'visible');
            if (this.map.getLayer(this.pointsLayerId)) {
                this.map.setLayoutProperty(this.pointsLayerId, 'visibility', 'visible');
            }
            if (this.map.getLayer(this.heatmapLayerId)) {
                this.map.setLayoutProperty(this.heatmapLayerId, 'visibility', 'visible');
            }
            
            // Clear existing point selection and accumulated points
            if (this.map.getSource(this.pointsSourceId)) {
                this.map.getSource(this.pointsSourceId).setData({type: 'FeatureCollection', features: []});
                this.accumulatedPoints = [];
                this.selectedHexes.clear();
            }

            this.isVisible = true;
            if (this.elements.filterBox) this.elements.filterBox.style.display = 'block';
            if (btn) {
                btn.textContent = "پاک‌سازی نتایج تحلیل"; // جایگزین عبارت "مخفی کردن"
                btn.className = 'state-active';
            }

        } catch (err) {
            console.error('[OpportunityLayer] Error during analysis:', err);
            alert("خطا در پردازش: " + err.message);
        } finally {
            if (btn) {
                btn.disabled = false;
                if (!this.isVisible) {
                    btn.textContent = "شروع پردازش و تحلیل مکانی";
                    btn.className = 'state-idle';
                }
            }
        }
}
    // Fetches and caches CSV data or extracts from global App state for chain stores
    async fetchMissingData(activeConfigs) {
        const hasChainConfigs = activeConfigs.some(c => c.type === 'chain');
        
        if (hasChainConfigs) {
            if (!window.App) window.App = {};
            if (!window.App.data) window.App.data = {};
            
            if (!window.App.data.allStores) {
                try {
                    const chainData = await this.parseCSV('total.csv');
                    window.App.data.allStores = chainData;
                } catch (error) {
                    return false;
                }
            }
        }
        
        for (const config of activeConfigs) {
            if (!this.dataCache[config.id]) {
                if (config.type === 'csv') {
                    const data = await this.parseCSV(config.url);
                    this.dataCache[config.id] = this.extractCoordinates(data, config.name);
                } else if (config.type === 'chain') {
                    const chainData = window.App.data.allStores.filter(row => row.brand === config.brandKey);
                    this.dataCache[config.id] = this.extractCoordinates(chainData, config.name);
                }
            }
        }
        
        return true;
    }

    // Parses CSV using PapaParse
    parseCSV(url) {
        return new Promise((resolve, reject) => {
            Papa.parse(url, {
                download: true,
                header: true,
                skipEmptyLines: true,
                complete: (results) => resolve(results.data),
                error: () => reject(new Error(`Error reading file: ${url}`))
            });
        });
    }

    // Extracts and validates latitude/longitude from raw data
    extractCoordinates(data, sourceName) {
        if (!data || data.length === 0) return [];
        const validPoints = [];
        data.forEach(row => {
            const lat = parseFloat(row.lat);
            const lng = parseFloat(row.lng);
            if (!isNaN(lat) && !isNaN(lng)) {
                validPoints.push({ lat, lng });
            }
        });
        return validPoints;
    }

    // Core spatial logic: Creates hex grid, calculates scores via distance decay, applies exclusions
    generateCombinedGrid(activeConfigs, allNeededConfigs, gridSizeMeters, activeExclusionRules = []) {
        const bounds = this.map.getBounds();
        const margin = 0.02; 
        const viewBbox = [
            bounds.getWest() - margin, 
            bounds.getSouth() - margin, 
            bounds.getEast() + margin, 
            bounds.getNorth() + margin
        ];

        const bandwidth = gridSizeMeters / 1000; 
        const searchRadius = bandwidth * 2; 

        const positivePoints = []; 
        const competitorPoints = []; 
        const allPointsForCounts = {}; 

        // Categorize points into attractive vs repulsive based on their assigned weight
        allNeededConfigs.forEach(config => {
            const points = this.dataCache[config.id] || [];
            allPointsForCounts[config.id] = [];
            
            points.forEach(pt => {
                if (pt.lng >= viewBbox[0] && pt.lng <= viewBbox[2] && pt.lat >= viewBbox[1] && pt.lat <= viewBbox[3]) {
                    const feature = turf.point([pt.lng, pt.lat], { 
                        weight: config.currentWeight,
                        layerId: config.id,
                        layerName: config.name
                    });

                    if (config.currentWeight > 0) {
                        positivePoints.push(feature);
                    } else if (config.currentWeight < 0) {
                        competitorPoints.push(feature);
                    }
                    allPointsForCounts[config.id].push(feature);
                }
            });
        });

        if (positivePoints.length === 0) {
            alert("در این محدوده از نقشه، هیچ نقطه‌ای برای تحلیل یافت نشد. لطفاً جابجا شوید.");
            return { type: 'FeatureCollection', features: [] };
        }
        
        const allPositivePointsFC = turf.featureCollection(positivePoints);

        // Generate Turf Hexagonal Grid
        let hexGrid;
        try {
            hexGrid = turf.hexGrid(viewBbox, bandwidth, { units: 'kilometers' });
        } catch (e) {
            alert("خطا در ساخت شبکه. لطفاً کمی زوم کنید (محدوده بسیار بزرگ است).");
            return { type: 'FeatureCollection', features: [] };
        }

        if (hexGrid.features.length > 15000) {
            alert(`تعداد سلول‌ها بسیار زیاد است (${hexGrid.features.length}). برای جلوگیری از هنگ کردن سیستم، لطفاً روی نقشه زوم کنید تا محدوده کوچکتر شود.`);
            return { type: 'FeatureCollection', features: [] };
        }

        // Pre-processing: Apply Daily Market exclusion rule to cull grid cells early and optimize calculation
        const excludeDailyMarketRule = activeExclusionRules.find(r => r.id === 'exclude_dailymarket');
        if (excludeDailyMarketRule) {
            const dmPoints = this.dataCache['dailymarket'];
            if (dmPoints && dmPoints.length > 0) {
                const dmFC = turf.featureCollection(dmPoints.map(pt => turf.point([pt.lng, pt.lat])));
                
                hexGrid.features = hexGrid.features.filter(hex => {
                    const center = turf.center(hex);
                    const nearest = turf.nearestPoint(center, dmFC);
                    const dist = turf.distance(center, nearest, { units: 'kilometers' });
                    // Exclude cells if distance to Daily Market is less than 0.25 km
                    // Math condition: $$ dist \ge 0.25 $$
                    return dist >= 0.25; 
                });
            }
        }

        // Calculate scores and entity counts for each remaining cell
        hexGrid.features.forEach(hex => {
            const center = turf.center(hex).geometry.coordinates;
            let raw_score = 0;
            let counts = {};

            // Linear Distance Decay calculation for positive influences
            // Formula used: $$ influence = weight \times \left(1 - \frac{dist}{searchRadius}\right) $$
            turf.featureEach(allPositivePointsFC, (point) => {
                const dist = turf.distance(center, point, { units: 'kilometers' });
                if (dist <= searchRadius) {
                    const influence = point.properties.weight * (1 - (dist / searchRadius));
                    raw_score += influence;
                }
            });

            // Evaluate competitors/repulsive influences strictly within the cell radius
            competitorPoints.forEach(competitor => {
                const dist = turf.distance(center, competitor, { units: 'kilometers' });
                if (dist < (bandwidth / 2)) {
                    raw_score += competitor.properties.weight; 
                }
            });
            
            // Count exact occurrences of each layer's points inside the polygon
            for (const config of allNeededConfigs) {
                const pointsInLayer = allPointsForCounts[config.id];
                if (pointsInLayer && pointsInLayer.length > 0) {
                     const pointsInside = turf.pointsWithinPolygon(turf.featureCollection(pointsInLayer), hex);
                     counts[config.id] = pointsInside.features.length;
                } else {
                     counts[config.id] = 0;
                }
            }

            hex.properties = { raw_score, counts };
        });

        // Filter cells based on user-defined min/max thresholds and remaining exclusion rules
        let validCells = hexGrid.features.filter(hex => {
            let isValid = true;
            const counts = hex.properties.counts;

            for (const cfg of activeConfigs) {
                const cnt = counts[cfg.id] || 0;
                if (cnt < cfg.minCount || cnt > cfg.maxCount) {
                    isValid = false; break;
                }
            }
            if (!isValid) return false;

            if (this.shouldExcludeCell(counts, activeExclusionRules)) {
                return false;
            }

            return true;
        });

        if (validCells.length === 0) {
            return { type: 'FeatureCollection', features: [] };
        }

        // Normalize raw scores to 0-100 index mapping
        let maxScore = -Infinity;
        let minScore = Infinity;
        validCells.forEach(hex => {
            if (hex.properties.raw_score > maxScore) maxScore = hex.properties.raw_score;
            if (hex.properties.raw_score < minScore) minScore = hex.properties.raw_score;
        });

        let scoreRange = maxScore - minScore;
        if (scoreRange === 0) scoreRange = 1;

        const finalFeatures = validCells.map(hex => {
            const { raw_score, counts } = hex.properties;
            
            // Normalization formula: $$ normalizedScore = \text{round}\left( \frac{raw\_score - minScore}{scoreRange} \times 100 \right) $$
            const normalizedScore = Math.round(((raw_score - minScore) / scoreRange) * 100);
            
            let category = 'Low';
            if (normalizedScore > 66) category = 'High';
            else if (normalizedScore > 33) category = 'Medium';

            const popupCounts = {};
            for (const [layerId, count] of Object.entries(counts)) {
                 const layerCfg = this.layerConfigs.find(c => c.id === layerId);
                 if (layerCfg && count > 0) { 
                     popupCounts[layerCfg.name] = count;
                 }
            }

            hex.properties = {
                raw_score: raw_score.toFixed(2),
                normalized_score: normalizedScore,
                class: category,
                counts_json: JSON.stringify(popupCounts)
            };
            return hex;
        });

        return { type: 'FeatureCollection', features: finalFeatures };
    }

    // Applies remaining exclusion rules based on entity counts within a cell
    shouldExcludeCell(counts, activeExclusionRules) {
        if (!activeExclusionRules || activeExclusionRules.length === 0) return false;

        for (const rule of activeExclusionRules) {
            // Rule: Exclude if a specific layer is present
            if (rule.type === 'layerExists') {
                const c = counts[rule.layerId] || 0;
                if (c > 0) return true;
            }

            // Rule: Exclude if cumulative count across multiple layers exceeds a threshold
            if (rule.type === 'highCompetition') {
                let sum = 0;
                rule.layers.forEach(layerId => {
                    sum += (counts[layerId] || 0);
                });
                if (sum > rule.threshold) return true;
            }
        }
        return false;
    }

    // Sets up or updates Mapbox sources and layers
    addOrUpdateLayers() {
        if (this.map.getSource(this.sourceId)) {
            this.map.getSource(this.sourceId).setData(this.geoJsonData);
        } else {
            // Hexagon fill layer
            this.map.addSource(this.sourceId, { type: 'geojson', data: this.geoJsonData });
            this.map.addLayer({
                id: this.layerId,
                type: 'fill',
                source: this.sourceId,
                paint: {
                    'fill-color': [
                        'interpolate', ['linear'], ['get', 'normalized_score'],
                        0,   '#81C784',
                        33,  '#FFF176',
                        66,  '#FFB74D',
                        100, '#E57373'
                    ],
                    'fill-opacity': 0.6,
                    'fill-outline-color': 'rgba(0,0,0,0.1)'
                }
            }, 'waterway-label');

            // Hexagon outline layer
            this.map.addLayer({
                id: this.lineLayerId,
                type: 'line',
                source: this.sourceId,
                paint: { 'line-color': 'rgba(0,0,0,0.2)', 'line-width': 1 }
            });

            this.map.on('click', this.layerId, (e) => this.showPopup(e));
            this.map.on('mouseenter', this.layerId, () => this.map.getCanvas().style.cursor = 'crosshair');
            this.map.on('mouseleave', this.layerId, () => this.map.getCanvas().style.cursor = '');
        }

        // Setup points source and visual layers (heatmap + dots)
        if (!this.map.getSource(this.pointsSourceId)) {
            this.map.addSource(this.pointsSourceId, {
                type: 'geojson',
                data: { type: 'FeatureCollection', features: [] }
            });

            // Meteorological-style heatmap overlay for density visualization
            this.map.addLayer({
                id: this.heatmapLayerId,
                type: 'heatmap',
                source: this.pointsSourceId,
                paint: {
                    'heatmap-intensity': [
                        'interpolate', ['linear'], ['zoom'],
                        11, 1,
                        15, 3
                    ],
                    'heatmap-color': [
                        'interpolate', ['linear'], ['heatmap-density'],
                        0, 'rgba(33,102,172,0)', // Transparent
                        0.2, 'rgb(103,169,207)', // Light blue
                        0.4, 'rgb(161,217,155)', // Light green
                        0.6, 'rgb(254,224,139)', // Yellow
                        0.8, 'rgb(253,141,60)',  // Orange
                        1, 'rgb(215,48,39)'      // Red (High density)
                    ],
                    'heatmap-radius': [
                        'interpolate', ['linear'], ['zoom'],
                        11, 15,
                        15, 40
                    ],
                    'heatmap-opacity': 0.7
                }
            });

            // Point overlay rendered on top of heatmap (Reduced size and opacity)
            this.map.addLayer({
                id: this.pointsLayerId,
                type: 'circle',
                source: this.pointsSourceId,
                 paint: {
                    'circle-radius': 2.5,          
                    'circle-opacity': 0.5,        
                    'circle-color': ['get', 'color'],
                    'circle-stroke-width': 1,     
                    'circle-stroke-color': '#ffffff',
                    'circle-stroke-opacity': 0.5  
                }
            });
        }
    }

    // Filters the visible hexagons by performance class (Low/Medium/High)
    applyFilter(type) {
        if (!this.map.getLayer(this.layerId)) return;
        let filterCondition = type === 'all' ? null : ['==', 'class', type];
        this.map.setFilter(this.layerId, filterCondition);
        this.map.setFilter(this.lineLayerId, filterCondition);
    }

    // Handles click events on hex grid cells: renders popup and populates underlying points
    showPopup(e) {
        const props = e.features[0].properties;
        const coordinates = e.lngLat;
        const counts = JSON.parse(props.counts_json);
        
        const clickedHexFeature = e.features[0]; 
        const activeConfigs = this.getActiveConfigs();
        const innerPoints = [];
        
        // Detect Multi-select (Ctrl or Command key pressed)
        const originalEvent = e.originalEvent;
        const isMultiSelect = originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey);

        // Isolate CSV points precisely located within the clicked hex polygon for the heatmap rendering
        activeConfigs.forEach(config => {
            if (config.type === 'csv' && this.dataCache[config.id]) {
                const points = this.dataCache[config.id];
                points.forEach(pt => {
                    const ptFeature = turf.point([pt.lng, pt.lat]);
                    if (turf.booleanPointInPolygon(ptFeature, clickedHexFeature)) {
                        ptFeature.properties = { color: config.color, name: config.name };
                        innerPoints.push(ptFeature);
                    }
                });
            }
        });
        const flatCoords = clickedHexFeature.geometry.coordinates.flat(Infinity);
        const hexId = `${flatCoords[0]},${flatCoords[1]}`;
        
        
        // Accumulate points if Ctrl is pressed, otherwise replace them
        if (isMultiSelect) {
            if (this.selectedHexes.has(hexId)) {
                this.selectedHexes.delete(hexId);
            } else {
                this.selectedHexes.set(hexId, innerPoints);
            }
        } else {
            this.selectedHexes.clear();
            this.selectedHexes.set(hexId, innerPoints);
        }
        this.accumulatedPoints = Array.from(this.selectedHexes.values()).flat();

        if (this.map.getSource(this.pointsSourceId)) {
            this.map.getSource(this.pointsSourceId).setData({
                type: 'FeatureCollection',
                features: this.accumulatedPoints 
            });
        }

        // Build popup HTML structure
        let itemsHtml = '';
        for (const [name, count] of Object.entries(counts)) {
            const conf = this.layerConfigs.find(c => c.name === name);
            const dotColor = (conf && conf.type === 'csv')
                ? `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${conf.color};margin-left:4px;"></span>`
                : ' ';
            
            itemsHtml += `
                <div style="display:flex; justify-content:space-between; padding:2px 0; border-bottom:1px dashed #eee;">
                    <span>${dotColor}${name}:</span>
                    <b>${count}</b>
                </div>
            `;
        }

        const html = `
            <div style="font-family: 'YekanBakh', sans-serif; text-align:right; direction:rtl; padding:5px; min-width:190px;">
                <h4 style="margin:0 0 8px 0; color:#333; font-size:14px; border-bottom:1px solid #ddd; padding-bottom:5px;">آمار سلول انتخابی</h4>
                
                <div style="font-size:12px; margin-bottom:10px; background:#f9f9f9; padding:5px; border-radius:4px;">
                    ${itemsHtml}
                </div>

                <div style="font-size:12px; margin-bottom:5px;">
                     مجموع امتیاز وزنی: 
                    <b style="color:${parseFloat(props.raw_score) < 0 ? 'red' : '#E65100'}">${props.raw_score}</b>
                </div>
                <div style="font-size:12px; margin-bottom:8px;">
                     شاخص: <b>${props.normalized_score}</b> از 100
                </div>
            </div>
        `;

        new nmp_mapboxgl.Popup({ closeButton: true, maxWidth: '250px' }) // Assuming nmp_mapboxgl is mapped to mapboxgl or equivalent
            .setLngLat(coordinates)
            .setHTML(html)
            .addTo(this.map);
    }
}
